From ad756c73a07cb455d3845c0da4cd38bab6af8f38 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 8 Jun 2022 10:18:05 -0400 Subject: [PATCH 001/723] internal/lsp: initial test set up for inlay hints Set up the tests for inlay hints. We test inlay hints by converting them to text edits and verifying the output is as we expected it. This change does not yet deal with making sure the server settings are correct. Change-Id: I136f971a87bf9936fd44047d45fe0a3f03c9164e Reviewed-on: https://go-review.googlesource.com/c/tools/+/411095 Run-TryBot: Suzy Mueller gopls-CI: kokoro Reviewed-by: Jamal Carvalho --- internal/lsp/cmd/test/cmdtest.go | 4 ++ internal/lsp/lsp_test.go | 42 +++++++++++++++++++++ internal/lsp/source/source_test.go | 4 ++ internal/lsp/testdata/inlayHint/a.go | 9 +++++ internal/lsp/testdata/inlayHint/a.go.golden | 11 ++++++ internal/lsp/tests/tests.go | 19 ++++++++++ 6 files changed, 89 insertions(+) create mode 100644 internal/lsp/testdata/inlayHint/a.go create mode 100644 internal/lsp/testdata/inlayHint/a.go.golden diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go index 312f7b8b435..ff0461b333f 100644 --- a/internal/lsp/cmd/test/cmdtest.go +++ b/internal/lsp/cmd/test/cmdtest.go @@ -113,6 +113,10 @@ func (r *runner) Hover(t *testing.T, spn span.Span, info string) { //TODO: hovering not supported on command line } +func (r *runner) InlayHints(t *testing.T, spn span.Span) { + // TODO: inlayHints not supported on command line +} + func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) { rStdout, wStdout, err := os.Pipe() if err != nil { diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index ee364b8b034..e097100c2ff 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -932,6 +932,48 @@ func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { } } +func (r *runner) InlayHints(t *testing.T, spn span.Span) { + uri := spn.URI() + filename := uri.Filename() + + hints, err := r.server.InlayHint(r.ctx, &protocol.InlayHintParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(uri), + }, + // TODO: add ViewPort + }) + if err != nil { + t.Fatal(err) + } + + // Map inlay hints to text edits. + edits := make([]protocol.TextEdit, len(hints)) + for i, hint := range hints { + edits[i] = protocol.TextEdit{ + Range: protocol.Range{Start: *hint.Position, End: *hint.Position}, + NewText: fmt.Sprintf("<%s>", hint.Label[0].Value), + } + } + + m, err := r.data.Mapper(uri) + if err != nil { + t.Fatal(err) + } + sedits, err := source.FromProtocolEdits(m, edits) + if err != nil { + t.Error(err) + } + got := diff.ApplyEdits(string(m.Content), sedits) + + withinlayHints := string(r.data.Golden("inlayHint", filename, func() ([]byte, error) { + return []byte(got), nil + })) + + if withinlayHints != got { + t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, withinlayHints, got) + } +} + func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { tag := fmt.Sprintf("%s-rename", newText) diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index 426bffc97b5..9218f9ddc1a 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -685,6 +685,10 @@ func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) { } } +func (r *runner) InlayHints(t *testing.T, src span.Span) { + // TODO(golang/go#53315): add source test +} + func (r *runner) Hover(t *testing.T, src span.Span, text string) { ctx := r.ctx _, srcRng, err := spanToRange(r.data, src) diff --git a/internal/lsp/testdata/inlayHint/a.go b/internal/lsp/testdata/inlayHint/a.go new file mode 100644 index 00000000000..90ef7c41d1d --- /dev/null +++ b/internal/lsp/testdata/inlayHint/a.go @@ -0,0 +1,9 @@ +package inlayHint //@inlayHint("package") + +func hello(name string) string { + return "Hello " + name +} + +func helloWorld() string { + return hello("World") +} diff --git a/internal/lsp/testdata/inlayHint/a.go.golden b/internal/lsp/testdata/inlayHint/a.go.golden new file mode 100644 index 00000000000..e4e6cc0c0cc --- /dev/null +++ b/internal/lsp/testdata/inlayHint/a.go.golden @@ -0,0 +1,11 @@ +-- inlayHint -- +package inlayHint //@inlayHint("package") + +func hello(name string) string { + return "Hello " + name +} + +func helloWorld() string { + return hello("World") +} + diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index 8265cf2e9b1..81a5d399029 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -81,6 +81,7 @@ type PrepareRenames map[span.Span]*source.PrepareItem type Symbols map[span.URI][]protocol.DocumentSymbol type SymbolsChildren map[string][]protocol.DocumentSymbol type SymbolInformation map[span.Span]protocol.SymbolInformation +type InlayHints []span.Span type WorkspaceSymbols map[WorkspaceSymbolsTestType]map[span.URI][]string type Signatures map[span.Span]*protocol.SignatureHelp type Links map[span.URI][]Link @@ -113,6 +114,7 @@ type Data struct { Highlights Highlights References References Renames Renames + InlayHints InlayHints PrepareRenames PrepareRenames Symbols Symbols symbolsChildren SymbolsChildren @@ -156,6 +158,7 @@ type Tests interface { Definition(*testing.T, span.Span, Definition) Implementation(*testing.T, span.Span, []span.Span) Highlight(*testing.T, span.Span, []span.Span) + InlayHints(*testing.T, span.Span) References(*testing.T, span.Span, []span.Span) Rename(*testing.T, span.Span, string) PrepareRename(*testing.T, span.Span, *source.PrepareItem) @@ -466,6 +469,7 @@ func load(t testing.TB, mode string, dir string) *Data { "hoverdef": datum.collectHoverDefinitions, "hover": datum.collectHovers, "highlight": datum.collectHighlights, + "inlayHint": datum.collectInlayHints, "refs": datum.collectReferences, "rename": datum.collectRenames, "prepare": datum.collectPrepareRenames, @@ -782,6 +786,17 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) + t.Run("InlayHints", func(t *testing.T) { + t.Skip("Inlay Hints not yet implemented") + t.Helper() + for _, src := range data.InlayHints { + t.Run(SpanName(src), func(t *testing.T) { + t.Helper() + tests.InlayHints(t, src) + }) + } + }) + t.Run("References", func(t *testing.T) { t.Helper() for src, itemList := range data.References { @@ -1292,6 +1307,10 @@ func (data *Data) collectHighlights(src span.Span, expected []span.Span) { data.Highlights[src] = append(data.Highlights[src], expected...) } +func (data *Data) collectInlayHints(src span.Span) { + data.InlayHints = append(data.InlayHints, src) +} + func (data *Data) collectReferences(src span.Span, expected []span.Span) { data.References[src] = expected } From 697795d6a801a72ad67b8c4fab6fdd74bc9150d1 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Sun, 8 Aug 2021 20:12:22 -0400 Subject: [PATCH 002/723] internal/lsp/regtest: don't run the connection on the test context When test awaiting fails, we often fail to shut down the server because the pipe is closed. Fix this by using a detached context for running the connection. Also clean up some unnecessary context arguments. Change-Id: I535c1cc1606e44df5f8e2177c92293d57836f992 Reviewed-on: https://go-review.googlesource.com/c/tools/+/340850 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan --- gopls/internal/regtest/misc/shared_test.go | 3 ++- internal/jsonrpc2/servertest/servertest.go | 2 +- internal/jsonrpc2/servertest/servertest_test.go | 2 +- internal/lsp/lsprpc/lsprpc_test.go | 7 +++---- internal/lsp/regtest/env.go | 14 ++++++++++---- internal/lsp/regtest/runner.go | 17 +++++++++-------- 6 files changed, 26 insertions(+), 19 deletions(-) diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index 6861743ff42..a6b0cd87ef1 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -30,7 +30,8 @@ func runShared(t *testing.T, testFunc func(env1 *Env, env2 *Env)) { WithOptions(Modes(modes)).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { // Create a second test session connected to the same workspace and server // as the first. - env2 := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config, true) + env2, cleanup := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config, true) + defer cleanup() env2.Await(InitialWorkspaceLoad) testFunc(env1, env2) }) diff --git a/internal/jsonrpc2/servertest/servertest.go b/internal/jsonrpc2/servertest/servertest.go index 392e084a9ad..b879ebdf181 100644 --- a/internal/jsonrpc2/servertest/servertest.go +++ b/internal/jsonrpc2/servertest/servertest.go @@ -68,7 +68,7 @@ type PipeServer struct { } // NewPipeServer returns a test server that can be connected to via io.Pipes. -func NewPipeServer(ctx context.Context, server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *PipeServer { +func NewPipeServer(server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *PipeServer { if framer == nil { framer = jsonrpc2.NewRawStream } diff --git a/internal/jsonrpc2/servertest/servertest_test.go b/internal/jsonrpc2/servertest/servertest_test.go index 38fa21a24d9..1780d4f9147 100644 --- a/internal/jsonrpc2/servertest/servertest_test.go +++ b/internal/jsonrpc2/servertest/servertest_test.go @@ -26,7 +26,7 @@ func TestTestServer(t *testing.T) { server := jsonrpc2.HandlerServer(fakeHandler) tcpTS := NewTCPServer(ctx, server, nil) defer tcpTS.Close() - pipeTS := NewPipeServer(ctx, server, nil) + pipeTS := NewPipeServer(server, nil) defer pipeTS.Close() tests := []struct { diff --git a/internal/lsp/lsprpc/lsprpc_test.go b/internal/lsp/lsprpc/lsprpc_test.go index 795c887e4b4..cde641c920b 100644 --- a/internal/lsp/lsprpc/lsprpc_test.go +++ b/internal/lsp/lsprpc/lsprpc_test.go @@ -60,7 +60,7 @@ func TestClientLogging(t *testing.T) { ctx = debug.WithInstance(ctx, "", "") ss := NewStreamServer(cache.New(nil), false) ss.serverForTest = server - ts := servertest.NewPipeServer(ctx, ss, nil) + ts := servertest.NewPipeServer(ss, nil) defer checkClose(t, ts.Close) cc := ts.Connect(ctx) cc.Go(ctx, protocol.ClientHandler(client, jsonrpc2.MethodNotFound)) @@ -125,12 +125,11 @@ func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (dire ss.serverForTest = s tsDirect := servertest.NewTCPServer(serveCtx, ss, nil) - forwarderCtx := debug.WithInstance(ctx, "", "") forwarder, err := NewForwarder("tcp;"+tsDirect.Addr, nil) if err != nil { t.Fatal(err) } - tsForwarded := servertest.NewPipeServer(forwarderCtx, forwarder, nil) + tsForwarded := servertest.NewPipeServer(forwarder, nil) return tsDirect, tsForwarded, func() { checkClose(t, tsDirect.Close) checkClose(t, tsForwarded.Close) @@ -225,7 +224,7 @@ func TestDebugInfoLifecycle(t *testing.T) { if err != nil { t.Fatal(err) } - tsForwarder := servertest.NewPipeServer(clientCtx, forwarder, nil) + tsForwarder := servertest.NewPipeServer(forwarder, nil) conn1 := tsForwarder.Connect(clientCtx) ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, conn1, fake.ClientHooks{}) diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go index f095c38f285..a37cbf66611 100644 --- a/internal/lsp/regtest/env.go +++ b/internal/lsp/regtest/env.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/xcontext" ) // Env holds an initialized fake Editor, Workspace, and Server, which may be @@ -109,9 +110,14 @@ type condition struct { // NewEnv creates a new test environment using the given scratch environment // and gopls server. -func NewEnv(ctx context.Context, tb testing.TB, sandbox *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig, withHooks bool) *Env { +// +// The resulting func must be called to close the jsonrpc2 connection. +func NewEnv(ctx context.Context, tb testing.TB, sandbox *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig, withHooks bool) (_ *Env, cleanup func()) { tb.Helper() - conn := ts.Connect(ctx) + + bgCtx, cleanupConn := context.WithCancel(xcontext.Detach(ctx)) + conn := ts.Connect(bgCtx) + env := &Env{ T: tb, Ctx: ctx, @@ -138,12 +144,12 @@ func NewEnv(ctx context.Context, tb testing.TB, sandbox *fake.Sandbox, ts server OnUnregistration: env.onUnregistration, } } - editor, err := fake.NewEditor(sandbox, editorConfig).Connect(ctx, conn, hooks) + editor, err := fake.NewEditor(sandbox, editorConfig).Connect(bgCtx, conn, hooks) if err != nil { tb.Fatal(err) } env.Editor = editor - return env + return env, cleanupConn } func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error { diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go index 3cfeb772a19..bebec53c527 100644 --- a/internal/lsp/regtest/runner.go +++ b/internal/lsp/regtest/runner.go @@ -234,7 +234,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio tests := []struct { name string mode Mode - getServer func(context.Context, *testing.T, func(*source.Options)) jsonrpc2.StreamServer + getServer func(*testing.T, func(*source.Options)) jsonrpc2.StreamServer }{ {"singleton", Singleton, singletonServer}, {"forwarded", Forwarded, r.forwardedServer}, @@ -301,14 +301,15 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio // better solution to ensure that all Go processes started by gopls have // exited before we clean up. r.AddCloser(sandbox) - ss := tc.getServer(ctx, t, config.optionsHook) + ss := tc.getServer(t, config.optionsHook) framer := jsonrpc2.NewRawStream ls := &loggingFramer{} if !config.skipLogs { framer = ls.framer(jsonrpc2.NewRawStream) } - ts := servertest.NewPipeServer(ctx, ss, framer) - env := NewEnv(ctx, t, sandbox, ts, config.editor, !config.skipHooks) + ts := servertest.NewPipeServer(ss, framer) + env, cleanup := NewEnv(ctx, t, sandbox, ts, config.editor, !config.skipHooks) + defer cleanup() defer func() { if t.Failed() && r.PrintGoroutinesOnFailure { pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) @@ -406,11 +407,11 @@ func (s *loggingFramer) printBuffers(testname string, w io.Writer) { fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname) } -func singletonServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { +func singletonServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { return lsprpc.NewStreamServer(cache.New(optsHook), false) } -func experimentalServer(_ context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { +func experimentalServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { options := func(o *source.Options) { optsHook(o) o.EnableAllExperiments() @@ -421,7 +422,7 @@ func experimentalServer(_ context.Context, t *testing.T, optsHook func(*source.O return lsprpc.NewStreamServer(cache.New(options), false) } -func (r *Runner) forwardedServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { +func (r *Runner) forwardedServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { ts := r.getTestServer(optsHook) return newForwarder("tcp", ts.Addr) } @@ -440,7 +441,7 @@ func (r *Runner) getTestServer(optsHook func(*source.Options)) *servertest.TCPSe return r.ts } -func (r *Runner) separateProcessServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { +func (r *Runner) separateProcessServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { // TODO(rfindley): can we use the autostart behavior here, instead of // pre-starting the remote? socket := r.getRemoteSocket(t) From 9651276d64be221532f972dbbbd8e6186c784fde Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 10 Jun 2022 13:34:52 -0400 Subject: [PATCH 003/723] internal/lsp/cache: optimize Snapshot.clone This change replaces the single large map used for snapshot.goFiles by a map of 256 stripes, each of which becomes immutable once shared. This optimizes the common case in which the copy is nearly identical to the original. We still need to visit each map entry to see whether it needs to be deleted (which is rare) and to inherit the handle in the usual case. This is now done concurrently. Also, share the (logically immutable) []PackageIDs slices across old and new snapshots. This was worth 5% of CPU and 1/3 of allocations (all small). Benchmark on darwin/arm64 shows a 29% reduction for DidChange. $ go test -v ./gopls/internal/regtest/bench -run=TestBenchmarkDidChange -didchange_dir=$HOME/w/kubernetes -didchange_file=pkg/util/hash/hash.go Before: BenchmarkStatistics 100 22955469 ns/op 11308095 B/op 47412 allocs/op BenchmarkStatistics 100 23454630 ns/op 11226742 B/op 46882 allocs/op BenchmarkStatistics 100 23618532 ns/op 11258619 B/op 47068 allocs/op After goFilesMap: BenchmarkStatistics 100 16643972 ns/op 8770787 B/op 46238 allocs/op BenchmarkStatistics 100 17805864 ns/op 8862926 B/op 46762 allocs/op BenchmarkStatistics 100 18618255 ns/op 9308864 B/op 49776 allocs/op After goFilesMap and ids sharing: BenchmarkStatistics 100 16703623 ns/op 8772626 B/op 33812 allocs/op BenchmarkStatistics 100 16927378 ns/op 8529491 B/op 32328 allocs/op BenchmarkStatistics 100 16632762 ns/op 8557533 B/op 32497 allocs/op Also: - Add comments documenting findings of profiling. - preallocate slice for knownSubdirs. - remove unwanted loop over slice in Generation.Inherit Updates golang/go#45686 Change-Id: Id953699191b8404cf36ba3a7ab9cd78b1d19c0a2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/410176 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Alan Donovan --- internal/lsp/cache/cache.go | 8 +- internal/lsp/cache/session.go | 2 +- internal/lsp/cache/snapshot.go | 184 +++++++++++++++++++++++++++---- internal/lsp/source/view.go | 2 +- internal/memoize/memoize.go | 21 ++-- internal/memoize/memoize_test.go | 3 +- internal/span/uri.go | 8 ++ 7 files changed, 191 insertions(+), 37 deletions(-) diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index ac670b573e5..f5796dfefa2 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -183,8 +183,14 @@ func (h *fileHandle) Read() ([]byte, error) { return h.bytes, h.err } +// hashContents returns a string of hex digits denoting the hash of contents. +// +// TODO(adonovan): opt: use [32]byte array as a value more widely and convert +// to hex digits on demand (rare). The array is larger when it appears as a +// struct field (32B vs 16B) but smaller overall (string data is 64B), has +// better locality, and is more efficiently hashed by runtime maps. func hashContents(contents []byte) string { - return fmt.Sprintf("%x", sha256.Sum256(contents)) + return fmt.Sprintf("%64x", sha256.Sum256(contents)) } var cacheIndex, sessionIndex, viewIndex int64 diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index e018cb33bd8..9da5c1e69f9 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -234,7 +234,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, packages: make(map[packageKey]*packageHandle), meta: NewMetadataGraph(), files: make(map[span.URI]source.VersionedFileHandle), - goFiles: make(map[parseKey]*parseGoHandle), + goFiles: newGoFileMap(), symbols: make(map[span.URI]*symbolHandle), actions: make(map[actionKey]*actionHandle), workspacePackages: make(map[PackageID]PackagePath), diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index a219935aa66..0d3c869cd2e 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -76,7 +76,7 @@ type snapshot struct { files map[span.URI]source.VersionedFileHandle // goFiles maps a parseKey to its parseGoHandle. - goFiles map[parseKey]*parseGoHandle + goFiles *goFileMap // TODO(rfindley): consider merging this with files to reduce burden on clone. symbols map[span.URI]*symbolHandle @@ -663,16 +663,17 @@ func (s *snapshot) transitiveReverseDependencies(id PackageID, ids map[PackageID func (s *snapshot) getGoFile(key parseKey) *parseGoHandle { s.mu.Lock() defer s.mu.Unlock() - return s.goFiles[key] + return s.goFiles.get(key) } func (s *snapshot) addGoFile(key parseKey, pgh *parseGoHandle) *parseGoHandle { s.mu.Lock() defer s.mu.Unlock() - if existing, ok := s.goFiles[key]; ok { - return existing + + if prev := s.goFiles.get(key); prev != nil { + return prev } - s.goFiles[key] = pgh + s.goFiles.set(key, pgh) return pgh } @@ -811,6 +812,8 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru patterns := map[string]struct{}{ fmt.Sprintf("**/*.{%s}", extensions): {}, } + + // Add a pattern for each Go module in the workspace that is not within the view. dirs := s.workspace.dirs(ctx, s) for _, dir := range dirs { dirName := dir.Filename() @@ -830,14 +833,19 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru // contain Go code (golang/go#42348). To handle this, explicitly watch all // of the directories in the workspace. We find them by adding the // directories of every file in the snapshot's workspace directories. - var dirNames []string - for _, uri := range s.getKnownSubdirs(dirs) { - dirNames = append(dirNames, uri.Filename()) - } - sort.Strings(dirNames) - if len(dirNames) > 0 { + // There may be thousands. + knownSubdirs := s.getKnownSubdirs(dirs) + if n := len(knownSubdirs); n > 0 { + dirNames := make([]string, 0, n) + for _, uri := range knownSubdirs { + dirNames = append(dirNames, uri.Filename()) + } + sort.Strings(dirNames) + // The double allocation of Sprintf(Join()) accounts for 8% + // of DidChange, but specializing doesn't appear to help. :( patterns[fmt.Sprintf("{%s}", strings.Join(dirNames, ","))] = struct{}{} } + return patterns } @@ -874,7 +882,7 @@ func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) []span.URI { } s.unprocessedSubdirChanges = nil - var result []span.URI + result := make([]span.URI, 0, len(s.knownSubdirs)) for uri := range s.knownSubdirs { result = append(result, uri) } @@ -1719,7 +1727,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC packages: make(map[packageKey]*packageHandle, len(s.packages)), actions: make(map[actionKey]*actionHandle, len(s.actions)), files: make(map[span.URI]source.VersionedFileHandle, len(s.files)), - goFiles: make(map[parseKey]*parseGoHandle, len(s.goFiles)), + goFiles: s.goFiles.clone(), symbols: make(map[span.URI]*symbolHandle, len(s.symbols)), workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)), unloadableFiles: make(map[span.URI]struct{}, len(s.unloadableFiles)), @@ -1764,12 +1772,27 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC result.parseWorkHandles[k] = v } - for k, v := range s.goFiles { - if _, ok := changes[k.file.URI]; ok { - continue + // Copy the handles of all Go source files. + // There may be tens of thousands of files, + // but changes are typically few, so we + // use a striped map optimized for this case + // and visit its stripes in parallel. + var ( + toDeleteMu sync.Mutex + toDelete []parseKey + ) + s.goFiles.forEachConcurrent(func(k parseKey, v *parseGoHandle) { + if changes[k.file.URI] == nil { + // no change (common case) + newGen.Inherit(v.handle) + } else { + toDeleteMu.Lock() + toDelete = append(toDelete, k) + toDeleteMu.Unlock() } - newGen.Inherit(v.handle) - result.goFiles[k] = v + }) + for _, k := range toDelete { + result.goFiles.delete(k) } // Copy all of the go.mod-related handles. They may be invalidated later, @@ -1975,21 +1998,34 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC deleteInvalidMetadata := forceReloadMetadata || workspaceModeChanged idsInSnapshot := map[PackageID]bool{} // track all known IDs for uri, ids := range s.meta.ids { - var resultIDs []PackageID - for _, id := range ids { + // Optimization: ids slices are typically numerous, short (<3), + // and rarely modified by this loop, so don't allocate copies + // until necessary. + var resultIDs []PackageID // nil implies equal to ids[:i:i] + for i, id := range ids { if skipID[id] || deleteInvalidMetadata && idsToInvalidate[id] { + resultIDs = ids[:i:i] // unshare continue } // The ID is not reachable from any workspace package, so it should // be deleted. if !reachableID[id] { + resultIDs = ids[:i:i] // unshare continue } idsInSnapshot[id] = true - resultIDs = append(resultIDs, id) + if resultIDs != nil { + resultIDs = append(resultIDs, id) + } + } + if resultIDs == nil { + resultIDs = ids } result.meta.ids[uri] = resultIDs } + // TODO(adonovan): opt: represent PackageID as an index into a process-global + // dup-free list of all package names ever seen, then use a bitmap instead of + // a hash table for "PackageSet" (e.g. idsInSnapshot). // Copy the package metadata. We only need to invalidate packages directly // containing the affected file, and only if it changed in a relevant way. @@ -2259,7 +2295,7 @@ func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH // lockedSnapshot must be locked. func peekOrParse(ctx context.Context, lockedSnapshot *snapshot, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { key := parseKey{file: fh.FileIdentity(), mode: mode} - if pgh := lockedSnapshot.goFiles[key]; pgh != nil { + if pgh := lockedSnapshot.goFiles.get(key); pgh != nil { cached := pgh.handle.Cached(lockedSnapshot.generation) if cached != nil { cached := cached.(*parseGoData) @@ -2547,3 +2583,107 @@ func readGoSum(dst map[module.Version][]string, file string, data []byte) error } return nil } + +// -- goFileMap -- + +// A goFileMap is conceptually a map[parseKey]*parseGoHandle, +// optimized for cloning all or nearly all entries. +type goFileMap struct { + // The map is represented as a map of 256 stripes, one per + // distinct value of the top 8 bits of key.file.Hash. + // Each stripe has an associated boolean indicating whether it + // is shared, and thus immutable, and thus must be copied before any update. + // (The bits could be packed but it hasn't been worth it yet.) + stripes [256]map[parseKey]*parseGoHandle + exclusive [256]bool // exclusive[i] means stripe[i] is not shared and may be safely mutated +} + +// newGoFileMap returns a new empty goFileMap. +func newGoFileMap() *goFileMap { + return new(goFileMap) // all stripes are shared (non-exclusive) nil maps +} + +// clone returns a copy of m. +// For concurrency, it counts as an update to m. +func (m *goFileMap) clone() *goFileMap { + m.exclusive = [256]bool{} // original and copy are now nonexclusive + copy := *m + return © +} + +// get returns the value for key k. +func (m *goFileMap) get(k parseKey) *parseGoHandle { + return m.stripes[m.hash(k)][k] +} + +// set updates the value for key k to v. +func (m *goFileMap) set(k parseKey, v *parseGoHandle) { + m.unshare(k)[k] = v +} + +// delete deletes the value for key k, if any. +func (m *goFileMap) delete(k parseKey) { + // TODO(adonovan): opt?: skip unshare if k isn't present. + delete(m.unshare(k), k) +} + +// forEachConcurrent calls f for each entry in the map. +// Calls may be concurrent. +// f must not modify m. +func (m *goFileMap) forEachConcurrent(f func(parseKey, *parseGoHandle)) { + // Visit stripes in parallel chunks. + const p = 16 // concurrency level + var wg sync.WaitGroup + wg.Add(p) + for i := 0; i < p; i++ { + chunk := m.stripes[i*p : (i+1)*p] + go func() { + for _, stripe := range chunk { + for k, v := range stripe { + f(k, v) + } + } + wg.Done() + }() + } + wg.Wait() +} + +// -- internal-- + +// hash returns 8 bits from the key's file digest. +func (m *goFileMap) hash(k parseKey) int { + h := k.file.Hash + if h == "" { + // Sadly the Hash isn't always a hash because cache.GetFile may + // successfully return a *fileHandle containing an error and no hash. + // Lump the duds together for now. + // TODO(adonovan): fix the underlying bug. + return 0 + } + return unhex(h[0])<<4 | unhex(h[1]) +} + +// unhex returns the value of a valid hex digit. +func unhex(b byte) int { + if '0' <= b && b <= '9' { + return int(b - '0') + } + return int(b) & ^0x20 - 'A' + 0xA // [a-fA-F] +} + +// unshare makes k's stripe exclusive, allocating a copy if needed, and returns it. +func (m *goFileMap) unshare(k parseKey) map[parseKey]*parseGoHandle { + i := m.hash(k) + if !m.exclusive[i] { + m.exclusive[i] = true + + // Copy the map. + copy := make(map[parseKey]*parseGoHandle, len(m.stripes[i])) + for k, v := range m.stripes[i] { + copy[k] = v + } + m.stripes[i] = copy + } + return m.stripes[i] +} diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 94037f33fe3..5b908bc721c 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -532,7 +532,7 @@ type FileHandle interface { type FileIdentity struct { URI span.URI - // Identifier represents a unique identifier for the file's content. + // Hash is a string of hex digits denoting the cryptographic digest of the file's content. Hash string } diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 89f79c68b7d..dec2fff6836 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -234,19 +234,18 @@ func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { } } -func (g *Generation) Inherit(hs ...*Handle) { - for _, h := range hs { - if atomic.LoadUint32(&g.destroyed) != 0 { - panic("inherit on generation " + g.name + " destroyed by " + g.destroyedBy) - } +// Inherit makes h valid in generation g. It is concurrency-safe. +func (g *Generation) Inherit(h *Handle) { + if atomic.LoadUint32(&g.destroyed) != 0 { + panic("inherit on generation " + g.name + " destroyed by " + g.destroyedBy) + } - h.mu.Lock() - if h.state == stateDestroyed { - panic(fmt.Sprintf("inheriting destroyed handle %#v (type %T) into generation %v", h.key, h.key, g.name)) - } - h.generations[g] = struct{}{} - h.mu.Unlock() + h.mu.Lock() + if h.state == stateDestroyed { + panic(fmt.Sprintf("inheriting destroyed handle %#v (type %T) into generation %v", h.key, h.key, g.name)) } + h.generations[g] = struct{}{} + h.mu.Unlock() } // Cached returns the value associated with a handle. diff --git a/internal/memoize/memoize_test.go b/internal/memoize/memoize_test.go index f05966b4614..ee0fd23ea1d 100644 --- a/internal/memoize/memoize_test.go +++ b/internal/memoize/memoize_test.go @@ -87,7 +87,8 @@ func TestCleanup(t *testing.T) { expectGet(t, h1, g1, &v1) expectGet(t, h2, g1, &v2) g2 := s.Generation("g2") - g2.Inherit(h1, h2) + g2.Inherit(h1) + g2.Inherit(h2) g1.Destroy("TestCleanup") expectGet(t, h1, g2, &v1) diff --git a/internal/span/uri.go b/internal/span/uri.go index a9777ff8598..f2b39ca424e 100644 --- a/internal/span/uri.go +++ b/internal/span/uri.go @@ -35,6 +35,10 @@ func (uri URI) Filename() string { } func filename(uri URI) (string, error) { + // This function is frequently called and its cost is + // dominated by the allocation of a net.URL. + // TODO(adonovan): opt: replace by a bespoke parseFileURI + // function that doesn't allocate. if uri == "" { return "", nil } @@ -80,6 +84,10 @@ func URIFromURI(s string) URI { return URI(u.String()) } +// CompareURI performs a three-valued comparison of two URIs. +// Lexically unequal URIs may compare equal if they are "file:" URIs +// that share the same base name (ignoring case) and denote the same +// file device/inode, according to stat(2). func CompareURI(a, b URI) int { if equalURI(a, b) { return 0 From 65c0181b23a8a3e8980181af0d8a7bfbc35775a4 Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Wed, 8 Jun 2022 18:52:37 +0000 Subject: [PATCH 004/723] internal/lsp: support textDocument/inlayHint for parameter names This change implements support for textDocument/inlayHint and adds inlay hints for parameter names. For golang/go#52343. For golang/vscode-go#1631. Change-Id: I3f989838b86cef4fd2b4076cb6340010fff7c24c Reviewed-on: https://go-review.googlesource.com/c/tools/+/411094 gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim Reviewed-by: Suzy Mueller Run-TryBot: Jamal Carvalho TryBot-Result: Gopher Robot --- internal/lsp/inlay_hint.go | 21 +++++ internal/lsp/server_gen.go | 4 +- internal/lsp/source/inlay_hint.go | 90 +++++++++++++++++++ .../testdata/inlay_hint/parameter_names.go | 45 ++++++++++ .../inlay_hint/parameter_names.go.golden | 47 ++++++++++ 5 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 internal/lsp/inlay_hint.go create mode 100644 internal/lsp/source/inlay_hint.go create mode 100644 internal/lsp/testdata/inlay_hint/parameter_names.go create mode 100644 internal/lsp/testdata/inlay_hint/parameter_names.go.golden diff --git a/internal/lsp/inlay_hint.go b/internal/lsp/inlay_hint.go new file mode 100644 index 00000000000..b2fd028d728 --- /dev/null +++ b/internal/lsp/inlay_hint.go @@ -0,0 +1,21 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lsp + +import ( + "context" + + "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/lsp/source" +) + +func (s *Server) inlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) { + snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) + defer release() + if !ok { + return nil, err + } + return source.InlayHint(ctx, snapshot, fh, params.ViewPort) +} diff --git a/internal/lsp/server_gen.go b/internal/lsp/server_gen.go index 93b2f9913b8..4e9db0efa19 100644 --- a/internal/lsp/server_gen.go +++ b/internal/lsp/server_gen.go @@ -160,8 +160,8 @@ func (s *Server) Initialized(ctx context.Context, params *protocol.InitializedPa return s.initialized(ctx, params) } -func (s *Server) InlayHint(context.Context, *protocol.InlayHintParams) ([]protocol.InlayHint, error) { - return nil, notImplemented("InlayHint") +func (s *Server) InlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) { + return s.inlayHint(ctx, params) } func (s *Server) InlayHintRefresh(context.Context) error { diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go new file mode 100644 index 00000000000..94dc1372d04 --- /dev/null +++ b/internal/lsp/source/inlay_hint.go @@ -0,0 +1,90 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "context" + "fmt" + "go/ast" + "go/types" + + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/lsp/lsppos" + "golang.org/x/tools/internal/lsp/protocol" +) + +const ( + maxLabelLength = 28 +) + +func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol.Range) ([]protocol.InlayHint, error) { + ctx, done := event.Start(ctx, "source.InlayHint") + defer done() + + pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + if err != nil { + return nil, fmt.Errorf("getting file for InlayHint: %w", err) + } + + tmap := lsppos.NewTokenMapper(pgf.Src, pgf.Tok) + info := pkg.GetTypesInfo() + + var hints []protocol.InlayHint + ast.Inspect(pgf.File, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.CallExpr: + hints = append(hints, parameterNames(n, tmap, info)...) + } + return true + }) + return hints, nil +} + +func parameterNames(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint { + signature, ok := info.TypeOf(node.Fun).(*types.Signature) + if !ok { + return nil + } + + var hints []protocol.InlayHint + for i, v := range node.Args { + start, ok := tmap.Position(v.Pos()) + if !ok { + continue + } + params := signature.Params() + // When a function has variadic params, we skip args after + // params.Len(). + if i > params.Len()-1 { + break + } + value := params.At(i).Name() + // param.Name is empty for built-ins like append + if value == "" { + continue + } + if signature.Variadic() && i == params.Len()-1 { + value = value + "..." + } + hints = append(hints, protocol.InlayHint{ + Position: &start, + Label: buildLabel(value + ":"), + Kind: protocol.Parameter, + PaddingRight: true, + }) + } + return hints +} + +func buildLabel(s string) []protocol.InlayHintLabelPart { + label := protocol.InlayHintLabelPart{ + Value: s, + } + if len(s) > maxLabelLength { + label.Value = s[:maxLabelLength] + "..." + label.Tooltip = s + } + return []protocol.InlayHintLabelPart{label} +} diff --git a/internal/lsp/testdata/inlay_hint/parameter_names.go b/internal/lsp/testdata/inlay_hint/parameter_names.go new file mode 100644 index 00000000000..6fba23530aa --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/parameter_names.go @@ -0,0 +1,45 @@ +package inlayHint //@inlayHint("package") + +import "fmt" + +func hello(name string) string { + return "Hello " + name +} + +func helloWorld() string { + return hello("World") +} + +type foo struct{} + +func (*foo) bar(baz string, qux int) int { + if baz != "" { + return qux + 1 + } + return qux +} + +func kase(foo int, bar bool, baz ...string) { + fmt.Println(foo, bar, baz) +} + +func kipp(foo string, bar, baz string) { + fmt.Println(foo, bar, baz) +} + +func plex(foo, bar string, baz string) { + fmt.Println(foo, bar, baz) +} + +func tars(foo string, bar, baz string) { + fmt.Println(foo, bar, baz) +} + +func foobar() { + var x foo + x.bar("", 1) + kase(0, true, "c", "d", "e") + kipp("a", "b", "c") + plex("a", "b", "c") + tars("a", "b", "c") +} diff --git a/internal/lsp/testdata/inlay_hint/parameter_names.go.golden b/internal/lsp/testdata/inlay_hint/parameter_names.go.golden new file mode 100644 index 00000000000..66351e48300 --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/parameter_names.go.golden @@ -0,0 +1,47 @@ +-- inlayHint -- +package inlayHint //@inlayHint("package") + +import "fmt" + +func hello(name string) string { + return "Hello " + name +} + +func helloWorld() string { + return hello("World") +} + +type foo struct{} + +func (*foo) bar(baz string, qux int) int { + if baz != "" { + return qux + 1 + } + return qux +} + +func kase(foo int, bar bool, baz ...string) { + fmt.Println(foo, bar, baz) +} + +func kipp(foo string, bar, baz string) { + fmt.Println(foo, bar, baz) +} + +func plex(foo, bar string, baz string) { + fmt.Println(foo, bar, baz) +} + +func tars(foo string, bar, baz string) { + fmt.Println(foo, bar, baz) +} + +func foobar() { + var x foo + x.bar("", 1) + kase(0, true, "c", "d", "e") + kipp("a", "b", "c") + plex("a", "b", "c") + tars("a", "b", "c") +} + From ecc147927830bfc0ccc572bd31fa6ade1679b72b Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Wed, 8 Jun 2022 18:57:57 +0000 Subject: [PATCH 005/723] internal/lsp: add inlay hints for variable types For golang/go#52343. For golang/vscode-go#1631. Change-Id: I94a1b3c389d8bfaa48754e28a52ef76c29eb6ead Reviewed-on: https://go-review.googlesource.com/c/tools/+/411100 Run-TryBot: Jamal Carvalho gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim Reviewed-by: Suzy Mueller --- internal/lsp/source/inlay_hint.go | 47 +++++++++++++++++++ .../lsp/testdata/inlay_hint/variable_types.go | 20 ++++++++ .../inlay_hint/variable_types.go.golden | 22 +++++++++ 3 files changed, 89 insertions(+) create mode 100644 internal/lsp/testdata/inlay_hint/variable_types.go create mode 100644 internal/lsp/testdata/inlay_hint/variable_types.go.golden diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 94dc1372d04..00a2b009db1 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "go/ast" + "go/token" "go/types" "golang.org/x/tools/internal/event" @@ -30,12 +31,17 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol tmap := lsppos.NewTokenMapper(pgf.Src, pgf.Tok) info := pkg.GetTypesInfo() + q := Qualifier(pgf.File, pkg.GetTypes(), info) var hints []protocol.InlayHint ast.Inspect(pgf.File, func(node ast.Node) bool { switch n := node.(type) { case *ast.CallExpr: hints = append(hints, parameterNames(n, tmap, info)...) + case *ast.AssignStmt: + hints = append(hints, assignVariableTypes(n, tmap, info, &q)...) + case *ast.RangeStmt: + hints = append(hints, rangeVariableTypes(n, tmap, info, &q)...) } return true }) @@ -78,6 +84,47 @@ func parameterNames(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.In return hints } +func assignVariableTypes(node *ast.AssignStmt, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + if node.Tok != token.DEFINE { + return nil + } + var hints []protocol.InlayHint + for _, v := range node.Lhs { + if h := variableType(v, tmap, info, q); h != nil { + hints = append(hints, *h) + } + } + return hints +} + +func rangeVariableTypes(node *ast.RangeStmt, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + var hints []protocol.InlayHint + if h := variableType(node.Key, tmap, info, q); h != nil { + hints = append(hints, *h) + } + if h := variableType(node.Value, tmap, info, q); h != nil { + hints = append(hints, *h) + } + return hints +} + +func variableType(e ast.Expr, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) *protocol.InlayHint { + typ := info.TypeOf(e) + if typ == nil { + return nil + } + end, ok := tmap.Position(e.End()) + if !ok { + return nil + } + return &protocol.InlayHint{ + Position: &end, + Label: buildLabel(types.TypeString(typ, *q)), + Kind: protocol.Type, + PaddingLeft: true, + } +} + func buildLabel(s string) []protocol.InlayHintLabelPart { label := protocol.InlayHintLabelPart{ Value: s, diff --git a/internal/lsp/testdata/inlay_hint/variable_types.go b/internal/lsp/testdata/inlay_hint/variable_types.go new file mode 100644 index 00000000000..219af7059c7 --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/variable_types.go @@ -0,0 +1,20 @@ +package inlayHint //@inlayHint("package") + +func assignTypes() { + i, j := 0, len([]string{})-1 + println(i, j) +} + +func rangeTypes() { + for k, v := range []string{} { + println(k, v) + } +} + +func funcLitType() { + myFunc := func(a string) string { return "" } +} + +func compositeLitType() { + foo := map[string]interface{}{"": ""} +} diff --git a/internal/lsp/testdata/inlay_hint/variable_types.go.golden b/internal/lsp/testdata/inlay_hint/variable_types.go.golden new file mode 100644 index 00000000000..70c019caa1f --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/variable_types.go.golden @@ -0,0 +1,22 @@ +-- inlayHint -- +package inlayHint //@inlayHint("package") + +func assignTypes() { + i, j := 0, len([]string{})-1 + println(i, j) +} + +func rangeTypes() { + for k, v := range []string{} { + println(k, v) + } +} + +func funcLitType() { + myFunc := func(a string) string { return "" } +} + +func compositeLitType() { + foo := map[string]interface{}{"": ""} +} + From 83b0675060419168e3e46f9dc821d9dd9a358f63 Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Wed, 8 Jun 2022 18:58:45 +0000 Subject: [PATCH 006/723] internal/lsp: add inlay hints for constant values For golang/go#52343. For golang/vscode-go#1631. Change-Id: Iaef0beab2837502f6428767f457d1da21848fcb6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411101 Run-TryBot: Jamal Carvalho TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller gopls-CI: kokoro --- internal/lsp/source/inlay_hint.go | 54 +++++++++++++++++++ .../testdata/inlay_hint/constant_values.go | 45 ++++++++++++++++ .../inlay_hint/constant_values.go.golden | 47 ++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 internal/lsp/testdata/inlay_hint/constant_values.go create mode 100644 internal/lsp/testdata/inlay_hint/constant_values.go.golden diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 00a2b009db1..95df237ad21 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -8,8 +8,10 @@ import ( "context" "fmt" "go/ast" + "go/constant" "go/token" "go/types" + "strings" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/lsppos" @@ -42,6 +44,8 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol hints = append(hints, assignVariableTypes(n, tmap, info, &q)...) case *ast.RangeStmt: hints = append(hints, rangeVariableTypes(n, tmap, info, &q)...) + case *ast.GenDecl: + hints = append(hints, constantValues(n, tmap, info)...) } return true }) @@ -125,6 +129,56 @@ func variableType(e ast.Expr, tmap *lsppos.TokenMapper, info *types.Info, q *typ } } +func constantValues(node *ast.GenDecl, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint { + if node.Tok != token.CONST { + return nil + } + + var hints []protocol.InlayHint + for _, v := range node.Specs { + spec, ok := v.(*ast.ValueSpec) + if !ok { + continue + } + end, ok := tmap.Position(v.End()) + if !ok { + continue + } + // Show hints when values are missing or at least one value is not + // a basic literal. + showHints := len(spec.Values) == 0 + checkValues := len(spec.Names) == len(spec.Values) + var values []string + for i, w := range spec.Names { + obj, ok := info.ObjectOf(w).(*types.Const) + if !ok || obj.Val().Kind() == constant.Unknown { + return nil + } + if checkValues { + switch spec.Values[i].(type) { + case *ast.BadExpr: + return nil + case *ast.BasicLit: + default: + if obj.Val().Kind() != constant.Bool { + showHints = true + } + } + } + values = append(values, fmt.Sprintf("%v", obj.Val())) + } + if !showHints || len(values) == 0 { + continue + } + hints = append(hints, protocol.InlayHint{ + Position: &end, + Label: buildLabel("= " + strings.Join(values, ", ")), + PaddingLeft: true, + }) + } + return hints +} + func buildLabel(s string) []protocol.InlayHintLabelPart { label := protocol.InlayHintLabelPart{ Value: s, diff --git a/internal/lsp/testdata/inlay_hint/constant_values.go b/internal/lsp/testdata/inlay_hint/constant_values.go new file mode 100644 index 00000000000..e3339b0f303 --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/constant_values.go @@ -0,0 +1,45 @@ +package inlayHint //@inlayHint("package") + +const True = true + +type Kind int + +const ( + KindNone Kind = iota + KindPrint + KindPrintf + KindErrorf +) + +const ( + u = iota * 4 + v float64 = iota * 42 + w = iota * 42 +) + +const ( + a, b = 1, 2 + c, d + e, f = 5 * 5, "hello" + "world" + g, h + i, j = true, f +) + +// No hint +const ( + Int = 3 + Float = 3.14 + Bool = true + Rune = '3' + Complex = 2.7i + String = "Hello, world!" +) + +var ( + varInt = 3 + varFloat = 3.14 + varBool = true + varRune = '3' + '4' + varComplex = 2.7i + varString = "Hello, world!" +) diff --git a/internal/lsp/testdata/inlay_hint/constant_values.go.golden b/internal/lsp/testdata/inlay_hint/constant_values.go.golden new file mode 100644 index 00000000000..69481b10d64 --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/constant_values.go.golden @@ -0,0 +1,47 @@ +-- inlayHint -- +package inlayHint //@inlayHint("package") + +const True = true + +type Kind int + +const ( + KindNone Kind = iota<= 0> + KindPrint<= 1> + KindPrintf<= 2> + KindErrorf<= 3> +) + +const ( + u = iota * 4<= 0> + v float64 = iota * 42<= 42> + w = iota * 42<= 84> +) + +const ( + a, b = 1, 2 + c, d<= 1, 2> + e, f = 5 * 5, "hello" + "world"<= 25, "helloworld"> + g, h<= 25, "helloworld"> + i, j = true, f<= true, "helloworld"> +) + +// No hint +const ( + Int = 3 + Float = 3.14 + Bool = true + Rune = '3' + Complex = 2.7i + String = "Hello, world!" +) + +var ( + varInt = 3 + varFloat = 3.14 + varBool = true + varRune = '3' + '4' + varComplex = 2.7i + varString = "Hello, world!" +) + From 5e48d261e2d147593b5464e78d5a95dd9442f070 Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Wed, 8 Jun 2022 19:00:34 +0000 Subject: [PATCH 007/723] internal/lsp: add inlay hints for composite literal names For golang/go#52343. For golang/vscode-go#1631. Change-Id: I8fba5ddf0bd25ba0fc20f3305ce13868f426087c Reviewed-on: https://go-review.googlesource.com/c/tools/+/411102 Run-TryBot: Jamal Carvalho TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller gopls-CI: kokoro --- internal/lsp/source/inlay_hint.go | 33 +++++++++++++++++++ .../testdata/inlay_hint/composite_literals.go | 15 +++++++++ .../inlay_hint/composite_literals.go.golden | 17 ++++++++++ 3 files changed, 65 insertions(+) create mode 100644 internal/lsp/testdata/inlay_hint/composite_literals.go create mode 100644 internal/lsp/testdata/inlay_hint/composite_literals.go.golden diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 95df237ad21..406e4ae80e8 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -46,6 +46,8 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol hints = append(hints, rangeVariableTypes(n, tmap, info, &q)...) case *ast.GenDecl: hints = append(hints, constantValues(n, tmap, info)...) + case *ast.CompositeLit: + hints = append(hints, compositeLiterals(n, tmap, info)...) } return true }) @@ -179,6 +181,37 @@ func constantValues(node *ast.GenDecl, tmap *lsppos.TokenMapper, info *types.Inf return hints } +func compositeLiterals(node *ast.CompositeLit, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint { + typ := info.TypeOf(node) + if typ == nil { + return nil + } + strct, ok := typ.Underlying().(*types.Struct) + if !ok { + return nil + } + + var hints []protocol.InlayHint + for i, v := range node.Elts { + if _, ok := v.(*ast.KeyValueExpr); !ok { + start, ok := tmap.Position(v.Pos()) + if !ok { + continue + } + if i > strct.NumFields()-1 { + break + } + hints = append(hints, protocol.InlayHint{ + Position: &start, + Label: buildLabel(strct.Field(i).Name() + ":"), + Kind: protocol.Parameter, + PaddingRight: true, + }) + } + } + return hints +} + func buildLabel(s string) []protocol.InlayHintLabelPart { label := protocol.InlayHintLabelPart{ Value: s, diff --git a/internal/lsp/testdata/inlay_hint/composite_literals.go b/internal/lsp/testdata/inlay_hint/composite_literals.go new file mode 100644 index 00000000000..7eeed03e81a --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/composite_literals.go @@ -0,0 +1,15 @@ +package inlayHint //@inlayHint("package") + +import "fmt" + +func fieldNames() { + for _, c := range []struct { + in, want string + }{ + {"Hello, world", "dlrow ,olleH"}, + {"Hello, 世界", "界世 ,olleH"}, + {"", ""}, + } { + fmt.Println(c.in == c.want) + } +} diff --git a/internal/lsp/testdata/inlay_hint/composite_literals.go.golden b/internal/lsp/testdata/inlay_hint/composite_literals.go.golden new file mode 100644 index 00000000000..efa87b0fea8 --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/composite_literals.go.golden @@ -0,0 +1,17 @@ +-- inlayHint -- +package inlayHint //@inlayHint("package") + +import "fmt" + +func fieldNames() { + for _, c := range []struct { + in, want string + }{ + {"Hello, world", "dlrow ,olleH"}, + {"Hello, 世界", "界世 ,olleH"}, + {"", ""}, + } { + fmt.Println(c.in == c.want) + } +} + From c41ddceaa4e81aad291932bf356dca891eb9488a Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Fri, 10 Jun 2022 13:48:16 +0000 Subject: [PATCH 008/723] internal/lsp: include padding in inlay hint marker tests The marker tests are updated to include padding values when mapping inlay hints to text edits. Change-Id: Ieb421088238c65b07abdad12763816d3d1e757c8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411654 Run-TryBot: Jamal Carvalho gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller --- internal/lsp/lsp_test.go | 9 +++++++- .../inlay_hint/composite_literals.go.golden | 10 ++++----- .../inlay_hint/constant_values.go.golden | 22 +++++++++---------- .../inlay_hint/parameter_names.go.golden | 20 ++++++++--------- .../inlay_hint/variable_types.go.golden | 8 +++---- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index e097100c2ff..2f46ff304bb 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -949,9 +949,16 @@ func (r *runner) InlayHints(t *testing.T, spn span.Span) { // Map inlay hints to text edits. edits := make([]protocol.TextEdit, len(hints)) for i, hint := range hints { + var paddingLeft, paddingRight string + if hint.PaddingLeft { + paddingLeft = " " + } + if hint.PaddingRight { + paddingRight = " " + } edits[i] = protocol.TextEdit{ Range: protocol.Range{Start: *hint.Position, End: *hint.Position}, - NewText: fmt.Sprintf("<%s>", hint.Label[0].Value), + NewText: fmt.Sprintf("<%s%s%s>", paddingLeft, hint.Label[0].Value, paddingRight), } } diff --git a/internal/lsp/testdata/inlay_hint/composite_literals.go.golden b/internal/lsp/testdata/inlay_hint/composite_literals.go.golden index efa87b0fea8..ecff7800387 100644 --- a/internal/lsp/testdata/inlay_hint/composite_literals.go.golden +++ b/internal/lsp/testdata/inlay_hint/composite_literals.go.golden @@ -4,14 +4,14 @@ package inlayHint //@inlayHint("package") import "fmt" func fieldNames() { - for _, c := range []struct { + for _< int>, c< struct{in string; want strin...> := range []struct { in, want string }{ - {"Hello, world", "dlrow ,olleH"}, - {"Hello, 世界", "界世 ,olleH"}, - {"", ""}, + {"Hello, world", "dlrow ,olleH"}, + {"Hello, 世界", "界世 ,olleH"}, + {"", ""}, } { - fmt.Println(c.in == c.want) + fmt.Println(c.in == c.want) } } diff --git a/internal/lsp/testdata/inlay_hint/constant_values.go.golden b/internal/lsp/testdata/inlay_hint/constant_values.go.golden index 69481b10d64..edc46debc37 100644 --- a/internal/lsp/testdata/inlay_hint/constant_values.go.golden +++ b/internal/lsp/testdata/inlay_hint/constant_values.go.golden @@ -6,24 +6,24 @@ const True = true type Kind int const ( - KindNone Kind = iota<= 0> - KindPrint<= 1> - KindPrintf<= 2> - KindErrorf<= 3> + KindNone Kind = iota< = 0> + KindPrint< = 1> + KindPrintf< = 2> + KindErrorf< = 3> ) const ( - u = iota * 4<= 0> - v float64 = iota * 42<= 42> - w = iota * 42<= 84> + u = iota * 4< = 0> + v float64 = iota * 42< = 42> + w = iota * 42< = 84> ) const ( a, b = 1, 2 - c, d<= 1, 2> - e, f = 5 * 5, "hello" + "world"<= 25, "helloworld"> - g, h<= 25, "helloworld"> - i, j = true, f<= true, "helloworld"> + c, d< = 1, 2> + e, f = 5 * 5, "hello" + "world"< = 25, "helloworld"> + g, h< = 25, "helloworld"> + i, j = true, f< = true, "helloworld"> ) // No hint diff --git a/internal/lsp/testdata/inlay_hint/parameter_names.go.golden b/internal/lsp/testdata/inlay_hint/parameter_names.go.golden index 66351e48300..46d3ea4e9bf 100644 --- a/internal/lsp/testdata/inlay_hint/parameter_names.go.golden +++ b/internal/lsp/testdata/inlay_hint/parameter_names.go.golden @@ -8,7 +8,7 @@ func hello(name string) string { } func helloWorld() string { - return hello("World") + return hello("World") } type foo struct{} @@ -21,27 +21,27 @@ func (*foo) bar(baz string, qux int) int { } func kase(foo int, bar bool, baz ...string) { - fmt.Println(foo, bar, baz) + fmt.Println(foo, bar, baz) } func kipp(foo string, bar, baz string) { - fmt.Println(foo, bar, baz) + fmt.Println(foo, bar, baz) } func plex(foo, bar string, baz string) { - fmt.Println(foo, bar, baz) + fmt.Println(foo, bar, baz) } func tars(foo string, bar, baz string) { - fmt.Println(foo, bar, baz) + fmt.Println(foo, bar, baz) } func foobar() { var x foo - x.bar("", 1) - kase(0, true, "c", "d", "e") - kipp("a", "b", "c") - plex("a", "b", "c") - tars("a", "b", "c") + x.bar("", 1) + kase(0, true, "c", "d", "e") + kipp("a", "b", "c") + plex("a", "b", "c") + tars("a", "b", "c") } diff --git a/internal/lsp/testdata/inlay_hint/variable_types.go.golden b/internal/lsp/testdata/inlay_hint/variable_types.go.golden index 70c019caa1f..6039950d5f3 100644 --- a/internal/lsp/testdata/inlay_hint/variable_types.go.golden +++ b/internal/lsp/testdata/inlay_hint/variable_types.go.golden @@ -2,21 +2,21 @@ package inlayHint //@inlayHint("package") func assignTypes() { - i, j := 0, len([]string{})-1 + i< int>, j< int> := 0, len([]string{})-1 println(i, j) } func rangeTypes() { - for k, v := range []string{} { + for k< int>, v< string> := range []string{} { println(k, v) } } func funcLitType() { - myFunc := func(a string) string { return "" } + myFunc< func(a string) string> := func(a string) string { return "" } } func compositeLitType() { - foo := map[string]interface{}{"": ""} + foo< map[string]interface{}> := map[string]interface{}{"": ""} } From a41fc9869a5ab7b215c33cb7549d3900adc27c5b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 9 Jun 2022 18:38:26 -0400 Subject: [PATCH 009/723] internal/lsp/cache: use [256]byte Hash instead of hex digit string I had hoped to see a reduction in total allocation, but it does not appear to be significant according to the included crude benchmark. Nonetheless this is a slight code clarity improvement. Change-Id: I94a503b377dd1146eb371ff11222a351cb5a43b7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411655 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/regtest/bench/bench_test.go | 30 +++++++++++++++++++++ internal/lsp/cache/analysis.go | 4 +-- internal/lsp/cache/cache.go | 19 ++----------- internal/lsp/cache/check.go | 31 +++++++++++----------- internal/lsp/cache/imports.go | 6 ++--- internal/lsp/cache/mod.go | 8 +++--- internal/lsp/cache/mod_tidy.go | 17 +++++------- internal/lsp/cache/session.go | 4 +-- internal/lsp/cache/snapshot.go | 24 +++-------------- internal/lsp/cache/symbols.go | 2 +- internal/lsp/cache/view.go | 2 +- internal/lsp/debug/serve.go | 3 ++- internal/lsp/source/view.go | 29 +++++++++++++++++--- 13 files changed, 98 insertions(+), 81 deletions(-) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index 5e4eb5fc23a..22f157f4719 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -8,6 +8,7 @@ import ( "flag" "fmt" "os" + "runtime" "runtime/pprof" "testing" @@ -66,6 +67,7 @@ func TestBenchmarkIWL(t *testing.T) { results := testing.Benchmark(func(b *testing.B) { for i := 0; i < b.N; i++ { WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {}) + } }) @@ -192,3 +194,31 @@ func TestBenchmarkDidChange(t *testing.T) { printBenchmarkResults(result) }) } + +// TestPrintMemStats measures the memory usage of loading a project. +// It uses the same -didchange_dir flag as above. +// Always run it in isolation since it measures global heap usage. +// +// Kubernetes example: +// $ go test -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes +// TotalAlloc: 5766 MB +// HeapAlloc: 1984 MB +// +// Both figures exhibit variance of less than 1%. +func TestPrintMemStats(t *testing.T) { + if *benchDir == "" { + t.Skip("-didchange_dir is not set") + } + + // Load the program... + opts := benchmarkOptions(*benchDir) + WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { + // ...and print the memory usage. + runtime.GC() + runtime.GC() + var mem runtime.MemStats + runtime.ReadMemStats(&mem) + t.Logf("TotalAlloc:\t%d MB", mem.TotalAlloc/1e6) + t.Logf("HeapAlloc:\t%d MB", mem.HeapAlloc/1e6) + }) +} diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index e882fb46f07..9f7a19c5c60 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -54,7 +54,7 @@ func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.A return results, nil } -type actionHandleKey string +type actionHandleKey source.Hash // An action represents one unit of analysis work: the application of // one analysis to one package. Actions form a DAG, both within a @@ -170,7 +170,7 @@ func (act *actionHandle) analyze(ctx context.Context, snapshot *snapshot) ([]*so } func buildActionKey(a *analysis.Analyzer, ph *packageHandle) actionHandleKey { - return actionHandleKey(hashContents([]byte(fmt.Sprintf("%p %s", a, string(ph.key))))) + return actionHandleKey(source.Hashf("%p%s", a, ph.key[:])) } func (act *actionHandle) String() string { diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index f5796dfefa2..2a8a169d510 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -6,7 +6,6 @@ package cache import ( "context" - "crypto/sha256" "fmt" "go/ast" "go/token" @@ -55,7 +54,7 @@ type fileHandle struct { modTime time.Time uri span.URI bytes []byte - hash string + hash source.Hash err error // size is the file length as reported by Stat, for the purpose of @@ -139,7 +138,7 @@ func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, e size: fi.Size(), uri: uri, bytes: data, - hash: hashContents(data), + hash: source.HashOf(data), }, nil } @@ -168,10 +167,6 @@ func (h *fileHandle) URI() span.URI { return h.uri } -func (h *fileHandle) Hash() string { - return h.hash -} - func (h *fileHandle) FileIdentity() source.FileIdentity { return source.FileIdentity{ URI: h.uri, @@ -183,16 +178,6 @@ func (h *fileHandle) Read() ([]byte, error) { return h.bytes, h.err } -// hashContents returns a string of hex digits denoting the hash of contents. -// -// TODO(adonovan): opt: use [32]byte array as a value more widely and convert -// to hex digits on demand (rare). The array is larger when it appears as a -// struct field (32B vs 16B) but smaller overall (string data is 64B), has -// better locality, and is more efficiently hashed by runtime maps. -func hashContents(contents []byte) string { - return fmt.Sprintf("%64x", sha256.Sum256(contents)) -} - var cacheIndex, sessionIndex, viewIndex int64 func (c *Cache) ID() string { return c.id } diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index b8a3655a9d4..f09fc298a98 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -32,7 +32,7 @@ import ( "golang.org/x/tools/internal/typesinternal" ) -type packageHandleKey string +type packageHandleKey source.Hash type packageHandle struct { handle *memoize.Handle @@ -187,7 +187,7 @@ func (s *snapshot) buildKey(ctx context.Context, id PackageID, mode source.Parse } // One bad dependency should not prevent us from checking the entire package. // Add a special key to mark a bad dependency. - depKeys = append(depKeys, packageHandleKey(fmt.Sprintf("%s import not found", depID))) + depKeys = append(depKeys, packageHandleKey(source.Hashf("%s import not found", depID))) continue } deps[depHandle.m.PkgPath] = depHandle @@ -215,6 +215,8 @@ func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { } func checkPackageKey(id PackageID, pghs []*parseGoHandle, m *KnownMetadata, deps []packageHandleKey, mode source.ParseMode, experimentalKey bool) packageHandleKey { + // TODO(adonovan): opt: no need to materalize the bytes; hash them directly. + // Also, use field separators to avoid spurious collisions. b := bytes.NewBuffer(nil) b.WriteString(string(id)) if m.Module != nil { @@ -225,38 +227,37 @@ func checkPackageKey(id PackageID, pghs []*parseGoHandle, m *KnownMetadata, deps // files, and deps). It should not otherwise affect the inputs to the type // checker, so this experiment omits it. This should increase cache hits on // the daemon as cfg contains the environment and working directory. - b.WriteString(hashConfig(m.Config)) + hc := hashConfig(m.Config) + b.Write(hc[:]) } b.WriteByte(byte(mode)) for _, dep := range deps { - b.WriteString(string(dep)) + b.Write(dep[:]) } for _, cgf := range pghs { b.WriteString(cgf.file.FileIdentity().String()) } - return packageHandleKey(hashContents(b.Bytes())) + return packageHandleKey(source.HashOf(b.Bytes())) } // hashEnv returns a hash of the snapshot's configuration. -func hashEnv(s *snapshot) string { +func hashEnv(s *snapshot) source.Hash { s.view.optionsMu.Lock() env := s.view.options.EnvSlice() s.view.optionsMu.Unlock() - b := &bytes.Buffer{} - for _, e := range env { - b.WriteString(e) - } - return hashContents(b.Bytes()) + return source.Hashf("%s", env) } // hashConfig returns the hash for the *packages.Config. -func hashConfig(config *packages.Config) string { - b := bytes.NewBuffer(nil) +func hashConfig(config *packages.Config) source.Hash { + // TODO(adonovan): opt: don't materialize the bytes; hash them directly. + // Also, use sound field separators to avoid collisions. + var b bytes.Buffer // Dir, Mode, Env, BuildFlags are the parts of the config that can change. b.WriteString(config.Dir) - b.WriteString(string(rune(config.Mode))) + b.WriteRune(rune(config.Mode)) for _, e := range config.Env { b.WriteString(e) @@ -264,7 +265,7 @@ func hashConfig(config *packages.Config) string { for _, f := range config.BuildFlags { b.WriteString(f) } - return hashContents(b.Bytes()) + return source.HashOf(b.Bytes()) } func (ph *packageHandle) Check(ctx context.Context, s source.Snapshot) (source.Package, error) { diff --git a/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go index 01a2468ef34..f333f700ddf 100644 --- a/internal/lsp/cache/imports.go +++ b/internal/lsp/cache/imports.go @@ -27,7 +27,7 @@ type importsState struct { cleanupProcessEnv func() cacheRefreshDuration time.Duration cacheRefreshTimer *time.Timer - cachedModFileHash string + cachedModFileHash source.Hash cachedBuildFlags []string } @@ -38,7 +38,7 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot // Find the hash of the active mod file, if any. Using the unsaved content // is slightly wasteful, since we'll drop caches a little too often, but // the mod file shouldn't be changing while people are autocompleting. - var modFileHash string + var modFileHash source.Hash // If we are using 'legacyWorkspace' mode, we can just read the modfile from // the snapshot. Otherwise, we need to get the synthetic workspace mod file. // @@ -61,7 +61,7 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot if err != nil { return err } - modFileHash = hashContents(modBytes) + modFileHash = source.HashOf(modBytes) } // view.goEnv is immutable -- changes make a new view. Options can change. diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index 5ac199bd96b..c076f424dc9 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -201,9 +201,11 @@ func sumFilename(modURI span.URI) string { // modKey is uniquely identifies cached data for `go mod why` or dependencies // to upgrade. type modKey struct { - sessionID, env, view string - mod source.FileIdentity - verb modAction + sessionID string + env source.Hash + view string + mod source.FileIdentity + verb modAction } type modAction int diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index aa525e7413d..bd2ff0c5f88 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -29,10 +29,10 @@ import ( type modTidyKey struct { sessionID string - env string + env source.Hash gomod source.FileIdentity - imports string - unsavedOverlays string + imports source.Hash + unsavedOverlays source.Hash view string } @@ -81,10 +81,6 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc if err != nil { return nil, err } - importHash, err := s.hashImports(ctx, workspacePkgs) - if err != nil { - return nil, err - } s.mu.Lock() overlayHash := hashUnsavedOverlays(s.files) @@ -93,7 +89,7 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc key := modTidyKey{ sessionID: s.view.session.id, view: s.view.folder.Filename(), - imports: importHash, + imports: s.hashImports(ctx, workspacePkgs), unsavedOverlays: overlayHash, gomod: fh.FileIdentity(), env: hashEnv(s), @@ -152,7 +148,7 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc return mth.tidy(ctx, s) } -func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) (string, error) { +func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) source.Hash { seen := map[string]struct{}{} var imports []string for _, ph := range wsPackages { @@ -164,8 +160,7 @@ func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) } } sort.Strings(imports) - hashed := strings.Join(imports, ",") - return hashContents([]byte(hashed)), nil + return source.Hashf("%s", imports) } // modTidyDiagnostics computes the differences between the original and tidied diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 9da5c1e69f9..cbb58740621 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -44,7 +44,7 @@ type overlay struct { session *Session uri span.URI text []byte - hash string + hash source.Hash version int32 kind source.FileKind @@ -637,7 +637,7 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif if c.OnDisk || c.Action == source.Save { version = o.version } - hash := hashContents(text) + hash := source.HashOf(text) var sameContentOnDisk bool switch c.Action { case source.Delete: diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 0d3c869cd2e..6edd1dbe658 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -466,7 +466,7 @@ func (s *snapshot) buildOverlay() map[string][]byte { return overlays } -func hashUnsavedOverlays(files map[span.URI]source.VersionedFileHandle) string { +func hashUnsavedOverlays(files map[span.URI]source.VersionedFileHandle) source.Hash { var unsaved []string for uri, fh := range files { if overlay, ok := fh.(*overlay); ok && !overlay.saved { @@ -474,7 +474,7 @@ func hashUnsavedOverlays(files map[span.URI]source.VersionedFileHandle) string { } } sort.Strings(unsaved) - return hashContents([]byte(strings.Join(unsaved, ""))) + return source.Hashf("%s", unsaved) } func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]source.Package, error) { @@ -2652,25 +2652,7 @@ func (m *goFileMap) forEachConcurrent(f func(parseKey, *parseGoHandle)) { // -- internal-- // hash returns 8 bits from the key's file digest. -func (m *goFileMap) hash(k parseKey) int { - h := k.file.Hash - if h == "" { - // Sadly the Hash isn't always a hash because cache.GetFile may - // successfully return a *fileHandle containing an error and no hash. - // Lump the duds together for now. - // TODO(adonovan): fix the underlying bug. - return 0 - } - return unhex(h[0])<<4 | unhex(h[1]) -} - -// unhex returns the value of a valid hex digit. -func unhex(b byte) int { - if '0' <= b && b <= '9' { - return int(b - '0') - } - return int(b) & ^0x20 - 'A' + 0xA // [a-fA-F] -} +func (*goFileMap) hash(k parseKey) byte { return k.file.Hash[0] } // unshare makes k's stripe exclusive, allocating a copy if needed, and returns it. func (m *goFileMap) unshare(k parseKey) map[parseKey]*parseGoHandle { diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go index db68912015e..bf5e00b1648 100644 --- a/internal/lsp/cache/symbols.go +++ b/internal/lsp/cache/symbols.go @@ -33,7 +33,7 @@ type symbolData struct { err error } -type symbolHandleKey string +type symbolHandleKey source.Hash func (s *snapshot) buildSymbolHandle(ctx context.Context, fh source.FileHandle) *symbolHandle { if h := s.getSymbolHandle(fh.URI()); h != nil { diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index b0390a3fbde..0ed9883451b 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -165,7 +165,7 @@ func (v *View) ID() string { return v.id } // given go.mod file. It is the caller's responsibility to clean up the files // when they are done using them. func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanup func(), err error) { - filenameHash := hashContents([]byte(modFh.URI().Filename())) + filenameHash := source.Hashf("%s", modFh.URI().Filename()) tmpMod, err := ioutil.TempFile("", fmt.Sprintf("go.%s.*.mod", filenameHash)) if err != nil { return "", nil, err diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go index 0bdee92c5e0..d343a6d65a2 100644 --- a/internal/lsp/debug/serve.go +++ b/internal/lsp/debug/serve.go @@ -320,7 +320,8 @@ func (i *Instance) getFile(r *http.Request) interface{} { return nil } for _, o := range s.Overlays() { - if o.FileIdentity().Hash == identifier { + // TODO(adonovan): understand and document this comparison. + if o.FileIdentity().Hash.String() == identifier { return o } } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 5b908bc721c..7960b0c0368 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -7,6 +7,7 @@ package source import ( "bytes" "context" + "crypto/sha256" "errors" "fmt" "go/ast" @@ -528,12 +529,32 @@ type FileHandle interface { Saved() bool } +// A Hash is a cryptographic digest of the contents of a file. +// (Although at 32B it is larger than a 16B string header, it is smaller +// and has better locality than the string header + 64B of hex digits.) +type Hash [sha256.Size]byte + +// HashOf returns the hash of some data. +func HashOf(data []byte) Hash { + return Hash(sha256.Sum256(data)) +} + +// Hashf returns the hash of a printf-formatted string. +func Hashf(format string, args ...interface{}) Hash { + // Although this looks alloc-heavy, it is faster than using + // Fprintf on sha256.New() because the allocations don't escape. + return HashOf([]byte(fmt.Sprintf(format, args...))) +} + +// String returns the digest as a string of hex digits. +func (h Hash) String() string { + return fmt.Sprintf("%64x", [sha256.Size]byte(h)) +} + // FileIdentity uniquely identifies a file at a version from a FileSystem. type FileIdentity struct { - URI span.URI - - // Hash is a string of hex digits denoting the cryptographic digest of the file's content. - Hash string + URI span.URI + Hash Hash // digest of file contents } func (id FileIdentity) String() string { From 034398994d5001d97b2e657118f00f7540c8d8fc Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Mon, 13 Jun 2022 14:47:03 -0400 Subject: [PATCH 010/723] internal/lsp: fix error message for inlay hints Fix the error message to describe inlay hints failure. Change-Id: If4597bc3e513c4dce344f11f6fa92ba20e29681a Reviewed-on: https://go-review.googlesource.com/c/tools/+/411899 gopls-CI: kokoro Run-TryBot: Suzy Mueller TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim --- internal/lsp/lsp_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 2f46ff304bb..56356e9b5a4 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -977,7 +977,7 @@ func (r *runner) InlayHints(t *testing.T, spn span.Span) { })) if withinlayHints != got { - t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, withinlayHints, got) + t.Errorf("inlay hints failed for %s, expected:\n%v\ngot:\n%v", filename, withinlayHints, got) } } From c15c04572c9db1074d3b456ded7917ba493e8f44 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Mon, 13 Jun 2022 14:41:22 -0400 Subject: [PATCH 011/723] internal/lsp: enable inlay hint tests The change to implement inlay hints has been merged, so we need to enable the tests. Change-Id: I47e7ab343d0ab10283caac0a3d6677dd69c7504a Reviewed-on: https://go-review.googlesource.com/c/tools/+/411898 Run-TryBot: Suzy Mueller gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim --- internal/lsp/testdata/inlayHint/a.go | 9 --------- internal/lsp/testdata/inlayHint/a.go.golden | 11 ----------- internal/lsp/tests/tests.go | 1 - 3 files changed, 21 deletions(-) delete mode 100644 internal/lsp/testdata/inlayHint/a.go delete mode 100644 internal/lsp/testdata/inlayHint/a.go.golden diff --git a/internal/lsp/testdata/inlayHint/a.go b/internal/lsp/testdata/inlayHint/a.go deleted file mode 100644 index 90ef7c41d1d..00000000000 --- a/internal/lsp/testdata/inlayHint/a.go +++ /dev/null @@ -1,9 +0,0 @@ -package inlayHint //@inlayHint("package") - -func hello(name string) string { - return "Hello " + name -} - -func helloWorld() string { - return hello("World") -} diff --git a/internal/lsp/testdata/inlayHint/a.go.golden b/internal/lsp/testdata/inlayHint/a.go.golden deleted file mode 100644 index e4e6cc0c0cc..00000000000 --- a/internal/lsp/testdata/inlayHint/a.go.golden +++ /dev/null @@ -1,11 +0,0 @@ --- inlayHint -- -package inlayHint //@inlayHint("package") - -func hello(name string) string { - return "Hello " + name -} - -func helloWorld() string { - return hello("World") -} - diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index 81a5d399029..f2766a2e319 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -787,7 +787,6 @@ func Run(t *testing.T, tests Tests, data *Data) { }) t.Run("InlayHints", func(t *testing.T) { - t.Skip("Inlay Hints not yet implemented") t.Helper() for _, src := range data.InlayHints { t.Run(SpanName(src), func(t *testing.T) { From ebc084af8ba794babff1d58912b41608629acd72 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Mon, 13 Jun 2022 17:08:38 -0400 Subject: [PATCH 012/723] internal/lsp: add inlay hints count to test summary Change-Id: Ia74f4a43a114715a6011405bf70f9dfa269c3318 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411901 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim Run-TryBot: Suzy Mueller --- internal/lsp/testdata/summary.txt.golden | 1 + internal/lsp/testdata/summary_go1.18.txt.golden | 1 + internal/lsp/tests/tests.go | 1 + 3 files changed, 3 insertions(+) diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index 9e1d84d1d56..0247551f8b9 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -19,6 +19,7 @@ MethodExtractionCount = 6 DefinitionsCount = 95 TypeDefinitionsCount = 18 HighlightsCount = 69 +InlayHintsCount = 4 ReferencesCount = 27 RenamesCount = 41 PrepareRenamesCount = 7 diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 1c6ad922c36..28a2672db50 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -19,6 +19,7 @@ MethodExtractionCount = 6 DefinitionsCount = 108 TypeDefinitionsCount = 18 HighlightsCount = 69 +InlayHintsCount = 4 ReferencesCount = 27 RenamesCount = 48 PrepareRenamesCount = 7 diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index f2766a2e319..ec804e5e79e 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -984,6 +984,7 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "DefinitionsCount = %v\n", definitionCount) fmt.Fprintf(buf, "TypeDefinitionsCount = %v\n", typeDefinitionCount) fmt.Fprintf(buf, "HighlightsCount = %v\n", len(data.Highlights)) + fmt.Fprintf(buf, "InlayHintsCount = %v\n", len(data.InlayHints)) fmt.Fprintf(buf, "ReferencesCount = %v\n", len(data.References)) fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames)) fmt.Fprintf(buf, "PrepareRenamesCount = %v\n", len(data.PrepareRenames)) From ed276111079290e7e1b77db2343972dac95c4bc9 Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Mon, 13 Jun 2022 19:52:45 +0000 Subject: [PATCH 013/723] internal/lsp/cache: cache known subdirs pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Known subdirs change rarely and it's quite expensive to compute a glob pattern derived from them, so cache computation result and inherit it across generations. Computation cost is divided ≈evenly between `sort.Sort` and `span.URI.Filename` calls, and there is no trivial way to optimize them away besides caching. Benchmark (didChange in kubernetes): ~37ms->30ms Change-Id: Idb1691c76b8ff163dc61f637f07229498888606c GitHub-Last-Rev: cd99a9ce5c797afb5aaa9b478fcf433edd0dc03c GitHub-Pull-Request: golang/tools#383 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411636 Reviewed-by: Alan Donovan Reviewed-by: Hyang-Ah Hana Kim --- internal/lsp/cache/snapshot.go | 56 +++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 6edd1dbe658..7b73f4b2794 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -114,7 +114,8 @@ type snapshot struct { // knownSubdirs is the set of subdirectories in the workspace, used to // create glob patterns for file watching. - knownSubdirs map[span.URI]struct{} + knownSubdirs map[span.URI]struct{} + knownSubdirsPatternCache string // unprocessedSubdirChanges are any changes that might affect the set of // subdirectories in the workspace. They are not reflected to knownSubdirs // during the snapshot cloning step as it can slow down cloning. @@ -834,19 +835,36 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru // of the directories in the workspace. We find them by adding the // directories of every file in the snapshot's workspace directories. // There may be thousands. - knownSubdirs := s.getKnownSubdirs(dirs) - if n := len(knownSubdirs); n > 0 { - dirNames := make([]string, 0, n) - for _, uri := range knownSubdirs { + if pattern := s.getKnownSubdirsPattern(dirs); pattern != "" { + patterns[pattern] = struct{}{} + } + + return patterns +} + +func (s *snapshot) getKnownSubdirsPattern(wsDirs []span.URI) string { + s.mu.Lock() + defer s.mu.Unlock() + + // First, process any pending changes and update the set of known + // subdirectories. + // It may change list of known subdirs and therefore invalidate the cache. + s.applyKnownSubdirsChangesLocked(wsDirs) + + if len(s.knownSubdirs) == 0 { + return "" + } + + if s.knownSubdirsPatternCache == "" { + dirNames := make([]string, 0, len(s.knownSubdirs)) + for uri := range s.knownSubdirs { dirNames = append(dirNames, uri.Filename()) } sort.Strings(dirNames) - // The double allocation of Sprintf(Join()) accounts for 8% - // of DidChange, but specializing doesn't appear to help. :( - patterns[fmt.Sprintf("{%s}", strings.Join(dirNames, ","))] = struct{}{} + s.knownSubdirsPatternCache = fmt.Sprintf("{%s}", strings.Join(dirNames, ",")) } - return patterns + return s.knownSubdirsPatternCache } // collectAllKnownSubdirs collects all of the subdirectories within the @@ -859,6 +877,7 @@ func (s *snapshot) collectAllKnownSubdirs(ctx context.Context) { defer s.mu.Unlock() s.knownSubdirs = map[span.URI]struct{}{} + s.knownSubdirsPatternCache = "" for uri := range s.files { s.addKnownSubdirLocked(uri, dirs) } @@ -870,6 +889,16 @@ func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) []span.URI { // First, process any pending changes and update the set of known // subdirectories. + s.applyKnownSubdirsChangesLocked(wsDirs) + + result := make([]span.URI, 0, len(s.knownSubdirs)) + for uri := range s.knownSubdirs { + result = append(result, uri) + } + return result +} + +func (s *snapshot) applyKnownSubdirsChangesLocked(wsDirs []span.URI) { for _, c := range s.unprocessedSubdirChanges { if c.isUnchanged { continue @@ -881,12 +910,6 @@ func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) []span.URI { } } s.unprocessedSubdirChanges = nil - - result := make([]span.URI, 0, len(s.knownSubdirs)) - for uri := range s.knownSubdirs { - result = append(result, uri) - } - return result } func (s *snapshot) addKnownSubdirLocked(uri span.URI, dirs []span.URI) { @@ -917,6 +940,7 @@ func (s *snapshot) addKnownSubdirLocked(uri span.URI, dirs []span.URI) { } s.knownSubdirs[uri] = struct{}{} dir = filepath.Dir(dir) + s.knownSubdirsPatternCache = "" } } @@ -929,6 +953,7 @@ func (s *snapshot) removeKnownSubdirLocked(uri span.URI) { } if info, _ := os.Stat(dir); info == nil { delete(s.knownSubdirs, uri) + s.knownSubdirsPatternCache = "" } dir = filepath.Dir(dir) } @@ -1816,6 +1841,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC for k, v := range s.knownSubdirs { result.knownSubdirs[k] = v } + result.knownSubdirsPatternCache = s.knownSubdirsPatternCache for _, c := range changes { result.unprocessedSubdirChanges = append(result.unprocessedSubdirChanges, c) } From c993be69238f2ed9aca41100e9ac8de62cf502a7 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Mon, 6 Jun 2022 18:50:18 -0400 Subject: [PATCH 014/723] go/analysis/internal/checker: log codeFact error, remove unused action.inputs Change-Id: I39ac785ed7666a5a1373443a2f56a1742a8c0858 Reviewed-on: https://go-review.googlesource.com/c/tools/+/410368 Reviewed-by: Alan Donovan --- go/analysis/internal/checker/checker.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index 51cbf689ac0..b5148ff26db 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -578,7 +578,6 @@ type action struct { deps []*action objectFacts map[objectFactKey]analysis.Fact packageFacts map[packageFactKey]analysis.Fact - inputs map[*analysis.Analyzer]interface{} result interface{} diagnostics []analysis.Diagnostic err error @@ -766,7 +765,7 @@ func inheritFacts(act, dep *action) { if serialize { encodedFact, err := codeFact(fact) if err != nil { - log.Panicf("internal error: encoding of %T fact failed in %v", fact, act) + log.Panicf("internal error: encoding of %T fact failed in %v: %v", fact, act, err) } fact = encodedFact } @@ -894,7 +893,7 @@ func (act *action) exportObjectFact(obj types.Object, fact analysis.Fact) { func (act *action) allObjectFacts() []analysis.ObjectFact { facts := make([]analysis.ObjectFact, 0, len(act.objectFacts)) for k := range act.objectFacts { - facts = append(facts, analysis.ObjectFact{k.obj, act.objectFacts[k]}) + facts = append(facts, analysis.ObjectFact{Object: k.obj, Fact: act.objectFacts[k]}) } return facts } @@ -940,7 +939,7 @@ func factType(fact analysis.Fact) reflect.Type { func (act *action) allPackageFacts() []analysis.PackageFact { facts := make([]analysis.PackageFact, 0, len(act.packageFacts)) for k := range act.packageFacts { - facts = append(facts, analysis.PackageFact{k.pkg, act.packageFacts[k]}) + facts = append(facts, analysis.PackageFact{Package: k.pkg, Fact: act.packageFacts[k]}) } return facts } From 27db7f40b912ea6504f06b93c1776b67550729c4 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 14 Jun 2022 11:11:56 -0400 Subject: [PATCH 015/723] gopls: update golang.org/x/vuln to latest @4eb5ba4 This picks up the recent performance improvement work in vulncheck api like https://go-review.googlesource.com/c/vuln/+/410897 and fix like https://go-review.googlesource.com/c/vuln/+/411354 Change-Id: Ie595fdb14ae27bd18b5cdd69ca9977d7c14d384c Reviewed-on: https://go-review.googlesource.com/c/tools/+/411908 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Jonathan Amsterdam --- gopls/go.mod | 4 ++-- gopls/go.sum | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gopls/go.mod b/gopls/go.mod index 85fb4301c02..5dc62d3df0a 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -9,8 +9,8 @@ require ( github.com/sergi/go-diff v1.1.0 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 golang.org/x/sys v0.0.0-20220209214540-3681064d5158 - golang.org/x/tools v0.1.11-0.20220330174940-8e193c2ba95e - golang.org/x/vuln v0.0.0-20220503210553-a5481fb0c8be + golang.org/x/tools v0.1.11-0.20220523181440-ccb10502d1a5 + golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c honnef.co/go/tools v0.3.0 mvdan.cc/gofumpt v0.3.0 mvdan.cc/xurls/v2 v2.4.0 diff --git a/gopls/go.sum b/gopls/go.sum index 5873afa1968..91f552ef905 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -73,6 +73,8 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/vuln v0.0.0-20220503210553-a5481fb0c8be h1:jokAF1mfylAi1iTQx7C44B7vyXUcSEMw8eDv0PzNu8s= golang.org/x/vuln v0.0.0-20220503210553-a5481fb0c8be/go.mod h1:twca1SxmF6/i2wHY/mj1vLIkkHdp+nil/yA32ZOP4kg= +golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c h1:r5bbIROBQtRRgoutV8Q3sFY58VzHW6jMBYl48ANSyS4= +golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c/go.mod h1:UZshlUPxXeGUM9I14UOawXQg6yosDE9cr1vKY/DzgWo= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= From 654a14b5274602698564a5e9710c0778be664c7a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 14 Jun 2022 14:15:04 -0400 Subject: [PATCH 016/723] internal/lsp/cache: reduce critical sections This change reduces the sizes of the critical sections in traces.ProcessEvent and Generation.Bind, in particular moving allocations ahead of Lock. This reduces the contention according to the trace profiler. See https://go-review.googlesource.com/c/go/+/411909 for another reduction in contention. The largest remaining contention is Handle.Get, which thousands of goroutines wait for because we initiate typechecking top down. Also, add a couple of possible optimization TODO comments, and delete a stale comment re: Bind. Change-Id: I995a0bb46e8c9bf0c23492fb62b56f4539bc32f8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411910 Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim --- internal/lsp/cache/check.go | 16 +++++++-------- internal/lsp/cache/parse.go | 2 +- internal/lsp/debug/trace.go | 39 ++++++++++++++++++++++--------------- internal/memoize/memoize.go | 6 +++++- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index f09fc298a98..51d7d1a7ea1 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -97,14 +97,6 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so return nil, err } - // Do not close over the packageHandle or the snapshot in the Bind function. - // This creates a cycle, which causes the finalizers to never run on the handles. - // The possible cycles are: - // - // packageHandle.h.function -> packageHandle - // packageHandle.h.function -> snapshot -> packageHandle - // - m := ph.m key := ph.key @@ -121,6 +113,13 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so }(dep) } + // TODO(adonovan): opt: consider moving the Wait here, + // so that dependencies complete before we start to + // read+parse+typecheck this package. Although the + // read+parse can proceed, typechecking will block + // almost immediately until the imports are done. + // The effect is to increase contention. + data := &packageData{} data.pkg, data.err = typeCheck(ctx, snapshot, m.Metadata, mode, deps) // Make sure that the workers above have finished before we return, @@ -448,6 +447,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour } typeparams.InitInstanceInfo(pkg.typesInfo) + // TODO(adonovan): opt: execute this loop in parallel. for _, gf := range pkg.m.GoFiles { // In the presence of line directives, we may need to report errors in // non-compiled Go files, so we need to register them on the package. diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index 668c437f5c9..ab55743ccf0 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -278,7 +278,7 @@ func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mod tok := fset.File(file.Pos()) if tok == nil { - // file.Pos is the location of the package declaration. If there was + // file.Pos is the location of the package declaration (issue #53202). If there was // none, we can't find the token.File that ParseFile created, and we // have no choice but to recreate it. tok = fset.AddFile(fh.URI().Filename(), -1, len(src)) diff --git a/internal/lsp/debug/trace.go b/internal/lsp/debug/trace.go index ca612867a5d..bb402cfaa8f 100644 --- a/internal/lsp/debug/trace.go +++ b/internal/lsp/debug/trace.go @@ -119,8 +119,6 @@ func formatEvent(ctx context.Context, ev core.Event, lm label.Map) string { } func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context { - t.mu.Lock() - defer t.mu.Unlock() span := export.GetSpan(ctx) if span == nil { return ctx @@ -128,11 +126,8 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) switch { case event.IsStart(ev): - if t.sets == nil { - t.sets = make(map[string]*traceSet) - t.unfinished = make(map[export.SpanContext]*traceData) - } - // just starting, add it to the unfinished map + // Just starting: add it to the unfinished map. + // Allocate before the critical section. td := &traceData{ TraceID: span.ID.TraceID, SpanID: span.ID.SpanID, @@ -141,6 +136,13 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) Start: span.Start().At(), Tags: renderLabels(span.Start()), } + + t.mu.Lock() + defer t.mu.Unlock() + if t.sets == nil { + t.sets = make(map[string]*traceSet) + t.unfinished = make(map[export.SpanContext]*traceData) + } t.unfinished[span.ID] = td // and wire up parents if we have them if !span.ParentID.IsValid() { @@ -155,7 +157,19 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) parent.Children = append(parent.Children, td) case event.IsEnd(ev): - // finishing, must be already in the map + // Finishing: must be already in the map. + // Allocate events before the critical section. + events := span.Events() + tdEvents := make([]traceEvent, len(events)) + for i, event := range events { + tdEvents[i] = traceEvent{ + Time: event.At(), + Tags: renderLabels(event), + } + } + + t.mu.Lock() + defer t.mu.Unlock() td, found := t.unfinished[span.ID] if !found { return ctx // if this happens we are in a bad place @@ -164,14 +178,7 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) td.Finish = span.Finish().At() td.Duration = span.Finish().At().Sub(span.Start().At()) - events := span.Events() - td.Events = make([]traceEvent, len(events)) - for i, event := range events { - td.Events[i] = traceEvent{ - Time: event.At(), - Tags: renderLabels(event), - } - } + td.Events = tdEvents set, ok := t.sets[span.Name] if !ok { diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index dec2fff6836..28d5e2c5bc8 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -181,8 +181,9 @@ func (g *Generation) Bind(key interface{}, function Function, cleanup func(inter if atomic.LoadUint32(&g.destroyed) != 0 { panic("operation on generation " + g.name + " destroyed by " + g.destroyedBy) } + + // Avoid 'defer Unlock' to reduce critical section. g.store.mu.Lock() - defer g.store.mu.Unlock() h, ok := g.store.handles[key] if !ok { h := &Handle{ @@ -192,8 +193,11 @@ func (g *Generation) Bind(key interface{}, function Function, cleanup func(inter cleanup: cleanup, } g.store.handles[key] = h + g.store.mu.Unlock() return h } + g.store.mu.Unlock() + h.mu.Lock() defer h.mu.Unlock() if _, ok := h.generations[g]; !ok { From e8b9ff129187b07b15cd001e4c72ae64a21221f1 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 14 Jun 2022 19:31:01 -0400 Subject: [PATCH 017/723] gopls/internal/govulncheck: sync x/vuln@4eb5ba4 Change-Id: Idf2147684626368116a5330fefb0a63d8c82f7a9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/412456 Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot --- gopls/internal/govulncheck/source.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/gopls/internal/govulncheck/source.go b/gopls/internal/govulncheck/source.go index 752a8313091..23028b9eb42 100644 --- a/gopls/internal/govulncheck/source.go +++ b/gopls/internal/govulncheck/source.go @@ -59,6 +59,8 @@ func LoadPackages(cfg *packages.Config, patterns ...string) ([]*vulncheck.Packag // Source calls vulncheck.Source on the Go source in pkgs. It returns the result // with Vulns trimmed to those that are actually called. +// +// This function is being used by the Go IDE team. func Source(ctx context.Context, pkgs []*vulncheck.Package, c client.Client) (*vulncheck.Result, error) { r, err := vulncheck.Source(ctx, pkgs, &vulncheck.Config{Client: c}) if err != nil { @@ -77,14 +79,21 @@ func Source(ctx context.Context, pkgs []*vulncheck.Package, c client.Client) (*v // CallInfo is information about calls to vulnerable functions. type CallInfo struct { - CallStacks map[*vulncheck.Vuln][]vulncheck.CallStack // all call stacks - VulnGroups [][]*vulncheck.Vuln // vulns grouped by ID and package - ModuleVersions map[string]string // map from module paths to versions - TopPackages map[string]bool // top-level packages + // CallStacks contains all call stacks to vulnerable functions. + CallStacks map[*vulncheck.Vuln][]vulncheck.CallStack + + // VulnGroups contains vulnerabilities grouped by ID and package. + VulnGroups [][]*vulncheck.Vuln + + // ModuleVersions is a map of module paths to versions. + ModuleVersions map[string]string + + // TopPackages contains the top-level packages in the call info. + TopPackages map[string]bool } // GetCallInfo computes call stacks and related information from a vulncheck.Result. -// I also makes a set of top-level packages from pkgs. +// It also makes a set of top-level packages from pkgs. func GetCallInfo(r *vulncheck.Result, pkgs []*vulncheck.Package) *CallInfo { pset := map[string]bool{} for _, p := range pkgs { From d097bc9f9d0168a89c8d85f5e2110d54bcbab78e Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 15 Jun 2022 18:51:42 -0400 Subject: [PATCH 018/723] gopls/internal/vulncheck: include nonaffecting vulnerability info This info is still useful to tell users that some required modules have known vulnerabilities, but the analyzed packages/workspaces are not affected. Those vulnerabilities are missing Symbol/PkgPath/CallStacks. Change-Id: I94ea0d8f9ebcb1270e05f055caff2a18ebacd034 Reviewed-on: https://go-review.googlesource.com/c/tools/+/412457 Reviewed-by: Jonathan Amsterdam --- gopls/internal/vulncheck/command.go | 88 +++++++++++++++++++++--- gopls/internal/vulncheck/command_test.go | 24 +++++++ internal/lsp/command/interface.go | 2 + 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index a89354f67ee..53bf0f03860 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -11,12 +11,15 @@ import ( "context" "log" "os" + "sort" "strings" "golang.org/x/tools/go/packages" gvc "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/internal/lsp/command" "golang.org/x/vuln/client" + "golang.org/x/vuln/osv" + "golang.org/x/vuln/vulncheck" ) func init() { @@ -79,29 +82,84 @@ func (c *cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) } log.Printf("loaded %d packages\n", len(loadedPkgs)) - r, err := gvc.Source(ctx, loadedPkgs, c.Client) + log.Printf("analyzing %d packages...\n", len(loadedPkgs)) + + r, err := vulncheck.Source(ctx, loadedPkgs, &vulncheck.Config{Client: c.Client}) if err != nil { return nil, err } + unaffectedMods := filterUnaffected(r.Vulns) + r.Vulns = filterCalled(r) + callInfo := gvc.GetCallInfo(r, loadedPkgs) - return toVulns(callInfo) + return toVulns(callInfo, unaffectedMods) // TODO: add import graphs. } -func toVulns(ci *gvc.CallInfo) ([]Vuln, error) { +// filterCalled returns vulnerabilities where the symbols are actually called. +func filterCalled(r *vulncheck.Result) []*vulncheck.Vuln { + var vulns []*vulncheck.Vuln + for _, v := range r.Vulns { + if v.CallSink != 0 { + vulns = append(vulns, v) + } + } + return vulns +} + +// filterUnaffected returns vulnerabilities where no symbols are called, +// grouped by module. +func filterUnaffected(vulns []*vulncheck.Vuln) map[string][]*osv.Entry { + // It is possible that the same vuln.OSV.ID has vuln.CallSink != 0 + // for one symbol, but vuln.CallSink == 0 for a different one, so + // we need to filter out ones that have been called. + called := map[string]bool{} + for _, vuln := range vulns { + if vuln.CallSink != 0 { + called[vuln.OSV.ID] = true + } + } + + modToIDs := map[string]map[string]*osv.Entry{} + for _, vuln := range vulns { + if !called[vuln.OSV.ID] { + if _, ok := modToIDs[vuln.ModPath]; !ok { + modToIDs[vuln.ModPath] = map[string]*osv.Entry{} + } + // keep only one vuln.OSV instance for the same ID. + modToIDs[vuln.ModPath][vuln.OSV.ID] = vuln.OSV + } + } + output := map[string][]*osv.Entry{} + for m, vulnSet := range modToIDs { + var vulns []*osv.Entry + for _, vuln := range vulnSet { + vulns = append(vulns, vuln) + } + sort.Slice(vulns, func(i, j int) bool { return vulns[i].ID < vulns[j].ID }) + output[m] = vulns + } + return output +} + +func fixed(v *osv.Entry) string { + lf := gvc.LatestFixed(v.Affected) + if lf != "" && lf[0] != 'v' { + lf = "v" + lf + } + return lf +} + +func toVulns(ci *gvc.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, error) { var vulns []Vuln for _, vg := range ci.VulnGroups { v0 := vg[0] - lf := gvc.LatestFixed(v0.OSV.Affected) - if lf != "" && lf[0] != 'v' { - lf = "v" + lf - } vuln := Vuln{ ID: v0.OSV.ID, PkgPath: v0.PkgPath, CurrentVersion: ci.ModuleVersions[v0.ModPath], - FixedVersion: lf, + FixedVersion: fixed(v0.OSV), Details: v0.OSV.Details, Aliases: v0.OSV.Aliases, @@ -119,5 +177,19 @@ func toVulns(ci *gvc.CallInfo) ([]Vuln, error) { } vulns = append(vulns, vuln) } + for m, vg := range unaffectedMods { + for _, v0 := range vg { + vuln := Vuln{ + ID: v0.ID, + Details: v0.Details, + Aliases: v0.Aliases, + ModPath: m, + URL: href(v0), + CurrentVersion: "", + FixedVersion: fixed(v0), + } + vulns = append(vulns, vuln) + } + } return vulns, nil } diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index f689ab96722..f6e2d1b7612 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -81,6 +81,15 @@ func TestCmd_Run(t *testing.T) { "golang.org/bmod/bvuln.Vuln (bvuln.go:2)\n", }, }, + { + Vuln: Vuln{ + ID: "GO-2022-03", + Details: "unaffecting vulnerability", + ModPath: "golang.org/amod", + URL: "https://pkg.go.dev/vuln/GO-2022-03", + FixedVersion: "v1.0.4", + }, + }, } // sort reports for stability before comparison. for _, rpts := range [][]report{got, want} { @@ -228,6 +237,21 @@ var testClient1 = &mockClient{ EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"VulnData.Vuln1", "VulnData.Vuln2"}}, }}, }, + { + ID: "GO-2022-03", + Details: "unaffecting vulnerability", + References: []osv.Reference{ + { + Type: "href", + URL: "pkg.go.dev/vuln/GO-2022-01", + }, + }, + Affected: []osv.Affected{{ + Package: osv.Package{Name: "golang.org/amod/avuln"}, + Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.0.4"}, {Introduced: "1.1.2"}}}}, + EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"nonExisting"}}, + }}, + }, }, "golang.org/bmod": { { diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go index 8e4b1056d32..1f3b092faba 100644 --- a/internal/lsp/command/interface.go +++ b/internal/lsp/command/interface.go @@ -359,8 +359,10 @@ type Vuln struct { Aliases []string `json:",omitempty"` // Symbol is the name of the detected vulnerable function or method. + // Can be empty if the vulnerability exists in required modules, but no vulnerable symbols are used. Symbol string `json:",omitempty"` // PkgPath is the package path of the detected Symbol. + // Can be empty if the vulnerability exists in required modules, but no vulnerable packages are used. PkgPath string `json:",omitempty"` // ModPath is the module path corresponding to PkgPath. // TODO: how do we specify standard library's vulnerability? From 041035c34a090a6505169f0dbb856b7ea9ce2ffa Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 16 Jun 2022 13:41:08 +0000 Subject: [PATCH 019/723] Revert "internal/lsp/cache: reduce critical sections" This reverts commit 654a14b5274602698564a5e9710c0778be664c7a. Reason for revert: my flawed understanding of the concurrency Change-Id: I31a35267323bb1ff4dff1d9244d3ce69c36cdda4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/412694 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro --- internal/lsp/cache/check.go | 16 +++++++-------- internal/lsp/cache/parse.go | 2 +- internal/lsp/debug/trace.go | 39 +++++++++++++++---------------------- internal/memoize/memoize.go | 6 +----- 4 files changed, 26 insertions(+), 37 deletions(-) diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index 51d7d1a7ea1..f09fc298a98 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -97,6 +97,14 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so return nil, err } + // Do not close over the packageHandle or the snapshot in the Bind function. + // This creates a cycle, which causes the finalizers to never run on the handles. + // The possible cycles are: + // + // packageHandle.h.function -> packageHandle + // packageHandle.h.function -> snapshot -> packageHandle + // + m := ph.m key := ph.key @@ -113,13 +121,6 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so }(dep) } - // TODO(adonovan): opt: consider moving the Wait here, - // so that dependencies complete before we start to - // read+parse+typecheck this package. Although the - // read+parse can proceed, typechecking will block - // almost immediately until the imports are done. - // The effect is to increase contention. - data := &packageData{} data.pkg, data.err = typeCheck(ctx, snapshot, m.Metadata, mode, deps) // Make sure that the workers above have finished before we return, @@ -447,7 +448,6 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour } typeparams.InitInstanceInfo(pkg.typesInfo) - // TODO(adonovan): opt: execute this loop in parallel. for _, gf := range pkg.m.GoFiles { // In the presence of line directives, we may need to report errors in // non-compiled Go files, so we need to register them on the package. diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index ab55743ccf0..668c437f5c9 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -278,7 +278,7 @@ func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mod tok := fset.File(file.Pos()) if tok == nil { - // file.Pos is the location of the package declaration (issue #53202). If there was + // file.Pos is the location of the package declaration. If there was // none, we can't find the token.File that ParseFile created, and we // have no choice but to recreate it. tok = fset.AddFile(fh.URI().Filename(), -1, len(src)) diff --git a/internal/lsp/debug/trace.go b/internal/lsp/debug/trace.go index bb402cfaa8f..ca612867a5d 100644 --- a/internal/lsp/debug/trace.go +++ b/internal/lsp/debug/trace.go @@ -119,6 +119,8 @@ func formatEvent(ctx context.Context, ev core.Event, lm label.Map) string { } func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context { + t.mu.Lock() + defer t.mu.Unlock() span := export.GetSpan(ctx) if span == nil { return ctx @@ -126,8 +128,11 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) switch { case event.IsStart(ev): - // Just starting: add it to the unfinished map. - // Allocate before the critical section. + if t.sets == nil { + t.sets = make(map[string]*traceSet) + t.unfinished = make(map[export.SpanContext]*traceData) + } + // just starting, add it to the unfinished map td := &traceData{ TraceID: span.ID.TraceID, SpanID: span.ID.SpanID, @@ -136,13 +141,6 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) Start: span.Start().At(), Tags: renderLabels(span.Start()), } - - t.mu.Lock() - defer t.mu.Unlock() - if t.sets == nil { - t.sets = make(map[string]*traceSet) - t.unfinished = make(map[export.SpanContext]*traceData) - } t.unfinished[span.ID] = td // and wire up parents if we have them if !span.ParentID.IsValid() { @@ -157,19 +155,7 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) parent.Children = append(parent.Children, td) case event.IsEnd(ev): - // Finishing: must be already in the map. - // Allocate events before the critical section. - events := span.Events() - tdEvents := make([]traceEvent, len(events)) - for i, event := range events { - tdEvents[i] = traceEvent{ - Time: event.At(), - Tags: renderLabels(event), - } - } - - t.mu.Lock() - defer t.mu.Unlock() + // finishing, must be already in the map td, found := t.unfinished[span.ID] if !found { return ctx // if this happens we are in a bad place @@ -178,7 +164,14 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) td.Finish = span.Finish().At() td.Duration = span.Finish().At().Sub(span.Start().At()) - td.Events = tdEvents + events := span.Events() + td.Events = make([]traceEvent, len(events)) + for i, event := range events { + td.Events[i] = traceEvent{ + Time: event.At(), + Tags: renderLabels(event), + } + } set, ok := t.sets[span.Name] if !ok { diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 28d5e2c5bc8..dec2fff6836 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -181,9 +181,8 @@ func (g *Generation) Bind(key interface{}, function Function, cleanup func(inter if atomic.LoadUint32(&g.destroyed) != 0 { panic("operation on generation " + g.name + " destroyed by " + g.destroyedBy) } - - // Avoid 'defer Unlock' to reduce critical section. g.store.mu.Lock() + defer g.store.mu.Unlock() h, ok := g.store.handles[key] if !ok { h := &Handle{ @@ -193,11 +192,8 @@ func (g *Generation) Bind(key interface{}, function Function, cleanup func(inter cleanup: cleanup, } g.store.handles[key] = h - g.store.mu.Unlock() return h } - g.store.mu.Unlock() - h.mu.Lock() defer h.mu.Unlock() if _, ok := h.generations[g]; !ok { From 9f38ef7f15ed12bf87e5d5e13817f6a0a6ffd9cf Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 6 Aug 2021 10:50:53 -0400 Subject: [PATCH 020/723] internal/lsp/cache: derive workspace packages from metadata Now that we preserve stale metadata, we can derive workspace packages entirely from known metadata and files. This consolidates the logic to compute workspace packages into a single location, which can be invoked whenever metadata changes (via load or invalidation in clone). Additionally: - Precompute 'HasWorkspaceFiles' when loading metadata. This value should never change for a given Metadata, and our view.contains func is actually quite slow due to evaluating symlinks. - Track 'PkgFilesChanged' on KnownMetadata, since we don't include packages whose package name has changed in our workspace. Also introduce a few debug helpers, so that we can leave some instrumentation in critical functions. For golang/go#45686 Change-Id: I2c994a1e8ca05c3c42f67bd2f4519bea5095c54c Reviewed-on: https://go-review.googlesource.com/c/tools/+/340735 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- internal/lsp/cache/debug.go | 53 ++++++++++++++++++++ internal/lsp/cache/load.go | 88 ++++++++++++++++++++++++++++------ internal/lsp/cache/metadata.go | 15 ++++++ internal/lsp/cache/snapshot.go | 60 +++++------------------ 4 files changed, 153 insertions(+), 63 deletions(-) create mode 100644 internal/lsp/cache/debug.go diff --git a/internal/lsp/cache/debug.go b/internal/lsp/cache/debug.go new file mode 100644 index 00000000000..b8d207d83dd --- /dev/null +++ b/internal/lsp/cache/debug.go @@ -0,0 +1,53 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "fmt" + "os" + "sort" +) + +// This file contains helpers that can be used to instrument code while +// debugging. + +// debugEnabled toggles the helpers below. +const debugEnabled = false + +// If debugEnabled is true, debugf formats its arguments and prints to stderr. +// If debugEnabled is false, it is a no-op. +func debugf(format string, args ...interface{}) { + if !debugEnabled { + return + } + if false { + fmt.Sprintf(format, args...) // encourage vet to validate format strings + } + fmt.Fprintf(os.Stderr, ">>> "+format+"\n", args...) +} + +// If debugEnabled is true, dumpWorkspace prints a summary of workspace +// packages to stderr. If debugEnabled is false, it is a no-op. +func (s *snapshot) dumpWorkspace(context string) { + if !debugEnabled { + return + } + + debugf("workspace (after %s):", context) + var ids []PackageID + for id := range s.workspacePackages { + ids = append(ids, id) + } + + sort.Slice(ids, func(i, j int) bool { + return ids[i] < ids[j] + }) + + for _, id := range ids { + pkgPath := s.workspacePackages[id] + _, ok := s.meta.metadata[id] + debugf(" %s:%s (metadata: %t)", id, pkgPath, ok) + } +} diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 96c2a0733a5..7b41d244829 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -149,6 +149,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } moduleErrs := make(map[string][]packages.Error) // module path -> errors + var newMetadata []*Metadata for _, pkg := range pkgs { // The Go command returns synthetic list results for module queries that // encountered module errors. @@ -195,17 +196,28 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } // Set the metadata for this package. s.mu.Lock() - m, err := s.setMetadataLocked(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, map[PackageID]struct{}{}) + seen := make(map[PackageID]struct{}) + m, err := s.setMetadataLocked(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, seen) s.mu.Unlock() if err != nil { return err } + newMetadata = append(newMetadata, m) + } + + // Rebuild package data when metadata is updated. + s.rebuildPackageData() + s.dumpWorkspace("load") + + // Now that the workspace has been rebuilt, verify that we can build package handles. + // + // TODO(rfindley): what's the point of returning an error here? Probably we + // can simply remove this step: The package handle will be rebuilt as needed. + for _, m := range newMetadata { if _, err := s.buildPackageHandle(ctx, m.ID, s.workspaceParseMode(m.ID)); err != nil { return err } } - // Rebuild the import graph when the metadata is updated. - s.clearAndRebuildImportGraph() if len(moduleErrs) > 0 { return &moduleErrorMap{moduleErrs} @@ -468,6 +480,19 @@ func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, p m.GoFiles = append(m.GoFiles, uri) uris[uri] = struct{}{} } + + for uri := range uris { + // In order for a package to be considered for the workspace, at least one + // file must be contained in the workspace and not vendored. + + // The package's files are in this view. It may be a workspace package. + // Vendored packages are not likely to be interesting to the user. + if !strings.Contains(string(uri), "/vendor/") && s.view.contains(uri) { + m.HasWorkspaceFiles = true + break + } + } + s.updateIDForURIsLocked(id, uris) // TODO(rstambler): is this still necessary? @@ -517,30 +542,65 @@ func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, p } } - // Set the workspace packages. If any of the package's files belong to the - // view, then the package may be a workspace package. - for _, uri := range append(m.CompiledGoFiles, m.GoFiles...) { - if !s.view.contains(uri) { + return m, nil +} + +// computeWorkspacePackages computes workspace packages for the given metadata +// graph. +func computeWorkspacePackages(meta *metadataGraph) map[PackageID]PackagePath { + workspacePackages := make(map[PackageID]PackagePath) + for _, m := range meta.metadata { + if !m.HasWorkspaceFiles { continue } - - // The package's files are in this view. It may be a workspace package. - if strings.Contains(string(uri), "/vendor/") { - // Vendored packages are not likely to be interesting to the user. + if m.PkgFilesChanged { + // If a package name has changed, it's possible that the package no + // longer exists. Leaving it as a workspace package can result in + // persistent stale diagnostics. + // + // If there are still valid files in the package, it will be reloaded. + // + // There may be more precise heuristics. continue } + if source.IsCommandLineArguments(string(m.ID)) { + // If all the files contained in m have a real package, we don't need to + // keep m as a workspace package. + if allFilesHaveRealPackages(meta, m) { + continue + } + } + switch { case m.ForTest == "": // A normal package. - s.workspacePackages[m.ID] = pkgPath + workspacePackages[m.ID] = m.PkgPath case m.ForTest == m.PkgPath, m.ForTest+"_test" == m.PkgPath: // The test variant of some workspace package or its x_test. // To load it, we need to load the non-test variant with -test. - s.workspacePackages[m.ID] = m.ForTest + workspacePackages[m.ID] = m.ForTest } } - return m, nil + return workspacePackages +} + +// allFilesHaveRealPackages reports whether all files referenced by m are +// contained in a "real" package (not command-line-arguments). +// +// If m is not a command-line-arguments package, this is trivially true. +func allFilesHaveRealPackages(g *metadataGraph, m *KnownMetadata) bool { + n := len(m.CompiledGoFiles) +checkURIs: + for _, uri := range append(m.CompiledGoFiles[0:n:n], m.GoFiles...) { + for _, id := range g.ids[uri] { + if !source.IsCommandLineArguments(string(id)) { + continue checkURIs + } + } + return false + } + return true } func isTestMain(pkg *packages.Package, gocache string) bool { diff --git a/internal/lsp/cache/metadata.go b/internal/lsp/cache/metadata.go index c2a21969d88..525a3e65495 100644 --- a/internal/lsp/cache/metadata.go +++ b/internal/lsp/cache/metadata.go @@ -67,6 +67,13 @@ type Metadata struct { // TODO(rfindley): this can probably just be a method, since it is derived // from other fields. IsIntermediateTestVariant bool + + // HasWorkspaceFiles reports whether m contains any files that are considered + // part of the workspace. + // + // TODO(golang/go#48929): this should be a property of the workspace + // (the go.work file), not a constant. + HasWorkspaceFiles bool } // Name implements the source.Metadata interface. @@ -92,6 +99,14 @@ type KnownMetadata struct { // Invalid metadata can still be used if a metadata reload fails. Valid bool + // PkgFilesChanged reports whether the file set of this metadata has + // potentially changed. + PkgFilesChanged bool + // ShouldLoad is true if the given metadata should be reloaded. + // + // Note that ShouldLoad is different from !Valid: when we try to load a + // package, we mark ShouldLoad = false regardless of whether the load + // succeeded, to prevent endless loads. ShouldLoad bool } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 7b73f4b2794..961a60f8aa6 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -716,13 +716,14 @@ func (s *snapshot) getImportedByLocked(id PackageID) []PackageID { return s.meta.importedBy[id] } -func (s *snapshot) clearAndRebuildImportGraph() { +func (s *snapshot) rebuildPackageData() { s.mu.Lock() defer s.mu.Unlock() // Completely invalidate the original map. s.meta.importedBy = make(map[PackageID][]PackageID) s.rebuildImportGraph() + s.workspacePackages = computeWorkspacePackages(s.meta) } func (s *snapshot) rebuildImportGraph() { @@ -1337,6 +1338,7 @@ func (s *snapshot) updateIDForURIsLocked(id PackageID, uris map[span.URI]struct{ }) s.meta.ids[uri] = newIDs } + s.dumpWorkspace("updateIDs") } func (s *snapshot) isWorkspacePackage(id PackageID) bool { @@ -1857,7 +1859,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } - changedPkgFiles := map[PackageID]struct{}{} // packages whose file set may have changed + changedPkgFiles := map[PackageID]bool{} // packages whose file set may have changed anyImportDeleted := false for uri, change := range changes { // Maybe reinitialize the view if we see a change in the vendor @@ -1885,7 +1887,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC filePackageIDs := invalidatedPackageIDs(uri, s.meta.ids, pkgFileChanged) if pkgFileChanged { for id := range filePackageIDs { - changedPkgFiles[id] = struct{}{} + changedPkgFiles[id] = true } } for id := range filePackageIDs { @@ -2064,55 +2066,14 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC invalidateMetadata := idsToInvalidate[k] // Mark invalidated metadata rather than deleting it outright. result.meta.metadata[k] = &KnownMetadata{ - Metadata: v.Metadata, - Valid: v.Valid && !invalidateMetadata, - ShouldLoad: v.ShouldLoad || invalidateMetadata, + Metadata: v.Metadata, + Valid: v.Valid && !invalidateMetadata, + PkgFilesChanged: v.PkgFilesChanged || changedPkgFiles[k], + ShouldLoad: v.ShouldLoad || invalidateMetadata, } } - // Copy the set of initially loaded packages. - for id, pkgPath := range s.workspacePackages { - // Packages with the id "command-line-arguments" are generated by the - // go command when the user is outside of GOPATH and outside of a - // module. Do not cache them as workspace packages for longer than - // necessary. - if source.IsCommandLineArguments(string(id)) { - if invalidateMetadata, ok := idsToInvalidate[id]; invalidateMetadata && ok { - continue - } - } - - // If all the files we know about in a package have been deleted, - // the package is gone and we should no longer try to load it. - if m := s.meta.metadata[id]; m != nil { - hasFiles := false - for _, uri := range s.meta.metadata[id].GoFiles { - // For internal tests, we need _test files, not just the normal - // ones. External tests only have _test files, but we can check - // them anyway. - if m.ForTest != "" && !strings.HasSuffix(string(uri), "_test.go") { - continue - } - if _, ok := result.files[uri]; ok { - hasFiles = true - break - } - } - if !hasFiles { - continue - } - } - - // If the package name of a file in the package has changed, it's - // possible that the package ID may no longer exist. Delete it from - // the set of workspace packages, on the assumption that we will add it - // back when the relevant files are reloaded. - if _, ok := changedPkgFiles[id]; ok { - continue - } - - result.workspacePackages[id] = pkgPath - } + result.workspacePackages = computeWorkspacePackages(result.meta) // Inherit all of the go.mod-related handles. for _, v := range result.modTidyHandles { @@ -2142,6 +2103,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC result.initializeOnce = &sync.Once{} } } + result.dumpWorkspace("clone") return result } From 8a9207816c6df78e430bfeb90c41039dfeed6f34 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Sun, 8 Aug 2021 14:49:30 -0400 Subject: [PATCH 021/723] internal/lsp/cache: build a new metadata graph on load Introduce a metadataGraph.Clone method that can be used to clone a metadata graph, applying a set of updates. During clone, ids and imports are recomputed from scratch based on the known metadata. Also refine the check for "real" packages when determining whether a command-line-arguments package should be kept as a workspace package: if all other packages are invalid, but the command-line-arguments package is valid, we should keep the command-line-arguments package. Updates golang/go#45686 Change-Id: Iea8d4f19c1d1c5a2b0582b9dda5f9143482a34af Reviewed-on: https://go-review.googlesource.com/c/tools/+/340851 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley --- internal/lsp/cache/debug.go | 4 +- internal/lsp/cache/graph.go | 116 +++++++++++++++++++++++++++++++-- internal/lsp/cache/load.go | 105 +++++++++++++++-------------- internal/lsp/cache/snapshot.go | 47 +------------ 4 files changed, 166 insertions(+), 106 deletions(-) diff --git a/internal/lsp/cache/debug.go b/internal/lsp/cache/debug.go index b8d207d83dd..ca8b7c866e4 100644 --- a/internal/lsp/cache/debug.go +++ b/internal/lsp/cache/debug.go @@ -47,7 +47,7 @@ func (s *snapshot) dumpWorkspace(context string) { for _, id := range ids { pkgPath := s.workspacePackages[id] - _, ok := s.meta.metadata[id] - debugf(" %s:%s (metadata: %t)", id, pkgPath, ok) + m, ok := s.meta.metadata[id] + debugf(" %s:%s (metadata: %t; valid: %t)", id, pkgPath, ok, m.Valid) } } diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go index f0f8724d375..f3fe077cf5b 100644 --- a/internal/lsp/cache/graph.go +++ b/internal/lsp/cache/graph.go @@ -4,7 +4,12 @@ package cache -import "golang.org/x/tools/internal/span" +import ( + "sort" + + "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" +) // A metadataGraph holds information about a transtively closed import graph of // Go packages, as obtained from go/packages. @@ -13,21 +18,122 @@ import "golang.org/x/tools/internal/span" // TODO(rfindley): make this type immutable, so that it may be shared across // snapshots. type metadataGraph struct { - // ids maps file URIs to package IDs. A single file may belong to multiple - // packages due to tests packages. - ids map[span.URI][]PackageID // metadata maps package IDs to their associated metadata. metadata map[PackageID]*KnownMetadata // importedBy maps package IDs to the list of packages that import them. importedBy map[PackageID][]PackageID + + // ids maps file URIs to package IDs. A single file may belong to multiple + // packages due to tests packages. + ids map[span.URI][]PackageID } func NewMetadataGraph() *metadataGraph { return &metadataGraph{ - ids: make(map[span.URI][]PackageID), metadata: make(map[PackageID]*KnownMetadata), importedBy: make(map[PackageID][]PackageID), + ids: make(map[span.URI][]PackageID), + } +} + +// Clone creates a new metadataGraph, applying the given updates to the +// receiver. +func (g *metadataGraph) Clone(updates map[PackageID]*KnownMetadata) *metadataGraph { + result := &metadataGraph{metadata: make(map[PackageID]*KnownMetadata, len(g.metadata))} + // Copy metadata. + for id, m := range g.metadata { + result.metadata[id] = m + } + for id, m := range updates { + if m == nil { + delete(result.metadata, id) + } else { + result.metadata[id] = m + } + } + result.build() + return result +} + +// build constructs g.importedBy and g.uris from g.metadata. +func (g *metadataGraph) build() { + // Build the import graph. + g.importedBy = make(map[PackageID][]PackageID) + for id, m := range g.metadata { + for _, importID := range m.Deps { + g.importedBy[importID] = append(g.importedBy[importID], id) + } + } + + // Collect file associations. + g.ids = make(map[span.URI][]PackageID) + for id, m := range g.metadata { + uris := map[span.URI]struct{}{} + for _, uri := range m.CompiledGoFiles { + uris[uri] = struct{}{} + } + for _, uri := range m.GoFiles { + uris[uri] = struct{}{} + } + for uri := range uris { + g.ids[uri] = append(g.ids[uri], id) + } + } + + // Sort and filter file associations. + // + // We choose the first non-empty set of package associations out of the + // following. For simplicity, call a non-command-line-arguments package a + // "real" package. + // + // 1: valid real packages + // 2: a valid command-line-arguments package + // 3: invalid real packages + // 4: an invalid command-line-arguments package + for uri, ids := range g.ids { + sort.Slice(ids, func(i, j int) bool { + // Sort valid packages first. + validi := g.metadata[ids[i]].Valid + validj := g.metadata[ids[j]].Valid + if validi != validj { + return validi + } + + cli := source.IsCommandLineArguments(string(ids[i])) + clj := source.IsCommandLineArguments(string(ids[j])) + if cli && !clj { + return false + } + if !cli && clj { + return true + } + return ids[i] < ids[j] + }) + + // Choose the best IDs for each URI, according to the following rules: + // - If there are any valid real packages, choose them. + // - Else, choose the first valid command-line-argument package, if it exists. + // - Else, keep using all the invalid metadata. + // + // TODO(rfindley): it might be better to track all IDs here, and exclude + // them later in PackagesForFile, but this is the existing behavior. + hasValidMetadata := false + for i, id := range ids { + m := g.metadata[id] + if m.Valid { + hasValidMetadata = true + } else if hasValidMetadata { + g.ids[uri] = ids[:i] + break + } + // If we've seen *anything* prior to command-line arguments package, take + // it. Note that ids[0] may itself be command-line-arguments. + if i > 0 && source.IsCommandLineArguments(string(id)) { + g.ids[uri] = ids[:i] + break + } + } } } diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 7b41d244829..085c7c28001 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -149,7 +149,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } moduleErrs := make(map[string][]packages.Error) // module path -> errors - var newMetadata []*Metadata + updates := make(map[PackageID]*KnownMetadata) for _, pkg := range pkgs { // The Go command returns synthetic list results for module queries that // encountered module errors. @@ -194,26 +194,36 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf if s.view.allFilesExcluded(pkg) { continue } - // Set the metadata for this package. + // TODO: once metadata is immutable, we shouldn't have to lock here. s.mu.Lock() - seen := make(map[PackageID]struct{}) - m, err := s.setMetadataLocked(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, seen) + err := s.setMetadataLocked(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, updates, nil) s.mu.Unlock() if err != nil { return err } - newMetadata = append(newMetadata, m) } - // Rebuild package data when metadata is updated. - s.rebuildPackageData() + s.mu.Lock() + s.meta = s.meta.Clone(updates) + // Invalidate any packages we may have associated with this metadata. + // + // TODO(rfindley): if we didn't already invalidate these in snapshot.clone, + // shouldn't we invalidate the reverse transitive closure? + for _, m := range updates { + for _, mode := range []source.ParseMode{source.ParseHeader, source.ParseExported, source.ParseFull} { + key := packageKey{mode, m.ID} + delete(s.packages, key) + } + } + s.workspacePackages = computeWorkspacePackages(s.meta) s.dumpWorkspace("load") + s.mu.Unlock() - // Now that the workspace has been rebuilt, verify that we can build package handles. + // Rebuild the workspace package handle for any packages we invalidated. // // TODO(rfindley): what's the point of returning an error here? Probably we // can simply remove this step: The package handle will be rebuilt as needed. - for _, m := range newMetadata { + for _, m := range updates { if _, err := s.buildPackageHandle(ctx, m.ID, s.workspaceParseMode(m.ID)); err != nil { return err } @@ -431,27 +441,41 @@ func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generati // setMetadataLocked extracts metadata from pkg and records it in s. It // recurs through pkg.Imports to ensure that metadata exists for all // dependencies. -func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, seen map[PackageID]struct{}) (*Metadata, error) { +func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { id := PackageID(pkg.ID) + if new := updates[id]; new != nil { + return nil + } if source.IsCommandLineArguments(pkg.ID) { suffix := ":" + strings.Join(query, ",") id = PackageID(string(id) + suffix) pkgPath = PackagePath(string(pkgPath) + suffix) } - if _, ok := seen[id]; ok { - return nil, fmt.Errorf("import cycle detected: %q", id) + if _, ok := updates[id]; ok { + // If we've already seen this dependency, there may be an import cycle, or + // we may have reached the same package transitively via distinct paths. + // Check the path to confirm. + for _, prev := range path { + if prev == id { + return fmt.Errorf("import cycle detected: %q", id) + } + } } // Recreate the metadata rather than reusing it to avoid locking. - m := &Metadata{ - ID: id, - PkgPath: pkgPath, - Name: PackageName(pkg.Name), - ForTest: PackagePath(packagesinternal.GetForTest(pkg)), - TypesSizes: pkg.TypesSizes, - Config: cfg, - Module: pkg.Module, - depsErrors: packagesinternal.GetDepsErrors(pkg), - } + m := &KnownMetadata{ + Metadata: &Metadata{ + ID: id, + PkgPath: pkgPath, + Name: PackageName(pkg.Name), + ForTest: PackagePath(packagesinternal.GetForTest(pkg)), + TypesSizes: pkg.TypesSizes, + Config: cfg, + Module: pkg.Module, + depsErrors: packagesinternal.GetDepsErrors(pkg), + }, + Valid: true, + } + updates[id] = m // Identify intermediate test variants for later filtering. See the // documentation of IsIntermediateTestVariant for more information. @@ -493,15 +517,6 @@ func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, p } } - s.updateIDForURIsLocked(id, uris) - - // TODO(rstambler): is this still necessary? - copied := map[PackageID]struct{}{ - id: {}, - } - for k, v := range seen { - copied[k] = v - } for importPath, importPkg := range pkg.Imports { importPkgPath := PackagePath(importPath) importID := PackageID(importPkg.ID) @@ -517,32 +532,13 @@ func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, p continue } if s.noValidMetadataForIDLocked(importID) { - if _, err := s.setMetadataLocked(ctx, importPkgPath, importPkg, cfg, query, copied); err != nil { + if err := s.setMetadataLocked(ctx, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { event.Error(ctx, "error in dependency", err) } } } - // Add the metadata to the cache. - - // If we've already set the metadata for this snapshot, reuse it. - if original, ok := s.meta.metadata[m.ID]; ok && original.Valid { - // Since we've just reloaded, clear out shouldLoad. - original.ShouldLoad = false - m = original.Metadata - } else { - s.meta.metadata[m.ID] = &KnownMetadata{ - Metadata: m, - Valid: true, - } - // Invalidate any packages we may have associated with this metadata. - for _, mode := range []source.ParseMode{source.ParseHeader, source.ParseExported, source.ParseFull} { - key := packageKey{mode, m.ID} - delete(s.packages, key) - } - } - - return m, nil + return nil } // computeWorkspacePackages computes workspace packages for the given metadata @@ -588,13 +584,16 @@ func computeWorkspacePackages(meta *metadataGraph) map[PackageID]PackagePath { // allFilesHaveRealPackages reports whether all files referenced by m are // contained in a "real" package (not command-line-arguments). // +// If m is valid but all "real" packages containing any file are invalid, this +// function returns false. +// // If m is not a command-line-arguments package, this is trivially true. func allFilesHaveRealPackages(g *metadataGraph, m *KnownMetadata) bool { n := len(m.CompiledGoFiles) checkURIs: for _, uri := range append(m.CompiledGoFiles[0:n:n], m.GoFiles...) { for _, id := range g.ids[uri] { - if !source.IsCommandLineArguments(string(id)) { + if !source.IsCommandLineArguments(string(id)) && (g.metadata[id].Valid || !m.Valid) { continue checkURIs } } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 961a60f8aa6..601ed450a19 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -716,16 +716,6 @@ func (s *snapshot) getImportedByLocked(id PackageID) []PackageID { return s.meta.importedBy[id] } -func (s *snapshot) rebuildPackageData() { - s.mu.Lock() - defer s.mu.Unlock() - - // Completely invalidate the original map. - s.meta.importedBy = make(map[PackageID][]PackageID) - s.rebuildImportGraph() - s.workspacePackages = computeWorkspacePackages(s.meta) -} - func (s *snapshot) rebuildImportGraph() { for id, m := range s.meta.metadata { for _, importID := range m.Deps { @@ -1306,41 +1296,6 @@ func (s *snapshot) noValidMetadataForIDLocked(id PackageID) bool { return m == nil || !m.Valid } -// updateIDForURIsLocked adds the given ID to the set of known IDs for the given URI. -// Any existing invalid IDs are removed from the set of known IDs. IDs that are -// not "command-line-arguments" are preferred, so if a new ID comes in for a -// URI that previously only had "command-line-arguments", the new ID will -// replace the "command-line-arguments" ID. -func (s *snapshot) updateIDForURIsLocked(id PackageID, uris map[span.URI]struct{}) { - for uri := range uris { - // Collect the new set of IDs, preserving any valid existing IDs. - newIDs := []PackageID{id} - for _, existingID := range s.meta.ids[uri] { - // Don't set duplicates of the same ID. - if existingID == id { - continue - } - // If the package previously only had a command-line-arguments ID, - // delete the command-line-arguments workspace package. - if source.IsCommandLineArguments(string(existingID)) { - delete(s.workspacePackages, existingID) - continue - } - // If the metadata for an existing ID is invalid, and we are - // setting metadata for a new, valid ID--don't preserve the old ID. - if m, ok := s.meta.metadata[existingID]; !ok || !m.Valid { - continue - } - newIDs = append(newIDs, existingID) - } - sort.Slice(newIDs, func(i, j int) bool { - return newIDs[i] < newIDs[j] - }) - s.meta.ids[uri] = newIDs - } - s.dumpWorkspace("updateIDs") -} - func (s *snapshot) isWorkspacePackage(id PackageID) bool { s.mu.Lock() defer s.mu.Unlock() @@ -1984,7 +1939,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // If the workspace mode has changed, we must delete all metadata, as it // is unusable and may produce confusing or incorrect diagnostics. - // If a file has been deleted, we must delete metadata all packages + // If a file has been deleted, we must delete metadata for all packages // containing that file. workspaceModeChanged := s.workspaceMode() != result.workspaceMode() skipID := map[PackageID]bool{} From 39d3d492601a90d889fee47076f09e3798734942 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 9 Aug 2021 10:39:21 -0400 Subject: [PATCH 022/723] internal/lsp/cache: use metadataGraph.Clone in snapshot.clone Rather than updating metadata directly in snapshot.clone, build a set of updates to apply and call metadata.Clone. After this change, metadata is only updated by cloning, so we can eliminate some code that works with mutable metadata. In the next CL we'll only update the metadata if something changed, but this is intentionally left out of this CL to isolate the change. Benchmark (didChange in kubernetes): ~55ms->65ms, because it is now more work to compute uris. For golang/go#45686 Change-Id: I048bed65760b266a209f67111c57fae29bd3e6f0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/340852 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- internal/lsp/cache/graph.go | 8 ---- internal/lsp/cache/session.go | 2 +- internal/lsp/cache/snapshot.go | 67 ++++++++++++---------------------- 3 files changed, 25 insertions(+), 52 deletions(-) diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go index f3fe077cf5b..36e658b3a86 100644 --- a/internal/lsp/cache/graph.go +++ b/internal/lsp/cache/graph.go @@ -30,14 +30,6 @@ type metadataGraph struct { ids map[span.URI][]PackageID } -func NewMetadataGraph() *metadataGraph { - return &metadataGraph{ - metadata: make(map[PackageID]*KnownMetadata), - importedBy: make(map[PackageID][]PackageID), - ids: make(map[span.URI][]PackageID), - } -} - // Clone creates a new metadataGraph, applying the given updates to the // receiver. func (g *metadataGraph) Clone(updates map[PackageID]*KnownMetadata) *metadataGraph { diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index cbb58740621..286d8f12c46 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -232,7 +232,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, initializeOnce: &sync.Once{}, generation: s.cache.store.Generation(generationName(v, 0)), packages: make(map[packageKey]*packageHandle), - meta: NewMetadataGraph(), + meta: &metadataGraph{}, files: make(map[span.URI]source.VersionedFileHandle), goFiles: newGoFileMap(), symbols: make(map[span.URI]*symbolHandle), diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 601ed450a19..369baca8def 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -705,25 +705,9 @@ func (s *snapshot) getModTidyHandle(uri span.URI) *modTidyHandle { func (s *snapshot) getImportedBy(id PackageID) []PackageID { s.mu.Lock() defer s.mu.Unlock() - return s.getImportedByLocked(id) -} - -func (s *snapshot) getImportedByLocked(id PackageID) []PackageID { - // If we haven't rebuilt the import graph since creating the snapshot. - if len(s.meta.importedBy) == 0 { - s.rebuildImportGraph() - } return s.meta.importedBy[id] } -func (s *snapshot) rebuildImportGraph() { - for id, m := range s.meta.metadata { - for _, importID := range m.Deps { - s.meta.importedBy[importID] = append(s.meta.importedBy[importID], id) - } - } -} - func (s *snapshot) addPackageHandle(ph *packageHandle) *packageHandle { s.mu.Lock() defer s.mu.Unlock() @@ -1705,7 +1689,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC builtin: s.builtin, initializeOnce: s.initializeOnce, initializedErr: s.initializedErr, - meta: NewMetadataGraph(), packages: make(map[packageKey]*packageHandle, len(s.packages)), actions: make(map[actionKey]*actionHandle, len(s.actions)), files: make(map[span.URI]source.VersionedFileHandle, len(s.files)), @@ -1912,7 +1895,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC return } idsToInvalidate[id] = newInvalidateMetadata - for _, rid := range s.getImportedByLocked(id) { + for _, rid := range s.meta.importedBy[id] { addRevDeps(rid, invalidateMetadata) } } @@ -1976,58 +1959,56 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC addForwardDeps(id) } - // Copy the URI to package ID mappings, skipping only those URIs whose - // metadata will be reloaded in future calls to load. + // Compute which IDs are in the snapshot. + // + // TODO(rfindley): this step shouldn't be necessary, since we compute skipID + // above based on meta.ids. deleteInvalidMetadata := forceReloadMetadata || workspaceModeChanged idsInSnapshot := map[PackageID]bool{} // track all known IDs - for uri, ids := range s.meta.ids { - // Optimization: ids slices are typically numerous, short (<3), - // and rarely modified by this loop, so don't allocate copies - // until necessary. - var resultIDs []PackageID // nil implies equal to ids[:i:i] - for i, id := range ids { + for _, ids := range s.meta.ids { + for _, id := range ids { if skipID[id] || deleteInvalidMetadata && idsToInvalidate[id] { - resultIDs = ids[:i:i] // unshare continue } // The ID is not reachable from any workspace package, so it should // be deleted. if !reachableID[id] { - resultIDs = ids[:i:i] // unshare continue } idsInSnapshot[id] = true - if resultIDs != nil { - resultIDs = append(resultIDs, id) - } - } - if resultIDs == nil { - resultIDs = ids } - result.meta.ids[uri] = resultIDs } // TODO(adonovan): opt: represent PackageID as an index into a process-global // dup-free list of all package names ever seen, then use a bitmap instead of // a hash table for "PackageSet" (e.g. idsInSnapshot). - // Copy the package metadata. We only need to invalidate packages directly - // containing the affected file, and only if it changed in a relevant way. + // Compute which metadata updates are required. We only need to invalidate + // packages directly containing the affected file, and only if it changed in + // a relevant way. + metadataUpdates := make(map[PackageID]*KnownMetadata) for k, v := range s.meta.metadata { if !idsInSnapshot[k] { // Delete metadata for IDs that are no longer reachable from files // in the snapshot. + metadataUpdates[k] = nil continue } invalidateMetadata := idsToInvalidate[k] - // Mark invalidated metadata rather than deleting it outright. - result.meta.metadata[k] = &KnownMetadata{ - Metadata: v.Metadata, - Valid: v.Valid && !invalidateMetadata, - PkgFilesChanged: v.PkgFilesChanged || changedPkgFiles[k], - ShouldLoad: v.ShouldLoad || invalidateMetadata, + valid := v.Valid && !invalidateMetadata + pkgFilesChanged := v.PkgFilesChanged || changedPkgFiles[k] + shouldLoad := v.ShouldLoad || invalidateMetadata + if valid != v.Valid || pkgFilesChanged != v.PkgFilesChanged || shouldLoad != v.ShouldLoad { + // Mark invalidated metadata rather than deleting it outright. + metadataUpdates[k] = &KnownMetadata{ + Metadata: v.Metadata, + Valid: valid, + PkgFilesChanged: pkgFilesChanged, + ShouldLoad: shouldLoad, + } } } + result.meta = s.meta.Clone(metadataUpdates) result.workspacePackages = computeWorkspacePackages(result.meta) // Inherit all of the go.mod-related handles. From 4ba3d2217f15fbd2daf23f631a1c402900512912 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 18 Mar 2022 15:45:23 -0400 Subject: [PATCH 023/723] internal/lsp/cache: clone the metadata graph when clearing ShouldLoad The Metadata type was mutated in exactly one place: when setting the ShouldLoad bit after package loading completes. Address this by cloning the metadata graph when clearing ShouldLoad. After this change, metadata graphs and the data within them are immutable. This also fixes a range-variable capture bug in load.go: previously we were deferring a call to clearShouldLoad for the range variable scope. After this change, we properly clear the ShouldLoad bit for all scopes. Change-Id: I8f9140a490f81fbabacfc9e0102d9c638c7fbb37 Reviewed-on: https://go-review.googlesource.com/c/tools/+/400821 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- internal/lsp/cache/load.go | 29 +++++++++--------- internal/lsp/cache/snapshot.go | 56 +++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 085c7c28001..5f24d0f08ef 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -37,6 +37,15 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf var query []string var containsDir bool // for logging + // Unless the context was canceled, set "shouldLoad" to false for all + // of the metadata we attempted to load. + defer func() { + if errors.Is(err, context.Canceled) { + return + } + s.clearShouldLoad(scopes...) + }() + // Keep track of module query -> module path so that we can later correlate query // errors with errors. moduleQueries := make(map[string]string) @@ -44,14 +53,6 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf if !s.shouldLoad(scope) { continue } - // Unless the context was canceled, set "shouldLoad" to false for all - // of the metadata we attempted to load. - defer func() { - if errors.Is(err, context.Canceled) { - return - } - s.clearShouldLoad(scope) - }() switch scope := scope.(type) { case PackagePath: if source.IsCommandLineArguments(string(scope)) { @@ -196,7 +197,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } // TODO: once metadata is immutable, we shouldn't have to lock here. s.mu.Lock() - err := s.setMetadataLocked(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, updates, nil) + err := s.computeMetadataUpdates(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, updates, nil) s.mu.Unlock() if err != nil { return err @@ -438,10 +439,10 @@ func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generati return span.URIFromPath(v.(*workspaceDirData).dir), nil } -// setMetadataLocked extracts metadata from pkg and records it in s. It -// recurs through pkg.Imports to ensure that metadata exists for all -// dependencies. -func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { +// computeMetadataUpdates populates the updates map with metadata updates to +// apply, based on the given pkg. It recurs through pkg.Imports to ensure that +// metadata exists for all dependencies. +func (s *snapshot) computeMetadataUpdates(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { id := PackageID(pkg.ID) if new := updates[id]; new != nil { return nil @@ -532,7 +533,7 @@ func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, p continue } if s.noValidMetadataForIDLocked(importID) { - if err := s.setMetadataLocked(ctx, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { + if err := s.computeMetadataUpdates(ctx, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { event.Error(ctx, "error in dependency", err) } } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 369baca8def..ef06a10f6b4 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1191,10 +1191,12 @@ func (s *snapshot) shouldLoad(scope interface{}) bool { s.mu.Lock() defer s.mu.Unlock() + g := s.meta + switch scope := scope.(type) { case PackagePath: var meta *KnownMetadata - for _, m := range s.meta.metadata { + for _, m := range g.metadata { if m.PkgPath != scope { continue } @@ -1206,12 +1208,12 @@ func (s *snapshot) shouldLoad(scope interface{}) bool { return false case fileURI: uri := span.URI(scope) - ids := s.meta.ids[uri] + ids := g.ids[uri] if len(ids) == 0 { return true } for _, id := range ids { - m, ok := s.meta.metadata[id] + m, ok := g.metadata[id] if !ok || m.ShouldLoad { return true } @@ -1222,34 +1224,40 @@ func (s *snapshot) shouldLoad(scope interface{}) bool { } } -func (s *snapshot) clearShouldLoad(scope interface{}) { +func (s *snapshot) clearShouldLoad(scopes ...interface{}) { s.mu.Lock() defer s.mu.Unlock() - switch scope := scope.(type) { - case PackagePath: - var meta *KnownMetadata - for _, m := range s.meta.metadata { - if m.PkgPath == scope { - meta = m - } - } - if meta == nil { - return - } - meta.ShouldLoad = false - case fileURI: - uri := span.URI(scope) - ids := s.meta.ids[uri] - if len(ids) == 0 { - return + g := s.meta + + var updates map[PackageID]*KnownMetadata + markLoaded := func(m *KnownMetadata) { + if updates == nil { + updates = make(map[PackageID]*KnownMetadata) } - for _, id := range ids { - if m, ok := s.meta.metadata[id]; ok { - m.ShouldLoad = false + next := *m + next.ShouldLoad = false + updates[next.ID] = &next + } + for _, scope := range scopes { + switch scope := scope.(type) { + case PackagePath: + for _, m := range g.metadata { + if m.PkgPath == scope { + markLoaded(m) + } + } + case fileURI: + uri := span.URI(scope) + ids := g.ids[uri] + for _, id := range ids { + if m, ok := g.metadata[id]; ok { + markLoaded(m) + } } } } + s.meta = g.Clone(updates) } // noValidMetadataForURILocked reports whether there is any valid metadata for From dffd6452c0f18fe993650d0aa76747dbe9d21052 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 9 Aug 2021 10:54:27 -0400 Subject: [PATCH 024/723] internal/lsp/cache: only clone metadata if something changed We with immutable metadata, we don't need to clone if nothing was invalidated. Benchmark (didChange in k8s): 65ms->45ms For golang/go#45686 Change-Id: I6b5e764c53a35784fd8c7b43bc26361f4ee8d928 Reviewed-on: https://go-review.googlesource.com/c/tools/+/340853 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/lsp/cache/snapshot.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index ef06a10f6b4..56ffdf88126 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -2016,8 +2016,15 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } - result.meta = s.meta.Clone(metadataUpdates) - result.workspacePackages = computeWorkspacePackages(result.meta) + if len(metadataUpdates) > 0 { + result.meta = s.meta.Clone(metadataUpdates) + result.workspacePackages = computeWorkspacePackages(result.meta) + } else { + // No metadata changes. Since metadata is only updated by cloning, it is + // safe to re-use the existing metadata here. + result.meta = s.meta + result.workspacePackages = s.workspacePackages + } // Inherit all of the go.mod-related handles. for _, v := range result.modTidyHandles { From 567c98ba1a680254947db41413f78fe8e4451086 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 9 Aug 2021 11:04:34 -0400 Subject: [PATCH 025/723] internal/lsp/cache: don't walk URIs to invalidate metadata Since ids is derived from metadata, we should not have to walk ids to see which metadata is still active. Just compute metadata updates directly. Benchmark (didChange in k8s): ~45ms->41ms For golang/go#45686 Change-Id: Id557ed3f2e05c903e4bb3f3f6a4af864751c4546 Reviewed-on: https://go-review.googlesource.com/c/tools/+/340854 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro --- internal/lsp/cache/snapshot.go | 42 ++++++++++++---------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 56ffdf88126..72403571972 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1933,6 +1933,12 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // If a file has been deleted, we must delete metadata for all packages // containing that file. workspaceModeChanged := s.workspaceMode() != result.workspaceMode() + + // Don't keep package metadata for packages that have lost files. + // + // TODO(rfindley): why not keep invalid metadata in this case? If we + // otherwise allow operate on invalid metadata, why not continue to do so, + // skipping the missing file? skipID := map[PackageID]bool{} for _, c := range changes { if c.exists { @@ -1967,41 +1973,23 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC addForwardDeps(id) } - // Compute which IDs are in the snapshot. - // - // TODO(rfindley): this step shouldn't be necessary, since we compute skipID - // above based on meta.ids. - deleteInvalidMetadata := forceReloadMetadata || workspaceModeChanged - idsInSnapshot := map[PackageID]bool{} // track all known IDs - for _, ids := range s.meta.ids { - for _, id := range ids { - if skipID[id] || deleteInvalidMetadata && idsToInvalidate[id] { - continue - } - // The ID is not reachable from any workspace package, so it should - // be deleted. - if !reachableID[id] { - continue - } - idsInSnapshot[id] = true - } - } - // TODO(adonovan): opt: represent PackageID as an index into a process-global - // dup-free list of all package names ever seen, then use a bitmap instead of - // a hash table for "PackageSet" (e.g. idsInSnapshot). - // Compute which metadata updates are required. We only need to invalidate // packages directly containing the affected file, and only if it changed in // a relevant way. metadataUpdates := make(map[PackageID]*KnownMetadata) + deleteInvalidMetadata := forceReloadMetadata || workspaceModeChanged for k, v := range s.meta.metadata { - if !idsInSnapshot[k] { - // Delete metadata for IDs that are no longer reachable from files - // in the snapshot. + invalidateMetadata := idsToInvalidate[k] + if skipID[k] || (invalidateMetadata && deleteInvalidMetadata) { + metadataUpdates[k] = nil + continue + } + // The ID is not reachable from any workspace package, so it should + // be deleted. + if !reachableID[k] { metadataUpdates[k] = nil continue } - invalidateMetadata := idsToInvalidate[k] valid := v.Valid && !invalidateMetadata pkgFilesChanged := v.PkgFilesChanged || changedPkgFiles[k] shouldLoad := v.ShouldLoad || invalidateMetadata From c353b054c48e6ce082f2cde31c5d6815c4b6126b Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 9 Aug 2021 11:06:54 -0400 Subject: [PATCH 026/723] internal/lsp/cache: delete checkSnapshotLocked Now that the entire metadata graph and workspace packages are derived from metadata, there should be no need to validate coherency. This results in a small improvement to didChange benchmarking (within statistical noise). For golang/go#45686 Change-Id: I32683e025f42d768d62864683e55d4c00146a31c Reviewed-on: https://go-review.googlesource.com/c/tools/+/340855 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley --- internal/lsp/cache/snapshot.go | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 72403571972..bdb73e31dc0 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -30,7 +30,6 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/debug/log" "golang.org/x/tools/internal/lsp/debug/tag" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/memoize" @@ -1638,29 +1637,6 @@ func generationName(v *View, snapshotID uint64) string { return fmt.Sprintf("v%v/%v", v.id, snapshotID) } -// checkSnapshotLocked verifies that some invariants are preserved on the -// snapshot. -func checkSnapshotLocked(ctx context.Context, s *snapshot) { - // Check that every go file for a workspace package is identified as - // belonging to that workspace package. - for wsID := range s.workspacePackages { - if m, ok := s.meta.metadata[wsID]; ok { - for _, uri := range m.GoFiles { - found := false - for _, id := range s.meta.ids[uri] { - if id == wsID { - found = true - break - } - } - if !found { - log.Error.Logf(ctx, "workspace package %v not associated with %v", wsID, uri) - } - } - } - } -} - // unappliedChanges is a file source that handles an uncloned snapshot. type unappliedChanges struct { originalSnapshot *snapshot @@ -1684,8 +1660,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC s.mu.Lock() defer s.mu.Unlock() - checkSnapshotLocked(ctx, s) - newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1)) bgCtx, cancel := context.WithCancel(bgCtx) result := &snapshot{ From 88325aa063540896255ba32a031e726c1fb0e545 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 16 Jun 2022 15:41:24 -0400 Subject: [PATCH 027/723] internal/memoize: add trace region for Handle.run The region name includes the type of the key, such as packageHandleKey or actionHandleKey, so we can separate these deferred computations into their own buckets. Change-Id: I0359127ccf47b158f353fae2bf74ba000668a40b Reviewed-on: https://go-review.googlesource.com/c/tools/+/412817 Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- internal/memoize/memoize.go | 65 +++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index dec2fff6836..480b87f5ce9 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -14,6 +14,7 @@ import ( "flag" "fmt" "reflect" + "runtime/trace" "sync" "sync/atomic" @@ -316,40 +317,42 @@ func (h *Handle) run(ctx context.Context, g *Generation, arg Arg) (interface{}, // Make sure that the generation isn't destroyed while we're running in it. release := g.Acquire() go func() { - defer release() - // Just in case the function does something expensive without checking - // the context, double-check we're still alive. - if childCtx.Err() != nil { - return - } - v := function(childCtx, arg) - if childCtx.Err() != nil { - // It's possible that v was computed despite the context cancellation. In - // this case we should ensure that it is cleaned up. - if h.cleanup != nil && v != nil { - h.cleanup(v) + trace.WithRegion(childCtx, fmt.Sprintf("Handle.run %T", h.key), func() { + defer release() + // Just in case the function does something expensive without checking + // the context, double-check we're still alive. + if childCtx.Err() != nil { + return + } + v := function(childCtx, arg) + if childCtx.Err() != nil { + // It's possible that v was computed despite the context cancellation. In + // this case we should ensure that it is cleaned up. + if h.cleanup != nil && v != nil { + h.cleanup(v) + } + return } - return - } - h.mu.Lock() - defer h.mu.Unlock() - // It's theoretically possible that the handle has been cancelled out - // of the run that started us, and then started running again since we - // checked childCtx above. Even so, that should be harmless, since each - // run should produce the same results. - if h.state != stateRunning { - // v will never be used, so ensure that it is cleaned up. - if h.cleanup != nil && v != nil { - h.cleanup(v) + h.mu.Lock() + defer h.mu.Unlock() + // It's theoretically possible that the handle has been cancelled out + // of the run that started us, and then started running again since we + // checked childCtx above. Even so, that should be harmless, since each + // run should produce the same results. + if h.state != stateRunning { + // v will never be used, so ensure that it is cleaned up. + if h.cleanup != nil && v != nil { + h.cleanup(v) + } + return } - return - } - // At this point v will be cleaned up whenever h is destroyed. - h.value = v - h.function = nil - h.state = stateCompleted - close(h.done) + // At this point v will be cleaned up whenever h is destroyed. + h.value = v + h.function = nil + h.state = stateCompleted + close(h.done) + }) }() return h.wait(ctx) From e9870152b0e8539a2ef361f87257bc7db693a30b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 16 Jun 2022 14:03:39 -0400 Subject: [PATCH 028/723] internal/lsp/cache: symbolize in parallel This change parallelizes the buildSymbolHandle().Get computation for each file, with 2xGOMAXPROCS goroutines, since it is a mix of I/O (read) and CPU (parse). (The symbolize AST walk happens in other goroutines.) This reduces the time for the source.WorkspaceSymbols trace task applied to kubernetes from 3981ms to 630ms (6x faster). Change-Id: I5f1ee4afc2f6b2dd752791a30d33a21f50180a9c Reviewed-on: https://go-review.googlesource.com/c/tools/+/412818 Reviewed-by: Robert Findley --- internal/lsp/cache/snapshot.go | 43 +++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index bdb73e31dc0..9875ae4bd70 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -17,6 +17,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "sort" "strconv" "strings" @@ -25,6 +26,7 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/mod/module" "golang.org/x/mod/semver" + "golang.org/x/sync/errgroup" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" @@ -994,26 +996,35 @@ func (s *snapshot) activePackageHandles(ctx context.Context) ([]*packageHandle, return phs, nil } +// Symbols extracts and returns the symbols for each file in all the snapshot's views. func (s *snapshot) Symbols(ctx context.Context) (map[span.URI][]source.Symbol, error) { - result := make(map[span.URI][]source.Symbol) - - // Keep going on errors, but log the first failure. Partial symbol results - // are better than no symbol results. - var firstErr error + // Keep going on errors, but log the first failure. + // Partial results are better than no symbol results. + var ( + group errgroup.Group + nprocs = 2 * runtime.GOMAXPROCS(-1) // symbolize is a mix of I/O and CPU + iolimit = make(chan struct{}, nprocs) // I/O limiting counting semaphore + resultMu sync.Mutex + result = make(map[span.URI][]source.Symbol) + ) for uri, f := range s.files { - sh := s.buildSymbolHandle(ctx, f) - v, err := sh.handle.Get(ctx, s.generation, s) - if err != nil { - if firstErr == nil { - firstErr = err + uri, f := uri, f + // TODO(adonovan): upgrade errgroup and use group.SetLimit(nprocs). + iolimit <- struct{}{} // acquire token + group.Go(func() error { + defer func() { <-iolimit }() // release token + v, err := s.buildSymbolHandle(ctx, f).handle.Get(ctx, s.generation, s) + if err != nil { + return err } - continue - } - data := v.(*symbolData) - result[uri] = data.symbols + resultMu.Lock() + result[uri] = v.(*symbolData).symbols + resultMu.Unlock() + return nil + }) } - if firstErr != nil { - event.Error(ctx, "getting snapshot symbols", firstErr) + if err := group.Wait(); err != nil { + event.Error(ctx, "getting snapshot symbols", err) } return result, nil } From 1e14d994d8902691ecce91c8f344cd228f8c7fb7 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Mon, 13 Jun 2022 18:16:50 -0400 Subject: [PATCH 029/723] internal/lsp: add inlay hints for composite literal types Add inlay hints for composite literal types. This will show type information for composite literals with no explicit types. Example: {"hello", "goodbye"} For golang/go#52343 Change-Id: Ia1f03b82669387c864353b8033940759fa1128e7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411905 gopls-CI: kokoro Run-TryBot: Suzy Mueller Reviewed-by: Jamal Carvalho TryBot-Result: Gopher Robot --- internal/lsp/source/inlay_hint.go | 24 ++++++++++++++++--- .../testdata/inlay_hint/composite_literals.go | 14 ++++++++++- .../inlay_hint/composite_literals.go.golden | 20 ++++++++++++---- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 406e4ae80e8..af9e715c91a 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -47,7 +47,7 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol case *ast.GenDecl: hints = append(hints, constantValues(n, tmap, info)...) case *ast.CompositeLit: - hints = append(hints, compositeLiterals(n, tmap, info)...) + hints = append(hints, compositeLiterals(n, tmap, info, &q)...) } return true }) @@ -181,17 +181,35 @@ func constantValues(node *ast.GenDecl, tmap *lsppos.TokenMapper, info *types.Inf return hints } -func compositeLiterals(node *ast.CompositeLit, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint { +func compositeLiterals(node *ast.CompositeLit, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { typ := info.TypeOf(node) if typ == nil { return nil } + + prefix := "" + if t, ok := typ.(*types.Pointer); ok { + typ = t.Elem() + prefix = "&" + } + strct, ok := typ.Underlying().(*types.Struct) if !ok { return nil } var hints []protocol.InlayHint + if node.Type == nil { + // The type for this struct is implicit, add an inlay hint. + if start, ok := tmap.Position(node.Lbrace); ok { + hints = append(hints, protocol.InlayHint{ + Position: &start, + Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))), + Kind: protocol.Type, + }) + } + } + for i, v := range node.Elts { if _, ok := v.(*ast.KeyValueExpr); !ok { start, ok := tmap.Position(v.Pos()) @@ -216,7 +234,7 @@ func buildLabel(s string) []protocol.InlayHintLabelPart { label := protocol.InlayHintLabelPart{ Value: s, } - if len(s) > maxLabelLength { + if len(s) > maxLabelLength+len("...") { label.Value = s[:maxLabelLength] + "..." label.Tooltip = s } diff --git a/internal/lsp/testdata/inlay_hint/composite_literals.go b/internal/lsp/testdata/inlay_hint/composite_literals.go index 7eeed03e81a..b05c95ec800 100644 --- a/internal/lsp/testdata/inlay_hint/composite_literals.go +++ b/internal/lsp/testdata/inlay_hint/composite_literals.go @@ -6,7 +6,19 @@ func fieldNames() { for _, c := range []struct { in, want string }{ - {"Hello, world", "dlrow ,olleH"}, + struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, + {"Hello, 世界", "界世 ,olleH"}, + {"", ""}, + } { + fmt.Println(c.in == c.want) + } +} + +func fieldNamesPointers() { + for _, c := range []*struct { + in, want string + }{ + &struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, {"Hello, 世界", "界世 ,olleH"}, {"", ""}, } { diff --git a/internal/lsp/testdata/inlay_hint/composite_literals.go.golden b/internal/lsp/testdata/inlay_hint/composite_literals.go.golden index ecff7800387..eb2febdb6a3 100644 --- a/internal/lsp/testdata/inlay_hint/composite_literals.go.golden +++ b/internal/lsp/testdata/inlay_hint/composite_literals.go.golden @@ -4,12 +4,24 @@ package inlayHint //@inlayHint("package") import "fmt" func fieldNames() { - for _< int>, c< struct{in string; want strin...> := range []struct { + for _< int>, c< struct{in string; want string}> := range []struct { in, want string }{ - {"Hello, world", "dlrow ,olleH"}, - {"Hello, 世界", "界世 ,olleH"}, - {"", ""}, + struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, + {"Hello, 世界", "界世 ,olleH"}, + {"", ""}, + } { + fmt.Println(c.in == c.want) + } +} + +func fieldNamesPointers() { + for _< int>, c< *struct{in string; want string}> := range []*struct { + in, want string + }{ + &struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, + <&struct{in string; want string}>{"Hello, 世界", "界世 ,olleH"}, + <&struct{in string; want string}>{"", ""}, } { fmt.Println(c.in == c.want) } From 381ac87aae563aca1b0fcf5ced49d661e1dba9f4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 16 Jun 2022 16:22:28 -0400 Subject: [PATCH 030/723] internal/lsp/debug: reduce critical sections in trace This change reduces the sizes of the critical section in traces.ProcessEvent, in particular moving allocations ahead of Lock. This reduces the contention according to the trace profiler. See https://go-review.googlesource.com/c/go/+/411909 for another reduction in contention. The largest remaining contention is Handle.Get, which thousands of goroutines wait for because we initiate typechecking top down. (Second attempt at https://go-review.googlesource.com/c/tools/+/411910, reverted in https://go-review.googlesource.com/c/tools/+/412694. The changes to Generation.Bind have been dropped.) Change-Id: Ia9050c97bd12d2d75055f8d1dfcda3ef1f5ad334 Reviewed-on: https://go-review.googlesource.com/c/tools/+/412820 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim --- internal/lsp/cache/parse.go | 2 +- internal/lsp/debug/trace.go | 39 ++++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index 668c437f5c9..ab55743ccf0 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -278,7 +278,7 @@ func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mod tok := fset.File(file.Pos()) if tok == nil { - // file.Pos is the location of the package declaration. If there was + // file.Pos is the location of the package declaration (issue #53202). If there was // none, we can't find the token.File that ParseFile created, and we // have no choice but to recreate it. tok = fset.AddFile(fh.URI().Filename(), -1, len(src)) diff --git a/internal/lsp/debug/trace.go b/internal/lsp/debug/trace.go index ca612867a5d..bb402cfaa8f 100644 --- a/internal/lsp/debug/trace.go +++ b/internal/lsp/debug/trace.go @@ -119,8 +119,6 @@ func formatEvent(ctx context.Context, ev core.Event, lm label.Map) string { } func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context { - t.mu.Lock() - defer t.mu.Unlock() span := export.GetSpan(ctx) if span == nil { return ctx @@ -128,11 +126,8 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) switch { case event.IsStart(ev): - if t.sets == nil { - t.sets = make(map[string]*traceSet) - t.unfinished = make(map[export.SpanContext]*traceData) - } - // just starting, add it to the unfinished map + // Just starting: add it to the unfinished map. + // Allocate before the critical section. td := &traceData{ TraceID: span.ID.TraceID, SpanID: span.ID.SpanID, @@ -141,6 +136,13 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) Start: span.Start().At(), Tags: renderLabels(span.Start()), } + + t.mu.Lock() + defer t.mu.Unlock() + if t.sets == nil { + t.sets = make(map[string]*traceSet) + t.unfinished = make(map[export.SpanContext]*traceData) + } t.unfinished[span.ID] = td // and wire up parents if we have them if !span.ParentID.IsValid() { @@ -155,7 +157,19 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) parent.Children = append(parent.Children, td) case event.IsEnd(ev): - // finishing, must be already in the map + // Finishing: must be already in the map. + // Allocate events before the critical section. + events := span.Events() + tdEvents := make([]traceEvent, len(events)) + for i, event := range events { + tdEvents[i] = traceEvent{ + Time: event.At(), + Tags: renderLabels(event), + } + } + + t.mu.Lock() + defer t.mu.Unlock() td, found := t.unfinished[span.ID] if !found { return ctx // if this happens we are in a bad place @@ -164,14 +178,7 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) td.Finish = span.Finish().At() td.Duration = span.Finish().At().Sub(span.Start().At()) - events := span.Events() - td.Events = make([]traceEvent, len(events)) - for i, event := range events { - td.Events[i] = traceEvent{ - Time: event.At(), - Tags: renderLabels(event), - } - } + td.Events = tdEvents set, ok := t.sets[span.Name] if !ok { From 70ccf57e4b75514eaa0d0b35525b2b5fd63e489a Mon Sep 17 00:00:00 2001 From: aarzilli Date: Fri, 3 Dec 2021 10:51:49 +0100 Subject: [PATCH 031/723] go/packages: fix loading single file when outside of GOPATH, module Allows a 'file=' query to load a single file even when it is outside of GOPATH or a module. Fixes golang/go#49949 Change-Id: I519f1412923dfc1d2504ec49620d10c823e5c0dc Reviewed-on: https://go-review.googlesource.com/c/tools/+/369014 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley Reviewed-by: Hyang-Ah Hana Kim --- go/packages/golist.go | 7 ++++--- go/packages/packages_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/go/packages/golist.go b/go/packages/golist.go index 50533995a65..de881562de1 100644 --- a/go/packages/golist.go +++ b/go/packages/golist.go @@ -302,11 +302,12 @@ func (state *golistState) runContainsQueries(response *responseDeduper, queries } dirResponse, err := state.createDriverResponse(pattern) - // If there was an error loading the package, or the package is returned - // with errors, try to load the file as an ad-hoc package. + // If there was an error loading the package, or no packages are returned, + // or the package is returned with errors, try to load the file as an + // ad-hoc package. // Usually the error will appear in a returned package, but may not if we're // in module mode and the ad-hoc is located outside a module. - if err != nil || len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].GoFiles) == 0 && + if err != nil || len(dirResponse.Packages) == 0 || len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].GoFiles) == 0 && len(dirResponse.Packages[0].Errors) == 1 { var queryErr error if dirResponse, queryErr = state.adhocPackage(pattern, query); queryErr != nil { diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 796edb6b7b4..647f3a366df 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -2709,6 +2709,31 @@ func TestEmptyEnvironment(t *testing.T) { } } +func TestPackageLoadSingleFile(t *testing.T) { + tmp, err := ioutil.TempDir("", "a") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + filename := filepath.Join(tmp, "a.go") + + if err := ioutil.WriteFile(filename, []byte(`package main; func main() { println("hello world") }`), 0775); err != nil { + t.Fatal(err) + } + + pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadSyntax, Dir: tmp}, "file="+filename) + if err != nil { + t.Fatalf("could not load package: %v", err) + } + if len(pkgs) != 1 { + t.Fatalf("expected one package to be loaded, got %d", len(pkgs)) + } + if len(pkgs[0].CompiledGoFiles) != 1 || pkgs[0].CompiledGoFiles[0] != filename { + t.Fatalf("expected one compiled go file (%q), got %v", filename, pkgs[0].CompiledGoFiles) + } +} + func errorMessages(errors []packages.Error) []string { var msgs []string for _, err := range errors { From 641b30b3f4033251a94b65a87e8baca26ab1a7a4 Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Fri, 17 Jun 2022 20:11:18 +0000 Subject: [PATCH 032/723] internal/lsp: add inlay hints for inferred type params This will show inferred type information for generic function call expressions. Example: SumNumbers<[string, int64]>(ints) For golang/go#52343 Change-Id: I05595f236626e8fb3666af5160611e074e8265a4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/412994 Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Jamal Carvalho --- internal/lsp/source/inlay_hint.go | 29 ++++++++++++ .../lsp/testdata/inlay_hint/type_params.go | 45 ++++++++++++++++++ .../testdata/inlay_hint/type_params.go.golden | 47 +++++++++++++++++++ .../lsp/testdata/summary_go1.18.txt.golden | 2 +- 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 internal/lsp/testdata/inlay_hint/type_params.go create mode 100644 internal/lsp/testdata/inlay_hint/type_params.go.golden diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index af9e715c91a..8fe46b27a89 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -16,6 +16,7 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/lsppos" "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/typeparams" ) const ( @@ -40,6 +41,7 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol switch n := node.(type) { case *ast.CallExpr: hints = append(hints, parameterNames(n, tmap, info)...) + hints = append(hints, funcTypeParams(n, tmap, info)...) case *ast.AssignStmt: hints = append(hints, assignVariableTypes(n, tmap, info, &q)...) case *ast.RangeStmt: @@ -90,6 +92,33 @@ func parameterNames(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.In return hints } +func funcTypeParams(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint { + id, ok := node.Fun.(*ast.Ident) + if !ok { + return nil + } + inst := typeparams.GetInstances(info)[id] + if inst.TypeArgs == nil { + return nil + } + start, ok := tmap.Position(id.End()) + if !ok { + return nil + } + var args []string + for i := 0; i < inst.TypeArgs.Len(); i++ { + args = append(args, inst.TypeArgs.At(i).String()) + } + if len(args) == 0 { + return nil + } + return []protocol.InlayHint{{ + Position: &start, + Label: buildLabel("[" + strings.Join(args, ", ") + "]"), + Kind: protocol.Type, + }} +} + func assignVariableTypes(node *ast.AssignStmt, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { if node.Tok != token.DEFINE { return nil diff --git a/internal/lsp/testdata/inlay_hint/type_params.go b/internal/lsp/testdata/inlay_hint/type_params.go new file mode 100644 index 00000000000..3a3c7e53734 --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/type_params.go @@ -0,0 +1,45 @@ +//go:build go1.18 +// +build go1.18 + +package inlayHint //@inlayHint("package") + +func main() { + ints := map[string]int64{ + "first": 34, + "second": 12, + } + + floats := map[string]float64{ + "first": 35.98, + "second": 26.99, + } + + SumIntsOrFloats[string, int64](ints) + SumIntsOrFloats[string, float64](floats) + + SumIntsOrFloats(ints) + SumIntsOrFloats(floats) + + SumNumbers(ints) + SumNumbers(floats) +} + +type Number interface { + int64 | float64 +} + +func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { + var s V + for _, v := range m { + s += v + } + return s +} + +func SumNumbers[K comparable, V Number](m map[K]V) V { + var s V + for _, v := range m { + s += v + } + return s +} diff --git a/internal/lsp/testdata/inlay_hint/type_params.go.golden b/internal/lsp/testdata/inlay_hint/type_params.go.golden new file mode 100644 index 00000000000..4819963b7a4 --- /dev/null +++ b/internal/lsp/testdata/inlay_hint/type_params.go.golden @@ -0,0 +1,47 @@ +-- inlayHint -- +//go:build go1.18 +// +build go1.18 + +package inlayHint //@inlayHint("package") + +func main() { + ints< map[string]int64> := map[string]int64{ + "first": 34, + "second": 12, + } + + floats< map[string]float64> := map[string]float64{ + "first": 35.98, + "second": 26.99, + } + + SumIntsOrFloats[string, int64](ints) + SumIntsOrFloats[string, float64](floats) + + SumIntsOrFloats<[string, int64]>(ints) + SumIntsOrFloats<[string, float64]>(floats) + + SumNumbers<[string, int64]>(ints) + SumNumbers<[string, float64]>(floats) +} + +type Number interface { + int64 | float64 +} + +func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { + var s V + for _< K>, v< V> := range m { + s += v + } + return s +} + +func SumNumbers[K comparable, V Number](m map[K]V) V { + var s V + for _< K>, v< V> := range m { + s += v + } + return s +} + diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 28a2672db50..7e8da12d764 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -19,7 +19,7 @@ MethodExtractionCount = 6 DefinitionsCount = 108 TypeDefinitionsCount = 18 HighlightsCount = 69 -InlayHintsCount = 4 +InlayHintsCount = 5 ReferencesCount = 27 RenamesCount = 48 PrepareRenamesCount = 7 From a1303c83f37e72ec45811934376e94ab7fbfd3dd Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Tue, 21 Jun 2022 14:44:14 +0000 Subject: [PATCH 033/723] internal/lsp: remove tooltip from inlay hints The tooltip for truncated inlay hint labels is redundant with the hover state of the target identifier. This matches the behavior of inlay hint implementations in other languages. Change-Id: I209054f8c65df504cae67121e3cbc3eacaf02710 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413417 Run-TryBot: Jamal Carvalho Reviewed-by: Suzy Mueller gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/lsp/source/inlay_hint.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 8fe46b27a89..8369681003a 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -265,7 +265,6 @@ func buildLabel(s string) []protocol.InlayHintLabelPart { } if len(s) > maxLabelLength+len("...") { label.Value = s[:maxLabelLength] + "..." - label.Tooltip = s } return []protocol.InlayHintLabelPart{label} } From 59bd4faed9b3724797fdffa7c1476ff8fb8a4878 Mon Sep 17 00:00:00 2001 From: Dylan Le Date: Wed, 25 May 2022 15:59:38 -0400 Subject: [PATCH 034/723] internal/lsp: find references to package Update References to detect if the package is referenced and a regtest to test within and external package references. Updates golang/go#41567 Change-Id: I607a47bf15f1c9f8236336f795fcef081db49d6a Reviewed-on: https://go-review.googlesource.com/c/tools/+/408714 Reviewed-by: Robert Findley Run-TryBot: Dylan Le gopls-CI: kokoro --- .../internal/regtest/misc/references_test.go | 88 +++++++++++++++++++ internal/lsp/source/references.go | 60 +++++++++++++ 2 files changed, 148 insertions(+) diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index 768251680f9..de2e9b97fd8 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -5,6 +5,8 @@ package misc import ( + "fmt" + "strings" "testing" . "golang.org/x/tools/internal/lsp/regtest" @@ -81,3 +83,89 @@ func _() { } }) } + +func TestPackageReferences(t *testing.T) { + tests := []struct { + packageName string + wantRefCount int + wantFiles []string + }{ + { + "lib1", + 3, + []string{ + "main.go", + "lib1/a.go", + "lib1/b.go", + }, + }, + { + "lib2", + 2, + []string{ + "main.go", + "lib2/a.go", + }, + }, + } + + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib1/a.go -- +package lib1 + +const A = 1 + +-- lib1/b.go -- +package lib1 + +const B = 1 + +-- lib2/a.go -- +package lib2 + +const C = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib1" + "mod.com/lib2" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + for _, test := range tests { + f := fmt.Sprintf("%s/a.go", test.packageName) + env.OpenFile(f) + pos := env.RegexpSearch(f, test.packageName) + refs := env.References(fmt.Sprintf("%s/a.go", test.packageName), pos) + if len(refs) != test.wantRefCount { + t.Fatalf("got %v reference(s), want %d", len(refs), test.wantRefCount) + } + var refURIs []string + for _, ref := range refs { + refURIs = append(refURIs, string(ref.URI)) + } + for _, base := range test.wantFiles { + hasBase := false + for _, ref := range refURIs { + if strings.HasSuffix(ref, base) { + hasBase = true + break + } + } + if !hasBase { + t.Fatalf("got [%v], want reference ends with \"%v\"", strings.Join(refURIs, ","), base) + } + } + } + }) +} diff --git a/internal/lsp/source/references.go b/internal/lsp/source/references.go index 3541600b207..85bf41a21b0 100644 --- a/internal/lsp/source/references.go +++ b/internal/lsp/source/references.go @@ -9,12 +9,15 @@ import ( "errors" "fmt" "go/ast" + "go/token" "go/types" "sort" + "strconv" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/lsp/safetoken" "golang.org/x/tools/internal/span" ) @@ -34,6 +37,63 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit ctx, done := event.Start(ctx, "source.References") defer done() + // Find position of the package name declaration + pgf, err := s.ParseGo(ctx, f, ParseFull) + if err != nil { + return nil, err + } + + cursorOffset, err := pgf.Mapper.Offset(pp) + if err != nil { + return nil, err + } + + packageNameStart, err := safetoken.Offset(pgf.Tok, pgf.File.Name.Pos()) + if err != nil { + return nil, err + } + + packageNameEnd, err := safetoken.Offset(pgf.Tok, pgf.File.Name.End()) + if err != nil { + return nil, err + } + + if packageNameStart <= cursorOffset && cursorOffset < packageNameEnd { + renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckAll, NarrowestPackage) + if err != nil { + return nil, err + } + + // Find external references to the package. + rdeps, err := s.GetReverseDependencies(ctx, renamingPkg.ID()) + if err != nil { + return nil, err + } + var refs []*ReferenceInfo + for _, dep := range rdeps { + for _, f := range dep.CompiledGoFiles() { + for _, imp := range f.File.Imports { + if path, err := strconv.Unquote(imp.Path.Value); err == nil && path == renamingPkg.PkgPath() { + refs = append(refs, &ReferenceInfo{ + Name: pgf.File.Name.Name, + MappedRange: NewMappedRange(s.FileSet(), f.Mapper, imp.Pos(), imp.End()), + }) + } + } + } + } + + // Find internal references to the package within the package itself + for _, f := range renamingPkg.CompiledGoFiles() { + refs = append(refs, &ReferenceInfo{ + Name: pgf.File.Name.Name, + MappedRange: NewMappedRange(s.FileSet(), f.Mapper, f.File.Name.Pos(), f.File.Name.End()), + }) + } + + return refs, nil + } + qualifiedObjs, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) // Don't return references for builtin types. if errors.Is(err, errBuiltin) { From 63d8015eb823e14eb3184232334cf911e5625815 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 16 Jun 2022 17:58:27 -0400 Subject: [PATCH 035/723] internal/lsp/cache: minor simplifications to Symbols Minor cleanups based on studying the code in preparation for saving a persistent index: - Remove unused error result from Symbols method. - Remove unnecessary fields from symbolHandle. - Add various explanatory comments. - In workspace_symbols.go: - separate extract and match phases of collectSymbols clearly - replace symbolCollector and matchWorker types by simple parameters - combine loops (roots, buildMatcher) - move buildMatcher creation down to where it is needed. Change-Id: Ifcad61a9a8c7d70f573024bcfa76d476552ee428 Reviewed-on: https://go-review.googlesource.com/c/tools/+/412822 Reviewed-by: Robert Findley --- internal/lsp/cache/cache.go | 1 + internal/lsp/cache/snapshot.go | 13 +- internal/lsp/cache/symbols.go | 30 ++--- internal/lsp/source/view.go | 2 +- internal/lsp/source/workspace_symbol.go | 162 ++++++++++-------------- 5 files changed, 88 insertions(+), 120 deletions(-) diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index 2a8a169d510..3640272688f 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -68,6 +68,7 @@ func (h *fileHandle) Saved() bool { return true } +// GetFile stats and (maybe) reads the file, updates the cache, and returns it. func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { return c.getFile(ctx, uri) } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 9875ae4bd70..32681735b28 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -997,9 +997,7 @@ func (s *snapshot) activePackageHandles(ctx context.Context) ([]*packageHandle, } // Symbols extracts and returns the symbols for each file in all the snapshot's views. -func (s *snapshot) Symbols(ctx context.Context) (map[span.URI][]source.Symbol, error) { - // Keep going on errors, but log the first failure. - // Partial results are better than no symbol results. +func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { var ( group errgroup.Group nprocs = 2 * runtime.GOMAXPROCS(-1) // symbolize is a mix of I/O and CPU @@ -1023,10 +1021,12 @@ func (s *snapshot) Symbols(ctx context.Context) (map[span.URI][]source.Symbol, e return nil }) } + // Keep going on errors, but log the first failure. + // Partial results are better than no symbol results. if err := group.Wait(); err != nil { event.Error(ctx, "getting snapshot symbols", err) } - return result, nil + return result } func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]source.Metadata, error) { @@ -1137,11 +1137,10 @@ func (s *snapshot) getSymbolHandle(uri span.URI) *symbolHandle { return s.symbols[uri] } -func (s *snapshot) addSymbolHandle(sh *symbolHandle) *symbolHandle { +func (s *snapshot) addSymbolHandle(uri span.URI, sh *symbolHandle) *symbolHandle { s.mu.Lock() defer s.mu.Unlock() - uri := sh.fh.URI() // If the package handle has already been cached, // return the cached handle instead of overriding it. if sh, ok := s.symbols[uri]; ok { @@ -1338,7 +1337,7 @@ func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.Versi return fh, nil } - fh, err := s.view.session.cache.getFile(ctx, f.URI()) + fh, err := s.view.session.cache.getFile(ctx, f.URI()) // read the file if err != nil { return nil, err } diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go index bf5e00b1648..d56a036ff6e 100644 --- a/internal/lsp/cache/symbols.go +++ b/internal/lsp/cache/symbols.go @@ -18,13 +18,9 @@ import ( "golang.org/x/tools/internal/memoize" ) +// A symbolHandle contains a handle to the result of symbolizing a file. type symbolHandle struct { handle *memoize.Handle - - fh source.FileHandle - - // key is the hashed key for the package. - key symbolHandleKey } // symbolData contains the data produced by extracting symbols from a file. @@ -33,30 +29,30 @@ type symbolData struct { err error } -type symbolHandleKey source.Hash - +// buildSymbolHandle returns a handle to the result of symbolizing a file, +// if necessary creating it and saving it in the snapshot. func (s *snapshot) buildSymbolHandle(ctx context.Context, fh source.FileHandle) *symbolHandle { if h := s.getSymbolHandle(fh.URI()); h != nil { return h } + type symbolHandleKey source.Hash key := symbolHandleKey(fh.FileIdentity().Hash) - h := s.generation.Bind(key, func(_ context.Context, arg memoize.Arg) interface{} { + handle := s.generation.Bind(key, func(_ context.Context, arg memoize.Arg) interface{} { snapshot := arg.(*snapshot) - data := &symbolData{} - data.symbols, data.err = symbolize(snapshot, fh) - return data + symbols, err := symbolize(snapshot, fh) + return &symbolData{symbols, err} }, nil) sh := &symbolHandle{ - handle: h, - fh: fh, - key: key, + handle: handle, } - return s.addSymbolHandle(sh) + + return s.addSymbolHandle(fh.URI(), sh) } -// symbolize extracts symbols from a file. It uses a parsed file already -// present in the cache but otherwise does not populate the cache. +// symbolize reads and parses a file and extracts symbols from it. +// It may use a parsed file already present in the cache but +// otherwise does not populate the cache. func symbolize(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) { src, err := fh.Read() if err != nil { diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 7960b0c0368..0d8d661d60c 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -175,7 +175,7 @@ type Snapshot interface { ActivePackages(ctx context.Context) ([]Package, error) // Symbols returns all symbols in the snapshot. - Symbols(ctx context.Context) (map[span.URI][]Symbol, error) + Symbols(ctx context.Context) map[span.URI][]Symbol // Metadata returns package metadata associated with the given file URI. MetadataForFile(ctx context.Context, uri span.URI) ([]Metadata, error) diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go index 11e22d17bea..c7cfe5c9ef8 100644 --- a/internal/lsp/source/workspace_symbol.go +++ b/internal/lsp/source/workspace_symbol.go @@ -50,14 +50,26 @@ const maxSymbols = 100 // with a different configured SymbolMatcher per View. Therefore we assume that // Session level configuration will define the SymbolMatcher to be used for the // WorkspaceSymbols method. -func WorkspaceSymbols(ctx context.Context, matcherType SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) { +func WorkspaceSymbols(ctx context.Context, matcher SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) { ctx, done := event.Start(ctx, "source.WorkspaceSymbols") defer done() if query == "" { return nil, nil } - sc := newSymbolCollector(matcherType, style, query) - return sc.walk(ctx, views) + + var s symbolizer + switch style { + case DynamicSymbols: + s = dynamicSymbolMatch + case FullyQualifiedSymbols: + s = fullyQualifiedSymbolMatch + case PackageQualifiedSymbols: + s = packageSymbolMatch + default: + panic(fmt.Errorf("unknown symbol style: %v", style)) + } + + return collectSymbols(ctx, views, matcher, s, query) } // A matcherFunc returns the index and score of a symbol match. @@ -136,43 +148,6 @@ func packageSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]strin return nil, 0 } -// symbolCollector holds context as we walk Packages, gathering symbols that -// match a given query. -// -// How we match symbols is parameterized by two interfaces: -// - A matcherFunc determines how well a string symbol matches a query. It -// returns a non-negative score indicating the quality of the match. A score -// of zero indicates no match. -// - A symbolizer determines how we extract the symbol for an object. This -// enables the 'symbolStyle' configuration option. -type symbolCollector struct { - // These types parameterize the symbol-matching pass. - matchers []matcherFunc - symbolizer symbolizer - - symbolStore -} - -func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string) *symbolCollector { - var s symbolizer - switch style { - case DynamicSymbols: - s = dynamicSymbolMatch - case FullyQualifiedSymbols: - s = fullyQualifiedSymbolMatch - case PackageQualifiedSymbols: - s = packageSymbolMatch - default: - panic(fmt.Errorf("unknown symbol style: %v", style)) - } - sc := &symbolCollector{symbolizer: s} - sc.matchers = make([]matcherFunc, runtime.GOMAXPROCS(-1)) - for i := range sc.matchers { - sc.matchers[i] = buildMatcher(matcher, query) - } - return sc -} - func buildMatcher(matcher SymbolMatcher, query string) matcherFunc { switch matcher { case SymbolFuzzy: @@ -302,36 +277,42 @@ func (c comboMatcher) match(chunks []string) (int, float64) { return first, score } -func (sc *symbolCollector) walk(ctx context.Context, views []View) ([]protocol.SymbolInformation, error) { - // Use the root view URIs for determining (lexically) whether a uri is in any - // open workspace. - var roots []string - for _, v := range views { - roots = append(roots, strings.TrimRight(string(v.Folder()), "/")) - } - - results := make(chan *symbolStore) - matcherlen := len(sc.matchers) - files := make(map[span.URI]symbolFile) +// collectSymbols calls snapshot.Symbols to walk the syntax trees of +// all files in the views' current snapshots, and returns a sorted, +// scored list of symbols that best match the parameters. +// +// How it matches symbols is parameterized by two interfaces: +// - A matcherFunc determines how well a string symbol matches a query. It +// returns a non-negative score indicating the quality of the match. A score +// of zero indicates no match. +// - A symbolizer determines how we extract the symbol for an object. This +// enables the 'symbolStyle' configuration option. +// +func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher, symbolizer symbolizer, query string) ([]protocol.SymbolInformation, error) { + // Extract symbols from all files. + var work []symbolFile + var roots []string + seen := make(map[span.URI]bool) + // TODO(adonovan): opt: parallelize this loop? How often is len > 1? for _, v := range views { snapshot, release := v.Snapshot(ctx) defer release() - psyms, err := snapshot.Symbols(ctx) - if err != nil { - return nil, err - } + + // Use the root view URIs for determining (lexically) + // whether a URI is in any open workspace. + roots = append(roots, strings.TrimRight(string(v.Folder()), "/")) filters := v.Options().DirectoryFilters folder := filepath.ToSlash(v.Folder().Filename()) - for uri, syms := range psyms { + for uri, syms := range snapshot.Symbols(ctx) { norm := filepath.ToSlash(uri.Filename()) nm := strings.TrimPrefix(norm, folder) if FiltersDisallow(nm, filters) { continue } // Only scan each file once. - if _, ok := files[uri]; ok { + if seen[uri] { continue } mds, err := snapshot.MetadataForFile(ctx, uri) @@ -343,39 +324,37 @@ func (sc *symbolCollector) walk(ctx context.Context, views []View) ([]protocol.S // TODO: should use the bug reporting API continue } - files[uri] = symbolFile{uri, mds[0], syms} + seen[uri] = true + work = append(work, symbolFile{uri, mds[0], syms}) } } - var work []symbolFile - for _, f := range files { - work = append(work, f) - } - - // Compute matches concurrently. Each symbolWorker has its own symbolStore, + // Match symbols in parallel. + // Each worker has its own symbolStore, // which we merge at the end. - for i, matcher := range sc.matchers { - go func(i int, matcher matcherFunc) { - w := &symbolWorker{ - symbolizer: sc.symbolizer, - matcher: matcher, - ss: &symbolStore{}, - roots: roots, - } - for j := i; j < len(work); j += matcherlen { - w.matchFile(work[j]) + nmatchers := runtime.GOMAXPROCS(-1) // matching is CPU bound + results := make(chan *symbolStore) + for i := 0; i < nmatchers; i++ { + go func(i int) { + matcher := buildMatcher(matcherType, query) + store := new(symbolStore) + // Assign files to workers in round-robin fashion. + for j := i; j < len(work); j += nmatchers { + matchFile(store, symbolizer, matcher, roots, work[j]) } - results <- w.ss - }(i, matcher) + results <- store + }(i) } - for i := 0; i < matcherlen; i++ { - ss := <-results - for _, si := range ss.res { - sc.store(si) + // Gather and merge results as they arrive. + var unified symbolStore + for i := 0; i < nmatchers; i++ { + store := <-results + for _, syms := range store.res { + unified.store(syms) } } - return sc.results(), nil + return unified.results(), nil } // FilterDisallow is code from the body of cache.pathExcludedByFilter in cache/view.go @@ -407,20 +386,13 @@ type symbolFile struct { syms []Symbol } -// symbolWorker matches symbols and captures the highest scoring results. -type symbolWorker struct { - symbolizer symbolizer - matcher matcherFunc - ss *symbolStore - roots []string -} - -func (w *symbolWorker) matchFile(i symbolFile) { +// matchFile scans a symbol file and adds matching symbols to the store. +func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, roots []string, i symbolFile) { for _, sym := range i.syms { - symbolParts, score := w.symbolizer(sym.Name, i.md, w.matcher) + symbolParts, score := symbolizer(sym.Name, i.md, matcher) // Check if the score is too low before applying any downranking. - if w.ss.tooLow(score) { + if store.tooLow(score) { continue } @@ -463,7 +435,7 @@ func (w *symbolWorker) matchFile(i symbolFile) { } inWorkspace := false - for _, root := range w.roots { + for _, root := range roots { if strings.HasPrefix(string(i.uri), root) { inWorkspace = true break @@ -484,7 +456,7 @@ func (w *symbolWorker) matchFile(i symbolFile) { } score *= 1.0 - depth*depthFactor - if w.ss.tooLow(score) { + if store.tooLow(score) { continue } @@ -496,7 +468,7 @@ func (w *symbolWorker) matchFile(i symbolFile) { rng: sym.Range, container: i.md.PackagePath(), } - w.ss.store(si) + store.store(si) } } From cbb8e8e9232d6ad09d1b469391c0161db87be765 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 13 Jun 2022 15:09:32 -0400 Subject: [PATCH 036/723] internal/span: optimise URI.Filename to avoid allocation This change adds a fast-path check for the common case: "file:", lowercase, followed by a simple POSIX absolute file name without special characters. This function used to account for 1% of CPU on the DidChange benchmark (and I'm sure I've seen higher fractions on other tests--but perhaps that was before the clone optimizations?). It was tested by adding an assertion that it agrees with the slow path and running all our tests. Change-Id: I15492b8a317715468870b00041bf8f6b0bb53bb2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411900 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro --- internal/span/uri.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/span/uri.go b/internal/span/uri.go index f2b39ca424e..8132665d75f 100644 --- a/internal/span/uri.go +++ b/internal/span/uri.go @@ -35,13 +35,29 @@ func (uri URI) Filename() string { } func filename(uri URI) (string, error) { - // This function is frequently called and its cost is - // dominated by the allocation of a net.URL. - // TODO(adonovan): opt: replace by a bespoke parseFileURI - // function that doesn't allocate. if uri == "" { return "", nil } + + // This conservative check for the common case + // of a simple non-empty absolute POSIX filename + // avoids the allocation of a net.URL. + if strings.HasPrefix(string(uri), "file:///") { + rest := string(uri)[len("file://"):] // leave one slash + for i := 0; i < len(rest); i++ { + b := rest[i] + // Reject these cases: + if b < ' ' || b == 0x7f || // control character + b == '%' || b == '+' || // URI escape + b == ':' || // Windows drive letter + b == '@' || b == '&' || b == '?' { // authority or query + goto slow + } + } + return rest, nil + } +slow: + u, err := url.ParseRequestURI(string(uri)) if err != nil { return "", err @@ -54,6 +70,7 @@ func filename(uri URI) (string, error) { if isWindowsDriveURIPath(u.Path) { u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:] } + return u.Path, nil } From a2de63544e7a13883025fb60af623a1e787df32e Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 12 Apr 2022 09:45:50 -0400 Subject: [PATCH 037/723] internal/lsp/cache: honor the go.work for computing workspace packages When using Go workspaces, the go.work file should be used to determine which packages are workspace packages. For golang/go#48929 Change-Id: I1a8753ab7887daf193e093fca5070b4cc250a245 Reviewed-on: https://go-review.googlesource.com/c/tools/+/400822 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- .../regtest/workspace/workspace_test.go | 64 +++++++++ internal/lsp/cache/load.go | 128 ++++++++++++++---- internal/lsp/cache/metadata.go | 7 - internal/lsp/cache/session.go | 2 + internal/lsp/cache/snapshot.go | 23 +++- internal/lsp/cache/view.go | 19 ++- internal/lsp/diagnostics.go | 2 + internal/lsp/regtest/expectation.go | 2 +- internal/lsp/source/util.go | 12 +- 9 files changed, 213 insertions(+), 46 deletions(-) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 5e5bcd13b5d..9e4b85fced8 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1305,3 +1305,67 @@ func (Server) Foo() {} _, _ = env.GoToDefinition("other_test.go", env.RegexpSearch("other_test.go", "Server")) }) } + +// Test for golang/go#48929. +func TestClearNonWorkspaceDiagnostics(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work + + const ws = ` +-- go.work -- +go 1.18 + +use ( + ./b +) +-- a/go.mod -- +module a + +go 1.17 +-- a/main.go -- +package main + +func main() { + var V string +} +-- b/go.mod -- +module b + +go 1.17 +-- b/main.go -- +package b + +import ( + _ "fmt" +) +` + Run(t, ws, func(t *testing.T, env *Env) { + env.OpenFile("b/main.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + NoDiagnostics("a/main.go"), + ), + ) + env.OpenFile("a/main.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexpWithMessage("a/main.go", "V", "declared but not used"), + ), + ) + env.CloseBuffer("a/main.go") + + // Make an arbitrary edit because gopls explicitly diagnoses a/main.go + // whenever it is "changed". + // + // TODO(rfindley): it should not be necessary to make another edit here. + // Gopls should be smart enough to avoid diagnosing a. + env.RegexpReplace("b/main.go", "package b", "package b // a package") + env.Await( + OnceMet( + env.DoneWithChange(), + EmptyDiagnostics("a/main.go"), + ), + ) + }) +} diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 5f24d0f08ef..5ce49f00d43 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -197,7 +197,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } // TODO: once metadata is immutable, we shouldn't have to lock here. s.mu.Lock() - err := s.computeMetadataUpdates(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, updates, nil) + err := computeMetadataUpdates(ctx, s.meta, PackagePath(pkg.PkgPath), pkg, cfg, query, updates, nil) s.mu.Unlock() if err != nil { return err @@ -216,7 +216,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf delete(s.packages, key) } } - s.workspacePackages = computeWorkspacePackages(s.meta) + s.workspacePackages = computeWorkspacePackagesLocked(s, s.meta) s.dumpWorkspace("load") s.mu.Unlock() @@ -442,7 +442,7 @@ func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generati // computeMetadataUpdates populates the updates map with metadata updates to // apply, based on the given pkg. It recurs through pkg.Imports to ensure that // metadata exists for all dependencies. -func (s *snapshot) computeMetadataUpdates(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { +func computeMetadataUpdates(ctx context.Context, g *metadataGraph, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { id := PackageID(pkg.ID) if new := updates[id]; new != nil { return nil @@ -494,28 +494,13 @@ func (s *snapshot) computeMetadataUpdates(ctx context.Context, pkgPath PackagePa m.Errors = append(m.Errors, err) } - uris := map[span.URI]struct{}{} for _, filename := range pkg.CompiledGoFiles { uri := span.URIFromPath(filename) m.CompiledGoFiles = append(m.CompiledGoFiles, uri) - uris[uri] = struct{}{} } for _, filename := range pkg.GoFiles { uri := span.URIFromPath(filename) m.GoFiles = append(m.GoFiles, uri) - uris[uri] = struct{}{} - } - - for uri := range uris { - // In order for a package to be considered for the workspace, at least one - // file must be contained in the workspace and not vendored. - - // The package's files are in this view. It may be a workspace package. - // Vendored packages are not likely to be interesting to the user. - if !strings.Contains(string(uri), "/vendor/") && s.view.contains(uri) { - m.HasWorkspaceFiles = true - break - } } for importPath, importPkg := range pkg.Imports { @@ -532,8 +517,8 @@ func (s *snapshot) computeMetadataUpdates(ctx context.Context, pkgPath PackagePa m.MissingDeps[importPkgPath] = struct{}{} continue } - if s.noValidMetadataForIDLocked(importID) { - if err := s.computeMetadataUpdates(ctx, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { + if noValidMetadataForID(g, importID) { + if err := computeMetadataUpdates(ctx, g, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { event.Error(ctx, "error in dependency", err) } } @@ -542,12 +527,101 @@ func (s *snapshot) computeMetadataUpdates(ctx context.Context, pkgPath PackagePa return nil } -// computeWorkspacePackages computes workspace packages for the given metadata -// graph. -func computeWorkspacePackages(meta *metadataGraph) map[PackageID]PackagePath { +// containsPackageLocked reports whether p is a workspace package for the +// snapshot s. +// +// s.mu must be held while calling this function. +func containsPackageLocked(s *snapshot, m *Metadata) bool { + // In legacy workspace mode, or if a package does not have an associated + // module, a package is considered inside the workspace if any of its files + // are under the workspace root (and not excluded). + // + // Otherwise if the package has a module it must be an active module (as + // defined by the module root or go.work file) and at least one file must not + // be filtered out by directoryFilters. + if m.Module != nil && s.workspace.moduleSource != legacyWorkspace { + modURI := span.URIFromPath(m.Module.GoMod) + _, ok := s.workspace.activeModFiles[modURI] + if !ok { + return false + } + + uris := map[span.URI]struct{}{} + for _, uri := range m.CompiledGoFiles { + uris[uri] = struct{}{} + } + for _, uri := range m.GoFiles { + uris[uri] = struct{}{} + } + + for uri := range uris { + // Don't use view.contains here. go.work files may include modules + // outside of the workspace folder. + if !strings.Contains(string(uri), "/vendor/") && !s.view.filters(uri) { + return true + } + } + return false + } + + return containsFileInWorkspaceLocked(s, m) +} + +// containsOpenFileLocked reports whether any file referenced by m is open in +// the snapshot s. +// +// s.mu must be held while calling this function. +func containsOpenFileLocked(s *snapshot, m *KnownMetadata) bool { + uris := map[span.URI]struct{}{} + for _, uri := range m.CompiledGoFiles { + uris[uri] = struct{}{} + } + for _, uri := range m.GoFiles { + uris[uri] = struct{}{} + } + + for uri := range uris { + if s.isOpenLocked(uri) { + return true + } + } + return false +} + +// containsFileInWorkspace reports whether m contains any file inside the +// workspace of the snapshot s. +// +// s.mu must be held while calling this function. +func containsFileInWorkspaceLocked(s *snapshot, m *Metadata) bool { + uris := map[span.URI]struct{}{} + for _, uri := range m.CompiledGoFiles { + uris[uri] = struct{}{} + } + for _, uri := range m.GoFiles { + uris[uri] = struct{}{} + } + + for uri := range uris { + // In order for a package to be considered for the workspace, at least one + // file must be contained in the workspace and not vendored. + + // The package's files are in this view. It may be a workspace package. + // Vendored packages are not likely to be interesting to the user. + if !strings.Contains(string(uri), "/vendor/") && s.view.contains(uri) { + return true + } + } + return false +} + +// computeWorkspacePackagesLocked computes workspace packages in the snapshot s +// for the given metadata graph. +// +// s.mu must be held while calling this function. +func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[PackageID]PackagePath { workspacePackages := make(map[PackageID]PackagePath) for _, m := range meta.metadata { - if !m.HasWorkspaceFiles { + if !containsPackageLocked(s, m.Metadata) { continue } if m.PkgFilesChanged { @@ -567,6 +641,12 @@ func computeWorkspacePackages(meta *metadataGraph) map[PackageID]PackagePath { if allFilesHaveRealPackages(meta, m) { continue } + + // We only care about command-line-arguments packages if they are still + // open. + if !containsOpenFileLocked(s, m) { + continue + } } switch { diff --git a/internal/lsp/cache/metadata.go b/internal/lsp/cache/metadata.go index 525a3e65495..b4da7130c23 100644 --- a/internal/lsp/cache/metadata.go +++ b/internal/lsp/cache/metadata.go @@ -67,13 +67,6 @@ type Metadata struct { // TODO(rfindley): this can probably just be a method, since it is derived // from other fields. IsIntermediateTestVariant bool - - // HasWorkspaceFiles reports whether m contains any files that are considered - // part of the workspace. - // - // TODO(golang/go#48929): this should be a property of the workspace - // (the go.work file), not a constant. - HasWorkspaceFiles bool } // Name implements the source.Metadata interface. diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 286d8f12c46..0d3e944b980 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -323,6 +323,8 @@ func bestViewForURI(uri span.URI, views []*View) *View { if longest != nil && len(longest.Folder()) > len(view.Folder()) { continue } + // TODO(rfindley): this should consider the workspace layout (i.e. + // go.work). if view.contains(uri) { longest = view } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 32681735b28..b85b46c64ce 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -768,6 +768,8 @@ func (s *snapshot) isActiveLocked(id PackageID, seen map[PackageID]bool) (active return true } } + // TODO(rfindley): it looks incorrect that we don't also check GoFiles here. + // If a CGo file is open, we want to consider the package active. for _, dep := range m.Deps { if s.isActiveLocked(dep, seen) { return true @@ -1289,11 +1291,11 @@ func (s *snapshot) noValidMetadataForURILocked(uri span.URI) bool { func (s *snapshot) noValidMetadataForID(id PackageID) bool { s.mu.Lock() defer s.mu.Unlock() - return s.noValidMetadataForIDLocked(id) + return noValidMetadataForID(s.meta, id) } -func (s *snapshot) noValidMetadataForIDLocked(id PackageID) bool { - m := s.meta.metadata[id] +func noValidMetadataForID(g *metadataGraph, id PackageID) bool { + m := g.metadata[id] return m == nil || !m.Valid } @@ -1789,8 +1791,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } + // Compute invalidations based on file changes. changedPkgFiles := map[PackageID]bool{} // packages whose file set may have changed anyImportDeleted := false + anyFileOpenedOrClosed := false for uri, change := range changes { // Maybe reinitialize the view if we see a change in the vendor // directory. @@ -1800,6 +1804,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // The original FileHandle for this URI is cached on the snapshot. originalFH := s.files[uri] + var originalOpen, newOpen bool + _, originalOpen = originalFH.(*overlay) + _, newOpen = change.fileHandle.(*overlay) + anyFileOpenedOrClosed = originalOpen != newOpen // If uri is a Go file, check if it has changed in a way that would // invalidate metadata. Note that we can't use s.view.FileKind here, @@ -1903,6 +1911,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC newGen.Inherit(v.handle) result.packages[k] = v } + // Copy the package analysis information. for k, v := range s.actions { if _, ok := idsToInvalidate[k.pkg.id]; ok { @@ -1988,13 +1997,19 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } + // Update metadata, if necessary. if len(metadataUpdates) > 0 { result.meta = s.meta.Clone(metadataUpdates) - result.workspacePackages = computeWorkspacePackages(result.meta) } else { // No metadata changes. Since metadata is only updated by cloning, it is // safe to re-use the existing metadata here. result.meta = s.meta + } + + // Update workspace packages, if necessary. + if result.meta != s.meta || anyFileOpenedOrClosed { + result.workspacePackages = computeWorkspacePackagesLocked(result, result.meta) + } else { result.workspacePackages = s.workspacePackages } diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 0ed9883451b..620efd8b965 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -397,16 +397,27 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) { } func (v *View) contains(uri span.URI) bool { + // TODO(rfindley): should we ignore the root here? It is not provided by the + // user, and is undefined when go.work is outside the workspace. It would be + // better to explicitly consider the set of active modules wherever relevant. inRoot := source.InDir(v.rootURI.Filename(), uri.Filename()) inFolder := source.InDir(v.folder.Filename(), uri.Filename()) + if !inRoot && !inFolder { return false } - // Filters are applied relative to the workspace folder. - if inFolder { - return !pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), v.rootURI.Filename(), v.gomodcache, v.Options()) + + return !v.filters(uri) +} + +// filters reports whether uri is filtered by the currently configured +// directoryFilters. +func (v *View) filters(uri span.URI) bool { + // Only filter relative to the configured root directory. + if source.InDirLex(v.folder.Filename(), uri.Filename()) { + return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), v.rootURI.Filename(), v.gomodcache, v.Options()) } - return true + return false } func (v *View) mapFile(uri span.URI, f *fileBase) { diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 0837b22cc22..9648921ef5d 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -66,6 +66,8 @@ func (d diagnosticSource) String() string { return "FromTypeChecking" case orphanedSource: return "FromOrphans" + case workSource: + return "FromGoWork" default: return fmt.Sprintf("From?%d?", d) } diff --git a/internal/lsp/regtest/expectation.go b/internal/lsp/regtest/expectation.go index ab808f9e8cf..737f83da89c 100644 --- a/internal/lsp/regtest/expectation.go +++ b/internal/lsp/regtest/expectation.go @@ -613,7 +613,7 @@ func NoDiagnostics(name string) Expectation { } return SimpleExpectation{ check: check, - description: "no diagnostics", + description: fmt.Sprintf("no diagnostics for %q", name), } } diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go index 9cb2ee69482..b8a7fc9135f 100644 --- a/internal/lsp/source/util.go +++ b/internal/lsp/source/util.go @@ -471,7 +471,7 @@ func CompareURI(left, right span.URI) int { // // Copied and slightly adjusted from go/src/cmd/go/internal/search/search.go. func InDir(dir, path string) bool { - if inDirLex(dir, path) { + if InDirLex(dir, path) { return true } if !honorSymlinks { @@ -481,18 +481,18 @@ func InDir(dir, path string) bool { if err != nil || xpath == path { xpath = "" } else { - if inDirLex(dir, xpath) { + if InDirLex(dir, xpath) { return true } } xdir, err := filepath.EvalSymlinks(dir) if err == nil && xdir != dir { - if inDirLex(xdir, path) { + if InDirLex(xdir, path) { return true } if xpath != "" { - if inDirLex(xdir, xpath) { + if InDirLex(xdir, xpath) { return true } } @@ -500,11 +500,11 @@ func InDir(dir, path string) bool { return false } -// inDirLex is like inDir but only checks the lexical form of the file names. +// InDirLex is like inDir but only checks the lexical form of the file names. // It does not consider symbolic links. // // Copied from go/src/cmd/go/internal/search/search.go. -func inDirLex(dir, path string) bool { +func InDirLex(dir, path string) bool { pv := strings.ToUpper(filepath.VolumeName(path)) dv := strings.ToUpper(filepath.VolumeName(dir)) path = path[len(pv):] From 4e231cb6f8b5a782a7973e80dbc8ef7183799dad Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Sun, 15 Aug 2021 12:02:50 -0400 Subject: [PATCH 038/723] internal/lsp/cache: don't prune unreachable metadata on clone Package metadata is small; there is no reason not to keep it around, and pruning it on every clone is needless work. Change-Id: I9ea73315cc6b673625f0f7defe1fd61c2e1eb123 Reviewed-on: https://go-review.googlesource.com/c/tools/+/373695 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/lsp/cache/snapshot.go | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index b85b46c64ce..3c46648bff9 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1945,27 +1945,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } - // Collect all of the IDs that are reachable from the workspace packages. - // Any unreachable IDs will have their metadata deleted outright. - reachableID := map[PackageID]bool{} - var addForwardDeps func(PackageID) - addForwardDeps = func(id PackageID) { - if reachableID[id] { - return - } - reachableID[id] = true - m, ok := s.meta.metadata[id] - if !ok { - return - } - for _, depID := range m.Deps { - addForwardDeps(depID) - } - } - for id := range s.workspacePackages { - addForwardDeps(id) - } - // Compute which metadata updates are required. We only need to invalidate // packages directly containing the affected file, and only if it changed in // a relevant way. @@ -1977,12 +1956,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC metadataUpdates[k] = nil continue } - // The ID is not reachable from any workspace package, so it should - // be deleted. - if !reachableID[k] { - metadataUpdates[k] = nil - continue - } valid := v.Valid && !invalidateMetadata pkgFilesChanged := v.PkgFilesChanged || changedPkgFiles[k] shouldLoad := v.ShouldLoad || invalidateMetadata From a44cc76dc11c26d2f2d8d04f05fe0094facfdce0 Mon Sep 17 00:00:00 2001 From: Tormod Erevik Lea Date: Fri, 17 Jun 2022 10:55:46 +0200 Subject: [PATCH 039/723] cmd/stringer: use explicit NeedX values instead of deprecated LoadSyntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ie25f67fd98d34b0a907bd13357e2643b1b79443b Reviewed-on: https://go-review.googlesource.com/c/tools/+/412914 Reviewed-by: Alex Rakoczy Auto-Submit: Daniel Martí gopls-CI: kokoro Reviewed-by: Ian Lance Taylor Reviewed-by: Daniel Martí Run-TryBot: Daniel Martí TryBot-Result: Gopher Robot Run-TryBot: Ian Lance Taylor Auto-Submit: Ian Lance Taylor --- cmd/stringer/stringer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/stringer/stringer.go b/cmd/stringer/stringer.go index 9f9c85a0370..b079985b36c 100644 --- a/cmd/stringer/stringer.go +++ b/cmd/stringer/stringer.go @@ -217,7 +217,7 @@ type Package struct { // parsePackage exits if there is an error. func (g *Generator) parsePackage(patterns []string, tags []string) { cfg := &packages.Config{ - Mode: packages.LoadSyntax, + Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax, // TODO: Need to think about constants in test files. Maybe write type_string_test.go // in a separate pass? For later. Tests: false, From 871637b6476ec258626a649fd4c4e5bc871f9535 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Mon, 13 Jun 2022 19:25:46 -0400 Subject: [PATCH 040/723] internal/lsp: add settings for inlay hints and enable This change adds user settings for enabling inlay hints, modeled roughly after analyzers. This will allow users to turn on specific inlay hints that they like and leave others off. With all of the inlay hints turned off by default, we can now enable inlay hints. Change-Id: Ie5dfcbbab1e0b7312eafcc4aa08cb4fe8a83fc31 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411906 Run-TryBot: Suzy Mueller Reviewed-by: Jamal Carvalho gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/doc/generate.go | 44 +++++++ gopls/doc/inlayHints.md | 73 +++++++++++ gopls/doc/settings.md | 13 ++ internal/lsp/general.go | 1 + internal/lsp/lsp_test.go | 3 + internal/lsp/source/api_json.go | 75 +++++++++++ internal/lsp/source/inlay_hint.go | 206 +++++++++++++++++++++++------- internal/lsp/source/options.go | 35 ++++- internal/lsp/tests/util.go | 9 ++ 9 files changed, 410 insertions(+), 49 deletions(-) create mode 100644 gopls/doc/inlayHints.md diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go index e63653de6bc..c7e0e0ffcc0 100644 --- a/gopls/doc/generate.go +++ b/gopls/doc/generate.go @@ -63,6 +63,9 @@ func doMain(baseDir string, write bool) (bool, error) { if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/analyzers.md"), api, write, rewriteAnalyzers); !ok || err != nil { return ok, err } + if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/inlayHints.md"), api, write, rewriteInlayHints); !ok || err != nil { + return ok, err + } return true, nil } @@ -102,6 +105,7 @@ func loadAPI() (*source.APIJSON, error) { } { api.Analyzers = append(api.Analyzers, loadAnalyzers(m)...) } + api.Hints = loadHints(source.AllInlayHints) for _, category := range []reflect.Value{ reflect.ValueOf(defaults.UserOptions), } { @@ -146,6 +150,14 @@ func loadAPI() (*source.APIJSON, error) { Default: def, }) } + case "hints": + for _, a := range api.Hints { + opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, source.EnumKey{ + Name: fmt.Sprintf("%q", a.Name), + Doc: a.Doc, + Default: strconv.FormatBool(a.Default), + }) + } } } } @@ -488,6 +500,23 @@ func loadAnalyzers(m map[string]*source.Analyzer) []*source.AnalyzerJSON { return json } +func loadHints(m map[string]*source.Hint) []*source.HintJSON { + var sorted []string + for _, h := range m { + sorted = append(sorted, h.Name) + } + sort.Strings(sorted) + var json []*source.HintJSON + for _, name := range sorted { + h := m[name] + json = append(json, &source.HintJSON{ + Name: h.Name, + Doc: h.Doc, + }) + } + return json +} + func lowerFirst(x string) string { if x == "" { return x @@ -699,6 +728,21 @@ func rewriteAnalyzers(doc []byte, api *source.APIJSON) ([]byte, error) { return replaceSection(doc, "Analyzers", section.Bytes()) } +func rewriteInlayHints(doc []byte, api *source.APIJSON) ([]byte, error) { + section := bytes.NewBuffer(nil) + for _, hint := range api.Hints { + fmt.Fprintf(section, "## **%v**\n\n", hint.Name) + fmt.Fprintf(section, "%s\n\n", hint.Doc) + switch hint.Default { + case true: + fmt.Fprintf(section, "**Enabled by default.**\n\n") + case false: + fmt.Fprintf(section, "**Disabled by default. Enable it by setting `\"hints\": {\"%s\": true}`.**\n\n", hint.Name) + } + } + return replaceSection(doc, "Hints", section.Bytes()) +} + func replaceSection(doc []byte, sectionName string, replacement []byte) ([]byte, error) { re := regexp.MustCompile(fmt.Sprintf(`(?s)\n(.*?)`, sectionName, sectionName)) idx := re.FindSubmatchIndex(doc) diff --git a/gopls/doc/inlayHints.md b/gopls/doc/inlayHints.md new file mode 100644 index 00000000000..a4fd3e51554 --- /dev/null +++ b/gopls/doc/inlayHints.md @@ -0,0 +1,73 @@ +# Hints + +This document describes the inlay hints that `gopls` uses inside the editor. + + +## **assign_variable_types** + +Enable/disable inlay hints for variable types in assign statements: + + i/* int/*, j/* int/* := 0, len(r)-1 + +**Disabled by default. Enable it by setting `"hints": {"assign_variable_types": true}`.** + +## **composite_literal_fields** + +Enable/disable inlay hints for composite literal field names: + + {in: "Hello, world", want: "dlrow ,olleH"} + +**Disabled by default. Enable it by setting `"hints": {"composite_literal_fields": true}`.** + +## **composite_literal_types** + +Enable/disable inlay hints for composite literal types: + + for _, c := range []struct { + in, want string + }{ + /*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"}, + } + +**Disabled by default. Enable it by setting `"hints": {"composite_literal_types": true}`.** + +## **constant_values** + +Enable/disable inlay hints for constant values: + + const ( + KindNone Kind = iota/* = 0*/ + KindPrint/* = 1*/ + KindPrintf/* = 2*/ + KindErrorf/* = 3*/ + ) + +**Disabled by default. Enable it by setting `"hints": {"constant_values": true}`.** + +## **function_type_parameters** + +Enable/disable inlay hints for implicit type parameters on generic functions: + + myFoo/*[int, string]*/(1, "hello") + +**Disabled by default. Enable it by setting `"hints": {"function_type_parameters": true}`.** + +## **parameter_names** + +Enable/disable inlay hints for parameter names: + + parseInt(/* str: */ "123", /* radix: */ 8) + +**Disabled by default. Enable it by setting `"hints": {"parameter_names": true}`.** + +## **range_variable_types** + +Enable/disable inlay hints for variable types in range statements: + + for k/* int*/, v/* string/* := range []string{} { + fmt.Println(k, v) + } + +**Disabled by default. Enable it by setting `"hints": {"range_variable_types": true}`.** + + diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 092a3c7cfaf..0ed0e19bb02 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -35,6 +35,7 @@ still be able to independently override specific experimental features. * [Completion](#completion) * [Diagnostic](#diagnostic) * [Documentation](#documentation) + * [Inlayhint](#inlayhint) * [Navigation](#navigation) ### Build @@ -370,6 +371,18 @@ linksInHover toggles the presence of links to documentation in hover. Default: `true`. +#### Inlayhint + +##### **hints** *map[string]bool* + +**This setting is experimental and may be deleted.** + +hints specify inlay hints that users want to see. +A full list of hints that gopls uses can be found +[here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). + +Default: `{}`. + #### Navigation ##### **importShortcut** *enum* diff --git a/internal/lsp/general.go b/internal/lsp/general.go index 478152bdf9a..385a04a25fd 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -153,6 +153,7 @@ See https://github.com/golang/go/issues/45732 for more information.`, HoverProvider: true, DocumentHighlightProvider: true, DocumentLinkProvider: protocol.DocumentLinkOptions{}, + InlayHintProvider: protocol.InlayHintOptions{}, ReferencesProvider: true, RenameProvider: renameOpts, SignatureHelpProvider: protocol.SignatureHelpOptions{ diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 56356e9b5a4..2ec833b860e 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -67,6 +67,9 @@ func testLSP(t *testing.T, datum *tests.Data) { tests.EnableAllAnalyzers(view, options) view.SetOptions(ctx, options) + // Enable all inlay hints for tests. + tests.EnableAllInlayHints(view, options) + // Only run the -modfile specific tests in module mode with Go 1.14 or above. datum.ModfileFlagAvailable = len(snapshot.ModFiles()) > 0 && testenv.Go1Point() >= 14 release() diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 0695efc2fa5..4188d9d06fe 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -505,6 +505,51 @@ var GeneratedAPIJSON = &APIJSON{ Status: "experimental", Hierarchy: "ui.diagnostic", }, + { + Name: "hints", + Type: "map[string]bool", + Doc: "hints specify inlay hints that users want to see.\nA full list of hints that gopls uses can be found\n[here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n", + EnumKeys: EnumKeys{Keys: []EnumKey{ + { + Name: "\"assign_variable_types\"", + Doc: "Enable/disable inlay hints for variable types in assign statements:\n\n\ti/* int/*, j/* int/* := 0, len(r)-1", + Default: "false", + }, + { + Name: "\"composite_literal_fields\"", + Doc: "Enable/disable inlay hints for composite literal field names:\n\n\t{in: \"Hello, world\", want: \"dlrow ,olleH\"}", + Default: "false", + }, + { + Name: "\"composite_literal_types\"", + Doc: "Enable/disable inlay hints for composite literal types:\n\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}", + Default: "false", + }, + { + Name: "\"constant_values\"", + Doc: "Enable/disable inlay hints for constant values:\n\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)", + Default: "false", + }, + { + Name: "\"function_type_parameters\"", + Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n\n\tmyFoo/*[int, string]*/(1, \"hello\")", + Default: "false", + }, + { + Name: "\"parameter_names\"", + Doc: "Enable/disable inlay hints for parameter names:\n\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)", + Default: "false", + }, + { + Name: "\"range_variable_types\"", + Doc: "Enable/disable inlay hints for variable types in range statements:\n\n\tfor k/* int*/, v/* string/* := range []string{} {\n\t\tfmt.Println(k, v)\n\t}", + Default: "false", + }, + }}, + Default: "{}", + Status: "experimental", + Hierarchy: "ui.inlayhint", + }, { Name: "codelenses", Type: "map[string]bool", @@ -979,4 +1024,34 @@ var GeneratedAPIJSON = &APIJSON{ Default: true, }, }, + Hints: []*HintJSON{ + { + Name: "assign_variable_types", + Doc: "Enable/disable inlay hints for variable types in assign statements:\n\n\ti/* int/*, j/* int/* := 0, len(r)-1", + }, + { + Name: "composite_literal_fields", + Doc: "Enable/disable inlay hints for composite literal field names:\n\n\t{in: \"Hello, world\", want: \"dlrow ,olleH\"}", + }, + { + Name: "composite_literal_types", + Doc: "Enable/disable inlay hints for composite literal types:\n\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}", + }, + { + Name: "constant_values", + Doc: "Enable/disable inlay hints for constant values:\n\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)", + }, + { + Name: "function_type_parameters", + Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n\n\tmyFoo/*[int, string]*/(1, \"hello\")", + }, + { + Name: "parameter_names", + Doc: "Enable/disable inlay hints for parameter names:\n\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)", + }, + { + Name: "range_variable_types", + Doc: "Enable/disable inlay hints for variable types in range statements:\n\n\tfor k/* int*/, v/* string/* := range []string{} {\n\t\tfmt.Println(k, v)\n\t}", + }, + }, } diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 8369681003a..99e1ad09d82 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -23,6 +23,87 @@ const ( maxLabelLength = 28 ) +type InlayHintFunc func(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint + +type Hint struct { + Name string + Doc string + Run InlayHintFunc +} + +const ( + ParameterNames = "parameter_names" + AssignVariableTypes = "assign_variable_types" + ConstantValues = "constant_values" + RangeVariableTypes = "range_variable_types" + CompositeLiteralTypes = "composite_literal_types" + CompositeLiteralFieldNames = "composite_literal_fields" + FunctionTypeParameters = "function_type_parameters" +) + +var AllInlayHints = map[string]*Hint{ + AssignVariableTypes: { + Name: AssignVariableTypes, + Doc: `Enable/disable inlay hints for variable types in assign statements: + + i/* int/*, j/* int/* := 0, len(r)-1`, + Run: assignVariableTypes, + }, + ParameterNames: { + Name: ParameterNames, + Doc: `Enable/disable inlay hints for parameter names: + + parseInt(/* str: */ "123", /* radix: */ 8)`, + Run: parameterNames, + }, + ConstantValues: { + Name: ConstantValues, + Doc: `Enable/disable inlay hints for constant values: + + const ( + KindNone Kind = iota/* = 0*/ + KindPrint/* = 1*/ + KindPrintf/* = 2*/ + KindErrorf/* = 3*/ + )`, + Run: constantValues, + }, + RangeVariableTypes: { + Name: RangeVariableTypes, + Doc: `Enable/disable inlay hints for variable types in range statements: + + for k/* int*/, v/* string/* := range []string{} { + fmt.Println(k, v) + }`, + Run: rangeVariableTypes, + }, + CompositeLiteralTypes: { + Name: CompositeLiteralTypes, + Doc: `Enable/disable inlay hints for composite literal types: + + for _, c := range []struct { + in, want string + }{ + /*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"}, + }`, + Run: compositeLiteralTypes, + }, + CompositeLiteralFieldNames: { + Name: CompositeLiteralFieldNames, + Doc: `Enable/disable inlay hints for composite literal field names: + + {in: "Hello, world", want: "dlrow ,olleH"}`, + Run: compositeLiteralFields, + }, + FunctionTypeParameters: { + Name: FunctionTypeParameters, + Doc: `Enable/disable inlay hints for implicit type parameters on generic functions: + + myFoo/*[int, string]*/(1, "hello")`, + Run: funcTypeParams, + }, +} + func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol.Range) ([]protocol.InlayHint, error) { ctx, done := event.Start(ctx, "source.InlayHint") defer done() @@ -32,38 +113,47 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol return nil, fmt.Errorf("getting file for InlayHint: %w", err) } + // Collect a list of the inlay hints that are enabled. + inlayHintOptions := snapshot.View().Options().InlayHintOptions + var enabledHints []InlayHintFunc + for hint, enabled := range inlayHintOptions.Hints { + if !enabled { + continue + } + if h, ok := AllInlayHints[hint]; ok { + enabledHints = append(enabledHints, h.Run) + } + } + if len(enabledHints) == 0 { + return nil, nil + } + tmap := lsppos.NewTokenMapper(pgf.Src, pgf.Tok) info := pkg.GetTypesInfo() q := Qualifier(pgf.File, pkg.GetTypes(), info) var hints []protocol.InlayHint ast.Inspect(pgf.File, func(node ast.Node) bool { - switch n := node.(type) { - case *ast.CallExpr: - hints = append(hints, parameterNames(n, tmap, info)...) - hints = append(hints, funcTypeParams(n, tmap, info)...) - case *ast.AssignStmt: - hints = append(hints, assignVariableTypes(n, tmap, info, &q)...) - case *ast.RangeStmt: - hints = append(hints, rangeVariableTypes(n, tmap, info, &q)...) - case *ast.GenDecl: - hints = append(hints, constantValues(n, tmap, info)...) - case *ast.CompositeLit: - hints = append(hints, compositeLiterals(n, tmap, info, &q)...) + for _, fn := range enabledHints { + hints = append(hints, fn(node, tmap, info, &q)...) } return true }) return hints, nil } -func parameterNames(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint { - signature, ok := info.TypeOf(node.Fun).(*types.Signature) +func parameterNames(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return nil + } + signature, ok := info.TypeOf(callExpr.Fun).(*types.Signature) if !ok { return nil } var hints []protocol.InlayHint - for i, v := range node.Args { + for i, v := range callExpr.Args { start, ok := tmap.Position(v.Pos()) if !ok { continue @@ -92,8 +182,12 @@ func parameterNames(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.In return hints } -func funcTypeParams(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint { - id, ok := node.Fun.(*ast.Ident) +func funcTypeParams(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { + ce, ok := node.(*ast.CallExpr) + if !ok { + return nil + } + id, ok := ce.Fun.(*ast.Ident) if !ok { return nil } @@ -119,12 +213,14 @@ func funcTypeParams(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.In }} } -func assignVariableTypes(node *ast.AssignStmt, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { - if node.Tok != token.DEFINE { +func assignVariableTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + stmt, ok := node.(*ast.AssignStmt) + if !ok || stmt.Tok != token.DEFINE { return nil } + var hints []protocol.InlayHint - for _, v := range node.Lhs { + for _, v := range stmt.Lhs { if h := variableType(v, tmap, info, q); h != nil { hints = append(hints, *h) } @@ -132,12 +228,16 @@ func assignVariableTypes(node *ast.AssignStmt, tmap *lsppos.TokenMapper, info *t return hints } -func rangeVariableTypes(node *ast.RangeStmt, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func rangeVariableTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + rStmt, ok := node.(*ast.RangeStmt) + if !ok { + return nil + } var hints []protocol.InlayHint - if h := variableType(node.Key, tmap, info, q); h != nil { + if h := variableType(rStmt.Key, tmap, info, q); h != nil { hints = append(hints, *h) } - if h := variableType(node.Value, tmap, info, q); h != nil { + if h := variableType(rStmt.Value, tmap, info, q); h != nil { hints = append(hints, *h) } return hints @@ -160,13 +260,14 @@ func variableType(e ast.Expr, tmap *lsppos.TokenMapper, info *types.Info, q *typ } } -func constantValues(node *ast.GenDecl, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint { - if node.Tok != token.CONST { +func constantValues(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { + genDecl, ok := node.(*ast.GenDecl) + if !ok || genDecl.Tok != token.CONST { return nil } var hints []protocol.InlayHint - for _, v := range node.Specs { + for _, v := range genDecl.Specs { spec, ok := v.(*ast.ValueSpec) if !ok { continue @@ -210,36 +311,26 @@ func constantValues(node *ast.GenDecl, tmap *lsppos.TokenMapper, info *types.Inf return hints } -func compositeLiterals(node *ast.CompositeLit, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { - typ := info.TypeOf(node) +func compositeLiteralFields(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + compLit, ok := node.(*ast.CompositeLit) + if !ok { + return nil + } + typ := info.TypeOf(compLit) if typ == nil { return nil } - - prefix := "" if t, ok := typ.(*types.Pointer); ok { typ = t.Elem() - prefix = "&" } - strct, ok := typ.Underlying().(*types.Struct) if !ok { return nil } var hints []protocol.InlayHint - if node.Type == nil { - // The type for this struct is implicit, add an inlay hint. - if start, ok := tmap.Position(node.Lbrace); ok { - hints = append(hints, protocol.InlayHint{ - Position: &start, - Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))), - Kind: protocol.Type, - }) - } - } - for i, v := range node.Elts { + for i, v := range compLit.Elts { if _, ok := v.(*ast.KeyValueExpr); !ok { start, ok := tmap.Position(v.Pos()) if !ok { @@ -259,6 +350,35 @@ func compositeLiterals(node *ast.CompositeLit, tmap *lsppos.TokenMapper, info *t return hints } +func compositeLiteralTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + compLit, ok := node.(*ast.CompositeLit) + if !ok { + return nil + } + typ := info.TypeOf(compLit) + if typ == nil { + return nil + } + if compLit.Type != nil { + return nil + } + prefix := "" + if t, ok := typ.(*types.Pointer); ok { + typ = t.Elem() + prefix = "&" + } + // The type for this composite literal is implicit, add an inlay hint. + start, ok := tmap.Position(compLit.Lbrace) + if !ok { + return nil + } + return []protocol.InlayHint{{ + Position: &start, + Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))), + Kind: protocol.Type, + }} +} + func buildLabel(s string) []protocol.InlayHintLabelPart { label := protocol.InlayHintLabelPart{ Value: s, diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index d1d34efe787..5da14ebfe92 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -130,6 +130,7 @@ func DefaultOptions() *Options { Nil: true, }, }, + InlayHintOptions: InlayHintOptions{}, DocumentationOptions: DocumentationOptions{ HoverKind: FullDocumentation, LinkTarget: "pkg.go.dev", @@ -289,6 +290,7 @@ type UIOptions struct { CompletionOptions NavigationOptions DiagnosticOptions + InlayHintOptions // Codelenses overrides the enabled/disabled state of code lenses. See the // "Code Lenses" section of the @@ -407,6 +409,13 @@ type DiagnosticOptions struct { ExperimentalWatchedFileDelay time.Duration `status:"experimental"` } +type InlayHintOptions struct { + // Hints specify inlay hints that users want to see. + // A full list of hints that gopls uses can be found + // [here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). + Hints map[string]bool `status:"experimental"` +} + type NavigationOptions struct { // ImportShortcut specifies whether import statements should link to // documentation or go to definitions. @@ -915,6 +924,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "analyses": result.setBoolMap(&o.Analyses) + case "hints": + result.setBoolMap(&o.Hints) + case "annotations": result.setAnnotationMap(&o.Annotations) @@ -1351,6 +1363,7 @@ type APIJSON struct { Commands []*CommandJSON Lenses []*LensJSON Analyzers []*AnalyzerJSON + Hints []*HintJSON } type OptionJSON struct { @@ -1416,12 +1429,8 @@ func collectEnums(opt *OptionJSON) string { } func shouldShowEnumKeysInSettings(name string) bool { - // Both of these fields have too many possible options to print. - return !hardcodedEnumKeys(name) -} - -func hardcodedEnumKeys(name string) bool { - return name == "analyses" || name == "codelenses" + // These fields have too many possible options to print. + return !(name == "analyses" || name == "codelenses" || name == "hints") } type EnumKeys struct { @@ -1489,3 +1498,17 @@ func (a *AnalyzerJSON) String() string { func (a *AnalyzerJSON) Write(w io.Writer) { fmt.Fprintf(w, "%s (%s): %v", a.Name, a.Doc, a.Default) } + +type HintJSON struct { + Name string + Doc string + Default bool +} + +func (h *HintJSON) String() string { + return h.Name +} + +func (h *HintJSON) Write(w io.Writer) { + fmt.Fprintf(w, "%s (%s): %v", h.Name, h.Doc, h.Default) +} diff --git a/internal/lsp/tests/util.go b/internal/lsp/tests/util.go index 11dda1f8edd..98562d63657 100644 --- a/internal/lsp/tests/util.go +++ b/internal/lsp/tests/util.go @@ -512,6 +512,15 @@ func EnableAllAnalyzers(view source.View, opts *source.Options) { } } +func EnableAllInlayHints(view source.View, opts *source.Options) { + if opts.Hints == nil { + opts.Hints = make(map[string]bool) + } + for name := range source.AllInlayHints { + opts.Hints[name] = true + } +} + func WorkspaceSymbolsString(ctx context.Context, data *Data, queryURI span.URI, symbols []protocol.SymbolInformation) (string, error) { queryDir := filepath.Dir(queryURI.Filename()) var filtered []string From f60e9bc48f8c1667440e2c0176c5e8931e775886 Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Wed, 22 Jun 2022 00:22:43 +0000 Subject: [PATCH 041/723] internal/lsp/cache: use persistent map for storing gofiles in the snapshot Use treap (https://en.wikipedia.org/wiki/Treap) as a persistent map to avoid copying s.goFiles across generations. Maintain an additional s.parseKeysByURIMap to avoid scanning s.goFiles on individual file's content invalidation. This on average reduces didChange latency on internal codebase from 160ms to 150ms. In a followup the same approach can be used to avoid copying s.files, s.packages, and s.knownSubdirs. Updates golang/go#45686 Change-Id: Ic4a9b3c8fb2b66256f224adf9896ddcaaa6865b1 GitHub-Last-Rev: 0abd2570ae9b20ea7126ff31bee69aa0dc3f40aa GitHub-Pull-Request: golang/tools#382 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411554 Reviewed-by: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/cache/maps.go | 112 +++++++++++ internal/lsp/cache/parse.go | 4 +- internal/lsp/cache/session.go | 3 +- internal/lsp/cache/snapshot.go | 150 ++++----------- internal/lsp/cache/view.go | 4 +- internal/lsp/source/view.go | 5 + internal/memoize/memoize.go | 104 ++++++++-- internal/memoize/memoize_test.go | 55 ++++++ internal/persistent/map.go | 268 ++++++++++++++++++++++++++ internal/persistent/map_test.go | 316 +++++++++++++++++++++++++++++++ 10 files changed, 879 insertions(+), 142 deletions(-) create mode 100644 internal/lsp/cache/maps.go create mode 100644 internal/persistent/map.go create mode 100644 internal/persistent/map_test.go diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go new file mode 100644 index 00000000000..70f8039bdac --- /dev/null +++ b/internal/lsp/cache/maps.go @@ -0,0 +1,112 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "golang.org/x/tools/internal/persistent" + "golang.org/x/tools/internal/span" +) + +// TODO(euroelessar): Use generics once support for go1.17 is dropped. + +type goFilesMap struct { + impl *persistent.Map +} + +func newGoFilesMap() *goFilesMap { + return &goFilesMap{ + impl: persistent.NewMap(func(a, b interface{}) bool { + return parseKeyLess(a.(parseKey), b.(parseKey)) + }), + } +} + +func parseKeyLess(a, b parseKey) bool { + if a.mode != b.mode { + return a.mode < b.mode + } + if a.file.Hash != b.file.Hash { + return a.file.Hash.Less(b.file.Hash) + } + return a.file.URI < b.file.URI +} + +func (m *goFilesMap) Clone() *goFilesMap { + return &goFilesMap{ + impl: m.impl.Clone(), + } +} + +func (m *goFilesMap) Destroy() { + m.impl.Destroy() +} + +func (m *goFilesMap) Load(key parseKey) (*parseGoHandle, bool) { + value, ok := m.impl.Load(key) + if !ok { + return nil, false + } + return value.(*parseGoHandle), true +} + +func (m *goFilesMap) Range(do func(key parseKey, value *parseGoHandle)) { + m.impl.Range(func(key, value interface{}) { + do(key.(parseKey), value.(*parseGoHandle)) + }) +} + +func (m *goFilesMap) Store(key parseKey, value *parseGoHandle, release func()) { + m.impl.Store(key, value, func(key, value interface{}) { + release() + }) +} + +func (m *goFilesMap) Delete(key parseKey) { + m.impl.Delete(key) +} + +type parseKeysByURIMap struct { + impl *persistent.Map +} + +func newParseKeysByURIMap() *parseKeysByURIMap { + return &parseKeysByURIMap{ + impl: persistent.NewMap(func(a, b interface{}) bool { + return a.(span.URI) < b.(span.URI) + }), + } +} + +func (m *parseKeysByURIMap) Clone() *parseKeysByURIMap { + return &parseKeysByURIMap{ + impl: m.impl.Clone(), + } +} + +func (m *parseKeysByURIMap) Destroy() { + m.impl.Destroy() +} + +func (m *parseKeysByURIMap) Load(key span.URI) ([]parseKey, bool) { + value, ok := m.impl.Load(key) + if !ok { + return nil, false + } + return value.([]parseKey), true +} + +func (m *parseKeysByURIMap) Range(do func(key span.URI, value []parseKey)) { + m.impl.Range(func(key, value interface{}) { + do(key.(span.URI), value.([]parseKey)) + }) +} + +func (m *parseKeysByURIMap) Store(key span.URI, value []parseKey) { + m.impl.Store(key, value, nil) +} + +func (m *parseKeysByURIMap) Delete(key span.URI) { + m.impl.Delete(key) +} diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index ab55743ccf0..376524bd324 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -58,7 +58,7 @@ func (s *snapshot) parseGoHandle(ctx context.Context, fh source.FileHandle, mode if pgh := s.getGoFile(key); pgh != nil { return pgh } - parseHandle := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { + parseHandle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { snapshot := arg.(*snapshot) return parseGo(ctx, snapshot.FileSet(), fh, mode) }, nil) @@ -68,7 +68,7 @@ func (s *snapshot) parseGoHandle(ctx context.Context, fh source.FileHandle, mode file: fh, mode: mode, } - return s.addGoFile(key, pgh) + return s.addGoFile(key, pgh, release) } func (pgh *parseGoHandle) String() string { diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 0d3e944b980..7dbccf7f6ed 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -234,7 +234,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, packages: make(map[packageKey]*packageHandle), meta: &metadataGraph{}, files: make(map[span.URI]source.VersionedFileHandle), - goFiles: newGoFileMap(), + goFiles: newGoFilesMap(), + parseKeysByURI: newParseKeysByURIMap(), symbols: make(map[span.URI]*symbolHandle), actions: make(map[actionKey]*actionHandle), workspacePackages: make(map[PackageID]PackagePath), diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 3c46648bff9..b2ac78208d7 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -77,7 +77,8 @@ type snapshot struct { files map[span.URI]source.VersionedFileHandle // goFiles maps a parseKey to its parseGoHandle. - goFiles *goFileMap + goFiles *goFilesMap + parseKeysByURI *parseKeysByURIMap // TODO(rfindley): consider merging this with files to reduce burden on clone. symbols map[span.URI]*symbolHandle @@ -133,6 +134,12 @@ type actionKey struct { analyzer *analysis.Analyzer } +func (s *snapshot) Destroy(destroyedBy string) { + s.generation.Destroy(destroyedBy) + s.goFiles.Destroy() + s.parseKeysByURI.Destroy() +} + func (s *snapshot) ID() uint64 { return s.id } @@ -665,17 +672,23 @@ func (s *snapshot) transitiveReverseDependencies(id PackageID, ids map[PackageID func (s *snapshot) getGoFile(key parseKey) *parseGoHandle { s.mu.Lock() defer s.mu.Unlock() - return s.goFiles.get(key) + if result, ok := s.goFiles.Load(key); ok { + return result + } + return nil } -func (s *snapshot) addGoFile(key parseKey, pgh *parseGoHandle) *parseGoHandle { +func (s *snapshot) addGoFile(key parseKey, pgh *parseGoHandle, release func()) *parseGoHandle { s.mu.Lock() defer s.mu.Unlock() - - if prev := s.goFiles.get(key); prev != nil { - return prev - } - s.goFiles.set(key, pgh) + if result, ok := s.goFiles.Load(key); ok { + release() + return result + } + s.goFiles.Store(key, pgh, release) + keys, _ := s.parseKeysByURI.Load(key.file.URI) + keys = append([]parseKey{key}, keys...) + s.parseKeysByURI.Store(key.file.URI, keys) return pgh } @@ -1663,6 +1676,9 @@ func (ac *unappliedChanges) GetFile(ctx context.Context, uri span.URI) (source.F } func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) *snapshot { + ctx, done := event.Start(ctx, "snapshot.clone") + defer done() + var vendorChanged bool newWorkspace, workspaceChanged, workspaceReload := s.workspace.invalidate(ctx, changes, &unappliedChanges{ originalSnapshot: s, @@ -1686,7 +1702,8 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC packages: make(map[packageKey]*packageHandle, len(s.packages)), actions: make(map[actionKey]*actionHandle, len(s.actions)), files: make(map[span.URI]source.VersionedFileHandle, len(s.files)), - goFiles: s.goFiles.clone(), + goFiles: s.goFiles.Clone(), + parseKeysByURI: s.parseKeysByURI.Clone(), symbols: make(map[span.URI]*symbolHandle, len(s.symbols)), workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)), unloadableFiles: make(map[span.URI]struct{}, len(s.unloadableFiles)), @@ -1731,27 +1748,14 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC result.parseWorkHandles[k] = v } - // Copy the handles of all Go source files. - // There may be tens of thousands of files, - // but changes are typically few, so we - // use a striped map optimized for this case - // and visit its stripes in parallel. - var ( - toDeleteMu sync.Mutex - toDelete []parseKey - ) - s.goFiles.forEachConcurrent(func(k parseKey, v *parseGoHandle) { - if changes[k.file.URI] == nil { - // no change (common case) - newGen.Inherit(v.handle) - } else { - toDeleteMu.Lock() - toDelete = append(toDelete, k) - toDeleteMu.Unlock() + for uri := range changes { + keys, ok := result.parseKeysByURI.Load(uri) + if ok { + for _, key := range keys { + result.goFiles.Delete(key) + } + result.parseKeysByURI.Delete(uri) } - }) - for _, k := range toDelete { - result.goFiles.delete(k) } // Copy all of the go.mod-related handles. They may be invalidated later, @@ -2194,7 +2198,7 @@ func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH // lockedSnapshot must be locked. func peekOrParse(ctx context.Context, lockedSnapshot *snapshot, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { key := parseKey{file: fh.FileIdentity(), mode: mode} - if pgh := lockedSnapshot.goFiles.get(key); pgh != nil { + if pgh, ok := lockedSnapshot.goFiles.Load(key); ok { cached := pgh.handle.Cached(lockedSnapshot.generation) if cached != nil { cached := cached.(*parseGoData) @@ -2482,89 +2486,3 @@ func readGoSum(dst map[module.Version][]string, file string, data []byte) error } return nil } - -// -- goFileMap -- - -// A goFileMap is conceptually a map[parseKey]*parseGoHandle, -// optimized for cloning all or nearly all entries. -type goFileMap struct { - // The map is represented as a map of 256 stripes, one per - // distinct value of the top 8 bits of key.file.Hash. - // Each stripe has an associated boolean indicating whether it - // is shared, and thus immutable, and thus must be copied before any update. - // (The bits could be packed but it hasn't been worth it yet.) - stripes [256]map[parseKey]*parseGoHandle - exclusive [256]bool // exclusive[i] means stripe[i] is not shared and may be safely mutated -} - -// newGoFileMap returns a new empty goFileMap. -func newGoFileMap() *goFileMap { - return new(goFileMap) // all stripes are shared (non-exclusive) nil maps -} - -// clone returns a copy of m. -// For concurrency, it counts as an update to m. -func (m *goFileMap) clone() *goFileMap { - m.exclusive = [256]bool{} // original and copy are now nonexclusive - copy := *m - return © -} - -// get returns the value for key k. -func (m *goFileMap) get(k parseKey) *parseGoHandle { - return m.stripes[m.hash(k)][k] -} - -// set updates the value for key k to v. -func (m *goFileMap) set(k parseKey, v *parseGoHandle) { - m.unshare(k)[k] = v -} - -// delete deletes the value for key k, if any. -func (m *goFileMap) delete(k parseKey) { - // TODO(adonovan): opt?: skip unshare if k isn't present. - delete(m.unshare(k), k) -} - -// forEachConcurrent calls f for each entry in the map. -// Calls may be concurrent. -// f must not modify m. -func (m *goFileMap) forEachConcurrent(f func(parseKey, *parseGoHandle)) { - // Visit stripes in parallel chunks. - const p = 16 // concurrency level - var wg sync.WaitGroup - wg.Add(p) - for i := 0; i < p; i++ { - chunk := m.stripes[i*p : (i+1)*p] - go func() { - for _, stripe := range chunk { - for k, v := range stripe { - f(k, v) - } - } - wg.Done() - }() - } - wg.Wait() -} - -// -- internal-- - -// hash returns 8 bits from the key's file digest. -func (*goFileMap) hash(k parseKey) byte { return k.file.Hash[0] } - -// unshare makes k's stripe exclusive, allocating a copy if needed, and returns it. -func (m *goFileMap) unshare(k parseKey) map[parseKey]*parseGoHandle { - i := m.hash(k) - if !m.exclusive[i] { - m.exclusive[i] = true - - // Copy the map. - copy := make(map[parseKey]*parseGoHandle, len(m.stripes[i])) - for k, v := range m.stripes[i] { - copy[k] = v - } - m.stripes[i] = copy - } - return m.stripes[i] -} diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 620efd8b965..1810f6e641d 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -535,7 +535,7 @@ func (v *View) shutdown(ctx context.Context) { v.mu.Unlock() v.snapshotMu.Lock() if v.snapshot != nil { - go v.snapshot.generation.Destroy("View.shutdown") + go v.snapshot.Destroy("View.shutdown") v.snapshot = nil } v.snapshotMu.Unlock() @@ -732,7 +732,7 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]*file oldSnapshot := v.snapshot v.snapshot = oldSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata) - go oldSnapshot.generation.Destroy("View.invalidateContent") + go oldSnapshot.Destroy("View.invalidateContent") return v.snapshot, v.snapshot.generation.Acquire() } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 0d8d661d60c..73e1b7f89ed 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -551,6 +551,11 @@ func (h Hash) String() string { return fmt.Sprintf("%64x", [sha256.Size]byte(h)) } +// Less returns true if the given hash is less than the other. +func (h Hash) Less(other Hash) bool { + return bytes.Compare(h[:], other[:]) < 0 +} + // FileIdentity uniquely identifies a file at a version from a FileSystem. type FileIdentity struct { URI span.URI diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 480b87f5ce9..48a642c990e 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -83,16 +83,15 @@ func (g *Generation) Destroy(destroyedBy string) { g.store.mu.Lock() defer g.store.mu.Unlock() - for k, e := range g.store.handles { + for _, e := range g.store.handles { + if !e.trackGenerations { + continue + } e.mu.Lock() if _, ok := e.generations[g]; ok { delete(e.generations, g) // delete even if it's dead, in case of dangling references to the entry. if len(e.generations) == 0 { - delete(g.store.handles, k) - e.state = stateDestroyed - if e.cleanup != nil && e.value != nil { - e.cleanup(e.value) - } + e.destroy(g.store) } } e.mu.Unlock() @@ -161,6 +160,12 @@ type Handle struct { // cleanup, if non-nil, is used to perform any necessary clean-up on values // produced by function. cleanup func(interface{}) + + // If trackGenerations is set, this handle tracks generations in which it + // is valid, via the generations field. Otherwise, it is explicitly reference + // counted via the refCounter field. + trackGenerations bool + refCounter int32 } // Bind returns a handle for the given key and function. @@ -173,7 +178,34 @@ type Handle struct { // // If cleanup is non-nil, it will be called on any non-nil values produced by // function when they are no longer referenced. +// +// It is responsibility of the caller to call Inherit on the handler whenever +// it should still be accessible by a next generation. func (g *Generation) Bind(key interface{}, function Function, cleanup func(interface{})) *Handle { + return g.getHandle(key, function, cleanup, true) +} + +// GetHandle returns a handle for the given key and function with similar +// properties and behavior as Bind. +// +// As in opposite to Bind it returns a release callback which has to be called +// once this reference to handle is not needed anymore. +func (g *Generation) GetHandle(key interface{}, function Function, cleanup func(interface{})) (*Handle, func()) { + handle := g.getHandle(key, function, cleanup, false) + store := g.store + release := func() { + store.mu.Lock() + defer store.mu.Unlock() + + handle.refCounter-- + if handle.refCounter == 0 { + handle.destroy(store) + } + } + return handle, release +} + +func (g *Generation) getHandle(key interface{}, function Function, cleanup func(interface{}), trackGenerations bool) *Handle { // panic early if the function is nil // it would panic later anyway, but in a way that was much harder to debug if function == nil { @@ -186,20 +218,19 @@ func (g *Generation) Bind(key interface{}, function Function, cleanup func(inter defer g.store.mu.Unlock() h, ok := g.store.handles[key] if !ok { - h := &Handle{ - key: key, - function: function, - generations: map[*Generation]struct{}{g: {}}, - cleanup: cleanup, + h = &Handle{ + key: key, + function: function, + cleanup: cleanup, + trackGenerations: trackGenerations, + } + if trackGenerations { + h.generations = make(map[*Generation]struct{}, 1) } g.store.handles[key] = h - return h - } - h.mu.Lock() - defer h.mu.Unlock() - if _, ok := h.generations[g]; !ok { - h.generations[g] = struct{}{} } + + h.incrementRef(g) return h } @@ -240,13 +271,44 @@ func (g *Generation) Inherit(h *Handle) { if atomic.LoadUint32(&g.destroyed) != 0 { panic("inherit on generation " + g.name + " destroyed by " + g.destroyedBy) } + if !h.trackGenerations { + panic("called Inherit on handle not created by Generation.Bind") + } + h.incrementRef(g) +} + +func (h *Handle) destroy(store *Store) { + h.state = stateDestroyed + if h.cleanup != nil && h.value != nil { + h.cleanup(h.value) + } + delete(store.handles, h.key) +} + +func (h *Handle) incrementRef(g *Generation) { h.mu.Lock() + defer h.mu.Unlock() + if h.state == stateDestroyed { panic(fmt.Sprintf("inheriting destroyed handle %#v (type %T) into generation %v", h.key, h.key, g.name)) } - h.generations[g] = struct{}{} - h.mu.Unlock() + + if h.trackGenerations { + h.generations[g] = struct{}{} + } else { + h.refCounter++ + } +} + +// hasRefLocked reports whether h is valid in generation g. h.mu must be held. +func (h *Handle) hasRefLocked(g *Generation) bool { + if !h.trackGenerations { + return true + } + + _, ok := h.generations[g] + return ok } // Cached returns the value associated with a handle. @@ -256,7 +318,7 @@ func (g *Generation) Inherit(h *Handle) { func (h *Handle) Cached(g *Generation) interface{} { h.mu.Lock() defer h.mu.Unlock() - if _, ok := h.generations[g]; !ok { + if !h.hasRefLocked(g) { return nil } if h.state == stateCompleted { @@ -277,7 +339,7 @@ func (h *Handle) Get(ctx context.Context, g *Generation, arg Arg) (interface{}, return nil, ctx.Err() } h.mu.Lock() - if _, ok := h.generations[g]; !ok { + if !h.hasRefLocked(g) { h.mu.Unlock() err := fmt.Errorf("reading key %#v: generation %v is not known", h.key, g.name) diff --git a/internal/memoize/memoize_test.go b/internal/memoize/memoize_test.go index ee0fd23ea1d..bffbfc2f6b3 100644 --- a/internal/memoize/memoize_test.go +++ b/internal/memoize/memoize_test.go @@ -106,3 +106,58 @@ func TestCleanup(t *testing.T) { t.Error("after destroying g2, v2 is not cleaned up") } } + +func TestHandleRefCounting(t *testing.T) { + s := &memoize.Store{} + g1 := s.Generation("g1") + v1 := false + v2 := false + cleanup := func(v interface{}) { + *(v.(*bool)) = true + } + h1, release1 := g1.GetHandle("key1", func(context.Context, memoize.Arg) interface{} { + return &v1 + }, nil) + h2, release2 := g1.GetHandle("key2", func(context.Context, memoize.Arg) interface{} { + return &v2 + }, cleanup) + expectGet(t, h1, g1, &v1) + expectGet(t, h2, g1, &v2) + + g2 := s.Generation("g2") + expectGet(t, h1, g2, &v1) + g1.Destroy("by test") + expectGet(t, h2, g2, &v2) + + h2Copy, release2Copy := g2.GetHandle("key2", func(context.Context, memoize.Arg) interface{} { + return &v1 + }, nil) + if h2 != h2Copy { + t.Error("NewHandle returned a new value while old is not destroyed yet") + } + expectGet(t, h2Copy, g2, &v2) + g2.Destroy("by test") + + release2() + if got, want := v2, false; got != want { + t.Error("after destroying first v2 ref, v2 is cleaned up") + } + release2Copy() + if got, want := v2, true; got != want { + t.Error("after destroying second v2 ref, v2 is not cleaned up") + } + if got, want := v1, false; got != want { + t.Error("after destroying v2, v1 is cleaned up") + } + release1() + + g3 := s.Generation("g3") + h2Copy, release2Copy = g3.GetHandle("key2", func(context.Context, memoize.Arg) interface{} { + return &v2 + }, cleanup) + if h2 == h2Copy { + t.Error("NewHandle returned previously destroyed value") + } + release2Copy() + g3.Destroy("by test") +} diff --git a/internal/persistent/map.go b/internal/persistent/map.go new file mode 100644 index 00000000000..bbcb72b6ee9 --- /dev/null +++ b/internal/persistent/map.go @@ -0,0 +1,268 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The persistent package defines various persistent data structures; +// that is, data structures that can be efficiently copied and modified +// in sublinear time. +package persistent + +import ( + "math/rand" + "sync/atomic" +) + +// Implementation details: +// * Each value is reference counted by nodes which hold it. +// * Each node is reference counted by its parent nodes. +// * Each map is considered a top-level parent node from reference counting perspective. +// * Each change does always effectivelly produce a new top level node. +// +// Functions which operate directly with nodes do have a notation in form of +// `foo(arg1:+n1, arg2:+n2) (ret1:+n3)`. +// Each argument is followed by a delta change to its reference counter. +// In case if no change is expected, the delta will be `-0`. + +// Map is an associative mapping from keys to values, both represented as +// interface{}. Key comparison and iteration order is defined by a +// client-provided function that implements a strict weak order. +// +// Maps can be Cloned in constant time. +// +// Values are reference counted, and a client-supplied release function +// is called when a value is no longer referenced by a map or any clone. +// +// Internally the implementation is based on a randomized persistent treap: +// https://en.wikipedia.org/wiki/Treap. +type Map struct { + less func(a, b interface{}) bool + root *mapNode +} + +type mapNode struct { + key interface{} + value *refValue + weight uint64 + refCount int32 + left, right *mapNode +} + +type refValue struct { + refCount int32 + value interface{} + release func(key, value interface{}) +} + +func newNodeWithRef(key, value interface{}, release func(key, value interface{})) *mapNode { + return &mapNode{ + key: key, + value: &refValue{ + value: value, + release: release, + refCount: 1, + }, + refCount: 1, + weight: rand.Uint64(), + } +} + +func (node *mapNode) shallowCloneWithRef() *mapNode { + atomic.AddInt32(&node.value.refCount, 1) + return &mapNode{ + key: node.key, + value: node.value, + weight: node.weight, + refCount: 1, + } +} + +func (node *mapNode) incref() *mapNode { + if node != nil { + atomic.AddInt32(&node.refCount, 1) + } + return node +} + +func (node *mapNode) decref() { + if node == nil { + return + } + if atomic.AddInt32(&node.refCount, -1) == 0 { + if atomic.AddInt32(&node.value.refCount, -1) == 0 { + if node.value.release != nil { + node.value.release(node.key, node.value.value) + } + node.value.value = nil + node.value.release = nil + } + node.left.decref() + node.right.decref() + } +} + +// NewMap returns a new map whose keys are ordered by the given comparison +// function (a strict weak order). It is the responsibility of the caller to +// Destroy it at later time. +func NewMap(less func(a, b interface{}) bool) *Map { + return &Map{ + less: less, + } +} + +// Clone returns a copy of the given map. It is a responsibility of the caller +// to Destroy it at later time. +func (pm *Map) Clone() *Map { + return &Map{ + less: pm.less, + root: pm.root.incref(), + } +} + +// Destroy the persistent map. +// +// After Destroy, the Map should not be used again. +func (pm *Map) Destroy() { + pm.root.decref() + pm.root = nil +} + +// Range calls f sequentially in ascending key order for all entries in the map. +func (pm *Map) Range(f func(key, value interface{})) { + pm.root.forEach(f) +} + +func (node *mapNode) forEach(f func(key, value interface{})) { + if node == nil { + return + } + node.left.forEach(f) + f(node.key, node.value.value) + node.right.forEach(f) +} + +// Load returns the value stored in the map for a key, or nil if no entry is +// present. The ok result indicates whether an entry was found in the map. +func (pm *Map) Load(key interface{}) (interface{}, bool) { + node := pm.root + for node != nil { + if pm.less(key, node.key) { + node = node.left + } else if pm.less(node.key, key) { + node = node.right + } else { + return node.value.value, true + } + } + return nil, false +} + +// Store sets the value for a key. +// If release is non-nil, it will be called with entry's key and value once the +// key is no longer contained in the map or any clone. +func (pm *Map) Store(key, value interface{}, release func(key, value interface{})) { + first := pm.root + second := newNodeWithRef(key, value, release) + pm.root = union(first, second, pm.less, true) + first.decref() + second.decref() +} + +// union returns a new tree which is a union of first and second one. +// If overwrite is set to true, second one would override a value for any duplicate keys. +// +// union(first:-0, second:-0) (result:+1) +// Union borrows both subtrees without affecting their refcount and returns a +// new reference that the caller is expected to call decref. +func union(first, second *mapNode, less func(a, b interface{}) bool, overwrite bool) *mapNode { + if first == nil { + return second.incref() + } + if second == nil { + return first.incref() + } + + if first.weight < second.weight { + second, first, overwrite = first, second, !overwrite + } + + left, mid, right := split(second, first.key, less) + var result *mapNode + if overwrite && mid != nil { + result = mid.shallowCloneWithRef() + } else { + result = first.shallowCloneWithRef() + } + result.weight = first.weight + result.left = union(first.left, left, less, overwrite) + result.right = union(first.right, right, less, overwrite) + left.decref() + mid.decref() + right.decref() + return result +} + +// split the tree midway by the key into three different ones. +// Return three new trees: left with all nodes with smaller than key, mid with +// the node matching the key, right with all nodes larger than key. +// If there are no nodes in one of trees, return nil instead of it. +// +// split(n:-0) (left:+1, mid:+1, right:+1) +// Split borrows n without affecting its refcount, and returns three +// new references that that caller is expected to call decref. +func split(n *mapNode, key interface{}, less func(a, b interface{}) bool) (left, mid, right *mapNode) { + if n == nil { + return nil, nil, nil + } + + if less(n.key, key) { + left, mid, right := split(n.right, key, less) + newN := n.shallowCloneWithRef() + newN.left = n.left.incref() + newN.right = left + return newN, mid, right + } else if less(key, n.key) { + left, mid, right := split(n.left, key, less) + newN := n.shallowCloneWithRef() + newN.left = right + newN.right = n.right.incref() + return left, mid, newN + } + mid = n.shallowCloneWithRef() + return n.left.incref(), mid, n.right.incref() +} + +// Delete deletes the value for a key. +func (pm *Map) Delete(key interface{}) { + root := pm.root + left, mid, right := split(root, key, pm.less) + pm.root = merge(left, right) + left.decref() + mid.decref() + right.decref() + root.decref() +} + +// merge two trees while preserving the weight invariant. +// All nodes in left must have smaller keys than any node in right. +// +// merge(left:-0, right:-0) (result:+1) +// Merge borrows its arguments without affecting their refcount +// and returns a new reference that the caller is expected to call decref. +func merge(left, right *mapNode) *mapNode { + switch { + case left == nil: + return right.incref() + case right == nil: + return left.incref() + case left.weight > right.weight: + root := left.shallowCloneWithRef() + root.left = left.left.incref() + root.right = merge(left.right, right) + return root + default: + root := right.shallowCloneWithRef() + root.left = merge(left, right.left) + root.right = right.right.incref() + return root + } +} diff --git a/internal/persistent/map_test.go b/internal/persistent/map_test.go new file mode 100644 index 00000000000..9585956100b --- /dev/null +++ b/internal/persistent/map_test.go @@ -0,0 +1,316 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package persistent + +import ( + "fmt" + "math/rand" + "reflect" + "sync/atomic" + "testing" +) + +type mapEntry struct { + key int + value int +} + +type validatedMap struct { + impl *Map + expected map[int]int + deleted map[mapEntry]struct{} + seen map[mapEntry]struct{} +} + +func TestSimpleMap(t *testing.T) { + deletedEntries := make(map[mapEntry]struct{}) + seenEntries := make(map[mapEntry]struct{}) + + m1 := &validatedMap{ + impl: NewMap(func(a, b interface{}) bool { + return a.(int) < b.(int) + }), + expected: make(map[int]int), + deleted: deletedEntries, + seen: seenEntries, + } + + m3 := m1.clone() + validateRef(t, m1, m3) + m3.insert(t, 8, 8) + validateRef(t, m1, m3) + m3.destroy() + + assertSameMap(t, deletedEntries, map[mapEntry]struct{}{ + {key: 8, value: 8}: {}, + }) + + validateRef(t, m1) + m1.insert(t, 1, 1) + validateRef(t, m1) + m1.insert(t, 2, 2) + validateRef(t, m1) + m1.insert(t, 3, 3) + validateRef(t, m1) + m1.remove(t, 2) + validateRef(t, m1) + m1.insert(t, 6, 6) + validateRef(t, m1) + + assertSameMap(t, deletedEntries, map[mapEntry]struct{}{ + {key: 2, value: 2}: {}, + {key: 8, value: 8}: {}, + }) + + m2 := m1.clone() + validateRef(t, m1, m2) + m1.insert(t, 6, 60) + validateRef(t, m1, m2) + m1.remove(t, 1) + validateRef(t, m1, m2) + + for i := 10; i < 14; i++ { + m1.insert(t, i, i) + validateRef(t, m1, m2) + } + + m1.insert(t, 10, 100) + validateRef(t, m1, m2) + + m1.remove(t, 12) + validateRef(t, m1, m2) + + m2.insert(t, 4, 4) + validateRef(t, m1, m2) + m2.insert(t, 5, 5) + validateRef(t, m1, m2) + + m1.destroy() + + assertSameMap(t, deletedEntries, map[mapEntry]struct{}{ + {key: 2, value: 2}: {}, + {key: 6, value: 60}: {}, + {key: 8, value: 8}: {}, + {key: 10, value: 10}: {}, + {key: 10, value: 100}: {}, + {key: 11, value: 11}: {}, + {key: 12, value: 12}: {}, + {key: 13, value: 13}: {}, + }) + + m2.insert(t, 7, 7) + validateRef(t, m2) + + m2.destroy() + + assertSameMap(t, seenEntries, deletedEntries) +} + +func TestRandomMap(t *testing.T) { + deletedEntries := make(map[mapEntry]struct{}) + seenEntries := make(map[mapEntry]struct{}) + + m := &validatedMap{ + impl: NewMap(func(a, b interface{}) bool { + return a.(int) < b.(int) + }), + expected: make(map[int]int), + deleted: deletedEntries, + seen: seenEntries, + } + + keys := make([]int, 0, 1000) + for i := 0; i < 1000; i++ { + key := rand.Int() + m.insert(t, key, key) + keys = append(keys, key) + + if i%10 == 1 { + index := rand.Intn(len(keys)) + last := len(keys) - 1 + key = keys[index] + keys[index], keys[last] = keys[last], keys[index] + keys = keys[:last] + + m.remove(t, key) + } + } + + m.destroy() + assertSameMap(t, seenEntries, deletedEntries) +} + +func (vm *validatedMap) onDelete(t *testing.T, key, value int) { + entry := mapEntry{key: key, value: value} + if _, ok := vm.deleted[entry]; ok { + t.Fatalf("tried to delete entry twice, key: %d, value: %d", key, value) + } + vm.deleted[entry] = struct{}{} +} + +func validateRef(t *testing.T, maps ...*validatedMap) { + t.Helper() + + actualCountByEntry := make(map[mapEntry]int32) + nodesByEntry := make(map[mapEntry]map[*mapNode]struct{}) + expectedCountByEntry := make(map[mapEntry]int32) + for i, m := range maps { + dfsRef(m.impl.root, actualCountByEntry, nodesByEntry) + dumpMap(t, fmt.Sprintf("%d:", i), m.impl.root) + } + for entry, nodes := range nodesByEntry { + expectedCountByEntry[entry] = int32(len(nodes)) + } + assertSameMap(t, expectedCountByEntry, actualCountByEntry) +} + +func dfsRef(node *mapNode, countByEntry map[mapEntry]int32, nodesByEntry map[mapEntry]map[*mapNode]struct{}) { + if node == nil { + return + } + + entry := mapEntry{key: node.key.(int), value: node.value.value.(int)} + countByEntry[entry] = atomic.LoadInt32(&node.value.refCount) + + nodes, ok := nodesByEntry[entry] + if !ok { + nodes = make(map[*mapNode]struct{}) + nodesByEntry[entry] = nodes + } + nodes[node] = struct{}{} + + dfsRef(node.left, countByEntry, nodesByEntry) + dfsRef(node.right, countByEntry, nodesByEntry) +} + +func dumpMap(t *testing.T, prefix string, n *mapNode) { + if n == nil { + t.Logf("%s nil", prefix) + return + } + t.Logf("%s {key: %v, value: %v (ref: %v), ref: %v, weight: %v}", prefix, n.key, n.value.value, n.value.refCount, n.refCount, n.weight) + dumpMap(t, prefix+"l", n.left) + dumpMap(t, prefix+"r", n.right) +} + +func (vm *validatedMap) validate(t *testing.T) { + t.Helper() + + validateNode(t, vm.impl.root, vm.impl.less) + + for key, value := range vm.expected { + entry := mapEntry{key: key, value: value} + if _, ok := vm.deleted[entry]; ok { + t.Fatalf("entry is deleted prematurely, key: %d, value: %d", key, value) + } + } + + actualMap := make(map[int]int, len(vm.expected)) + vm.impl.Range(func(key, value interface{}) { + if other, ok := actualMap[key.(int)]; ok { + t.Fatalf("key is present twice, key: %d, first value: %d, second value: %d", key, value, other) + } + actualMap[key.(int)] = value.(int) + }) + + assertSameMap(t, actualMap, vm.expected) +} + +func validateNode(t *testing.T, node *mapNode, less func(a, b interface{}) bool) { + if node == nil { + return + } + + if node.left != nil { + if less(node.key, node.left.key) { + t.Fatalf("left child has larger key: %v vs %v", node.left.key, node.key) + } + if node.left.weight > node.weight { + t.Fatalf("left child has larger weight: %v vs %v", node.left.weight, node.weight) + } + } + + if node.right != nil { + if less(node.right.key, node.key) { + t.Fatalf("right child has smaller key: %v vs %v", node.right.key, node.key) + } + if node.right.weight > node.weight { + t.Fatalf("right child has larger weight: %v vs %v", node.right.weight, node.weight) + } + } + + validateNode(t, node.left, less) + validateNode(t, node.right, less) +} + +func (vm *validatedMap) insert(t *testing.T, key, value int) { + vm.seen[mapEntry{key: key, value: value}] = struct{}{} + vm.impl.Store(key, value, func(deletedKey, deletedValue interface{}) { + if deletedKey != key || deletedValue != value { + t.Fatalf("unexpected passed in deleted entry: %v/%v, expected: %v/%v", deletedKey, deletedValue, key, value) + } + vm.onDelete(t, key, value) + }) + vm.expected[key] = value + vm.validate(t) + + loadValue, ok := vm.impl.Load(key) + if !ok || loadValue != value { + t.Fatalf("unexpected load result after insertion, key: %v, expected: %v, got: %v (%v)", key, value, loadValue, ok) + } +} + +func (vm *validatedMap) remove(t *testing.T, key int) { + vm.impl.Delete(key) + delete(vm.expected, key) + vm.validate(t) + + loadValue, ok := vm.impl.Load(key) + if ok { + t.Fatalf("unexpected load result after removal, key: %v, got: %v", key, loadValue) + } +} + +func (vm *validatedMap) clone() *validatedMap { + expected := make(map[int]int, len(vm.expected)) + for key, value := range vm.expected { + expected[key] = value + } + + return &validatedMap{ + impl: vm.impl.Clone(), + expected: expected, + deleted: vm.deleted, + seen: vm.seen, + } +} + +func (vm *validatedMap) destroy() { + vm.impl.Destroy() +} + +func assertSameMap(t *testing.T, map1, map2 interface{}) { + t.Helper() + + if !reflect.DeepEqual(map1, map2) { + t.Fatalf("different maps:\n%v\nvs\n%v", map1, map2) + } +} + +func isSameMap(map1, map2 reflect.Value) bool { + if map1.Len() != map2.Len() { + return false + } + iter := map1.MapRange() + for iter.Next() { + key := iter.Key() + value1 := iter.Value() + value2 := map2.MapIndex(key) + if value2.IsZero() || !reflect.DeepEqual(value1.Interface(), value2.Interface()) { + return false + } + } + return true +} From 3f5f798e2a0fc1b948638897b74eaa09eb0567ca Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Sun, 12 Jun 2022 03:38:32 +0000 Subject: [PATCH 042/723] internal/lsp/cache: use persistent map for storing files in the snapshot This on average reduces latency from 34ms to 25ms on internal codebase. Updates golang/go#45686 Change-Id: I57b05e5679620d8481b1f1a051645cf1cc00aca5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413654 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Alan Donovan --- internal/lsp/cache/maps.go | 81 ++++++++++++++++++++++++++-------- internal/lsp/cache/session.go | 2 +- internal/lsp/cache/snapshot.go | 75 ++++++++++++++++--------------- 3 files changed, 101 insertions(+), 57 deletions(-) diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go index 70f8039bdac..cad4465db89 100644 --- a/internal/lsp/cache/maps.go +++ b/internal/lsp/cache/maps.go @@ -5,18 +5,63 @@ package cache import ( + "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/persistent" "golang.org/x/tools/internal/span" ) // TODO(euroelessar): Use generics once support for go1.17 is dropped. +type filesMap struct { + impl *persistent.Map +} + +func newFilesMap() filesMap { + return filesMap{ + impl: persistent.NewMap(func(a, b interface{}) bool { + return a.(span.URI) < b.(span.URI) + }), + } +} + +func (m filesMap) Clone() filesMap { + return filesMap{ + impl: m.impl.Clone(), + } +} + +func (m filesMap) Destroy() { + m.impl.Destroy() +} + +func (m filesMap) Load(key span.URI) (source.VersionedFileHandle, bool) { + value, ok := m.impl.Load(key) + if !ok { + return nil, false + } + return value.(source.VersionedFileHandle), true +} + +func (m filesMap) Range(do func(key span.URI, value source.VersionedFileHandle)) { + m.impl.Range(func(key, value interface{}) { + do(key.(span.URI), value.(source.VersionedFileHandle)) + }) +} + +func (m filesMap) Store(key span.URI, value source.VersionedFileHandle) { + m.impl.Store(key, value, nil) +} + +func (m filesMap) Delete(key span.URI) { + m.impl.Delete(key) +} + type goFilesMap struct { impl *persistent.Map } -func newGoFilesMap() *goFilesMap { - return &goFilesMap{ +func newGoFilesMap() goFilesMap { + return goFilesMap{ impl: persistent.NewMap(func(a, b interface{}) bool { return parseKeyLess(a.(parseKey), b.(parseKey)) }), @@ -33,17 +78,17 @@ func parseKeyLess(a, b parseKey) bool { return a.file.URI < b.file.URI } -func (m *goFilesMap) Clone() *goFilesMap { - return &goFilesMap{ +func (m goFilesMap) Clone() goFilesMap { + return goFilesMap{ impl: m.impl.Clone(), } } -func (m *goFilesMap) Destroy() { +func (m goFilesMap) Destroy() { m.impl.Destroy() } -func (m *goFilesMap) Load(key parseKey) (*parseGoHandle, bool) { +func (m goFilesMap) Load(key parseKey) (*parseGoHandle, bool) { value, ok := m.impl.Load(key) if !ok { return nil, false @@ -51,19 +96,19 @@ func (m *goFilesMap) Load(key parseKey) (*parseGoHandle, bool) { return value.(*parseGoHandle), true } -func (m *goFilesMap) Range(do func(key parseKey, value *parseGoHandle)) { +func (m goFilesMap) Range(do func(key parseKey, value *parseGoHandle)) { m.impl.Range(func(key, value interface{}) { do(key.(parseKey), value.(*parseGoHandle)) }) } -func (m *goFilesMap) Store(key parseKey, value *parseGoHandle, release func()) { +func (m goFilesMap) Store(key parseKey, value *parseGoHandle, release func()) { m.impl.Store(key, value, func(key, value interface{}) { release() }) } -func (m *goFilesMap) Delete(key parseKey) { +func (m goFilesMap) Delete(key parseKey) { m.impl.Delete(key) } @@ -71,25 +116,25 @@ type parseKeysByURIMap struct { impl *persistent.Map } -func newParseKeysByURIMap() *parseKeysByURIMap { - return &parseKeysByURIMap{ +func newParseKeysByURIMap() parseKeysByURIMap { + return parseKeysByURIMap{ impl: persistent.NewMap(func(a, b interface{}) bool { return a.(span.URI) < b.(span.URI) }), } } -func (m *parseKeysByURIMap) Clone() *parseKeysByURIMap { - return &parseKeysByURIMap{ +func (m parseKeysByURIMap) Clone() parseKeysByURIMap { + return parseKeysByURIMap{ impl: m.impl.Clone(), } } -func (m *parseKeysByURIMap) Destroy() { +func (m parseKeysByURIMap) Destroy() { m.impl.Destroy() } -func (m *parseKeysByURIMap) Load(key span.URI) ([]parseKey, bool) { +func (m parseKeysByURIMap) Load(key span.URI) ([]parseKey, bool) { value, ok := m.impl.Load(key) if !ok { return nil, false @@ -97,16 +142,16 @@ func (m *parseKeysByURIMap) Load(key span.URI) ([]parseKey, bool) { return value.([]parseKey), true } -func (m *parseKeysByURIMap) Range(do func(key span.URI, value []parseKey)) { +func (m parseKeysByURIMap) Range(do func(key span.URI, value []parseKey)) { m.impl.Range(func(key, value interface{}) { do(key.(span.URI), value.([]parseKey)) }) } -func (m *parseKeysByURIMap) Store(key span.URI, value []parseKey) { +func (m parseKeysByURIMap) Store(key span.URI, value []parseKey) { m.impl.Store(key, value, nil) } -func (m *parseKeysByURIMap) Delete(key span.URI) { +func (m parseKeysByURIMap) Delete(key span.URI) { m.impl.Delete(key) } diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 7dbccf7f6ed..4a7a5b2f4a6 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -233,7 +233,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, generation: s.cache.store.Generation(generationName(v, 0)), packages: make(map[packageKey]*packageHandle), meta: &metadataGraph{}, - files: make(map[span.URI]source.VersionedFileHandle), + files: newFilesMap(), goFiles: newGoFilesMap(), parseKeysByURI: newParseKeysByURIMap(), symbols: make(map[span.URI]*symbolHandle), diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index b2ac78208d7..60cf4167ec9 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -74,11 +74,11 @@ type snapshot struct { // files maps file URIs to their corresponding FileHandles. // It may invalidated when a file's content changes. - files map[span.URI]source.VersionedFileHandle + files filesMap // goFiles maps a parseKey to its parseGoHandle. - goFiles *goFilesMap - parseKeysByURI *parseKeysByURIMap + goFiles goFilesMap + parseKeysByURI parseKeysByURIMap // TODO(rfindley): consider merging this with files to reduce burden on clone. symbols map[span.URI]*symbolHandle @@ -136,6 +136,7 @@ type actionKey struct { func (s *snapshot) Destroy(destroyedBy string) { s.generation.Destroy(destroyedBy) + s.files.Destroy() s.goFiles.Destroy() s.parseKeysByURI.Destroy() } @@ -173,11 +174,11 @@ func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle { defer s.mu.Unlock() tmpls := map[span.URI]source.VersionedFileHandle{} - for k, fh := range s.files { + s.files.Range(func(k span.URI, fh source.VersionedFileHandle) { if s.view.FileKind(fh) == source.Tmpl { tmpls[k] = fh } - } + }) return tmpls } @@ -461,27 +462,27 @@ func (s *snapshot) buildOverlay() map[string][]byte { defer s.mu.Unlock() overlays := make(map[string][]byte) - for uri, fh := range s.files { + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { overlay, ok := fh.(*overlay) if !ok { - continue + return } if overlay.saved { - continue + return } // TODO(rstambler): Make sure not to send overlays outside of the current view. overlays[uri.Filename()] = overlay.text - } + }) return overlays } -func hashUnsavedOverlays(files map[span.URI]source.VersionedFileHandle) source.Hash { +func hashUnsavedOverlays(files filesMap) source.Hash { var unsaved []string - for uri, fh := range files { + files.Range(func(uri span.URI, fh source.VersionedFileHandle) { if overlay, ok := fh.(*overlay); ok && !overlay.saved { unsaved = append(unsaved, uri.Filename()) } - } + }) sort.Strings(unsaved) return source.Hashf("%s", unsaved) } @@ -869,9 +870,9 @@ func (s *snapshot) collectAllKnownSubdirs(ctx context.Context) { s.knownSubdirs = map[span.URI]struct{}{} s.knownSubdirsPatternCache = "" - for uri := range s.files { + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { s.addKnownSubdirLocked(uri, dirs) - } + }) } func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) []span.URI { @@ -957,11 +958,11 @@ func (s *snapshot) knownFilesInDir(ctx context.Context, dir span.URI) []span.URI s.mu.Lock() defer s.mu.Unlock() - for uri := range s.files { + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { if source.InDir(dir.Filename(), uri.Filename()) { files = append(files, uri) } - } + }) return files } @@ -1020,8 +1021,7 @@ func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { resultMu sync.Mutex result = make(map[span.URI][]source.Symbol) ) - for uri, f := range s.files { - uri, f := uri, f + s.files.Range(func(uri span.URI, f source.VersionedFileHandle) { // TODO(adonovan): upgrade errgroup and use group.SetLimit(nprocs). iolimit <- struct{}{} // acquire token group.Go(func() error { @@ -1035,7 +1035,7 @@ func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { resultMu.Unlock() return nil }) - } + }) // Keep going on errors, but log the first failure. // Partial results are better than no symbol results. if err := group.Wait(); err != nil { @@ -1326,7 +1326,8 @@ func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle { s.mu.Lock() defer s.mu.Unlock() - return s.files[f.URI()] + result, _ := s.files.Load(f.URI()) + return result } // GetVersionedFile returns a File for the given URI. If the file is unknown it @@ -1348,7 +1349,7 @@ func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle } func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.VersionedFileHandle, error) { - if fh, ok := s.files[f.URI()]; ok { + if fh, ok := s.files.Load(f.URI()); ok { return fh, nil } @@ -1357,7 +1358,7 @@ func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.Versi return nil, err } closed := &closedFile{fh} - s.files[f.URI()] = closed + s.files.Store(f.URI(), closed) return closed, nil } @@ -1373,16 +1374,17 @@ func (s *snapshot) openFiles() []source.VersionedFileHandle { defer s.mu.Unlock() var open []source.VersionedFileHandle - for _, fh := range s.files { + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { if s.isOpenLocked(fh.URI()) { open = append(open, fh) } - } + }) return open } func (s *snapshot) isOpenLocked(uri span.URI) bool { - _, open := s.files[uri].(*overlay) + fh, _ := s.files.Load(uri) + _, open := fh.(*overlay) return open } @@ -1610,29 +1612,29 @@ func (s *snapshot) orphanedFiles() []source.VersionedFileHandle { defer s.mu.Unlock() var files []source.VersionedFileHandle - for uri, fh := range s.files { + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { // Don't try to reload metadata for go.mod files. if s.view.FileKind(fh) != source.Go { - continue + return } // If the URI doesn't belong to this view, then it's not in a workspace // package and should not be reloaded directly. if !contains(s.view.session.viewsOf(uri), s.view) { - continue + return } // If the file is not open and is in a vendor directory, don't treat it // like a workspace package. if _, ok := fh.(*overlay); !ok && inVendor(uri) { - continue + return } // Don't reload metadata for files we've already deemed unloadable. if _, ok := s.unloadableFiles[uri]; ok { - continue + return } if s.noValidMetadataForURILocked(uri) { files = append(files, fh) } - } + }) return files } @@ -1701,7 +1703,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC initializedErr: s.initializedErr, packages: make(map[packageKey]*packageHandle, len(s.packages)), actions: make(map[actionKey]*actionHandle, len(s.actions)), - files: make(map[span.URI]source.VersionedFileHandle, len(s.files)), + files: s.files.Clone(), goFiles: s.goFiles.Clone(), parseKeysByURI: s.parseKeysByURI.Clone(), symbols: make(map[span.URI]*symbolHandle, len(s.symbols)), @@ -1721,9 +1723,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // Copy all of the FileHandles. - for k, v := range s.files { - result.files[k] = v - } for k, v := range s.symbols { if change, ok := changes[k]; ok { if change.exists { @@ -1807,7 +1806,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // The original FileHandle for this URI is cached on the snapshot. - originalFH := s.files[uri] + originalFH, _ := s.files.Load(uri) var originalOpen, newOpen bool _, originalOpen = originalFH.(*overlay) _, newOpen = change.fileHandle.(*overlay) @@ -1852,9 +1851,9 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC delete(result.parseWorkHandles, uri) // Handle the invalidated file; it may have new contents or not exist. if !change.exists { - delete(result.files, uri) + result.files.Delete(uri) } else { - result.files[uri] = change.fileHandle + result.files.Store(uri, change.fileHandle) } // Make sure to remove the changed file from the unloadable set. From 22ab2538d44ec522fbfca114ab0cdc436e9eddd5 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Thu, 23 Jun 2022 13:38:50 -0400 Subject: [PATCH 043/723] internal/lsp: rename viewport to range The final LSP spec for 3.17 changed the name of ViewPort to Range for both InlayHints and InlineValues. This manually updates just these fields in our protocol. Change-Id: I0303a36536016ca59c87dc45f55fadcd80e72bfc Reviewed-on: https://go-review.googlesource.com/c/tools/+/413677 Reviewed-by: Robert Findley Run-TryBot: Suzy Mueller TryBot-Result: Gopher Robot gopls-CI: kokoro --- internal/lsp/inlay_hint.go | 2 +- internal/lsp/lsp_test.go | 2 +- internal/lsp/protocol/tsprotocol.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/lsp/inlay_hint.go b/internal/lsp/inlay_hint.go index b2fd028d728..8d8a419c235 100644 --- a/internal/lsp/inlay_hint.go +++ b/internal/lsp/inlay_hint.go @@ -17,5 +17,5 @@ func (s *Server) inlayHint(ctx context.Context, params *protocol.InlayHintParams if !ok { return nil, err } - return source.InlayHint(ctx, snapshot, fh, params.ViewPort) + return source.InlayHint(ctx, snapshot, fh, params.Range) } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 2ec833b860e..e8febec93f3 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -943,7 +943,7 @@ func (r *runner) InlayHints(t *testing.T, spn span.Span) { TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.URIFromSpanURI(uri), }, - // TODO: add ViewPort + // TODO: add Range }) if err != nil { t.Fatal(err) diff --git a/internal/lsp/protocol/tsprotocol.go b/internal/lsp/protocol/tsprotocol.go index 647aabc2ee1..5dd3d09e188 100644 --- a/internal/lsp/protocol/tsprotocol.go +++ b/internal/lsp/protocol/tsprotocol.go @@ -2866,7 +2866,7 @@ type InlayHintParams struct { /** * The visible document range for which inlay hints should be computed. */ - ViewPort Range `json:"viewPort"` + Range Range `json:"range"` } /** @@ -2988,7 +2988,7 @@ type InlineValueParams struct { /** * The visible document range for which inline values should be computed. */ - ViewPort Range `json:"viewPort"` + Range Range `json:"range"` /** * Additional information about the context in which inline values were * requested. From 2994e99415f52ae83985b566c317a726c14ca8ab Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Thu, 23 Jun 2022 22:40:01 +0000 Subject: [PATCH 044/723] internal/persistent: change map to use set/get as method names Purely a style change, no expected behavior difference. Change-Id: Ib882eb54537126b31d20dde65c4a517d5452a8b0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413661 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/cache/maps.go | 24 ++++++++++----------- internal/lsp/cache/snapshot.go | 26 +++++++++++----------- internal/persistent/map.go | 10 ++++----- internal/persistent/map_test.go | 38 ++++++++++++++++----------------- 4 files changed, 49 insertions(+), 49 deletions(-) diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go index cad4465db89..91b0e77e87e 100644 --- a/internal/lsp/cache/maps.go +++ b/internal/lsp/cache/maps.go @@ -34,8 +34,8 @@ func (m filesMap) Destroy() { m.impl.Destroy() } -func (m filesMap) Load(key span.URI) (source.VersionedFileHandle, bool) { - value, ok := m.impl.Load(key) +func (m filesMap) Get(key span.URI) (source.VersionedFileHandle, bool) { + value, ok := m.impl.Get(key) if !ok { return nil, false } @@ -48,8 +48,8 @@ func (m filesMap) Range(do func(key span.URI, value source.VersionedFileHandle)) }) } -func (m filesMap) Store(key span.URI, value source.VersionedFileHandle) { - m.impl.Store(key, value, nil) +func (m filesMap) Set(key span.URI, value source.VersionedFileHandle) { + m.impl.Set(key, value, nil) } func (m filesMap) Delete(key span.URI) { @@ -88,8 +88,8 @@ func (m goFilesMap) Destroy() { m.impl.Destroy() } -func (m goFilesMap) Load(key parseKey) (*parseGoHandle, bool) { - value, ok := m.impl.Load(key) +func (m goFilesMap) Get(key parseKey) (*parseGoHandle, bool) { + value, ok := m.impl.Get(key) if !ok { return nil, false } @@ -102,8 +102,8 @@ func (m goFilesMap) Range(do func(key parseKey, value *parseGoHandle)) { }) } -func (m goFilesMap) Store(key parseKey, value *parseGoHandle, release func()) { - m.impl.Store(key, value, func(key, value interface{}) { +func (m goFilesMap) Set(key parseKey, value *parseGoHandle, release func()) { + m.impl.Set(key, value, func(key, value interface{}) { release() }) } @@ -134,8 +134,8 @@ func (m parseKeysByURIMap) Destroy() { m.impl.Destroy() } -func (m parseKeysByURIMap) Load(key span.URI) ([]parseKey, bool) { - value, ok := m.impl.Load(key) +func (m parseKeysByURIMap) Get(key span.URI) ([]parseKey, bool) { + value, ok := m.impl.Get(key) if !ok { return nil, false } @@ -148,8 +148,8 @@ func (m parseKeysByURIMap) Range(do func(key span.URI, value []parseKey)) { }) } -func (m parseKeysByURIMap) Store(key span.URI, value []parseKey) { - m.impl.Store(key, value, nil) +func (m parseKeysByURIMap) Set(key span.URI, value []parseKey) { + m.impl.Set(key, value, nil) } func (m parseKeysByURIMap) Delete(key span.URI) { diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 60cf4167ec9..c8d60853332 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -673,7 +673,7 @@ func (s *snapshot) transitiveReverseDependencies(id PackageID, ids map[PackageID func (s *snapshot) getGoFile(key parseKey) *parseGoHandle { s.mu.Lock() defer s.mu.Unlock() - if result, ok := s.goFiles.Load(key); ok { + if result, ok := s.goFiles.Get(key); ok { return result } return nil @@ -682,14 +682,14 @@ func (s *snapshot) getGoFile(key parseKey) *parseGoHandle { func (s *snapshot) addGoFile(key parseKey, pgh *parseGoHandle, release func()) *parseGoHandle { s.mu.Lock() defer s.mu.Unlock() - if result, ok := s.goFiles.Load(key); ok { + if result, ok := s.goFiles.Get(key); ok { release() return result } - s.goFiles.Store(key, pgh, release) - keys, _ := s.parseKeysByURI.Load(key.file.URI) + s.goFiles.Set(key, pgh, release) + keys, _ := s.parseKeysByURI.Get(key.file.URI) keys = append([]parseKey{key}, keys...) - s.parseKeysByURI.Store(key.file.URI, keys) + s.parseKeysByURI.Set(key.file.URI, keys) return pgh } @@ -1326,7 +1326,7 @@ func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle { s.mu.Lock() defer s.mu.Unlock() - result, _ := s.files.Load(f.URI()) + result, _ := s.files.Get(f.URI()) return result } @@ -1349,7 +1349,7 @@ func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle } func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.VersionedFileHandle, error) { - if fh, ok := s.files.Load(f.URI()); ok { + if fh, ok := s.files.Get(f.URI()); ok { return fh, nil } @@ -1358,7 +1358,7 @@ func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.Versi return nil, err } closed := &closedFile{fh} - s.files.Store(f.URI(), closed) + s.files.Set(f.URI(), closed) return closed, nil } @@ -1383,7 +1383,7 @@ func (s *snapshot) openFiles() []source.VersionedFileHandle { } func (s *snapshot) isOpenLocked(uri span.URI) bool { - fh, _ := s.files.Load(uri) + fh, _ := s.files.Get(uri) _, open := fh.(*overlay) return open } @@ -1748,7 +1748,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } for uri := range changes { - keys, ok := result.parseKeysByURI.Load(uri) + keys, ok := result.parseKeysByURI.Get(uri) if ok { for _, key := range keys { result.goFiles.Delete(key) @@ -1806,7 +1806,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // The original FileHandle for this URI is cached on the snapshot. - originalFH, _ := s.files.Load(uri) + originalFH, _ := s.files.Get(uri) var originalOpen, newOpen bool _, originalOpen = originalFH.(*overlay) _, newOpen = change.fileHandle.(*overlay) @@ -1853,7 +1853,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC if !change.exists { result.files.Delete(uri) } else { - result.files.Store(uri, change.fileHandle) + result.files.Set(uri, change.fileHandle) } // Make sure to remove the changed file from the unloadable set. @@ -2197,7 +2197,7 @@ func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH // lockedSnapshot must be locked. func peekOrParse(ctx context.Context, lockedSnapshot *snapshot, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { key := parseKey{file: fh.FileIdentity(), mode: mode} - if pgh, ok := lockedSnapshot.goFiles.Load(key); ok { + if pgh, ok := lockedSnapshot.goFiles.Get(key); ok { cached := pgh.handle.Cached(lockedSnapshot.generation) if cached != nil { cached := cached.(*parseGoData) diff --git a/internal/persistent/map.go b/internal/persistent/map.go index bbcb72b6ee9..9c17ad09a7f 100644 --- a/internal/persistent/map.go +++ b/internal/persistent/map.go @@ -140,9 +140,9 @@ func (node *mapNode) forEach(f func(key, value interface{})) { node.right.forEach(f) } -// Load returns the value stored in the map for a key, or nil if no entry is -// present. The ok result indicates whether an entry was found in the map. -func (pm *Map) Load(key interface{}) (interface{}, bool) { +// Get returns the map value associated with the specified key, or nil if no entry +// is present. The ok result indicates whether an entry was found in the map. +func (pm *Map) Get(key interface{}) (interface{}, bool) { node := pm.root for node != nil { if pm.less(key, node.key) { @@ -156,10 +156,10 @@ func (pm *Map) Load(key interface{}) (interface{}, bool) { return nil, false } -// Store sets the value for a key. +// Set updates the value associated with the specified key. // If release is non-nil, it will be called with entry's key and value once the // key is no longer contained in the map or any clone. -func (pm *Map) Store(key, value interface{}, release func(key, value interface{})) { +func (pm *Map) Set(key, value interface{}, release func(key, value interface{})) { first := pm.root second := newNodeWithRef(key, value, release) pm.root = union(first, second, pm.less, true) diff --git a/internal/persistent/map_test.go b/internal/persistent/map_test.go index 9585956100b..059f0da4c03 100644 --- a/internal/persistent/map_test.go +++ b/internal/persistent/map_test.go @@ -39,7 +39,7 @@ func TestSimpleMap(t *testing.T) { m3 := m1.clone() validateRef(t, m1, m3) - m3.insert(t, 8, 8) + m3.set(t, 8, 8) validateRef(t, m1, m3) m3.destroy() @@ -48,15 +48,15 @@ func TestSimpleMap(t *testing.T) { }) validateRef(t, m1) - m1.insert(t, 1, 1) + m1.set(t, 1, 1) validateRef(t, m1) - m1.insert(t, 2, 2) + m1.set(t, 2, 2) validateRef(t, m1) - m1.insert(t, 3, 3) + m1.set(t, 3, 3) validateRef(t, m1) m1.remove(t, 2) validateRef(t, m1) - m1.insert(t, 6, 6) + m1.set(t, 6, 6) validateRef(t, m1) assertSameMap(t, deletedEntries, map[mapEntry]struct{}{ @@ -66,25 +66,25 @@ func TestSimpleMap(t *testing.T) { m2 := m1.clone() validateRef(t, m1, m2) - m1.insert(t, 6, 60) + m1.set(t, 6, 60) validateRef(t, m1, m2) m1.remove(t, 1) validateRef(t, m1, m2) for i := 10; i < 14; i++ { - m1.insert(t, i, i) + m1.set(t, i, i) validateRef(t, m1, m2) } - m1.insert(t, 10, 100) + m1.set(t, 10, 100) validateRef(t, m1, m2) m1.remove(t, 12) validateRef(t, m1, m2) - m2.insert(t, 4, 4) + m2.set(t, 4, 4) validateRef(t, m1, m2) - m2.insert(t, 5, 5) + m2.set(t, 5, 5) validateRef(t, m1, m2) m1.destroy() @@ -100,7 +100,7 @@ func TestSimpleMap(t *testing.T) { {key: 13, value: 13}: {}, }) - m2.insert(t, 7, 7) + m2.set(t, 7, 7) validateRef(t, m2) m2.destroy() @@ -124,7 +124,7 @@ func TestRandomMap(t *testing.T) { keys := make([]int, 0, 1000) for i := 0; i < 1000; i++ { key := rand.Int() - m.insert(t, key, key) + m.set(t, key, key) keys = append(keys, key) if i%10 == 1 { @@ -245,9 +245,9 @@ func validateNode(t *testing.T, node *mapNode, less func(a, b interface{}) bool) validateNode(t, node.right, less) } -func (vm *validatedMap) insert(t *testing.T, key, value int) { +func (vm *validatedMap) set(t *testing.T, key, value int) { vm.seen[mapEntry{key: key, value: value}] = struct{}{} - vm.impl.Store(key, value, func(deletedKey, deletedValue interface{}) { + vm.impl.Set(key, value, func(deletedKey, deletedValue interface{}) { if deletedKey != key || deletedValue != value { t.Fatalf("unexpected passed in deleted entry: %v/%v, expected: %v/%v", deletedKey, deletedValue, key, value) } @@ -256,9 +256,9 @@ func (vm *validatedMap) insert(t *testing.T, key, value int) { vm.expected[key] = value vm.validate(t) - loadValue, ok := vm.impl.Load(key) - if !ok || loadValue != value { - t.Fatalf("unexpected load result after insertion, key: %v, expected: %v, got: %v (%v)", key, value, loadValue, ok) + gotValue, ok := vm.impl.Get(key) + if !ok || gotValue != value { + t.Fatalf("unexpected get result after insertion, key: %v, expected: %v, got: %v (%v)", key, value, gotValue, ok) } } @@ -267,9 +267,9 @@ func (vm *validatedMap) remove(t *testing.T, key int) { delete(vm.expected, key) vm.validate(t) - loadValue, ok := vm.impl.Load(key) + gotValue, ok := vm.impl.Get(key) if ok { - t.Fatalf("unexpected load result after removal, key: %v, got: %v", key, loadValue) + t.Fatalf("unexpected get result after removal, key: %v, got: %v", key, gotValue) } } From 60ca6366e648a6c37776ee8ae7c756d2c2576767 Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Fri, 24 Jun 2022 17:11:38 +0000 Subject: [PATCH 045/723] internal/lsp: use camel case for inlay hint config fields To properly format these field names in the vscode config ui these fields should be camel case. Change-Id: I3b8b8fb6371172ecb464710f7d91b9fc67e0ed42 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413684 TryBot-Result: Gopher Robot Run-TryBot: Jamal Carvalho Reviewed-by: Suzy Mueller gopls-CI: kokoro --- gopls/doc/inlayHints.md | 28 ++++++++++++++-------------- internal/lsp/source/api_json.go | 28 ++++++++++++++-------------- internal/lsp/source/inlay_hint.go | 14 +++++++------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/gopls/doc/inlayHints.md b/gopls/doc/inlayHints.md index a4fd3e51554..15957b52ede 100644 --- a/gopls/doc/inlayHints.md +++ b/gopls/doc/inlayHints.md @@ -3,23 +3,23 @@ This document describes the inlay hints that `gopls` uses inside the editor. -## **assign_variable_types** +## **assignVariableTypes** Enable/disable inlay hints for variable types in assign statements: i/* int/*, j/* int/* := 0, len(r)-1 -**Disabled by default. Enable it by setting `"hints": {"assign_variable_types": true}`.** +**Disabled by default. Enable it by setting `"hints": {"assignVariableTypes": true}`.** -## **composite_literal_fields** +## **compositeLiteralFields** Enable/disable inlay hints for composite literal field names: {in: "Hello, world", want: "dlrow ,olleH"} -**Disabled by default. Enable it by setting `"hints": {"composite_literal_fields": true}`.** +**Disabled by default. Enable it by setting `"hints": {"compositeLiteralFields": true}`.** -## **composite_literal_types** +## **compositeLiteralTypes** Enable/disable inlay hints for composite literal types: @@ -29,9 +29,9 @@ Enable/disable inlay hints for composite literal types: /*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"}, } -**Disabled by default. Enable it by setting `"hints": {"composite_literal_types": true}`.** +**Disabled by default. Enable it by setting `"hints": {"compositeLiteralTypes": true}`.** -## **constant_values** +## **constantValues** Enable/disable inlay hints for constant values: @@ -42,25 +42,25 @@ Enable/disable inlay hints for constant values: KindErrorf/* = 3*/ ) -**Disabled by default. Enable it by setting `"hints": {"constant_values": true}`.** +**Disabled by default. Enable it by setting `"hints": {"constantValues": true}`.** -## **function_type_parameters** +## **functionTypeParameters** Enable/disable inlay hints for implicit type parameters on generic functions: myFoo/*[int, string]*/(1, "hello") -**Disabled by default. Enable it by setting `"hints": {"function_type_parameters": true}`.** +**Disabled by default. Enable it by setting `"hints": {"functionTypeParameters": true}`.** -## **parameter_names** +## **parameterNames** Enable/disable inlay hints for parameter names: parseInt(/* str: */ "123", /* radix: */ 8) -**Disabled by default. Enable it by setting `"hints": {"parameter_names": true}`.** +**Disabled by default. Enable it by setting `"hints": {"parameterNames": true}`.** -## **range_variable_types** +## **rangeVariableTypes** Enable/disable inlay hints for variable types in range statements: @@ -68,6 +68,6 @@ Enable/disable inlay hints for variable types in range statements: fmt.Println(k, v) } -**Disabled by default. Enable it by setting `"hints": {"range_variable_types": true}`.** +**Disabled by default. Enable it by setting `"hints": {"rangeVariableTypes": true}`.** diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 4188d9d06fe..ef683f31a01 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -511,37 +511,37 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "hints specify inlay hints that users want to see.\nA full list of hints that gopls uses can be found\n[here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n", EnumKeys: EnumKeys{Keys: []EnumKey{ { - Name: "\"assign_variable_types\"", + Name: "\"assignVariableTypes\"", Doc: "Enable/disable inlay hints for variable types in assign statements:\n\n\ti/* int/*, j/* int/* := 0, len(r)-1", Default: "false", }, { - Name: "\"composite_literal_fields\"", + Name: "\"compositeLiteralFields\"", Doc: "Enable/disable inlay hints for composite literal field names:\n\n\t{in: \"Hello, world\", want: \"dlrow ,olleH\"}", Default: "false", }, { - Name: "\"composite_literal_types\"", + Name: "\"compositeLiteralTypes\"", Doc: "Enable/disable inlay hints for composite literal types:\n\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}", Default: "false", }, { - Name: "\"constant_values\"", + Name: "\"constantValues\"", Doc: "Enable/disable inlay hints for constant values:\n\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)", Default: "false", }, { - Name: "\"function_type_parameters\"", + Name: "\"functionTypeParameters\"", Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n\n\tmyFoo/*[int, string]*/(1, \"hello\")", Default: "false", }, { - Name: "\"parameter_names\"", + Name: "\"parameterNames\"", Doc: "Enable/disable inlay hints for parameter names:\n\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)", Default: "false", }, { - Name: "\"range_variable_types\"", + Name: "\"rangeVariableTypes\"", Doc: "Enable/disable inlay hints for variable types in range statements:\n\n\tfor k/* int*/, v/* string/* := range []string{} {\n\t\tfmt.Println(k, v)\n\t}", Default: "false", }, @@ -1026,31 +1026,31 @@ var GeneratedAPIJSON = &APIJSON{ }, Hints: []*HintJSON{ { - Name: "assign_variable_types", + Name: "assignVariableTypes", Doc: "Enable/disable inlay hints for variable types in assign statements:\n\n\ti/* int/*, j/* int/* := 0, len(r)-1", }, { - Name: "composite_literal_fields", + Name: "compositeLiteralFields", Doc: "Enable/disable inlay hints for composite literal field names:\n\n\t{in: \"Hello, world\", want: \"dlrow ,olleH\"}", }, { - Name: "composite_literal_types", + Name: "compositeLiteralTypes", Doc: "Enable/disable inlay hints for composite literal types:\n\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}", }, { - Name: "constant_values", + Name: "constantValues", Doc: "Enable/disable inlay hints for constant values:\n\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)", }, { - Name: "function_type_parameters", + Name: "functionTypeParameters", Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n\n\tmyFoo/*[int, string]*/(1, \"hello\")", }, { - Name: "parameter_names", + Name: "parameterNames", Doc: "Enable/disable inlay hints for parameter names:\n\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)", }, { - Name: "range_variable_types", + Name: "rangeVariableTypes", Doc: "Enable/disable inlay hints for variable types in range statements:\n\n\tfor k/* int*/, v/* string/* := range []string{} {\n\t\tfmt.Println(k, v)\n\t}", }, }, diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 99e1ad09d82..0c147283532 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -32,13 +32,13 @@ type Hint struct { } const ( - ParameterNames = "parameter_names" - AssignVariableTypes = "assign_variable_types" - ConstantValues = "constant_values" - RangeVariableTypes = "range_variable_types" - CompositeLiteralTypes = "composite_literal_types" - CompositeLiteralFieldNames = "composite_literal_fields" - FunctionTypeParameters = "function_type_parameters" + ParameterNames = "parameterNames" + AssignVariableTypes = "assignVariableTypes" + ConstantValues = "constantValues" + RangeVariableTypes = "rangeVariableTypes" + CompositeLiteralTypes = "compositeLiteralTypes" + CompositeLiteralFieldNames = "compositeLiteralFields" + FunctionTypeParameters = "functionTypeParameters" ) var AllInlayHints = map[string]*Hint{ From e1ec1f32302c374d496c1be1dc701b6c5a064a00 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 24 Jun 2022 16:31:42 -0400 Subject: [PATCH 046/723] internal/imports: use a module resolver if GOWORK is set Previously, gopls would fall back on a gopath resolver when running goimports from a directory containing go.work (but not go.mod). Fix this by update the code to recognize that GOWORK also puts goimports into module mode. All the work to _support_ go.work had already been done, but the tests were only passing because they were setting GO111MODULE=on explicitly (and therefore GOMOD=/dev/null was satisfying the pre-existing check). Also add a test for the regression in gopls. Fixes golang/go#52784 Change-Id: I31df6f71a949a5668e8dc001b3ee25ad26f2f927 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413689 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Michael Matloob --- gopls/internal/regtest/misc/imports_test.go | 46 ++++++++++++++++ internal/imports/fix.go | 4 +- internal/imports/mod.go | 14 ++++- internal/imports/mod_test.go | 61 +++++++++++---------- internal/lsp/cache/load.go | 2 +- 5 files changed, 93 insertions(+), 34 deletions(-) diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index 4ae2be6bf10..1250e78e776 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -214,3 +214,49 @@ func TestA(t *testing.T) { ) }) } + +// Test for golang/go#52784 +func TestGoWorkImports(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + const pkg = ` +-- go.work -- +go 1.19 + +use ( + ./caller + ./mod +) +-- caller/go.mod -- +module caller.com + +go 1.18 + +require mod.com v0.0.0 + +replace mod.com => ../mod +-- caller/caller.go -- +package main + +func main() { + a.Test() +} +-- mod/go.mod -- +module mod.com + +go 1.18 +-- mod/a/a.go -- +package a + +func Test() { +} +` + Run(t, pkg, func(t *testing.T, env *Env) { + env.OpenFile("caller/caller.go") + env.Await(env.DiagnosticAtRegexp("caller/caller.go", "a.Test")) + + // Saving caller.go should trigger goimports, which should find a.Test in + // the mod.com module, thanks to the go.work file. + env.SaveBuffer("caller/caller.go") + env.Await(EmptyDiagnostics("caller/caller.go")) + }) +} diff --git a/internal/imports/fix.go b/internal/imports/fix.go index d859617b774..9e373d64ebc 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -796,7 +796,7 @@ func GetPackageExports(ctx context.Context, wrapped func(PackageExport), searchP return getCandidatePkgs(ctx, callback, filename, filePkg, env) } -var RequiredGoEnvVars = []string{"GO111MODULE", "GOFLAGS", "GOINSECURE", "GOMOD", "GOMODCACHE", "GONOPROXY", "GONOSUMDB", "GOPATH", "GOPROXY", "GOROOT", "GOSUMDB"} +var RequiredGoEnvVars = []string{"GO111MODULE", "GOFLAGS", "GOINSECURE", "GOMOD", "GOMODCACHE", "GONOPROXY", "GONOSUMDB", "GOPATH", "GOPROXY", "GOROOT", "GOSUMDB", "GOWORK"} // ProcessEnv contains environment variables and settings that affect the use of // the go command, the go/build package, etc. @@ -906,7 +906,7 @@ func (e *ProcessEnv) GetResolver() (Resolver, error) { if err := e.init(); err != nil { return nil, err } - if len(e.Env["GOMOD"]) == 0 { + if len(e.Env["GOMOD"]) == 0 && len(e.Env["GOWORK"]) == 0 { e.resolver = newGopathResolver(e) return e.resolver, nil } diff --git a/internal/imports/mod.go b/internal/imports/mod.go index 2bcf41f5fa7..46693f24339 100644 --- a/internal/imports/mod.go +++ b/internal/imports/mod.go @@ -70,9 +70,17 @@ func (r *ModuleResolver) init() error { Logf: r.env.Logf, WorkingDir: r.env.WorkingDir, } - vendorEnabled, mainModVendor, err := gocommand.VendorEnabled(context.TODO(), inv, r.env.GocmdRunner) - if err != nil { - return err + + vendorEnabled := false + var mainModVendor *gocommand.ModuleJSON + + // Module vendor directories are ignored in workspace mode: + // https://go.googlesource.com/proposal/+/master/design/45713-workspace.md + if len(r.env.Env["GOWORK"]) == 0 { + vendorEnabled, mainModVendor, err = gocommand.VendorEnabled(context.TODO(), inv, r.env.GocmdRunner) + if err != nil { + return err + } } if mainModVendor != nil && vendorEnabled { diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go index 5f71805fa77..8063dbe0f74 100644 --- a/internal/imports/mod_test.go +++ b/internal/imports/mod_test.go @@ -29,7 +29,7 @@ import ( // Tests that we can find packages in the stdlib. func TestScanStdlib(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x `, "") @@ -42,7 +42,7 @@ module x // where the module is in scope -- here we have to figure out the import path // without any help from go list. func TestScanOutOfScopeNestedModule(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -68,7 +68,7 @@ package x`, "") // Tests that we don't find a nested module contained in a local replace target. // The code for this case is too annoying to write, so it's just ignored. func TestScanNestedModuleInLocalReplace(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -107,7 +107,7 @@ package z // Tests that path encoding is handled correctly. Adapted from mod_case.txt. func TestModCase(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -124,7 +124,7 @@ import _ "rsc.io/QUOTE/QUOTE" // Not obviously relevant to goimports. Adapted from mod_domain_root.txt anyway. func TestModDomainRoot(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -140,7 +140,7 @@ import _ "example.com" // Tests that scanning the module cache > 1 time is able to find the same module. func TestModMultipleScans(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -159,7 +159,7 @@ import _ "example.com" // Tests that scanning the module cache > 1 time is able to find the same module // in the module cache. func TestModMultipleScansWithSubdirs(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -178,7 +178,7 @@ import _ "rsc.io/quote" // Tests that scanning the module cache > 1 after changing a package in module cache to make it unimportable // is able to find the same module. func TestModCacheEditModFile(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -219,7 +219,7 @@ import _ "rsc.io/quote" // Tests that -mod=vendor works. Adapted from mod_vendor_build.txt. func TestModVendorBuild(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module m go 1.12 @@ -250,7 +250,7 @@ import _ "rsc.io/sampler" // Tests that -mod=vendor is auto-enabled only for go1.14 and higher. // Vaguely inspired by mod_vendor_auto.txt. func TestModVendorAuto(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module m go 1.14 @@ -276,7 +276,7 @@ import _ "rsc.io/sampler" // Tests that a module replace works. Adapted from mod_list.txt. We start with // go.mod2; the first part of the test is irrelevant. func TestModList(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x require rsc.io/quote v1.5.1 @@ -293,7 +293,7 @@ import _ "rsc.io/quote" // Tests that a local replace works. Adapted from mod_local_replace.txt. func TestModLocalReplace(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- x/y/go.mod -- module x/y require zz v1.0.0 @@ -317,7 +317,7 @@ package z // Tests that the package at the root of the main module can be found. // Adapted from the first part of mod_multirepo.txt. func TestModMultirepo1(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module rsc.io/quote @@ -333,7 +333,7 @@ package quote // of mod_multirepo.txt (We skip the case where it doesn't have a go.mod // entry -- we just don't work in that case.) func TestModMultirepo3(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module rsc.io/quote @@ -352,7 +352,7 @@ import _ "rsc.io/quote/v2" // Tests that a nested module is found in the module cache, even though // it's checked out. Adapted from the fourth part of mod_multirepo.txt. func TestModMultirepo4(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module rsc.io/quote require rsc.io/quote/v2 v2.0.1 @@ -376,7 +376,7 @@ import _ "rsc.io/quote/v2" // Tests a simple module dependency. Adapted from the first part of mod_replace.txt. func TestModReplace1(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module quoter @@ -392,7 +392,7 @@ package main // Tests a local replace. Adapted from the second part of mod_replace.txt. func TestModReplace2(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module quoter @@ -418,7 +418,7 @@ import "rsc.io/sampler" // Tests that a module can be replaced by a different module path. Adapted // from the third part of mod_replace.txt. func TestModReplace3(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module quoter @@ -451,7 +451,7 @@ package quote // mod_replace_import.txt, with example.com/v changed to /vv because Go 1.11 // thinks /v is an invalid major version. func TestModReplaceImport(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module example.com/m @@ -556,7 +556,7 @@ package v func TestModWorkspace(t *testing.T) { testenv.NeedsGo1Point(t, 18) - mt := setup(t, ` + mt := setup(t, nil, ` -- go.work -- go 1.18 @@ -592,7 +592,7 @@ package b func TestModWorkspaceReplace(t *testing.T) { testenv.NeedsGo1Point(t, 18) - mt := setup(t, ` + mt := setup(t, nil, ` -- go.work -- use m @@ -651,7 +651,7 @@ func G() { func TestModWorkspaceReplaceOverride(t *testing.T) { testenv.NeedsGo1Point(t, 18) - mt := setup(t, `-- go.work -- + mt := setup(t, nil, `-- go.work -- use m use n replace example.com/dep => ./dep3 @@ -716,7 +716,7 @@ func G() { func TestModWorkspacePrune(t *testing.T) { testenv.NeedsGo1Point(t, 18) - mt := setup(t, ` + mt := setup(t, nil, ` -- go.work -- go 1.18 @@ -885,7 +885,7 @@ package z // Tests that we handle GO111MODULE=on with no go.mod file. See #30855. func TestNoMainModule(t *testing.T) { testenv.NeedsGo1Point(t, 12) - mt := setup(t, ` + mt := setup(t, map[string]string{"GO111MODULE": "on"}, ` -- x.go -- package x `, "") @@ -993,7 +993,9 @@ type modTest struct { // setup builds a test environment from a txtar and supporting modules // in testdata/mod, along the lines of TestScript in cmd/go. -func setup(t *testing.T, main, wd string) *modTest { +// +// extraEnv is applied on top of the default test env. +func setup(t *testing.T, extraEnv map[string]string, main, wd string) *modTest { t.Helper() testenv.NeedsGo1Point(t, 11) testenv.NeedsTool(t, "go") @@ -1023,13 +1025,16 @@ func setup(t *testing.T, main, wd string) *modTest { Env: map[string]string{ "GOPATH": filepath.Join(dir, "gopath"), "GOMODCACHE": "", - "GO111MODULE": "on", + "GO111MODULE": "auto", "GOSUMDB": "off", "GOPROXY": proxydir.ToURL(proxyDir), }, WorkingDir: filepath.Join(mainDir, wd), GocmdRunner: &gocommand.Runner{}, } + for k, v := range extraEnv { + env.Env[k] = v + } if *testDebug { env.Logf = log.Printf } @@ -1168,7 +1173,7 @@ func removeDir(dir string) { // Tests that findModFile can find the mod files from a path in the module cache. func TestFindModFileModCache(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -1220,7 +1225,7 @@ func TestInvalidModCache(t *testing.T) { } func TestGetCandidatesRanking(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module example.com diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 5ce49f00d43..3c6795370d1 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -253,7 +253,7 @@ func (m *moduleErrorMap) Error() string { var buf bytes.Buffer fmt.Fprintf(&buf, "%d modules have errors:\n", len(paths)) for _, path := range paths { - fmt.Fprintf(&buf, "\t%s", m.errs[path][0].Msg) + fmt.Fprintf(&buf, "\t%s:%s\n", path, m.errs[path][0].Msg) } return buf.String() From c36379be2b347665c06d85bea46f9d4b977ec3ce Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 24 Jun 2022 11:31:51 -0400 Subject: [PATCH 047/723] internal/lsp/cache: don't set new metadata if existing is valid If gopls believes it has valid metadata for a package, don't set new metadata. This is consistent with previous behavior that was changed in CL 340851. In principle this shouldn't matter, but in practice there are places where gopls doesn't yet want to invalidate packages, *even though* their metadata may have changed (such as while editing a go.mod file before saving). In the future we should eliminate these places, but for now we should let snapshot.clone control this invalidation. This also reduces the number of type-checked packages we invalidate on load. Change-Id: I0cc9bd4186245bec401332198de0047ff37e7ec7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413681 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan --- internal/lsp/cache/graph.go | 1 - internal/lsp/cache/load.go | 38 +++++++++++++++++++++++++++------- internal/lsp/cache/snapshot.go | 5 +---- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go index 36e658b3a86..3f247739fd7 100644 --- a/internal/lsp/cache/graph.go +++ b/internal/lsp/cache/graph.go @@ -18,7 +18,6 @@ import ( // TODO(rfindley): make this type immutable, so that it may be shared across // snapshots. type metadataGraph struct { - // metadata maps package IDs to their associated metadata. metadata map[PackageID]*KnownMetadata diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 3c6795370d1..bdafdcd115b 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -444,24 +444,42 @@ func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generati // metadata exists for all dependencies. func computeMetadataUpdates(ctx context.Context, g *metadataGraph, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { id := PackageID(pkg.ID) - if new := updates[id]; new != nil { - return nil - } if source.IsCommandLineArguments(pkg.ID) { suffix := ":" + strings.Join(query, ",") id = PackageID(string(id) + suffix) pkgPath = PackagePath(string(pkgPath) + suffix) } + + // If we have valid metadata for this package, don't update. This minimizes + // the amount of subsequent invalidation. + // + // TODO(rfindley): perform a sanity check that metadata matches here. If not, + // we have an invalidation bug elsewhere. + if existing := g.metadata[id]; existing != nil && existing.Valid { + return nil + } + if _, ok := updates[id]; ok { // If we've already seen this dependency, there may be an import cycle, or // we may have reached the same package transitively via distinct paths. // Check the path to confirm. + + // TODO(rfindley): this doesn't look right. Any single piece of new + // metadata could theoretically introduce import cycles in the metadata + // graph. What's the point of this limited check here (and is it even + // possible to get an import cycle in data from go/packages)? Consider + // simply returning, so that this function need not return an error. + // + // We should consider doing a more complete guard against import cycles + // elsewhere. for _, prev := range path { if prev == id { return fmt.Errorf("import cycle detected: %q", id) } } + return nil } + // Recreate the metadata rather than reusing it to avoid locking. m := &KnownMetadata{ Metadata: &Metadata{ @@ -504,6 +522,14 @@ func computeMetadataUpdates(ctx context.Context, g *metadataGraph, pkgPath Packa } for importPath, importPkg := range pkg.Imports { + // TODO(rfindley): in rare cases it is possible that the import package + // path is not the same as the package path of the import. That is to say + // (quoting adonovan): + // "The importPath string is the path by which one package is imported from + // another, but that needn't be the same as its internal name (sometimes + // called the "package path") used to prefix its linker symbols" + // + // We should not set this package path on the metadata of the dep. importPkgPath := PackagePath(importPath) importID := PackageID(importPkg.ID) @@ -517,10 +543,8 @@ func computeMetadataUpdates(ctx context.Context, g *metadataGraph, pkgPath Packa m.MissingDeps[importPkgPath] = struct{}{} continue } - if noValidMetadataForID(g, importID) { - if err := computeMetadataUpdates(ctx, g, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { - event.Error(ctx, "error in dependency", err) - } + if err := computeMetadataUpdates(ctx, g, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { + event.Error(ctx, "error in dependency", err) } } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index c8d60853332..05f892808fa 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1304,11 +1304,8 @@ func (s *snapshot) noValidMetadataForURILocked(uri span.URI) bool { func (s *snapshot) noValidMetadataForID(id PackageID) bool { s.mu.Lock() defer s.mu.Unlock() - return noValidMetadataForID(s.meta, id) -} -func noValidMetadataForID(g *metadataGraph, id PackageID) bool { - m := g.metadata[id] + m := s.meta.metadata[id] return m == nil || !m.Valid } From 93a03c2c548821d80e09ec6159b80b6c8bee8887 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 24 Jun 2022 12:42:15 -0400 Subject: [PATCH 048/723] internal/lsp/cache: invalidate reverse closure when loading packages When setting new metadata for a package ID, we invalidate the corresponding type-checked packages. Whenever we invalidate a package, we must also invalidate its reverse transitive closure. Otherwise we may end up in a scenario where the go/types import graph does not match gopls' view of the import graph. Change-Id: I8db6ff3e4a722656e6dde7907e7c0470375c4847 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413683 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- internal/lsp/cache/graph.go | 41 ++++++++++++++++++++++++++++++++++ internal/lsp/cache/load.go | 18 +++++++++++---- internal/lsp/cache/snapshot.go | 29 +++++++----------------- 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go index 3f247739fd7..dc7d4faef78 100644 --- a/internal/lsp/cache/graph.go +++ b/internal/lsp/cache/graph.go @@ -128,3 +128,44 @@ func (g *metadataGraph) build() { } } } + +// reverseTransitiveClosure calculates the set of packages that transitively +// reach an id in ids via their Deps. The result also includes given ids. +// +// If includeInvalid is false, the algorithm ignores packages with invalid +// metadata (including those in the given list of ids). +func (g *metadataGraph) reverseTransitiveClosure(includeInvalid bool, ids ...PackageID) map[PackageID]struct{} { + seen := make(map[PackageID]struct{}) + var visitAll func([]PackageID) + visitAll = func(ids []PackageID) { + for _, id := range ids { + if _, ok := seen[id]; ok { + continue + } + m := g.metadata[id] + // Only use invalid metadata if we support it. + if m == nil || !(m.Valid || includeInvalid) { + continue + } + seen[id] = struct{}{} + visitAll(g.importedBy[id]) + } + } + visitAll(ids) + return seen +} + +func collectReverseTransitiveClosure(g *metadataGraph, includeInvalid bool, ids []PackageID, seen map[PackageID]struct{}) { + for _, id := range ids { + if _, ok := seen[id]; ok { + continue + } + m := g.metadata[id] + // Only use invalid metadata if we support it. + if m == nil || !(m.Valid || includeInvalid) { + continue + } + seen[id] = struct{}{} + collectReverseTransitiveClosure(g, includeInvalid, g.importedBy[id], seen) + } +} diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index bdafdcd115b..db9a06d4dee 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -204,18 +204,28 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } } + var loadedIDs []PackageID + for id := range updates { + loadedIDs = append(loadedIDs, id) + } + s.mu.Lock() + + // invalidate the reverse transitive closure of packages that have changed. + invalidatedPackages := s.meta.reverseTransitiveClosure(true, loadedIDs...) s.meta = s.meta.Clone(updates) + // Invalidate any packages we may have associated with this metadata. // - // TODO(rfindley): if we didn't already invalidate these in snapshot.clone, - // shouldn't we invalidate the reverse transitive closure? - for _, m := range updates { + // TODO(rfindley): this should not be necessary, as we should have already + // invalidated in snapshot.clone. + for id := range invalidatedPackages { for _, mode := range []source.ParseMode{source.ParseHeader, source.ParseExported, source.ParseFull} { - key := packageKey{mode, m.ID} + key := packageKey{mode, id} delete(s.packages, key) } } + s.workspacePackages = computeWorkspacePackagesLocked(s, s.meta) s.dumpWorkspace("load") s.mu.Unlock() diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 05f892808fa..8194750b331 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -70,6 +70,10 @@ type snapshot struct { builtin span.URI // meta holds loaded metadata. + // + // meta is guarded by mu, but the metadataGraph itself is immutable. + // TODO(rfindley): in many places we hold mu while operating on meta, even + // though we only need to hold mu while reading the pointer. meta *metadataGraph // files maps file URIs to their corresponding FileHandles. @@ -627,8 +631,10 @@ func (s *snapshot) GetReverseDependencies(ctx context.Context, id string) ([]sou if err := s.awaitLoaded(ctx); err != nil { return nil, err } - ids := make(map[PackageID]struct{}) - s.transitiveReverseDependencies(PackageID(id), ids) + s.mu.Lock() + meta := s.meta + s.mu.Unlock() + ids := meta.reverseTransitiveClosure(s.useInvalidMetadata(), PackageID(id)) // Make sure to delete the original package ID from the map. delete(ids, PackageID(id)) @@ -652,24 +658,6 @@ func (s *snapshot) checkedPackage(ctx context.Context, id PackageID, mode source return ph.check(ctx, s) } -// transitiveReverseDependencies populates the ids map with package IDs -// belonging to the provided package and its transitive reverse dependencies. -func (s *snapshot) transitiveReverseDependencies(id PackageID, ids map[PackageID]struct{}) { - if _, ok := ids[id]; ok { - return - } - m := s.getMetadata(id) - // Only use invalid metadata if we support it. - if m == nil || !(m.Valid || s.useInvalidMetadata()) { - return - } - ids[id] = struct{}{} - importedBy := s.getImportedBy(id) - for _, parentID := range importedBy { - s.transitiveReverseDependencies(parentID, ids) - } -} - func (s *snapshot) getGoFile(key parseKey) *parseGoHandle { s.mu.Lock() defer s.mu.Unlock() @@ -1879,7 +1867,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // Invalidate reverse dependencies too. - // TODO(heschi): figure out the locking model and use transitiveReverseDeps? // idsToInvalidate keeps track of transitive reverse dependencies. // If an ID is present in the map, invalidate its types. // If an ID's value is true, invalidate its metadata too. From 10494c735e6ba5ec3a5eed252fc61a116a21a31d Mon Sep 17 00:00:00 2001 From: Ben Sarah Golightly Date: Fri, 24 Jun 2022 23:26:15 +0100 Subject: [PATCH 049/723] cmd/digraph: fix typo Change-Id: I086edda41c57b603afa660afb9396e17ba6c1a36 Reviewed-on: https://go-review.googlesource.com/c/tools/+/414074 Reviewed-by: Ian Lance Taylor Auto-Submit: Ian Lance Taylor TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Ian Lance Taylor --- cmd/digraph/digraph.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/digraph/digraph.go b/cmd/digraph/digraph.go index 62cb08d23a8..69d84ad5012 100644 --- a/cmd/digraph/digraph.go +++ b/cmd/digraph/digraph.go @@ -34,7 +34,7 @@ The support commands are: sccs all strongly connected components (one per line) scc - the set of nodes nodes strongly connected to the specified one + the set of nodes strongly connected to the specified one focus the subgraph containing all directed paths that pass through the specified node From 56116ec0159179782987cb761912b6fd3fa997ee Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Sun, 26 Jun 2022 15:17:46 -0400 Subject: [PATCH 050/723] internal/memoize: don't destroy reference counted handles Unlike generational handles, when reference counted handles are evicted from the Store we don't know that they are also no longer in use by active goroutines. Destroying them causes goroutine leaks. Also fix a data race because Handle.mu was not acquired in the release func returned by GetHandle. Change-Id: Ida7bb6961a035dd24ef8566c7e4faa6890296b5b Reviewed-on: https://go-review.googlesource.com/c/tools/+/414455 Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- internal/lsp/cache/parse.go | 2 +- internal/memoize/memoize.go | 70 +++++++++++++++++++++++--------- internal/memoize/memoize_test.go | 64 +++++++++++++++++++++++------ 3 files changed, 104 insertions(+), 32 deletions(-) diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index 376524bd324..f7b4f9c7031 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -61,7 +61,7 @@ func (s *snapshot) parseGoHandle(ctx context.Context, fh source.FileHandle, mode parseHandle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { snapshot := arg.(*snapshot) return parseGo(ctx, snapshot.FileSet(), fh, mode) - }, nil) + }) pgh := &parseGoHandle{ handle: parseHandle, diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 48a642c990e..6477abbb810 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -83,18 +83,18 @@ func (g *Generation) Destroy(destroyedBy string) { g.store.mu.Lock() defer g.store.mu.Unlock() - for _, e := range g.store.handles { - if !e.trackGenerations { + for _, h := range g.store.handles { + if !h.trackGenerations { continue } - e.mu.Lock() - if _, ok := e.generations[g]; ok { - delete(e.generations, g) // delete even if it's dead, in case of dangling references to the entry. - if len(e.generations) == 0 { - e.destroy(g.store) + h.mu.Lock() + if _, ok := h.generations[g]; ok { + delete(h.generations, g) // delete even if it's dead, in case of dangling references to the entry. + if len(h.generations) == 0 { + h.destroy(g.store) } } - e.mu.Unlock() + h.mu.Unlock() } delete(g.store.generations, g) } @@ -120,6 +120,12 @@ type Function func(ctx context.Context, arg Arg) interface{} type state int +// TODO(rfindley): remove stateDestroyed; Handles should not need to know +// whether or not they have been destroyed. +// +// TODO(rfindley): also consider removing stateIdle. Why create a handle if you +// aren't certain you're going to need its result? And if you know you need its +// result, why wait to begin computing it? const ( stateIdle = iota stateRunning @@ -139,6 +145,12 @@ const ( // they decrement waiters. If it drops to zero, the inner context is cancelled, // computation is abandoned, and state resets to idle to start the process over // again. +// +// Handles may be tracked by generations, or directly reference counted, as +// determined by the trackGenerations field. See the field comments for more +// information about the differences between these two forms. +// +// TODO(rfindley): eliminate generational handles. type Handle struct { key interface{} mu sync.Mutex @@ -159,6 +171,11 @@ type Handle struct { value interface{} // cleanup, if non-nil, is used to perform any necessary clean-up on values // produced by function. + // + // cleanup is never set for reference counted handles. + // + // TODO(rfindley): remove this field once workspace folders no longer need to + // be tracked. cleanup func(interface{}) // If trackGenerations is set, this handle tracks generations in which it @@ -190,19 +207,27 @@ func (g *Generation) Bind(key interface{}, function Function, cleanup func(inter // // As in opposite to Bind it returns a release callback which has to be called // once this reference to handle is not needed anymore. -func (g *Generation) GetHandle(key interface{}, function Function, cleanup func(interface{})) (*Handle, func()) { - handle := g.getHandle(key, function, cleanup, false) +func (g *Generation) GetHandle(key interface{}, function Function) (*Handle, func()) { + h := g.getHandle(key, function, nil, false) store := g.store release := func() { + // Acquire store.mu before mutating refCounter store.mu.Lock() defer store.mu.Unlock() - handle.refCounter-- - if handle.refCounter == 0 { - handle.destroy(store) + h.mu.Lock() + defer h.mu.Unlock() + + h.refCounter-- + if h.refCounter == 0 { + // Don't call h.destroy: for reference counted handles we can't know when + // they are no longer reachable from runnable goroutines. For example, + // gopls could have a current operation that is using a packageHandle. + // Destroying the handle here would cause that operation to hang. + delete(store.handles, h.key) } } - return handle, release + return h, release } func (g *Generation) getHandle(key interface{}, function Function, cleanup func(interface{}), trackGenerations bool) *Handle { @@ -252,13 +277,13 @@ func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { s.mu.Lock() defer s.mu.Unlock() - for k, e := range s.handles { + for k, h := range s.handles { var v interface{} - e.mu.Lock() - if e.state == stateCompleted { - v = e.value + h.mu.Lock() + if h.state == stateCompleted { + v = h.value } - e.mu.Unlock() + h.mu.Unlock() if v == nil { continue } @@ -278,6 +303,7 @@ func (g *Generation) Inherit(h *Handle) { h.incrementRef(g) } +// destroy marks h as destroyed. h.mu and store.mu must be held. func (h *Handle) destroy(store *Store) { h.state = stateDestroyed if h.cleanup != nil && h.value != nil { @@ -409,6 +435,12 @@ func (h *Handle) run(ctx context.Context, g *Generation, arg Arg) (interface{}, } return } + + if h.cleanup != nil && h.value != nil { + // Clean up before overwriting an existing value. + h.cleanup(h.value) + } + // At this point v will be cleaned up whenever h is destroyed. h.value = v h.function = nil diff --git a/internal/memoize/memoize_test.go b/internal/memoize/memoize_test.go index bffbfc2f6b3..ae387b8d049 100644 --- a/internal/memoize/memoize_test.go +++ b/internal/memoize/memoize_test.go @@ -7,7 +7,9 @@ package memoize_test import ( "context" "strings" + "sync" "testing" + "time" "golang.org/x/tools/internal/memoize" ) @@ -112,15 +114,12 @@ func TestHandleRefCounting(t *testing.T) { g1 := s.Generation("g1") v1 := false v2 := false - cleanup := func(v interface{}) { - *(v.(*bool)) = true - } h1, release1 := g1.GetHandle("key1", func(context.Context, memoize.Arg) interface{} { return &v1 - }, nil) + }) h2, release2 := g1.GetHandle("key2", func(context.Context, memoize.Arg) interface{} { return &v2 - }, cleanup) + }) expectGet(t, h1, g1, &v1) expectGet(t, h2, g1, &v2) @@ -131,7 +130,7 @@ func TestHandleRefCounting(t *testing.T) { h2Copy, release2Copy := g2.GetHandle("key2", func(context.Context, memoize.Arg) interface{} { return &v1 - }, nil) + }) if h2 != h2Copy { t.Error("NewHandle returned a new value while old is not destroyed yet") } @@ -140,24 +139,65 @@ func TestHandleRefCounting(t *testing.T) { release2() if got, want := v2, false; got != want { - t.Error("after destroying first v2 ref, v2 is cleaned up") + t.Errorf("after destroying first v2 ref, got %v, want %v", got, want) } release2Copy() - if got, want := v2, true; got != want { - t.Error("after destroying second v2 ref, v2 is not cleaned up") - } if got, want := v1, false; got != want { - t.Error("after destroying v2, v1 is cleaned up") + t.Errorf("after destroying v2, got %v, want %v", got, want) } release1() g3 := s.Generation("g3") h2Copy, release2Copy = g3.GetHandle("key2", func(context.Context, memoize.Arg) interface{} { return &v2 - }, cleanup) + }) if h2 == h2Copy { t.Error("NewHandle returned previously destroyed value") } release2Copy() g3.Destroy("by test") } + +func TestHandleDestroyedWhileRunning(t *testing.T) { + // Test that calls to Handle.Get return even if the handle is destroyed while + // running. + + s := &memoize.Store{} + g := s.Generation("g") + c := make(chan int) + + var v int + h, release := g.GetHandle("key", func(ctx context.Context, _ memoize.Arg) interface{} { + <-c + <-c + if err := ctx.Err(); err != nil { + t.Errorf("ctx.Err() = %v, want nil", err) + } + return &v + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // arbitrary timeout; may be removed if it causes flakes + defer cancel() + + var wg sync.WaitGroup + wg.Add(1) + var got interface{} + var err error + go func() { + got, err = h.Get(ctx, g, nil) + wg.Done() + }() + + c <- 0 // send once to enter the handle function + release() // release before the handle function returns + c <- 0 // let the handle function proceed + + wg.Wait() + + if err != nil { + t.Errorf("Get() failed: %v", err) + } + if got != &v { + t.Errorf("Get() = %v, want %v", got, v) + } +} From 66bbba3d58b08b34aaf65516ae70d820b70b7b3e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 27 Jun 2022 12:43:16 -0400 Subject: [PATCH 051/723] internal/memoize: remove unused Store.generations map This change removes an unused map, renames Store.mu, and add minor commentary. Change-Id: I2f064ff0daf87e0f73930bc980760a453d18e70a Reviewed-on: https://go-review.googlesource.com/c/tools/+/414494 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- internal/memoize/memoize.go | 52 ++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 6477abbb810..a758deeb7f8 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -31,27 +31,15 @@ var ( // Store binds keys to functions, returning handles that can be used to access // the functions results. type Store struct { - mu sync.Mutex - // handles is the set of values stored. - handles map[interface{}]*Handle - - // generations is the set of generations live in this store. - generations map[*Generation]struct{} + handlesMu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu + handles map[interface{}]*Handle } // Generation creates a new Generation associated with s. Destroy must be // called on the returned Generation once it is no longer in use. name is // for debugging purposes only. func (s *Store) Generation(name string) *Generation { - s.mu.Lock() - defer s.mu.Unlock() - if s.handles == nil { - s.handles = map[interface{}]*Handle{} - s.generations = map[*Generation]struct{}{} - } - g := &Generation{store: s, name: name} - s.generations[g] = struct{}{} - return g + return &Generation{store: s, name: name} } // A Generation is a logical point in time of the cache life-cycle. Cache @@ -81,8 +69,8 @@ func (g *Generation) Destroy(destroyedBy string) { panic("Destroy on generation " + g.name + " already destroyed by " + prevDestroyedBy) } - g.store.mu.Lock() - defer g.store.mu.Unlock() + g.store.handlesMu.Lock() + defer g.store.handlesMu.Unlock() for _, h := range g.store.handles { if !h.trackGenerations { continue @@ -96,7 +84,6 @@ func (g *Generation) Destroy(destroyedBy string) { } h.mu.Unlock() } - delete(g.store.generations, g) } // Acquire creates a new reference to g, and returns a func to release that @@ -153,7 +140,7 @@ const ( // TODO(rfindley): eliminate generational handles. type Handle struct { key interface{} - mu sync.Mutex + mu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu // generations is the set of generations in which this handle is valid. generations map[*Generation]struct{} @@ -211,9 +198,9 @@ func (g *Generation) GetHandle(key interface{}, function Function) (*Handle, fun h := g.getHandle(key, function, nil, false) store := g.store release := func() { - // Acquire store.mu before mutating refCounter - store.mu.Lock() - defer store.mu.Unlock() + // Acquire store.handlesMu before mutating refCounter + store.handlesMu.Lock() + defer store.handlesMu.Unlock() h.mu.Lock() defer h.mu.Unlock() @@ -239,8 +226,8 @@ func (g *Generation) getHandle(key interface{}, function Function, cleanup func( if atomic.LoadUint32(&g.destroyed) != 0 { panic("operation on generation " + g.name + " destroyed by " + g.destroyedBy) } - g.store.mu.Lock() - defer g.store.mu.Unlock() + g.store.handlesMu.Lock() + defer g.store.handlesMu.Unlock() h, ok := g.store.handles[key] if !ok { h = &Handle{ @@ -252,6 +239,10 @@ func (g *Generation) getHandle(key interface{}, function Function, cleanup func( if trackGenerations { h.generations = make(map[*Generation]struct{}, 1) } + + if g.store.handles == nil { + g.store.handles = map[interface{}]*Handle{} + } g.store.handles[key] = h } @@ -261,10 +252,11 @@ func (g *Generation) getHandle(key interface{}, function Function, cleanup func( // Stats returns the number of each type of value in the store. func (s *Store) Stats() map[reflect.Type]int { - s.mu.Lock() - defer s.mu.Unlock() - result := map[reflect.Type]int{} + + s.handlesMu.Lock() + defer s.handlesMu.Unlock() + for k := range s.handles { result[reflect.TypeOf(k)]++ } @@ -274,8 +266,8 @@ func (s *Store) Stats() map[reflect.Type]int { // DebugOnlyIterate iterates through all live cache entries and calls f on them. // It should only be used for debugging purposes. func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { - s.mu.Lock() - defer s.mu.Unlock() + s.handlesMu.Lock() + defer s.handlesMu.Unlock() for k, h := range s.handles { var v interface{} @@ -303,7 +295,7 @@ func (g *Generation) Inherit(h *Handle) { h.incrementRef(g) } -// destroy marks h as destroyed. h.mu and store.mu must be held. +// destroy marks h as destroyed. h.mu and store.handlesMu must be held. func (h *Handle) destroy(store *Store) { h.state = stateDestroyed if h.cleanup != nil && h.value != nil { From ec0831a43429a1ab34768947d9c4874fa9906e60 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 24 Jun 2022 18:10:35 -0400 Subject: [PATCH 052/723] refactor/satisfy: don't crash on type parameters This change causes the satisfy constraint pass to correctly handle type parameters. In nearly all cases this means calling coreType(T) instead of T.Underlying(). This, and the addition of cases for C[T] and C[X, Y], should make the code robust to generic syntax. However, it is still not clear what the semantics of constraints are for the renaming tool. That work is left to a follow-up. Also, add a test suite that exercises all the basic operators, using generics in each case. Fixes golang/go#52940 Change-Id: Ic1261eb551c99b582c35fadaa148b979532588df Reviewed-on: https://go-review.googlesource.com/c/tools/+/413690 Reviewed-by: Robert Findley --- refactor/satisfy/find.go | 78 ++++++++---- refactor/satisfy/find_test.go | 226 ++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 27 deletions(-) create mode 100644 refactor/satisfy/find_test.go diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go index ff4212b7645..91fb7de0279 100644 --- a/refactor/satisfy/find.go +++ b/refactor/satisfy/find.go @@ -10,10 +10,8 @@ // // THIS PACKAGE IS EXPERIMENTAL AND MAY CHANGE AT ANY TIME. // -// It is provided only for the gorename tool. Ideally this -// functionality will become part of the type-checker in due course, -// since it is computing it anyway, and it is robust for ill-typed -// inputs, which this package is not. +// It is provided only for the gopls tool. It requires well-typed inputs. +// package satisfy // import "golang.org/x/tools/refactor/satisfy" // NOTES: @@ -25,9 +23,6 @@ package satisfy // import "golang.org/x/tools/refactor/satisfy" // ... // }}) // -// TODO(adonovan): make this robust against ill-typed input. -// Or move it into the type-checker. -// // Assignability conversions are possible in the following places: // - in assignments y = x, y := x, var y = x. // - from call argument types to formal parameter types @@ -51,11 +46,15 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typeparams" ) // A Constraint records the fact that the RHS type does and must // satisfy the LHS type, which is an interface. // The names are suggestive of an assignment statement LHS = RHS. +// +// The constraint is implicitly universally quantified over any type +// parameters appearing within the two types. type Constraint struct { LHS, RHS types.Type } @@ -129,13 +128,13 @@ func (f *Finder) exprN(e ast.Expr) types.Type { case *ast.CallExpr: // x, err := f(args) - sig := f.expr(e.Fun).Underlying().(*types.Signature) + sig := coreType(f.expr(e.Fun)).(*types.Signature) f.call(sig, e.Args) case *ast.IndexExpr: // y, ok := x[i] x := f.expr(e.X) - f.assign(f.expr(e.Index), x.Underlying().(*types.Map).Key()) + f.assign(f.expr(e.Index), coreType(x).(*types.Map).Key()) case *ast.TypeAssertExpr: // y, ok := x.(T) @@ -215,7 +214,7 @@ func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Ex f.expr(args[1]) } else { // append(x, y, z) - tElem := s.Underlying().(*types.Slice).Elem() + tElem := coreType(s).(*types.Slice).Elem() for _, arg := range args[1:] { f.assign(tElem, f.expr(arg)) } @@ -224,7 +223,7 @@ func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Ex case "delete": m := f.expr(args[0]) k := f.expr(args[1]) - f.assign(m.Underlying().(*types.Map).Key(), k) + f.assign(coreType(m).(*types.Map).Key(), k) default: // ordinary call @@ -358,6 +357,7 @@ func (f *Finder) expr(e ast.Expr) types.Type { f.sig = saved case *ast.CompositeLit: + // No need for coreType here: go1.18 disallows P{...} for type param P. switch T := deref(tv.Type).Underlying().(type) { case *types.Struct: for i, elem := range e.Elts { @@ -403,12 +403,20 @@ func (f *Finder) expr(e ast.Expr) types.Type { } case *ast.IndexExpr: - x := f.expr(e.X) - i := f.expr(e.Index) - if ux, ok := x.Underlying().(*types.Map); ok { - f.assign(ux.Key(), i) + if instance(f.info, e.X) { + // f[T] or C[T] -- generic instantiation + } else { + // x[i] or m[k] -- index or lookup operation + x := f.expr(e.X) + i := f.expr(e.Index) + if ux, ok := coreType(x).(*types.Map); ok { + f.assign(ux.Key(), i) + } } + case *typeparams.IndexListExpr: + // f[X, Y] -- generic instantiation + case *ast.SliceExpr: f.expr(e.X) if e.Low != nil { @@ -439,7 +447,7 @@ func (f *Finder) expr(e ast.Expr) types.Type { } } // ordinary call - f.call(f.expr(e.Fun).Underlying().(*types.Signature), e.Args) + f.call(coreType(f.expr(e.Fun)).(*types.Signature), e.Args) } case *ast.StarExpr: @@ -499,7 +507,7 @@ func (f *Finder) stmt(s ast.Stmt) { case *ast.SendStmt: ch := f.expr(s.Chan) val := f.expr(s.Value) - f.assign(ch.Underlying().(*types.Chan).Elem(), val) + f.assign(coreType(ch).(*types.Chan).Elem(), val) case *ast.IncDecStmt: f.expr(s.X) @@ -647,35 +655,35 @@ func (f *Finder) stmt(s ast.Stmt) { if s.Key != nil { k := f.expr(s.Key) var xelem types.Type - // keys of array, *array, slice, string aren't interesting - switch ux := x.Underlying().(type) { + // Keys of array, *array, slice, string aren't interesting + // since the RHS key type is just an int. + switch ux := coreType(x).(type) { case *types.Chan: xelem = ux.Elem() case *types.Map: xelem = ux.Key() } if xelem != nil { - f.assign(xelem, k) + f.assign(k, xelem) } } if s.Value != nil { val := f.expr(s.Value) var xelem types.Type - // values of strings aren't interesting - switch ux := x.Underlying().(type) { + // Values of type strings aren't interesting because + // the RHS value type is just a rune. + switch ux := coreType(x).(type) { case *types.Array: xelem = ux.Elem() - case *types.Chan: - xelem = ux.Elem() case *types.Map: xelem = ux.Elem() case *types.Pointer: // *array - xelem = deref(ux).(*types.Array).Elem() + xelem = coreType(deref(ux)).(*types.Array).Elem() case *types.Slice: xelem = ux.Elem() } if xelem != nil { - f.assign(xelem, val) + f.assign(val, xelem) } } } @@ -690,7 +698,7 @@ func (f *Finder) stmt(s ast.Stmt) { // deref returns a pointer's element type; otherwise it returns typ. func deref(typ types.Type) types.Type { - if p, ok := typ.Underlying().(*types.Pointer); ok { + if p, ok := coreType(typ).(*types.Pointer); ok { return p.Elem() } return typ @@ -699,3 +707,19 @@ func deref(typ types.Type) types.Type { func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } func isInterface(T types.Type) bool { return types.IsInterface(T) } + +func coreType(T types.Type) types.Type { return typeparams.CoreType(T) } + +func instance(info *types.Info, expr ast.Expr) bool { + var id *ast.Ident + switch x := expr.(type) { + case *ast.Ident: + id = x + case *ast.SelectorExpr: + id = x.Sel + default: + return false + } + _, ok := typeparams.GetInstances(info)[id] + return ok +} diff --git a/refactor/satisfy/find_test.go b/refactor/satisfy/find_test.go new file mode 100644 index 00000000000..234bce905d3 --- /dev/null +++ b/refactor/satisfy/find_test.go @@ -0,0 +1,226 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package satisfy_test + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "go/types" + "reflect" + "sort" + "testing" + + "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/refactor/satisfy" +) + +// This test exercises various operations on core types of type parameters. +// (It also provides pretty decent coverage of the non-generic operations.) +func TestGenericCoreOperations(t *testing.T) { + if !typeparams.Enabled { + t.Skip("!typeparams.Enabled") + } + + const src = `package foo + +type I interface { f() } + +type impl struct{} +func (impl) f() {} + +// A big pile of single-serving types that implement I. +type A struct{impl} +type B struct{impl} +type C struct{impl} +type D struct{impl} +type E struct{impl} +type F struct{impl} +type G struct{impl} +type H struct{impl} +type J struct{impl} +type K struct{impl} +type L struct{impl} +type M struct{impl} +type N struct{impl} +type O struct{impl} +type P struct{impl} +type Q struct{impl} +type R struct{impl} +type S struct{impl} +type T struct{impl} +type U struct{impl} + +type Generic[T any] struct{impl} +func (Generic[T]) g(T) {} + +type GI[T any] interface{ + g(T) +} + +func _[Slice interface{ []I }](s Slice) Slice { + s[0] = L{} // I <- L + return append(s, A{}) // I <- A +} + +func _[Func interface{ func(I) B }](fn Func) { + b := fn(C{}) // I <- C + var _ I = b // I <- B +} + +func _[Chan interface{ chan D }](ch Chan) { + var i I + for i = range ch {} // I <- D + _ = i +} + +func _[Chan interface{ chan E }](ch Chan) { + var _ I = <-ch // I <- E +} + +func _[Chan interface{ chan I }](ch Chan) { + ch <- F{} // I <- F +} + +func _[Map interface{ map[G]H }](m Map) { + var k, v I + for k, v = range m {} // I <- G, I <- H + _, _ = k, v +} + +func _[Map interface{ map[I]K }](m Map) { + var _ I = m[J{}] // I <- J, I <- K + delete(m, R{}) // I <- R + _, _ = m[J{}] +} + +func _[Array interface{ [1]I }](a Array) { + a[0] = M{} // I <- M +} + +func _[Array interface{ [1]N }](a Array) { + var _ I = a[0] // I <- N +} + +func _[Array interface{ [1]O }](a Array) { + var v I + for _, v = range a {} // I <- O + _ = v +} + +func _[ArrayPtr interface{ *[1]P }](a ArrayPtr) { + var v I + for _, v = range a {} // I <- P + _ = v +} + +func _[Slice interface{ []Q }](s Slice) { + var v I + for _, v = range s {} // I <- Q + _ = v +} + +func _[Func interface{ func() (S, bool) }](fn Func) { + var i I + i, _ = fn() // I <- S + _ = i +} + +func _() I { + var _ I = T{} // I <- T + var _ I = Generic[T]{} // I <- Generic[T] + var _ I = Generic[string]{} // I <- Generic[string] + return U{} // I <- U +} + +var _ GI[string] = Generic[string]{} // GI[string] <- Generic[string] + +// universally quantified constraints: +// the type parameter may appear on the left, the right, or both sides. + +func _[T any](g Generic[T]) GI[T] { + return g // GI[T] <- Generic[T] +} + +func _[T any]() { + type GI2[T any] interface{ g(string) } + var _ GI2[T] = Generic[string]{} // GI2[T] <- Generic[string] +} + +type Gen2[T any] struct{} +func (f Gen2[T]) g(string) { global = f } // GI[string] <- Gen2[T] + +var global GI[string] + +` + got := constraints(t, src) + want := []string{ + "p.GI2[T] <- p.Generic[string]", // implicitly "forall T" quantified + "p.GI[T] <- p.Generic[T]", // implicitly "forall T" quantified + "p.GI[string] <- p.Gen2[T]", // implicitly "forall T" quantified + "p.GI[string] <- p.Generic[string]", + "p.I <- p.A", + "p.I <- p.B", + "p.I <- p.C", + "p.I <- p.D", + "p.I <- p.E", + "p.I <- p.F", + "p.I <- p.G", + "p.I <- p.Generic[p.T]", + "p.I <- p.Generic[string]", + "p.I <- p.H", + "p.I <- p.J", + "p.I <- p.K", + "p.I <- p.L", + "p.I <- p.M", + "p.I <- p.N", + "p.I <- p.O", + "p.I <- p.P", + "p.I <- p.Q", + "p.I <- p.R", + "p.I <- p.S", + "p.I <- p.T", + "p.I <- p.U", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("found unexpected constraints: got %s, want %s", got, want) + } +} + +func constraints(t *testing.T, src string) []string { + // parse + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", src, 0) + if err != nil { + t.Fatal(err) // parse error + } + files := []*ast.File{f} + + // type-check + info := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Scopes: make(map[ast.Node]*types.Scope), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + } + typeparams.InitInstanceInfo(info) + conf := types.Config{} + if _, err := conf.Check("p", fset, files, info); err != nil { + t.Fatal(err) // type error + } + + // gather constraints + var finder satisfy.Finder + finder.Find(info, files) + var constraints []string + for c := range finder.Result { + constraints = append(constraints, fmt.Sprintf("%v <- %v", c.LHS, c.RHS)) + } + sort.Strings(constraints) + return constraints +} From 7404bd2ffdbcd390fb7b678d9a82da10fcbf48f9 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 27 Jun 2022 15:23:55 -0400 Subject: [PATCH 053/723] all: gofmt some recent file changes Change-Id: I62d2d35275964b35032e36d6ed3c9f4a31176f91 Reviewed-on: https://go-review.googlesource.com/c/tools/+/414495 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- internal/lsp/source/workspace_symbol.go | 1 - refactor/satisfy/find.go | 1 - 2 files changed, 2 deletions(-) diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go index c7cfe5c9ef8..0822de0810d 100644 --- a/internal/lsp/source/workspace_symbol.go +++ b/internal/lsp/source/workspace_symbol.go @@ -287,7 +287,6 @@ func (c comboMatcher) match(chunks []string) (int, float64) { // of zero indicates no match. // - A symbolizer determines how we extract the symbol for an object. This // enables the 'symbolStyle' configuration option. -// func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher, symbolizer symbolizer, query string) ([]protocol.SymbolInformation, error) { // Extract symbols from all files. diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go index 91fb7de0279..aacb56bce8b 100644 --- a/refactor/satisfy/find.go +++ b/refactor/satisfy/find.go @@ -11,7 +11,6 @@ // THIS PACKAGE IS EXPERIMENTAL AND MAY CHANGE AT ANY TIME. // // It is provided only for the gopls tool. It requires well-typed inputs. -// package satisfy // import "golang.org/x/tools/refactor/satisfy" // NOTES: From 2a900561e78a69afe5828ff8388aeb0dfc6220dc Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Tue, 28 Jun 2022 09:12:07 -0400 Subject: [PATCH 054/723] go/gcexportdata: fix Find for Go modules Find needs to invoke the go command to find the package export data. It cannot rely on GOPATH-based file location heuristics. This has the nice side effect of automatically compiling the code, removing the possibility of stale export data. Ideally Find would use go/packages, but go/packages imports this package for the export data parser (not for Find itself), so we have to make do with an explicit go command invocation. Marked both Find and NewImporter deprecated: using go/packages will be faster for nearly all use cases, because it can gather info about multiple packages in a single go command invocation. They are essentially unused anyway. Removed the file name print from the example because the file may be in the cache, in which case it will not be named "fmt.a". Change-Id: I7940c90e230b22df9dcbfc8103a69a2d18df3bb0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/310515 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Russ Cox Auto-Submit: Russ Cox --- go/gcexportdata/example_test.go | 2 -- go/gcexportdata/gcexportdata.go | 20 ++++++++++++++++++-- go/gcexportdata/importer.go | 3 +++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/go/gcexportdata/example_test.go b/go/gcexportdata/example_test.go index e81e705b1c4..cdd68f49c53 100644 --- a/go/gcexportdata/example_test.go +++ b/go/gcexportdata/example_test.go @@ -30,7 +30,6 @@ func ExampleRead() { log.Fatalf("can't find export data for fmt") } fmt.Printf("Package path: %s\n", path) - fmt.Printf("Export data: %s\n", filepath.Base(filename)) // Open and read the file. f, err := os.Open(filename) @@ -80,7 +79,6 @@ func ExampleRead() { // Output: // // Package path: fmt - // Export data: fmt.a // Package members: Println found // Println type: func(a ...any) (n int, err error) // Println location: $GOROOT/src/fmt/print.go:123:1 diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go index d50826dbf7e..ddc276cfbcb 100644 --- a/go/gcexportdata/gcexportdata.go +++ b/go/gcexportdata/gcexportdata.go @@ -22,26 +22,42 @@ package gcexportdata // import "golang.org/x/tools/go/gcexportdata" import ( "bufio" "bytes" + "encoding/json" "fmt" "go/token" "go/types" "io" "io/ioutil" + "os/exec" "golang.org/x/tools/go/internal/gcimporter" ) // Find returns the name of an object (.o) or archive (.a) file // containing type information for the specified import path, -// using the workspace layout conventions of go/build. +// using the go command. // If no file was found, an empty filename is returned. // // A relative srcDir is interpreted relative to the current working directory. // // Find also returns the package's resolved (canonical) import path, // reflecting the effects of srcDir and vendoring on importPath. +// +// Deprecated: Use the higher-level API in golang.org/x/tools/go/packages, +// which is more efficient. func Find(importPath, srcDir string) (filename, path string) { - return gcimporter.FindPkg(importPath, srcDir) + cmd := exec.Command("go", "list", "-json", "-export", "--", importPath) + cmd.Dir = srcDir + out, err := cmd.CombinedOutput() + if err != nil { + return "", "" + } + var data struct { + ImportPath string + Export string + } + json.Unmarshal(out, &data) + return data.Export, data.ImportPath } // NewReader returns a reader for the export data section of an object diff --git a/go/gcexportdata/importer.go b/go/gcexportdata/importer.go index fe6ed93215c..37a7247e268 100644 --- a/go/gcexportdata/importer.go +++ b/go/gcexportdata/importer.go @@ -22,6 +22,9 @@ import ( // version-skew problems described in the documentation of this package, // or to control the FileSet or access the imports map populated during // package loading. +// +// Deprecated: Use the higher-level API in golang.org/x/tools/go/packages, +// which is more efficient. func NewImporter(fset *token.FileSet, imports map[string]*types.Package) types.ImporterFrom { return importer{fset, imports} } From e5b33249972ac93113ee6a34ee08f8096d06271c Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 14 Jun 2022 15:18:07 -0400 Subject: [PATCH 055/723] internal/lsp: add InlayHint regtests Add regtests for inlay hints to verify we can turn on and off different inlay hints. Change-Id: Id88450c40c048b6c2544d22a0d3eadb57b70a723 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411911 Reviewed-by: Robert Findley Reviewed-by: Jamal Carvalho gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Suzy Mueller --- .../regtest/inlayHints/inlayHints_test.go | 72 +++++++++++++++++++ internal/lsp/fake/editor.go | 21 ++++++ internal/lsp/regtest/wrappers.go | 11 +++ 3 files changed, 104 insertions(+) create mode 100644 gopls/internal/regtest/inlayHints/inlayHints_test.go diff --git a/gopls/internal/regtest/inlayHints/inlayHints_test.go b/gopls/internal/regtest/inlayHints/inlayHints_test.go new file mode 100644 index 00000000000..67931fbdc83 --- /dev/null +++ b/gopls/internal/regtest/inlayHints/inlayHints_test.go @@ -0,0 +1,72 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package inlayHint + +import ( + "testing" + + "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/internal/lsp/bug" + . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/testenv" +) + +func TestMain(m *testing.M) { + bug.PanicOnBugs = true + Main(m, hooks.Options) +} +func TestEnablingInlayHints(t *testing.T) { + testenv.NeedsGo1Point(t, 14) // Test fails on 1.13. + const workspace = ` +-- go.mod -- +module inlayHint.test +go 1.12 +-- lib.go -- +package lib +type Number int +const ( + Zero Number = iota + One + Two +) +` + tests := []struct { + label string + enabled map[string]bool + wantInlayHint bool + }{ + { + label: "default", + wantInlayHint: false, + }, + { + label: "enable const", + enabled: map[string]bool{source.ConstantValues: true}, + wantInlayHint: true, + }, + { + label: "enable parameter names", + enabled: map[string]bool{source.ParameterNames: true}, + wantInlayHint: false, + }, + } + for _, test := range tests { + t.Run(test.label, func(t *testing.T) { + WithOptions( + EditorConfig{ + Settings: map[string]interface{}{ + "hints": test.enabled, + }, + }, + ).Run(t, workspace, func(t *testing.T, env *Env) { + env.OpenFile("lib.go") + lens := env.InlayHints("lib.go") + if gotInlayHint := len(lens) > 0; gotInlayHint != test.wantInlayHint { + t.Errorf("got inlayHint: %t, want %t", gotInlayHint, test.wantInlayHint) + } + }) + }) + } +} diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index 06b90bb84e5..0fc99a04982 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -1114,6 +1114,27 @@ func (e *Editor) Symbols(ctx context.Context, sym string) ([]protocol.SymbolInfo return ans, err } +// CodeLens executes a codelens request on the server. +func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHint, error) { + if e.Server == nil { + return nil, nil + } + e.mu.Lock() + _, ok := e.buffers[path] + e.mu.Unlock() + if !ok { + return nil, fmt.Errorf("buffer %q is not open", path) + } + params := &protocol.InlayHintParams{ + TextDocument: e.textDocumentIdentifier(path), + } + hints, err := e.Server.InlayHint(ctx, params) + if err != nil { + return nil, err + } + return hints, nil +} + // References executes a reference request on the server. func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) { if e.Server == nil { diff --git a/internal/lsp/regtest/wrappers.go b/internal/lsp/regtest/wrappers.go index 9031e71f1f1..96e2de96271 100644 --- a/internal/lsp/regtest/wrappers.go +++ b/internal/lsp/regtest/wrappers.go @@ -358,6 +358,17 @@ func (e *Env) ExecuteCommand(params *protocol.ExecuteCommandParams, result inter } } +// InlayHints calls textDocument/inlayHints for the given path, calling t.Fatal on +// any error. +func (e *Env) InlayHints(path string) []protocol.InlayHint { + e.T.Helper() + hints, err := e.Editor.InlayHint(e.Ctx, path) + if err != nil { + e.T.Fatal(err) + } + return hints +} + // WorkspaceSymbol calls workspace/symbol func (e *Env) WorkspaceSymbol(sym string) []protocol.SymbolInformation { e.T.Helper() From 0248714391a4231dea84e35fc04f8b65a609821e Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 28 Jun 2022 14:50:14 -0400 Subject: [PATCH 056/723] internal/lsp: add additional instrumentation around package loading Add some additional logging to help debug golang/go#53586 For golang/go#53586 Change-Id: I0574fb01be47b265cd6e412855794bc2cb836dff Reviewed-on: https://go-review.googlesource.com/c/tools/+/414854 gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Auto-Submit: Robert Findley --- internal/lsp/cache/load.go | 14 +++++++++++--- internal/lsp/source/completion/completion.go | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index db9a06d4dee..da0b246c54f 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -15,6 +15,7 @@ import ( "path/filepath" "sort" "strings" + "sync/atomic" "time" "golang.org/x/tools/go/packages" @@ -28,12 +29,17 @@ import ( "golang.org/x/tools/internal/span" ) +var loadID uint64 // atomic identifier for loads + // load calls packages.Load for the given scopes, updating package metadata, // import graph, and mapped files with the result. // // The resulting error may wrap the moduleErrorMap error type, representing // errors associated with specific modules. func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) (err error) { + id := atomic.AddUint64(&loadID, 1) + eventName := fmt.Sprintf("go/packages.Load #%d", id) // unique name for logging + var query []string var containsDir bool // for logging @@ -138,9 +144,9 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf return ctx.Err() } if err != nil { - event.Error(ctx, "go/packages.Load", err, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) + event.Error(ctx, eventName, err, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) } else { - event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) + event.Log(ctx, eventName, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) } if len(pkgs) == 0 { if err == nil { @@ -168,7 +174,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } if !containsDir || s.view.Options().VerboseOutput { - event.Log(ctx, "go/packages.Load", + event.Log(ctx, eventName, tag.Snapshot.Of(s.ID()), tag.Package.Of(pkg.ID), tag.Files.Of(pkg.CompiledGoFiles)) @@ -209,6 +215,8 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf loadedIDs = append(loadedIDs, id) } + event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates))) + s.mu.Lock() // invalidate the reverse transitive closure of packages that have changed. diff --git a/internal/lsp/source/completion/completion.go b/internal/lsp/source/completion/completion.go index bb1c68d2238..0c1ff3f21b0 100644 --- a/internal/lsp/source/completion/completion.go +++ b/internal/lsp/source/completion/completion.go @@ -441,7 +441,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan items, surrounding, innerErr := packageClauseCompletions(ctx, snapshot, fh, protoPos) if innerErr != nil { // return the error for GetParsedFile since it's more relevant in this situation. - return nil, nil, fmt.Errorf("getting file for Completion: %w (package completions: %v)", err, innerErr) + return nil, nil, fmt.Errorf("getting file %s for Completion: %w (package completions: %v)", fh.URI(), err, innerErr) } return items, surrounding, nil } From 7743d1d949f1006ad12b190d68996acafc84d1d6 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 15 Jun 2022 15:43:22 -0400 Subject: [PATCH 057/723] internal/lsp: respect range for inlay hints This is an optimization to avoid calculating inlayhints that are not in the requested range. Change-Id: I311f297d2998ae7d0db822eac540b1c12cae6e23 Reviewed-on: https://go-review.googlesource.com/c/tools/+/412455 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Suzy Mueller Reviewed-by: Jamal Carvalho --- internal/lsp/source/inlay_hint.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 0c147283532..009cc52fdd5 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -104,7 +104,7 @@ var AllInlayHints = map[string]*Hint{ }, } -func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol.Range) ([]protocol.InlayHint, error) { +func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.InlayHint, error) { ctx, done := event.Start(ctx, "source.InlayHint") defer done() @@ -132,8 +132,23 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol info := pkg.GetTypesInfo() q := Qualifier(pgf.File, pkg.GetTypes(), info) + // Set the range to the full file if the range is not valid. + start, end := pgf.File.Pos(), pgf.File.End() + if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character { + // Adjust start and end for the specified range. + rng, err := pgf.Mapper.RangeToSpanRange(pRng) + if err != nil { + return nil, err + } + start, end = rng.Start, rng.End + } + var hints []protocol.InlayHint ast.Inspect(pgf.File, func(node ast.Node) bool { + // If not in range, we can stop looking. + if node == nil || node.End() < start || node.Pos() > end { + return false + } for _, fn := range enabledHints { hints = append(hints, fn(node, tmap, info, &q)...) } From c10541a14b3e5db8a9ae9fad7640035745288ab3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 29 Jun 2022 11:50:37 -0400 Subject: [PATCH 058/723] go/analysis/passes/fieldalignment: document "false sharing" ...and don't claim that the most compact field order is optimal. The exception is relatively obscure, but the fact that it exists is important because it means it is not safe to apply the code transformation unconditionally. Change-Id: I391fbc1872b578d5340dd7c8fded48be30b820e0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415057 Reviewed-by: Robert Findley --- go/analysis/passes/fieldalignment/fieldalignment.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/go/analysis/passes/fieldalignment/fieldalignment.go b/go/analysis/passes/fieldalignment/fieldalignment.go index 78afe94ab30..aff663046a3 100644 --- a/go/analysis/passes/fieldalignment/fieldalignment.go +++ b/go/analysis/passes/fieldalignment/fieldalignment.go @@ -23,7 +23,7 @@ import ( const Doc = `find structs that would use less memory if their fields were sorted This analyzer find structs that can be rearranged to use less memory, and provides -a suggested edit with the optimal order. +a suggested edit with the most compact order. Note that there are two different diagnostics reported. One checks struct size, and the other reports "pointer bytes" used. Pointer bytes is how many bytes of the @@ -41,6 +41,11 @@ has 24 pointer bytes because it has to scan further through the *uint32. struct { string; uint32 } has 8 because it can stop immediately after the string pointer. + +Be aware that the most compact order is not always the most efficient. +In rare cases it may cause two variables each updated by its own goroutine +to occupy the same CPU cache line, inducing a form of memory contention +known as "false sharing" that slows down both goroutines. ` var Analyzer = &analysis.Analyzer{ From b84d509d6ffee06a6a8d82fd218009fc7f548e76 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 29 Jun 2022 13:02:25 -0400 Subject: [PATCH 059/723] gopls/doc: regenerate documentation This change should have been included in https://go-review.googlesource.com/c/tools/+/415057 but I hastily submitted it without a CI run thinking "how can a doc only change break something?". Well now I know. Sorry. :( Change-Id: Ib0fd25fddd7f9580961b44dcad032d4851684f63 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415058 Reviewed-by: Dmitri Shuralyov Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Reviewed-by: Bryan Mills Auto-Submit: Bryan Mills --- gopls/doc/analyzers.md | 7 ++++++- internal/lsp/source/api_json.go | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index f5c83d5771d..fd65c3a2a9d 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -131,7 +131,7 @@ of the second argument is not a pointer to a type implementing error. find structs that would use less memory if their fields were sorted This analyzer find structs that can be rearranged to use less memory, and provides -a suggested edit with the optimal order. +a suggested edit with the most compact order. Note that there are two different diagnostics reported. One checks struct size, and the other reports "pointer bytes" used. Pointer bytes is how many bytes of the @@ -150,6 +150,11 @@ has 24 pointer bytes because it has to scan further through the *uint32. has 8 because it can stop immediately after the string pointer. +Be aware that the most compact order is not always the most efficient. +In rare cases it may cause two variables each updated by its own goroutine +to occupy the same CPU cache line, inducing a form of memory contention +known as "false sharing" that slows down both goroutines. + **Disabled by default. Enable it by setting `"analyses": {"fieldalignment": true}`.** diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index ef683f31a01..4e2183cf4e6 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -280,7 +280,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "\"fieldalignment\"", - Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the optimal order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n", + Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the most compact order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n\nBe aware that the most compact order is not always the most efficient.\nIn rare cases it may cause two variables each updated by its own goroutine\nto occupy the same CPU cache line, inducing a form of memory contention\nknown as \"false sharing\" that slows down both goroutines.\n", Default: "false", }, { @@ -866,7 +866,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "fieldalignment", - Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the optimal order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n", + Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the most compact order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n\nBe aware that the most compact order is not always the most efficient.\nIn rare cases it may cause two variables each updated by its own goroutine\nto occupy the same CPU cache line, inducing a form of memory contention\nknown as \"false sharing\" that slows down both goroutines.\n", }, { Name: "httpresponse", From 1a196f04970c04bea7d88492e2618c03b60d4789 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 30 Jun 2022 10:50:45 -0400 Subject: [PATCH 060/723] internal/lsp/cache: don't build symbol info for non-Go files Our symbol query only searches Go files, so there is no point to building (broken) symbol information for non-Go files. Doing so introduces a lot more symbol handles that need to be tracked and walked. Change-Id: I96dd62766d079805fcb1d16eb361adfc0c31eea1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415199 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- internal/lsp/cache/snapshot.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 8194750b331..e94ad7ba09c 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1010,6 +1010,10 @@ func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { result = make(map[span.URI][]source.Symbol) ) s.files.Range(func(uri span.URI, f source.VersionedFileHandle) { + if s.View().FileKind(f) != source.Go { + return // workspace symbols currently supports only Go files. + } + // TODO(adonovan): upgrade errgroup and use group.SetLimit(nprocs). iolimit <- struct{}{} // acquire token group.Go(func() error { From 8865782bc0d76401a05a438cccb05486ca1e5c62 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 28 Jun 2022 15:44:45 -0400 Subject: [PATCH 061/723] internal/lsp: add text edits for unkeyed literals Add text edits that a user can accept to make the unkeyed composite literals keyed from the inlay hints. The text edits modify all of the unkeyed fields in a composite literal, since a mixture of keyed and unkeyed fields are not allowed. Change-Id: I0683fbaa5e22bc004b91c98fc09e495e797826ee Reviewed-on: https://go-review.googlesource.com/c/tools/+/414855 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Jamal Carvalho Run-TryBot: Suzy Mueller --- internal/lsp/protocol/tsprotocol.go | 8 ++++++++ internal/lsp/source/inlay_hint.go | 11 ++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/lsp/protocol/tsprotocol.go b/internal/lsp/protocol/tsprotocol.go index 5dd3d09e188..3a284bf45a6 100644 --- a/internal/lsp/protocol/tsprotocol.go +++ b/internal/lsp/protocol/tsprotocol.go @@ -2744,6 +2744,14 @@ type InlayHint = struct { * should fall back to a reasonable default. */ Kind InlayHintKind `json:"kind,omitempty"` + /** + * Optional text edits that are performed when accepting this inlay hint. + * + * *Note* that edits are expected to change the document so that the inlay + * hint (or its nearest variant) is now part of the document and the inlay + * hint itself is now obsolete. + */ + TextEdits []TextEdit `json:"textEdits,omitempty"` /** * The tooltip text when you hover over this item. */ diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 009cc52fdd5..967752b5c51 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -344,7 +344,7 @@ func compositeLiteralFields(node ast.Node, tmap *lsppos.TokenMapper, info *types } var hints []protocol.InlayHint - + var allEdits []protocol.TextEdit for i, v := range compLit.Elts { if _, ok := v.(*ast.KeyValueExpr); !ok { start, ok := tmap.Position(v.Pos()) @@ -360,8 +360,17 @@ func compositeLiteralFields(node ast.Node, tmap *lsppos.TokenMapper, info *types Kind: protocol.Parameter, PaddingRight: true, }) + allEdits = append(allEdits, protocol.TextEdit{ + Range: protocol.Range{Start: start, End: start}, + NewText: strct.Field(i).Name() + ": ", + }) } } + // It is not allowed to have a mix of keyed and unkeyed fields, so + // have the text edits add keys to all fields. + for i := range hints { + hints[i].TextEdits = allEdits + } return hints } From 8314b7aa0d97fe43e050787d55308e25f265bc63 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Mon, 27 Jun 2022 18:15:05 -0400 Subject: [PATCH 062/723] go/analysis: add suggested fix for unkeyed composite literals Include a suggested fix with the diagnostic for unkeyed composite literals. This suggested fix will add the name of each of the fields. For golang/go#53062 Change-Id: I0c33191ff3cf66c95a9a055848274cc2b0c38224 Reviewed-on: https://go-review.googlesource.com/c/tools/+/414674 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Suzy Mueller --- go/analysis/passes/composite/composite.go | 41 ++++- .../passes/composite/composite_test.go | 2 +- .../passes/composite/testdata/src/a/a.go | 17 +++ .../composite/testdata/src/a/a.go.golden | 144 ++++++++++++++++++ .../testdata/src/a/a_fuzz_test.go.golden | 16 ++ .../testdata/src/typeparams/typeparams.go | 10 +- .../src/typeparams/typeparams.go.golden | 27 ++++ 7 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 go/analysis/passes/composite/testdata/src/a/a.go.golden create mode 100644 go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden create mode 100644 go/analysis/passes/composite/testdata/src/typeparams/typeparams.go.golden diff --git a/go/analysis/passes/composite/composite.go b/go/analysis/passes/composite/composite.go index d3670aca97a..64e184d3439 100644 --- a/go/analysis/passes/composite/composite.go +++ b/go/analysis/passes/composite/composite.go @@ -7,6 +7,7 @@ package composite import ( + "fmt" "go/ast" "go/types" "strings" @@ -83,7 +84,8 @@ func run(pass *analysis.Pass) (interface{}, error) { } for _, typ := range structuralTypes { under := deref(typ.Underlying()) - if _, ok := under.(*types.Struct); !ok { + strct, ok := under.(*types.Struct) + if !ok { // skip non-struct composite literals continue } @@ -92,20 +94,47 @@ func run(pass *analysis.Pass) (interface{}, error) { continue } - // check if the CompositeLit contains an unkeyed field + // check if the struct contains an unkeyed field allKeyValue := true - for _, e := range cl.Elts { + var suggestedFixAvailable = len(cl.Elts) == strct.NumFields() + var missingKeys []analysis.TextEdit + for i, e := range cl.Elts { if _, ok := e.(*ast.KeyValueExpr); !ok { allKeyValue = false - break + if i >= strct.NumFields() { + break + } + field := strct.Field(i) + if !field.Exported() { + // Adding unexported field names for structs not defined + // locally will not work. + suggestedFixAvailable = false + break + } + missingKeys = append(missingKeys, analysis.TextEdit{ + Pos: e.Pos(), + End: e.Pos(), + NewText: []byte(fmt.Sprintf("%s: ", field.Name())), + }) } } if allKeyValue { - // all the composite literal fields are keyed + // all the struct fields are keyed continue } - pass.ReportRangef(cl, "%s composite literal uses unkeyed fields", typeName) + diag := analysis.Diagnostic{ + Pos: cl.Pos(), + End: cl.End(), + Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName), + } + if suggestedFixAvailable { + diag.SuggestedFixes = []analysis.SuggestedFix{{ + Message: "Add field names to struct literal", + TextEdits: missingKeys, + }} + } + pass.Report(diag) return } }) diff --git a/go/analysis/passes/composite/composite_test.go b/go/analysis/passes/composite/composite_test.go index 952de8bfdad..7afaaa7ffd4 100644 --- a/go/analysis/passes/composite/composite_test.go +++ b/go/analysis/passes/composite/composite_test.go @@ -18,5 +18,5 @@ func Test(t *testing.T) { if typeparams.Enabled { pkgs = append(pkgs, "typeparams") } - analysistest.Run(t, testdata, composite.Analyzer, pkgs...) + analysistest.RunWithSuggestedFixes(t, testdata, composite.Analyzer, pkgs...) } diff --git a/go/analysis/passes/composite/testdata/src/a/a.go b/go/analysis/passes/composite/testdata/src/a/a.go index 3a5bc203b03..cd69d395173 100644 --- a/go/analysis/passes/composite/testdata/src/a/a.go +++ b/go/analysis/passes/composite/testdata/src/a/a.go @@ -11,6 +11,7 @@ import ( "go/scanner" "go/token" "image" + "sync" "unicode" ) @@ -79,6 +80,18 @@ var badStructLiteral = flag.Flag{ // want "unkeyed fields" nil, // Value "DefValue", } +var tooManyFieldsStructLiteral = flag.Flag{ // want "unkeyed fields" + "Name", + "Usage", + nil, // Value + "DefValue", + "Extra Field", +} +var tooFewFieldsStructLiteral = flag.Flag{ // want "unkeyed fields" + "Name", + "Usage", + nil, // Value +} var delta [3]rune @@ -100,6 +113,10 @@ var badScannerErrorList = scanner.ErrorList{ &scanner.Error{token.Position{}, "foobar"}, // want "unkeyed fields" } +// sync.Mutex has unexported fields. We expect a diagnostic but no +// suggested fix. +var mu = sync.Mutex{0, 0} // want "unkeyed fields" + // Check whitelisted structs: if vet is run with --compositewhitelist=false, // this line triggers an error. var whitelistedPoint = image.Point{1, 2} diff --git a/go/analysis/passes/composite/testdata/src/a/a.go.golden b/go/analysis/passes/composite/testdata/src/a/a.go.golden new file mode 100644 index 00000000000..fe73a2e0a1d --- /dev/null +++ b/go/analysis/passes/composite/testdata/src/a/a.go.golden @@ -0,0 +1,144 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains the test for untagged struct literals. + +package a + +import ( + "flag" + "go/scanner" + "go/token" + "image" + "sync" + "unicode" +) + +var Okay1 = []string{ + "Name", + "Usage", + "DefValue", +} + +var Okay2 = map[string]bool{ + "Name": true, + "Usage": true, + "DefValue": true, +} + +var Okay3 = struct { + X string + Y string + Z string +}{ + "Name", + "Usage", + "DefValue", +} + +var Okay4 = []struct { + A int + B int +}{ + {1, 2}, + {3, 4}, +} + +type MyStruct struct { + X string + Y string + Z string +} + +var Okay5 = &MyStruct{ + "Name", + "Usage", + "DefValue", +} + +var Okay6 = []MyStruct{ + {"foo", "bar", "baz"}, + {"aa", "bb", "cc"}, +} + +var Okay7 = []*MyStruct{ + {"foo", "bar", "baz"}, + {"aa", "bb", "cc"}, +} + +// Testing is awkward because we need to reference things from a separate package +// to trigger the warnings. + +var goodStructLiteral = flag.Flag{ + Name: "Name", + Usage: "Usage", +} +var badStructLiteral = flag.Flag{ // want "unkeyed fields" + Name: "Name", + Usage: "Usage", + Value: nil, // Value + DefValue: "DefValue", +} +var tooManyFieldsStructLiteral = flag.Flag{ // want "unkeyed fields" + "Name", + "Usage", + nil, // Value + "DefValue", + "Extra Field", +} +var tooFewFieldsStructLiteral = flag.Flag{ // want "unkeyed fields" + "Name", + "Usage", + nil, // Value +} + +var delta [3]rune + +// SpecialCase is a named slice of CaseRange to test issue 9171. +var goodNamedSliceLiteral = unicode.SpecialCase{ + {Lo: 1, Hi: 2, Delta: delta}, + unicode.CaseRange{Lo: 1, Hi: 2, Delta: delta}, +} +var badNamedSliceLiteral = unicode.SpecialCase{ + {Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields" + unicode.CaseRange{Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields" +} + +// ErrorList is a named slice, so no warnings should be emitted. +var goodScannerErrorList = scanner.ErrorList{ + &scanner.Error{Msg: "foobar"}, +} +var badScannerErrorList = scanner.ErrorList{ + &scanner.Error{Pos: token.Position{}, Msg: "foobar"}, // want "unkeyed fields" +} + +// sync.Mutex has unexported fields. We expect a diagnostic but no +// suggested fix. +var mu = sync.Mutex{0, 0} // want "unkeyed fields" + +// Check whitelisted structs: if vet is run with --compositewhitelist=false, +// this line triggers an error. +var whitelistedPoint = image.Point{1, 2} + +// Do not check type from unknown package. +// See issue 15408. +var unknownPkgVar = unicode.NoSuchType{"foo", "bar"} + +// A named pointer slice of CaseRange to test issue 23539. In +// particular, we're interested in how some slice elements omit their +// type. +var goodNamedPointerSliceLiteral = []*unicode.CaseRange{ + {Lo: 1, Hi: 2}, + &unicode.CaseRange{Lo: 1, Hi: 2}, +} +var badNamedPointerSliceLiteral = []*unicode.CaseRange{ + {Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields" + &unicode.CaseRange{Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields" +} + +// unicode.Range16 is whitelisted, so there'll be no vet error +var range16 = unicode.Range16{0xfdd0, 0xfdef, 1} + +// unicode.Range32 is whitelisted, so there'll be no vet error +var range32 = unicode.Range32{0x1fffe, 0x1ffff, 1} diff --git a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden new file mode 100644 index 00000000000..20b652e88dd --- /dev/null +++ b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden @@ -0,0 +1,16 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package a + +import "testing" + +var fuzzTargets = []testing.InternalFuzzTarget{ + {"Fuzz", Fuzz}, +} + +func Fuzz(f *testing.F) {} diff --git a/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go index dd5d57efed4..f9a5e1fb105 100644 --- a/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go @@ -6,7 +6,7 @@ package typeparams import "typeparams/lib" -type localStruct struct { F int } +type localStruct struct{ F int } func F[ T1 ~struct{ f int }, @@ -20,8 +20,8 @@ func F[ _ = T1{2} _ = T2a{2} _ = T2b{2} // want "unkeyed fields" - _ = T3{1,2} - _ = T4{1,2} - _ = T5{1:2} - _ = T6{1:2} + _ = T3{1, 2} + _ = T4{1, 2} + _ = T5{1: 2} + _ = T6{1: 2} } diff --git a/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go.golden b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go.golden new file mode 100644 index 00000000000..66cd9158cb6 --- /dev/null +++ b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go.golden @@ -0,0 +1,27 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package typeparams + +import "typeparams/lib" + +type localStruct struct{ F int } + +func F[ + T1 ~struct{ f int }, + T2a localStruct, + T2b lib.Struct, + T3 ~[]int, + T4 lib.Slice, + T5 ~map[int]int, + T6 lib.Map, +]() { + _ = T1{2} + _ = T2a{2} + _ = T2b{F: 2} // want "unkeyed fields" + _ = T3{1, 2} + _ = T4{1, 2} + _ = T5{1: 2} + _ = T6{1: 2} +} From e8e5b37084ab41357340419ff15cba5fb08af935 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 30 Jun 2022 14:01:50 -0400 Subject: [PATCH 063/723] internal/lsp/cache: don't construct a new metadata graph if no changes Change-Id: I3f074d1fd29cf7ad0323cec76154f9b2e31f7356 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415494 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- internal/lsp/cache/graph.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go index dc7d4faef78..ad39aa8d862 100644 --- a/internal/lsp/cache/graph.go +++ b/internal/lsp/cache/graph.go @@ -32,6 +32,10 @@ type metadataGraph struct { // Clone creates a new metadataGraph, applying the given updates to the // receiver. func (g *metadataGraph) Clone(updates map[PackageID]*KnownMetadata) *metadataGraph { + if len(updates) == 0 { + // Optimization: since the graph is immutable, we can return the receiver. + return g + } result := &metadataGraph{metadata: make(map[PackageID]*KnownMetadata, len(g.metadata))} // Copy metadata. for id, m := range g.metadata { From c77473fa95f1277e5bf9708de95d29e1ff350d34 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 30 Jun 2022 15:13:28 -0400 Subject: [PATCH 064/723] gopls: upgrade staticcheck to v0.3.2 Selectively upgrade only staticcheck, to pick up fixes for support to generic code. Change-Id: Ia625c4d46780139aa6e70447eebe1b6d476d4722 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415495 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley --- gopls/go.mod | 2 +- gopls/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gopls/go.mod b/gopls/go.mod index 5dc62d3df0a..bd118e226bb 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -11,7 +11,7 @@ require ( golang.org/x/sys v0.0.0-20220209214540-3681064d5158 golang.org/x/tools v0.1.11-0.20220523181440-ccb10502d1a5 golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c - honnef.co/go/tools v0.3.0 + honnef.co/go/tools v0.3.2 mvdan.cc/gofumpt v0.3.0 mvdan.cc/xurls/v2 v2.4.0 ) diff --git a/gopls/go.sum b/gopls/go.sum index 91f552ef905..73a55fbad82 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -89,6 +89,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.3.0 h1:2LdYUZ7CIxnYgskbUZfY7FPggmqnh6shBqfWa8Tn3XU= honnef.co/go/tools v0.3.0/go.mod h1:vlRD9XErLMGT+mDuofSr0mMMquscM/1nQqtRSsh6m70= +honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= +honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= mvdan.cc/gofumpt v0.3.0 h1:kTojdZo9AcEYbQYhGuLf/zszYthRdhDNDUi2JKTxas4= mvdan.cc/gofumpt v0.3.0/go.mod h1:0+VyGZWleeIj5oostkOex+nDBA0eyavuDnDusAJ8ylo= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio= From 9358addbaa72dd292045f8ba280aba316ca8eff1 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 28 Jun 2022 20:25:04 -0400 Subject: [PATCH 065/723] internal/lsp/cache: remove unused function This function was actually left behind, after making suggested changes to CL 413683. Change-Id: I6933e870ded9da5af06724c28839c37d58fb4cdc Reviewed-on: https://go-review.googlesource.com/c/tools/+/414856 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley --- internal/lsp/cache/graph.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go index ad39aa8d862..88c9f147195 100644 --- a/internal/lsp/cache/graph.go +++ b/internal/lsp/cache/graph.go @@ -158,18 +158,3 @@ func (g *metadataGraph) reverseTransitiveClosure(includeInvalid bool, ids ...Pac visitAll(ids) return seen } - -func collectReverseTransitiveClosure(g *metadataGraph, includeInvalid bool, ids []PackageID, seen map[PackageID]struct{}) { - for _, id := range ids { - if _, ok := seen[id]; ok { - continue - } - m := g.metadata[id] - // Only use invalid metadata if we support it. - if m == nil || !(m.Valid || includeInvalid) { - continue - } - seen[id] = struct{}{} - collectReverseTransitiveClosure(g, includeInvalid, g.importedBy[id], seen) - } -} From fa4babcd9abca1cbd669c8dd725f770ba4f75800 Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Sun, 12 Jun 2022 04:22:04 +0000 Subject: [PATCH 066/723] internal/lsp/cache: use persistent map for storing packages in the snapshot This on average reduces latency from 25ms to 12ms on internal codebase. Updates golang/go#45686 Change-Id: I49c8f09f8e54b7b486d7ff7eb8f4ba9f0d90b278 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413655 gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Reviewed-by: Alan Donovan --- internal/lsp/cache/check.go | 6 ++-- internal/lsp/cache/load.go | 4 +-- internal/lsp/cache/maps.go | 51 ++++++++++++++++++++++++++++++++++ internal/lsp/cache/session.go | 2 +- internal/lsp/cache/snapshot.go | 34 ++++++++++++----------- internal/lsp/source/view.go | 4 +++ 6 files changed, 79 insertions(+), 22 deletions(-) diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index f09fc298a98..797298a4192 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -108,7 +108,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so m := ph.m key := ph.key - h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { + h, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { snapshot := arg.(*snapshot) // Begin loading the direct dependencies, in parallel. @@ -128,14 +128,14 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so wg.Wait() return data - }, nil) + }) ph.handle = h // Cache the handle in the snapshot. If a package handle has already // been cached, addPackage will return the cached value. This is fine, // since the original package handle above will have no references and be // garbage collected. - ph = s.addPackageHandle(ph) + ph = s.addPackageHandle(ph, release) return ph, nil } diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index da0b246c54f..3fb67a7a98b 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -228,9 +228,9 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf // TODO(rfindley): this should not be necessary, as we should have already // invalidated in snapshot.clone. for id := range invalidatedPackages { - for _, mode := range []source.ParseMode{source.ParseHeader, source.ParseExported, source.ParseFull} { + for _, mode := range source.AllParseModes { key := packageKey{mode, id} - delete(s.packages, key) + s.packages.Delete(key) } } diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go index 91b0e77e87e..14026abd92b 100644 --- a/internal/lsp/cache/maps.go +++ b/internal/lsp/cache/maps.go @@ -155,3 +155,54 @@ func (m parseKeysByURIMap) Set(key span.URI, value []parseKey) { func (m parseKeysByURIMap) Delete(key span.URI) { m.impl.Delete(key) } + +type packagesMap struct { + impl *persistent.Map +} + +func newPackagesMap() packagesMap { + return packagesMap{ + impl: persistent.NewMap(func(a, b interface{}) bool { + left := a.(packageKey) + right := b.(packageKey) + if left.mode != right.mode { + return left.mode < right.mode + } + return left.id < right.id + }), + } +} + +func (m packagesMap) Clone() packagesMap { + return packagesMap{ + impl: m.impl.Clone(), + } +} + +func (m packagesMap) Destroy() { + m.impl.Destroy() +} + +func (m packagesMap) Get(key packageKey) (*packageHandle, bool) { + value, ok := m.impl.Get(key) + if !ok { + return nil, false + } + return value.(*packageHandle), true +} + +func (m packagesMap) Range(do func(key packageKey, value *packageHandle)) { + m.impl.Range(func(key, value interface{}) { + do(key.(packageKey), value.(*packageHandle)) + }) +} + +func (m packagesMap) Set(key packageKey, value *packageHandle, release func()) { + m.impl.Set(key, value, func(key, value interface{}) { + release() + }) +} + +func (m packagesMap) Delete(key packageKey) { + m.impl.Delete(key) +} diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 4a7a5b2f4a6..8d8e63f13e8 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -231,7 +231,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, cancel: cancel, initializeOnce: &sync.Once{}, generation: s.cache.store.Generation(generationName(v, 0)), - packages: make(map[packageKey]*packageHandle), + packages: newPackagesMap(), meta: &metadataGraph{}, files: newFilesMap(), goFiles: newGoFilesMap(), diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index e94ad7ba09c..39e958e0fc2 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -89,7 +89,7 @@ type snapshot struct { // packages maps a packageKey to a set of packageHandles to which that file belongs. // It may be invalidated when a file's content changes. - packages map[packageKey]*packageHandle + packages packagesMap // actions maps an actionkey to its actionHandle. actions map[actionKey]*actionHandle @@ -140,6 +140,7 @@ type actionKey struct { func (s *snapshot) Destroy(destroyedBy string) { s.generation.Destroy(destroyedBy) + s.packages.Destroy() s.files.Destroy() s.goFiles.Destroy() s.parseKeysByURI.Destroy() @@ -711,16 +712,17 @@ func (s *snapshot) getImportedBy(id PackageID) []PackageID { return s.meta.importedBy[id] } -func (s *snapshot) addPackageHandle(ph *packageHandle) *packageHandle { +func (s *snapshot) addPackageHandle(ph *packageHandle, release func()) *packageHandle { s.mu.Lock() defer s.mu.Unlock() // If the package handle has already been cached, // return the cached handle instead of overriding it. - if ph, ok := s.packages[ph.packageKey()]; ok { - return ph + if result, ok := s.packages.Get(ph.packageKey()); ok { + release() + return result } - s.packages[ph.packageKey()] = ph + s.packages.Set(ph.packageKey(), ph, release) return ph } @@ -1090,10 +1092,10 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac defer s.mu.Unlock() results := map[string]source.Package{} - for _, ph := range s.packages { + s.packages.Range(func(key packageKey, ph *packageHandle) { cachedPkg, err := ph.cached(s.generation) if err != nil { - continue + return } for importPath, newPkg := range cachedPkg.imports { if oldPkg, ok := results[string(importPath)]; ok { @@ -1105,7 +1107,7 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac results[string(importPath)] = newPkg } } - } + }) return results, nil } @@ -1134,7 +1136,8 @@ func (s *snapshot) getPackage(id PackageID, mode source.ParseMode) *packageHandl id: id, mode: mode, } - return s.packages[key] + ph, _ := s.packages.Get(key) + return ph } func (s *snapshot) getSymbolHandle(uri span.URI) *symbolHandle { @@ -1690,7 +1693,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC builtin: s.builtin, initializeOnce: s.initializeOnce, initializedErr: s.initializedErr, - packages: make(map[packageKey]*packageHandle, len(s.packages)), + packages: s.packages.Clone(), actions: make(map[actionKey]*actionHandle, len(s.actions)), files: s.files.Clone(), goFiles: s.goFiles.Clone(), @@ -1894,13 +1897,12 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC addRevDeps(id, invalidateMetadata) } - // Copy the package type information. - for k, v := range s.packages { - if _, ok := idsToInvalidate[k.id]; ok { - continue + // Delete invalidated package type information. + for id := range idsToInvalidate { + for _, mode := range source.AllParseModes { + key := packageKey{mode, id} + result.packages.Delete(key) } - newGen.Inherit(v.handle) - result.packages[k] = v } // Copy the package analysis information. diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 73e1b7f89ed..98d11517d87 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -478,6 +478,10 @@ const ( ParseFull ) +// AllParseModes contains all possible values of ParseMode. +// It is used for cache invalidation on a file content change. +var AllParseModes = []ParseMode{ParseHeader, ParseExported, ParseFull} + // TypecheckMode controls what kind of parsing should be done (see ParseMode) // while type checking a package. type TypecheckMode int From 79fefdf61d2c7cb1291d58d68aab082144115b05 Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Tue, 14 Jun 2022 18:14:38 +0000 Subject: [PATCH 067/723] internal/memoize: do not iterate all handles on generation destruction This allows reducing critical section of `g.store.mu` as the vast majority of entries do not rely on generation-based GC. Change-Id: I985af0b38504ddedb22649290deac91797577b75 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413656 Reviewed-by: David Chase Reviewed-by: Alan Donovan Run-TryBot: Robert Findley Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- internal/memoize/memoize.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index a758deeb7f8..7fa5340c5a3 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -33,6 +33,8 @@ var ( type Store struct { handlesMu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu handles map[interface{}]*Handle + // handles which are bound to generations for GC purposes. + boundHandles map[*Handle]struct{} } // Generation creates a new Generation associated with s. Destroy must be @@ -71,10 +73,7 @@ func (g *Generation) Destroy(destroyedBy string) { g.store.handlesMu.Lock() defer g.store.handlesMu.Unlock() - for _, h := range g.store.handles { - if !h.trackGenerations { - continue - } + for h := range g.store.boundHandles { h.mu.Lock() if _, ok := h.generations[g]; ok { delete(h.generations, g) // delete even if it's dead, in case of dangling references to the entry. @@ -237,7 +236,11 @@ func (g *Generation) getHandle(key interface{}, function Function, cleanup func( trackGenerations: trackGenerations, } if trackGenerations { + if g.store.boundHandles == nil { + g.store.boundHandles = map[*Handle]struct{}{} + } h.generations = make(map[*Generation]struct{}, 1) + g.store.boundHandles[h] = struct{}{} } if g.store.handles == nil { @@ -302,6 +305,9 @@ func (h *Handle) destroy(store *Store) { h.cleanup(h.value) } delete(store.handles, h.key) + if h.trackGenerations { + delete(store.boundHandles, h) + } } func (h *Handle) incrementRef(g *Generation) { From 93bf1fcc7c91fd5ef6c2a85234ab9f6543936707 Mon Sep 17 00:00:00 2001 From: Davide Masserut Date: Sat, 25 Jun 2022 20:55:23 +0000 Subject: [PATCH 068/723] gopls: add range over channel postfix completion This adds a snippet that applies to variables of type chan. When used, it replaces `channel.range!` with the following snippet: ``` for e := range channel { | } ``` Where `|` indicates the location of the cursor. Change-Id: I8b2f889b22b9f2c292041e5ca5f63c5d0ca98f11 GitHub-Last-Rev: 9cb894be80d0c5243a5e42779c3e96ba79aa66b5 GitHub-Pull-Request: golang/tools#386 Reviewed-on: https://go-review.googlesource.com/c/tools/+/414194 Reviewed-by: Hyang-Ah Hana Kim Reviewed-by: Robert Findley Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- .../completion/postfix_snippet_test.go | 21 +++++++++++++++++++ .../lsp/source/completion/postfix_snippets.go | 8 +++++++ 2 files changed, 29 insertions(+) diff --git a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go index 2674d555c5a..7e595aaad1e 100644 --- a/gopls/internal/regtest/completion/postfix_snippet_test.go +++ b/gopls/internal/regtest/completion/postfix_snippet_test.go @@ -264,6 +264,27 @@ for k := range foo { keys = append(keys, k) } +} +`, + }, + { + name: "channel_range", + before: ` +package foo + +func _() { + foo := make(chan int) + foo.range +} +`, + after: ` +package foo + +func _() { + foo := make(chan int) + for e := range foo { + $0 +} } `, }, diff --git a/internal/lsp/source/completion/postfix_snippets.go b/internal/lsp/source/completion/postfix_snippets.go index d7f0d90da9e..aa8454f8e98 100644 --- a/internal/lsp/source/completion/postfix_snippets.go +++ b/internal/lsp/source/completion/postfix_snippets.go @@ -149,6 +149,14 @@ for {{.VarName .KeyType "k"}}, {{.VarName .ElemType "v"}} := range {{.X}} { {{$keysVar}} = append({{$keysVar}}, {{$k}}) } {{end}}`, +}, { + label: "range", + details: "range over channel", + body: `{{if and (eq .Kind "chan") .StmtOK -}} +for {{.VarName .ElemType "e"}} := range {{.X}} { + {{.Cursor}} +} +{{- end}}`, }, { label: "var", details: "assign to variables", From ffc70b9ac150680da53ed53e8543fd0e938ce4f4 Mon Sep 17 00:00:00 2001 From: Muir Manders Date: Thu, 30 Jun 2022 19:44:46 -0700 Subject: [PATCH 069/723] lsp/completion: fix ranking of *types.PkgName candidates In Go 1.18 types.AssignableTo() started reporting that an invalid type is assignable to any interface. *types.PkgName (i.e. an import at the top of the file) has an invalid type for its Type(), so we started thinking all in scope imports were great candidates when the expected type was an interface. Fix by wrapping the AssignableTo (and AssertableTo) to explicitly return false if either operand is invalid. Updates golang/go#53595 Change-Id: Ie5a84b7f410ff5c73c6b7870e052bafaf3e21e99 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415595 Reviewed-by: Hyang-Ah Hana Kim Reviewed-by: Robert Findley Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- internal/lsp/source/completion/completion.go | 12 +++++------ internal/lsp/source/completion/util.go | 20 +++++++++++++++++++ internal/lsp/testdata/deep/deep.go | 7 +++++++ internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/internal/lsp/source/completion/completion.go b/internal/lsp/source/completion/completion.go index 0c1ff3f21b0..a2dfae69841 100644 --- a/internal/lsp/source/completion/completion.go +++ b/internal/lsp/source/completion/completion.go @@ -2314,7 +2314,7 @@ func (ci candidateInference) applyTypeNameModifiers(typ types.Type) types.Type { // matchesVariadic returns true if we are completing a variadic // parameter and candType is a compatible slice type. func (ci candidateInference) matchesVariadic(candType types.Type) bool { - return ci.variadic && ci.objType != nil && types.AssignableTo(candType, types.NewSlice(ci.objType)) + return ci.variadic && ci.objType != nil && assignableTo(candType, types.NewSlice(ci.objType)) } // findSwitchStmt returns an *ast.CaseClause's corresponding *ast.SwitchStmt or @@ -2640,7 +2640,7 @@ func (ci *candidateInference) candTypeMatches(cand *candidate) bool { return false } - if ci.convertibleTo != nil && types.ConvertibleTo(candType, ci.convertibleTo) { + if ci.convertibleTo != nil && convertibleTo(candType, ci.convertibleTo) { return true } @@ -2728,7 +2728,7 @@ func considerTypeConversion(from, to types.Type, path []types.Object) bool { return false } - if !types.ConvertibleTo(from, to) { + if !convertibleTo(from, to) { return false } @@ -2777,7 +2777,7 @@ func (ci *candidateInference) typeMatches(expType, candType types.Type) bool { // AssignableTo covers the case where the types are equal, but also handles // cases like assigning a concrete type to an interface type. - return types.AssignableTo(candType, expType) + return assignableTo(candType, expType) } // kindMatches reports whether candType's kind matches our expected @@ -2840,7 +2840,7 @@ func (ci *candidateInference) assigneesMatch(cand *candidate, sig *types.Signatu assignee = ci.assignees[i] } - if assignee == nil { + if assignee == nil || assignee == types.Typ[types.Invalid] { continue } @@ -2894,7 +2894,7 @@ func (c *completer) matchingTypeName(cand *candidate) bool { // // Where our expected type is "[]int", and we expect a type name. if c.inference.objType != nil { - return types.AssignableTo(candType, c.inference.objType) + return assignableTo(candType, c.inference.objType) } // Default to saying any type name is a match. diff --git a/internal/lsp/source/completion/util.go b/internal/lsp/source/completion/util.go index cd7849af262..e6d3bfd745f 100644 --- a/internal/lsp/source/completion/util.go +++ b/internal/lsp/source/completion/util.go @@ -321,3 +321,23 @@ func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.Tex NewText: newText, }}) } + +// assignableTo is like types.AssignableTo, but returns false if +// either type is invalid. +func assignableTo(x, to types.Type) bool { + if x == types.Typ[types.Invalid] || to == types.Typ[types.Invalid] { + return false + } + + return types.AssignableTo(x, to) +} + +// convertibleTo is like types.ConvertibleTo, but returns false if +// either type is invalid. +func convertibleTo(x, to types.Type) bool { + if x == types.Typ[types.Invalid] || to == types.Typ[types.Invalid] { + return false + } + + return types.ConvertibleTo(x, to) +} diff --git a/internal/lsp/testdata/deep/deep.go b/internal/lsp/testdata/deep/deep.go index 6ed5ff83999..6908824f82f 100644 --- a/internal/lsp/testdata/deep/deep.go +++ b/internal/lsp/testdata/deep/deep.go @@ -28,6 +28,13 @@ func _() { wantsContext(c) //@rank(")", ctxBackground),rank(")", ctxTODO) } +func _() { + var cork struct{ err error } + cork.err //@item(deepCorkErr, "cork.err", "error", "field") + context //@item(deepContextPkg, "context", "\"context\"", "package") + var _ error = co //@rank(" //", deepCorkErr, deepContextPkg) +} + func _() { // deepCircle is circular. type deepCircle struct { diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index 0247551f8b9..b6c6c07b15d 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -6,7 +6,7 @@ CompletionSnippetCount = 106 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 -RankedCompletionsCount = 163 +RankedCompletionsCount = 164 CaseSensitiveCompletionsCount = 4 DiagnosticsCount = 37 FoldingRangesCount = 2 diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 7e8da12d764..9fadf634090 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -6,7 +6,7 @@ CompletionSnippetCount = 116 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 -RankedCompletionsCount = 173 +RankedCompletionsCount = 174 CaseSensitiveCompletionsCount = 4 DiagnosticsCount = 37 FoldingRangesCount = 2 From bec0cf16be3beb52370134de9d720cefec13863f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 27 Jun 2022 17:42:21 -0400 Subject: [PATCH 070/723] internal/lsp/cache: avoid Handle mechanism for workspace dir This change causes (*snapshot).getWorkspaceDir to create a temporary directory directly, rather than via the Store/Generation/Handle mechanism. The work is done at most once per snapshot, and the directory is deleted in Snapshot.Destroy. This removes the last remaining use of Handle's cleanup mechanism, which will be deleted in a follow-up. Change-Id: I32f09a67846d9b5577cb8849b226427f86443303 Reviewed-on: https://go-review.googlesource.com/c/tools/+/414499 gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/cache/load.go | 86 ++++++++++++---------------------- internal/lsp/cache/snapshot.go | 18 ++++--- 2 files changed, 41 insertions(+), 63 deletions(-) diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 3fb67a7a98b..d613dc3337e 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -7,7 +7,6 @@ package cache import ( "bytes" "context" - "crypto/sha256" "errors" "fmt" "io/ioutil" @@ -24,7 +23,6 @@ import ( "golang.org/x/tools/internal/lsp/debug/tag" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/span" ) @@ -384,77 +382,53 @@ func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, fi return srcDiags } -type workspaceDirKey string - -type workspaceDirData struct { - dir string - err error -} - -// getWorkspaceDir gets the URI for the workspace directory associated with -// this snapshot. The workspace directory is a temp directory containing the -// go.mod file computed from all active modules. +// getWorkspaceDir returns the URI for the workspace directory +// associated with this snapshot. The workspace directory is a +// temporary directory containing the go.mod file computed from all +// active modules. func (s *snapshot) getWorkspaceDir(ctx context.Context) (span.URI, error) { s.mu.Lock() - h := s.workspaceDirHandle + dir, err := s.workspaceDir, s.workspaceDirErr s.mu.Unlock() - if h != nil { - return getWorkspaceDir(ctx, h, s.generation) + if dir == "" && err == nil { // cache miss + dir, err = makeWorkspaceDir(ctx, s.workspace, s) + s.mu.Lock() + s.workspaceDir, s.workspaceDirErr = dir, err + s.mu.Unlock() } - file, err := s.workspace.modFile(ctx, s) + return span.URIFromPath(dir), err +} + +// makeWorkspaceDir creates a temporary directory containing a go.mod +// and go.sum file for each module in the workspace. +// Note: snapshot's mutex must be unlocked for it to satisfy FileSource. +func makeWorkspaceDir(ctx context.Context, workspace *workspace, fs source.FileSource) (string, error) { + file, err := workspace.modFile(ctx, fs) if err != nil { return "", err } - hash := sha256.New() modContent, err := file.Format() if err != nil { return "", err } - sumContent, err := s.workspace.sumFile(ctx, s) + sumContent, err := workspace.sumFile(ctx, fs) if err != nil { return "", err } - hash.Write(modContent) - hash.Write(sumContent) - key := workspaceDirKey(hash.Sum(nil)) - s.mu.Lock() - h = s.generation.Bind(key, func(context.Context, memoize.Arg) interface{} { - tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod") - if err != nil { - return &workspaceDirData{err: err} - } - - for name, content := range map[string][]byte{ - "go.mod": modContent, - "go.sum": sumContent, - } { - filename := filepath.Join(tmpdir, name) - if err := ioutil.WriteFile(filename, content, 0644); err != nil { - os.RemoveAll(tmpdir) - return &workspaceDirData{err: err} - } - } - - return &workspaceDirData{dir: tmpdir} - }, func(v interface{}) { - d := v.(*workspaceDirData) - if d.dir != "" { - if err := os.RemoveAll(d.dir); err != nil { - event.Error(context.Background(), "cleaning workspace dir", err) - } - } - }) - s.workspaceDirHandle = h - s.mu.Unlock() - return getWorkspaceDir(ctx, h, s.generation) -} - -func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generation) (span.URI, error) { - v, err := h.Get(ctx, g, nil) + tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod") if err != nil { return "", err } - return span.URIFromPath(v.(*workspaceDirData).dir), nil + for name, content := range map[string][]byte{ + "go.mod": modContent, + "go.sum": sumContent, + } { + if err := ioutil.WriteFile(filepath.Join(tmpdir, name), content, 0644); err != nil { + os.RemoveAll(tmpdir) // ignore error + return "", err + } + } + return tmpdir, nil } // computeMetadataUpdates populates the updates map with metadata updates to diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 39e958e0fc2..259345bdc8f 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -115,8 +115,11 @@ type snapshot struct { modTidyHandles map[span.URI]*modTidyHandle modWhyHandles map[span.URI]*modWhyHandle - workspace *workspace - workspaceDirHandle *memoize.Handle + workspace *workspace // (not guarded by mu) + + // The cached result of makeWorkspaceDir, created on demand and deleted by Snapshot.Destroy. + workspaceDir string + workspaceDirErr error // knownSubdirs is the set of subdirectories in the workspace, used to // create glob patterns for file watching. @@ -144,6 +147,12 @@ func (s *snapshot) Destroy(destroyedBy string) { s.files.Destroy() s.goFiles.Destroy() s.parseKeysByURI.Destroy() + + if s.workspaceDir != "" { + if err := os.RemoveAll(s.workspaceDir); err != nil { + event.Error(context.Background(), "cleaning workspace dir", err) + } + } } func (s *snapshot) ID() uint64 { @@ -1709,11 +1718,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC workspace: newWorkspace, } - if !workspaceChanged && s.workspaceDirHandle != nil { - result.workspaceDirHandle = s.workspaceDirHandle - newGen.Inherit(s.workspaceDirHandle) - } - // Copy all of the FileHandles. for k, v := range s.symbols { if change, ok := changes[k]; ok { From f042799df4543ff8c76f0bf549b48338bcba94ac Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 1 Jul 2022 10:35:27 -0400 Subject: [PATCH 071/723] internal/memoize: delete Bind(cleanup) hook Now that the workspace directory uses Snapshot.Destroy to clean up (see https://go-review.googlesource.com/c/tools/+/414499) there is no need for this feature. Change-Id: Id5782273ce5030b4fb8f3b66a8d16a45a831ed91 Reviewed-on: https://go-review.googlesource.com/c/tools/+/414500 Reviewed-by: Robert Findley gopls-CI: kokoro Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan --- internal/lsp/cache/analysis.go | 2 +- internal/lsp/cache/mod.go | 6 +-- internal/lsp/cache/mod_tidy.go | 2 +- internal/lsp/cache/parse.go | 2 +- internal/lsp/cache/symbols.go | 2 +- internal/memoize/memoize.go | 67 ++++++++------------------------ internal/memoize/memoize_test.go | 45 ++------------------- 7 files changed, 27 insertions(+), 99 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index 9f7a19c5c60..847ac2db8d0 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -147,7 +147,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } } return runAnalysis(ctx, snapshot, a, pkg, results) - }, nil) + }) act.handle = h act = s.addActionHandle(act) diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index c076f424dc9..843919d7b36 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -88,7 +88,7 @@ func (s *snapshot) ParseMod(ctx context.Context, modFH source.FileHandle) (*sour }, err: parseErr, } - }, nil) + }) pmh := &parseModHandle{handle: h} s.mu.Lock() @@ -162,7 +162,7 @@ func (s *snapshot) ParseWork(ctx context.Context, modFH source.FileHandle) (*sou }, err: parseErr, } - }, nil) + }) pwh := &parseWorkHandle{handle: h} s.mu.Lock() @@ -288,7 +288,7 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string why[req.Mod.Path] = whyList[i] } return &modWhyData{why: why} - }, nil) + }) mwh := &modWhyHandle{handle: h} s.mu.Lock() diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index bd2ff0c5f88..0f36249e057 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -138,7 +138,7 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc TidiedContent: tempContents, }, } - }, nil) + }) mth := &modTidyHandle{handle: h} s.mu.Lock() diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index f7b4f9c7031..c3eae2f7643 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -130,7 +130,7 @@ func (s *snapshot) astCacheData(ctx context.Context, spkg source.Package, pos to } astHandle := s.generation.Bind(astCacheKey{pkgHandle.key, pgf.URI}, func(ctx context.Context, arg memoize.Arg) interface{} { return buildASTCache(pgf) - }, nil) + }) d, err := astHandle.Get(ctx, s.generation, s) if err != nil { diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go index d56a036ff6e..f27178e2e7f 100644 --- a/internal/lsp/cache/symbols.go +++ b/internal/lsp/cache/symbols.go @@ -41,7 +41,7 @@ func (s *snapshot) buildSymbolHandle(ctx context.Context, fh source.FileHandle) snapshot := arg.(*snapshot) symbols, err := symbolize(snapshot, fh) return &symbolData{symbols, err} - }, nil) + }) sh := &symbolHandle{ handle: handle, diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 7fa5340c5a3..4b84410d506 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -34,6 +34,7 @@ type Store struct { handlesMu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu handles map[interface{}]*Handle // handles which are bound to generations for GC purposes. + // (It is the subset of values of 'handles' with trackGenerations enabled.) boundHandles map[*Handle]struct{} } @@ -78,7 +79,11 @@ func (g *Generation) Destroy(destroyedBy string) { if _, ok := h.generations[g]; ok { delete(h.generations, g) // delete even if it's dead, in case of dangling references to the entry. if len(h.generations) == 0 { - h.destroy(g.store) + h.state = stateDestroyed + delete(g.store.handles, h.key) + if h.trackGenerations { + delete(g.store.boundHandles, h) + } } } h.mu.Unlock() @@ -155,14 +160,6 @@ type Handle struct { function Function // value is set in completed state. value interface{} - // cleanup, if non-nil, is used to perform any necessary clean-up on values - // produced by function. - // - // cleanup is never set for reference counted handles. - // - // TODO(rfindley): remove this field once workspace folders no longer need to - // be tracked. - cleanup func(interface{}) // If trackGenerations is set, this handle tracks generations in which it // is valid, via the generations field. Otherwise, it is explicitly reference @@ -171,7 +168,7 @@ type Handle struct { refCounter int32 } -// Bind returns a handle for the given key and function. +// Bind returns a "generational" handle for the given key and function. // // Each call to bind will return the same handle if it is already bound. Bind // will always return a valid handle, creating one if needed. Each key can @@ -179,22 +176,18 @@ type Handle struct { // until the associated generation is destroyed. Bind does not cause the value // to be generated. // -// If cleanup is non-nil, it will be called on any non-nil values produced by -// function when they are no longer referenced. -// // It is responsibility of the caller to call Inherit on the handler whenever // it should still be accessible by a next generation. -func (g *Generation) Bind(key interface{}, function Function, cleanup func(interface{})) *Handle { - return g.getHandle(key, function, cleanup, true) +func (g *Generation) Bind(key interface{}, function Function) *Handle { + return g.getHandle(key, function, true) } -// GetHandle returns a handle for the given key and function with similar -// properties and behavior as Bind. -// -// As in opposite to Bind it returns a release callback which has to be called -// once this reference to handle is not needed anymore. +// GetHandle returns a "reference-counted" handle for the given key +// and function with similar properties and behavior as Bind. Unlike +// Bind, it returns a release callback which must be called once the +// handle is no longer needed. func (g *Generation) GetHandle(key interface{}, function Function) (*Handle, func()) { - h := g.getHandle(key, function, nil, false) + h := g.getHandle(key, function, false) store := g.store release := func() { // Acquire store.handlesMu before mutating refCounter @@ -206,7 +199,7 @@ func (g *Generation) GetHandle(key interface{}, function Function) (*Handle, fun h.refCounter-- if h.refCounter == 0 { - // Don't call h.destroy: for reference counted handles we can't know when + // Don't mark destroyed: for reference counted handles we can't know when // they are no longer reachable from runnable goroutines. For example, // gopls could have a current operation that is using a packageHandle. // Destroying the handle here would cause that operation to hang. @@ -216,7 +209,7 @@ func (g *Generation) GetHandle(key interface{}, function Function) (*Handle, fun return h, release } -func (g *Generation) getHandle(key interface{}, function Function, cleanup func(interface{}), trackGenerations bool) *Handle { +func (g *Generation) getHandle(key interface{}, function Function, trackGenerations bool) *Handle { // panic early if the function is nil // it would panic later anyway, but in a way that was much harder to debug if function == nil { @@ -232,7 +225,6 @@ func (g *Generation) getHandle(key interface{}, function Function, cleanup func( h = &Handle{ key: key, function: function, - cleanup: cleanup, trackGenerations: trackGenerations, } if trackGenerations { @@ -298,18 +290,6 @@ func (g *Generation) Inherit(h *Handle) { h.incrementRef(g) } -// destroy marks h as destroyed. h.mu and store.handlesMu must be held. -func (h *Handle) destroy(store *Store) { - h.state = stateDestroyed - if h.cleanup != nil && h.value != nil { - h.cleanup(h.value) - } - delete(store.handles, h.key) - if h.trackGenerations { - delete(store.boundHandles, h) - } -} - func (h *Handle) incrementRef(g *Generation) { h.mu.Lock() defer h.mu.Unlock() @@ -412,11 +392,6 @@ func (h *Handle) run(ctx context.Context, g *Generation, arg Arg) (interface{}, } v := function(childCtx, arg) if childCtx.Err() != nil { - // It's possible that v was computed despite the context cancellation. In - // this case we should ensure that it is cleaned up. - if h.cleanup != nil && v != nil { - h.cleanup(v) - } return } @@ -427,19 +402,9 @@ func (h *Handle) run(ctx context.Context, g *Generation, arg Arg) (interface{}, // checked childCtx above. Even so, that should be harmless, since each // run should produce the same results. if h.state != stateRunning { - // v will never be used, so ensure that it is cleaned up. - if h.cleanup != nil && v != nil { - h.cleanup(v) - } return } - if h.cleanup != nil && h.value != nil { - // Clean up before overwriting an existing value. - h.cleanup(h.value) - } - - // At this point v will be cleaned up whenever h is destroyed. h.value = v h.function = nil h.state = stateCompleted diff --git a/internal/memoize/memoize_test.go b/internal/memoize/memoize_test.go index ae387b8d049..48bb181173e 100644 --- a/internal/memoize/memoize_test.go +++ b/internal/memoize/memoize_test.go @@ -23,7 +23,7 @@ func TestGet(t *testing.T) { h := g.Bind("key", func(context.Context, memoize.Arg) interface{} { evaled++ return "res" - }, nil) + }) expectGet(t, h, g, "res") expectGet(t, h, g, "res") if evaled != 1 { @@ -50,7 +50,7 @@ func TestGenerations(t *testing.T) { s := &memoize.Store{} // Evaluate key in g1. g1 := s.Generation("g1") - h1 := g1.Bind("key", func(context.Context, memoize.Arg) interface{} { return "res" }, nil) + h1 := g1.Bind("key", func(context.Context, memoize.Arg) interface{} { return "res" }) expectGet(t, h1, g1, "res") // Get key in g2. It should inherit the value from g1. @@ -58,7 +58,7 @@ func TestGenerations(t *testing.T) { h2 := g2.Bind("key", func(context.Context, memoize.Arg) interface{} { t.Fatal("h2 should not need evaluation") return "error" - }, nil) + }) expectGet(t, h2, g2, "res") // With g1 destroyed, g2 should still work. @@ -68,47 +68,10 @@ func TestGenerations(t *testing.T) { // With all generations destroyed, key should be re-evaluated. g2.Destroy("TestGenerations") g3 := s.Generation("g3") - h3 := g3.Bind("key", func(context.Context, memoize.Arg) interface{} { return "new res" }, nil) + h3 := g3.Bind("key", func(context.Context, memoize.Arg) interface{} { return "new res" }) expectGet(t, h3, g3, "new res") } -func TestCleanup(t *testing.T) { - s := &memoize.Store{} - g1 := s.Generation("g1") - v1 := false - v2 := false - cleanup := func(v interface{}) { - *(v.(*bool)) = true - } - h1 := g1.Bind("key1", func(context.Context, memoize.Arg) interface{} { - return &v1 - }, nil) - h2 := g1.Bind("key2", func(context.Context, memoize.Arg) interface{} { - return &v2 - }, cleanup) - expectGet(t, h1, g1, &v1) - expectGet(t, h2, g1, &v2) - g2 := s.Generation("g2") - g2.Inherit(h1) - g2.Inherit(h2) - - g1.Destroy("TestCleanup") - expectGet(t, h1, g2, &v1) - expectGet(t, h2, g2, &v2) - for k, v := range map[string]*bool{"key1": &v1, "key2": &v2} { - if got, want := *v, false; got != want { - t.Errorf("after destroying g1, bound value %q is cleaned up", k) - } - } - g2.Destroy("TestCleanup") - if got, want := v1, false; got != want { - t.Error("after destroying g2, v1 is cleaned up") - } - if got, want := v2, true; got != want { - t.Error("after destroying g2, v2 is not cleaned up") - } -} - func TestHandleRefCounting(t *testing.T) { s := &memoize.Store{} g1 := s.Generation("g1") From 7b04e8b59ec2e614a9a351d6da0ae4ae69f4ca12 Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Wed, 22 Jun 2022 23:05:59 +0000 Subject: [PATCH 072/723] internal/persistent: no-op deletion from map does not allocate We can use a property that split does a dfs search for the key before doing an actual work. This allows us to do a low-cost early return if there is no key to delete. Change-Id: I6ed8068945f9f2dacc356d72b18afce04ec89a3c Reviewed-on: https://go-review.googlesource.com/c/tools/+/413659 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan Reviewed-by: Dmitri Shuralyov --- internal/persistent/map.go | 21 ++++++++++++++++----- internal/persistent/map_test.go | 25 +++++++++---------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/internal/persistent/map.go b/internal/persistent/map.go index 9c17ad09a7f..19b50480db4 100644 --- a/internal/persistent/map.go +++ b/internal/persistent/map.go @@ -185,7 +185,7 @@ func union(first, second *mapNode, less func(a, b interface{}) bool, overwrite b second, first, overwrite = first, second, !overwrite } - left, mid, right := split(second, first.key, less) + left, mid, right := split(second, first.key, less, false) var result *mapNode if overwrite && mid != nil { result = mid.shallowCloneWithRef() @@ -205,23 +205,31 @@ func union(first, second *mapNode, less func(a, b interface{}) bool, overwrite b // Return three new trees: left with all nodes with smaller than key, mid with // the node matching the key, right with all nodes larger than key. // If there are no nodes in one of trees, return nil instead of it. +// If requireMid is set (such as during deletion), then all return arguments +// are nil if mid is not found. // // split(n:-0) (left:+1, mid:+1, right:+1) // Split borrows n without affecting its refcount, and returns three // new references that that caller is expected to call decref. -func split(n *mapNode, key interface{}, less func(a, b interface{}) bool) (left, mid, right *mapNode) { +func split(n *mapNode, key interface{}, less func(a, b interface{}) bool, requireMid bool) (left, mid, right *mapNode) { if n == nil { return nil, nil, nil } if less(n.key, key) { - left, mid, right := split(n.right, key, less) + left, mid, right := split(n.right, key, less, requireMid) + if requireMid && mid == nil { + return nil, nil, nil + } newN := n.shallowCloneWithRef() newN.left = n.left.incref() newN.right = left return newN, mid, right } else if less(key, n.key) { - left, mid, right := split(n.left, key, less) + left, mid, right := split(n.left, key, less, requireMid) + if requireMid && mid == nil { + return nil, nil, nil + } newN := n.shallowCloneWithRef() newN.left = right newN.right = n.right.incref() @@ -234,7 +242,10 @@ func split(n *mapNode, key interface{}, less func(a, b interface{}) bool) (left, // Delete deletes the value for a key. func (pm *Map) Delete(key interface{}) { root := pm.root - left, mid, right := split(root, key, pm.less) + left, mid, right := split(root, key, pm.less, true) + if mid == nil { + return + } pm.root = merge(left, right) left.decref() mid.decref() diff --git a/internal/persistent/map_test.go b/internal/persistent/map_test.go index 059f0da4c03..bd2cbfa0e12 100644 --- a/internal/persistent/map_test.go +++ b/internal/persistent/map_test.go @@ -71,6 +71,15 @@ func TestSimpleMap(t *testing.T) { m1.remove(t, 1) validateRef(t, m1, m2) + gotAllocs := int(testing.AllocsPerRun(10, func() { + m1.impl.Delete(100) + m1.impl.Delete(1) + })) + wantAllocs := 0 + if gotAllocs != wantAllocs { + t.Errorf("wanted %d allocs, got %d", wantAllocs, gotAllocs) + } + for i := 10; i < 14; i++ { m1.set(t, i, i) validateRef(t, m1, m2) @@ -298,19 +307,3 @@ func assertSameMap(t *testing.T, map1, map2 interface{}) { t.Fatalf("different maps:\n%v\nvs\n%v", map1, map2) } } - -func isSameMap(map1, map2 reflect.Value) bool { - if map1.Len() != map2.Len() { - return false - } - iter := map1.MapRange() - for iter.Next() { - key := iter.Key() - value1 := iter.Value() - value2 := map2.MapIndex(key) - if value2.IsZero() || !reflect.DeepEqual(value1.Interface(), value2.Interface()) { - return false - } - } - return true -} From f487f3623ea4e13340147e56a278980f28781f8b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 1 Jul 2022 11:52:34 -0400 Subject: [PATCH 073/723] internal/lsp/source: reduce allocation in workspace-symbols dynamicSymbolMatch is an allocation hotspot (9% of all bytes), because it allocates a 3-element []string that quickly becomes garbage. This change passes in an empty slice with spare capacity allowing the same array to be reused throughout the matchFile loop. BenchmarkSymbols on k8s shows -72% bytes, -88% allocs, -9% wall time. Change-Id: Id20c7cd649874a212e4d4c5f1aa095277b044a5b Reviewed-on: https://go-review.googlesource.com/c/tools/+/415500 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- internal/lsp/source/workspace_symbol.go | 31 ++++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go index 0822de0810d..6167c586a9a 100644 --- a/internal/lsp/source/workspace_symbol.go +++ b/internal/lsp/source/workspace_symbol.go @@ -83,17 +83,19 @@ type matcherFunc func(chunks []string) (int, float64) // []string{"myType.field"} or []string{"myType.", "field"}. // // See the comment for symbolCollector for more information. -type symbolizer func(name string, pkg Metadata, m matcherFunc) ([]string, float64) +// +// The space argument is an empty slice with spare capacity that may be used +// to allocate the result. +type symbolizer func(space []string, name string, pkg Metadata, m matcherFunc) ([]string, float64) -func fullyQualifiedSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { - _, score := dynamicSymbolMatch(name, pkg, matcher) - if score > 0 { - return []string{pkg.PackagePath(), ".", name}, score +func fullyQualifiedSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { + if _, score := dynamicSymbolMatch(space, name, pkg, matcher); score > 0 { + return append(space, pkg.PackagePath(), ".", name), score } return nil, 0 } -func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { +func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { var score float64 endsInPkgName := strings.HasSuffix(pkg.PackagePath(), pkg.PackageName()) @@ -101,14 +103,14 @@ func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]strin // If the package path does not end in the package name, we need to check the // package-qualified symbol as an extra pass first. if !endsInPkgName { - pkgQualified := []string{pkg.PackageName(), ".", name} + pkgQualified := append(space, pkg.PackageName(), ".", name) idx, score := matcher(pkgQualified) nameStart := len(pkg.PackageName()) + 1 if score > 0 { // If our match is contained entirely within the unqualified portion, // just return that. if idx >= nameStart { - return []string{name}, score + return append(space, name), score } // Lower the score for matches that include the package name. return pkgQualified, score * 0.8 @@ -116,13 +118,13 @@ func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]strin } // Now try matching the fully qualified symbol. - fullyQualified := []string{pkg.PackagePath(), ".", name} + fullyQualified := append(space, pkg.PackagePath(), ".", name) idx, score := matcher(fullyQualified) // As above, check if we matched just the unqualified symbol name. nameStart := len(pkg.PackagePath()) + 1 if idx >= nameStart { - return []string{name}, score + return append(space, name), score } // If our package path ends in the package name, we'll have skipped the @@ -131,7 +133,7 @@ func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]strin if endsInPkgName && idx >= 0 { pkgStart := len(pkg.PackagePath()) - len(pkg.PackageName()) if idx >= pkgStart { - return []string{pkg.PackageName(), ".", name}, score + return append(space, pkg.PackageName(), ".", name), score } } @@ -140,8 +142,8 @@ func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]strin return fullyQualified, score * 0.6 } -func packageSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { - qualified := []string{pkg.PackageName(), ".", name} +func packageSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { + qualified := append(space, pkg.PackageName(), ".", name) if _, s := matcher(qualified); s > 0 { return qualified, s } @@ -387,8 +389,9 @@ type symbolFile struct { // matchFile scans a symbol file and adds matching symbols to the store. func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, roots []string, i symbolFile) { + space := make([]string, 0, 3) for _, sym := range i.syms { - symbolParts, score := symbolizer(sym.Name, i.md, matcher) + symbolParts, score := symbolizer(space, sym.Name, i.md, matcher) // Check if the score is too low before applying any downranking. if store.tooLow(score) { From e92a18fd15832b49c64c4d7637711b91a50dcde8 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 1 Jul 2022 12:22:37 -0400 Subject: [PATCH 074/723] internal/lsp/lsppos: reduce allocations in NewMapper Before, NewMapper accounts for 2.1% of bytes allocated in the WorkspaceSymbols benchmark. This change causes the newline index table to be allocated once instead of by appending. The function now accounts for 0.55%. Change-Id: I9172dd34ee2be9e7175e311d4a6518f1e6660a5f Reviewed-on: https://go-review.googlesource.com/c/tools/+/415501 Auto-Submit: Alan Donovan gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- internal/lsp/lsppos/lsppos.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/lsp/lsppos/lsppos.go b/internal/lsp/lsppos/lsppos.go index 35f6f134854..6afad47b7d2 100644 --- a/internal/lsp/lsppos/lsppos.go +++ b/internal/lsp/lsppos/lsppos.go @@ -17,6 +17,7 @@ package lsppos import ( + "bytes" "errors" "sort" "unicode/utf8" @@ -36,9 +37,10 @@ type Mapper struct { // NewMapper creates a new Mapper for the given content. func NewMapper(content []byte) *Mapper { + nlines := bytes.Count(content, []byte("\n")) m := &Mapper{ content: content, - lines: []int{0}, + lines: make([]int, 1, nlines+1), // initially []int{0} } for offset, b := range content { if b == '\n' { From f79f3aac190554ef62f0257555394b31c29f0320 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 14 Jun 2022 17:51:56 -0400 Subject: [PATCH 075/723] internal/lsp/cache: clarify buildPackageHandle Before, buildPackageHandle and buildKey were mutually recursive. Together they performed a sequential recursion over Metadata.Deps, calling GetFile and parseGoHandle for every file, and then finally (in postorder) binding a Handle for the type checking step. This change inlines buildKey to make the recursion more obvious, performs the recursion over dependencies first, followed by the reading of Go source files for this package, in parallel. (The IWL benchmark reports improvement but its variance is so high I'm not sure I trust it.) Other opportunities for parallelism are pointed out in new comments. The Bind operation for typechecking calls dep.check for each dependency in a separate goroutine. It no longer waits for each one since it is only prefetching the information that will be required during import processing, which will block until the information becomes available. Before, both reading and parsing appear to occur twice: once in buildPackageHandle and once in doTypeCheck. (Perhaps the second was a cache hits, but there's no need to rely on a cache.) Now, only file reading (GetFile) occurs in buildPackageHandle, since that's all that's needed for the packageKey. And parsing only occurs in doTypeCheck. The source.FileHandles are plumbed through as parameters. Also: - move parseGoHandles to a local function, since it exists only for buildPackageKey. It no longer parses, it only reads. - lots of TODO comments for possible optimizations, and typical measured times of various operations. - remove obsolete comment re: Bind and finalizers. Change-Id: Iad049884607b73eaa6701bdf7771f96b042142d5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411913 Run-TryBot: Alan Donovan gopls-CI: kokoro Auto-Submit: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/cache/cache.go | 4 +- internal/lsp/cache/check.go | 271 +++++++++++++++++---------------- internal/lsp/cache/mod_tidy.go | 4 +- internal/lsp/cache/symbols.go | 3 +- 4 files changed, 147 insertions(+), 135 deletions(-) diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index 3640272688f..8ac8b851a25 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -101,7 +101,7 @@ func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error) return fh, nil } - fh, err := readFile(ctx, uri, fi) + fh, err := readFile(ctx, uri, fi) // ~25us if err != nil { return nil, err } @@ -126,7 +126,7 @@ func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, e _ = ctx defer done() - data, err := ioutil.ReadFile(uri.Filename()) + data, err := ioutil.ReadFile(uri.Filename()) // ~20us if err != nil { return &fileHandle{ modTime: fi.ModTime(), diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index 797298a4192..aeb45635c3b 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -15,10 +15,12 @@ import ( "path/filepath" "regexp" "sort" + "strconv" "strings" "sync" "golang.org/x/mod/module" + "golang.org/x/sync/errgroup" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" @@ -37,7 +39,10 @@ type packageHandleKey source.Hash type packageHandle struct { handle *memoize.Handle - goFiles, compiledGoFiles []*parseGoHandle + // goFiles and compiledGoFiles are the lists of files in the package. + // The latter is the list of files seen by the type checker (in which + // those that import "C" have been replaced by generated code). + goFiles, compiledGoFiles []source.FileHandle // mode is the mode the files were parsed in. mode source.ParseMode @@ -57,14 +62,14 @@ func (ph *packageHandle) packageKey() packageKey { } func (ph *packageHandle) imports(ctx context.Context, s source.Snapshot) (result []string) { - for _, pgh := range ph.goFiles { - f, err := s.ParseGo(ctx, pgh.file, source.ParseHeader) + for _, goFile := range ph.goFiles { + f, err := s.ParseGo(ctx, goFile, source.ParseHeader) if err != nil { continue } seen := map[string]struct{}{} for _, impSpec := range f.File.Imports { - imp := strings.Trim(impSpec.Path.Value, `"`) + imp, _ := strconv.Unquote(impSpec.Path.Value) if _, ok := seen[imp]; !ok { seen[imp] = struct{}{} result = append(result, imp) @@ -82,7 +87,8 @@ type packageData struct { err error } -// buildPackageHandle returns a packageHandle for a given package and mode. +// buildPackageHandle returns a handle for the future results of +// type-checking the package identified by id in the given mode. // It assumes that the given ID already has metadata available, so it does not // attempt to reload missing or invalid metadata. The caller must reload // metadata if needed. @@ -91,86 +97,33 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so return ph, nil } - // Build the packageHandle for this ID and its dependencies. - ph, deps, err := s.buildKey(ctx, id, mode) - if err != nil { - return nil, err - } - - // Do not close over the packageHandle or the snapshot in the Bind function. - // This creates a cycle, which causes the finalizers to never run on the handles. - // The possible cycles are: - // - // packageHandle.h.function -> packageHandle - // packageHandle.h.function -> snapshot -> packageHandle - // - - m := ph.m - key := ph.key - - h, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { - snapshot := arg.(*snapshot) - - // Begin loading the direct dependencies, in parallel. - var wg sync.WaitGroup - for _, dep := range deps { - wg.Add(1) - go func(dep *packageHandle) { - dep.check(ctx, snapshot) - wg.Done() - }(dep) - } - - data := &packageData{} - data.pkg, data.err = typeCheck(ctx, snapshot, m.Metadata, mode, deps) - // Make sure that the workers above have finished before we return, - // especially in case of cancellation. - wg.Wait() - - return data - }) - ph.handle = h - - // Cache the handle in the snapshot. If a package handle has already - // been cached, addPackage will return the cached value. This is fine, - // since the original package handle above will have no references and be - // garbage collected. - ph = s.addPackageHandle(ph, release) - - return ph, nil -} - -// buildKey computes the key for a given packageHandle. -func (s *snapshot) buildKey(ctx context.Context, id PackageID, mode source.ParseMode) (*packageHandle, map[PackagePath]*packageHandle, error) { m := s.getMetadata(id) if m == nil { - return nil, nil, fmt.Errorf("no metadata for %s", id) - } - goFiles, err := s.parseGoHandles(ctx, m.GoFiles, mode) - if err != nil { - return nil, nil, err - } - compiledGoFiles, err := s.parseGoHandles(ctx, m.CompiledGoFiles, mode) - if err != nil { - return nil, nil, err - } - ph := &packageHandle{ - m: m, - goFiles: goFiles, - compiledGoFiles: compiledGoFiles, - mode: mode, + return nil, fmt.Errorf("no metadata for %s", id) } - // Make sure all of the depList are sorted. + + // For key stability, sort depList. + // TODO(adonovan): make m.Deps have a well defined order. depList := append([]PackageID{}, m.Deps...) sort.Slice(depList, func(i, j int) bool { return depList[i] < depList[j] }) - deps := make(map[PackagePath]*packageHandle) - // Begin computing the key by getting the depKeys for all dependencies. - var depKeys []packageHandleKey - for _, depID := range depList { + // This requires reading the transitive closure of dependencies' source files. + // + // It is tempting to parallelize the recursion here, but + // without de-duplication of subtasks this would lead to an + // exponential amount of work, and computing the key is + // expensive as it reads all the source files transitively. + // Notably, we don't update the s.packages cache until the + // entire key has been computed. + // TODO(adonovan): use a promise cache to ensure that the key + // for each package is computed by at most one thread, then do + // the recursive key building of dependencies in parallel. + deps := make(map[PackagePath]*packageHandle) + depKeys := make([]packageHandleKey, len(depList)) + for i, depID := range depList { depHandle, err := s.buildPackageHandle(ctx, depID, s.workspaceParseMode(depID)) // Don't use invalid metadata for dependencies if the top-level // metadata is valid. We only load top-level packages, so if the @@ -182,20 +135,90 @@ func (s *snapshot) buildKey(ctx context.Context, id PackageID, mode source.Parse event.Log(ctx, fmt.Sprintf("%s: invalid dep handle for %s", id, depID), tag.Snapshot.Of(s.id)) } + // This check ensures we break out of the slow + // buildPackageHandle recursion quickly when + // context cancelation is detected within GetFile. if ctx.Err() != nil { - return nil, nil, ctx.Err() + return nil, ctx.Err() // cancelled } - // One bad dependency should not prevent us from checking the entire package. - // Add a special key to mark a bad dependency. - depKeys = append(depKeys, packageHandleKey(source.Hashf("%s import not found", depID))) + + // One bad dependency should not prevent us from + // checking the entire package. Leave depKeys[i] unset. continue } + deps[depHandle.m.PkgPath] = depHandle - depKeys = append(depKeys, depHandle.key) + depKeys[i] = depHandle.key + } + + // Read both lists of files of this package, in parallel. + var group errgroup.Group + getFileHandles := func(files []span.URI) []source.FileHandle { + fhs := make([]source.FileHandle, len(files)) + for i, uri := range files { + i, uri := i, uri + group.Go(func() (err error) { + fhs[i], err = s.GetFile(ctx, uri) // ~25us + return + }) + } + return fhs } + goFiles := getFileHandles(m.GoFiles) + compiledGoFiles := getFileHandles(m.CompiledGoFiles) + if err := group.Wait(); err != nil { + return nil, err + } + + // All the file reading has now been done. + // Create a handle for the result of type checking. experimentalKey := s.View().Options().ExperimentalPackageCacheKey - ph.key = checkPackageKey(ph.m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) - return ph, deps, nil + key := computePackageKey(m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) + handle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { + // TODO(adonovan): eliminate use of arg with this handle. + // (In all cases snapshot is equal to the enclosing s.) + snapshot := arg.(*snapshot) + + // Start type checking of direct dependencies, + // in parallel and asynchronously. + // As the type checker imports each of these + // packages, it will wait for its completion. + var wg sync.WaitGroup + for _, dep := range deps { + wg.Add(1) + go func(dep *packageHandle) { + dep.check(ctx, snapshot) // ignore result + wg.Done() + }(dep) + } + // The 'defer' below is unusual but intentional: + // it is not necessary that each call to dep.check + // complete before type checking begins, as the type + // checker will wait for those it needs. But they do + // need to complete before this function returns and + // the snapshot is possibly destroyed. + defer wg.Wait() + + pkg, err := typeCheck(ctx, snapshot, goFiles, compiledGoFiles, m.Metadata, mode, deps) + return &packageData{pkg, err} + }) + + ph := &packageHandle{ + handle: handle, + goFiles: goFiles, + compiledGoFiles: compiledGoFiles, + mode: mode, + m: m, + key: key, + } + + // Cache the handle in the snapshot. If a package handle has already + // been cached, addPackage will return the cached value. This is fine, + // since the original package handle above will have no references and be + // garbage collected. + ph = s.addPackageHandle(ph, release) + + return ph, nil } func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { @@ -214,7 +237,10 @@ func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { return source.ParseExported } -func checkPackageKey(id PackageID, pghs []*parseGoHandle, m *KnownMetadata, deps []packageHandleKey, mode source.ParseMode, experimentalKey bool) packageHandleKey { +// computePackageKey returns a key representing the act of type checking +// a package named id containing the specified files, metadata, and +// dependency hashes. +func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata, deps []packageHandleKey, mode source.ParseMode, experimentalKey bool) packageHandleKey { // TODO(adonovan): opt: no need to materalize the bytes; hash them directly. // Also, use field separators to avoid spurious collisions. b := bytes.NewBuffer(nil) @@ -234,8 +260,8 @@ func checkPackageKey(id PackageID, pghs []*parseGoHandle, m *KnownMetadata, deps for _, dep := range deps { b.Write(dep[:]) } - for _, cgf := range pghs { - b.WriteString(cgf.file.FileIdentity().String()) + for _, file := range files { + b.WriteString(file.FileIdentity().String()) } return packageHandleKey(source.HashOf(b.Bytes())) } @@ -268,10 +294,6 @@ func hashConfig(config *packages.Config) source.Hash { return source.HashOf(b.Bytes()) } -func (ph *packageHandle) Check(ctx context.Context, s source.Snapshot) (source.Package, error) { - return ph.check(ctx, s.(*snapshot)) -} - func (ph *packageHandle) check(ctx context.Context, s *snapshot) (*pkg, error) { v, err := ph.handle.Get(ctx, s.generation, s) if err != nil { @@ -298,24 +320,15 @@ func (ph *packageHandle) cached(g *memoize.Generation) (*pkg, error) { return data.pkg, data.err } -func (s *snapshot) parseGoHandles(ctx context.Context, files []span.URI, mode source.ParseMode) ([]*parseGoHandle, error) { - pghs := make([]*parseGoHandle, 0, len(files)) - for _, uri := range files { - fh, err := s.GetFile(ctx, uri) - if err != nil { - return nil, err - } - pghs = append(pghs, s.parseGoHandle(ctx, fh, mode)) - } - return pghs, nil -} - -func typeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle) (*pkg, error) { +// typeCheck type checks the parsed source files in compiledGoFiles. +// (The resulting pkg also holds the parsed but not type-checked goFiles.) +// deps holds the future results of type-checking the direct dependencies. +func typeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle) (*pkg, error) { var filter *unexportedFilter if mode == source.ParseExported { filter = &unexportedFilter{uses: map[string]bool{}} } - pkg, err := doTypeCheck(ctx, snapshot, m, mode, deps, filter) + pkg, err := doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, filter) if err != nil { return nil, err } @@ -327,7 +340,7 @@ func typeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode source missing, unexpected := filter.ProcessErrors(pkg.typeErrors) if len(unexpected) == 0 && len(missing) != 0 { event.Log(ctx, fmt.Sprintf("discovered missing identifiers: %v", missing), tag.Package.Of(string(m.ID))) - pkg, err = doTypeCheck(ctx, snapshot, m, mode, deps, filter) + pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, filter) if err != nil { return nil, err } @@ -335,7 +348,7 @@ func typeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode source } if len(unexpected) != 0 || len(missing) != 0 { event.Log(ctx, fmt.Sprintf("falling back to safe trimming due to type errors: %v or still-missing identifiers: %v", unexpected, missing), tag.Package.Of(string(m.ID))) - pkg, err = doTypeCheck(ctx, snapshot, m, mode, deps, nil) + pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, nil) if err != nil { return nil, err } @@ -427,7 +440,7 @@ func typeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode source var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) -func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { +func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { ctx, done := event.Start(ctx, "cache.typeCheck", tag.Package.Of(string(m.ID))) defer done() @@ -448,19 +461,19 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour } typeparams.InitInstanceInfo(pkg.typesInfo) - for _, gf := range pkg.m.GoFiles { - // In the presence of line directives, we may need to report errors in - // non-compiled Go files, so we need to register them on the package. - // However, we only need to really parse them in ParseFull mode, when - // the user might actually be looking at the file. - fh, err := snapshot.GetFile(ctx, gf) - if err != nil { - return nil, err - } - goMode := source.ParseFull - if mode != source.ParseFull { - goMode = source.ParseHeader - } + // In the presence of line directives, we may need to report errors in + // non-compiled Go files, so we need to register them on the package. + // However, we only need to really parse them in ParseFull mode, when + // the user might actually be looking at the file. + goMode := source.ParseFull + if mode != source.ParseFull { + goMode = source.ParseHeader + } + + // Parse the GoFiles. (These aren't presented to the type + // checker but are part of the returned pkg.) + // TODO(adonovan): opt: parallelize parsing. + for _, fh := range goFiles { pgf, err := snapshot.ParseGo(ctx, fh, goMode) if err != nil { return nil, err @@ -468,7 +481,8 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour pkg.goFiles = append(pkg.goFiles, pgf) } - if err := parseCompiledGoFiles(ctx, snapshot, mode, pkg, astFilter); err != nil { + // Parse the CompiledGoFiles: those seen by the compiler/typechecker. + if err := parseCompiledGoFiles(ctx, compiledGoFiles, snapshot, mode, pkg, astFilter); err != nil { return nil, err } @@ -549,7 +563,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour } // Type checking errors are handled via the config, so ignore them here. - _ = check.Files(files) + _ = check.Files(files) // 50us-15ms, depending on size of package // If the context was cancelled, we may have returned a ton of transient // errors to the type checker. Swallow them. @@ -559,21 +573,18 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour return pkg, nil } -func parseCompiledGoFiles(ctx context.Context, snapshot *snapshot, mode source.ParseMode, pkg *pkg, astFilter *unexportedFilter) error { - for _, cgf := range pkg.m.CompiledGoFiles { - fh, err := snapshot.GetFile(ctx, cgf) - if err != nil { - return err - } - +func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHandle, snapshot *snapshot, mode source.ParseMode, pkg *pkg, astFilter *unexportedFilter) error { + // TODO(adonovan): opt: parallelize this loop, which takes 1-25ms. + for _, fh := range compiledGoFiles { var pgf *source.ParsedGoFile var fixed bool + var err error // Only parse Full through the cache -- we need to own Exported ASTs // to prune them. if mode == source.ParseFull { pgf, fixed, err = snapshot.parseGo(ctx, fh, mode) } else { - d := parseGo(ctx, snapshot.FileSet(), fh, mode) + d := parseGo(ctx, snapshot.FileSet(), fh, mode) // ~20us/KB pgf, fixed, err = d.parsed, d.fixed, d.err } if err != nil { diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index 0f36249e057..b81caabde5f 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -252,8 +252,8 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc if len(missingImports) == 0 { continue } - for _, pgh := range ph.compiledGoFiles { - pgf, err := snapshot.ParseGo(ctx, pgh.file, source.ParseHeader) + for _, goFile := range ph.compiledGoFiles { + pgf, err := snapshot.ParseGo(ctx, goFile, source.ParseHeader) if err != nil { continue } diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go index f27178e2e7f..50d7b123ec9 100644 --- a/internal/lsp/cache/symbols.go +++ b/internal/lsp/cache/symbols.go @@ -29,7 +29,8 @@ type symbolData struct { err error } -// buildSymbolHandle returns a handle to the result of symbolizing a file, +// buildSymbolHandle returns a handle to the future result of +// symbolizing the file identified by fh, // if necessary creating it and saving it in the snapshot. func (s *snapshot) buildSymbolHandle(ctx context.Context, fh source.FileHandle) *symbolHandle { if h := s.getSymbolHandle(fh.URI()); h != nil { From 698251aaa532d49ac69d2c416b0241afb2f65ea5 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 1 Jul 2022 13:10:44 -0400 Subject: [PATCH 076/723] internal/lsp/cache: sort Metadata.Deps, for determinism ...so that each client doesn't have to. Change-Id: I039c493031c5c90c4479741cf6f7572dad480808 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415502 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley Auto-Submit: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/lsp/cache/analysis.go | 8 +------- internal/lsp/cache/check.go | 11 ++--------- internal/lsp/cache/load.go | 1 + internal/lsp/cache/metadata.go | 2 +- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index 847ac2db8d0..db2ca2a8b09 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -10,7 +10,6 @@ import ( "go/ast" "go/types" "reflect" - "sort" "sync" "golang.org/x/sync/errgroup" @@ -122,13 +121,8 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A // An analysis that consumes/produces facts // must run on the package's dependencies too. if len(a.FactTypes) > 0 { - importIDs := make([]string, 0, len(ph.m.Deps)) for _, importID := range ph.m.Deps { - importIDs = append(importIDs, string(importID)) - } - sort.Strings(importIDs) // for determinism - for _, importID := range importIDs { - depActionHandle, err := s.actionHandle(ctx, PackageID(importID), a) + depActionHandle, err := s.actionHandle(ctx, importID, a) if err != nil { return nil, err } diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index aeb45635c3b..5ded278f514 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -102,13 +102,6 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so return nil, fmt.Errorf("no metadata for %s", id) } - // For key stability, sort depList. - // TODO(adonovan): make m.Deps have a well defined order. - depList := append([]PackageID{}, m.Deps...) - sort.Slice(depList, func(i, j int) bool { - return depList[i] < depList[j] - }) - // Begin computing the key by getting the depKeys for all dependencies. // This requires reading the transitive closure of dependencies' source files. // @@ -122,8 +115,8 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // for each package is computed by at most one thread, then do // the recursive key building of dependencies in parallel. deps := make(map[PackagePath]*packageHandle) - depKeys := make([]packageHandleKey, len(depList)) - for i, depID := range depList { + depKeys := make([]packageHandleKey, len(m.Deps)) + for i, depID := range m.Deps { depHandle, err := s.buildPackageHandle(ctx, depID, s.workspaceParseMode(depID)) // Don't use invalid metadata for dependencies if the top-level // metadata is valid. We only load top-level packages, so if the diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index d613dc3337e..00d4ab2cbb3 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -539,6 +539,7 @@ func computeMetadataUpdates(ctx context.Context, g *metadataGraph, pkgPath Packa event.Error(ctx, "error in dependency", err) } } + sort.Slice(m.Deps, func(i, j int) bool { return m.Deps[i] < m.Deps[j] }) // for determinism return nil } diff --git a/internal/lsp/cache/metadata.go b/internal/lsp/cache/metadata.go index b4da7130c23..486035f9390 100644 --- a/internal/lsp/cache/metadata.go +++ b/internal/lsp/cache/metadata.go @@ -32,7 +32,7 @@ type Metadata struct { ForTest PackagePath TypesSizes types.Sizes Errors []packages.Error - Deps []PackageID + Deps []PackageID // direct dependencies, in string order MissingDeps map[PackagePath]struct{} Module *packages.Module depsErrors []*packagesinternal.PackageError From afa4a9562fc7af3f6dfff75f7c0ac8106870d51e Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Wed, 15 Jun 2022 02:28:52 +0000 Subject: [PATCH 077/723] internal/lsp/cache: persist known subdirs This on average reduces latency from 12ms to 4ms on internal codebase. Updates golang/go#45686 Change-Id: Id376fcd97ce375210f2ad8b88e42f6ca283d29d3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413657 Reviewed-by: Robert Findley Reviewed-by: Alan Donovan --- internal/lsp/cache/maps.go | 45 +++++++++++++++++++++++++++++ internal/lsp/cache/session.go | 18 +++++++----- internal/lsp/cache/snapshot.go | 51 ++++++++++++++++----------------- internal/persistent/map.go | 11 +++++++ internal/persistent/map_test.go | 33 +++++++++++++++++++++ 5 files changed, 125 insertions(+), 33 deletions(-) diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go index 14026abd92b..f2958b0e121 100644 --- a/internal/lsp/cache/maps.go +++ b/internal/lsp/cache/maps.go @@ -206,3 +206,48 @@ func (m packagesMap) Set(key packageKey, value *packageHandle, release func()) { func (m packagesMap) Delete(key packageKey) { m.impl.Delete(key) } + +type knownDirsSet struct { + impl *persistent.Map +} + +func newKnownDirsSet() knownDirsSet { + return knownDirsSet{ + impl: persistent.NewMap(func(a, b interface{}) bool { + return a.(span.URI) < b.(span.URI) + }), + } +} + +func (s knownDirsSet) Clone() knownDirsSet { + return knownDirsSet{ + impl: s.impl.Clone(), + } +} + +func (s knownDirsSet) Destroy() { + s.impl.Destroy() +} + +func (s knownDirsSet) Contains(key span.URI) bool { + _, ok := s.impl.Get(key) + return ok +} + +func (s knownDirsSet) Range(do func(key span.URI)) { + s.impl.Range(func(key, value interface{}) { + do(key.(span.URI)) + }) +} + +func (s knownDirsSet) SetAll(other knownDirsSet) { + s.impl.SetAll(other.impl) +} + +func (s knownDirsSet) Insert(key span.URI) { + s.impl.Set(key, nil, nil) +} + +func (s knownDirsSet) Remove(key span.URI) { + s.impl.Delete(key) +} diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 8d8e63f13e8..52b141a78d7 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -244,6 +244,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, parseWorkHandles: make(map[span.URI]*parseWorkHandle), modTidyHandles: make(map[span.URI]*modTidyHandle), modWhyHandles: make(map[span.URI]*modWhyHandle), + knownSubdirs: newKnownDirsSet(), workspace: workspace, } @@ -537,9 +538,11 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes snapshots = append(snapshots, snapshot) } knownDirs := knownDirectories(ctx, snapshots) + defer knownDirs.Destroy() + var result []source.FileModification for _, c := range changes { - if _, ok := knownDirs[c.URI]; !ok { + if !knownDirs.Contains(c.URI) { result = append(result, c) continue } @@ -561,16 +564,17 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes // knownDirectories returns all of the directories known to the given // snapshots, including workspace directories and their subdirectories. -func knownDirectories(ctx context.Context, snapshots []*snapshot) map[span.URI]struct{} { - result := map[span.URI]struct{}{} +// It is responsibility of the caller to destroy the returned set. +func knownDirectories(ctx context.Context, snapshots []*snapshot) knownDirsSet { + result := newKnownDirsSet() for _, snapshot := range snapshots { dirs := snapshot.workspace.dirs(ctx, snapshot) for _, dir := range dirs { - result[dir] = struct{}{} - } - for _, dir := range snapshot.getKnownSubdirs(dirs) { - result[dir] = struct{}{} + result.Insert(dir) } + knownSubdirs := snapshot.getKnownSubdirs(dirs) + result.SetAll(knownSubdirs) + knownSubdirs.Destroy() } return result } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 259345bdc8f..85232ea8357 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -123,7 +123,7 @@ type snapshot struct { // knownSubdirs is the set of subdirectories in the workspace, used to // create glob patterns for file watching. - knownSubdirs map[span.URI]struct{} + knownSubdirs knownDirsSet knownSubdirsPatternCache string // unprocessedSubdirChanges are any changes that might affect the set of // subdirectories in the workspace. They are not reflected to knownSubdirs @@ -147,6 +147,7 @@ func (s *snapshot) Destroy(destroyedBy string) { s.files.Destroy() s.goFiles.Destroy() s.parseKeysByURI.Destroy() + s.knownSubdirs.Destroy() if s.workspaceDir != "" { if err := os.RemoveAll(s.workspaceDir); err != nil { @@ -842,17 +843,20 @@ func (s *snapshot) getKnownSubdirsPattern(wsDirs []span.URI) string { // It may change list of known subdirs and therefore invalidate the cache. s.applyKnownSubdirsChangesLocked(wsDirs) - if len(s.knownSubdirs) == 0 { - return "" - } - if s.knownSubdirsPatternCache == "" { - dirNames := make([]string, 0, len(s.knownSubdirs)) - for uri := range s.knownSubdirs { - dirNames = append(dirNames, uri.Filename()) + var builder strings.Builder + s.knownSubdirs.Range(func(uri span.URI) { + if builder.Len() == 0 { + builder.WriteString("{") + } else { + builder.WriteString(",") + } + builder.WriteString(uri.Filename()) + }) + if builder.Len() > 0 { + builder.WriteString("}") + s.knownSubdirsPatternCache = builder.String() } - sort.Strings(dirNames) - s.knownSubdirsPatternCache = fmt.Sprintf("{%s}", strings.Join(dirNames, ",")) } return s.knownSubdirsPatternCache @@ -867,14 +871,15 @@ func (s *snapshot) collectAllKnownSubdirs(ctx context.Context) { s.mu.Lock() defer s.mu.Unlock() - s.knownSubdirs = map[span.URI]struct{}{} + s.knownSubdirs.Destroy() + s.knownSubdirs = newKnownDirsSet() s.knownSubdirsPatternCache = "" s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { s.addKnownSubdirLocked(uri, dirs) }) } -func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) []span.URI { +func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) knownDirsSet { s.mu.Lock() defer s.mu.Unlock() @@ -882,11 +887,7 @@ func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) []span.URI { // subdirectories. s.applyKnownSubdirsChangesLocked(wsDirs) - result := make([]span.URI, 0, len(s.knownSubdirs)) - for uri := range s.knownSubdirs { - result = append(result, uri) - } - return result + return s.knownSubdirs.Clone() } func (s *snapshot) applyKnownSubdirsChangesLocked(wsDirs []span.URI) { @@ -907,7 +908,7 @@ func (s *snapshot) addKnownSubdirLocked(uri span.URI, dirs []span.URI) { dir := filepath.Dir(uri.Filename()) // First check if the directory is already known, because then we can // return early. - if _, ok := s.knownSubdirs[span.URIFromPath(dir)]; ok { + if s.knownSubdirs.Contains(span.URIFromPath(dir)) { return } var matched span.URI @@ -926,10 +927,10 @@ func (s *snapshot) addKnownSubdirLocked(uri span.URI, dirs []span.URI) { break } uri := span.URIFromPath(dir) - if _, ok := s.knownSubdirs[uri]; ok { + if s.knownSubdirs.Contains(uri) { break } - s.knownSubdirs[uri] = struct{}{} + s.knownSubdirs.Insert(uri) dir = filepath.Dir(dir) s.knownSubdirsPatternCache = "" } @@ -939,11 +940,11 @@ func (s *snapshot) removeKnownSubdirLocked(uri span.URI) { dir := filepath.Dir(uri.Filename()) for dir != "" { uri := span.URIFromPath(dir) - if _, ok := s.knownSubdirs[uri]; !ok { + if !s.knownSubdirs.Contains(uri) { break } if info, _ := os.Stat(dir); info == nil { - delete(s.knownSubdirs, uri) + s.knownSubdirs.Remove(uri) s.knownSubdirsPatternCache = "" } dir = filepath.Dir(dir) @@ -1714,7 +1715,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC parseWorkHandles: make(map[span.URI]*parseWorkHandle, len(s.parseWorkHandles)), modTidyHandles: make(map[span.URI]*modTidyHandle, len(s.modTidyHandles)), modWhyHandles: make(map[span.URI]*modWhyHandle, len(s.modWhyHandles)), - knownSubdirs: make(map[span.URI]struct{}, len(s.knownSubdirs)), + knownSubdirs: s.knownSubdirs.Clone(), workspace: newWorkspace, } @@ -1771,9 +1772,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Add all of the known subdirectories, but don't update them for the // changed files. We need to rebuild the workspace module to know the // true set of known subdirectories, but we don't want to do that in clone. - for k, v := range s.knownSubdirs { - result.knownSubdirs[k] = v - } + result.knownSubdirs = s.knownSubdirs.Clone() result.knownSubdirsPatternCache = s.knownSubdirsPatternCache for _, c := range changes { result.unprocessedSubdirChanges = append(result.unprocessedSubdirChanges, c) diff --git a/internal/persistent/map.go b/internal/persistent/map.go index 19b50480db4..55b7065e9f7 100644 --- a/internal/persistent/map.go +++ b/internal/persistent/map.go @@ -28,6 +28,8 @@ import ( // client-provided function that implements a strict weak order. // // Maps can be Cloned in constant time. +// Get, Store, and Delete operations are done on average in logarithmic time. +// Maps can be Updated in O(m log(n/m)) time for maps of size n and m, where m < n. // // Values are reference counted, and a client-supplied release function // is called when a value is no longer referenced by a map or any clone. @@ -156,6 +158,15 @@ func (pm *Map) Get(key interface{}) (interface{}, bool) { return nil, false } +// SetAll updates the map with key/value pairs from the other map, overwriting existing keys. +// It is equivalent to calling Set for each entry in the other map but is more efficient. +// Both maps must have the same comparison function, otherwise behavior is undefined. +func (pm *Map) SetAll(other *Map) { + root := pm.root + pm.root = union(root, other.root, pm.less, true) + root.decref() +} + // Set updates the value associated with the specified key. // If release is non-nil, it will be called with entry's key and value once the // key is no longer contained in the map or any clone. diff --git a/internal/persistent/map_test.go b/internal/persistent/map_test.go index bd2cbfa0e12..1c413d78fa7 100644 --- a/internal/persistent/map_test.go +++ b/internal/persistent/map_test.go @@ -151,6 +151,31 @@ func TestRandomMap(t *testing.T) { assertSameMap(t, seenEntries, deletedEntries) } +func TestUpdate(t *testing.T) { + deletedEntries := make(map[mapEntry]struct{}) + seenEntries := make(map[mapEntry]struct{}) + + m1 := &validatedMap{ + impl: NewMap(func(a, b interface{}) bool { + return a.(int) < b.(int) + }), + expected: make(map[int]int), + deleted: deletedEntries, + seen: seenEntries, + } + m2 := m1.clone() + + m1.set(t, 1, 1) + m1.set(t, 2, 2) + m2.set(t, 2, 20) + m2.set(t, 3, 3) + m1.setAll(t, m2) + + m1.destroy() + m2.destroy() + assertSameMap(t, seenEntries, deletedEntries) +} + func (vm *validatedMap) onDelete(t *testing.T, key, value int) { entry := mapEntry{key: key, value: value} if _, ok := vm.deleted[entry]; ok { @@ -254,6 +279,14 @@ func validateNode(t *testing.T, node *mapNode, less func(a, b interface{}) bool) validateNode(t, node.right, less) } +func (vm *validatedMap) setAll(t *testing.T, other *validatedMap) { + vm.impl.SetAll(other.impl) + for key, value := range other.expected { + vm.expected[key] = value + } + vm.validate(t) +} + func (vm *validatedMap) set(t *testing.T, key, value int) { vm.seen[mapEntry{key: key, value: value}] = struct{}{} vm.impl.Set(key, value, func(deletedKey, deletedValue interface{}) { From d69bac6d882444114a07779657ae97128a4fac76 Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Wed, 22 Jun 2022 22:39:54 +0000 Subject: [PATCH 078/723] internal/lsp/cache: cache isActiveLocked calculation across snapshots This speeds up workspace initialization time in DegradeClosed memory mode from 3m to 1m by avoiding unnecessary recomputation of results. Change-Id: Ie5df82952d50ab42125defd148136329f0d50a48 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413658 Reviewed-by: Alan Donovan Reviewed-by: David Chase --- internal/lsp/cache/check.go | 2 +- internal/lsp/cache/load.go | 2 +- internal/lsp/cache/maps.go | 34 +++++++++++++++ internal/lsp/cache/session.go | 43 +++++++++--------- internal/lsp/cache/snapshot.go | 79 ++++++++++++++++++++-------------- 5 files changed, 104 insertions(+), 56 deletions(-) diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index 5ded278f514..7ebc777d762 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -224,7 +224,7 @@ func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { if s.view.Options().MemoryMode == source.ModeNormal { return source.ParseFull } - if s.isActiveLocked(id, nil) { + if s.isActiveLocked(id) { return source.ParseFull } return source.ParseExported diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 00d4ab2cbb3..e4fd6719872 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -220,7 +220,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf // invalidate the reverse transitive closure of packages that have changed. invalidatedPackages := s.meta.reverseTransitiveClosure(true, loadedIDs...) s.meta = s.meta.Clone(updates) - + s.resetIsActivePackageLocked() // Invalidate any packages we may have associated with this metadata. // // TODO(rfindley): this should not be necessary, as we should have already diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go index f2958b0e121..af041777188 100644 --- a/internal/lsp/cache/maps.go +++ b/internal/lsp/cache/maps.go @@ -112,6 +112,40 @@ func (m goFilesMap) Delete(key parseKey) { m.impl.Delete(key) } +type isActivePackageCacheMap struct { + impl *persistent.Map +} + +func newIsActivePackageCacheMap() isActivePackageCacheMap { + return isActivePackageCacheMap{ + impl: persistent.NewMap(func(a, b interface{}) bool { + return a.(PackageID) < b.(PackageID) + }), + } +} + +func (m isActivePackageCacheMap) Clone() isActivePackageCacheMap { + return isActivePackageCacheMap{ + impl: m.impl.Clone(), + } +} + +func (m isActivePackageCacheMap) Destroy() { + m.impl.Destroy() +} + +func (m isActivePackageCacheMap) Get(key PackageID) (bool, bool) { + value, ok := m.impl.Get(key) + if !ok { + return false, false + } + return value.(bool), true +} + +func (m isActivePackageCacheMap) Set(key PackageID, value bool) { + m.impl.Set(key, value, nil) +} + type parseKeysByURIMap struct { impl *persistent.Map } diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 52b141a78d7..aca46dd2a88 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -225,27 +225,28 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, }, } v.snapshot = &snapshot{ - id: snapshotID, - view: v, - backgroundCtx: backgroundCtx, - cancel: cancel, - initializeOnce: &sync.Once{}, - generation: s.cache.store.Generation(generationName(v, 0)), - packages: newPackagesMap(), - meta: &metadataGraph{}, - files: newFilesMap(), - goFiles: newGoFilesMap(), - parseKeysByURI: newParseKeysByURIMap(), - symbols: make(map[span.URI]*symbolHandle), - actions: make(map[actionKey]*actionHandle), - workspacePackages: make(map[PackageID]PackagePath), - unloadableFiles: make(map[span.URI]struct{}), - parseModHandles: make(map[span.URI]*parseModHandle), - parseWorkHandles: make(map[span.URI]*parseWorkHandle), - modTidyHandles: make(map[span.URI]*modTidyHandle), - modWhyHandles: make(map[span.URI]*modWhyHandle), - knownSubdirs: newKnownDirsSet(), - workspace: workspace, + id: snapshotID, + view: v, + backgroundCtx: backgroundCtx, + cancel: cancel, + initializeOnce: &sync.Once{}, + generation: s.cache.store.Generation(generationName(v, 0)), + packages: newPackagesMap(), + meta: &metadataGraph{}, + files: newFilesMap(), + isActivePackageCache: newIsActivePackageCacheMap(), + goFiles: newGoFilesMap(), + parseKeysByURI: newParseKeysByURIMap(), + symbols: make(map[span.URI]*symbolHandle), + actions: make(map[actionKey]*actionHandle), + workspacePackages: make(map[PackageID]PackagePath), + unloadableFiles: make(map[span.URI]struct{}), + parseModHandles: make(map[span.URI]*parseModHandle), + parseWorkHandles: make(map[span.URI]*parseWorkHandle), + modTidyHandles: make(map[span.URI]*modTidyHandle), + modWhyHandles: make(map[span.URI]*modWhyHandle), + knownSubdirs: newKnownDirsSet(), + workspace: workspace, } // Initialize the view without blocking. diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 85232ea8357..36dcafeaca6 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -91,6 +91,10 @@ type snapshot struct { // It may be invalidated when a file's content changes. packages packagesMap + // isActivePackageCache maps package ID to the cached value if it is active or not. + // It may be invalidated when metadata changes or a new file is opened or closed. + isActivePackageCache isActivePackageCacheMap + // actions maps an actionkey to its actionHandle. actions map[actionKey]*actionHandle @@ -144,6 +148,7 @@ type actionKey struct { func (s *snapshot) Destroy(destroyedBy string) { s.generation.Destroy(destroyedBy) s.packages.Destroy() + s.isActivePackageCache.Destroy() s.files.Destroy() s.goFiles.Destroy() s.parseKeysByURI.Destroy() @@ -754,24 +759,20 @@ func (s *snapshot) activePackageIDs() (ids []PackageID) { s.mu.Lock() defer s.mu.Unlock() - seen := make(map[PackageID]bool) for id := range s.workspacePackages { - if s.isActiveLocked(id, seen) { + if s.isActiveLocked(id) { ids = append(ids, id) } } return ids } -func (s *snapshot) isActiveLocked(id PackageID, seen map[PackageID]bool) (active bool) { - if seen == nil { - seen = make(map[PackageID]bool) - } - if seen, ok := seen[id]; ok { +func (s *snapshot) isActiveLocked(id PackageID) (active bool) { + if seen, ok := s.isActivePackageCache.Get(id); ok { return seen } defer func() { - seen[id] = active + s.isActivePackageCache.Set(id, active) }() m, ok := s.meta.metadata[id] if !ok { @@ -785,13 +786,18 @@ func (s *snapshot) isActiveLocked(id PackageID, seen map[PackageID]bool) (active // TODO(rfindley): it looks incorrect that we don't also check GoFiles here. // If a CGo file is open, we want to consider the package active. for _, dep := range m.Deps { - if s.isActiveLocked(dep, seen) { + if s.isActiveLocked(dep) { return true } } return false } +func (s *snapshot) resetIsActivePackageLocked() { + s.isActivePackageCache.Destroy() + s.isActivePackageCache = newIsActivePackageCacheMap() +} + const fileExtensions = "go,mod,sum,work" func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]struct{} { @@ -1287,6 +1293,7 @@ func (s *snapshot) clearShouldLoad(scopes ...interface{}) { } } s.meta = g.Clone(updates) + s.resetIsActivePackageLocked() } // noValidMetadataForURILocked reports whether there is any valid metadata for @@ -1377,7 +1384,7 @@ func (s *snapshot) openFiles() []source.VersionedFileHandle { var open []source.VersionedFileHandle s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { - if s.isOpenLocked(fh.URI()) { + if isFileOpen(fh) { open = append(open, fh) } }) @@ -1386,6 +1393,10 @@ func (s *snapshot) openFiles() []source.VersionedFileHandle { func (s *snapshot) isOpenLocked(uri span.URI) bool { fh, _ := s.files.Get(uri) + return isFileOpen(fh) +} + +func isFileOpen(fh source.VersionedFileHandle) bool { _, open := fh.(*overlay) return open } @@ -1695,28 +1706,29 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1)) bgCtx, cancel := context.WithCancel(bgCtx) result := &snapshot{ - id: s.id + 1, - generation: newGen, - view: s.view, - backgroundCtx: bgCtx, - cancel: cancel, - builtin: s.builtin, - initializeOnce: s.initializeOnce, - initializedErr: s.initializedErr, - packages: s.packages.Clone(), - actions: make(map[actionKey]*actionHandle, len(s.actions)), - files: s.files.Clone(), - goFiles: s.goFiles.Clone(), - parseKeysByURI: s.parseKeysByURI.Clone(), - symbols: make(map[span.URI]*symbolHandle, len(s.symbols)), - workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)), - unloadableFiles: make(map[span.URI]struct{}, len(s.unloadableFiles)), - parseModHandles: make(map[span.URI]*parseModHandle, len(s.parseModHandles)), - parseWorkHandles: make(map[span.URI]*parseWorkHandle, len(s.parseWorkHandles)), - modTidyHandles: make(map[span.URI]*modTidyHandle, len(s.modTidyHandles)), - modWhyHandles: make(map[span.URI]*modWhyHandle, len(s.modWhyHandles)), - knownSubdirs: s.knownSubdirs.Clone(), - workspace: newWorkspace, + id: s.id + 1, + generation: newGen, + view: s.view, + backgroundCtx: bgCtx, + cancel: cancel, + builtin: s.builtin, + initializeOnce: s.initializeOnce, + initializedErr: s.initializedErr, + packages: s.packages.Clone(), + isActivePackageCache: s.isActivePackageCache.Clone(), + actions: make(map[actionKey]*actionHandle, len(s.actions)), + files: s.files.Clone(), + goFiles: s.goFiles.Clone(), + parseKeysByURI: s.parseKeysByURI.Clone(), + symbols: make(map[span.URI]*symbolHandle, len(s.symbols)), + workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)), + unloadableFiles: make(map[span.URI]struct{}, len(s.unloadableFiles)), + parseModHandles: make(map[span.URI]*parseModHandle, len(s.parseModHandles)), + parseWorkHandles: make(map[span.URI]*parseWorkHandle, len(s.parseWorkHandles)), + modTidyHandles: make(map[span.URI]*modTidyHandle, len(s.modTidyHandles)), + modWhyHandles: make(map[span.URI]*modWhyHandle, len(s.modWhyHandles)), + knownSubdirs: s.knownSubdirs.Clone(), + workspace: newWorkspace, } // Copy all of the FileHandles. @@ -1975,9 +1987,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC result.meta = s.meta } - // Update workspace packages, if necessary. + // Update workspace and active packages, if necessary. if result.meta != s.meta || anyFileOpenedOrClosed { result.workspacePackages = computeWorkspacePackagesLocked(result, result.meta) + result.resetIsActivePackageLocked() } else { result.workspacePackages = s.workspacePackages } From b929f3bf4d57d9023b392ff552aa21a4845e1a07 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 1 Jun 2022 15:20:56 -0400 Subject: [PATCH 079/723] internal/span: make NewRange accept File, not FileSet span.NewRange now accepts a *token.File and two token.Pos. It is the caller's responsibility to look up the File in the FileSet, if necessary (it usually isn't), and to ensure the Pos values are valid. Ditto NewMappedRange. This reduces the creep of Snapshot into functions that have no need to know about it. Also the bug.Report call in NewRange has been pushed up into the caller and logically eliminated in all but one case. I think we should aim for the invariant that functions that operate on a single file should accept a *token.File, not a FileSet; only functions that operate on sets of files (e.g. type checking, analysis) should use a FileSet. This is not always possible: some public functions accept a FileSet only to re-lookup a single file already known to the caller; if necessary we could provide token.File variants of these. This may ultimately allow us to create a new FileSet per call to the parser, so that Files and FileSets are in 1:1 correspondance and there is no global FileSet. (It currently grows without bound, on the order of kilobytes per keystroke.) FileSets containing multiple files, needed when we interact with the type checker and analysis, may be temporarily synthesized on demand from a set of Files, analogous to mmap'ing a few files into a blank address space. Also: - replace File.Position(pos).Line by File.Line(pos) - replace pos == token.NoPos by pos.IsValid() - avoid fishy token.Pos conversions in link.go - other minor simplifications Change-Id: Ia3119e0ac7e193801fbafa81c8f48acfa14e9ae4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/409935 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- go/analysis/analysistest/analysistest.go | 2 +- go/analysis/passes/tests/tests.go | 2 + go/packages/packagestest/expect.go | 16 +++---- internal/lsp/cache/check.go | 2 +- internal/lsp/cache/errors.go | 28 ++++++++--- internal/lsp/cache/load.go | 2 +- internal/lsp/cache/mod_tidy.go | 7 +-- internal/lsp/code_action.go | 2 +- internal/lsp/diagnostics.go | 2 +- internal/lsp/link.go | 27 +++++++---- internal/lsp/semantic.go | 2 +- internal/lsp/source/call_hierarchy.go | 20 +++++--- internal/lsp/source/code_lens.go | 10 ++-- internal/lsp/source/completion/completion.go | 16 ++++--- internal/lsp/source/completion/definition.go | 4 +- internal/lsp/source/completion/package.go | 12 ++--- internal/lsp/source/completion/util.go | 2 +- internal/lsp/source/extract.go | 23 +++++---- internal/lsp/source/fix.go | 11 ++++- internal/lsp/source/folding_range.go | 38 +++++++-------- internal/lsp/source/identifier.go | 4 +- internal/lsp/source/references.go | 9 ++-- internal/lsp/source/rename.go | 31 ++++--------- internal/lsp/source/rename_check.go | 19 ++++---- internal/lsp/source/util.go | 49 +++++++++++--------- internal/lsp/source/util_test.go | 2 +- internal/span/token.go | 42 +++++++++++------ 27 files changed, 217 insertions(+), 167 deletions(-) diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index 6ef2e7984fa..ea67807f78c 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -142,7 +142,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns } fileContents[file] = contents } - spn, err := span.NewRange(act.Pass.Fset, edit.Pos, edit.End).Span() + spn, err := span.NewRange(file, edit.Pos, edit.End).Span() if err != nil { t.Errorf("error converting edit to span %s: %v", file.Name(), err) } diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go index 56b20ebd519..cab2fa20fa5 100644 --- a/go/analysis/passes/tests/tests.go +++ b/go/analysis/passes/tests/tests.go @@ -475,10 +475,12 @@ func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) { if tparams := typeparams.ForFuncType(fn.Type); tparams != nil && len(tparams.List) > 0 { // Note: cmd/go/internal/load also errors about TestXXX and BenchmarkXXX functions with type parameters. // We have currently decided to also warn before compilation/package loading. This can help users in IDEs. + // TODO(adonovan): use ReportRangef(tparams). pass.Reportf(fn.Pos(), "%s has type parameters: it will not be run by go test as a %sXXX function", fn.Name.Name, prefix) } if !isTestSuffix(fn.Name.Name[len(prefix):]) { + // TODO(adonovan): use ReportRangef(fn.Name). pass.Reportf(fn.Pos(), "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix) } } diff --git a/go/packages/packagestest/expect.go b/go/packages/packagestest/expect.go index 430258681f5..841099c0cdc 100644 --- a/go/packages/packagestest/expect.go +++ b/go/packages/packagestest/expect.go @@ -409,6 +409,7 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (span.Range, []interface{}, error) { + tokFile := e.ExpectFileSet.File(n.Pos) if len(args) < 1 { return span.Range{}, nil, fmt.Errorf("missing argument") } @@ -419,10 +420,9 @@ func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (span.Rang // handle the special identifiers switch arg { case eofIdentifier: - // end of file identifier, look up the current file - f := e.ExpectFileSet.File(n.Pos) - eof := f.Pos(f.Size()) - return span.NewRange(e.ExpectFileSet, eof, token.NoPos), args, nil + // end of file identifier + eof := tokFile.Pos(tokFile.Size()) + return span.NewRange(tokFile, eof, eof), args, nil default: // look up an marker by name mark, ok := e.markers[string(arg)] @@ -436,19 +436,19 @@ func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (span.Rang if err != nil { return span.Range{}, nil, err } - if start == token.NoPos { + if !start.IsValid() { return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) } - return span.NewRange(e.ExpectFileSet, start, end), args, nil + return span.NewRange(tokFile, start, end), args, nil case *regexp.Regexp: start, end, err := expect.MatchBefore(e.ExpectFileSet, e.FileContents, n.Pos, arg) if err != nil { return span.Range{}, nil, err } - if start == token.NoPos { + if !start.IsValid() { return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) } - return span.NewRange(e.ExpectFileSet, start, end), args, nil + return span.NewRange(tokFile, start, end), args, nil default: return span.Range{}, nil, fmt.Errorf("cannot convert %v to pos", arg) } diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index 7ebc777d762..aae6de0eea7 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -664,7 +664,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost } for _, imp := range allImports[item] { - rng, err := source.NewMappedRange(s.FileSet(), imp.cgf.Mapper, imp.imp.Pos(), imp.imp.End()).Range() + rng, err := source.NewMappedRange(imp.cgf.Tok, imp.cgf.Mapper, imp.imp.Pos(), imp.imp.End()).Range() if err != nil { return nil, err } diff --git a/internal/lsp/cache/errors.go b/internal/lsp/cache/errors.go index 342f2bea5d7..a1aecb35c7b 100644 --- a/internal/lsp/cache/errors.go +++ b/internal/lsp/cache/errors.go @@ -16,6 +16,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" @@ -82,7 +83,7 @@ func parseErrorDiagnostics(snapshot *snapshot, pkg *pkg, errList scanner.ErrorLi return nil, err } pos := pgf.Tok.Pos(e.Pos.Offset) - spn, err := span.NewRange(snapshot.FileSet(), pos, pos).Span() + spn, err := span.NewRange(pgf.Tok, pos, pos).Span() if err != nil { return nil, err } @@ -196,8 +197,15 @@ func analysisDiagnosticDiagnostics(snapshot *snapshot, pkg *pkg, a *analysis.Ana break } } - - spn, err := span.NewRange(snapshot.FileSet(), e.Pos, e.End).Span() + tokFile := snapshot.FileSet().File(e.Pos) + if tokFile == nil { + return nil, bug.Errorf("no file for position of %q diagnostic", e.Category) + } + end := e.End + if !end.IsValid() { + end = e.Pos + } + spn, err := span.NewRange(tokFile, e.Pos, end).Span() if err != nil { return nil, err } @@ -282,7 +290,11 @@ func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnos for _, fix := range diag.SuggestedFixes { edits := make(map[span.URI][]protocol.TextEdit) for _, e := range fix.TextEdits { - spn, err := span.NewRange(snapshot.FileSet(), e.Pos, e.End).Span() + tokFile := snapshot.FileSet().File(e.Pos) + if tokFile == nil { + return nil, bug.Errorf("no file for edit position") + } + spn, err := span.NewRange(tokFile, e.Pos, e.End).Span() if err != nil { return nil, err } @@ -310,7 +322,11 @@ func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnos func relatedInformation(pkg *pkg, fset *token.FileSet, diag *analysis.Diagnostic) ([]source.RelatedInformation, error) { var out []source.RelatedInformation for _, related := range diag.Related { - spn, err := span.NewRange(fset, related.Pos, related.End).Span() + tokFile := fset.File(related.Pos) + if tokFile == nil { + return nil, bug.Errorf("no file for %q diagnostic position", diag.Category) + } + spn, err := span.NewRange(tokFile, related.Pos, related.End).Span() if err != nil { return nil, err } @@ -397,7 +413,7 @@ func parseGoListImportCycleError(snapshot *snapshot, e packages.Error, pkg *pkg) // Search file imports for the import that is causing the import cycle. for _, imp := range cgf.File.Imports { if imp.Path.Value == circImp { - spn, err := span.NewRange(snapshot.FileSet(), imp.Pos(), imp.End()).Span() + spn, err := span.NewRange(cgf.Tok, imp.Pos(), imp.End()).Span() if err != nil { return msg, span.Span{}, false } diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index e4fd6719872..08c88ab8e74 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -359,7 +359,7 @@ func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, fi switch s.view.FileKind(fh) { case source.Go: if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil { - pkgDecl := span.NewRange(s.FileSet(), pgf.File.Package, pgf.File.Name.End()) + pkgDecl := span.NewRange(pgf.Tok, pgf.File.Package, pgf.File.Name.End()) if spn, err := pkgDecl.Span(); err == nil { rng, _ = pgf.Mapper.Range(spn) } diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index b81caabde5f..91394659503 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "go/ast" + "go/token" "io/ioutil" "os" "path/filepath" @@ -282,7 +283,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc if !ok { return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path) } - srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes) + srcErr, err := missingModuleForImport(pgf.Tok, m, imp, req, fixes) if err != nil { return nil, err } @@ -445,11 +446,11 @@ func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdi // missingModuleForImport creates an error for a given import path that comes // from a missing module. -func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) { +func missingModuleForImport(file *token.File, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) { if req.Syntax == nil { return nil, fmt.Errorf("no syntax for %v", req) } - spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span() + spn, err := span.NewRange(file, imp.Path.Pos(), imp.Path.End()).Span() if err != nil { return nil, err } diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go index 9d78e3c9ac9..4147e17ce6d 100644 --- a/internal/lsp/code_action.go +++ b/internal/lsp/code_action.go @@ -294,7 +294,7 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.P } puri := protocol.URIFromSpanURI(uri) var commands []protocol.Command - if _, ok, methodOk, _ := source.CanExtractFunction(snapshot.FileSet(), srng, pgf.Src, pgf.File); ok { + if _, ok, methodOk, _ := source.CanExtractFunction(pgf.Tok, srng, pgf.Src, pgf.File); ok { cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ URI: puri, Fix: source.ExtractFunction, diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 9648921ef5d..59fe1716b9b 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -467,7 +467,7 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps if !pgf.File.Name.Pos().IsValid() { return nil } - spn, err := span.NewRange(snapshot.FileSet(), pgf.File.Name.Pos(), pgf.File.Name.End()).Span() + spn, err := span.NewRange(pgf.Tok, pgf.File.Name.Pos(), pgf.File.Name.End()).Span() if err != nil { return nil } diff --git a/internal/lsp/link.go b/internal/lsp/link.go index 7bb09b40355..a2962b6659a 100644 --- a/internal/lsp/link.go +++ b/internal/lsp/link.go @@ -68,7 +68,7 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl // dependency within the require statement. start, end := token.Pos(s+i), token.Pos(s+i+len(dep)) target := source.BuildLink(snapshot.View().Options().LinkTarget, "mod/"+req.Mod.String(), "") - l, err := toProtocolLink(snapshot, pm.Mapper, target, start, end, source.Mod) + l, err := toProtocolLink(nil, pm.Mapper, target, start, end, source.Mod) if err != nil { return nil, err } @@ -78,6 +78,10 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl if syntax := pm.File.Syntax; syntax == nil { return links, nil } + + // Create a throwaway token.File. + tokFile := token.NewFileSet().AddFile(fh.URI().Filename(), -1, len(pm.Mapper.Content)) + // Get all the links that are contained in the comments of the file. for _, expr := range pm.File.Syntax.Stmt { comments := expr.Comment() @@ -86,7 +90,8 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl } for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} { for _, comment := range section { - l, err := findLinksInString(ctx, snapshot, comment.Token, token.Pos(comment.Start.Byte), pm.Mapper, source.Mod) + start := tokFile.Pos(comment.Start.Byte) + l, err := findLinksInString(ctx, snapshot, comment.Token, start, tokFile, pm.Mapper, source.Mod) if err != nil { return nil, err } @@ -144,7 +149,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle start := imp.Path.Pos() + 1 end := imp.Path.End() - 1 target = source.BuildLink(view.Options().LinkTarget, target, "") - l, err := toProtocolLink(snapshot, pgf.Mapper, target, start, end, source.Go) + l, err := toProtocolLink(pgf.Tok, pgf.Mapper, target, start, end, source.Go) if err != nil { return nil, err } @@ -152,7 +157,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle } } for _, s := range str { - l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Mapper, source.Go) + l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Tok, pgf.Mapper, source.Go) if err != nil { return nil, err } @@ -160,7 +165,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle } for _, commentGroup := range pgf.File.Comments { for _, comment := range commentGroup.List { - l, err := findLinksInString(ctx, snapshot, comment.Text, comment.Pos(), pgf.Mapper, source.Go) + l, err := findLinksInString(ctx, snapshot, comment.Text, comment.Pos(), pgf.Tok, pgf.Mapper, source.Go) if err != nil { return nil, err } @@ -193,7 +198,8 @@ var acceptedSchemes = map[string]bool{ "https": true, } -func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string, pos token.Pos, m *protocol.ColumnMapper, fileKind source.FileKind) ([]protocol.DocumentLink, error) { +// tokFile may be a throwaway File for non-Go files. +func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string, pos token.Pos, tokFile *token.File, m *protocol.ColumnMapper, fileKind source.FileKind) ([]protocol.DocumentLink, error) { var links []protocol.DocumentLink for _, index := range snapshot.View().Options().URLRegexp.FindAllIndex([]byte(src), -1) { start, end := index[0], index[1] @@ -216,7 +222,7 @@ func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string if !acceptedSchemes[linkURL.Scheme] { continue } - l, err := toProtocolLink(snapshot, m, linkURL.String(), startPos, endPos, fileKind) + l, err := toProtocolLink(tokFile, m, linkURL.String(), startPos, endPos, fileKind) if err != nil { return nil, err } @@ -234,7 +240,7 @@ func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string } org, repo, number := matches[1], matches[2], matches[3] target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) - l, err := toProtocolLink(snapshot, m, target, startPos, endPos, fileKind) + l, err := toProtocolLink(tokFile, m, target, startPos, endPos, fileKind) if err != nil { return nil, err } @@ -255,11 +261,12 @@ var ( issueRegexp *regexp.Regexp ) -func toProtocolLink(snapshot source.Snapshot, m *protocol.ColumnMapper, target string, start, end token.Pos, fileKind source.FileKind) (protocol.DocumentLink, error) { +func toProtocolLink(tokFile *token.File, m *protocol.ColumnMapper, target string, start, end token.Pos, fileKind source.FileKind) (protocol.DocumentLink, error) { var rng protocol.Range switch fileKind { case source.Go: - spn, err := span.NewRange(snapshot.FileSet(), start, end).Span() + // TODO(adonovan): can we now use this logic for the Mod case too? + spn, err := span.NewRange(tokFile, start, end).Span() if err != nil { return protocol.DocumentLink{}, err } diff --git a/internal/lsp/semantic.go b/internal/lsp/semantic.go index 286d2fd160d..429dc0660b2 100644 --- a/internal/lsp/semantic.go +++ b/internal/lsp/semantic.go @@ -186,7 +186,7 @@ func (e *encoded) token(start token.Pos, leng int, typ tokenType, mods []string) } // want a line and column from start (in LSP coordinates) // [//line directives should be ignored] - rng := source.NewMappedRange(e.fset, e.pgf.Mapper, start, start+token.Pos(leng)) + rng := source.NewMappedRange(e.pgf.Tok, e.pgf.Mapper, start, start+token.Pos(leng)) lspRange, err := rng.Range() if err != nil { // possibly a //line directive. TODO(pjw): fix this somehow diff --git a/internal/lsp/source/call_hierarchy.go b/internal/lsp/source/call_hierarchy.go index c2c8a1866d0..4e7daf0f9bc 100644 --- a/internal/lsp/source/call_hierarchy.go +++ b/internal/lsp/source/call_hierarchy.go @@ -152,12 +152,12 @@ outer: kind = protocol.Function } - nameStart, nameEnd := nameIdent.NamePos, nameIdent.NamePos+token.Pos(len(nameIdent.Name)) + nameStart, nameEnd := nameIdent.Pos(), nameIdent.End() if funcLit != nil { nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos() kind = protocol.Function } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, nameStart, nameEnd).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, nameStart, nameEnd).Range() if err != nil { return protocol.CallHierarchyItem{}, err } @@ -194,14 +194,22 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok { return nil, nil } - if identifier.Declaration.node == nil { + node := identifier.Declaration.node + if node == nil { return nil, nil } if len(identifier.Declaration.MappedRange) == 0 { return nil, nil } declMappedRange := identifier.Declaration.MappedRange[0] - callExprs, err := collectCallExpressions(snapshot.FileSet(), declMappedRange.m, identifier.Declaration.node) + // TODO(adonovan): avoid Fileset.File call by somehow getting at + // declMappedRange.spanRange.TokFile, or making Identifier retain the + // token.File of the identifier and its declaration, since it looks up both anyway. + tokFile := snapshot.FileSet().File(node.Pos()) + if tokFile == nil { + return nil, fmt.Errorf("no file for position") + } + callExprs, err := collectCallExpressions(tokFile, declMappedRange.m, node) if err != nil { return nil, err } @@ -210,7 +218,7 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr } // collectCallExpressions collects call expression ranges inside a function. -func collectCallExpressions(fset *token.FileSet, mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) { +func collectCallExpressions(tokFile *token.File, mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) { type callPos struct { start, end token.Pos } @@ -240,7 +248,7 @@ func collectCallExpressions(fset *token.FileSet, mapper *protocol.ColumnMapper, callRanges := []protocol.Range{} for _, call := range callPositions { - callRange, err := NewMappedRange(fset, mapper, call.start, call.end).Range() + callRange, err := NewMappedRange(tokFile, mapper, call.start, call.end).Range() if err != nil { return nil, err } diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go index 0ab857ac600..0e9453a662b 100644 --- a/internal/lsp/source/code_lens.go +++ b/internal/lsp/source/code_lens.go @@ -67,7 +67,7 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p return nil, err } // add a code lens to the top of the file which runs all benchmarks in the file - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() if err != nil { return nil, err } @@ -111,7 +111,7 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) ( continue } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, d.Pos(), fn.End()).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, fn.Pos(), fn.End()).Range() if err != nil { return out, err } @@ -177,7 +177,7 @@ func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ( if !strings.HasPrefix(l.Text, ggDirective) { continue } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() if err != nil { return nil, err } @@ -214,7 +214,7 @@ func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([ if c == nil { return nil, nil } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, c.Pos(), c.EndPos).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, c.Pos(), c.End()).Range() if err != nil { return nil, err } @@ -231,7 +231,7 @@ func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle if err != nil { return nil, err } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() if err != nil { return nil, err } diff --git a/internal/lsp/source/completion/completion.go b/internal/lsp/source/completion/completion.go index a2dfae69841..be613d3e3e3 100644 --- a/internal/lsp/source/completion/completion.go +++ b/internal/lsp/source/completion/completion.go @@ -173,8 +173,9 @@ type completer struct { // file is the AST of the file associated with this completion request. file *ast.File - // pos is the position at which the request was triggered. - pos token.Pos + // (tokFile, pos) is the position at which the request was triggered. + tokFile *token.File + pos token.Pos // path is the path of AST nodes enclosing the position. path []ast.Node @@ -325,7 +326,7 @@ func (c *completer) setSurrounding(ident *ast.Ident) { content: ident.Name, cursor: c.pos, // Overwrite the prefix only. - rng: span.NewRange(c.snapshot.FileSet(), ident.Pos(), ident.End()), + rng: span.NewRange(c.tokFile, ident.Pos(), ident.End()), } c.setMatcherFromPrefix(c.surrounding.Prefix()) @@ -347,7 +348,7 @@ func (c *completer) getSurrounding() *Selection { c.surrounding = &Selection{ content: "", cursor: c.pos, - rng: span.NewRange(c.snapshot.FileSet(), c.pos, c.pos), + rng: span.NewRange(c.tokFile, c.pos, c.pos), } } return c.surrounding @@ -486,7 +487,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan qual := types.RelativeTo(pkg.GetTypes()) objStr = types.ObjectString(obj, qual) } - ans, sel := definition(path, obj, snapshot.FileSet(), fh) + ans, sel := definition(path, obj, pgf.Tok, fh) if ans != nil { sort.Slice(ans, func(i, j int) bool { return ans[i].Score > ans[j].Score @@ -513,6 +514,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan }, fh: fh, filename: fh.URI().Filename(), + tokFile: pgf.Tok, file: pgf.File, path: path, pos: pos, @@ -798,7 +800,7 @@ func (c *completer) populateImportCompletions(ctx context.Context, searchImport c.surrounding = &Selection{ content: content, cursor: c.pos, - rng: span.NewRange(c.snapshot.FileSet(), start, end), + rng: span.NewRange(c.tokFile, start, end), } seenImports := make(map[string]struct{}) @@ -1018,7 +1020,7 @@ func (c *completer) setSurroundingForComment(comments *ast.CommentGroup) { c.surrounding = &Selection{ content: cursorComment.Text[start:end], cursor: c.pos, - rng: span.NewRange(c.snapshot.FileSet(), token.Pos(int(cursorComment.Slash)+start), token.Pos(int(cursorComment.Slash)+end)), + rng: span.NewRange(c.tokFile, token.Pos(int(cursorComment.Slash)+start), token.Pos(int(cursorComment.Slash)+end)), } c.setMatcherFromPrefix(c.surrounding.Prefix()) } diff --git a/internal/lsp/source/completion/definition.go b/internal/lsp/source/completion/definition.go index 44d5a33b2f4..7644fc443d6 100644 --- a/internal/lsp/source/completion/definition.go +++ b/internal/lsp/source/completion/definition.go @@ -23,7 +23,7 @@ import ( // BenchmarkFoo(b *testing.B), FuzzFoo(f *testing.F) // path[0] is known to be *ast.Ident -func definition(path []ast.Node, obj types.Object, fset *token.FileSet, fh source.FileHandle) ([]CompletionItem, *Selection) { +func definition(path []ast.Node, obj types.Object, tokFile *token.File, fh source.FileHandle) ([]CompletionItem, *Selection) { if _, ok := obj.(*types.Func); !ok { return nil, nil // not a function at all } @@ -40,7 +40,7 @@ func definition(path []ast.Node, obj types.Object, fset *token.FileSet, fh sourc sel := &Selection{ content: "", cursor: pos, - rng: span.NewRange(fset, pos, pos), + rng: span.NewRange(tokFile, pos, pos), } var ans []CompletionItem diff --git a/internal/lsp/source/completion/package.go b/internal/lsp/source/completion/package.go index 21244efb5ec..566d8ee2a05 100644 --- a/internal/lsp/source/completion/package.go +++ b/internal/lsp/source/completion/package.go @@ -104,7 +104,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, return &Selection{ content: name.Name, cursor: cursor, - rng: span.NewRange(fset, name.Pos(), name.End()), + rng: span.NewRange(tok, name.Pos(), name.End()), }, nil } } @@ -141,7 +141,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, return &Selection{ content: content, cursor: cursor, - rng: span.NewRange(fset, start, end), + rng: span.NewRange(tok, start, end), }, nil } } @@ -154,7 +154,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, } // If the cursor is in a comment, don't offer any completions. - if cursorInComment(fset, cursor, pgf.Src) { + if cursorInComment(fset.File(cursor), cursor, pgf.Src) { return nil, fmt.Errorf("cursor in comment") } @@ -168,13 +168,13 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, return &Selection{ content: "", cursor: cursor, - rng: span.NewRange(fset, start, end), + rng: span.NewRange(tok, start, end), }, nil } -func cursorInComment(fset *token.FileSet, cursor token.Pos, src []byte) bool { +func cursorInComment(file *token.File, cursor token.Pos, src []byte) bool { var s scanner.Scanner - s.Init(fset.File(cursor), src, func(_ token.Position, _ string) {}, scanner.ScanComments) + s.Init(file, src, func(_ token.Position, _ string) {}, scanner.ScanComments) for { pos, tok, lit := s.Scan() if pos <= cursor && cursor <= token.Pos(int(pos)+len(lit)) { diff --git a/internal/lsp/source/completion/util.go b/internal/lsp/source/completion/util.go index e6d3bfd745f..e0a264bef9e 100644 --- a/internal/lsp/source/completion/util.go +++ b/internal/lsp/source/completion/util.go @@ -311,7 +311,7 @@ func isBasicKind(t types.Type, k types.BasicInfo) bool { } func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.TextEdit, error) { - rng := source.NewMappedRange(c.snapshot.FileSet(), c.mapper, from, to) + rng := source.NewMappedRange(c.tokFile, c.mapper, from, to) spn, err := rng.Span() if err != nil { return nil, err diff --git a/internal/lsp/source/extract.go b/internal/lsp/source/extract.go index 90999d821a6..a4e0a148adb 100644 --- a/internal/lsp/source/extract.go +++ b/internal/lsp/source/extract.go @@ -18,11 +18,13 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/safetoken" "golang.org/x/tools/internal/span" ) func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { + tokFile := fset.File(file.Pos()) expr, path, ok, err := CanExtractVariable(rng, file) if !ok { return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", fset.Position(rng.Start), err) @@ -60,11 +62,7 @@ func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast. if insertBeforeStmt == nil { return nil, fmt.Errorf("cannot find location to insert extraction") } - tok := fset.File(expr.Pos()) - if tok == nil { - return nil, fmt.Errorf("no file for pos %v", fset.Position(file.Pos())) - } - indent, err := calculateIndentation(src, tok, insertBeforeStmt) + indent, err := calculateIndentation(src, tokFile, insertBeforeStmt) if err != nil { return nil, err } @@ -217,7 +215,12 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file if isMethod { errorPrefix = "extractMethod" } - p, ok, methodOk, err := CanExtractFunction(fset, rng, src, file) + + tok := fset.File(file.Pos()) + if tok == nil { + return nil, bug.Errorf("no file for position") + } + p, ok, methodOk, err := CanExtractFunction(tok, rng, src, file) if (!ok && !isMethod) || (!methodOk && isMethod) { return nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix, fset.Position(rng.Start), err) @@ -344,7 +347,7 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file if v.obj.Parent() == nil { return nil, fmt.Errorf("parent nil") } - isUsed, firstUseAfter := objUsed(info, span.NewRange(fset, rng.End, v.obj.Parent().End()), v.obj) + isUsed, firstUseAfter := objUsed(info, span.NewRange(tok, rng.End, v.obj.Parent().End()), v.obj) if v.assigned && isUsed && !varOverridden(info, firstUseAfter, v.obj, v.free, outer) { returnTypes = append(returnTypes, &ast.Field{Type: typ}) returns = append(returns, identifier) @@ -941,14 +944,10 @@ type fnExtractParams struct { // CanExtractFunction reports whether the code in the given range can be // extracted to a function. -func CanExtractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) { +func CanExtractFunction(tok *token.File, rng span.Range, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) { if rng.Start == rng.End { return nil, false, false, fmt.Errorf("start and end are equal") } - tok := fset.File(file.Pos()) - if tok == nil { - return nil, false, false, fmt.Errorf("no file for pos %v", fset.Position(file.Pos())) - } var err error rng, err = adjustRangeForWhitespace(rng, tok, src) if err != nil { diff --git a/internal/lsp/source/fix.go b/internal/lsp/source/fix.go index 6a7f77dab36..dce279e2016 100644 --- a/internal/lsp/source/fix.go +++ b/internal/lsp/source/fix.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/internal/lsp/analysis/fillstruct" "golang.org/x/tools/internal/lsp/analysis/undeclaredname" + "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) @@ -84,7 +85,15 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi fset := snapshot.FileSet() editsPerFile := map[span.URI]*protocol.TextDocumentEdit{} for _, edit := range suggestion.TextEdits { - spn, err := span.NewRange(fset, edit.Pos, edit.End).Span() + tokFile := fset.File(edit.Pos) + if tokFile == nil { + return nil, bug.Errorf("no file for edit position") + } + end := edit.End + if !end.IsValid() { + end = edit.Pos + } + spn, err := span.NewRange(tokFile, edit.Pos, end).Span() if err != nil { return nil, err } diff --git a/internal/lsp/source/folding_range.go b/internal/lsp/source/folding_range.go index 576308f9967..b70cb4decd8 100644 --- a/internal/lsp/source/folding_range.go +++ b/internal/lsp/source/folding_range.go @@ -41,13 +41,11 @@ func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFol return nil, nil } - fset := snapshot.FileSet() - // Get folding ranges for comments separately as they are not walked by ast.Inspect. - ranges = append(ranges, commentsFoldingRange(fset, pgf.Mapper, pgf.File)...) + ranges = append(ranges, commentsFoldingRange(pgf.Tok, pgf.Mapper, pgf.File)...) visit := func(n ast.Node) bool { - rng := foldingRangeFunc(fset, pgf.Mapper, n, lineFoldingOnly) + rng := foldingRangeFunc(pgf.Tok, pgf.Mapper, n, lineFoldingOnly) if rng != nil { ranges = append(ranges, rng) } @@ -66,7 +64,7 @@ func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFol } // foldingRangeFunc calculates the line folding range for ast.Node n -func foldingRangeFunc(fset *token.FileSet, m *protocol.ColumnMapper, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { +func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { // TODO(suzmue): include trailing empty lines before the closing // parenthesis/brace. var kind protocol.FoldingRangeKind @@ -78,7 +76,7 @@ func foldingRangeFunc(fset *token.FileSet, m *protocol.ColumnMapper, n ast.Node, if num := len(n.List); num != 0 { startList, endList = n.List[0].Pos(), n.List[num-1].End() } - start, end = validLineFoldingRange(fset, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly) + start, end = validLineFoldingRange(tokFile, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly) case *ast.CaseClause: // Fold from position of ":" to end. start, end = n.Colon+1, n.End() @@ -94,7 +92,7 @@ func foldingRangeFunc(fset *token.FileSet, m *protocol.ColumnMapper, n ast.Node, if num := len(n.List); num != 0 { startList, endList = n.List[0].Pos(), n.List[num-1].End() } - start, end = validLineFoldingRange(fset, n.Opening, n.Closing, startList, endList, lineFoldingOnly) + start, end = validLineFoldingRange(tokFile, n.Opening, n.Closing, startList, endList, lineFoldingOnly) case *ast.GenDecl: // If this is an import declaration, set the kind to be protocol.Imports. if n.Tok == token.IMPORT { @@ -105,7 +103,7 @@ func foldingRangeFunc(fset *token.FileSet, m *protocol.ColumnMapper, n ast.Node, if num := len(n.Specs); num != 0 { startSpecs, endSpecs = n.Specs[0].Pos(), n.Specs[num-1].End() } - start, end = validLineFoldingRange(fset, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly) + start, end = validLineFoldingRange(tokFile, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly) case *ast.BasicLit: // Fold raw string literals from position of "`" to position of "`". if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' { @@ -117,7 +115,7 @@ func foldingRangeFunc(fset *token.FileSet, m *protocol.ColumnMapper, n ast.Node, if num := len(n.Elts); num != 0 { startElts, endElts = n.Elts[0].Pos(), n.Elts[num-1].End() } - start, end = validLineFoldingRange(fset, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) + start, end = validLineFoldingRange(tokFile, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) } // Check that folding positions are valid. @@ -125,18 +123,18 @@ func foldingRangeFunc(fset *token.FileSet, m *protocol.ColumnMapper, n ast.Node, return nil } // in line folding mode, do not fold if the start and end lines are the same. - if lineFoldingOnly && fset.Position(start).Line == fset.Position(end).Line { + if lineFoldingOnly && tokFile.Line(start) == tokFile.Line(end) { return nil } return &FoldingRangeInfo{ - MappedRange: NewMappedRange(fset, m, start, end), + MappedRange: NewMappedRange(tokFile, m, start, end), Kind: kind, } } // validLineFoldingRange returns start and end token.Pos for folding range if the range is valid. // returns token.NoPos otherwise, which fails token.IsValid check -func validLineFoldingRange(fset *token.FileSet, open, close, start, end token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) { +func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) { if lineFoldingOnly { if !open.IsValid() || !close.IsValid() { return token.NoPos, token.NoPos @@ -146,8 +144,8 @@ func validLineFoldingRange(fset *token.FileSet, open, close, start, end token.Po // as an example, the example below should *not* fold: // var x = [2]string{"d", // "e" } - if fset.Position(open).Line == fset.Position(start).Line || - fset.Position(close).Line == fset.Position(end).Line { + if tokFile.Line(open) == tokFile.Line(start) || + tokFile.Line(close) == tokFile.Line(end) { return token.NoPos, token.NoPos } @@ -159,25 +157,25 @@ func validLineFoldingRange(fset *token.FileSet, open, close, start, end token.Po // commentsFoldingRange returns the folding ranges for all comment blocks in file. // The folding range starts at the end of the first line of the comment block, and ends at the end of the // comment block and has kind protocol.Comment. -func commentsFoldingRange(fset *token.FileSet, m *protocol.ColumnMapper, file *ast.File) (comments []*FoldingRangeInfo) { +func commentsFoldingRange(tokFile *token.File, m *protocol.ColumnMapper, file *ast.File) (comments []*FoldingRangeInfo) { for _, commentGrp := range file.Comments { - startGrp, endGrp := fset.Position(commentGrp.Pos()), fset.Position(commentGrp.End()) - if startGrp.Line == endGrp.Line { + startGrpLine, endGrpLine := tokFile.Line(commentGrp.Pos()), tokFile.Line(commentGrp.End()) + if startGrpLine == endGrpLine { // Don't fold single line comments. continue } firstComment := commentGrp.List[0] startPos, endLinePos := firstComment.Pos(), firstComment.End() - startCmmnt, endCmmnt := fset.Position(startPos), fset.Position(endLinePos) - if startCmmnt.Line != endCmmnt.Line { + startCmmntLine, endCmmntLine := tokFile.Line(startPos), tokFile.Line(endLinePos) + if startCmmntLine != endCmmntLine { // If the first comment spans multiple lines, then we want to have the // folding range start at the end of the first line. endLinePos = token.Pos(int(startPos) + len(strings.Split(firstComment.Text, "\n")[0])) } comments = append(comments, &FoldingRangeInfo{ // Fold from the end of the first line comment to the end of the comment block. - MappedRange: NewMappedRange(fset, m, endLinePos, commentGrp.End()), + MappedRange: NewMappedRange(tokFile, m, endLinePos, commentGrp.End()), Kind: protocol.Comment, }) } diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go index 40655e20779..c87725c4854 100644 --- a/internal/lsp/source/identifier.go +++ b/internal/lsp/source/identifier.go @@ -226,7 +226,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // The builtin package isn't in the dependency graph, so the usual // utilities won't work here. - rng := NewMappedRange(snapshot.FileSet(), builtin.Mapper, decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) + rng := NewMappedRange(builtin.Tok, builtin.Mapper, decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } @@ -267,7 +267,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa } name := method.Names[0].Name result.Declaration.node = method - rng := NewMappedRange(snapshot.FileSet(), builtin.Mapper, method.Pos(), method.Pos()+token.Pos(len(name))) + rng := NewMappedRange(builtin.Tok, builtin.Mapper, method.Pos(), method.Pos()+token.Pos(len(name))) result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } diff --git a/internal/lsp/source/references.go b/internal/lsp/source/references.go index 85bf41a21b0..a3d32a6d717 100644 --- a/internal/lsp/source/references.go +++ b/internal/lsp/source/references.go @@ -48,6 +48,7 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit return nil, err } + packageName := pgf.File.Name.Name // from package decl packageNameStart, err := safetoken.Offset(pgf.Tok, pgf.File.Name.Pos()) if err != nil { return nil, err @@ -75,8 +76,8 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit for _, imp := range f.File.Imports { if path, err := strconv.Unquote(imp.Path.Value); err == nil && path == renamingPkg.PkgPath() { refs = append(refs, &ReferenceInfo{ - Name: pgf.File.Name.Name, - MappedRange: NewMappedRange(s.FileSet(), f.Mapper, imp.Pos(), imp.End()), + Name: packageName, + MappedRange: NewMappedRange(f.Tok, f.Mapper, imp.Pos(), imp.End()), }) } } @@ -86,8 +87,8 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit // Find internal references to the package within the package itself for _, f := range renamingPkg.CompiledGoFiles() { refs = append(refs, &ReferenceInfo{ - Name: pgf.File.Name.Name, - MappedRange: NewMappedRange(s.FileSet(), f.Mapper, f.File.Name.Pos(), f.File.Name.End()), + Name: packageName, + MappedRange: NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()), }) } diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go index 6312bcb1296..503422aa906 100644 --- a/internal/lsp/source/rename.go +++ b/internal/lsp/source/rename.go @@ -238,15 +238,15 @@ func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) { continue } lines := strings.Split(comment.Text, "\n") - tok := r.fset.File(comment.Pos()) - commentLine := tok.Position(comment.Pos()).Line + tokFile := r.fset.File(comment.Pos()) + commentLine := tokFile.Line(comment.Pos()) for i, line := range lines { lineStart := comment.Pos() if i > 0 { - lineStart = tok.LineStart(commentLine + i) + lineStart = tokFile.LineStart(commentLine + i) } for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) { - rng := span.NewRange(r.fset, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1])) + rng := span.NewRange(tokFile, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1])) spn, err := rng.Span() if err != nil { return nil, err @@ -265,7 +265,7 @@ func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) { // docComment returns the doc for an identifier. func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { - _, nodes, _ := pathEnclosingInterval(r.fset, pkg, id.Pos(), id.End()) + _, tokFile, nodes, _ := pathEnclosingInterval(r.fset, pkg, id.Pos(), id.End()) for _, node := range nodes { switch decl := node.(type) { case *ast.FuncDecl: @@ -294,25 +294,14 @@ func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { return nil } - var file *ast.File - for _, f := range pkg.GetSyntax() { - if f.Pos() <= id.Pos() && id.Pos() <= f.End() { - file = f - break - } - } - if file == nil { - return nil - } - - identLine := r.fset.Position(id.Pos()).Line - for _, comment := range file.Comments { + identLine := tokFile.Line(id.Pos()) + for _, comment := range nodes[len(nodes)-1].(*ast.File).Comments { if comment.Pos() > id.Pos() { // Comment is after the identifier. continue } - lastCommentLine := r.fset.Position(comment.End()).Line + lastCommentLine := tokFile.Line(comment.End()) if lastCommentLine+1 == identLine { return comment } @@ -328,7 +317,7 @@ func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) { // Modify ImportSpec syntax to add or remove the Name as needed. pkg := r.packages[pkgName.Pkg()] - _, path, _ := pathEnclosingInterval(r.fset, pkg, pkgName.Pos(), pkgName.Pos()) + _, tokFile, path, _ := pathEnclosingInterval(r.fset, pkg, pkgName.Pos(), pkgName.Pos()) if len(path) < 2 { return nil, fmt.Errorf("no path enclosing interval for %s", pkgName.Name()) } @@ -350,7 +339,7 @@ func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) EndPos: spec.EndPos, } - rng := span.NewRange(r.fset, spec.Pos(), spec.End()) + rng := span.NewRange(tokFile, spec.Pos(), spec.End()) spn, err := rng.Span() if err != nil { return nil, err diff --git a/internal/lsp/source/rename_check.go b/internal/lsp/source/rename_check.go index b17f9b87067..6fb7ddf9ba5 100644 --- a/internal/lsp/source/rename_check.go +++ b/internal/lsp/source/rename_check.go @@ -372,7 +372,7 @@ func (r *renamer) checkStructField(from *types.Var) { if !ok { return } - pkg, path, _ := pathEnclosingInterval(r.fset, fromPkg, from.Pos(), from.Pos()) + pkg, _, path, _ := pathEnclosingInterval(r.fset, fromPkg, from.Pos(), from.Pos()) if pkg == nil || path == nil { return } @@ -821,13 +821,13 @@ func someUse(info *types.Info, obj types.Object) *ast.Ident { return nil } -// pathEnclosingInterval returns the Package and ast.Node that +// pathEnclosingInterval returns the Package, token.File, and ast.Node that // contain source interval [start, end), and all the node's ancestors // up to the AST root. It searches all ast.Files of all packages. // exact is defined as for astutil.PathEnclosingInterval. // // The zero value is returned if not found. -func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Pos) (resPkg Package, path []ast.Node, exact bool) { +func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Pos) (resPkg Package, tokFile *token.File, path []ast.Node, exact bool) { pkgs := []Package{pkg} for _, f := range pkg.GetSyntax() { for _, imp := range f.Imports { @@ -840,35 +840,36 @@ func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Po } importPkg, err := pkg.GetImport(importPath) if err != nil { - return nil, nil, false + return nil, nil, nil, false } pkgs = append(pkgs, importPkg) } } for _, p := range pkgs { for _, f := range p.GetSyntax() { - if f.Pos() == token.NoPos { + if !f.Pos().IsValid() { // This can happen if the parser saw // too many errors and bailed out. // (Use parser.AllErrors to prevent that.) continue } - if !tokenFileContainsPos(fset.File(f.Pos()), start) { + tokFile := fset.File(f.Pos()) + if !tokenFileContainsPos(tokFile, start) { continue } if path, exact := astutil.PathEnclosingInterval(f, start, end); path != nil { - return pkg, path, exact + return pkg, tokFile, path, exact } } } - return nil, nil, false + return nil, nil, nil, false } // TODO(adonovan): make this a method: func (*token.File) Contains(token.Pos) func tokenFileContainsPos(tf *token.File, pos token.Pos) bool { p := int(pos) base := tf.Base() - return base <= p && p < base+tf.Size() + return base <= p && p <= base+tf.Size() } func objectKind(obj types.Object) string { diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go index b8a7fc9135f..10970389290 100644 --- a/internal/lsp/source/util.go +++ b/internal/lsp/source/util.go @@ -30,26 +30,22 @@ type MappedRange struct { m *protocol.ColumnMapper // a mapper of the edited source (package.GoFiles) } -// NewMappedRange returns a MappedRange for the given start and end token.Pos. +// NewMappedRange returns a MappedRange for the given file and valid start/end token.Pos. // // By convention, start and end are assumed to be positions in the compiled (== // type checked) source, whereas the column mapper m maps positions in the -// user-edited source. Note that these may not be the same, as when using CGo: +// user-edited source. Note that these may not be the same, as when using goyacc or CGo: // CompiledGoFiles contains generated files, whose positions (via // token.File.Position) point to locations in the edited file -- the file // containing `import "C"`. -func NewMappedRange(fset *token.FileSet, m *protocol.ColumnMapper, start, end token.Pos) MappedRange { - if tf := fset.File(start); tf == nil { - bug.Report("nil file", nil) - } else { - mapped := m.TokFile.Name() - adjusted := tf.PositionFor(start, true) // adjusted position - if adjusted.Filename != mapped { - bug.Reportf("mapped file %q does not match start position file %q", mapped, adjusted.Filename) - } +func NewMappedRange(file *token.File, m *protocol.ColumnMapper, start, end token.Pos) MappedRange { + mapped := m.TokFile.Name() + adjusted := file.PositionFor(start, true) // adjusted position + if adjusted.Filename != mapped { + bug.Reportf("mapped file %q does not match start position file %q", mapped, adjusted.Filename) } return MappedRange{ - spanRange: span.NewRange(fset, start, end), + spanRange: span.NewRange(file, start, end), m: m, } } @@ -134,7 +130,10 @@ func nodeToProtocolRange(snapshot Snapshot, pkg Package, n ast.Node) (protocol.R return mrng.Range() } +// objToMappedRange returns the MappedRange for the object's declaring +// identifier (or string literal, for an import). func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedRange, error) { + nameLen := len(obj.Name()) if pkgName, ok := obj.(*types.PkgName); ok { // An imported Go package has a package-local, unqualified name. // When the name matches the imported package name, there is no @@ -147,29 +146,35 @@ func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedR // When the identifier does not appear in the source, have the range // of the object be the import path, including quotes. if pkgName.Imported().Name() == pkgName.Name() { - return posToMappedRange(snapshot, pkg, obj.Pos(), obj.Pos()+token.Pos(len(pkgName.Imported().Path())+2)) + nameLen = len(pkgName.Imported().Path()) + len(`""`) } } - return nameToMappedRange(snapshot, pkg, obj.Pos(), obj.Name()) -} - -func nameToMappedRange(snapshot Snapshot, pkg Package, pos token.Pos, name string) (MappedRange, error) { - return posToMappedRange(snapshot, pkg, pos, pos+token.Pos(len(name))) + return posToMappedRange(snapshot, pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen)) } +// posToMappedRange returns the MappedRange for the given [start, end) span, +// which must be among the transitive dependencies of pkg. func posToMappedRange(snapshot Snapshot, pkg Package, pos, end token.Pos) (MappedRange, error) { - logicalFilename := snapshot.FileSet().File(pos).Position(pos).Filename + tokFile := snapshot.FileSet().File(pos) + // Subtle: it is not safe to simplify this to tokFile.Name + // because, due to //line directives, a Position within a + // token.File may have a different filename than the File itself. + logicalFilename := tokFile.Position(pos).Filename pgf, _, err := findFileInDeps(pkg, span.URIFromPath(logicalFilename)) if err != nil { return MappedRange{}, err } if !pos.IsValid() { - return MappedRange{}, fmt.Errorf("invalid position for %v", pos) + return MappedRange{}, fmt.Errorf("invalid start position") } if !end.IsValid() { - return MappedRange{}, fmt.Errorf("invalid position for %v", end) + return MappedRange{}, fmt.Errorf("invalid end position") } - return NewMappedRange(snapshot.FileSet(), pgf.Mapper, pos, end), nil + // It is fishy that pgf.Mapper (from the parsed Go file) is + // accompanied here not by pgf.Tok but by tokFile from the global + // FileSet, which is a distinct token.File that doesn't + // contain [pos,end). TODO(adonovan): clean this up. + return NewMappedRange(tokFile, pgf.Mapper, pos, end), nil } // Matches cgo generated comment as well as the proposed standard: diff --git a/internal/lsp/source/util_test.go b/internal/lsp/source/util_test.go index 5d4e98f151c..fe505e4d06c 100644 --- a/internal/lsp/source/util_test.go +++ b/internal/lsp/source/util_test.go @@ -41,7 +41,7 @@ const a𐐀b = 42`) start := cf.Pos(bytes.Index(compiled, []byte("a𐐀b"))) end := start + token.Pos(len("a𐐀b")) - mr := NewMappedRange(fset, mapper, start, end) + mr := NewMappedRange(cf, mapper, start, end) gotRange, err := mr.Range() if err != nil { t.Fatal(err) diff --git a/internal/span/token.go b/internal/span/token.go index af01d7b8348..cae696db757 100644 --- a/internal/span/token.go +++ b/internal/span/token.go @@ -12,28 +12,40 @@ import ( ) // Range represents a source code range in token.Pos form. -// It also carries the FileSet that produced the positions, so that it is +// It also carries the token.File that produced the positions, so that it is // self contained. type Range struct { - Start token.Pos - End token.Pos - - // TokFile may be nil if Start or End is invalid. - // TODO: Eventually we should guarantee that it is non-nil. - TokFile *token.File + TokFile *token.File // non-nil + Start, End token.Pos // both IsValid() } -// NewRange creates a new Range from a FileSet and two positions. -// To represent a point pass a 0 as the end pos. -func NewRange(fset *token.FileSet, start, end token.Pos) Range { - tf := fset.File(start) - if tf == nil { - bug.Reportf("nil file") - } +// NewRange creates a new Range from a token.File and two valid positions within it. +// +// (If you only have a token.FileSet, use file = fset.File(start). But +// most callers know exactly which token.File they're dealing with and +// should pass it explicitly. Not only does this save a lookup, but it +// brings us a step closer to eliminating the global FileSet.) +func NewRange(file *token.File, start, end token.Pos) Range { + if file == nil { + panic("nil *token.File") + } + if !start.IsValid() || !end.IsValid() { + panic("invalid start/end token.Pos") + } + + // TODO(adonovan): ideally we would make this stronger assertion: + // + // // Assert that file is non-nil and contains start and end. + // _ = file.Offset(start) + // _ = file.Offset(end) + // + // but some callers (e.g. packageCompletionSurrounding, + // posToMappedRange) don't ensure this precondition. + return Range{ + TokFile: file, Start: start, End: end, - TokFile: tf, } } From 36430f4b355177a2580d8df0ec38bbf98556a14b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 1 Jul 2022 17:28:48 -0400 Subject: [PATCH 080/723] internal/lsp/cache: use GetHandle not Bind for actions This change uses a persistent.Map for actions, just like packages. Actions are now reference counted rather than generational. Also: - note optimization opportunities. - minor cleanups. Change-Id: Ibbac8848a3beb3fe19056a7b160d2185155e7021 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415504 gopls-CI: kokoro Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/cache/analysis.go | 30 ++++++++++++------- internal/lsp/cache/maps.go | 24 +++++++++++---- internal/lsp/cache/session.go | 3 +- internal/lsp/cache/snapshot.go | 54 ++++++++++++++++++++++------------ 4 files changed, 75 insertions(+), 36 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index db2ca2a8b09..4b437858ef3 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -85,12 +85,21 @@ type packageFactKey struct { } func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.Analyzer) (*actionHandle, error) { + // TODO(adonovan): opt: this block of code sequentially loads a package + // (and all its dependencies), then sequentially creates action handles + // for the direct dependencies (whose packages have by then been loaded + // as a consequence of ph.check) which does a sequential recursion + // down the action graph. Only once all that work is complete do we + // put a handle in the cache. As with buildPackageHandle, this does + // not exploit the natural parallelism in the problem, and the naive + // use of concurrency would lead to an exponential amount of duplicated + // work. We should instead use an atomically updated future cache + // and a parallel graph traversal. ph, err := s.buildPackageHandle(ctx, id, source.ParseFull) if err != nil { return nil, err } - act := s.getActionHandle(id, ph.mode, a) - if act != nil { + if act := s.getActionHandle(id, ph.mode, a); act != nil { return act, nil } if len(ph.key) == 0 { @@ -100,12 +109,9 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A if err != nil { return nil, err } - act = &actionHandle{ - analyzer: a, - pkg: pkg, - } + + // Add a dependency on each required analyzer. var deps []*actionHandle - // Add a dependency on each required analyzers. for _, req := range a.Requires { reqActionHandle, err := s.actionHandle(ctx, id, req) if err != nil { @@ -131,7 +137,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } } - h := s.generation.Bind(buildActionKey(a, ph), func(ctx context.Context, arg memoize.Arg) interface{} { + handle, release := s.generation.GetHandle(buildActionKey(a, ph), func(ctx context.Context, arg memoize.Arg) interface{} { snapshot := arg.(*snapshot) // Analyze dependencies first. results, err := execAll(ctx, snapshot, deps) @@ -142,9 +148,13 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } return runAnalysis(ctx, snapshot, a, pkg, results) }) - act.handle = h - act = s.addActionHandle(act) + act := &actionHandle{ + analyzer: a, + pkg: pkg, + handle: handle, + } + act = s.addActionHandle(act, release) return act, nil } diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go index af041777188..f8e03057cfd 100644 --- a/internal/lsp/cache/maps.go +++ b/internal/lsp/cache/maps.go @@ -197,16 +197,18 @@ type packagesMap struct { func newPackagesMap() packagesMap { return packagesMap{ impl: persistent.NewMap(func(a, b interface{}) bool { - left := a.(packageKey) - right := b.(packageKey) - if left.mode != right.mode { - return left.mode < right.mode - } - return left.id < right.id + return packageKeyLess(a.(packageKey), b.(packageKey)) }), } } +func packageKeyLess(x, y packageKey) bool { + if x.mode != y.mode { + return x.mode < y.mode + } + return x.id < y.id +} + func (m packagesMap) Clone() packagesMap { return packagesMap{ impl: m.impl.Clone(), @@ -285,3 +287,13 @@ func (s knownDirsSet) Insert(key span.URI) { func (s knownDirsSet) Remove(key span.URI) { s.impl.Delete(key) } + +// actionKeyLessInterface is the less-than relation for actionKey +// values wrapped in an interface. +func actionKeyLessInterface(a, b interface{}) bool { + x, y := a.(actionKey), b.(actionKey) + if x.analyzer.Name != y.analyzer.Name { + return x.analyzer.Name < y.analyzer.Name + } + return packageKeyLess(x.pkg, y.pkg) +} diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index aca46dd2a88..98d3c250433 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -16,6 +16,7 @@ import ( "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/lsp/progress" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/persistent" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) @@ -238,7 +239,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, goFiles: newGoFilesMap(), parseKeysByURI: newParseKeysByURIMap(), symbols: make(map[span.URI]*symbolHandle), - actions: make(map[actionKey]*actionHandle), + actions: persistent.NewMap(actionKeyLessInterface), workspacePackages: make(map[PackageID]PackagePath), unloadableFiles: make(map[span.URI]struct{}), parseModHandles: make(map[span.URI]*parseModHandle), diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 36dcafeaca6..93316653af4 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -36,6 +36,7 @@ import ( "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" + "golang.org/x/tools/internal/persistent" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typesinternal" ) @@ -87,7 +88,7 @@ type snapshot struct { // TODO(rfindley): consider merging this with files to reduce burden on clone. symbols map[span.URI]*symbolHandle - // packages maps a packageKey to a set of packageHandles to which that file belongs. + // packages maps a packageKey to a *packageHandle. // It may be invalidated when a file's content changes. packages packagesMap @@ -95,8 +96,9 @@ type snapshot struct { // It may be invalidated when metadata changes or a new file is opened or closed. isActivePackageCache isActivePackageCacheMap - // actions maps an actionkey to its actionHandle. - actions map[actionKey]*actionHandle + // actions maps an actionKey to the handle for the future + // result of execution an analysis pass on a package. + actions *persistent.Map // from actionKey to *actionHandle // workspacePackages contains the workspace's packages, which are loaded // when the view is created. @@ -149,6 +151,7 @@ func (s *snapshot) Destroy(destroyedBy string) { s.generation.Destroy(destroyedBy) s.packages.Destroy() s.isActivePackageCache.Destroy() + s.actions.Destroy() s.files.Destroy() s.goFiles.Destroy() s.parseKeysByURI.Destroy() @@ -1177,9 +1180,6 @@ func (s *snapshot) addSymbolHandle(uri span.URI, sh *symbolHandle) *symbolHandle } func (s *snapshot) getActionHandle(id PackageID, m source.ParseMode, a *analysis.Analyzer) *actionHandle { - s.mu.Lock() - defer s.mu.Unlock() - key := actionKey{ pkg: packageKey{ id: id, @@ -1187,13 +1187,18 @@ func (s *snapshot) getActionHandle(id PackageID, m source.ParseMode, a *analysis }, analyzer: a, } - return s.actions[key] -} -func (s *snapshot) addActionHandle(ah *actionHandle) *actionHandle { s.mu.Lock() defer s.mu.Unlock() + ah, ok := s.actions.Get(key) + if !ok { + return nil + } + return ah.(*actionHandle) +} + +func (s *snapshot) addActionHandle(ah *actionHandle, release func()) *actionHandle { key := actionKey{ analyzer: ah.analyzer, pkg: packageKey{ @@ -1201,10 +1206,17 @@ func (s *snapshot) addActionHandle(ah *actionHandle) *actionHandle { mode: ah.pkg.mode, }, } - if ah, ok := s.actions[key]; ok { - return ah + + s.mu.Lock() + defer s.mu.Unlock() + + // If another thread since cached a different handle, + // return it instead of overriding it. + if result, ok := s.actions.Get(key); ok { + release() + return result.(*actionHandle) } - s.actions[key] = ah + s.actions.Set(key, ah, func(_, _ interface{}) { release() }) return ah } @@ -1716,7 +1728,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC initializedErr: s.initializedErr, packages: s.packages.Clone(), isActivePackageCache: s.isActivePackageCache.Clone(), - actions: make(map[actionKey]*actionHandle, len(s.actions)), + actions: s.actions.Clone(), files: s.files.Clone(), goFiles: s.goFiles.Clone(), parseKeysByURI: s.parseKeysByURI.Clone(), @@ -1920,13 +1932,17 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } - // Copy the package analysis information. - for k, v := range s.actions { - if _, ok := idsToInvalidate[k.pkg.id]; ok { - continue + // Copy actions. + // TODO(adonovan): opt: avoid iteration over s.actions. + var actionsToDelete []actionKey + s.actions.Range(func(k, _ interface{}) { + key := k.(actionKey) + if _, ok := idsToInvalidate[key.pkg.id]; ok { + actionsToDelete = append(actionsToDelete, key) } - newGen.Inherit(v.handle) - result.actions[k] = v + }) + for _, key := range actionsToDelete { + result.actions.Delete(key) } // If the workspace mode has changed, we must delete all metadata, as it From 8184d1ff7a52751ae937e76f2fd00333ed193799 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 4 Jul 2022 10:00:43 -0400 Subject: [PATCH 081/723] internal/lsp/cache: use GetHandle not Bind in astCacheData This change replaces Bind (generational lifetime) with GetHandle (reference counting) for the cache of buildASTCache calls, as Bind is deprecated. Also: - add missing commentary, particularly on the question of why this cache is needed at all. - remove unused field astCacheData.err - simplify SignatureHelp to avoid unnecessary use of Declaration. - minor simplifications to logic surrounding FindPackageFromPos and PosTo{Decl,Field}. Change-Id: I2b7a798b84f23856037797fa6e9ccc5595422e7c Reviewed-on: https://go-review.googlesource.com/c/tools/+/415975 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan gopls-CI: kokoro --- internal/lsp/cache/graph.go | 16 +++++------ internal/lsp/cache/parse.go | 39 +++++++++++++++++++++------ internal/lsp/source/signature_help.go | 11 +------- internal/lsp/source/types_format.go | 17 +++--------- internal/lsp/source/util.go | 12 ++++----- internal/lsp/source/view.go | 8 ++++-- 6 files changed, 55 insertions(+), 48 deletions(-) diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go index 88c9f147195..c1beff82866 100644 --- a/internal/lsp/cache/graph.go +++ b/internal/lsp/cache/graph.go @@ -24,8 +24,8 @@ type metadataGraph struct { // importedBy maps package IDs to the list of packages that import them. importedBy map[PackageID][]PackageID - // ids maps file URIs to package IDs. A single file may belong to multiple - // packages due to tests packages. + // ids maps file URIs to package IDs, sorted by (!valid, cli, packageID). + // A single file may belong to multiple packages due to tests packages. ids map[span.URI][]PackageID } @@ -89,21 +89,21 @@ func (g *metadataGraph) build() { // 4: an invalid command-line-arguments package for uri, ids := range g.ids { sort.Slice(ids, func(i, j int) bool { - // Sort valid packages first. + // 1. valid packages appear earlier. validi := g.metadata[ids[i]].Valid validj := g.metadata[ids[j]].Valid if validi != validj { return validi } + // 2. command-line-args packages appear later. cli := source.IsCommandLineArguments(string(ids[i])) clj := source.IsCommandLineArguments(string(ids[j])) - if cli && !clj { - return false - } - if !cli && clj { - return true + if cli != clj { + return clj } + + // 3. packages appear in name order. return ids[i] < ids[j] }) diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index c3eae2f7643..712f26ad715 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -128,19 +128,37 @@ func (s *snapshot) astCacheData(ctx context.Context, spkg source.Package, pos to if err != nil { return nil, err } - astHandle := s.generation.Bind(astCacheKey{pkgHandle.key, pgf.URI}, func(ctx context.Context, arg memoize.Arg) interface{} { + + // TODO(adonovan): opt: is it necessary to cache this operation? + // + // I expect the main benefit of CL 221021, which introduced it, + // was the replacement of PathEnclosingInterval, whose + // traversal is allocation-intensive, by buildASTCache. + // + // When run on the largest file in k8s, buildASTCache took + // ~6ms, but I expect most of that cost could be eliminated by + // using a stripped-down version of PathEnclosingInterval that + // cares only about syntax trees and not tokens. A stateless + // utility function that is cheap enough to call for each Pos + // would be a nice simplification. + // + // (The basic approach would be to use ast.Inspect, compare + // each node with the search Pos, and bail out as soon + // as a match is found. The pre-order hook would return false + // to avoid descending into any tree whose End is before + // the search Pos.) + // + // A representative benchmark would help. + astHandle, release := s.generation.GetHandle(astCacheKey{pkgHandle.key, pgf.URI}, func(ctx context.Context, arg memoize.Arg) interface{} { return buildASTCache(pgf) }) + defer release() d, err := astHandle.Get(ctx, s.generation, s) if err != nil { return nil, err } - data := d.(*astCacheData) - if data.err != nil { - return nil, data.err - } - return data, nil + return d.(*astCacheData), nil } func (s *snapshot) PosToDecl(ctx context.Context, spkg source.Package, pos token.Pos) (ast.Decl, error) { @@ -159,10 +177,15 @@ func (s *snapshot) PosToField(ctx context.Context, spkg source.Package, pos toke return data.posToField[pos], nil } +// An astCacheData maps object positions to syntax nodes for a single Go file. type astCacheData struct { - err error + // Maps the position of each name declared by a func/var/const/type + // Decl to the Decl node. Also maps the name and type of each field + // (broadly defined) to its innermost enclosing Decl. + posToDecl map[token.Pos]ast.Decl - posToDecl map[token.Pos]ast.Decl + // Maps the position of the Name and Type of each field + // (broadly defined) to the Field node. posToField map[token.Pos]*ast.Field } diff --git a/internal/lsp/source/signature_help.go b/internal/lsp/source/signature_help.go index 813f67e7b3b..12e359008fe 100644 --- a/internal/lsp/source/signature_help.go +++ b/internal/lsp/source/signature_help.go @@ -102,16 +102,7 @@ FindCall: if err != nil { return nil, 0, err } - rng, err := objToMappedRange(snapshot, pkg, obj) - if err != nil { - return nil, 0, err - } - decl := Declaration{ - obj: obj, - node: node, - } - decl.MappedRange = append(decl.MappedRange, rng) - d, err := FindHoverContext(ctx, snapshot, pkg, decl.obj, decl.node, nil) + d, err := FindHoverContext(ctx, snapshot, pkg, obj, node, nil) if err != nil { return nil, 0, err } diff --git a/internal/lsp/source/types_format.go b/internal/lsp/source/types_format.go index 93344e08678..5e10a509005 100644 --- a/internal/lsp/source/types_format.go +++ b/internal/lsp/source/types_format.go @@ -259,10 +259,11 @@ func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj * return types.TypeString(obj.Type(), qf) } - expr, err := varType(ctx, snapshot, pkg, obj) - if err != nil { + field, err := snapshot.PosToField(ctx, pkg, obj.Pos()) + if err != nil || field == nil { return types.TypeString(obj.Type(), qf) } + expr := field.Type // If the given expr refers to a type parameter, then use the // object's Type instead of the type parameter declaration. This helps @@ -286,18 +287,6 @@ func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj * return fmted } -// varType returns the type expression for a *types.Var. -func varType(ctx context.Context, snapshot Snapshot, pkg Package, obj *types.Var) (ast.Expr, error) { - field, err := snapshot.PosToField(ctx, pkg, obj.Pos()) - if err != nil { - return nil, err - } - if field == nil { - return nil, fmt.Errorf("no declaration for object %s", obj.Name()) - } - return field.Type, nil -} - // qualifyExpr applies the "pkgName." prefix to any *ast.Ident in the expr. func qualifyExpr(expr ast.Expr, srcpkg, pkg Package, clonedInfo map[token.Pos]*types.PkgName, qf types.Qualifier) ast.Expr { ast.Inspect(expr, func(n ast.Node) bool { diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go index 10970389290..8d205ee6cee 100644 --- a/internal/lsp/source/util.go +++ b/internal/lsp/source/util.go @@ -311,15 +311,15 @@ func FindPackageFromPos(ctx context.Context, snapshot Snapshot, pos token.Pos) ( for _, pkg := range pkgs { parsed, err := pkg.File(uri) if err != nil { + // TODO(adonovan): should this be a bug.Report or log.Fatal? + // The logic in Identifier seems to think so. + // Should it be a postcondition of PackagesForFile? + // And perhaps PackagesForFile should return the PGFs too. return nil, err } - if parsed == nil { - continue - } - if parsed.Tok.Base() != tok.Base() { - continue + if parsed != nil && parsed.Tok.Base() == tok.Base() { + return pkg, nil } - return pkg, nil } return nil, fmt.Errorf("no package for given file position") } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 98d11517d87..c8656153bd3 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -83,11 +83,15 @@ type Snapshot interface { // to quickly find corresponding *ast.Field node given a *types.Var. // We must refer to the AST to render type aliases properly when // formatting signatures and other types. + // May return (nil, nil) if the file didn't declare an object at that position. + // TODO(adonovan): seems like a bug? PosToField(ctx context.Context, pkg Package, pos token.Pos) (*ast.Field, error) // PosToDecl maps certain objects' positions to their surrounding // ast.Decl. This mapping is used when building the documentation // string for the objects. + // May return (nil, nil) if the file didn't declare an object at that position. + // TODO(adonovan): seems like a bug? PosToDecl(ctx context.Context, pkg Package, pos token.Pos) (ast.Decl, error) // DiagnosePackage returns basic diagnostics, including list, parse, and type errors @@ -147,8 +151,8 @@ type Snapshot interface { // IsBuiltin reports whether uri is part of the builtin package. IsBuiltin(ctx context.Context, uri span.URI) bool - // PackagesForFile returns the packages that this file belongs to, checked - // in mode. + // PackagesForFile returns an unordered list of packages that contain + // the file denoted by uri, type checked in the specified mode. PackagesForFile(ctx context.Context, uri span.URI, mode TypecheckMode, includeTestVariants bool) ([]Package, error) // PackageForFile returns a single package that this file belongs to, From 2aef121b8361efd5b8d56dd25a1ec046c50a4e01 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 2 Jun 2022 13:33:11 -0400 Subject: [PATCH 082/723] internal/lsp: consolidate .go/go.mod link logic Now that we have a token.File (albeit throwaway) for a parsed go.mod file, we can combine the .go and go.mod logic for turning it into a protocol.DocumentLink. Change-Id: Id1783644cbd450f0e8dc807beb8ba625675d8540 Reviewed-on: https://go-review.googlesource.com/c/tools/+/410136 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- internal/lsp/link.go | 66 +++++++++++++++----------------------------- 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/internal/lsp/link.go b/internal/lsp/link.go index a2962b6659a..65da8a54c31 100644 --- a/internal/lsp/link.go +++ b/internal/lsp/link.go @@ -49,6 +49,8 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl if err != nil { return nil, err } + tokFile := pm.Mapper.TokFile + var links []protocol.DocumentLink for _, req := range pm.File.Require { if req.Syntax == nil { @@ -66,9 +68,9 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl } // Shift the start position to the location of the // dependency within the require statement. - start, end := token.Pos(s+i), token.Pos(s+i+len(dep)) + start, end := tokFile.Pos(s+i), tokFile.Pos(s+i+len(dep)) target := source.BuildLink(snapshot.View().Options().LinkTarget, "mod/"+req.Mod.String(), "") - l, err := toProtocolLink(nil, pm.Mapper, target, start, end, source.Mod) + l, err := toProtocolLink(tokFile, pm.Mapper, target, start, end) if err != nil { return nil, err } @@ -79,9 +81,6 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl return links, nil } - // Create a throwaway token.File. - tokFile := token.NewFileSet().AddFile(fh.URI().Filename(), -1, len(pm.Mapper.Content)) - // Get all the links that are contained in the comments of the file. for _, expr := range pm.File.Syntax.Stmt { comments := expr.Comment() @@ -91,7 +90,7 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} { for _, comment := range section { start := tokFile.Pos(comment.Start.Byte) - l, err := findLinksInString(ctx, snapshot, comment.Token, start, tokFile, pm.Mapper, source.Mod) + l, err := findLinksInString(ctx, snapshot, comment.Token, start, tokFile, pm.Mapper) if err != nil { return nil, err } @@ -148,8 +147,8 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle // Account for the quotation marks in the positions. start := imp.Path.Pos() + 1 end := imp.Path.End() - 1 - target = source.BuildLink(view.Options().LinkTarget, target, "") - l, err := toProtocolLink(pgf.Tok, pgf.Mapper, target, start, end, source.Go) + targetURL := source.BuildLink(view.Options().LinkTarget, target, "") + l, err := toProtocolLink(pgf.Tok, pgf.Mapper, targetURL, start, end) if err != nil { return nil, err } @@ -157,7 +156,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle } } for _, s := range str { - l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Tok, pgf.Mapper, source.Go) + l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Tok, pgf.Mapper) if err != nil { return nil, err } @@ -165,7 +164,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle } for _, commentGroup := range pgf.File.Comments { for _, comment := range commentGroup.List { - l, err := findLinksInString(ctx, snapshot, comment.Text, comment.Pos(), pgf.Tok, pgf.Mapper, source.Go) + l, err := findLinksInString(ctx, snapshot, comment.Text, comment.Pos(), pgf.Tok, pgf.Mapper) if err != nil { return nil, err } @@ -199,7 +198,7 @@ var acceptedSchemes = map[string]bool{ } // tokFile may be a throwaway File for non-Go files. -func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string, pos token.Pos, tokFile *token.File, m *protocol.ColumnMapper, fileKind source.FileKind) ([]protocol.DocumentLink, error) { +func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string, pos token.Pos, tokFile *token.File, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) { var links []protocol.DocumentLink for _, index := range snapshot.View().Options().URLRegexp.FindAllIndex([]byte(src), -1) { start, end := index[0], index[1] @@ -222,7 +221,7 @@ func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string if !acceptedSchemes[linkURL.Scheme] { continue } - l, err := toProtocolLink(tokFile, m, linkURL.String(), startPos, endPos, fileKind) + l, err := toProtocolLink(tokFile, m, linkURL.String(), startPos, endPos) if err != nil { return nil, err } @@ -239,8 +238,8 @@ func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string continue } org, repo, number := matches[1], matches[2], matches[3] - target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) - l, err := toProtocolLink(tokFile, m, target, startPos, endPos, fileKind) + targetURL := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) + l, err := toProtocolLink(tokFile, m, targetURL, startPos, endPos) if err != nil { return nil, err } @@ -261,38 +260,17 @@ var ( issueRegexp *regexp.Regexp ) -func toProtocolLink(tokFile *token.File, m *protocol.ColumnMapper, target string, start, end token.Pos, fileKind source.FileKind) (protocol.DocumentLink, error) { - var rng protocol.Range - switch fileKind { - case source.Go: - // TODO(adonovan): can we now use this logic for the Mod case too? - spn, err := span.NewRange(tokFile, start, end).Span() - if err != nil { - return protocol.DocumentLink{}, err - } - rng, err = m.Range(spn) - if err != nil { - return protocol.DocumentLink{}, err - } - case source.Mod: - s, e := int(start), int(end) - line, col, err := span.ToPosition(m.TokFile, s) - if err != nil { - return protocol.DocumentLink{}, err - } - start := span.NewPoint(line, col, s) - line, col, err = span.ToPosition(m.TokFile, e) - if err != nil { - return protocol.DocumentLink{}, err - } - end := span.NewPoint(line, col, e) - rng, err = m.Range(span.New(m.URI, start, end)) - if err != nil { - return protocol.DocumentLink{}, err - } +func toProtocolLink(tokFile *token.File, m *protocol.ColumnMapper, targetURL string, start, end token.Pos) (protocol.DocumentLink, error) { + spn, err := span.NewRange(tokFile, start, end).Span() + if err != nil { + return protocol.DocumentLink{}, err + } + rng, err := m.Range(spn) + if err != nil { + return protocol.DocumentLink{}, err } return protocol.DocumentLink{ Range: rng, - Target: target, + Target: targetURL, }, nil } From 1dfab61a4877c8b77d3b89afe7b36b74d3dba889 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 4 Jul 2022 13:22:21 -0400 Subject: [PATCH 083/723] internal/lsp/cache: use GetHandle not Bind for 5 URI-keyed maps This change replaces the 5 remaining calls to Bind (generational lifetime) with GetHandle (reference counting). The handles are now stored in persistent.Maps, which simplifies the invalidation logic. All 5 have span.URIs as keys: symbolizeHandles parse{Mod,Work}Handles mod{Tidy,Why}Handles Also, factor the functions that use these maps to have a common form: - a fooImpl function that returns an R result and an error; - a foo wrapper that decorates it with caching. - a local fooResult type, defined struct{R; error} that is the cache entry. The functions for getting/setting map entries are all inlined. The fooHandle types are all replaced by *memoize.Handle, now that their use is local. No behavior change is intended. The other uses of Bind are deleted in these CLs: https://go-review.googlesource.com/c/tools/+/415975 (astCacheData) https://go-review.googlesource.com/c/tools/+/415504 (actions) Change-Id: I77cc4e828936fe171152ca13a12f7a639299e9e5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415976 Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- internal/lsp/cache/maps.go | 13 +- internal/lsp/cache/mod.go | 408 ++++++++++++++++----------------- internal/lsp/cache/mod_tidy.go | 214 +++++++++-------- internal/lsp/cache/session.go | 10 +- internal/lsp/cache/snapshot.go | 149 +++--------- internal/lsp/cache/symbols.go | 59 ++--- internal/persistent/map.go | 9 +- 7 files changed, 405 insertions(+), 457 deletions(-) diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go index f8e03057cfd..1ec34151540 100644 --- a/internal/lsp/cache/maps.go +++ b/internal/lsp/cache/maps.go @@ -16,11 +16,14 @@ type filesMap struct { impl *persistent.Map } +// uriLessInterface is the < relation for "any" values containing span.URIs. +func uriLessInterface(a, b interface{}) bool { + return a.(span.URI) < b.(span.URI) +} + func newFilesMap() filesMap { return filesMap{ - impl: persistent.NewMap(func(a, b interface{}) bool { - return a.(span.URI) < b.(span.URI) - }), + impl: persistent.NewMap(uriLessInterface), } } @@ -152,9 +155,7 @@ type parseKeysByURIMap struct { func newParseKeysByURIMap() parseKeysByURIMap { return parseKeysByURIMap{ - impl: persistent.NewMap(func(a, b interface{}) bool { - return a.(span.URI) < b.(span.URI) - }), + impl: persistent.NewMap(uriLessInterface), } } diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index 843919d7b36..1963feea5ac 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -24,152 +24,156 @@ import ( "golang.org/x/tools/internal/span" ) -type parseModHandle struct { - handle *memoize.Handle -} +// ParseMod parses a go.mod file, using a cache. It may return partial results and an error. +func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) { + uri := fh.URI() -type parseModData struct { - parsed *source.ParsedModule + s.mu.Lock() + entry, hit := s.parseModHandles.Get(uri) + s.mu.Unlock() - // err is any error encountered while parsing the file. - err error -} + type parseModResult struct { + parsed *source.ParsedModule + err error + } -func (mh *parseModHandle) parse(ctx context.Context, snapshot *snapshot) (*source.ParsedModule, error) { - v, err := mh.handle.Get(ctx, snapshot.generation, snapshot) + // cache miss? + if !hit { + handle, release := s.generation.GetHandle(fh.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} { + parsed, err := parseModImpl(ctx, fh) + return parseModResult{parsed, err} + }) + + entry = handle + s.mu.Lock() + s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.mu.Unlock() + } + + // Await result. + v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) if err != nil { return nil, err } - data := v.(*parseModData) - return data.parsed, data.err + res := v.(parseModResult) + return res.parsed, res.err } -func (s *snapshot) ParseMod(ctx context.Context, modFH source.FileHandle) (*source.ParsedModule, error) { - if handle := s.getParseModHandle(modFH.URI()); handle != nil { - return handle.parse(ctx, s) - } - h := s.generation.Bind(modFH.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} { - _, done := event.Start(ctx, "cache.ParseModHandle", tag.URI.Of(modFH.URI())) - defer done() +// parseModImpl parses the go.mod file whose name and contents are in fh. +// It may return partial results and an error. +func parseModImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) { + _, done := event.Start(ctx, "cache.ParseMod", tag.URI.Of(fh.URI())) + defer done() - contents, err := modFH.Read() - if err != nil { - return &parseModData{err: err} - } - m := protocol.NewColumnMapper(modFH.URI(), contents) - file, parseErr := modfile.Parse(modFH.URI().Filename(), contents, nil) - // Attempt to convert the error to a standardized parse error. - var parseErrors []*source.Diagnostic - if parseErr != nil { - mfErrList, ok := parseErr.(modfile.ErrorList) - if !ok { - return &parseModData{err: fmt.Errorf("unexpected parse error type %v", parseErr)} - } - for _, mfErr := range mfErrList { - rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos) - if err != nil { - return &parseModData{err: err} - } - parseErrors = append(parseErrors, &source.Diagnostic{ - URI: modFH.URI(), - Range: rng, - Severity: protocol.SeverityError, - Source: source.ParseError, - Message: mfErr.Err.Error(), - }) + contents, err := fh.Read() + if err != nil { + return nil, err + } + m := protocol.NewColumnMapper(fh.URI(), contents) + file, parseErr := modfile.Parse(fh.URI().Filename(), contents, nil) + // Attempt to convert the error to a standardized parse error. + var parseErrors []*source.Diagnostic + if parseErr != nil { + mfErrList, ok := parseErr.(modfile.ErrorList) + if !ok { + return nil, fmt.Errorf("unexpected parse error type %v", parseErr) + } + for _, mfErr := range mfErrList { + rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos) + if err != nil { + return nil, err } + parseErrors = append(parseErrors, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityError, + Source: source.ParseError, + Message: mfErr.Err.Error(), + }) } - return &parseModData{ - parsed: &source.ParsedModule{ - URI: modFH.URI(), - Mapper: m, - File: file, - ParseErrors: parseErrors, - }, - err: parseErr, - } - }) + } + return &source.ParsedModule{ + URI: fh.URI(), + Mapper: m, + File: file, + ParseErrors: parseErrors, + }, parseErr +} + +// ParseWork parses a go.work file, using a cache. It may return partial results and an error. +// TODO(adonovan): move to new work.go file. +func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) { + uri := fh.URI() - pmh := &parseModHandle{handle: h} s.mu.Lock() - s.parseModHandles[modFH.URI()] = pmh + entry, hit := s.parseWorkHandles.Get(uri) s.mu.Unlock() - return pmh.parse(ctx, s) -} - -type parseWorkHandle struct { - handle *memoize.Handle -} + type parseWorkResult struct { + parsed *source.ParsedWorkFile + err error + } -type parseWorkData struct { - parsed *source.ParsedWorkFile + // cache miss? + if !hit { + handle, release := s.generation.GetHandle(fh.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} { + parsed, err := parseWorkImpl(ctx, fh) + return parseWorkResult{parsed, err} + }) - // err is any error encountered while parsing the file. - err error -} + entry = handle + s.mu.Lock() + s.parseWorkHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.mu.Unlock() + } -func (mh *parseWorkHandle) parse(ctx context.Context, snapshot *snapshot) (*source.ParsedWorkFile, error) { - v, err := mh.handle.Get(ctx, snapshot.generation, snapshot) + // Await result. + v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) if err != nil { return nil, err } - data := v.(*parseWorkData) - return data.parsed, data.err + res := v.(parseWorkResult) + return res.parsed, res.err } -func (s *snapshot) ParseWork(ctx context.Context, modFH source.FileHandle) (*source.ParsedWorkFile, error) { - if handle := s.getParseWorkHandle(modFH.URI()); handle != nil { - return handle.parse(ctx, s) - } - h := s.generation.Bind(modFH.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} { - _, done := event.Start(ctx, "cache.ParseModHandle", tag.URI.Of(modFH.URI())) - defer done() +// parseWorkImpl parses a go.work file. It may return partial results and an error. +func parseWorkImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) { + _, done := event.Start(ctx, "cache.ParseWork", tag.URI.Of(fh.URI())) + defer done() - contents, err := modFH.Read() - if err != nil { - return &parseWorkData{err: err} - } - m := protocol.NewColumnMapper(modFH.URI(), contents) - file, parseErr := modfile.ParseWork(modFH.URI().Filename(), contents, nil) - // Attempt to convert the error to a standardized parse error. - var parseErrors []*source.Diagnostic - if parseErr != nil { - mfErrList, ok := parseErr.(modfile.ErrorList) - if !ok { - return &parseWorkData{err: fmt.Errorf("unexpected parse error type %v", parseErr)} - } - for _, mfErr := range mfErrList { - rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos) - if err != nil { - return &parseWorkData{err: err} - } - parseErrors = append(parseErrors, &source.Diagnostic{ - URI: modFH.URI(), - Range: rng, - Severity: protocol.SeverityError, - Source: source.ParseError, - Message: mfErr.Err.Error(), - }) + contents, err := fh.Read() + if err != nil { + return nil, err + } + m := protocol.NewColumnMapper(fh.URI(), contents) + file, parseErr := modfile.ParseWork(fh.URI().Filename(), contents, nil) + // Attempt to convert the error to a standardized parse error. + var parseErrors []*source.Diagnostic + if parseErr != nil { + mfErrList, ok := parseErr.(modfile.ErrorList) + if !ok { + return nil, fmt.Errorf("unexpected parse error type %v", parseErr) + } + for _, mfErr := range mfErrList { + rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos) + if err != nil { + return nil, err } + parseErrors = append(parseErrors, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityError, + Source: source.ParseError, + Message: mfErr.Err.Error(), + }) } - return &parseWorkData{ - parsed: &source.ParsedWorkFile{ - URI: modFH.URI(), - Mapper: m, - File: file, - ParseErrors: parseErrors, - }, - err: parseErr, - } - }) - - pwh := &parseWorkHandle{handle: h} - s.mu.Lock() - s.parseWorkHandles[modFH.URI()] = pwh - s.mu.Unlock() - - return pwh.parse(ctx, s) + } + return &source.ParsedWorkFile{ + URI: fh.URI(), + Mapper: m, + File: file, + ParseErrors: parseErrors, + }, parseErr } // goSum reads the go.sum file for the go.mod file at modURI, if it exists. If @@ -198,104 +202,100 @@ func sumFilename(modURI span.URI) string { return strings.TrimSuffix(modURI.Filename(), ".mod") + ".sum" } -// modKey is uniquely identifies cached data for `go mod why` or dependencies -// to upgrade. -type modKey struct { - sessionID string - env source.Hash - view string - mod source.FileIdentity - verb modAction -} +// ModWhy returns the "go mod why" result for each module named in a +// require statement in the go.mod file. +// TODO(adonovan): move to new mod_why.go file. +func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) { + uri := fh.URI() -type modAction int + if s.View().FileKind(fh) != source.Mod { + return nil, fmt.Errorf("%s is not a go.mod file", uri) + } -const ( - why modAction = iota - upgrade -) + s.mu.Lock() + entry, hit := s.modWhyHandles.Get(uri) + s.mu.Unlock() -type modWhyHandle struct { - handle *memoize.Handle -} + type modWhyResult struct { + why map[string]string + err error + } -type modWhyData struct { - // why keeps track of the `go mod why` results for each require statement - // in the go.mod file. - why map[string]string + // cache miss? + if !hit { + // TODO(adonovan): use a simpler cache of promises that + // is shared across snapshots. See comment at modTidyKey. + type modWhyKey struct { + // TODO(rfindley): is sessionID used to identify overlays because modWhy + // looks at overlay state? In that case, I am not sure that this key + // is actually correct. The key should probably just be URI, and + // invalidated in clone when any import changes. + sessionID string + env source.Hash + view string + mod source.FileIdentity + } + key := modWhyKey{ + sessionID: s.view.session.id, + env: hashEnv(s), + mod: fh.FileIdentity(), + view: s.view.rootURI.Filename(), + } + handle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { + why, err := modWhyImpl(ctx, arg.(*snapshot), fh) + return modWhyResult{why, err} + }) - err error -} + entry = handle + s.mu.Lock() + s.modWhyHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.mu.Unlock() + } -func (mwh *modWhyHandle) why(ctx context.Context, snapshot *snapshot) (map[string]string, error) { - v, err := mwh.handle.Get(ctx, snapshot.generation, snapshot) + // Await result. + v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) if err != nil { return nil, err } - data := v.(*modWhyData) - return data.why, data.err + res := v.(modWhyResult) + return res.why, res.err } -func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) { - if s.View().FileKind(fh) != source.Mod { - return nil, fmt.Errorf("%s is not a go.mod file", fh.URI()) +// modWhyImpl returns the result of "go mod why -m" on the specified go.mod file. +func modWhyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle) (map[string]string, error) { + ctx, done := event.Start(ctx, "cache.ModWhy", tag.URI.Of(fh.URI())) + defer done() + + pm, err := snapshot.ParseMod(ctx, fh) + if err != nil { + return nil, err } - if handle := s.getModWhyHandle(fh.URI()); handle != nil { - return handle.why(ctx, s) + // No requires to explain. + if len(pm.File.Require) == 0 { + return nil, nil // empty result } - key := modKey{ - sessionID: s.view.session.id, - env: hashEnv(s), - mod: fh.FileIdentity(), - view: s.view.rootURI.Filename(), - verb: why, + // Run `go mod why` on all the dependencies. + inv := &gocommand.Invocation{ + Verb: "mod", + Args: []string{"why", "-m"}, + WorkingDir: filepath.Dir(fh.URI().Filename()), } - h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { - ctx, done := event.Start(ctx, "cache.ModWhyHandle", tag.URI.Of(fh.URI())) - defer done() - - snapshot := arg.(*snapshot) - - pm, err := snapshot.ParseMod(ctx, fh) - if err != nil { - return &modWhyData{err: err} - } - // No requires to explain. - if len(pm.File.Require) == 0 { - return &modWhyData{} - } - // Run `go mod why` on all the dependencies. - inv := &gocommand.Invocation{ - Verb: "mod", - Args: []string{"why", "-m"}, - WorkingDir: filepath.Dir(fh.URI().Filename()), - } - for _, req := range pm.File.Require { - inv.Args = append(inv.Args, req.Mod.Path) - } - stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal, inv) - if err != nil { - return &modWhyData{err: err} - } - whyList := strings.Split(stdout.String(), "\n\n") - if len(whyList) != len(pm.File.Require) { - return &modWhyData{ - err: fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)), - } - } - why := make(map[string]string, len(pm.File.Require)) - for i, req := range pm.File.Require { - why[req.Mod.Path] = whyList[i] - } - return &modWhyData{why: why} - }) - - mwh := &modWhyHandle{handle: h} - s.mu.Lock() - s.modWhyHandles[fh.URI()] = mwh - s.mu.Unlock() - - return mwh.why(ctx, s) + for _, req := range pm.File.Require { + inv.Args = append(inv.Args, req.Mod.Path) + } + stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal, inv) + if err != nil { + return nil, err + } + whyList := strings.Split(stdout.String(), "\n\n") + if len(whyList) != len(pm.File.Require) { + return nil, fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)) + } + why := make(map[string]string, len(pm.File.Require)) + for i, req := range pm.File.Require { + why[req.Mod.Path] = whyList[i] + } + return why, nil } // extractGoCommandError tries to parse errors that come from the go command diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index 91394659503..84f369ef3d4 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -28,125 +28,139 @@ import ( "golang.org/x/tools/internal/span" ) -type modTidyKey struct { - sessionID string - env source.Hash - gomod source.FileIdentity - imports source.Hash - unsavedOverlays source.Hash - view string -} +// modTidyImpl runs "go mod tidy" on a go.mod file, using a cache. +// +// REVIEWERS: what does it mean to cache an operation that has side effects? +// Or are we de-duplicating operations in flight on the same file? +func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) { + uri := pm.URI + if pm.File == nil { + return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", uri) + } -type modTidyHandle struct { - handle *memoize.Handle -} + s.mu.Lock() + entry, hit := s.modTidyHandles.Get(uri) + s.mu.Unlock() -type modTidyData struct { - tidied *source.TidiedModule - err error -} + type modTidyResult struct { + tidied *source.TidiedModule + err error + } + + // Cache miss? + if !hit { + fh, err := s.GetFile(ctx, pm.URI) + if err != nil { + return nil, err + } + // If the file handle is an overlay, it may not be written to disk. + // The go.mod file has to be on disk for `go mod tidy` to work. + // TODO(rfindley): is this still true with Go 1.16 overlay support? + if _, ok := fh.(*overlay); ok { + if info, _ := os.Stat(fh.URI().Filename()); info == nil { + return nil, source.ErrNoModOnDisk + } + } + if criticalErr := s.GetCriticalError(ctx); criticalErr != nil { + return &source.TidiedModule{ + Diagnostics: criticalErr.DiagList, + }, nil + } + workspacePkgs, err := s.workspacePackageHandles(ctx) + if err != nil { + return nil, err + } + + s.mu.Lock() + overlayHash := hashUnsavedOverlays(s.files) + s.mu.Unlock() + + // There's little reason at to use the shared cache for mod + // tidy (and mod why) as their key includes the view and session. + // TODO(adonovan): use a simpler cache of promises that + // is shared across snapshots. + type modTidyKey struct { + // TODO(rfindley): this key is also suspicious (see modWhyKey). + sessionID string + env source.Hash + gomod source.FileIdentity + imports source.Hash + unsavedOverlays source.Hash + view string + } + key := modTidyKey{ + sessionID: s.view.session.id, + view: s.view.folder.Filename(), + imports: s.hashImports(ctx, workspacePkgs), + unsavedOverlays: overlayHash, + gomod: fh.FileIdentity(), + env: hashEnv(s), + } + handle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { + tidied, err := modTidyImpl(ctx, arg.(*snapshot), fh, pm, workspacePkgs) + return modTidyResult{tidied, err} + }) -func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) { - v, err := mth.handle.Get(ctx, snapshot.generation, snapshot) + entry = handle + s.mu.Lock() + s.modTidyHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.mu.Unlock() + } + + // Await result. + v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) if err != nil { return nil, err } - data := v.(*modTidyData) - return data.tidied, data.err + res := v.(modTidyResult) + return res.tidied, res.err } -func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) { - if pm.File == nil { - return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", pm.URI) - } - if handle := s.getModTidyHandle(pm.URI); handle != nil { - return handle.tidy(ctx, s) +// modTidyImpl runs "go mod tidy" on a go.mod file. +func modTidyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle, pm *source.ParsedModule, workspacePkgs []*packageHandle) (*source.TidiedModule, error) { + ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(fh.URI())) + defer done() + + inv := &gocommand.Invocation{ + Verb: "mod", + Args: []string{"tidy"}, + WorkingDir: filepath.Dir(fh.URI().Filename()), } - fh, err := s.GetFile(ctx, pm.URI) + tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv) if err != nil { return nil, err } - // If the file handle is an overlay, it may not be written to disk. - // The go.mod file has to be on disk for `go mod tidy` to work. - if _, ok := fh.(*overlay); ok { - if info, _ := os.Stat(fh.URI().Filename()); info == nil { - return nil, source.ErrNoModOnDisk - } + // Keep the temporary go.mod file around long enough to parse it. + defer cleanup() + + if _, err := snapshot.view.session.gocmdRunner.Run(ctx, *inv); err != nil { + return nil, err } - if criticalErr := s.GetCriticalError(ctx); criticalErr != nil { - return &source.TidiedModule{ - Diagnostics: criticalErr.DiagList, - }, nil + + // Go directly to disk to get the temporary mod file, + // since it is always on disk. + tempContents, err := ioutil.ReadFile(tmpURI.Filename()) + if err != nil { + return nil, err } - workspacePkgs, err := s.workspacePackageHandles(ctx) + ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil) if err != nil { + // We do not need to worry about the temporary file's parse errors + // since it has been "tidied". return nil, err } - s.mu.Lock() - overlayHash := hashUnsavedOverlays(s.files) - s.mu.Unlock() - - key := modTidyKey{ - sessionID: s.view.session.id, - view: s.view.folder.Filename(), - imports: s.hashImports(ctx, workspacePkgs), - unsavedOverlays: overlayHash, - gomod: fh.FileIdentity(), - env: hashEnv(s), - } - h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { - ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI())) - defer done() - - snapshot := arg.(*snapshot) - inv := &gocommand.Invocation{ - Verb: "mod", - Args: []string{"tidy"}, - WorkingDir: filepath.Dir(fh.URI().Filename()), - } - tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv) - if err != nil { - return &modTidyData{err: err} - } - // Keep the temporary go.mod file around long enough to parse it. - defer cleanup() - - if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil { - return &modTidyData{err: err} - } - // Go directly to disk to get the temporary mod file, since it is - // always on disk. - tempContents, err := ioutil.ReadFile(tmpURI.Filename()) - if err != nil { - return &modTidyData{err: err} - } - ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil) - if err != nil { - // We do not need to worry about the temporary file's parse errors - // since it has been "tidied". - return &modTidyData{err: err} - } - // Compare the original and tidied go.mod files to compute errors and - // suggested fixes. - diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs) - if err != nil { - return &modTidyData{err: err} - } - return &modTidyData{ - tidied: &source.TidiedModule{ - Diagnostics: diagnostics, - TidiedContent: tempContents, - }, - } - }) - - mth := &modTidyHandle{handle: h} - s.mu.Lock() - s.modTidyHandles[fh.URI()] = mth - s.mu.Unlock() + // Compare the original and tidied go.mod files to compute errors and + // suggested fixes. + diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs) + if err != nil { + return nil, err + } - return mth.tidy(ctx, s) + return &source.TidiedModule{ + Diagnostics: diagnostics, + TidiedContent: tempContents, + }, nil } func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) source.Hash { diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 98d3c250433..80468bc5931 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -238,14 +238,14 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, isActivePackageCache: newIsActivePackageCacheMap(), goFiles: newGoFilesMap(), parseKeysByURI: newParseKeysByURIMap(), - symbols: make(map[span.URI]*symbolHandle), + symbolizeHandles: persistent.NewMap(uriLessInterface), actions: persistent.NewMap(actionKeyLessInterface), workspacePackages: make(map[PackageID]PackagePath), unloadableFiles: make(map[span.URI]struct{}), - parseModHandles: make(map[span.URI]*parseModHandle), - parseWorkHandles: make(map[span.URI]*parseWorkHandle), - modTidyHandles: make(map[span.URI]*modTidyHandle), - modWhyHandles: make(map[span.URI]*modWhyHandle), + parseModHandles: persistent.NewMap(uriLessInterface), + parseWorkHandles: persistent.NewMap(uriLessInterface), + modTidyHandles: persistent.NewMap(uriLessInterface), + modWhyHandles: persistent.NewMap(uriLessInterface), knownSubdirs: newKnownDirsSet(), workspace: workspace, } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 93316653af4..b962435b62e 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -85,8 +85,9 @@ type snapshot struct { goFiles goFilesMap parseKeysByURI parseKeysByURIMap - // TODO(rfindley): consider merging this with files to reduce burden on clone. - symbols map[span.URI]*symbolHandle + // symbolizeHandles maps each file URI to a handle for the future + // result of computing the symbols declared in that file. + symbolizeHandles *persistent.Map // from span.URI to *memoize.Handle // packages maps a packageKey to a *packageHandle. // It may be invalidated when a file's content changes. @@ -109,17 +110,17 @@ type snapshot struct { // parseModHandles keeps track of any parseModHandles for the snapshot. // The handles need not refer to only the view's go.mod file. - parseModHandles map[span.URI]*parseModHandle + parseModHandles *persistent.Map // from span.URI to *memoize.Handle // parseWorkHandles keeps track of any parseWorkHandles for the snapshot. // The handles need not refer to only the view's go.work file. - parseWorkHandles map[span.URI]*parseWorkHandle + parseWorkHandles *persistent.Map // from span.URI to *memoize.Handle // Preserve go.mod-related handles to avoid garbage-collecting the results // of various calls to the go command. The handles need not refer to only // the view's go.mod file. - modTidyHandles map[span.URI]*modTidyHandle - modWhyHandles map[span.URI]*modWhyHandle + modTidyHandles *persistent.Map // from span.URI to *memoize.Handle + modWhyHandles *persistent.Map // from span.URI to *memoize.Handle workspace *workspace // (not guarded by mu) @@ -156,6 +157,11 @@ func (s *snapshot) Destroy(destroyedBy string) { s.goFiles.Destroy() s.parseKeysByURI.Destroy() s.knownSubdirs.Destroy() + s.symbolizeHandles.Destroy() + s.parseModHandles.Destroy() + s.parseWorkHandles.Destroy() + s.modTidyHandles.Destroy() + s.modWhyHandles.Destroy() if s.workspaceDir != "" { if err := os.RemoveAll(s.workspaceDir); err != nil { @@ -700,30 +706,6 @@ func (s *snapshot) addGoFile(key parseKey, pgh *parseGoHandle, release func()) * return pgh } -func (s *snapshot) getParseModHandle(uri span.URI) *parseModHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.parseModHandles[uri] -} - -func (s *snapshot) getParseWorkHandle(uri span.URI) *parseWorkHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.parseWorkHandles[uri] -} - -func (s *snapshot) getModWhyHandle(uri span.URI) *modWhyHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.modWhyHandles[uri] -} - -func (s *snapshot) getModTidyHandle(uri span.URI) *modTidyHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.modTidyHandles[uri] -} - func (s *snapshot) getImportedBy(id PackageID) []PackageID { s.mu.Lock() defer s.mu.Unlock() @@ -1039,12 +1021,12 @@ func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { iolimit <- struct{}{} // acquire token group.Go(func() error { defer func() { <-iolimit }() // release token - v, err := s.buildSymbolHandle(ctx, f).handle.Get(ctx, s.generation, s) + symbols, err := s.symbolize(ctx, f) if err != nil { return err } resultMu.Lock() - result[uri] = v.(*symbolData).symbols + result[uri] = symbols resultMu.Unlock() return nil }) @@ -1159,26 +1141,6 @@ func (s *snapshot) getPackage(id PackageID, mode source.ParseMode) *packageHandl return ph } -func (s *snapshot) getSymbolHandle(uri span.URI) *symbolHandle { - s.mu.Lock() - defer s.mu.Unlock() - - return s.symbols[uri] -} - -func (s *snapshot) addSymbolHandle(uri span.URI, sh *symbolHandle) *symbolHandle { - s.mu.Lock() - defer s.mu.Unlock() - - // If the package handle has already been cached, - // return the cached handle instead of overriding it. - if sh, ok := s.symbols[uri]; ok { - return sh - } - s.symbols[uri] = sh - return sh -} - func (s *snapshot) getActionHandle(id PackageID, m source.ParseMode, a *analysis.Analyzer) *actionHandle { key := actionKey{ pkg: packageKey{ @@ -1732,42 +1694,23 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC files: s.files.Clone(), goFiles: s.goFiles.Clone(), parseKeysByURI: s.parseKeysByURI.Clone(), - symbols: make(map[span.URI]*symbolHandle, len(s.symbols)), + symbolizeHandles: s.symbolizeHandles.Clone(), workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)), unloadableFiles: make(map[span.URI]struct{}, len(s.unloadableFiles)), - parseModHandles: make(map[span.URI]*parseModHandle, len(s.parseModHandles)), - parseWorkHandles: make(map[span.URI]*parseWorkHandle, len(s.parseWorkHandles)), - modTidyHandles: make(map[span.URI]*modTidyHandle, len(s.modTidyHandles)), - modWhyHandles: make(map[span.URI]*modWhyHandle, len(s.modWhyHandles)), + parseModHandles: s.parseModHandles.Clone(), + parseWorkHandles: s.parseWorkHandles.Clone(), + modTidyHandles: s.modTidyHandles.Clone(), + modWhyHandles: s.modWhyHandles.Clone(), knownSubdirs: s.knownSubdirs.Clone(), workspace: newWorkspace, } - // Copy all of the FileHandles. - for k, v := range s.symbols { - if change, ok := changes[k]; ok { - if change.exists { - result.symbols[k] = result.buildSymbolHandle(ctx, change.fileHandle) - } - continue - } - newGen.Inherit(v.handle) - result.symbols[k] = v - } - // Copy the set of unloadable files. for k, v := range s.unloadableFiles { result.unloadableFiles[k] = v } - // Copy all of the modHandles. - for k, v := range s.parseModHandles { - result.parseModHandles[k] = v - } - // Copy all of the parseWorkHandles. - for k, v := range s.parseWorkHandles { - result.parseWorkHandles[k] = v - } + // TODO(adonovan): merge loops over "changes". for uri := range changes { keys, ok := result.parseKeysByURI.Get(uri) if ok { @@ -1776,21 +1719,13 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } result.parseKeysByURI.Delete(uri) } - } - // Copy all of the go.mod-related handles. They may be invalidated later, - // so we inherit them at the end of the function. - for k, v := range s.modTidyHandles { - if _, ok := changes[k]; ok { - continue - } - result.modTidyHandles[k] = v - } - for k, v := range s.modWhyHandles { - if _, ok := changes[k]; ok { - continue - } - result.modWhyHandles[k] = v + // Invalidate go.mod-related handles. + result.modTidyHandles.Delete(uri) + result.modWhyHandles.Delete(uri) + + // Invalidate handles for cached symbols. + result.symbolizeHandles.Delete(uri) } // Add all of the known subdirectories, but don't update them for the @@ -1857,17 +1792,16 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Invalidate the previous modTidyHandle if any of the files have been // saved or if any of the metadata has been invalidated. if invalidateMetadata || fileWasSaved(originalFH, change.fileHandle) { - // TODO(rstambler): Only delete mod handles for which the - // withoutURI is relevant. - for k := range s.modTidyHandles { - delete(result.modTidyHandles, k) - } - for k := range s.modWhyHandles { - delete(result.modWhyHandles, k) - } + // TODO(maybe): Only delete mod handles for + // which the withoutURI is relevant. + // Requires reverse-engineering the go command. (!) + + result.modTidyHandles.Clear() + result.modWhyHandles.Clear() } - delete(result.parseModHandles, uri) - delete(result.parseWorkHandles, uri) + + result.parseModHandles.Delete(uri) + result.parseWorkHandles.Delete(uri) // Handle the invalidated file; it may have new contents or not exist. if !change.exists { result.files.Delete(uri) @@ -2011,19 +1945,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC result.workspacePackages = s.workspacePackages } - // Inherit all of the go.mod-related handles. - for _, v := range result.modTidyHandles { - newGen.Inherit(v.handle) - } - for _, v := range result.modWhyHandles { - newGen.Inherit(v.handle) - } - for _, v := range result.parseModHandles { - newGen.Inherit(v.handle) - } - for _, v := range result.parseWorkHandles { - newGen.Inherit(v.handle) - } // Don't bother copying the importedBy graph, // as it changes each time we update metadata. diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go index 50d7b123ec9..ab031bf64b9 100644 --- a/internal/lsp/cache/symbols.go +++ b/internal/lsp/cache/symbols.go @@ -18,43 +18,48 @@ import ( "golang.org/x/tools/internal/memoize" ) -// A symbolHandle contains a handle to the result of symbolizing a file. -type symbolHandle struct { - handle *memoize.Handle -} +// symbolize returns the result of symbolizing the file identified by fh, using a cache. +func (s *snapshot) symbolize(ctx context.Context, fh source.FileHandle) ([]source.Symbol, error) { + uri := fh.URI() -// symbolData contains the data produced by extracting symbols from a file. -type symbolData struct { - symbols []source.Symbol - err error -} + s.mu.Lock() + entry, hit := s.symbolizeHandles.Get(uri) + s.mu.Unlock() -// buildSymbolHandle returns a handle to the future result of -// symbolizing the file identified by fh, -// if necessary creating it and saving it in the snapshot. -func (s *snapshot) buildSymbolHandle(ctx context.Context, fh source.FileHandle) *symbolHandle { - if h := s.getSymbolHandle(fh.URI()); h != nil { - return h + type symbolizeResult struct { + symbols []source.Symbol + err error } - type symbolHandleKey source.Hash - key := symbolHandleKey(fh.FileIdentity().Hash) - handle := s.generation.Bind(key, func(_ context.Context, arg memoize.Arg) interface{} { - snapshot := arg.(*snapshot) - symbols, err := symbolize(snapshot, fh) - return &symbolData{symbols, err} - }) - sh := &symbolHandle{ - handle: handle, + // Cache miss? + if !hit { + type symbolHandleKey source.Hash + key := symbolHandleKey(fh.FileIdentity().Hash) + handle, release := s.generation.GetHandle(key, func(_ context.Context, arg memoize.Arg) interface{} { + symbols, err := symbolizeImpl(arg.(*snapshot), fh) + return symbolizeResult{symbols, err} + }) + + entry = handle + + s.mu.Lock() + s.symbolizeHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.mu.Unlock() } - return s.addSymbolHandle(fh.URI(), sh) + // Await result. + v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) + if err != nil { + return nil, err + } + res := v.(symbolizeResult) + return res.symbols, res.err } -// symbolize reads and parses a file and extracts symbols from it. +// symbolizeImpl reads and parses a file and extracts symbols from it. // It may use a parsed file already present in the cache but // otherwise does not populate the cache. -func symbolize(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) { +func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) { src, err := fh.Read() if err != nil { return nil, err diff --git a/internal/persistent/map.go b/internal/persistent/map.go index 55b7065e9f7..f5dd10206b8 100644 --- a/internal/persistent/map.go +++ b/internal/persistent/map.go @@ -120,10 +120,17 @@ func (pm *Map) Clone() *Map { } } -// Destroy the persistent map. +// Destroy destroys the map. // // After Destroy, the Map should not be used again. func (pm *Map) Destroy() { + // The implementation of these two functions is the same, + // but their intent is different. + pm.Clear() +} + +// Clear removes all entries from the map. +func (pm *Map) Clear() { pm.root.decref() pm.root = nil } From 9c2a5567e347d6d57de667256c748abf757d1ee4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 7 Jul 2022 18:41:33 -0400 Subject: [PATCH 084/723] internal/lsp/cache: fail addPackageHandle if metadata is stale If metadata is refreshed during the execution of buildPackageHandle, we should not store the resulting package handle in the snapshot, as it breaks the invariant that computed packages match the currently loaded metadata. This strictness revealed another bug: because of our fine-grained locking in snapshot.load, it is possible that we set valid metadata multiple times, leading to unnecessary invalidation and potential further races to package handles. Fix this by building all metadata when processing the go/packages result, and only filtering updates after grabbing the lock. For golang/go#53733 Change-Id: Ib63ae4fbd97d0d25d45fe04f9bcd835996db41da Reviewed-on: https://go-review.googlesource.com/c/tools/+/416224 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- internal/lsp/cache/check.go | 4 +-- internal/lsp/cache/load.go | 60 ++++++++++++++++++++-------------- internal/lsp/cache/snapshot.go | 22 +++++++++++-- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index aae6de0eea7..c8b314a072d 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -209,9 +209,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // been cached, addPackage will return the cached value. This is fine, // since the original package handle above will have no references and be // garbage collected. - ph = s.addPackageHandle(ph, release) - - return ph, nil + return s.addPackageHandle(ph, release) } func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 08c88ab8e74..8937f934031 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -47,6 +47,8 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf if errors.Is(err, context.Canceled) { return } + // TODO(rfindley): merge these metadata updates with the updates below, to + // avoid updating the graph twice. s.clearShouldLoad(scopes...) }() @@ -154,7 +156,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } moduleErrs := make(map[string][]packages.Error) // module path -> errors - updates := make(map[PackageID]*KnownMetadata) + newMetadata := make(map[PackageID]*KnownMetadata) for _, pkg := range pkgs { // The Go command returns synthetic list results for module queries that // encountered module errors. @@ -196,31 +198,48 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } // Skip filtered packages. They may be added anyway if they're // dependencies of non-filtered packages. + // + // TODO(rfindley): why exclude metadata arbitrarily here? It should be safe + // to capture all metadata. if s.view.allFilesExcluded(pkg) { continue } - // TODO: once metadata is immutable, we shouldn't have to lock here. - s.mu.Lock() - err := computeMetadataUpdates(ctx, s.meta, PackagePath(pkg.PkgPath), pkg, cfg, query, updates, nil) - s.mu.Unlock() - if err != nil { + if err := buildMetadata(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, newMetadata, nil); err != nil { return err } } - var loadedIDs []PackageID - for id := range updates { - loadedIDs = append(loadedIDs, id) + s.mu.Lock() + + // Only update metadata where we don't already have valid metadata. + // + // We want to preserve an invariant that s.packages.Get(id).m.Metadata + // matches s.meta.metadata[id].Metadata. By avoiding overwriting valid + // metadata, we minimize the amount of invalidation required to preserve this + // invariant. + // + // TODO(rfindley): perform a sanity check that metadata matches here. If not, + // we have an invalidation bug elsewhere. + updates := make(map[PackageID]*KnownMetadata) + var updatedIDs []PackageID + for _, m := range newMetadata { + if existing := s.meta.metadata[m.ID]; existing == nil || !existing.Valid { + updates[m.ID] = m + updatedIDs = append(updatedIDs, m.ID) + } } event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates))) - s.mu.Lock() + // Invalidate the reverse transitive closure of packages that have changed. + // + // Note that the original metadata is being invalidated here, so we use the + // original metadata graph to compute the reverse closure. + invalidatedPackages := s.meta.reverseTransitiveClosure(true, updatedIDs...) - // invalidate the reverse transitive closure of packages that have changed. - invalidatedPackages := s.meta.reverseTransitiveClosure(true, loadedIDs...) s.meta = s.meta.Clone(updates) s.resetIsActivePackageLocked() + // Invalidate any packages we may have associated with this metadata. // // TODO(rfindley): this should not be necessary, as we should have already @@ -431,10 +450,10 @@ func makeWorkspaceDir(ctx context.Context, workspace *workspace, fs source.FileS return tmpdir, nil } -// computeMetadataUpdates populates the updates map with metadata updates to +// buildMetadata populates the updates map with metadata updates to // apply, based on the given pkg. It recurs through pkg.Imports to ensure that // metadata exists for all dependencies. -func computeMetadataUpdates(ctx context.Context, g *metadataGraph, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { +func buildMetadata(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { id := PackageID(pkg.ID) if source.IsCommandLineArguments(pkg.ID) { suffix := ":" + strings.Join(query, ",") @@ -442,21 +461,12 @@ func computeMetadataUpdates(ctx context.Context, g *metadataGraph, pkgPath Packa pkgPath = PackagePath(string(pkgPath) + suffix) } - // If we have valid metadata for this package, don't update. This minimizes - // the amount of subsequent invalidation. - // - // TODO(rfindley): perform a sanity check that metadata matches here. If not, - // we have an invalidation bug elsewhere. - if existing := g.metadata[id]; existing != nil && existing.Valid { - return nil - } - if _, ok := updates[id]; ok { // If we've already seen this dependency, there may be an import cycle, or // we may have reached the same package transitively via distinct paths. // Check the path to confirm. - // TODO(rfindley): this doesn't look right. Any single piece of new + // TODO(rfindley): this doesn't look sufficient. Any single piece of new // metadata could theoretically introduce import cycles in the metadata // graph. What's the point of this limited check here (and is it even // possible to get an import cycle in data from go/packages)? Consider @@ -535,7 +545,7 @@ func computeMetadataUpdates(ctx context.Context, g *metadataGraph, pkgPath Packa m.MissingDeps[importPkgPath] = struct{}{} continue } - if err := computeMetadataUpdates(ctx, g, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { + if err := buildMetadata(ctx, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { event.Error(ctx, "error in dependency", err) } } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index b962435b62e..5623f24c550 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -91,6 +91,11 @@ type snapshot struct { // packages maps a packageKey to a *packageHandle. // It may be invalidated when a file's content changes. + // + // Invariants to preserve: + // - packages.Get(id).m.Metadata == meta.metadata[id].Metadata for all ids + // - if a package is in packages, then all of its dependencies should also + // be in packages, unless there is a missing import packages packagesMap // isActivePackageCache maps package ID to the cached value if it is active or not. @@ -712,18 +717,29 @@ func (s *snapshot) getImportedBy(id PackageID) []PackageID { return s.meta.importedBy[id] } -func (s *snapshot) addPackageHandle(ph *packageHandle, release func()) *packageHandle { +// addPackageHandle stores ph in the snapshot, or returns a pre-existing handle +// for the given package key, if it exists. +// +// An error is returned if the metadata used to build ph is no longer relevant. +func (s *snapshot) addPackageHandle(ph *packageHandle, release func()) (*packageHandle, error) { s.mu.Lock() defer s.mu.Unlock() + if s.meta.metadata[ph.m.ID].Metadata != ph.m.Metadata { + return nil, fmt.Errorf("stale metadata for %s", ph.m.ID) + } + // If the package handle has already been cached, // return the cached handle instead of overriding it. if result, ok := s.packages.Get(ph.packageKey()); ok { release() - return result + if result.m.Metadata != ph.m.Metadata { + return nil, bug.Errorf("existing package handle does not match for %s", ph.m.ID) + } + return result, nil } s.packages.Set(ph.packageKey(), ph, release) - return ph + return ph, nil } func (s *snapshot) workspacePackageIDs() (ids []PackageID) { From 8746177218db2b5640e4134ec99a5a905018b50c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 7 Jul 2022 15:29:21 -0400 Subject: [PATCH 085/723] internal/lsp/cache: simplify ParseGo This change simplifes the ParseGo interface to make it consistent with the other handle+map operations: - ParseGoImpl is the basic parser. - The 'fixed' bool result is a field of ParsedGoFile. - ParseGo is the caching wrapper. The map accessors have been inlined into it. - goFiles (renamed parsedGoFiles) is now just a bare persistent.Map. - parseGoHandle is replaced by *memoize.Handle - the operations of "make a handle" and "wait for it" are no longer separate (since clients never want one without the other). - cachedPGF and peekOrParse have been combined into peekParseGoLocked. Change-Id: If01a6aaa7e6a8d78cb89c305e5279738e8e7bb55 Reviewed-on: https://go-review.googlesource.com/c/tools/+/416223 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Alan Donovan --- internal/lsp/cache/check.go | 8 +- internal/lsp/cache/maps.go | 46 +--------- internal/lsp/cache/parse.go | 149 ++++++++++++++++----------------- internal/lsp/cache/session.go | 2 +- internal/lsp/cache/snapshot.go | 60 ++++--------- internal/lsp/cache/symbols.go | 10 ++- internal/lsp/source/view.go | 1 + 7 files changed, 104 insertions(+), 172 deletions(-) diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index c8b314a072d..c17288c9e15 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -568,15 +568,13 @@ func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHand // TODO(adonovan): opt: parallelize this loop, which takes 1-25ms. for _, fh := range compiledGoFiles { var pgf *source.ParsedGoFile - var fixed bool var err error // Only parse Full through the cache -- we need to own Exported ASTs // to prune them. if mode == source.ParseFull { - pgf, fixed, err = snapshot.parseGo(ctx, fh, mode) + pgf, err = snapshot.ParseGo(ctx, fh, mode) } else { - d := parseGo(ctx, snapshot.FileSet(), fh, mode) // ~20us/KB - pgf, fixed, err = d.parsed, d.fixed, d.err + pgf, err = parseGoImpl(ctx, snapshot.FileSet(), fh, mode) // ~20us/KB } if err != nil { return err @@ -587,7 +585,7 @@ func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHand } // If we have fixed parse errors in any of the files, we should hide type // errors, as they may be completely nonsensical. - pkg.hasFixedFiles = pkg.hasFixedFiles || fixed + pkg.hasFixedFiles = pkg.hasFixedFiles || pgf.Fixed } if mode != source.ParseExported { return nil diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go index 1ec34151540..4bb3b3b2689 100644 --- a/internal/lsp/cache/maps.go +++ b/internal/lsp/cache/maps.go @@ -59,16 +59,8 @@ func (m filesMap) Delete(key span.URI) { m.impl.Delete(key) } -type goFilesMap struct { - impl *persistent.Map -} - -func newGoFilesMap() goFilesMap { - return goFilesMap{ - impl: persistent.NewMap(func(a, b interface{}) bool { - return parseKeyLess(a.(parseKey), b.(parseKey)) - }), - } +func parseKeyLessInterface(a, b interface{}) bool { + return parseKeyLess(a.(parseKey), b.(parseKey)) } func parseKeyLess(a, b parseKey) bool { @@ -81,40 +73,6 @@ func parseKeyLess(a, b parseKey) bool { return a.file.URI < b.file.URI } -func (m goFilesMap) Clone() goFilesMap { - return goFilesMap{ - impl: m.impl.Clone(), - } -} - -func (m goFilesMap) Destroy() { - m.impl.Destroy() -} - -func (m goFilesMap) Get(key parseKey) (*parseGoHandle, bool) { - value, ok := m.impl.Get(key) - if !ok { - return nil, false - } - return value.(*parseGoHandle), true -} - -func (m goFilesMap) Range(do func(key parseKey, value *parseGoHandle)) { - m.impl.Range(func(key, value interface{}) { - do(key.(parseKey), value.(*parseGoHandle)) - }) -} - -func (m goFilesMap) Set(key parseKey, value *parseGoHandle, release func()) { - m.impl.Set(key, value, func(key, value interface{}) { - release() - }) -} - -func (m goFilesMap) Delete(key parseKey) { - m.impl.Delete(key) -} - type isActivePackageCacheMap struct { impl *persistent.Map } diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index 712f26ad715..c8c751f0b2e 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -35,78 +35,76 @@ type parseKey struct { mode source.ParseMode } -type parseGoHandle struct { - handle *memoize.Handle - file source.FileHandle - mode source.ParseMode -} - -type parseGoData struct { - parsed *source.ParsedGoFile - - // If true, we adjusted the AST to make it type check better, and - // it may not match the source code. - fixed bool - err error // any other errors -} +// ParseGo parses the file whose contents are provided by fh, using a cache. +// The resulting tree may have be fixed up. +// +// The parser mode must not be ParseExported: that mode is used during +// type checking to destructively trim the tree to reduce work, +// which is not safe for values from a shared cache. +// TODO(adonovan): opt: shouldn't parseGoImpl do the trimming? +// Then we can cache the result since it would never change. +func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { + if mode == source.ParseExported { + panic("only type checking should use Exported") + } -func (s *snapshot) parseGoHandle(ctx context.Context, fh source.FileHandle, mode source.ParseMode) *parseGoHandle { key := parseKey{ file: fh.FileIdentity(), mode: mode, } - if pgh := s.getGoFile(key); pgh != nil { - return pgh - } - parseHandle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { - snapshot := arg.(*snapshot) - return parseGo(ctx, snapshot.FileSet(), fh, mode) - }) - pgh := &parseGoHandle{ - handle: parseHandle, - file: fh, - mode: mode, + s.mu.Lock() + entry, hit := s.parsedGoFiles.Get(key) + s.mu.Unlock() + + // cache miss? + if !hit { + handle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { + parsed, err := parseGoImpl(ctx, arg.(*snapshot).FileSet(), fh, mode) + return parseGoResult{parsed, err} + }) + + s.mu.Lock() + // Check cache again in case another thread got there first. + if prev, ok := s.parsedGoFiles.Get(key); ok { + entry = prev + release() + } else { + entry = handle + s.parsedGoFiles.Set(key, entry, func(_, _ interface{}) { release() }) + } + s.mu.Unlock() } - return s.addGoFile(key, pgh, release) -} - -func (pgh *parseGoHandle) String() string { - return pgh.file.URI().Filename() -} -func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { - pgf, _, err := s.parseGo(ctx, fh, mode) - return pgf, err + // Await result. + v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) + if err != nil { + return nil, err + } + res := v.(parseGoResult) + return res.parsed, res.err } -func (s *snapshot) parseGo(ctx context.Context, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, bool, error) { - if mode == source.ParseExported { - panic("only type checking should use Exported") +// peekParseGoLocked peeks at the cache used by ParseGo but does not +// populate it or wait for other threads to do so. On cache hit, it returns +// the cache result of parseGoImpl; otherwise it returns (nil, nil). +func (s *snapshot) peekParseGoLocked(fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { + entry, hit := s.parsedGoFiles.Get(parseKey{fh.FileIdentity(), mode}) + if !hit { + return nil, nil // no-one has requested this file } - pgh := s.parseGoHandle(ctx, fh, mode) - d, err := pgh.handle.Get(ctx, s.generation, s) - if err != nil { - return nil, false, err + v := entry.(*memoize.Handle).Cached(s.generation) + if v == nil { + return nil, nil // parsing is still in progress } - data := d.(*parseGoData) - return data.parsed, data.fixed, data.err + res := v.(parseGoResult) + return res.parsed, res.err } -// cachedPGF returns the cached ParsedGoFile for the given ParseMode, if it -// has already been computed. Otherwise, it returns nil. -func (s *snapshot) cachedPGF(fh source.FileHandle, mode source.ParseMode) *source.ParsedGoFile { - key := parseKey{file: fh.FileIdentity(), mode: mode} - if pgh := s.getGoFile(key); pgh != nil { - cached := pgh.handle.Cached(s.generation) - if cached != nil { - cached := cached.(*parseGoData) - if cached.parsed != nil { - return cached.parsed - } - } - } - return nil +// parseGoResult holds the result of a call to parseGoImpl. +type parseGoResult struct { + parsed *source.ParsedGoFile + err error } type astCacheKey struct { @@ -274,17 +272,18 @@ func buildASTCache(pgf *source.ParsedGoFile) *astCacheData { return data } -func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode source.ParseMode) *parseGoData { +// parseGoImpl parses the Go source file whose content is provided by fh. +func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { ctx, done := event.Start(ctx, "cache.parseGo", tag.File.Of(fh.URI().Filename())) defer done() ext := filepath.Ext(fh.URI().Filename()) if ext != ".go" && ext != "" { // files generated by cgo have no extension - return &parseGoData{err: fmt.Errorf("cannot parse non-Go file %s", fh.URI())} + return nil, fmt.Errorf("cannot parse non-Go file %s", fh.URI()) } src, err := fh.Read() if err != nil { - return &parseGoData{err: err} + return nil, err } parserMode := parser.AllErrors | parser.ParseComments @@ -346,22 +345,20 @@ func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mod } } - return &parseGoData{ - parsed: &source.ParsedGoFile{ - URI: fh.URI(), - Mode: mode, - Src: src, - File: file, - Tok: tok, - Mapper: &protocol.ColumnMapper{ - URI: fh.URI(), - TokFile: tok, - Content: src, - }, - ParseErr: parseErr, + return &source.ParsedGoFile{ + URI: fh.URI(), + Mode: mode, + Src: src, + Fixed: fixed, + File: file, + Tok: tok, + Mapper: &protocol.ColumnMapper{ + URI: fh.URI(), + TokFile: tok, + Content: src, }, - fixed: fixed, - } + ParseErr: parseErr, + }, nil } // An unexportedFilter removes as much unexported AST from a set of Files as possible. diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 80468bc5931..9ea612a3ab4 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -236,7 +236,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, meta: &metadataGraph{}, files: newFilesMap(), isActivePackageCache: newIsActivePackageCacheMap(), - goFiles: newGoFilesMap(), + parsedGoFiles: persistent.NewMap(parseKeyLessInterface), parseKeysByURI: newParseKeysByURIMap(), symbolizeHandles: persistent.NewMap(uriLessInterface), actions: persistent.NewMap(actionKeyLessInterface), diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 5623f24c550..c228db9655b 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -81,8 +81,13 @@ type snapshot struct { // It may invalidated when a file's content changes. files filesMap - // goFiles maps a parseKey to its parseGoHandle. - goFiles goFilesMap + // parsedGoFiles maps a parseKey to the handle of the future result of parsing it. + parsedGoFiles *persistent.Map // from parseKey to *memoize.Handle + + // parseKeysByURI records the set of keys of parsedGoFiles that + // need to be invalidated for each URI. + // TODO(adonovan): opt: parseKey = ParseMode + URI, so this could + // be just a set of ParseModes, or we could loop over AllParseModes. parseKeysByURI parseKeysByURIMap // symbolizeHandles maps each file URI to a handle for the future @@ -159,7 +164,7 @@ func (s *snapshot) Destroy(destroyedBy string) { s.isActivePackageCache.Destroy() s.actions.Destroy() s.files.Destroy() - s.goFiles.Destroy() + s.parsedGoFiles.Destroy() s.parseKeysByURI.Destroy() s.knownSubdirs.Destroy() s.symbolizeHandles.Destroy() @@ -688,29 +693,6 @@ func (s *snapshot) checkedPackage(ctx context.Context, id PackageID, mode source return ph.check(ctx, s) } -func (s *snapshot) getGoFile(key parseKey) *parseGoHandle { - s.mu.Lock() - defer s.mu.Unlock() - if result, ok := s.goFiles.Get(key); ok { - return result - } - return nil -} - -func (s *snapshot) addGoFile(key parseKey, pgh *parseGoHandle, release func()) *parseGoHandle { - s.mu.Lock() - defer s.mu.Unlock() - if result, ok := s.goFiles.Get(key); ok { - release() - return result - } - s.goFiles.Set(key, pgh, release) - keys, _ := s.parseKeysByURI.Get(key.file.URI) - keys = append([]parseKey{key}, keys...) - s.parseKeysByURI.Set(key.file.URI, keys) - return pgh -} - func (s *snapshot) getImportedBy(id PackageID) []PackageID { s.mu.Lock() defer s.mu.Unlock() @@ -1708,7 +1690,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC isActivePackageCache: s.isActivePackageCache.Clone(), actions: s.actions.Clone(), files: s.files.Clone(), - goFiles: s.goFiles.Clone(), + parsedGoFiles: s.parsedGoFiles.Clone(), parseKeysByURI: s.parseKeysByURI.Clone(), symbolizeHandles: s.symbolizeHandles.Clone(), workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)), @@ -1731,7 +1713,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC keys, ok := result.parseKeysByURI.Get(uri) if ok { for _, key := range keys { - result.goFiles.Delete(key) + result.parsedGoFiles.Delete(key) } result.parseKeysByURI.Delete(uri) } @@ -2147,28 +2129,20 @@ func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH return invalidate, pkgFileChanged, importDeleted } -// peekOrParse returns the cached ParsedGoFile if it exists, otherwise parses -// without caching. +// peekOrParse returns the cached ParsedGoFile if it exists, +// otherwise parses without populating the cache. // // It returns an error if the file could not be read (note that parsing errors // are stored in ParsedGoFile.ParseErr). // // lockedSnapshot must be locked. func peekOrParse(ctx context.Context, lockedSnapshot *snapshot, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { - key := parseKey{file: fh.FileIdentity(), mode: mode} - if pgh, ok := lockedSnapshot.goFiles.Get(key); ok { - cached := pgh.handle.Cached(lockedSnapshot.generation) - if cached != nil { - cached := cached.(*parseGoData) - if cached.parsed != nil { - return cached.parsed, nil - } - } + // Peek in the cache without populating it. + // We do this to reduce retained heap, not work. + if parsed, _ := lockedSnapshot.peekParseGoLocked(fh, mode); parsed != nil { + return parsed, nil // cache hit } - - fset := token.NewFileSet() - data := parseGo(ctx, fset, fh, mode) - return data.parsed, data.err + return parseGoImpl(ctx, token.NewFileSet(), fh, mode) } func magicCommentsChanged(original *ast.File, current *ast.File) bool { diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go index ab031bf64b9..4cbf8589025 100644 --- a/internal/lsp/cache/symbols.go +++ b/internal/lsp/cache/symbols.go @@ -70,9 +70,13 @@ func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, e fileDesc *token.File ) - // If the file has already been fully parsed through the cache, we can just - // use the result. - if pgf := snapshot.cachedPGF(fh, source.ParseFull); pgf != nil { + // If the file has already been fully parsed through the + // cache, we can just use the result. But we don't want to + // populate the cache after a miss. + snapshot.mu.Lock() + pgf, _ := snapshot.peekParseGoLocked(fh, source.ParseFull) + snapshot.mu.Unlock() + if pgf != nil { file = pgf.File fileDesc = pgf.Tok } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index c8656153bd3..caf18505856 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -297,6 +297,7 @@ type ParsedGoFile struct { // Source code used to build the AST. It may be different from the // actual content of the file if we have fixed the AST. Src []byte + Fixed bool Mapper *protocol.ColumnMapper ParseErr scanner.ErrorList } From 53ead67a981c04bcacdd4f593330c43ee9285578 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 5 Jul 2022 10:30:58 -0400 Subject: [PATCH 086/723] internal/memoize: delete Generation and Bind Now that the lifetime of all handles in the store is determined by reference counting, we no longer need the generation feature. The Arg interface, renamed RefCounted, is now optional, and causes the lifetime of the argument to be extended for the duration of the Function call. This is important when the Get(ctx) context is cancelled, causing the function call to outlive Get: if Get's reference to the argument was borrowed, it needs to increase the refcount to prevent premature destruction. Also: - add missing snapshot.release() call in importsState.populateProcessEnv. - remove the --memoize_panic_on_destroyed flag. Change-Id: I0b3d37c16f8b3f550bb10120c066b628c3db244b Reviewed-on: https://go-review.googlesource.com/c/tools/+/416076 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro --- internal/lsp/cache/analysis.go | 6 +- internal/lsp/cache/check.go | 8 +- internal/lsp/cache/imports.go | 3 +- internal/lsp/cache/mod.go | 12 +- internal/lsp/cache/mod_tidy.go | 9 +- internal/lsp/cache/parse.go | 10 +- internal/lsp/cache/session.go | 26 ++- internal/lsp/cache/snapshot.go | 48 +++-- internal/lsp/cache/symbols.go | 4 +- internal/lsp/cache/view.go | 4 +- internal/lsp/command.go | 2 +- internal/lsp/general.go | 3 +- internal/lsp/source/view.go | 13 +- internal/memoize/memoize.go | 295 +++++++------------------------ internal/memoize/memoize_test.go | 84 +++------ 15 files changed, 184 insertions(+), 343 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index 4b437858ef3..e196d1c4a35 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -137,7 +137,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } } - handle, release := s.generation.GetHandle(buildActionKey(a, ph), func(ctx context.Context, arg memoize.Arg) interface{} { + handle, release := s.store.Handle(buildActionKey(a, ph), func(ctx context.Context, arg interface{}) interface{} { snapshot := arg.(*snapshot) // Analyze dependencies first. results, err := execAll(ctx, snapshot, deps) @@ -159,7 +159,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } func (act *actionHandle) analyze(ctx context.Context, snapshot *snapshot) ([]*source.Diagnostic, interface{}, error) { - d, err := act.handle.Get(ctx, snapshot.generation, snapshot) + d, err := snapshot.awaitHandle(ctx, act.handle) if err != nil { return nil, nil, err } @@ -189,7 +189,7 @@ func execAll(ctx context.Context, snapshot *snapshot, actions []*actionHandle) ( for _, act := range actions { act := act g.Go(func() error { - v, err := act.handle.Get(ctx, snapshot.generation, snapshot) + v, err := snapshot.awaitHandle(ctx, act.handle) if err != nil { return err } diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index c17288c9e15..4680c6e7285 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -167,7 +167,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // Create a handle for the result of type checking. experimentalKey := s.View().Options().ExperimentalPackageCacheKey key := computePackageKey(m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) - handle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { + handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} { // TODO(adonovan): eliminate use of arg with this handle. // (In all cases snapshot is equal to the enclosing s.) snapshot := arg.(*snapshot) @@ -286,7 +286,7 @@ func hashConfig(config *packages.Config) source.Hash { } func (ph *packageHandle) check(ctx context.Context, s *snapshot) (*pkg, error) { - v, err := ph.handle.Get(ctx, s.generation, s) + v, err := s.awaitHandle(ctx, ph.handle) if err != nil { return nil, err } @@ -302,8 +302,8 @@ func (ph *packageHandle) ID() string { return string(ph.m.ID) } -func (ph *packageHandle) cached(g *memoize.Generation) (*pkg, error) { - v := ph.handle.Cached(g) +func (ph *packageHandle) cached() (*pkg, error) { + v := ph.handle.Cached() if v == nil { return nil, fmt.Errorf("no cached type information for %s", ph.m.PkgPath) } diff --git a/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go index f333f700ddf..710a1f3407a 100644 --- a/internal/lsp/cache/imports.go +++ b/internal/lsp/cache/imports.go @@ -143,11 +143,12 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho // Take an extra reference to the snapshot so that its workspace directory // (if any) isn't destroyed while we're using it. - release := snapshot.generation.Acquire() + release := snapshot.Acquire() _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ WorkingDir: snapshot.view.rootURI.Filename(), }) if err != nil { + release() return nil, err } pe.WorkingDir = inv.WorkingDir diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index 1963feea5ac..79b3fd016d6 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -39,7 +39,7 @@ func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source. // cache miss? if !hit { - handle, release := s.generation.GetHandle(fh.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} { + handle, release := s.store.Handle(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { parsed, err := parseModImpl(ctx, fh) return parseModResult{parsed, err} }) @@ -51,7 +51,7 @@ func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source. } // Await result. - v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) + v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) if err != nil { return nil, err } @@ -116,7 +116,7 @@ func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source // cache miss? if !hit { - handle, release := s.generation.GetHandle(fh.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} { + handle, release := s.store.Handle(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { parsed, err := parseWorkImpl(ctx, fh) return parseWorkResult{parsed, err} }) @@ -128,7 +128,7 @@ func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source } // Await result. - v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) + v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) if err != nil { return nil, err } @@ -241,7 +241,7 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string mod: fh.FileIdentity(), view: s.view.rootURI.Filename(), } - handle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { + handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} { why, err := modWhyImpl(ctx, arg.(*snapshot), fh) return modWhyResult{why, err} }) @@ -253,7 +253,7 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string } // Await result. - v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) + v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) if err != nil { return nil, err } diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index 84f369ef3d4..b59b4fd8832 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -29,9 +29,6 @@ import ( ) // modTidyImpl runs "go mod tidy" on a go.mod file, using a cache. -// -// REVIEWERS: what does it mean to cache an operation that has side effects? -// Or are we de-duplicating operations in flight on the same file? func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) { uri := pm.URI if pm.File == nil { @@ -77,6 +74,8 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc // There's little reason at to use the shared cache for mod // tidy (and mod why) as their key includes the view and session. + // Its only real value is to de-dup requests in flight, for + // which a singleflight in the View would suffice. // TODO(adonovan): use a simpler cache of promises that // is shared across snapshots. type modTidyKey struct { @@ -96,7 +95,7 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc gomod: fh.FileIdentity(), env: hashEnv(s), } - handle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { + handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} { tidied, err := modTidyImpl(ctx, arg.(*snapshot), fh, pm, workspacePkgs) return modTidyResult{tidied, err} }) @@ -108,7 +107,7 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc } // Await result. - v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) + v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) if err != nil { return nil, err } diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index c8c751f0b2e..ef588c60597 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -59,7 +59,7 @@ func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode sourc // cache miss? if !hit { - handle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} { + handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} { parsed, err := parseGoImpl(ctx, arg.(*snapshot).FileSet(), fh, mode) return parseGoResult{parsed, err} }) @@ -77,7 +77,7 @@ func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode sourc } // Await result. - v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) + v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) if err != nil { return nil, err } @@ -93,7 +93,7 @@ func (s *snapshot) peekParseGoLocked(fh source.FileHandle, mode source.ParseMode if !hit { return nil, nil // no-one has requested this file } - v := entry.(*memoize.Handle).Cached(s.generation) + v := entry.(*memoize.Handle).Cached() if v == nil { return nil, nil // parsing is still in progress } @@ -147,12 +147,12 @@ func (s *snapshot) astCacheData(ctx context.Context, spkg source.Package, pos to // the search Pos.) // // A representative benchmark would help. - astHandle, release := s.generation.GetHandle(astCacheKey{pkgHandle.key, pgf.URI}, func(ctx context.Context, arg memoize.Arg) interface{} { + astHandle, release := s.store.Handle(astCacheKey{pkgHandle.key, pgf.URI}, func(ctx context.Context, arg interface{}) interface{} { return buildASTCache(pgf) }) defer release() - d, err := astHandle.Get(ctx, s.generation, s) + d, err := s.awaitHandle(ctx, astHandle) if err != nil { return nil, err } diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 9ea612a3ab4..a46b7928c78 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -231,7 +231,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, backgroundCtx: backgroundCtx, cancel: cancel, initializeOnce: &sync.Once{}, - generation: s.cache.store.Generation(generationName(v, 0)), + store: &s.cache.store, packages: newPackagesMap(), meta: &metadataGraph{}, files: newFilesMap(), @@ -254,12 +254,28 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx)) v.initCancelFirstAttempt = initCancel snapshot := v.snapshot - release := snapshot.generation.Acquire() + + // Acquire both references before the possibility + // of releasing either one, to avoid premature + // destruction if initialize returns quickly. + // + // TODO(adonovan): our reference counting discipline is not sound: + // the count is initially zero and incremented/decremented by + // acquire/release, but there is a race between object birth + // and the first call to acquire during which the snapshot may be + // destroyed. + // + // In most systems, an object is born with a count of 1 and + // destroyed by any decref that brings the count to zero. + // We should do that too. + release1 := snapshot.Acquire() + release2 := snapshot.Acquire() go func() { - defer release() + defer release2() snapshot.initialize(initCtx, true) }() - return v, snapshot, snapshot.generation.Acquire(), nil + + return v, snapshot, release1, nil } // View returns the view by name. @@ -539,6 +555,8 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes defer release() snapshots = append(snapshots, snapshot) } + // TODO(adonovan): opt: release lock here. + knownDirs := knownDirectories(ctx, snapshots) defer knownDirs.Destroy() diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index c228db9655b..fa71fbd8a80 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -14,6 +14,7 @@ import ( "go/types" "io" "io/ioutil" + "log" "os" "path/filepath" "regexp" @@ -22,6 +23,8 @@ import ( "strconv" "strings" "sync" + "sync/atomic" + "unsafe" "golang.org/x/mod/modfile" "golang.org/x/mod/module" @@ -42,16 +45,16 @@ import ( ) type snapshot struct { - memoize.Arg // allow as a memoize.Function arg - id uint64 view *View cancel func() backgroundCtx context.Context - // the cache generation that contains the data for this snapshot. - generation *memoize.Generation + store *memoize.Store // cache of handles shared by all snapshots + + refcount sync.WaitGroup // number of references + destroyedBy *string // atomically set to non-nil in Destroy once refcount = 0 // The snapshot's initialization state is controlled by the fields below. // @@ -148,6 +151,22 @@ type snapshot struct { unprocessedSubdirChanges []*fileChange } +var _ memoize.RefCounted = (*snapshot)(nil) // snapshots are reference-counted + +// Acquire prevents the snapshot from being destroyed until the returned function is called. +func (s *snapshot) Acquire() func() { + type uP = unsafe.Pointer + if destroyedBy := atomic.LoadPointer((*uP)(uP(&s.destroyedBy))); destroyedBy != nil { + log.Panicf("%d: acquire() after Destroy(%q)", s.id, *(*string)(destroyedBy)) + } + s.refcount.Add(1) + return s.refcount.Done +} + +func (s *snapshot) awaitHandle(ctx context.Context, h *memoize.Handle) (interface{}, error) { + return h.Get(ctx, s) +} + type packageKey struct { mode source.ParseMode id PackageID @@ -159,7 +178,16 @@ type actionKey struct { } func (s *snapshot) Destroy(destroyedBy string) { - s.generation.Destroy(destroyedBy) + // Wait for all leases to end before commencing destruction. + s.refcount.Wait() + + // Report bad state as a debugging aid. + // Not foolproof: another thread could acquire() at this moment. + type uP = unsafe.Pointer // looking forward to generics... + if old := atomic.SwapPointer((*uP)(uP(&s.destroyedBy)), uP(&destroyedBy)); old != nil { + log.Panicf("%d: Destroy(%q) after Destroy(%q)", s.id, destroyedBy, *(*string)(old)) + } + s.packages.Destroy() s.isActivePackageCache.Destroy() s.actions.Destroy() @@ -355,6 +383,7 @@ func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd stri return true, modBytes, sumBytes, nil } +// TODO(adonovan): remove unused cleanup mechanism. func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.InvocationFlags, inv *gocommand.Invocation) (tmpURI span.URI, updatedInv *gocommand.Invocation, cleanup func(), err error) { s.view.optionsMu.Lock() allowModfileModificationOption := s.view.options.AllowModfileModifications @@ -1092,7 +1121,7 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac results := map[string]source.Package{} s.packages.Range(func(key packageKey, ph *packageHandle) { - cachedPkg, err := ph.cached(s.generation) + cachedPkg, err := ph.cached() if err != nil { return } @@ -1645,10 +1674,6 @@ func inVendor(uri span.URI) bool { return strings.Contains(split[1], "/") } -func generationName(v *View, snapshotID uint64) string { - return fmt.Sprintf("v%v/%v", v.id, snapshotID) -} - // unappliedChanges is a file source that handles an uncloned snapshot. type unappliedChanges struct { originalSnapshot *snapshot @@ -1675,11 +1700,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC s.mu.Lock() defer s.mu.Unlock() - newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1)) bgCtx, cancel := context.WithCancel(bgCtx) result := &snapshot{ id: s.id + 1, - generation: newGen, + store: s.store, view: s.view, backgroundCtx: bgCtx, cancel: cancel, diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go index 4cbf8589025..b562d5bbdd8 100644 --- a/internal/lsp/cache/symbols.go +++ b/internal/lsp/cache/symbols.go @@ -35,7 +35,7 @@ func (s *snapshot) symbolize(ctx context.Context, fh source.FileHandle) ([]sourc if !hit { type symbolHandleKey source.Hash key := symbolHandleKey(fh.FileIdentity().Hash) - handle, release := s.generation.GetHandle(key, func(_ context.Context, arg memoize.Arg) interface{} { + handle, release := s.store.Handle(key, func(_ context.Context, arg interface{}) interface{} { symbols, err := symbolizeImpl(arg.(*snapshot), fh) return symbolizeResult{symbols, err} }) @@ -48,7 +48,7 @@ func (s *snapshot) symbolize(ctx context.Context, fh source.FileHandle) ([]sourc } // Await result. - v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s) + v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) if err != nil { return nil, err } diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 1810f6e641d..f95c4759219 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -594,7 +594,7 @@ func (v *View) getSnapshot() (*snapshot, func()) { if v.snapshot == nil { panic("getSnapshot called after shutdown") } - return v.snapshot, v.snapshot.generation.Acquire() + return v.snapshot, v.snapshot.Acquire() } func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) { @@ -734,7 +734,7 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]*file v.snapshot = oldSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata) go oldSnapshot.Destroy("View.invalidateContent") - return v.snapshot, v.snapshot.generation.Acquire() + return v.snapshot, v.snapshot.Acquire() } func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, options *source.Options) (*workspaceInformation, error) { diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 862af6088ec..cd4c7273101 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -691,7 +691,7 @@ func (c *commandHandler) GenerateGoplsMod(ctx context.Context, args command.URIA if err != nil { return fmt.Errorf("formatting mod file: %w", err) } - filename := filepath.Join(snapshot.View().Folder().Filename(), "gopls.mod") + filename := filepath.Join(v.Folder().Filename(), "gopls.mod") if err := ioutil.WriteFile(filename, content, 0644); err != nil { return fmt.Errorf("writing mod file: %w", err) } diff --git a/internal/lsp/general.go b/internal/lsp/general.go index 385a04a25fd..06633acb0c4 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -474,8 +474,7 @@ func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI release() return nil, nil, false, func() {}, err } - kind := snapshot.View().FileKind(fh) - if expectKind != source.UnknownKind && kind != expectKind { + if expectKind != source.UnknownKind && view.FileKind(fh) != expectKind { // Wrong kind of file. Nothing to do. release() return nil, nil, false, func() {}, nil diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index caf18505856..d7e212a121d 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -260,10 +260,14 @@ type View interface { // original one will be. SetOptions(context.Context, *Options) (View, error) - // Snapshot returns the current snapshot for the view. + // Snapshot returns the current snapshot for the view, and a + // release function that must be called when the Snapshot is + // no longer needed. Snapshot(ctx context.Context) (Snapshot, func()) - // Rebuild rebuilds the current view, replacing the original view in its session. + // Rebuild rebuilds the current view, replacing the original + // view in its session. It returns a Snapshot and a release + // function that must be called when the Snapshot is no longer needed. Rebuild(ctx context.Context) (Snapshot, func(), error) // IsGoPrivatePath reports whether target is a private import path, as identified @@ -348,7 +352,8 @@ type Session interface { // NewView creates a new View, returning it and its first snapshot. If a // non-empty tempWorkspace directory is provided, the View will record a copy // of its gopls workspace module in that directory, so that client tooling - // can execute in the same main module. + // can execute in the same main module. It returns a release + // function that must be called when the Snapshot is no longer needed. NewView(ctx context.Context, name string, folder span.URI, options *Options) (View, Snapshot, func(), error) // Cache returns the cache that created this session, for debugging only. @@ -372,6 +377,8 @@ type Session interface { // DidModifyFile reports a file modification to the session. It returns // the new snapshots after the modifications have been applied, paired with // the affected file URIs for those snapshots. + // On success, it returns a list of release functions that + // must be called when the snapshots are no longer needed. DidModifyFiles(ctx context.Context, changes []FileModification) (map[Snapshot][]span.URI, []func(), error) // ExpandModificationsToDirectories returns the set of changes with the diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 4b84410d506..2db7945428f 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -5,13 +5,14 @@ // Package memoize supports memoizing the return values of functions with // idempotent results that are expensive to compute. // -// To use this package, build a store and use it to acquire handles with the -// Bind method. +// To use this package, create a Store, call its Handle method to +// acquire a handle to (aka a "promise" of) the future result of a +// function, and call Handle.Get to obtain the result. Get may block +// if the function has not finished (or started). package memoize import ( "context" - "flag" "fmt" "reflect" "runtime/trace" @@ -21,107 +22,44 @@ import ( "golang.org/x/tools/internal/xcontext" ) -var ( - panicOnDestroyed = flag.Bool("memoize_panic_on_destroyed", false, - "Panic when a destroyed generation is read rather than returning an error. "+ - "Panicking may make it easier to debug lifetime errors, especially when "+ - "used with GOTRACEBACK=crash to see all running goroutines.") -) - // Store binds keys to functions, returning handles that can be used to access // the functions results. type Store struct { handlesMu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu handles map[interface{}]*Handle - // handles which are bound to generations for GC purposes. - // (It is the subset of values of 'handles' with trackGenerations enabled.) - boundHandles map[*Handle]struct{} -} - -// Generation creates a new Generation associated with s. Destroy must be -// called on the returned Generation once it is no longer in use. name is -// for debugging purposes only. -func (s *Store) Generation(name string) *Generation { - return &Generation{store: s, name: name} -} - -// A Generation is a logical point in time of the cache life-cycle. Cache -// entries associated with a Generation will not be removed until the -// Generation is destroyed. -type Generation struct { - // destroyed is 1 after the generation is destroyed. Atomic. - destroyed uint32 - store *Store - name string - // destroyedBy describes the caller that togged destroyed from 0 to 1. - destroyedBy string - // wg tracks the reference count of this generation. - wg sync.WaitGroup } -// Destroy waits for all operations referencing g to complete, then removes -// all references to g from cache entries. Cache entries that no longer -// reference any non-destroyed generation are removed. Destroy must be called -// exactly once for each generation, and destroyedBy describes the caller. -func (g *Generation) Destroy(destroyedBy string) { - g.wg.Wait() - - prevDestroyedBy := g.destroyedBy - g.destroyedBy = destroyedBy - if ok := atomic.CompareAndSwapUint32(&g.destroyed, 0, 1); !ok { - panic("Destroy on generation " + g.name + " already destroyed by " + prevDestroyedBy) - } - - g.store.handlesMu.Lock() - defer g.store.handlesMu.Unlock() - for h := range g.store.boundHandles { - h.mu.Lock() - if _, ok := h.generations[g]; ok { - delete(h.generations, g) // delete even if it's dead, in case of dangling references to the entry. - if len(h.generations) == 0 { - h.state = stateDestroyed - delete(g.store.handles, h.key) - if h.trackGenerations { - delete(g.store.boundHandles, h) - } - } - } - h.mu.Unlock() - } -} - -// Acquire creates a new reference to g, and returns a func to release that -// reference. -func (g *Generation) Acquire() func() { - destroyed := atomic.LoadUint32(&g.destroyed) - if destroyed != 0 { - panic("acquire on generation " + g.name + " destroyed by " + g.destroyedBy) - } - g.wg.Add(1) - return g.wg.Done +// A RefCounted is a value whose functional lifetime is determined by +// reference counting. +// +// Its Acquire method is called before the Function is invoked, and +// the corresponding release is called when the Function returns. +// Usually both events happen within a single call to Get, so Get +// would be fine with a "borrowed" reference, but if the context is +// cancelled, Get may return before the Function is complete, causing +// the argument to escape, and potential premature destruction of the +// value. For a reference-counted type, this requires a pair of +// increment/decrement operations to extend its life. +type RefCounted interface { + // Acquire prevents the value from being destroyed until the + // returned function is called. + Acquire() func() } -// Arg is a marker interface that can be embedded to indicate a type is -// intended for use as a Function argument. -type Arg interface{ memoizeArg() } - // Function is the type for functions that can be memoized. -// The result must be a pointer. -type Function func(ctx context.Context, arg Arg) interface{} +// +// If the arg is a RefCounted, its Acquire/Release operations are called. +type Function func(ctx context.Context, arg interface{}) interface{} type state int -// TODO(rfindley): remove stateDestroyed; Handles should not need to know -// whether or not they have been destroyed. -// -// TODO(rfindley): also consider removing stateIdle. Why create a handle if you +// TODO(rfindley): consider removing stateIdle. Why create a handle if you // aren't certain you're going to need its result? And if you know you need its // result, why wait to begin computing it? const ( stateIdle = iota stateRunning stateCompleted - stateDestroyed ) // Handle is returned from a store when a key is bound to a function. @@ -136,19 +74,10 @@ const ( // they decrement waiters. If it drops to zero, the inner context is cancelled, // computation is abandoned, and state resets to idle to start the process over // again. -// -// Handles may be tracked by generations, or directly reference counted, as -// determined by the trackGenerations field. See the field comments for more -// information about the differences between these two forms. -// -// TODO(rfindley): eliminate generational handles. type Handle struct { key interface{} mu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu - // generations is the set of generations in which this handle is valid. - generations map[*Generation]struct{} - state state // done is set in running state, and closed when exiting it. done chan struct{} @@ -161,88 +90,49 @@ type Handle struct { // value is set in completed state. value interface{} - // If trackGenerations is set, this handle tracks generations in which it - // is valid, via the generations field. Otherwise, it is explicitly reference - // counted via the refCounter field. - trackGenerations bool - refCounter int32 + refcount int32 // accessed using atomic load/store } -// Bind returns a "generational" handle for the given key and function. +// Handle returns a reference-counted handle for the future result of +// calling the specified function. Calls to Handle with the same key +// return the same handle, and all calls to Handle.Get on a given +// handle return the same result but the function is called at most once. // -// Each call to bind will return the same handle if it is already bound. Bind -// will always return a valid handle, creating one if needed. Each key can -// only have one handle at any given time. The value will be held at least -// until the associated generation is destroyed. Bind does not cause the value -// to be generated. -// -// It is responsibility of the caller to call Inherit on the handler whenever -// it should still be accessible by a next generation. -func (g *Generation) Bind(key interface{}, function Function) *Handle { - return g.getHandle(key, function, true) -} - -// GetHandle returns a "reference-counted" handle for the given key -// and function with similar properties and behavior as Bind. Unlike -// Bind, it returns a release callback which must be called once the -// handle is no longer needed. -func (g *Generation) GetHandle(key interface{}, function Function) (*Handle, func()) { - h := g.getHandle(key, function, false) - store := g.store - release := func() { - // Acquire store.handlesMu before mutating refCounter - store.handlesMu.Lock() - defer store.handlesMu.Unlock() - - h.mu.Lock() - defer h.mu.Unlock() - - h.refCounter-- - if h.refCounter == 0 { - // Don't mark destroyed: for reference counted handles we can't know when - // they are no longer reachable from runnable goroutines. For example, - // gopls could have a current operation that is using a packageHandle. - // Destroying the handle here would cause that operation to hang. - delete(store.handles, h.key) - } - } - return h, release -} - -func (g *Generation) getHandle(key interface{}, function Function, trackGenerations bool) *Handle { - // panic early if the function is nil - // it would panic later anyway, but in a way that was much harder to debug +// The caller must call the returned function to decrement the +// handle's reference count when it is no longer needed. +func (store *Store) Handle(key interface{}, function Function) (*Handle, func()) { if function == nil { - panic("the function passed to bind must not be nil") + panic("nil function") } - if atomic.LoadUint32(&g.destroyed) != 0 { - panic("operation on generation " + g.name + " destroyed by " + g.destroyedBy) - } - g.store.handlesMu.Lock() - defer g.store.handlesMu.Unlock() - h, ok := g.store.handles[key] + + store.handlesMu.Lock() + h, ok := store.handles[key] if !ok { + // new handle h = &Handle{ - key: key, - function: function, - trackGenerations: trackGenerations, - } - if trackGenerations { - if g.store.boundHandles == nil { - g.store.boundHandles = map[*Handle]struct{}{} - } - h.generations = make(map[*Generation]struct{}, 1) - g.store.boundHandles[h] = struct{}{} + key: key, + function: function, + refcount: 1, } - if g.store.handles == nil { - g.store.handles = map[interface{}]*Handle{} + if store.handles == nil { + store.handles = map[interface{}]*Handle{} } - g.store.handles[key] = h + store.handles[key] = h + } else { + // existing handle + atomic.AddInt32(&h.refcount, 1) } + store.handlesMu.Unlock() - h.incrementRef(g) - return h + release := func() { + if atomic.AddInt32(&h.refcount, -1) == 0 { + store.handlesMu.Lock() + delete(store.handles, h.key) + store.handlesMu.Unlock() + } + } + return h, release } // Stats returns the number of each type of value in the store. @@ -278,53 +168,13 @@ func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { } } -// Inherit makes h valid in generation g. It is concurrency-safe. -func (g *Generation) Inherit(h *Handle) { - if atomic.LoadUint32(&g.destroyed) != 0 { - panic("inherit on generation " + g.name + " destroyed by " + g.destroyedBy) - } - if !h.trackGenerations { - panic("called Inherit on handle not created by Generation.Bind") - } - - h.incrementRef(g) -} - -func (h *Handle) incrementRef(g *Generation) { - h.mu.Lock() - defer h.mu.Unlock() - - if h.state == stateDestroyed { - panic(fmt.Sprintf("inheriting destroyed handle %#v (type %T) into generation %v", h.key, h.key, g.name)) - } - - if h.trackGenerations { - h.generations[g] = struct{}{} - } else { - h.refCounter++ - } -} - -// hasRefLocked reports whether h is valid in generation g. h.mu must be held. -func (h *Handle) hasRefLocked(g *Generation) bool { - if !h.trackGenerations { - return true - } - - _, ok := h.generations[g] - return ok -} - // Cached returns the value associated with a handle. // // It will never cause the value to be generated. // It will return the cached value, if present. -func (h *Handle) Cached(g *Generation) interface{} { +func (h *Handle) Cached() interface{} { h.mu.Lock() defer h.mu.Unlock() - if !h.hasRefLocked(g) { - return nil - } if h.state == stateCompleted { return h.value } @@ -334,54 +184,39 @@ func (h *Handle) Cached(g *Generation) interface{} { // Get returns the value associated with a handle. // // If the value is not yet ready, the underlying function will be invoked. -// If ctx is cancelled, Get returns nil. -func (h *Handle) Get(ctx context.Context, g *Generation, arg Arg) (interface{}, error) { - release := g.Acquire() - defer release() - +// If ctx is cancelled, Get returns (nil, Canceled). +func (h *Handle) Get(ctx context.Context, arg interface{}) (interface{}, error) { if ctx.Err() != nil { return nil, ctx.Err() } h.mu.Lock() - if !h.hasRefLocked(g) { - h.mu.Unlock() - - err := fmt.Errorf("reading key %#v: generation %v is not known", h.key, g.name) - if *panicOnDestroyed && ctx.Err() != nil { - panic(err) - } - return nil, err - } switch h.state { case stateIdle: - return h.run(ctx, g, arg) + return h.run(ctx, arg) case stateRunning: return h.wait(ctx) case stateCompleted: defer h.mu.Unlock() return h.value, nil - case stateDestroyed: - h.mu.Unlock() - err := fmt.Errorf("Get on destroyed entry %#v (type %T) in generation %v", h.key, h.key, g.name) - if *panicOnDestroyed { - panic(err) - } - return nil, err default: panic("unknown state") } } // run starts h.function and returns the result. h.mu must be locked. -func (h *Handle) run(ctx context.Context, g *Generation, arg Arg) (interface{}, error) { +func (h *Handle) run(ctx context.Context, arg interface{}) (interface{}, error) { childCtx, cancel := context.WithCancel(xcontext.Detach(ctx)) h.cancel = cancel h.state = stateRunning h.done = make(chan struct{}) function := h.function // Read under the lock - // Make sure that the generation isn't destroyed while we're running in it. - release := g.Acquire() + // Make sure that the argument isn't destroyed while we're running in it. + release := func() {} + if rc, ok := arg.(RefCounted); ok { + release = rc.Acquire() + } + go func() { trace.WithRegion(childCtx, fmt.Sprintf("Handle.run %T", h.key), func() { defer release() diff --git a/internal/memoize/memoize_test.go b/internal/memoize/memoize_test.go index 48bb181173e..bde02bf6136 100644 --- a/internal/memoize/memoize_test.go +++ b/internal/memoize/memoize_test.go @@ -6,7 +6,6 @@ package memoize_test import ( "context" - "strings" "sync" "testing" "time" @@ -15,90 +14,53 @@ import ( ) func TestGet(t *testing.T) { - s := &memoize.Store{} - g := s.Generation("x") + var store memoize.Store evaled := 0 - h := g.Bind("key", func(context.Context, memoize.Arg) interface{} { + h, release := store.Handle("key", func(context.Context, interface{}) interface{} { evaled++ return "res" }) - expectGet(t, h, g, "res") - expectGet(t, h, g, "res") + defer release() + expectGet(t, h, "res") + expectGet(t, h, "res") if evaled != 1 { t.Errorf("got %v calls to function, wanted 1", evaled) } } -func expectGet(t *testing.T, h *memoize.Handle, g *memoize.Generation, wantV interface{}) { +func expectGet(t *testing.T, h *memoize.Handle, wantV interface{}) { t.Helper() - gotV, gotErr := h.Get(context.Background(), g, nil) + gotV, gotErr := h.Get(context.Background(), nil) if gotV != wantV || gotErr != nil { t.Fatalf("Get() = %v, %v, wanted %v, nil", gotV, gotErr, wantV) } } -func expectGetError(t *testing.T, h *memoize.Handle, g *memoize.Generation, substr string) { - gotV, gotErr := h.Get(context.Background(), g, nil) - if gotErr == nil || !strings.Contains(gotErr.Error(), substr) { - t.Fatalf("Get() = %v, %v, wanted err %q", gotV, gotErr, substr) - } -} - -func TestGenerations(t *testing.T) { - s := &memoize.Store{} - // Evaluate key in g1. - g1 := s.Generation("g1") - h1 := g1.Bind("key", func(context.Context, memoize.Arg) interface{} { return "res" }) - expectGet(t, h1, g1, "res") - - // Get key in g2. It should inherit the value from g1. - g2 := s.Generation("g2") - h2 := g2.Bind("key", func(context.Context, memoize.Arg) interface{} { - t.Fatal("h2 should not need evaluation") - return "error" - }) - expectGet(t, h2, g2, "res") - - // With g1 destroyed, g2 should still work. - g1.Destroy("TestGenerations") - expectGet(t, h2, g2, "res") - - // With all generations destroyed, key should be re-evaluated. - g2.Destroy("TestGenerations") - g3 := s.Generation("g3") - h3 := g3.Bind("key", func(context.Context, memoize.Arg) interface{} { return "new res" }) - expectGet(t, h3, g3, "new res") -} - func TestHandleRefCounting(t *testing.T) { - s := &memoize.Store{} - g1 := s.Generation("g1") + var store memoize.Store v1 := false v2 := false - h1, release1 := g1.GetHandle("key1", func(context.Context, memoize.Arg) interface{} { + h1, release1 := store.Handle("key1", func(context.Context, interface{}) interface{} { return &v1 }) - h2, release2 := g1.GetHandle("key2", func(context.Context, memoize.Arg) interface{} { + h2, release2 := store.Handle("key2", func(context.Context, interface{}) interface{} { return &v2 }) - expectGet(t, h1, g1, &v1) - expectGet(t, h2, g1, &v2) + expectGet(t, h1, &v1) + expectGet(t, h2, &v2) - g2 := s.Generation("g2") - expectGet(t, h1, g2, &v1) - g1.Destroy("by test") - expectGet(t, h2, g2, &v2) + expectGet(t, h1, &v1) + expectGet(t, h2, &v2) - h2Copy, release2Copy := g2.GetHandle("key2", func(context.Context, memoize.Arg) interface{} { + h2Copy, release2Copy := store.Handle("key2", func(context.Context, interface{}) interface{} { return &v1 }) if h2 != h2Copy { t.Error("NewHandle returned a new value while old is not destroyed yet") } - expectGet(t, h2Copy, g2, &v2) - g2.Destroy("by test") + expectGet(t, h2Copy, &v2) release2() if got, want := v2, false; got != want { @@ -110,27 +72,23 @@ func TestHandleRefCounting(t *testing.T) { } release1() - g3 := s.Generation("g3") - h2Copy, release2Copy = g3.GetHandle("key2", func(context.Context, memoize.Arg) interface{} { + h2Copy, release2Copy = store.Handle("key2", func(context.Context, interface{}) interface{} { return &v2 }) if h2 == h2Copy { t.Error("NewHandle returned previously destroyed value") } release2Copy() - g3.Destroy("by test") } func TestHandleDestroyedWhileRunning(t *testing.T) { - // Test that calls to Handle.Get return even if the handle is destroyed while - // running. + // Test that calls to Handle.Get return even if the handle is destroyed while running. - s := &memoize.Store{} - g := s.Generation("g") + var store memoize.Store c := make(chan int) var v int - h, release := g.GetHandle("key", func(ctx context.Context, _ memoize.Arg) interface{} { + h, release := store.Handle("key", func(ctx context.Context, _ interface{}) interface{} { <-c <-c if err := ctx.Err(); err != nil { @@ -147,7 +105,7 @@ func TestHandleDestroyedWhileRunning(t *testing.T) { var got interface{} var err error go func() { - got, err = h.Get(ctx, g, nil) + got, err = h.Get(ctx, nil) wg.Done() }() From d6c099e3c1a39214a49a5b7c5c5faa604650e581 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 5 Jul 2022 16:10:16 -0400 Subject: [PATCH 087/723] internal/memoize: document stateIdle, RefCounted ...now that I have understood why they are not in fact inessential. Change-Id: I1ab881a7d24cd71ee183bc8c6a1a058dbda641e2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/416077 TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- internal/lsp/cache/check.go | 4 +-- internal/memoize/memoize.go | 68 +++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index 4680c6e7285..a2599f930c2 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -167,9 +167,9 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // Create a handle for the result of type checking. experimentalKey := s.View().Options().ExperimentalPackageCacheKey key := computePackageKey(m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) + // TODO(adonovan): extract lambda into a standalone function to + // avoid implicit lexical dependencies. handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} { - // TODO(adonovan): eliminate use of arg with this handle. - // (In all cases snapshot is equal to the enclosing s.) snapshot := arg.(*snapshot) // Start type checking of direct dependencies, diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 2db7945428f..6d62ebb0d96 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -29,6 +29,21 @@ type Store struct { handles map[interface{}]*Handle } +// Function is the type of a function that can be memoized. +// +// If the arg is a RefCounted, its Acquire/Release operations are called. +// +// The argument must not materially affect the result of the function +// in ways that are not captured by the handle's key, since if +// Handle.Get is called twice concurrently, with the same (implicit) +// key but different arguments, the Function is called only once but +// its result must be suitable for both callers. +// +// The main purpose of the argument is to avoid the Function closure +// needing to retain large objects (in practice: the snapshot) in +// memory that can be supplied at call time by any caller. +type Function func(ctx context.Context, arg interface{}) interface{} + // A RefCounted is a value whose functional lifetime is determined by // reference counting. // @@ -46,38 +61,32 @@ type RefCounted interface { Acquire() func() } -// Function is the type for functions that can be memoized. -// -// If the arg is a RefCounted, its Acquire/Release operations are called. -type Function func(ctx context.Context, arg interface{}) interface{} - type state int -// TODO(rfindley): consider removing stateIdle. Why create a handle if you -// aren't certain you're going to need its result? And if you know you need its -// result, why wait to begin computing it? const ( - stateIdle = iota - stateRunning - stateCompleted + stateIdle = iota // newly constructed, or last waiter was cancelled + stateRunning // start was called and not cancelled + stateCompleted // function call ran to completion ) -// Handle is returned from a store when a key is bound to a function. -// It is then used to access the results of that function. -// -// A Handle starts out in idle state, waiting for something to demand its -// evaluation. It then transitions into running state. While it's running, -// waiters tracks the number of Get calls waiting for a result, and the done -// channel is used to notify waiters of the next state transition. Once the -// evaluation finishes, value is set, state changes to completed, and done -// is closed, unblocking waiters. Alternatively, as Get calls are cancelled, -// they decrement waiters. If it drops to zero, the inner context is cancelled, -// computation is abandoned, and state resets to idle to start the process over -// again. +// A Handle represents the future result of a call to a function. type Handle struct { key interface{} mu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu + // A Handle starts out IDLE, waiting for something to demand + // its evaluation. It then transitions into RUNNING state. + // + // While RUNNING, waiters tracks the number of Get calls + // waiting for a result, and the done channel is used to + // notify waiters of the next state transition. Once + // evaluation finishes, value is set, state changes to + // COMPLETED, and done is closed, unblocking waiters. + // + // Alternatively, as Get calls are cancelled, they decrement + // waiters. If it drops to zero, the inner context is + // cancelled, computation is abandoned, and state resets to + // IDLE to start the process over again. state state // done is set in running state, and closed when exiting it. done chan struct{} @@ -155,16 +164,9 @@ func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { defer s.handlesMu.Unlock() for k, h := range s.handles { - var v interface{} - h.mu.Lock() - if h.state == stateCompleted { - v = h.value - } - h.mu.Unlock() - if v == nil { - continue + if v := h.Cached(); v != nil { + f(k, v) } - f(k, v) } } @@ -241,7 +243,7 @@ func (h *Handle) run(ctx context.Context, arg interface{}) (interface{}, error) } h.value = v - h.function = nil + h.function = nil // aid GC h.state = stateCompleted close(h.done) }) From 42457a544a678826371e9ee4f874257a54314320 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Sun, 10 Jul 2022 12:41:17 -0400 Subject: [PATCH 088/723] internal/lsp/cache: don't pin a snapshot to view.importsState Whenever a view.importsState value is created, we use snapshot.goCommandInvocation to extract information used to run the go command in the imports.ProcessEnv. Before this CL, that process acquired the current snapshot (whatever it was), and held onto it for the purpose of sharing a workspace directory. As a consequence all the memory in that snapshot was pinned for the lifecycle of the importsState, which can be the entire editing session. This results in a memory leak as information in the session is invalidated. Fix this by creating a copy of the workspace directory to be owned by the importsState. Also: - Add some TODOs - Clean up some stale comments Fixes golang/go#53780 Change-Id: I2c55cc26b2d46c9320c6c7cd86b3e24971cd5073 Reviewed-on: https://go-review.googlesource.com/c/tools/+/416874 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley --- internal/lsp/cache/imports.go | 44 ++++++++++++++++++++++++---------- internal/lsp/cache/snapshot.go | 40 ++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go index 710a1f3407a..7877c4f074d 100644 --- a/internal/lsp/cache/imports.go +++ b/internal/lsp/cache/imports.go @@ -7,6 +7,7 @@ package cache import ( "context" "fmt" + "os" "reflect" "strings" "sync" @@ -141,21 +142,20 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho pe.Logf = nil } - // Take an extra reference to the snapshot so that its workspace directory - // (if any) isn't destroyed while we're using it. - release := snapshot.Acquire() + // Extract invocation details from the snapshot to use with goimports. + // + // TODO(rfindley): refactor to extract the necessary invocation logic into + // separate functions. Using goCommandInvocation is unnecessarily indirect, + // and has led to memory leaks in the past, when the snapshot was + // unintentionally held past its lifetime. _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ WorkingDir: snapshot.view.rootURI.Filename(), }) if err != nil { - release() return nil, err } - pe.WorkingDir = inv.WorkingDir + pe.BuildFlags = inv.BuildFlags - pe.WorkingDir = inv.WorkingDir - pe.ModFile = inv.ModFile - pe.ModFlag = inv.ModFlag pe.Env = map[string]string{} for _, kv := range inv.Env { split := strings.SplitN(kv, "=", 2) @@ -164,11 +164,31 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho } pe.Env[split[0]] = split[1] } + // We don't actually use the invocation, so clean it up now. + cleanupInvocation() + + // If the snapshot uses a synthetic workspace directory, create a copy for + // the lifecycle of the importsState. + // + // Notably, we cannot use the snapshot invocation working directory, as that + // is tied to the lifecycle of the snapshot. + // + // Otherwise return a no-op cleanup function. + cleanup = func() {} + if snapshot.usesWorkspaceDir() { + tmpDir, err := makeWorkspaceDir(ctx, snapshot.workspace, snapshot) + if err != nil { + return nil, err + } + pe.WorkingDir = tmpDir + cleanup = func() { + os.RemoveAll(tmpDir) // ignore error + } + } else { + pe.WorkingDir = snapshot.view.rootURI.Filename() + } - return func() { - cleanupInvocation() - release() - }, nil + return cleanup, nil } func (s *importsState) refreshProcessEnv() { diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index fa71fbd8a80..fe56d67acdd 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -383,6 +383,11 @@ func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd stri return true, modBytes, sumBytes, nil } +// goCommandInvocation populates inv with configuration for running go commands on the snapshot. +// +// TODO(rfindley): refactor this function to compose the required configuration +// explicitly, rather than implicitly deriving it from flags and inv. +// // TODO(adonovan): remove unused cleanup mechanism. func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.InvocationFlags, inv *gocommand.Invocation) (tmpURI span.URI, updatedInv *gocommand.Invocation, cleanup func(), err error) { s.view.optionsMu.Lock() @@ -412,7 +417,6 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat // - the working directory. // - the -mod flag // - the -modfile flag - // - the -workfile flag // // These are dependent on a number of factors: whether we need to run in a // synthetic workspace, whether flags are supported at the current go @@ -463,6 +467,9 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat } } + // TODO(rfindley): in the case of go.work mode, modURI is empty and we fall + // back on the default behavior of vendorEnabled with an empty modURI. Figure + // out what is correct here and implement it explicitly. vendorEnabled, err := s.vendorEnabled(ctx, modURI, modContent) if err != nil { return "", nil, cleanup, err @@ -498,13 +505,15 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return "", nil, cleanup, source.ErrTmpModfileUnsupported } - // We should use -workfile if: - // 1. We're not actively trying to mutate a modfile. - // 2. We have an active go.work file. - // 3. We're using at least Go 1.18. + // We should use -modfile if: + // - the workspace mode supports it + // - we're using a go.work file on go1.18+, or we need a temp mod file (for + // example, if running go mod tidy in a go.work workspace) + // + // TODO(rfindley): this is very hard to follow. Refactor. useWorkFile := !needTempMod && s.workspace.moduleSource == goWorkWorkspace && s.view.goversion >= 18 if useWorkFile { - // TODO(#51215): build a temp workfile and set GOWORK in the environment. + // Since we're running in the workspace root, the go command will resolve GOWORK automatically. } else if useTempMod { if modURI == "" { return "", nil, cleanup, fmt.Errorf("no go.mod file found in %s", inv.WorkingDir) @@ -525,6 +534,25 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return tmpURI, inv, cleanup, nil } +// usesWorkspaceDir reports whether the snapshot should use a synthetic +// workspace directory for running workspace go commands such as go list. +// +// TODO(rfindley): this logic is duplicated with goCommandInvocation. Clean up +// the latter, and deduplicate. +func (s *snapshot) usesWorkspaceDir() bool { + switch s.workspace.moduleSource { + case legacyWorkspace: + return false + case goWorkWorkspace: + if s.view.goversion >= 18 { + return false + } + // Before go 1.18, the Go command did not natively support go.work files, + // so we 'fake' them with a workspace module. + } + return true +} + func (s *snapshot) buildOverlay() map[string][]byte { s.mu.Lock() defer s.mu.Unlock() From 71dc5e295f34038a34374651070f7039a4682133 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 8 Jul 2022 16:01:25 -0400 Subject: [PATCH 089/723] internal/lsp/cache: make snapshot reference counting uniform Previously, snapshots were born with a reference count of zero. Although leases were created and returned by functions that return a snapshot, and correctly disposed of by the caller, the view did not hold a lease for its snapshot. The view's lease on the snapshot was implicit, and for this reason the view made explicit calls to Destroy when these implicit leases ended, which would then wait for all explicit leases to end. Now that the view holds an explicit lease to its snapshot, it is safe (as a follow-up change) to move the destroy logic into the call of release() that brings the refcount to zero. Also: - clarify that release functions should be called when and only when err is nil. - in View: remove unused View.cancel field. - in tempModFile: clarify tricky return + defer + named-result-var statement. - in DidModifyFiles: simplify type of releases from []func() to func(). - in Server.addFolders: fix reference leak (looks minor). Change-Id: Ibf61f4ce109e91060e2ccd854c32214b119f2f0a Reviewed-on: https://go-review.googlesource.com/c/tools/+/416875 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- internal/lsp/cache/session.go | 45 +++++++++++------------ internal/lsp/cache/snapshot.go | 29 ++++++++++++--- internal/lsp/cache/view.go | 53 ++++++++++++++-------------- internal/lsp/general.go | 11 ++++-- internal/lsp/mod/mod_test.go | 2 +- internal/lsp/source/source_test.go | 2 +- internal/lsp/source/view.go | 6 ++-- internal/lsp/text_synchronization.go | 6 ++-- internal/lsp/workspace.go | 6 ++-- 9 files changed, 92 insertions(+), 68 deletions(-) diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index a46b7928c78..8c90c77ea8d 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -165,7 +165,7 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt } view, snapshot, release, err := s.createView(ctx, name, folder, options, 0) if err != nil { - return nil, nil, func() {}, err + return nil, nil, nil, err } s.views = append(s.views, view) // we always need to drop the view map @@ -249,33 +249,23 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, knownSubdirs: newKnownDirsSet(), workspace: workspace, } + // Save one reference in the view. + v.releaseSnapshot = v.snapshot.Acquire() // Initialize the view without blocking. initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx)) v.initCancelFirstAttempt = initCancel snapshot := v.snapshot - // Acquire both references before the possibility - // of releasing either one, to avoid premature - // destruction if initialize returns quickly. - // - // TODO(adonovan): our reference counting discipline is not sound: - // the count is initially zero and incremented/decremented by - // acquire/release, but there is a race between object birth - // and the first call to acquire during which the snapshot may be - // destroyed. - // - // In most systems, an object is born with a count of 1 and - // destroyed by any decref that brings the count to zero. - // We should do that too. - release1 := snapshot.Acquire() - release2 := snapshot.Acquire() + // Pass a second reference to the background goroutine. + bgRelease := snapshot.Acquire() go func() { - defer release2() + defer bgRelease() snapshot.initialize(initCtx, true) }() - return v, snapshot, release1, nil + // Return a third reference to the caller. + return v, snapshot, snapshot.Acquire(), nil } // View returns the view by name. @@ -427,10 +417,8 @@ func (s *Session) dropView(ctx context.Context, v *View) (int, error) { } func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModification) error { - _, releases, err := s.DidModifyFiles(ctx, changes) - for _, release := range releases { - release() - } + _, release, err := s.DidModifyFiles(ctx, changes) + release() return err } @@ -445,7 +433,7 @@ type fileChange struct { isUnchanged bool } -func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]span.URI, []func(), error) { +func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]span.URI, func(), error) { s.viewMu.RLock() defer s.viewMu.RUnlock() views := make(map[*View]map[span.URI]*fileChange) @@ -526,6 +514,14 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif viewToSnapshot[view] = snapshot } + // The release function is called when the + // returned URIs no longer need to be valid. + release := func() { + for _, release := range releases { + release() + } + } + // We only want to diagnose each changed file once, in the view to which // it "most" belongs. We do this by picking the best view for each URI, // and then aggregating the set of snapshots and their URIs (to avoid @@ -543,7 +539,8 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif } snapshotURIs[snapshot] = append(snapshotURIs[snapshot], mod.URI) } - return snapshotURIs, releases, nil + + return snapshotURIs, release, nil } func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []source.FileModification) []source.FileModification { diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index fe56d67acdd..c2438cdf208 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -154,6 +154,14 @@ type snapshot struct { var _ memoize.RefCounted = (*snapshot)(nil) // snapshots are reference-counted // Acquire prevents the snapshot from being destroyed until the returned function is called. +// +// (s.Acquire().release() could instead be expressed as a pair of +// method calls s.IncRef(); s.DecRef(). The latter has the advantage +// that the DecRefs are fungible and don't require holding anything in +// addition to the refcounted object s, but paradoxically that is also +// an advantage of the current approach, which forces the caller to +// consider the release function at every stage, making a reference +// leak more obvious.) func (s *snapshot) Acquire() func() { type uP = unsafe.Pointer if destroyedBy := atomic.LoadPointer((*uP)(uP(&s.destroyedBy))); destroyedBy != nil { @@ -177,7 +185,14 @@ type actionKey struct { analyzer *analysis.Analyzer } -func (s *snapshot) Destroy(destroyedBy string) { +// destroy waits for all leases on the snapshot to expire then releases +// any resources (reference counts and files) associated with it. +// +// TODO(adonovan): move this logic into the release function returned +// by Acquire when the reference count becomes zero. (This would cost +// us the destroyedBy debug info, unless we add it to the signature of +// memoize.RefCounted.Acquire.) +func (s *snapshot) destroy(destroyedBy string) { // Wait for all leases to end before commencing destruction. s.refcount.Wait() @@ -388,7 +403,9 @@ func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd stri // TODO(rfindley): refactor this function to compose the required configuration // explicitly, rather than implicitly deriving it from flags and inv. // -// TODO(adonovan): remove unused cleanup mechanism. +// TODO(adonovan): simplify cleanup mechanism. It's hard to see, but +// it used only after call to tempModFile. Clarify that it is only +// non-nil on success. func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.InvocationFlags, inv *gocommand.Invocation) (tmpURI span.URI, updatedInv *gocommand.Invocation, cleanup func(), err error) { s.view.optionsMu.Lock() allowModfileModificationOption := s.view.options.AllowModfileModifications @@ -1715,7 +1732,7 @@ func (ac *unappliedChanges) GetFile(ctx context.Context, uri span.URI) (source.F return ac.originalSnapshot.GetFile(ctx, uri) } -func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) *snapshot { +func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) (*snapshot, func()) { ctx, done := event.Start(ctx, "snapshot.clone") defer done() @@ -1754,6 +1771,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC knownSubdirs: s.knownSubdirs.Clone(), workspace: newWorkspace, } + // Create a lease on the new snapshot. + // (Best to do this early in case the code below hides an + // incref/decref operation that might destroy it prematurely.) + release := result.Acquire() // Copy the set of unloadable files. for k, v := range s.unloadableFiles { @@ -2011,7 +2032,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } result.dumpWorkspace("clone") - return result + return result, release } // invalidatedPackageIDs returns all packages invalidated by a change to uri. diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index f95c4759219..8ed9ecd0d16 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -47,10 +47,6 @@ type View struct { // background contexts created for this view. baseCtx context.Context - // cancel is called when all action being performed by the current view - // should be stopped. - cancel context.CancelFunc - // name is the user visible name of this view. name string @@ -71,8 +67,9 @@ type View struct { // attempt at initialization. initCancelFirstAttempt context.CancelFunc - snapshotMu sync.Mutex - snapshot *snapshot // nil after shutdown has been called + snapshotMu sync.Mutex + snapshot *snapshot // nil after shutdown has been called + releaseSnapshot func() // called when snapshot is no longer needed // initialWorkspaceLoad is closed when the first workspace initialization has // completed. If we failed to load, we only retry if the go.mod file changes, @@ -161,9 +158,10 @@ func (f *fileBase) addURI(uri span.URI) int { func (v *View) ID() string { return v.id } -// tempModFile creates a temporary go.mod file based on the contents of the -// given go.mod file. It is the caller's responsibility to clean up the files -// when they are done using them. +// tempModFile creates a temporary go.mod file based on the contents +// of the given go.mod file. On success, it is the caller's +// responsibility to call the cleanup function when the file is no +// longer needed. func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanup func(), err error) { filenameHash := source.Hashf("%s", modFh.URI().Filename()) tmpMod, err := ioutil.TempFile("", fmt.Sprintf("go.%s.*.mod", filenameHash)) @@ -184,7 +182,9 @@ func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanu return "", nil, err } - cleanup = func() { + // We use a distinct name here to avoid subtlety around the fact + // that both 'return' and 'defer' update the "cleanup" variable. + doCleanup := func() { _ = os.Remove(tmpSumName) _ = os.Remove(tmpURI.Filename()) } @@ -192,7 +192,7 @@ func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanu // Be careful to clean up if we return an error from this function. defer func() { if err != nil { - cleanup() + doCleanup() cleanup = nil } }() @@ -200,11 +200,11 @@ func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanu // Create an analogous go.sum, if one exists. if gosum != nil { if err := ioutil.WriteFile(tmpSumName, gosum, 0655); err != nil { - return "", cleanup, err + return "", nil, err } } - return tmpURI, cleanup, nil + return tmpURI, doCleanup, nil } // Name returns the user visible name of this view. @@ -527,18 +527,15 @@ func (v *View) shutdown(ctx context.Context) { // Cancel the initial workspace load if it is still running. v.initCancelFirstAttempt() - v.mu.Lock() - if v.cancel != nil { - v.cancel() - v.cancel = nil - } - v.mu.Unlock() v.snapshotMu.Lock() if v.snapshot != nil { - go v.snapshot.Destroy("View.shutdown") + v.releaseSnapshot() + go v.snapshot.destroy("View.shutdown") v.snapshot = nil + v.releaseSnapshot = nil } v.snapshotMu.Unlock() + v.importsState.destroy() } @@ -718,22 +715,26 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]*file v.snapshotMu.Lock() defer v.snapshotMu.Unlock() - if v.snapshot == nil { + prevSnapshot, prevReleaseSnapshot := v.snapshot, v.releaseSnapshot + + if prevSnapshot == nil { panic("invalidateContent called after shutdown") } // Cancel all still-running previous requests, since they would be // operating on stale data. - v.snapshot.cancel() + prevSnapshot.cancel() // Do not clone a snapshot until its view has finished initializing. - v.snapshot.AwaitInitialized(ctx) + prevSnapshot.AwaitInitialized(ctx) - oldSnapshot := v.snapshot + // Save one lease of the cloned snapshot in the view. + v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata) - v.snapshot = oldSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata) - go oldSnapshot.Destroy("View.invalidateContent") + prevReleaseSnapshot() + go prevSnapshot.destroy("View.invalidateContent") + // Return a second lease to the caller. return v.snapshot, v.snapshot.Acquire() } diff --git a/internal/lsp/general.go b/internal/lsp/general.go index 06633acb0c4..fbb9692ea82 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -249,17 +249,21 @@ func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol } work := s.progress.Start(ctx, "Setting up workspace", "Loading packages...", nil, nil) snapshot, release, err := s.addView(ctx, folder.Name, uri) - if err == source.ErrViewExists { - continue - } if err != nil { + if err == source.ErrViewExists { + continue + } viewErrors[uri] = err work.End(ctx, fmt.Sprintf("Error loading packages: %s", err)) continue } + // Inv: release() must be called once. + var swg sync.WaitGroup swg.Add(1) allFoldersWg.Add(1) + // TODO(adonovan): this looks fishy. Is AwaitInitialized + // supposed to be called once per folder? go func() { defer swg.Done() defer allFoldersWg.Done() @@ -271,6 +275,7 @@ func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol buf := &bytes.Buffer{} if err := snapshot.WriteEnv(ctx, buf); err != nil { viewErrors[uri] = err + release() continue } event.Log(ctx, buf.String()) diff --git a/internal/lsp/mod/mod_test.go b/internal/lsp/mod/mod_test.go index b2d257caeeb..09a182d16d7 100644 --- a/internal/lsp/mod/mod_test.go +++ b/internal/lsp/mod/mod_test.go @@ -46,10 +46,10 @@ func TestModfileRemainsUnchanged(t *testing.T) { t.Fatal(err) } _, _, release, err := session.NewView(ctx, "diagnostics_test", span.URIFromPath(folder), options) - release() if err != nil { t.Fatal(err) } + release() after, err := ioutil.ReadFile(filepath.Join(folder, "go.mod")) if err != nil { t.Fatal(err) diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index 9218f9ddc1a..8beb8a5dde0 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -55,10 +55,10 @@ func testSource(t *testing.T, datum *tests.Data) { tests.DefaultOptions(options) options.SetEnvSlice(datum.Config.Env) view, _, release, err := session.NewView(ctx, "source_test", span.URIFromPath(datum.Config.Dir), options) - release() if err != nil { t.Fatal(err) } + release() defer view.Shutdown(ctx) // Enable type error analyses for tests. diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index d7e212a121d..004d830dd07 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -352,7 +352,7 @@ type Session interface { // NewView creates a new View, returning it and its first snapshot. If a // non-empty tempWorkspace directory is provided, the View will record a copy // of its gopls workspace module in that directory, so that client tooling - // can execute in the same main module. It returns a release + // can execute in the same main module. On success it also returns a release // function that must be called when the Snapshot is no longer needed. NewView(ctx context.Context, name string, folder span.URI, options *Options) (View, Snapshot, func(), error) @@ -377,9 +377,9 @@ type Session interface { // DidModifyFile reports a file modification to the session. It returns // the new snapshots after the modifications have been applied, paired with // the affected file URIs for those snapshots. - // On success, it returns a list of release functions that + // On success, it returns a release function that // must be called when the snapshots are no longer needed. - DidModifyFiles(ctx context.Context, changes []FileModification) (map[Snapshot][]span.URI, []func(), error) + DidModifyFiles(ctx context.Context, changes []FileModification) (map[Snapshot][]span.URI, func(), error) // ExpandModificationsToDirectories returns the set of changes with the // directory changes removed and expanded to include all of the files in diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go index 3276a47bf99..514834b077c 100644 --- a/internal/lsp/text_synchronization.go +++ b/internal/lsp/text_synchronization.go @@ -290,7 +290,7 @@ func (s *Server) processModifications(ctx context.Context, modifications []sourc // to their files. modifications = s.session.ExpandModificationsToDirectories(ctx, modifications) - snapshots, releases, err := s.session.DidModifyFiles(ctx, modifications) + snapshots, release, err := s.session.DidModifyFiles(ctx, modifications) if err != nil { close(diagnoseDone) return err @@ -298,9 +298,7 @@ func (s *Server) processModifications(ctx context.Context, modifications []sourc go func() { s.diagnoseSnapshots(snapshots, onDisk) - for _, release := range releases { - release() - } + release() close(diagnoseDone) }() diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go index a1f837e2309..b41406db5dc 100644 --- a/internal/lsp/workspace.go +++ b/internal/lsp/workspace.go @@ -26,16 +26,18 @@ func (s *Server) didChangeWorkspaceFolders(ctx context.Context, params *protocol return s.addFolders(ctx, event.Added) } +// addView returns a Snapshot and a release function that must be +// called when it is no longer needed. func (s *Server) addView(ctx context.Context, name string, uri span.URI) (source.Snapshot, func(), error) { s.stateMu.Lock() state := s.state s.stateMu.Unlock() if state < serverInitialized { - return nil, func() {}, fmt.Errorf("addView called before server initialized") + return nil, nil, fmt.Errorf("addView called before server initialized") } options := s.session.Options().Clone() if err := s.fetchConfig(ctx, name, uri, options); err != nil { - return nil, func() {}, err + return nil, nil, err } _, snapshot, release, err := s.session.NewView(ctx, name, uri, options) return snapshot, release, err From b6e495100ec74eb3127d0b0af75ec441e2979077 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 11 Jul 2022 21:58:26 +0000 Subject: [PATCH 090/723] Revert "internal/lsp/cache: don't pin a snapshot to view.importsState" This reverts commit 42457a544a678826371e9ee4f874257a54314320. Reason for revert: test flakes (golang/go#53796) Change-Id: I9d7061220b43f9de88060a0bba5c5635d92fe3d9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/416879 Reviewed-by: Alan Donovan Auto-Submit: Robert Findley Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/lsp/cache/imports.go | 44 ++++++++++------------------------ internal/lsp/cache/snapshot.go | 40 +++++-------------------------- 2 files changed, 18 insertions(+), 66 deletions(-) diff --git a/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go index 7877c4f074d..710a1f3407a 100644 --- a/internal/lsp/cache/imports.go +++ b/internal/lsp/cache/imports.go @@ -7,7 +7,6 @@ package cache import ( "context" "fmt" - "os" "reflect" "strings" "sync" @@ -142,20 +141,21 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho pe.Logf = nil } - // Extract invocation details from the snapshot to use with goimports. - // - // TODO(rfindley): refactor to extract the necessary invocation logic into - // separate functions. Using goCommandInvocation is unnecessarily indirect, - // and has led to memory leaks in the past, when the snapshot was - // unintentionally held past its lifetime. + // Take an extra reference to the snapshot so that its workspace directory + // (if any) isn't destroyed while we're using it. + release := snapshot.Acquire() _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ WorkingDir: snapshot.view.rootURI.Filename(), }) if err != nil { + release() return nil, err } - + pe.WorkingDir = inv.WorkingDir pe.BuildFlags = inv.BuildFlags + pe.WorkingDir = inv.WorkingDir + pe.ModFile = inv.ModFile + pe.ModFlag = inv.ModFlag pe.Env = map[string]string{} for _, kv := range inv.Env { split := strings.SplitN(kv, "=", 2) @@ -164,31 +164,11 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho } pe.Env[split[0]] = split[1] } - // We don't actually use the invocation, so clean it up now. - cleanupInvocation() - - // If the snapshot uses a synthetic workspace directory, create a copy for - // the lifecycle of the importsState. - // - // Notably, we cannot use the snapshot invocation working directory, as that - // is tied to the lifecycle of the snapshot. - // - // Otherwise return a no-op cleanup function. - cleanup = func() {} - if snapshot.usesWorkspaceDir() { - tmpDir, err := makeWorkspaceDir(ctx, snapshot.workspace, snapshot) - if err != nil { - return nil, err - } - pe.WorkingDir = tmpDir - cleanup = func() { - os.RemoveAll(tmpDir) // ignore error - } - } else { - pe.WorkingDir = snapshot.view.rootURI.Filename() - } - return cleanup, nil + return func() { + cleanupInvocation() + release() + }, nil } func (s *importsState) refreshProcessEnv() { diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index c2438cdf208..a5cb74355e7 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -398,11 +398,6 @@ func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd stri return true, modBytes, sumBytes, nil } -// goCommandInvocation populates inv with configuration for running go commands on the snapshot. -// -// TODO(rfindley): refactor this function to compose the required configuration -// explicitly, rather than implicitly deriving it from flags and inv. -// // TODO(adonovan): simplify cleanup mechanism. It's hard to see, but // it used only after call to tempModFile. Clarify that it is only // non-nil on success. @@ -434,6 +429,7 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat // - the working directory. // - the -mod flag // - the -modfile flag + // - the -workfile flag // // These are dependent on a number of factors: whether we need to run in a // synthetic workspace, whether flags are supported at the current go @@ -484,9 +480,6 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat } } - // TODO(rfindley): in the case of go.work mode, modURI is empty and we fall - // back on the default behavior of vendorEnabled with an empty modURI. Figure - // out what is correct here and implement it explicitly. vendorEnabled, err := s.vendorEnabled(ctx, modURI, modContent) if err != nil { return "", nil, cleanup, err @@ -522,15 +515,13 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return "", nil, cleanup, source.ErrTmpModfileUnsupported } - // We should use -modfile if: - // - the workspace mode supports it - // - we're using a go.work file on go1.18+, or we need a temp mod file (for - // example, if running go mod tidy in a go.work workspace) - // - // TODO(rfindley): this is very hard to follow. Refactor. + // We should use -workfile if: + // 1. We're not actively trying to mutate a modfile. + // 2. We have an active go.work file. + // 3. We're using at least Go 1.18. useWorkFile := !needTempMod && s.workspace.moduleSource == goWorkWorkspace && s.view.goversion >= 18 if useWorkFile { - // Since we're running in the workspace root, the go command will resolve GOWORK automatically. + // TODO(#51215): build a temp workfile and set GOWORK in the environment. } else if useTempMod { if modURI == "" { return "", nil, cleanup, fmt.Errorf("no go.mod file found in %s", inv.WorkingDir) @@ -551,25 +542,6 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return tmpURI, inv, cleanup, nil } -// usesWorkspaceDir reports whether the snapshot should use a synthetic -// workspace directory for running workspace go commands such as go list. -// -// TODO(rfindley): this logic is duplicated with goCommandInvocation. Clean up -// the latter, and deduplicate. -func (s *snapshot) usesWorkspaceDir() bool { - switch s.workspace.moduleSource { - case legacyWorkspace: - return false - case goWorkWorkspace: - if s.view.goversion >= 18 { - return false - } - // Before go 1.18, the Go command did not natively support go.work files, - // so we 'fake' them with a workspace module. - } - return true -} - func (s *snapshot) buildOverlay() map[string][]byte { s.mu.Lock() defer s.mu.Unlock() From bc957ec62f029f49308c06e257f078b6c74e9d2f Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 11 Jul 2022 20:42:38 -0400 Subject: [PATCH 091/723] internal/lsp/source: use token.File-agnostic positions to dedupe refs We were already using a token.File-agnostic position for object positions in our references algorithm, but still relied on token.Pos for identifying duplicate references. This breaks down when a file may have multiple parsed representations in different packages. While we should endeavor to eliminate duplicate parsing, our algorithms should not rely on this for correctness. Update the reference de-duplication to use the same position key as object search. For golang/go#53796 Change-Id: Ic2e6c23380ea4e6b2747e4e5b45d7bfa6e656f0f Reviewed-on: https://go-review.googlesource.com/c/tools/+/416881 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- internal/lsp/source/implementation.go | 42 +++++++++++++++------------ internal/lsp/source/references.go | 12 ++++++-- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/internal/lsp/source/implementation.go b/internal/lsp/source/implementation.go index 6666605a99a..39a9289d1d6 100644 --- a/internal/lsp/source/implementation.go +++ b/internal/lsp/source/implementation.go @@ -235,17 +235,23 @@ func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, p if err != nil { return nil, err } - return qualifiedObjsAtLocation(ctx, s, objSearchKey{uri, offset}, map[objSearchKey]bool{}) + return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{}) } -type objSearchKey struct { +// A positionKey identifies a byte offset within a file (URI). +// +// When a file has been parsed multiple times in the same FileSet, +// there may be multiple token.Pos values denoting the same logical +// position. In such situations, a positionKey may be used for +// de-duplication. +type positionKey struct { uri span.URI offset int } // qualifiedObjsAtLocation finds all objects referenced at offset in uri, across // all packages in the snapshot. -func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key objSearchKey, seen map[objSearchKey]bool) ([]qualifiedObject, error) { +func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, seen map[positionKey]bool) ([]qualifiedObject, error) { if seen[key] { return nil, nil } @@ -343,21 +349,8 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key objSearchKey, // is in another package, but this should be good enough to find all // uses. - pos := obj.Pos() - var uri span.URI - offset := -1 - for _, pgf := range pkg.CompiledGoFiles() { - if pgf.Tok.Base() <= int(pos) && int(pos) <= pgf.Tok.Base()+pgf.Tok.Size() { - var err error - offset, err = safetoken.Offset(pgf.Tok, pos) - if err != nil { - return nil, err - } - uri = pgf.URI - } - } - if offset >= 0 { - otherObjs, err := qualifiedObjsAtLocation(ctx, s, objSearchKey{uri, offset}, seen) + if key, found := packagePositionKey(pkg, obj.Pos()); found { + otherObjs, err := qualifiedObjsAtLocation(ctx, s, key, seen) if err != nil { return nil, err } @@ -380,6 +373,19 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key objSearchKey, return qualifiedObjs, nil } +// packagePositionKey finds the positionKey for the given pos. +// +// The second result reports whether the position was found. +func packagePositionKey(pkg Package, pos token.Pos) (positionKey, bool) { + for _, pgf := range pkg.CompiledGoFiles() { + offset, err := safetoken.Offset(pgf.Tok, pos) + if err == nil { + return positionKey{pgf.URI, offset}, true + } + } + return positionKey{}, false +} + // pathEnclosingObjNode returns the AST path to the object-defining // node associated with pos. "Object-defining" means either an // *ast.Ident mapped directly to a types.Object or an ast.Node mapped diff --git a/internal/lsp/source/references.go b/internal/lsp/source/references.go index a3d32a6d717..a1643fbec6c 100644 --- a/internal/lsp/source/references.go +++ b/internal/lsp/source/references.go @@ -16,6 +16,7 @@ import ( "strconv" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/safetoken" "golang.org/x/tools/internal/span" @@ -127,7 +128,7 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, includeDeclaration, includeInterfaceRefs, includeEmbeddedRefs bool) ([]*ReferenceInfo, error) { var ( references []*ReferenceInfo - seen = make(map[token.Pos]bool) + seen = make(map[positionKey]bool) ) pos := qos[0].obj.Pos() @@ -189,10 +190,15 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i continue } } - if seen[ident.Pos()] { + key, found := packagePositionKey(pkg, ident.Pos()) + if !found { + bug.Reportf("ident %v (pos: %v) not found in package %v", ident.Name, ident.Pos(), pkg.Name()) continue } - seen[ident.Pos()] = true + if seen[key] { + continue + } + seen[key] = true rng, err := posToMappedRange(snapshot, pkg, ident.Pos(), ident.End()) if err != nil { return nil, err From a79ee0f0f02a90ce00bfd721fae9d2d154cb7568 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 12 Jul 2022 01:24:52 +0000 Subject: [PATCH 092/723] Revert "Revert "internal/lsp/cache: don't pin a snapshot to view.importsState" This reverts CL 416879, which itself was a revert of CL 416874. Reason for revert: failure is understood now, as described in https://github.com/golang/go/issues/53796#issuecomment-1181157704 and fixed in CL 416881. Change-Id: I1d6a4ae46fbb1bf78e2f23656de7885b439f43fb Reviewed-on: https://go-review.googlesource.com/c/tools/+/416882 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/cache/imports.go | 44 ++++++++++++++++++++++++---------- internal/lsp/cache/snapshot.go | 40 ++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go index 710a1f3407a..7877c4f074d 100644 --- a/internal/lsp/cache/imports.go +++ b/internal/lsp/cache/imports.go @@ -7,6 +7,7 @@ package cache import ( "context" "fmt" + "os" "reflect" "strings" "sync" @@ -141,21 +142,20 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho pe.Logf = nil } - // Take an extra reference to the snapshot so that its workspace directory - // (if any) isn't destroyed while we're using it. - release := snapshot.Acquire() + // Extract invocation details from the snapshot to use with goimports. + // + // TODO(rfindley): refactor to extract the necessary invocation logic into + // separate functions. Using goCommandInvocation is unnecessarily indirect, + // and has led to memory leaks in the past, when the snapshot was + // unintentionally held past its lifetime. _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ WorkingDir: snapshot.view.rootURI.Filename(), }) if err != nil { - release() return nil, err } - pe.WorkingDir = inv.WorkingDir + pe.BuildFlags = inv.BuildFlags - pe.WorkingDir = inv.WorkingDir - pe.ModFile = inv.ModFile - pe.ModFlag = inv.ModFlag pe.Env = map[string]string{} for _, kv := range inv.Env { split := strings.SplitN(kv, "=", 2) @@ -164,11 +164,31 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho } pe.Env[split[0]] = split[1] } + // We don't actually use the invocation, so clean it up now. + cleanupInvocation() + + // If the snapshot uses a synthetic workspace directory, create a copy for + // the lifecycle of the importsState. + // + // Notably, we cannot use the snapshot invocation working directory, as that + // is tied to the lifecycle of the snapshot. + // + // Otherwise return a no-op cleanup function. + cleanup = func() {} + if snapshot.usesWorkspaceDir() { + tmpDir, err := makeWorkspaceDir(ctx, snapshot.workspace, snapshot) + if err != nil { + return nil, err + } + pe.WorkingDir = tmpDir + cleanup = func() { + os.RemoveAll(tmpDir) // ignore error + } + } else { + pe.WorkingDir = snapshot.view.rootURI.Filename() + } - return func() { - cleanupInvocation() - release() - }, nil + return cleanup, nil } func (s *importsState) refreshProcessEnv() { diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index a5cb74355e7..c2438cdf208 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -398,6 +398,11 @@ func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd stri return true, modBytes, sumBytes, nil } +// goCommandInvocation populates inv with configuration for running go commands on the snapshot. +// +// TODO(rfindley): refactor this function to compose the required configuration +// explicitly, rather than implicitly deriving it from flags and inv. +// // TODO(adonovan): simplify cleanup mechanism. It's hard to see, but // it used only after call to tempModFile. Clarify that it is only // non-nil on success. @@ -429,7 +434,6 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat // - the working directory. // - the -mod flag // - the -modfile flag - // - the -workfile flag // // These are dependent on a number of factors: whether we need to run in a // synthetic workspace, whether flags are supported at the current go @@ -480,6 +484,9 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat } } + // TODO(rfindley): in the case of go.work mode, modURI is empty and we fall + // back on the default behavior of vendorEnabled with an empty modURI. Figure + // out what is correct here and implement it explicitly. vendorEnabled, err := s.vendorEnabled(ctx, modURI, modContent) if err != nil { return "", nil, cleanup, err @@ -515,13 +522,15 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return "", nil, cleanup, source.ErrTmpModfileUnsupported } - // We should use -workfile if: - // 1. We're not actively trying to mutate a modfile. - // 2. We have an active go.work file. - // 3. We're using at least Go 1.18. + // We should use -modfile if: + // - the workspace mode supports it + // - we're using a go.work file on go1.18+, or we need a temp mod file (for + // example, if running go mod tidy in a go.work workspace) + // + // TODO(rfindley): this is very hard to follow. Refactor. useWorkFile := !needTempMod && s.workspace.moduleSource == goWorkWorkspace && s.view.goversion >= 18 if useWorkFile { - // TODO(#51215): build a temp workfile and set GOWORK in the environment. + // Since we're running in the workspace root, the go command will resolve GOWORK automatically. } else if useTempMod { if modURI == "" { return "", nil, cleanup, fmt.Errorf("no go.mod file found in %s", inv.WorkingDir) @@ -542,6 +551,25 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return tmpURI, inv, cleanup, nil } +// usesWorkspaceDir reports whether the snapshot should use a synthetic +// workspace directory for running workspace go commands such as go list. +// +// TODO(rfindley): this logic is duplicated with goCommandInvocation. Clean up +// the latter, and deduplicate. +func (s *snapshot) usesWorkspaceDir() bool { + switch s.workspace.moduleSource { + case legacyWorkspace: + return false + case goWorkWorkspace: + if s.view.goversion >= 18 { + return false + } + // Before go 1.18, the Go command did not natively support go.work files, + // so we 'fake' them with a workspace module. + } + return true +} + func (s *snapshot) buildOverlay() map[string][]byte { s.mu.Lock() defer s.mu.Unlock() From a5adb0f2c2ab868ee0a7594bfd8432fc55545285 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 11 Jul 2022 16:55:34 -0400 Subject: [PATCH 093/723] internal/lsp/cache: use mod=readonly for process env funcs CL 416874 changed the logic of populateProcessEnv and incorrectly removed a write of ProcessEnv.ModFlag. We should explicitly set -mod=readonly. Change-Id: Ibacf3d4b4c0c978d65fde345741945d6136db159 Reviewed-on: https://go-review.googlesource.com/c/tools/+/416877 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- internal/lsp/cache/imports.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go index 7877c4f074d..a08953db646 100644 --- a/internal/lsp/cache/imports.go +++ b/internal/lsp/cache/imports.go @@ -156,6 +156,7 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho } pe.BuildFlags = inv.BuildFlags + pe.ModFlag = "readonly" // processEnv operations should not mutate the modfile pe.Env = map[string]string{} for _, kv := range inv.Env { split := strings.SplitN(kv, "=", 2) From 3db2cdc06058a734d8038db36aa889f80a7d8c5a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 8 Jul 2022 15:06:14 -0400 Subject: [PATCH 094/723] internal/lsp: wait for ongoing work to complete during server shutdown Add a new WaitGroup to the View that allows waiting for all snapshot destroy operations to complete. This helps ensure that the server properly cleans up resources when shutting down, and lets us remove work-arounds in the gopls regtests intended to avoid races during shutdown. Also: - re-enable postfix completion tests that had to be disabled due to being too racy - rename the inlayHints regtest package to follow lower-cased naming conventions - add several TODOs Fixes golang/go#50707 Fixes golang/go#53735 Change-Id: If216763fb7a32f487f6116459e3dc45f4c903b8a Reviewed-on: https://go-review.googlesource.com/c/tools/+/416594 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- .../completion/postfix_snippet_test.go | 6 +-- .../inlayhints_test.go} | 3 +- gopls/internal/vulncheck/command_test.go | 9 ++++- internal/lsp/cache/session.go | 13 ------- internal/lsp/cache/snapshot.go | 16 +++++++- internal/lsp/cache/view.go | 26 +++++++++++-- internal/lsp/general.go | 2 + internal/lsp/regtest/runner.go | 39 ++++++++----------- 8 files changed, 66 insertions(+), 48 deletions(-) rename gopls/internal/regtest/{inlayHints/inlayHints_test.go => inlayhints/inlayhints_test.go} (98%) diff --git a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go index 7e595aaad1e..5a7ffb80d26 100644 --- a/gopls/internal/regtest/completion/postfix_snippet_test.go +++ b/gopls/internal/regtest/completion/postfix_snippet_test.go @@ -13,8 +13,6 @@ import ( ) func TestPostfixSnippetCompletion(t *testing.T) { - t.Skipf("skipping test due to suspected synchronization bug; see https://go.dev/issue/50707") - const mod = ` -- go.mod -- module mod.com @@ -400,7 +398,7 @@ func _() { before: ` package foo -func foo() []string { +func foo() []string { x := "test" return x.split }`, @@ -409,7 +407,7 @@ package foo import "strings" -func foo() []string { +func foo() []string { x := "test" return strings.Split(x, "$0") }`, diff --git a/gopls/internal/regtest/inlayHints/inlayHints_test.go b/gopls/internal/regtest/inlayhints/inlayhints_test.go similarity index 98% rename from gopls/internal/regtest/inlayHints/inlayHints_test.go rename to gopls/internal/regtest/inlayhints/inlayhints_test.go index 67931fbdc83..a7cbe65731d 100644 --- a/gopls/internal/regtest/inlayHints/inlayHints_test.go +++ b/gopls/internal/regtest/inlayhints/inlayhints_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package inlayHint +package inlayhint import ( "testing" @@ -17,6 +17,7 @@ func TestMain(m *testing.M) { bug.PanicOnBugs = true Main(m, hooks.Options) } + func TestEnablingInlayHints(t *testing.T) { testenv.NeedsGo1Point(t, 14) // Test fails on 1.13. const workspace = ` diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index f6e2d1b7612..e7bf7085f88 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -309,8 +309,13 @@ func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Co if err != nil { t.Fatal(err) } - defer release() - defer view.Shutdown(ctx) + + defer func() { + // The snapshot must be released before calling view.Shutdown, to avoid a + // deadlock. + release() + view.Shutdown(ctx) + }() test(ctx, snapshot) } diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 8c90c77ea8d..9c1505850c2 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -301,19 +301,6 @@ func (s *Session) viewOf(uri span.URI) (*View, error) { return s.viewMap[uri], nil } -func (s *Session) viewsOf(uri span.URI) []*View { - s.viewMu.RLock() - defer s.viewMu.RUnlock() - - var views []*View - for _, view := range s.views { - if source.InDir(view.folder.Filename(), uri.Filename()) { - views = append(views, view) - } - } - return views -} - func (s *Session) Views() []source.View { s.viewMu.RLock() defer s.viewMu.RUnlock() diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index c2438cdf208..a9dd1dfb935 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -187,11 +187,25 @@ type actionKey struct { // destroy waits for all leases on the snapshot to expire then releases // any resources (reference counts and files) associated with it. +// Snapshots being destroyed can be awaited using v.destroyWG. // // TODO(adonovan): move this logic into the release function returned // by Acquire when the reference count becomes zero. (This would cost // us the destroyedBy debug info, unless we add it to the signature of // memoize.RefCounted.Acquire.) +// +// The destroyedBy argument is used for debugging. +// +// v.snapshotMu must be held while calling this function, in order to preserve +// the invariants described by the the docstring for v.snapshot. +func (v *View) destroy(s *snapshot, destroyedBy string) { + v.snapshotWG.Add(1) + go func() { + defer v.snapshotWG.Done() + s.destroy(destroyedBy) + }() +} + func (s *snapshot) destroy(destroyedBy string) { // Wait for all leases to end before commencing destruction. s.refcount.Wait() @@ -1678,7 +1692,7 @@ func (s *snapshot) orphanedFiles() []source.VersionedFileHandle { } // If the URI doesn't belong to this view, then it's not in a workspace // package and should not be reloaded directly. - if !contains(s.view.session.viewsOf(uri), s.view) { + if !source.InDir(s.view.folder.Filename(), uri.Filename()) { return } // If the file is not open and is in a vendor directory, don't treat it diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 8ed9ecd0d16..0991797b8e4 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -67,9 +67,18 @@ type View struct { // attempt at initialization. initCancelFirstAttempt context.CancelFunc + // Track the latest snapshot via the snapshot field, guarded by snapshotMu. + // + // Invariant: whenever the snapshot field is overwritten, destroy(snapshot) + // is called on the previous (overwritten) snapshot while snapshotMu is held, + // incrementing snapshotWG. During shutdown the final snapshot is + // overwritten with nil and destroyed, guaranteeing that all observed + // snapshots have been destroyed via the destroy method, and snapshotWG may + // be waited upon to let these destroy operations complete. snapshotMu sync.Mutex - snapshot *snapshot // nil after shutdown has been called - releaseSnapshot func() // called when snapshot is no longer needed + snapshot *snapshot // latest snapshot + releaseSnapshot func() // called when snapshot is no longer needed + snapshotWG sync.WaitGroup // refcount for pending destroy operations // initialWorkspaceLoad is closed when the first workspace initialization has // completed. If we failed to load, we only retry if the go.mod file changes, @@ -125,6 +134,11 @@ type environmentVariables struct { gocache, gopath, goroot, goprivate, gomodcache, go111module string } +// workspaceMode holds various flags defining how the gopls workspace should +// behave. They may be derived from the environment, user configuration, or +// depend on the Go version. +// +// TODO(rfindley): remove workspace mode, in favor of explicit checks. type workspaceMode int const ( @@ -521,6 +535,9 @@ func (v *View) Shutdown(ctx context.Context) { v.session.removeView(ctx, v) } +// shutdown releases resources associated with the view, and waits for ongoing +// work to complete. +// // TODO(rFindley): probably some of this should also be one in View.Shutdown // above? func (v *View) shutdown(ctx context.Context) { @@ -530,13 +547,14 @@ func (v *View) shutdown(ctx context.Context) { v.snapshotMu.Lock() if v.snapshot != nil { v.releaseSnapshot() - go v.snapshot.destroy("View.shutdown") + v.destroy(v.snapshot, "View.shutdown") v.snapshot = nil v.releaseSnapshot = nil } v.snapshotMu.Unlock() v.importsState.destroy() + v.snapshotWG.Wait() } func (v *View) Session() *Session { @@ -732,7 +750,7 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]*file v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata) prevReleaseSnapshot() - go prevSnapshot.destroy("View.invalidateContent") + v.destroy(prevSnapshot, "View.invalidateContent") // Return a second lease to the caller. return v.snapshot, v.snapshot.Acquire() diff --git a/internal/lsp/general.go b/internal/lsp/general.go index fbb9692ea82..8ea4d7f5fa3 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -487,6 +487,8 @@ func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI return snapshot, fh, true, release, nil } +// shutdown implements the 'shutdown' LSP handler. It releases resources +// associated with the server and waits for all ongoing work to complete. func (s *Server) shutdown(ctx context.Context) error { s.stateMu.Lock() defer s.stateMu.Unlock() diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go index bebec53c527..5726f88ea0c 100644 --- a/internal/lsp/regtest/runner.go +++ b/internal/lsp/regtest/runner.go @@ -66,9 +66,6 @@ type Runner struct { mu sync.Mutex ts *servertest.TCPServer socketDir string - // closers is a queue of clean-up functions to run at the end of the entire - // test suite. - closers []io.Closer } type runConfig struct { @@ -228,6 +225,8 @@ type TestFunc func(t *testing.T, env *Env) // modes. For each a test run, a new workspace is created containing the // un-txtared files specified by filedata. func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) { + // TODO(rfindley): this function has gotten overly complicated, and warrants + // refactoring. t.Helper() checkBuilder(t) @@ -259,6 +258,10 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio } t.Run(tc.name, func(t *testing.T) { + // TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak + // goroutines in this test function. + // stacktest.NoLeak(t) + ctx := context.Background() if r.Timeout != 0 && !config.noDefaultTimeout { var cancel context.CancelFunc @@ -282,6 +285,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio if err := os.MkdirAll(rootDir, 0755); err != nil { t.Fatal(err) } + files := fake.UnpackTxt(files) if config.editor.WindowsLineEndings { for name, data := range files { @@ -294,13 +298,14 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio if err != nil { t.Fatal(err) } - // Deferring the closure of ws until the end of the entire test suite - // has, in testing, given the LSP server time to properly shutdown and - // release any file locks held in workspace, which is a problem on - // Windows. This may still be flaky however, and in the future we need a - // better solution to ensure that all Go processes started by gopls have - // exited before we clean up. - r.AddCloser(sandbox) + defer func() { + if !r.SkipCleanup { + if err := sandbox.Close(); err != nil { + pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) + t.Errorf("closing the sandbox: %v", err) + } + } + }() ss := tc.getServer(t, config.optionsHook) framer := jsonrpc2.NewRawStream ls := &loggingFramer{} @@ -322,6 +327,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio closeCtx, cancel := context.WithTimeout(xcontext.Detach(ctx), 5*time.Second) defer cancel() if err := env.Editor.Close(closeCtx); err != nil { + pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) t.Errorf("closing editor: %v", err) } }() @@ -493,14 +499,6 @@ func (r *Runner) getRemoteSocket(t *testing.T) string { return socket } -// AddCloser schedules a closer to be closed at the end of the test run. This -// is useful for Windows in particular, as -func (r *Runner) AddCloser(closer io.Closer) { - r.mu.Lock() - defer r.mu.Unlock() - r.closers = append(r.closers, closer) -} - // Close cleans up resource that have been allocated to this workspace. func (r *Runner) Close() error { r.mu.Lock() @@ -518,11 +516,6 @@ func (r *Runner) Close() error { } } if !r.SkipCleanup { - for _, closer := range r.closers { - if err := closer.Close(); err != nil { - errmsgs = append(errmsgs, err.Error()) - } - } if err := os.RemoveAll(r.TempDir); err != nil { errmsgs = append(errmsgs, err.Error()) } From 6e6f3131ec43d80282f625583006adee3f335b78 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 11 Jul 2022 14:01:19 -0400 Subject: [PATCH 095/723] internal/lsp/regtest: simplify, consolidate, and document settings Configuration of LSP settings within the regression test runner had become a bit of a grab-bag: some were configured via explicit fields on EditorConfig, some via the catch-all EditorConfig.Settings field, and others via custom RunOption implementations. Consolidate these fields as follows: - Add an EnvVars and Settings field, for configuring environment and LSP settings. - Eliminate the EditorConfig RunOption wrapper. RunOptions help build the config. - Remove RunOptions that just wrap a key-value settings pair. By definition settings are user-facing and cannot change without breaking compatibility. Therefore, our tests can and should set the exact string keys they are using. - Eliminate the unused SendPID option. Also clean up some logic to change configuration. For golang/go#39384 Change-Id: Id5d1614f139550cbc62db2bab1d1e1f545ad9393 Reviewed-on: https://go-review.googlesource.com/c/tools/+/416876 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/regtest/bench/bench_test.go | 15 +- .../regtest/codelens/codelens_test.go | 11 +- .../regtest/completion/completion_test.go | 2 +- gopls/internal/regtest/debug/debug_test.go | 6 +- .../regtest/diagnostics/diagnostics_test.go | 70 +++----- .../regtest/inlayhints/inlayhints_test.go | 6 +- .../regtest/misc/configuration_test.go | 14 +- .../internal/regtest/misc/definition_test.go | 4 +- .../internal/regtest/misc/formatting_test.go | 6 +- gopls/internal/regtest/misc/imports_test.go | 3 +- gopls/internal/regtest/misc/link_test.go | 4 +- .../regtest/misc/semantictokens_test.go | 4 +- gopls/internal/regtest/misc/settings_test.go | 6 +- gopls/internal/regtest/misc/shared_test.go | 2 +- .../internal/regtest/misc/staticcheck_test.go | 8 +- .../regtest/misc/workspace_symbol_test.go | 8 +- .../internal/regtest/modfile/modfile_test.go | 10 +- .../regtest/template/template_test.go | 30 ++-- gopls/internal/regtest/watch/watch_test.go | 20 +-- .../regtest/workspace/workspace_test.go | 28 ++- internal/lsp/fake/client.go | 2 +- internal/lsp/fake/editor.go | 166 ++++++++---------- internal/lsp/regtest/env.go | 6 +- internal/lsp/regtest/runner.go | 43 +++-- internal/lsp/regtest/wrappers.go | 28 +-- 25 files changed, 201 insertions(+), 301 deletions(-) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index 22f157f4719..dfe41f65b1d 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -93,14 +93,14 @@ func TestBenchmarkSymbols(t *testing.T) { } opts := benchmarkOptions(symbolOptions.workdir) - conf := EditorConfig{} + settings := make(Settings) if symbolOptions.matcher != "" { - conf.SymbolMatcher = &symbolOptions.matcher + settings["symbolMatcher"] = symbolOptions.matcher } if symbolOptions.style != "" { - conf.SymbolStyle = &symbolOptions.style + settings["symbolStyle"] = symbolOptions.style } - opts = append(opts, conf) + opts = append(opts, settings) WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) { // We can't Await in this test, since we have disabled hooks. Instead, run @@ -200,9 +200,10 @@ func TestBenchmarkDidChange(t *testing.T) { // Always run it in isolation since it measures global heap usage. // // Kubernetes example: -// $ go test -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes -// TotalAlloc: 5766 MB -// HeapAlloc: 1984 MB +// +// $ go test -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes +// TotalAlloc: 5766 MB +// HeapAlloc: 1984 MB // // Both figures exhibit variance of less than 1%. func TestPrintMemStats(t *testing.T) { diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index a64f9c480ae..12a75754152 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -63,9 +63,7 @@ const ( for _, test := range tests { t.Run(test.label, func(t *testing.T) { WithOptions( - EditorConfig{ - CodeLenses: test.enabled, - }, + Settings{"codelenses": test.enabled}, ).Run(t, workspace, func(t *testing.T, env *Env) { env.OpenFile("lib.go") lens := env.CodeLens("lib.go") @@ -308,10 +306,11 @@ func main() { } ` WithOptions( - EditorConfig{ - CodeLenses: map[string]bool{ + Settings{ + "codelenses": map[string]bool{ "gc_details": true, - }}, + }, + }, ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.ExecuteCodeLensCommand("main.go", command.GCDetails) diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 1ffb0000d3b..51a54c49737 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -529,7 +529,7 @@ func main() { } ` WithOptions( - EditorConfig{WindowsLineEndings: true}, + WindowsLineEndings(), ).Run(t, src, func(t *testing.T, env *Env) { // Trigger unimported completions for the example.com/blah package. env.OpenFile("main.go") diff --git a/gopls/internal/regtest/debug/debug_test.go b/gopls/internal/regtest/debug/debug_test.go index d60b3f780d7..d01d44ed980 100644 --- a/gopls/internal/regtest/debug/debug_test.go +++ b/gopls/internal/regtest/debug/debug_test.go @@ -21,11 +21,7 @@ func TestBugNotification(t *testing.T) { // server. WithOptions( Modes(Singleton), // must be in-process to receive the bug report below - EditorConfig{ - Settings: map[string]interface{}{ - "showBugReports": true, - }, - }, + Settings{"showBugReports": true}, ).Run(t, "", func(t *testing.T, env *Env) { const desc = "got a bug" bug.Report(desc, nil) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 6f5db4cd419..b9dc2d434b2 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -471,12 +471,11 @@ func _() { } ` WithOptions( - EditorConfig{ - Env: map[string]string{ - "GOPATH": "", - "GO111MODULE": "off", - }, - }).Run(t, files, func(t *testing.T, env *Env) { + EnvVars{ + "GOPATH": "", + "GO111MODULE": "off", + }, + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.Await(env.DiagnosticAtRegexp("main.go", "fmt")) env.SaveBuffer("main.go") @@ -500,8 +499,9 @@ package x var X = 0 ` - editorConfig := EditorConfig{Env: map[string]string{"GOFLAGS": "-tags=foo"}} - WithOptions(editorConfig).Run(t, files, func(t *testing.T, env *Env) { + WithOptions( + EnvVars{"GOFLAGS": "-tags=foo"}, + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OrganizeImports("main.go") env.Await(EmptyDiagnostics("main.go")) @@ -573,9 +573,9 @@ hi mom ` for _, go111module := range []string{"on", "off", ""} { t.Run(fmt.Sprintf("GO111MODULE_%v", go111module), func(t *testing.T) { - WithOptions(EditorConfig{ - Env: map[string]string{"GO111MODULE": go111module}, - }).Run(t, files, func(t *testing.T, env *Env) { + WithOptions( + EnvVars{"GO111MODULE": go111module}, + ).Run(t, files, func(t *testing.T, env *Env) { env.Await( NoOutstandingWork(), ) @@ -605,11 +605,7 @@ func main() { ` WithOptions( InGOPATH(), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "off", - }, - }, + EnvVars{"GO111MODULE": "off"}, ).Run(t, collision, func(t *testing.T, env *Env) { env.OpenFile("x/x.go") env.Await( @@ -1236,7 +1232,7 @@ func main() { }) WithOptions( WorkspaceFolders("a"), - LimitWorkspaceScope(), + Settings{"expandWorkspaceToModule": false}, ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.Await( @@ -1267,11 +1263,7 @@ func main() { ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "staticcheck": true, - }, - }, + Settings{"staticcheck": true}, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams @@ -1381,9 +1373,7 @@ func b(c bytes.Buffer) { } ` WithOptions( - EditorConfig{ - AllExperiments: true, - }, + Settings{"allExperiments": true}, ).Run(t, mod, func(t *testing.T, env *Env) { // Confirm that the setting doesn't cause any warnings. env.Await(NoShowMessage()) @@ -1495,11 +1485,7 @@ package foo_ WithOptions( ProxyFiles(proxy), InGOPATH(), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "off", - }, - }, + EnvVars{"GO111MODULE": "off"}, ).Run(t, contents, func(t *testing.T, env *Env) { // Simulate typing character by character. env.OpenFile("foo/foo_test.go") @@ -1698,9 +1684,7 @@ import ( t.Run("GOPATH", func(t *testing.T) { WithOptions( InGOPATH(), - EditorConfig{ - Env: map[string]string{"GO111MODULE": "off"}, - }, + EnvVars{"GO111MODULE": "off"}, Modes(Singleton), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( @@ -1729,11 +1713,7 @@ package b t.Run("GO111MODULE="+go111module, func(t *testing.T) { WithOptions( Modes(Singleton), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": go111module, - }, - }, + EnvVars{"GO111MODULE": go111module}, ).Run(t, modules, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("b/go.mod") @@ -1750,11 +1730,7 @@ package b t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) { WithOptions( Modes(Singleton), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "auto", - }, - }, + EnvVars{"GO111MODULE": "auto"}, InGOPATH(), ).Run(t, modules, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") @@ -2026,9 +2002,7 @@ package a func Hello() {} ` WithOptions( - EditorConfig{ - ExperimentalUseInvalidMetadata: true, - }, + Settings{"experimentalUseInvalidMetadata": true}, Modes(Singleton), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") @@ -2082,9 +2056,7 @@ package main func _() {} ` WithOptions( - EditorConfig{ - ExperimentalUseInvalidMetadata: true, - }, + Settings{"experimentalUseInvalidMetadata": true}, // ExperimentalWorkspaceModule has a different failure mode for this // case. Modes(Singleton), diff --git a/gopls/internal/regtest/inlayhints/inlayhints_test.go b/gopls/internal/regtest/inlayhints/inlayhints_test.go index a7cbe65731d..1ca1dfbc09e 100644 --- a/gopls/internal/regtest/inlayhints/inlayhints_test.go +++ b/gopls/internal/regtest/inlayhints/inlayhints_test.go @@ -56,10 +56,8 @@ const ( for _, test := range tests { t.Run(test.label, func(t *testing.T) { WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "hints": test.enabled, - }, + Settings{ + "hints": test.enabled, }, ).Run(t, workspace, func(t *testing.T, env *Env) { env.OpenFile("lib.go") diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index d9cce96a43e..9629a2382ee 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -9,7 +9,6 @@ import ( . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/testenv" ) @@ -40,12 +39,11 @@ var FooErr = errors.New("foo") env.DoneWithOpen(), NoDiagnostics("a/a.go"), ) - cfg := &fake.EditorConfig{} - *cfg = env.Editor.Config + cfg := env.Editor.Config() cfg.Settings = map[string]interface{}{ "staticcheck": true, } - env.ChangeConfiguration(t, cfg) + env.ChangeConfiguration(cfg) env.Await( DiagnosticAt("a/a.go", 5, 4), ) @@ -70,11 +68,9 @@ import "errors" var FooErr = errors.New("foo") ` - WithOptions(EditorConfig{ - Settings: map[string]interface{}{ - "staticcheck": true, - }, - }).Run(t, files, func(t *testing.T, env *Env) { + WithOptions( + Settings{"staticcheck": true}, + ).Run(t, files, func(t *testing.T, env *Env) { env.Await(ShownMessage("staticcheck is not supported")) }) } diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index 2f5a54820d0..b71cf231079 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -162,9 +162,7 @@ func main() {} } { t.Run(tt.importShortcut, func(t *testing.T) { WithOptions( - EditorConfig{ - ImportShortcut: tt.importShortcut, - }, + Settings{"importShortcut": tt.importShortcut}, ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"fmt"`)) diff --git a/gopls/internal/regtest/misc/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go index 75d8f622458..71b8cadab40 100644 --- a/gopls/internal/regtest/misc/formatting_test.go +++ b/gopls/internal/regtest/misc/formatting_test.go @@ -352,10 +352,8 @@ const Bar = 42 ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "gofumpt": true, - }, + Settings{ + "gofumpt": true, }, ).Run(t, input, func(t *testing.T, env *Env) { env.OpenFile("foo.go") diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index 1250e78e776..c0e213e9aec 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -153,9 +153,8 @@ var _, _ = x.X, y.Y t.Fatal(err) } defer os.RemoveAll(modcache) - editorConfig := EditorConfig{Env: map[string]string{"GOMODCACHE": modcache}} WithOptions( - editorConfig, + EnvVars{"GOMODCACHE": modcache}, ProxyFiles(proxy), ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") diff --git a/gopls/internal/regtest/misc/link_test.go b/gopls/internal/regtest/misc/link_test.go index e84f6377eeb..1005de9a24f 100644 --- a/gopls/internal/regtest/misc/link_test.go +++ b/gopls/internal/regtest/misc/link_test.go @@ -75,7 +75,9 @@ const Hello = "Hello" } // Then change the environment to make these links private. - env.ChangeEnv(map[string]string{"GOPRIVATE": "import.test"}) + cfg := env.Editor.Config() + cfg.Env = map[string]string{"GOPRIVATE": "import.test"} + env.ChangeConfiguration(cfg) // Finally, verify that the links are gone. content, _ = env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello")) diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index 79507876a64..dca2b8e7514 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -26,9 +26,7 @@ func main() {} ` WithOptions( Modes(Singleton), - EditorConfig{ - AllExperiments: true, - }, + Settings{"allExperiments": true}, ).Run(t, src, func(t *testing.T, env *Env) { params := &protocol.SemanticTokensParams{} const badURI = "http://foo" diff --git a/gopls/internal/regtest/misc/settings_test.go b/gopls/internal/regtest/misc/settings_test.go index 7704c3c043e..62d3d903160 100644 --- a/gopls/internal/regtest/misc/settings_test.go +++ b/gopls/internal/regtest/misc/settings_test.go @@ -24,11 +24,7 @@ func main() { ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "directoryFilters": []string{""}, - }, - }, + Settings{"directoryFilters": []string{""}}, ).Run(t, src, func(t *testing.T, env *Env) { // No need to do anything. Issue golang/go#51843 is triggered by the empty // directory filter above. diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index a6b0cd87ef1..6b5acd02f71 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -30,7 +30,7 @@ func runShared(t *testing.T, testFunc func(env1 *Env, env2 *Env)) { WithOptions(Modes(modes)).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { // Create a second test session connected to the same workspace and server // as the first. - env2, cleanup := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config, true) + env2, cleanup := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config(), true) defer cleanup() env2.Await(InitialWorkspaceLoad) testFunc(env1, env2) diff --git a/gopls/internal/regtest/misc/staticcheck_test.go b/gopls/internal/regtest/misc/staticcheck_test.go index 94bb39903a5..6f1bda35068 100644 --- a/gopls/internal/regtest/misc/staticcheck_test.go +++ b/gopls/internal/regtest/misc/staticcheck_test.go @@ -60,11 +60,9 @@ func testGenerics[P *T, T any](p P) { var FooErr error = errors.New("foo") ` - WithOptions(EditorConfig{ - Settings: map[string]interface{}{ - "staticcheck": true, - }, - }).Run(t, files, func(t *testing.T, env *Env) { + WithOptions( + Settings{"staticcheck": true}, + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.Await( env.DiagnosticAtRegexpFromSource("a/a.go", "sort.Slice", "sortslice"), diff --git a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go index a21d47312dd..2dc3a1b10d0 100644 --- a/gopls/internal/regtest/misc/workspace_symbol_test.go +++ b/gopls/internal/regtest/misc/workspace_symbol_test.go @@ -72,9 +72,7 @@ const ( var symbolMatcher = string(source.SymbolFastFuzzy) WithOptions( - EditorConfig{ - SymbolMatcher: &symbolMatcher, - }, + Settings{"symbolMatcher": symbolMatcher}, ).Run(t, files, func(t *testing.T, env *Env) { want := []string{ "Foo", // prefer exact segment matches first @@ -105,9 +103,7 @@ const ( var symbolMatcher = string(source.SymbolFastFuzzy) WithOptions( - EditorConfig{ - SymbolMatcher: &symbolMatcher, - }, + Settings{"symbolMatcher": symbolMatcher}, ).Run(t, files, func(t *testing.T, env *Env) { compareSymbols(t, env.WorkspaceSymbol("ABC"), []string{"ABC", "AxxBxxCxx"}) compareSymbols(t, env.WorkspaceSymbol("'ABC"), []string{"ABC"}) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 93d43253044..c0bef833f44 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -740,11 +740,7 @@ func main() { } ` WithOptions( - EditorConfig{ - Env: map[string]string{ - "GOFLAGS": "-mod=readonly", - }, - }, + EnvVars{"GOFLAGS": "-mod=readonly"}, ProxyFiles(proxy), Modes(Singleton), ).Run(t, mod, func(t *testing.T, env *Env) { @@ -830,9 +826,7 @@ func main() { ` WithOptions( ProxyFiles(workspaceProxy), - EditorConfig{ - BuildFlags: []string{"-tags", "bob"}, - }, + Settings{"buildFlags": []string{"-tags", "bob"}}, ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("main.go", `"example.com/blah"`), diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index 9489e9bf7fe..0fdc3bda6ff 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -35,11 +35,9 @@ go 1.17 {{end}} ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "templateExtensions": []string{"tmpl"}, - "semanticTokens": true, - }, + Settings{ + "templateExtensions": []string{"tmpl"}, + "semanticTokens": true, }, ).Run(t, files, func(t *testing.T, env *Env) { var p protocol.SemanticTokensParams @@ -66,11 +64,9 @@ Hello {{}} <-- missing body {{end}} ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "templateExtensions": []string{"tmpl"}, - "semanticTokens": true, - }, + Settings{ + "templateExtensions": []string{"tmpl"}, + "semanticTokens": true, }, ).Run(t, files, func(t *testing.T, env *Env) { // TODO: can we move this diagnostic onto {{}}? @@ -112,11 +108,9 @@ B {{}} <-- missing body ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "templateExtensions": []string{"tmpl"}, - }, - DirectoryFilters: []string{"-b"}, + Settings{ + "directoryFilters": []string{"-b"}, + "templateExtensions": []string{"tmpl"}, }, ).Run(t, files, func(t *testing.T, env *Env) { env.Await( @@ -184,10 +178,8 @@ go 1.12 ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "templateExtensions": []string{"tmpl", "gotmpl"}, - }, + Settings{ + "templateExtensions": []string{"tmpl", "gotmpl"}, }, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a.tmpl") diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index e66d08ab125..04414f6b744 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -389,9 +389,9 @@ func _() { package a ` t.Run("close then delete", func(t *testing.T) { - WithOptions(EditorConfig{ - VerboseOutput: true, - }).Run(t, pkg, func(t *testing.T, env *Env) { + WithOptions( + Settings{"verboseOutput": true}, + ).Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("a/a_unneeded.go") env.Await( @@ -424,7 +424,7 @@ package a t.Run("delete then close", func(t *testing.T) { WithOptions( - EditorConfig{VerboseOutput: true}, + Settings{"verboseOutput": true}, ).Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("a/a_unneeded.go") @@ -620,11 +620,7 @@ func main() { ` WithOptions( InGOPATH(), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "auto", - }, - }, + EnvVars{"GO111MODULE": "auto"}, Modes(Experimental), // module is in a subdirectory ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/main.go") @@ -663,11 +659,7 @@ func main() { ` WithOptions( InGOPATH(), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "auto", - }, - }, + EnvVars{"GO111MODULE": "auto"}, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/main.go") env.RemoveWorkspaceFile("foo/go.mod") diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 9e4b85fced8..7eafaf191dd 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1036,10 +1036,10 @@ package exclude const _ = Nonexistant ` - cfg := EditorConfig{ - DirectoryFilters: []string{"-exclude"}, - } - WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) { + + WithOptions( + Settings{"directoryFilters": []string{"-exclude"}}, + ).Run(t, files, func(t *testing.T, env *Env) { env.Await(NoDiagnostics("exclude/x.go")) }) } @@ -1064,10 +1064,9 @@ const _ = Nonexistant // should be ignored, since this is a non-workspace packag const X = 1 ` - cfg := EditorConfig{ - DirectoryFilters: []string{"-exclude"}, - } - WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) { + WithOptions( + Settings{"directoryFilters": []string{"-exclude"}}, + ).Run(t, files, func(t *testing.T, env *Env) { env.Await( NoDiagnostics("exclude/exclude.go"), // filtered out NoDiagnostics("include/include.go"), // successfully builds @@ -1114,10 +1113,11 @@ go 1.12 -- exclude.com@v1.0.0/exclude.go -- package exclude ` - cfg := EditorConfig{ - DirectoryFilters: []string{"-exclude"}, - } - WithOptions(cfg, Modes(Experimental), ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) { + WithOptions( + Modes(Experimental), + ProxyFiles(proxy), + Settings{"directoryFilters": []string{"-exclude"}}, + ).Run(t, files, func(t *testing.T, env *Env) { env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`)) }) } @@ -1204,9 +1204,7 @@ go 1.12 package main ` WithOptions( - EditorConfig{Env: map[string]string{ - "GOPATH": filepath.FromSlash("$SANDBOX_WORKDIR/gopath"), - }}, + EnvVars{"GOPATH": filepath.FromSlash("$SANDBOX_WORKDIR/gopath")}, Modes(Singleton), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( diff --git a/internal/lsp/fake/client.go b/internal/lsp/fake/client.go index fdc67a6cc64..4c5f2a2e1bd 100644 --- a/internal/lsp/fake/client.go +++ b/internal/lsp/fake/client.go @@ -77,7 +77,7 @@ func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration if item.Section != "gopls" { continue } - results[i] = c.editor.configuration() + results[i] = c.editor.settings() } return results, nil } diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index 0fc99a04982..240e35cffa3 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -25,7 +25,6 @@ import ( // Editor is a fake editor client. It keeps track of client state and can be // used for writing LSP tests. type Editor struct { - Config EditorConfig // Server, client, and sandbox are concurrency safe and written only // at construction time, so do not require synchronization. @@ -35,13 +34,10 @@ type Editor struct { sandbox *Sandbox defaultEnv map[string]string - // Since this editor is intended just for testing, we use very coarse - // locking. - mu sync.Mutex - // Editor state. - buffers map[string]buffer - // Capabilities / Options - serverCapabilities protocol.ServerCapabilities + mu sync.Mutex // guards config, buffers, serverCapabilities + config EditorConfig // editor configuration + buffers map[string]buffer // open buffers + serverCapabilities protocol.ServerCapabilities // capabilities / options // Call metrics for the purpose of expectations. This is done in an ad-hoc // manner for now. Perhaps in the future we should do something more @@ -77,21 +73,11 @@ func (b buffer) text() string { // // The zero value for EditorConfig should correspond to its defaults. type EditorConfig struct { - Env map[string]string - BuildFlags []string - - // CodeLenses is a map defining whether codelens are enabled, keyed by the - // codeLens command. CodeLenses which are not present in this map are left in - // their default state. - CodeLenses map[string]bool - - // SymbolMatcher is the config associated with the "symbolMatcher" gopls - // config option. - SymbolMatcher, SymbolStyle *string - - // LimitWorkspaceScope is true if the user does not want to expand their - // workspace scope to the entire module. - LimitWorkspaceScope bool + // Env holds environment variables to apply on top of the default editor + // environment. When applying these variables, the special string + // $SANDBOX_WORKDIR is replaced by the absolute path to the sandbox working + // directory. + Env map[string]string // WorkspaceFolders is the workspace folders to configure on the LSP server, // relative to the sandbox workdir. @@ -101,14 +87,6 @@ type EditorConfig struct { // To explicitly send no workspace folders, use an empty (non-nil) slice. WorkspaceFolders []string - // AllExperiments sets the "allExperiments" configuration, which enables - // all of gopls's opt-in settings. - AllExperiments bool - - // Whether to send the current process ID, for testing data that is joined to - // the PID. This can only be set by one test. - SendPID bool - // Whether to edit files with windows line endings. WindowsLineEndings bool @@ -120,14 +98,8 @@ type EditorConfig struct { // "gotmpl" -> ".*tmpl" FileAssociations map[string]string - // Settings holds arbitrary additional settings to apply to the gopls config. - // TODO(rfindley): replace existing EditorConfig fields with Settings. + // Settings holds user-provided configuration for the LSP server. Settings map[string]interface{} - - ImportShortcut string - DirectoryFilters []string - VerboseOutput bool - ExperimentalUseInvalidMetadata bool } // NewEditor Creates a new Editor. @@ -136,7 +108,7 @@ func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { buffers: make(map[string]buffer), sandbox: sandbox, defaultEnv: sandbox.GoEnv(), - Config: config, + config: config, } } @@ -155,7 +127,7 @@ func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHo protocol.Handlers( protocol.ClientHandler(e.client, jsonrpc2.MethodNotFound))) - if err := e.initialize(ctx, e.Config.WorkspaceFolders); err != nil { + if err := e.initialize(ctx, e.config.WorkspaceFolders); err != nil { return nil, err } e.sandbox.Workdir.AddWatcher(e.onFileChanges) @@ -213,65 +185,47 @@ func (e *Editor) Client() *Client { return e.client } -func (e *Editor) overlayEnv() map[string]string { +// settings builds the settings map for use in LSP settings +// RPCs. +func (e *Editor) settings() map[string]interface{} { + e.mu.Lock() + defer e.mu.Unlock() env := make(map[string]string) for k, v := range e.defaultEnv { - v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename()) env[k] = v } - for k, v := range e.Config.Env { + for k, v := range e.config.Env { + env[k] = v + } + for k, v := range env { v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename()) env[k] = v } - return env -} -func (e *Editor) configuration() map[string]interface{} { - config := map[string]interface{}{ - "verboseWorkDoneProgress": true, - "env": e.overlayEnv(), - "expandWorkspaceToModule": !e.Config.LimitWorkspaceScope, - "completionBudget": "10s", - } + settings := map[string]interface{}{ + "env": env, - for k, v := range e.Config.Settings { - config[k] = v - } + // Use verbose progress reporting so that regtests can assert on + // asynchronous operations being completed (such as diagnosing a snapshot). + "verboseWorkDoneProgress": true, - if e.Config.BuildFlags != nil { - config["buildFlags"] = e.Config.BuildFlags - } - if e.Config.DirectoryFilters != nil { - config["directoryFilters"] = e.Config.DirectoryFilters - } - if e.Config.ExperimentalUseInvalidMetadata { - config["experimentalUseInvalidMetadata"] = true - } - if e.Config.CodeLenses != nil { - config["codelenses"] = e.Config.CodeLenses - } - if e.Config.SymbolMatcher != nil { - config["symbolMatcher"] = *e.Config.SymbolMatcher - } - if e.Config.SymbolStyle != nil { - config["symbolStyle"] = *e.Config.SymbolStyle - } - if e.Config.AllExperiments { - config["allExperiments"] = true - } + // Set a generous completion budget, so that tests don't flake because + // completions are too slow. + "completionBudget": "10s", - if e.Config.VerboseOutput { - config["verboseOutput"] = true + // Shorten the diagnostic delay to speed up test execution (else we'd add + // the default delay to each assertion about diagnostics) + "diagnosticsDelay": "10ms", } - if e.Config.ImportShortcut != "" { - config["importShortcut"] = e.Config.ImportShortcut + for k, v := range e.config.Settings { + if k == "env" { + panic("must not provide env via the EditorConfig.Settings field: use the EditorConfig.Env field instead") + } + settings[k] = v } - config["diagnosticsDelay"] = "10ms" - - // ExperimentalWorkspaceModule is only set as a mode, not a configuration. - return config + return settings } func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error { @@ -293,10 +247,7 @@ func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) erro params.Capabilities.Window.WorkDoneProgress = true // TODO: set client capabilities params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated} - params.InitializationOptions = e.configuration() - if e.Config.SendPID { - params.ProcessID = int32(os.Getpid()) - } + params.InitializationOptions = e.settings() params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true @@ -397,20 +348,21 @@ func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { } func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error { + e.mu.Lock() + defer e.mu.Unlock() + buf := buffer{ - windowsLineEndings: e.Config.WindowsLineEndings, + windowsLineEndings: e.config.WindowsLineEndings, version: 1, path: path, lines: lines(content), dirty: dirty, } - e.mu.Lock() - defer e.mu.Unlock() e.buffers[path] = buf item := protocol.TextDocumentItem{ URI: e.sandbox.Workdir.URI(buf.path), - LanguageID: e.languageID(buf.path), + LanguageID: languageID(buf.path, e.config.FileAssociations), Version: int32(buf.version), Text: buf.text(), } @@ -436,9 +388,11 @@ var defaultFileAssociations = map[string]*regexp.Regexp{ "gotmpl": regexp.MustCompile(`^.*tmpl$`), } -func (e *Editor) languageID(p string) string { +// languageID returns the language identifier for the path p given the user +// configured fileAssociations. +func languageID(p string, fileAssociations map[string]string) string { base := path.Base(p) - for lang, re := range e.Config.FileAssociations { + for lang, re := range fileAssociations { re := regexp.MustCompile(re) if re.MatchString(base) { return lang @@ -1205,6 +1159,30 @@ func (e *Editor) applyProtocolEdit(ctx context.Context, change protocol.TextDocu return e.EditBuffer(ctx, path, fakeEdits) } +// Config returns the current editor configuration. +func (e *Editor) Config() EditorConfig { + e.mu.Lock() + defer e.mu.Unlock() + return e.config +} + +// ChangeConfiguration sets the new editor configuration, and if applicable +// sends a didChangeConfiguration notification. +// +// An error is returned if the change notification failed to send. +func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig) error { + e.mu.Lock() + e.config = newConfig + e.mu.Unlock() // don't hold e.mu during server calls + if e.Server != nil { + var params protocol.DidChangeConfigurationParams // empty: gopls ignores the Settings field + if err := e.Server.DidChangeConfiguration(ctx, ¶ms); err != nil { + return err + } + } + return nil +} + // CodeAction executes a codeAction request on the server. func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { if e.Server == nil { diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go index a37cbf66611..8960a0dc913 100644 --- a/internal/lsp/regtest/env.go +++ b/internal/lsp/regtest/env.go @@ -111,7 +111,11 @@ type condition struct { // NewEnv creates a new test environment using the given scratch environment // and gopls server. // -// The resulting func must be called to close the jsonrpc2 connection. +// The resulting cleanup func must be called to close the jsonrpc2 connection. +// +// TODO(rfindley): this function provides questionable value. Consider +// refactoring to move things like creating the server outside of this +// constructor. func NewEnv(ctx context.Context, tb testing.TB, sandbox *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig, withHooks bool) (_ *Env, cleanup func()) { tb.Helper() diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go index 5726f88ea0c..0640e452fb1 100644 --- a/internal/lsp/regtest/runner.go +++ b/internal/lsp/regtest/runner.go @@ -133,17 +133,27 @@ func Options(hook func(*source.Options)) RunOption { }) } -func SendPID() RunOption { +// WindowsLineEndings configures the editor to use windows line endings. +func WindowsLineEndings() RunOption { return optionSetter(func(opts *runConfig) { - opts.editor.SendPID = true + opts.editor.WindowsLineEndings = true }) } -// EditorConfig is a RunOption option that configured the regtest editor. -type EditorConfig fake.EditorConfig +// Settings is a RunOption that sets user-provided configuration for the LSP +// server. +// +// As a special case, the env setting must not be provided via Settings: use +// EnvVars instead. +type Settings map[string]interface{} -func (c EditorConfig) set(opts *runConfig) { - opts.editor = fake.EditorConfig(c) +func (s Settings) set(opts *runConfig) { + if opts.editor.Settings == nil { + opts.editor.Settings = make(map[string]interface{}) + } + for k, v := range s { + opts.editor.Settings[k] = v + } } // WorkspaceFolders configures the workdir-relative workspace folders to send @@ -160,6 +170,20 @@ func WorkspaceFolders(relFolders ...string) RunOption { }) } +// EnvVars sets environment variables for the LSP session. When applying these +// variables to the session, the special string $SANDBOX_WORKDIR is replaced by +// the absolute path to the sandbox working directory. +type EnvVars map[string]string + +func (e EnvVars) set(opts *runConfig) { + if opts.editor.Env == nil { + opts.editor.Env = make(map[string]string) + } + for k, v := range e { + opts.editor.Env[k] = v + } +} + // InGOPATH configures the workspace working directory to be GOPATH, rather // than a separate working directory for use with modules. func InGOPATH() RunOption { @@ -212,13 +236,6 @@ func GOPROXY(goproxy string) RunOption { }) } -// LimitWorkspaceScope sets the LimitWorkspaceScope configuration. -func LimitWorkspaceScope() RunOption { - return optionSetter(func(opts *runConfig) { - opts.editor.LimitWorkspaceScope = true - }) -} - type TestFunc func(t *testing.T, env *Env) // Run executes the test function in the default configured gopls execution diff --git a/internal/lsp/regtest/wrappers.go b/internal/lsp/regtest/wrappers.go index 96e2de96271..d8c080c0fec 100644 --- a/internal/lsp/regtest/wrappers.go +++ b/internal/lsp/regtest/wrappers.go @@ -7,7 +7,6 @@ package regtest import ( "encoding/json" "path" - "testing" "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/fake" @@ -427,31 +426,10 @@ func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []proto return actions } -func (e *Env) ChangeConfiguration(t *testing.T, config *fake.EditorConfig) { - e.Editor.Config = *config - if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &protocol.DidChangeConfigurationParams{ - // gopls currently ignores the Settings field - }); err != nil { - t.Fatal(err) - } -} - -// ChangeEnv modifies the editor environment and reconfigures the LSP client. -// TODO: extend this to "ChangeConfiguration", once we refactor the way editor -// configuration is defined. -func (e *Env) ChangeEnv(overlay map[string]string) { +// ChangeConfiguration updates the editor config, calling t.Fatal on any error. +func (e *Env) ChangeConfiguration(newConfig fake.EditorConfig) { e.T.Helper() - // TODO: to be correct, this should probably be synchronized, but right now - // configuration is only ever modified synchronously in a regtest, so this - // correctness can wait for the previously mentioned refactoring. - if e.Editor.Config.Env == nil { - e.Editor.Config.Env = make(map[string]string) - } - for k, v := range overlay { - e.Editor.Config.Env[k] = v - } - var params protocol.DidChangeConfigurationParams - if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, ¶ms); err != nil { + if err := e.Editor.ChangeConfiguration(e.Ctx, newConfig); err != nil { e.T.Fatal(err) } } From 7c06b01db6ec68c2ffb216019a21bf59ac40cb35 Mon Sep 17 00:00:00 2001 From: Zvonimir Pavlinovic Date: Fri, 8 Jul 2022 12:55:50 -0700 Subject: [PATCH 096/723] go/callgraph/vta: remove interprocedural flows for receiver objects Suppose a call i.Foo(j) and suppose that the initial call graph resolves this call to just a.Foo(x). Here, i is an interface and a is concrete type. VTA then creates flows between j and x. However, it also tries to create flows between i and a. The latter flows are not needed. The flow from i to a will not be registered in the type flow graph as it does not make sense. The flow from a to i would bake in the information from the initial call graph which would defy the purpose of VTA. Note that the flow a -> i doesn't occur in practice as that flow is only created when a and i can alias. Change-Id: Ia4087651c72a14b94d83d07bb5e6d77603842362 Reviewed-on: https://go-review.googlesource.com/c/tools/+/416517 TryBot-Result: Gopher Robot Run-TryBot: Zvonimir Pavlinovic gopls-CI: kokoro Reviewed-by: Tim King --- go/callgraph/vta/graph.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 365d7a5b0f7..48547a52527 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -586,14 +586,14 @@ func addArgumentFlows(b *builder, c ssa.CallInstruction, f *ssa.Function) { return } cc := c.Common() - // When c is an unresolved method call (cc.Method != nil), cc.Value contains - // the receiver object rather than cc.Args[0]. - if cc.Method != nil { - b.addInFlowAliasEdges(b.nodeFromVal(f.Params[0]), b.nodeFromVal(cc.Value)) - } offset := 0 if cc.Method != nil { + // We don't add interprocedural flows for receiver objects. + // At a call site, the receiver object is interface while the + // callee object is concrete. The flow from interface to + // concrete type does not make sense. The flow other way around + // would bake in information from the initial call graph. offset = 1 } for i, v := range cc.Args { From 459e2b88fc869374454543f70b377f13599756f7 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 12 Jul 2022 17:13:32 -0400 Subject: [PATCH 097/723] internal/lsp/progress: actually close over Context in WorkDoneWriter CL 409936 eliminated cases where we close over a Context during progress reporting, except in one instance where it wasn't possible: the WorkDoneWriter that must implement the io.Writer interface. Unfortunately it contained a glaring bug that the ctx field was never set, and the regression test for progress reporting during `go generate` was disabled due to flakiness (golang/go#49901). Incidentally, the fundamental problem that CL 409936 addressed may also fix the flakiness of TestGenerateProgress. Fix the bug, and re-enable the test. Fixes golang/go#53781 Change-Id: Ideb99a5525667e45d2e41fcc5078699ba1e0f1a3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/417115 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Auto-Submit: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/regtest/misc/generate_test.go | 2 -- internal/lsp/command.go | 4 ++-- internal/lsp/progress/progress.go | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go index 1dc22d737ba..44789514f40 100644 --- a/gopls/internal/regtest/misc/generate_test.go +++ b/gopls/internal/regtest/misc/generate_test.go @@ -16,8 +16,6 @@ import ( ) func TestGenerateProgress(t *testing.T) { - t.Skipf("skipping flaky test: https://golang.org/issue/49901") - const generatedWorkspace = ` -- go.mod -- module fake.test diff --git a/internal/lsp/command.go b/internal/lsp/command.go index cd4c7273101..c173ef23543 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -404,7 +404,7 @@ func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, // create output buf := &bytes.Buffer{} ew := progress.NewEventWriter(ctx, "test") - out := io.MultiWriter(ew, progress.NewWorkDoneWriter(work), buf) + out := io.MultiWriter(ew, progress.NewWorkDoneWriter(ctx, work), buf) // Run `go test -run Func` on each test. var failedTests int @@ -487,7 +487,7 @@ func (c *commandHandler) Generate(ctx context.Context, args command.GenerateArgs Args: []string{"-x", pattern}, WorkingDir: args.Dir.SpanURI().Filename(), } - stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(deps.work)) + stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work)) if err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil { return err } diff --git a/internal/lsp/progress/progress.go b/internal/lsp/progress/progress.go index d6794cf338b..8b0d1c6a224 100644 --- a/internal/lsp/progress/progress.go +++ b/internal/lsp/progress/progress.go @@ -260,8 +260,8 @@ type WorkDoneWriter struct { wd *WorkDone } -func NewWorkDoneWriter(wd *WorkDone) *WorkDoneWriter { - return &WorkDoneWriter{wd: wd} +func NewWorkDoneWriter(ctx context.Context, wd *WorkDone) *WorkDoneWriter { + return &WorkDoneWriter{ctx: ctx, wd: wd} } func (wdw *WorkDoneWriter) Write(p []byte) (n int, err error) { From 8730184efb0c83f831a94eb2456ac28d3511f8ae Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 12 Jul 2022 17:56:04 -0400 Subject: [PATCH 098/723] internal/lsp/fake: retry spurious file lock errors on windows Cleaning the regtest sandbox sometimes fails on windows with errors that may be spurious. Copy logic from the testing package to retry these errors. For golang/go#53819 Change-Id: I059fbb5e023af1cd52a5d231cd11a7c2ae72bc92 Reviewed-on: https://go-review.googlesource.com/c/tools/+/417117 Run-TryBot: Robert Findley Reviewed-by: Bryan Mills TryBot-Result: Gopher Robot gopls-CI: kokoro --- internal/lsp/fake/sandbox.go | 31 +++++++++++++++++++++++++++- internal/lsp/fake/workdir.go | 4 ++++ internal/lsp/fake/workdir_windows.go | 25 ++++++++++++++++++++-- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/internal/lsp/fake/sandbox.go b/internal/lsp/fake/sandbox.go index b4395646bc6..72b01217ae7 100644 --- a/internal/lsp/fake/sandbox.go +++ b/internal/lsp/fake/sandbox.go @@ -9,9 +9,11 @@ import ( "errors" "fmt" "io/ioutil" + "math/rand" "os" "path/filepath" "strings" + "time" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/testenv" @@ -266,9 +268,36 @@ func (sb *Sandbox) Close() error { if sb.gopath != "" { goCleanErr = sb.RunGoCommand(context.Background(), "", "clean", []string{"-modcache"}, false) } - err := os.RemoveAll(sb.rootdir) + err := removeAll(sb.rootdir) if err != nil || goCleanErr != nil { return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err) } return nil } + +// removeAll is copied from GOROOT/src/testing/testing.go +// +// removeAll is like os.RemoveAll, but retries Windows "Access is denied." +// errors up to an arbitrary timeout. +// +// See https://go.dev/issue/50051 for additional context. +func removeAll(path string) error { + const arbitraryTimeout = 2 * time.Second + var ( + start time.Time + nextSleep = 1 * time.Millisecond + ) + for { + err := os.RemoveAll(path) + if !isWindowsRetryable(err) { + return err + } + if start.IsZero() { + start = time.Now() + } else if d := time.Since(start) + nextSleep; d >= arbitraryTimeout { + return err + } + time.Sleep(nextSleep) + nextSleep += time.Duration(rand.Int63n(int64(nextSleep))) + } +} diff --git a/internal/lsp/fake/workdir.go b/internal/lsp/fake/workdir.go index 734f5fd8197..0a72083bf2f 100644 --- a/internal/lsp/fake/workdir.go +++ b/internal/lsp/fake/workdir.go @@ -77,6 +77,10 @@ func WriteFileData(path string, content []byte, rel RelativeTo) error { // on Windows. var isWindowsErrLockViolation = func(err error) bool { return false } +// isWindowsRetryable reports whether err is a Windows error code +// that may be fixed by retrying a failed filesystem operation. +var isWindowsRetryable = func(err error) bool { return false } + // Workdir is a temporary working directory for tests. It exposes file // operations in terms of relative paths, and fakes file watching by triggering // events on file operations. diff --git a/internal/lsp/fake/workdir_windows.go b/internal/lsp/fake/workdir_windows.go index bcd18b7a226..fc5ad1a89af 100644 --- a/internal/lsp/fake/workdir_windows.go +++ b/internal/lsp/fake/workdir_windows.go @@ -10,10 +10,31 @@ import ( ) func init() { - // from https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- - const ERROR_LOCK_VIOLATION syscall.Errno = 33 + // constants copied from GOROOT/src/internal/syscall/windows/syscall_windows.go + const ( + ERROR_SHARING_VIOLATION syscall.Errno = 32 + ERROR_LOCK_VIOLATION syscall.Errno = 33 + ) isWindowsErrLockViolation = func(err error) bool { return errors.Is(err, ERROR_LOCK_VIOLATION) } + + // Copied from GOROOT/src/testing/testing_windows.go + isWindowsRetryable = func(err error) bool { + for { + unwrapped := errors.Unwrap(err) + if unwrapped == nil { + break + } + err = unwrapped + } + if err == syscall.ERROR_ACCESS_DENIED { + return true // Observed in https://go.dev/issue/50051. + } + if err == ERROR_SHARING_VIOLATION { + return true // Observed in https://go.dev/issue/51442. + } + return false + } } From b230791f2dc806191a96162c83840236aef3990e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 11 Jul 2022 14:39:26 -0400 Subject: [PATCH 099/723] internal/lsp/cache: move PosTo{Decl,Field} out of cache Before, these methods of the Source interface used to use a cache of buildASTCache, which built a Pos-keyed map for a whole file, but the necessary algorithm is essentially a binary search which is plenty fast enough to avoid the need for cache. This change implements that algorithm and moves both methods out of the interface into a single function, source.FindDeclAndField. -- I measured the duration of all calls to astCacheData (before) and FindDeclAndField (after) occurring within this command: $ go test -bench=TestBenchmarkConfiguredCompletion -v ./gopls/internal/regtest/bench -completion_workdir=$HOME/w/kubernetes -completion_file=../kubernetes/pkg/generated/openapi/zz_generated.openapi.go -completion_regexp=Get (The numbers reported by this benchmark are problematic, which is why I measured call times directly; see https://github.com/golang/go/issues/53798.) Results: before (n=4727) max = 21ms, 90% = 4.4ms, median = 19us after (n=6282) max = 2.3ms, 90% = 25us, median = 14us The increased number of calls to the function after the change is due to a longstanding bug in the benchmark: each iteration of the b.N loop doesn't do a fixed amount of work, it does as much as it can in 10s. Thus making the code faster simply causes the benchmark to spend the same amount of time on other parts of the program--such as the loop that calls FindDeclAndField. See https://go-review.googlesource.com/c/tools/+/221021 for background on the previous implementation. Change-Id: I745ecc4e65378fbe97f456228cafba84105b7e49 Reviewed-on: https://go-review.googlesource.com/c/tools/+/416880 Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- internal/lsp/cache/parse.go | 166 ----------------------- internal/lsp/source/completion/format.go | 5 +- internal/lsp/source/hover.go | 102 +++++++++++++- internal/lsp/source/identifier.go | 5 +- internal/lsp/source/signature_help.go | 5 +- internal/lsp/source/types_format.go | 4 +- internal/lsp/source/view.go | 15 -- 7 files changed, 103 insertions(+), 199 deletions(-) diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index ef588c60597..11075195330 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -26,7 +26,6 @@ import ( "golang.org/x/tools/internal/lsp/safetoken" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" ) // parseKey uniquely identifies a parsed Go file. @@ -107,171 +106,6 @@ type parseGoResult struct { err error } -type astCacheKey struct { - pkg packageHandleKey - uri span.URI -} - -func (s *snapshot) astCacheData(ctx context.Context, spkg source.Package, pos token.Pos) (*astCacheData, error) { - pkg := spkg.(*pkg) - pkgHandle := s.getPackage(pkg.m.ID, pkg.mode) - if pkgHandle == nil { - return nil, fmt.Errorf("could not reconstruct package handle for %v", pkg.m.ID) - } - tok := s.FileSet().File(pos) - if tok == nil { - return nil, fmt.Errorf("no file for pos %v", pos) - } - pgf, err := pkg.File(span.URIFromPath(tok.Name())) - if err != nil { - return nil, err - } - - // TODO(adonovan): opt: is it necessary to cache this operation? - // - // I expect the main benefit of CL 221021, which introduced it, - // was the replacement of PathEnclosingInterval, whose - // traversal is allocation-intensive, by buildASTCache. - // - // When run on the largest file in k8s, buildASTCache took - // ~6ms, but I expect most of that cost could be eliminated by - // using a stripped-down version of PathEnclosingInterval that - // cares only about syntax trees and not tokens. A stateless - // utility function that is cheap enough to call for each Pos - // would be a nice simplification. - // - // (The basic approach would be to use ast.Inspect, compare - // each node with the search Pos, and bail out as soon - // as a match is found. The pre-order hook would return false - // to avoid descending into any tree whose End is before - // the search Pos.) - // - // A representative benchmark would help. - astHandle, release := s.store.Handle(astCacheKey{pkgHandle.key, pgf.URI}, func(ctx context.Context, arg interface{}) interface{} { - return buildASTCache(pgf) - }) - defer release() - - d, err := s.awaitHandle(ctx, astHandle) - if err != nil { - return nil, err - } - return d.(*astCacheData), nil -} - -func (s *snapshot) PosToDecl(ctx context.Context, spkg source.Package, pos token.Pos) (ast.Decl, error) { - data, err := s.astCacheData(ctx, spkg, pos) - if err != nil { - return nil, err - } - return data.posToDecl[pos], nil -} - -func (s *snapshot) PosToField(ctx context.Context, spkg source.Package, pos token.Pos) (*ast.Field, error) { - data, err := s.astCacheData(ctx, spkg, pos) - if err != nil { - return nil, err - } - return data.posToField[pos], nil -} - -// An astCacheData maps object positions to syntax nodes for a single Go file. -type astCacheData struct { - // Maps the position of each name declared by a func/var/const/type - // Decl to the Decl node. Also maps the name and type of each field - // (broadly defined) to its innermost enclosing Decl. - posToDecl map[token.Pos]ast.Decl - - // Maps the position of the Name and Type of each field - // (broadly defined) to the Field node. - posToField map[token.Pos]*ast.Field -} - -// buildASTCache builds caches to aid in quickly going from the typed -// world to the syntactic world. -func buildASTCache(pgf *source.ParsedGoFile) *astCacheData { - var ( - // path contains all ancestors, including n. - path []ast.Node - // decls contains all ancestors that are decls. - decls []ast.Decl - ) - - data := &astCacheData{ - posToDecl: make(map[token.Pos]ast.Decl), - posToField: make(map[token.Pos]*ast.Field), - } - - ast.Inspect(pgf.File, func(n ast.Node) bool { - if n == nil { - lastP := path[len(path)-1] - path = path[:len(path)-1] - if len(decls) > 0 && decls[len(decls)-1] == lastP { - decls = decls[:len(decls)-1] - } - return false - } - - path = append(path, n) - - switch n := n.(type) { - case *ast.Field: - addField := func(f ast.Node) { - if f.Pos().IsValid() { - data.posToField[f.Pos()] = n - if len(decls) > 0 { - data.posToDecl[f.Pos()] = decls[len(decls)-1] - } - } - } - - // Add mapping for *ast.Field itself. This handles embedded - // fields which have no associated *ast.Ident name. - addField(n) - - // Add mapping for each field name since you can have - // multiple names for the same type expression. - for _, name := range n.Names { - addField(name) - } - - // Also map "X" in "...X" to the containing *ast.Field. This - // makes it easy to format variadic signature params - // properly. - if elips, ok := n.Type.(*ast.Ellipsis); ok && elips.Elt != nil { - addField(elips.Elt) - } - case *ast.FuncDecl: - decls = append(decls, n) - - if n.Name != nil && n.Name.Pos().IsValid() { - data.posToDecl[n.Name.Pos()] = n - } - case *ast.GenDecl: - decls = append(decls, n) - - for _, spec := range n.Specs { - switch spec := spec.(type) { - case *ast.TypeSpec: - if spec.Name != nil && spec.Name.Pos().IsValid() { - data.posToDecl[spec.Name.Pos()] = n - } - case *ast.ValueSpec: - for _, id := range spec.Names { - if id != nil && id.Pos().IsValid() { - data.posToDecl[id.Pos()] = n - } - } - } - } - } - - return true - }) - - return data -} - // parseGoImpl parses the Go source file whose content is provided by fh. func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { ctx, done := event.Start(ctx, "cache.parseGo", tag.File.Of(fh.URI().Filename())) diff --git a/internal/lsp/source/completion/format.go b/internal/lsp/source/completion/format.go index 72498cc6874..d34cee22ad2 100644 --- a/internal/lsp/source/completion/format.go +++ b/internal/lsp/source/completion/format.go @@ -242,10 +242,7 @@ Suffixes: return item, nil } - decl, err := c.snapshot.PosToDecl(ctx, pkg, obj.Pos()) - if err != nil { - return CompletionItem{}, err - } + decl, _ := source.FindDeclAndField(pkg.GetSyntax(), obj.Pos()) // may be nil hover, err := source.FindHoverContext(ctx, c.snapshot, pkg, obj, decl, nil) if err != nil { event.Error(ctx, "failed to find Hover", err, tag.URI.Of(uri)) diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go index 58ea9696203..b2524c499e4 100644 --- a/internal/lsp/source/hover.go +++ b/internal/lsp/source/hover.go @@ -610,11 +610,7 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob break } - field, err := s.PosToField(ctx, pkg, obj.Pos()) - if err != nil { - return nil, err - } - + _, field := FindDeclAndField(pkg.GetSyntax(), obj.Pos()) if field != nil { comment := field.Doc if comment.Text() == "" { @@ -876,3 +872,99 @@ func anyNonEmpty(x []string) bool { } return false } + +// FindDeclAndField returns the var/func/type/const Decl that declares +// the identifier at pos, searching the given list of file syntax +// trees. If pos is the position of an ast.Field or one of its Names +// or Ellipsis.Elt, the field is returned, along with the innermost +// enclosing Decl, which could be only loosely related---consider: +// +// var decl = f( func(field int) {} ) +// +// It returns (nil, nil) if no Field or Decl is found at pos. +func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *ast.Field) { + // panic(nil) breaks off the traversal and + // causes the function to return normally. + defer func() { + if x := recover(); x != nil { + panic(x) + } + }() + + // Visit the files in search of the node at pos. + var stack []ast.Node + for _, file := range files { + ast.Inspect(file, func(n ast.Node) bool { + if n != nil { + stack = append(stack, n) // push + } else { + stack = stack[:len(stack)-1] // pop + return false + } + + // Skip subtrees (incl. files) that don't contain the search point. + if !(n.Pos() <= pos && pos < n.End()) { + return false + } + + switch n := n.(type) { + case *ast.Field: + checkField := func(f ast.Node) { + if f.Pos() == pos { + field = n + for i := len(stack) - 1; i >= 0; i-- { + if d, ok := stack[i].(ast.Decl); ok { + decl = d // innermost enclosing decl + break + } + } + panic(nil) // found + } + } + + // Check *ast.Field itself. This handles embedded + // fields which have no associated *ast.Ident name. + checkField(n) + + // Check each field name since you can have + // multiple names for the same type expression. + for _, name := range n.Names { + checkField(name) + } + + // Also check "X" in "...X". This makes it easy + // to format variadic signature params properly. + if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil { + checkField(ell.Elt) + } + + case *ast.FuncDecl: + if n.Name.Pos() == pos { + decl = n + panic(nil) // found + } + + case *ast.GenDecl: + for _, spec := range n.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + if spec.Name.Pos() == pos { + decl = n + panic(nil) // found + } + case *ast.ValueSpec: + for _, id := range spec.Names { + if id.Pos() == pos { + decl = n + panic(nil) // found + } + } + } + } + } + return true + }) + } + + return nil, nil +} diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go index c87725c4854..5378ae840ed 100644 --- a/internal/lsp/source/identifier.go +++ b/internal/lsp/source/identifier.go @@ -292,9 +292,8 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if err != nil { return nil, err } - if result.Declaration.node, err = snapshot.PosToDecl(ctx, declPkg, result.Declaration.obj.Pos()); err != nil { - return nil, err - } + result.Declaration.node, _ = FindDeclAndField(declPkg.GetSyntax(), result.Declaration.obj.Pos()) // may be nil + // Ensure that we have the full declaration, in case the declaration was // parsed in ParseExported and therefore could be missing information. if result.Declaration.fullDecl, err = fullNode(snapshot, result.Declaration.obj, declPkg); err != nil { diff --git a/internal/lsp/source/signature_help.go b/internal/lsp/source/signature_help.go index 12e359008fe..5b087e83769 100644 --- a/internal/lsp/source/signature_help.go +++ b/internal/lsp/source/signature_help.go @@ -98,10 +98,7 @@ FindCall: if err != nil { return nil, 0, err } - node, err := snapshot.PosToDecl(ctx, declPkg, obj.Pos()) - if err != nil { - return nil, 0, err - } + node, _ := FindDeclAndField(declPkg.GetSyntax(), obj.Pos()) // may be nil d, err := FindHoverContext(ctx, snapshot, pkg, obj, node, nil) if err != nil { return nil, 0, err diff --git a/internal/lsp/source/types_format.go b/internal/lsp/source/types_format.go index 5e10a509005..756d02de22a 100644 --- a/internal/lsp/source/types_format.go +++ b/internal/lsp/source/types_format.go @@ -259,8 +259,8 @@ func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj * return types.TypeString(obj.Type(), qf) } - field, err := snapshot.PosToField(ctx, pkg, obj.Pos()) - if err != nil || field == nil { + _, field := FindDeclAndField(pkg.GetSyntax(), obj.Pos()) + if field == nil { return types.TypeString(obj.Type(), qf) } expr := field.Type diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 004d830dd07..c067fe5a4f6 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -79,21 +79,6 @@ type Snapshot interface { // If the file is not available, returns nil and an error. ParseGo(ctx context.Context, fh FileHandle, mode ParseMode) (*ParsedGoFile, error) - // PosToField is a cache of *ast.Fields by token.Pos. This allows us - // to quickly find corresponding *ast.Field node given a *types.Var. - // We must refer to the AST to render type aliases properly when - // formatting signatures and other types. - // May return (nil, nil) if the file didn't declare an object at that position. - // TODO(adonovan): seems like a bug? - PosToField(ctx context.Context, pkg Package, pos token.Pos) (*ast.Field, error) - - // PosToDecl maps certain objects' positions to their surrounding - // ast.Decl. This mapping is used when building the documentation - // string for the objects. - // May return (nil, nil) if the file didn't declare an object at that position. - // TODO(adonovan): seems like a bug? - PosToDecl(ctx context.Context, pkg Package, pos token.Pos) (ast.Decl, error) - // DiagnosePackage returns basic diagnostics, including list, parse, and type errors // for pkg, grouped by file. DiagnosePackage(ctx context.Context, pkg Package) (map[span.URI][]*Diagnostic, error) From dcb576d3b6e02adc9dadcbaafedf16b67034edda Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 12 Jul 2022 16:48:37 -0400 Subject: [PATCH 100/723] internal/lsp/cache: simplify modtidy Previously, the modtidy operation used a persistent map of handles in the central store that cached the result of a parsing the go.mod file after running 'go mod tidy'. The key was complex, including the session, view, imports of all dependencies, and the names of all unsaved overlays. The fine-grained key prevented spurious cache hits for invalid inputs by (we suspect) preventing nearly all cache hits. The existing snapshot invalidation mechanism should be sufficient to solve this problem, as the map entry is evicted whenever the metadata or overlays change. So, this change avoids keeping handles in the central store, so they are never shared across views. Also, modtidy exploited the fact that a packageHandle used to include a copy of all the Go source files of each package, to avoid having to read the files itself. As a result it would entail lots of unnecessary work building package handles and reading dependencies when it has no business even thinking about type checking. This change: - extracts the logic to read Metadata.{GoFiles,CompiledGo}Files so that it can be shared by modtidy and buildPackageHandle. - packageHandle.imports has moved into mod_tidy. One call (to compute the hash key) has gone away, as have various other hashing operations. - removes the packagesMap typed persistent.Map wrapper. - analysis: check cache before calling buildPackageHandle. - decouple Handle from Store so that unstored handles may be used. - adds various TODO comments for further simplification. Change-Id: Ibdc086ca76d6483b094ef48aac5b1dd0cdd04973 Reviewed-on: https://go-review.googlesource.com/c/tools/+/417116 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro Auto-Submit: Alan Donovan --- internal/lsp/cache/analysis.go | 22 ++++-- internal/lsp/cache/check.go | 90 ++++++++++++------------- internal/lsp/cache/maps.go | 46 +------------ internal/lsp/cache/mod.go | 1 + internal/lsp/cache/mod_tidy.go | 119 ++++++++++++++++----------------- internal/lsp/cache/session.go | 2 +- internal/lsp/cache/snapshot.go | 49 ++++++-------- internal/lsp/cache/view.go | 2 +- internal/memoize/memoize.go | 29 ++++++-- 9 files changed, 160 insertions(+), 200 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index e196d1c4a35..a5e34b406da 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -23,6 +23,11 @@ import ( ) func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) { + // TODO(adonovan): merge these two loops. There's no need to + // construct all the root action handles before beginning + // analysis. Operations should be concurrent (though that first + // requires buildPackageHandle not to be inefficient when + // called in parallel.) var roots []*actionHandle for _, a := range analyzers { if !a.IsEnabled(s.view) { @@ -95,15 +100,18 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A // use of concurrency would lead to an exponential amount of duplicated // work. We should instead use an atomically updated future cache // and a parallel graph traversal. - ph, err := s.buildPackageHandle(ctx, id, source.ParseFull) - if err != nil { - return nil, err - } - if act := s.getActionHandle(id, ph.mode, a); act != nil { + + // TODO(adonovan): in the code below, follow the structure of + // the other handle-map accessors. + + const mode = source.ParseFull + if act := s.getActionHandle(id, mode, a); act != nil { return act, nil } - if len(ph.key) == 0 { - return nil, fmt.Errorf("actionHandle: no key for package %s", id) + + ph, err := s.buildPackageHandle(ctx, id, mode) + if err != nil { + return nil, err } pkg, err := ph.check(ctx, s) if err != nil { diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index a2599f930c2..a830cc7f59d 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -14,8 +14,6 @@ import ( "path" "path/filepath" "regexp" - "sort" - "strconv" "strings" "sync" @@ -34,23 +32,32 @@ import ( "golang.org/x/tools/internal/typesinternal" ) +// A packageKey identifies a packageHandle in the snapshot.packages map. +type packageKey struct { + mode source.ParseMode + id PackageID +} + type packageHandleKey source.Hash +// A packageHandle is a handle to the future result of type-checking a package. +// The resulting package is obtained from the check() method. type packageHandle struct { handle *memoize.Handle - // goFiles and compiledGoFiles are the lists of files in the package. - // The latter is the list of files seen by the type checker (in which - // those that import "C" have been replaced by generated code). - goFiles, compiledGoFiles []source.FileHandle - - // mode is the mode the files were parsed in. + // mode is the mode the files will be parsed in. mode source.ParseMode // m is the metadata associated with the package. m *KnownMetadata // key is the hashed key for the package. + // + // It includes the all bits of the transitive closure of + // dependencies's sources. This is more than type checking + // really depends on: export data of direct deps should be + // enough. (The key for analysis actions could similarly + // hash only Facts of direct dependencies.) key packageHandleKey } @@ -61,26 +68,6 @@ func (ph *packageHandle) packageKey() packageKey { } } -func (ph *packageHandle) imports(ctx context.Context, s source.Snapshot) (result []string) { - for _, goFile := range ph.goFiles { - f, err := s.ParseGo(ctx, goFile, source.ParseHeader) - if err != nil { - continue - } - seen := map[string]struct{}{} - for _, impSpec := range f.File.Imports { - imp, _ := strconv.Unquote(impSpec.Path.Value) - if _, ok := seen[imp]; !ok { - seen[imp] = struct{}{} - result = append(result, imp) - } - } - } - - sort.Strings(result) - return result -} - // packageData contains the data produced by type-checking a package. type packageData struct { pkg *pkg @@ -145,21 +132,8 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so } // Read both lists of files of this package, in parallel. - var group errgroup.Group - getFileHandles := func(files []span.URI) []source.FileHandle { - fhs := make([]source.FileHandle, len(files)) - for i, uri := range files { - i, uri := i, uri - group.Go(func() (err error) { - fhs[i], err = s.GetFile(ctx, uri) // ~25us - return - }) - } - return fhs - } - goFiles := getFileHandles(m.GoFiles) - compiledGoFiles := getFileHandles(m.CompiledGoFiles) - if err := group.Wait(); err != nil { + goFiles, compiledGoFiles, err := readGoFiles(ctx, s, m.Metadata) + if err != nil { return nil, err } @@ -197,12 +171,10 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so }) ph := &packageHandle{ - handle: handle, - goFiles: goFiles, - compiledGoFiles: compiledGoFiles, - mode: mode, - m: m, - key: key, + handle: handle, + mode: mode, + m: m, + key: key, } // Cache the handle in the snapshot. If a package handle has already @@ -212,6 +184,26 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so return s.addPackageHandle(ph, release) } +// readGoFiles reads the content of Metadata.GoFiles and +// Metadata.CompiledGoFiles, in parallel. +func readGoFiles(ctx context.Context, s *snapshot, m *Metadata) (goFiles, compiledGoFiles []source.FileHandle, err error) { + var group errgroup.Group + getFileHandles := func(files []span.URI) []source.FileHandle { + fhs := make([]source.FileHandle, len(files)) + for i, uri := range files { + i, uri := i, uri + group.Go(func() (err error) { + fhs[i], err = s.GetFile(ctx, uri) // ~25us + return + }) + } + return fhs + } + return getFileHandles(m.GoFiles), + getFileHandles(m.CompiledGoFiles), + group.Wait() +} + func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { s.mu.Lock() defer s.mu.Unlock() diff --git a/internal/lsp/cache/maps.go b/internal/lsp/cache/maps.go index 4bb3b3b2689..eef918843e8 100644 --- a/internal/lsp/cache/maps.go +++ b/internal/lsp/cache/maps.go @@ -149,16 +149,8 @@ func (m parseKeysByURIMap) Delete(key span.URI) { m.impl.Delete(key) } -type packagesMap struct { - impl *persistent.Map -} - -func newPackagesMap() packagesMap { - return packagesMap{ - impl: persistent.NewMap(func(a, b interface{}) bool { - return packageKeyLess(a.(packageKey), b.(packageKey)) - }), - } +func packageKeyLessInterface(x, y interface{}) bool { + return packageKeyLess(x.(packageKey), y.(packageKey)) } func packageKeyLess(x, y packageKey) bool { @@ -168,40 +160,6 @@ func packageKeyLess(x, y packageKey) bool { return x.id < y.id } -func (m packagesMap) Clone() packagesMap { - return packagesMap{ - impl: m.impl.Clone(), - } -} - -func (m packagesMap) Destroy() { - m.impl.Destroy() -} - -func (m packagesMap) Get(key packageKey) (*packageHandle, bool) { - value, ok := m.impl.Get(key) - if !ok { - return nil, false - } - return value.(*packageHandle), true -} - -func (m packagesMap) Range(do func(key packageKey, value *packageHandle)) { - m.impl.Range(func(key, value interface{}) { - do(key.(packageKey), value.(*packageHandle)) - }) -} - -func (m packagesMap) Set(key packageKey, value *packageHandle, release func()) { - m.impl.Set(key, value, func(key, value interface{}) { - release() - }) -} - -func (m packagesMap) Delete(key packageKey) { - m.impl.Delete(key) -} - type knownDirsSet struct { impl *persistent.Map } diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index 79b3fd016d6..8e6017648cc 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -225,6 +225,7 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string if !hit { // TODO(adonovan): use a simpler cache of promises that // is shared across snapshots. See comment at modTidyKey. + // We can then delete hashEnv too. type modWhyKey struct { // TODO(rfindley): is sessionID used to identify overlays because modWhy // looks at overlay state? In that case, I am not sure that this key diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index b59b4fd8832..a04bacf8ee2 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -12,7 +12,6 @@ import ( "io/ioutil" "os" "path/filepath" - "sort" "strconv" "strings" @@ -28,7 +27,8 @@ import ( "golang.org/x/tools/internal/span" ) -// modTidyImpl runs "go mod tidy" on a go.mod file, using a cache. +// ModTidy returns the go.mod file that would be obtained by running +// "go mod tidy". Concurrent requests are combined into a single command. func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) { uri := pm.URI if pm.File == nil { @@ -46,63 +46,38 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc // Cache miss? if !hit { + // If the file handle is an overlay, it may not be written to disk. + // The go.mod file has to be on disk for `go mod tidy` to work. + // TODO(rfindley): is this still true with Go 1.16 overlay support? fh, err := s.GetFile(ctx, pm.URI) if err != nil { return nil, err } - // If the file handle is an overlay, it may not be written to disk. - // The go.mod file has to be on disk for `go mod tidy` to work. - // TODO(rfindley): is this still true with Go 1.16 overlay support? if _, ok := fh.(*overlay); ok { - if info, _ := os.Stat(fh.URI().Filename()); info == nil { + if info, _ := os.Stat(uri.Filename()); info == nil { return nil, source.ErrNoModOnDisk } } + if criticalErr := s.GetCriticalError(ctx); criticalErr != nil { return &source.TidiedModule{ Diagnostics: criticalErr.DiagList, }, nil } - workspacePkgs, err := s.workspacePackageHandles(ctx) - if err != nil { + + if err := s.awaitLoaded(ctx); err != nil { return nil, err } - s.mu.Lock() - overlayHash := hashUnsavedOverlays(s.files) - s.mu.Unlock() + handle := memoize.NewHandle("modTidy", func(ctx context.Context, arg interface{}) interface{} { - // There's little reason at to use the shared cache for mod - // tidy (and mod why) as their key includes the view and session. - // Its only real value is to de-dup requests in flight, for - // which a singleflight in the View would suffice. - // TODO(adonovan): use a simpler cache of promises that - // is shared across snapshots. - type modTidyKey struct { - // TODO(rfindley): this key is also suspicious (see modWhyKey). - sessionID string - env source.Hash - gomod source.FileIdentity - imports source.Hash - unsavedOverlays source.Hash - view string - } - key := modTidyKey{ - sessionID: s.view.session.id, - view: s.view.folder.Filename(), - imports: s.hashImports(ctx, workspacePkgs), - unsavedOverlays: overlayHash, - gomod: fh.FileIdentity(), - env: hashEnv(s), - } - handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} { - tidied, err := modTidyImpl(ctx, arg.(*snapshot), fh, pm, workspacePkgs) + tidied, err := modTidyImpl(ctx, arg.(*snapshot), uri.Filename(), pm) return modTidyResult{tidied, err} }) entry = handle s.mu.Lock() - s.modTidyHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.modTidyHandles.Set(uri, entry, nil) s.mu.Unlock() } @@ -116,15 +91,16 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc } // modTidyImpl runs "go mod tidy" on a go.mod file. -func modTidyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle, pm *source.ParsedModule, workspacePkgs []*packageHandle) (*source.TidiedModule, error) { - ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(fh.URI())) +func modTidyImpl(ctx context.Context, snapshot *snapshot, filename string, pm *source.ParsedModule) (*source.TidiedModule, error) { + ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(filename)) defer done() inv := &gocommand.Invocation{ Verb: "mod", Args: []string{"tidy"}, - WorkingDir: filepath.Dir(fh.URI().Filename()), + WorkingDir: filepath.Dir(filename), } + // TODO(adonovan): ensure that unsaved overlays are passed through to 'go'. tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv) if err != nil { return nil, err @@ -151,7 +127,7 @@ func modTidyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle, // Compare the original and tidied go.mod files to compute errors and // suggested fixes. - diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs) + diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal) if err != nil { return nil, err } @@ -162,25 +138,10 @@ func modTidyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle, }, nil } -func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) source.Hash { - seen := map[string]struct{}{} - var imports []string - for _, ph := range wsPackages { - for _, imp := range ph.imports(ctx, s) { - if _, ok := seen[imp]; !ok { - imports = append(imports, imp) - seen[imp] = struct{}{} - } - } - } - sort.Strings(imports) - return source.Hashf("%s", imports) -} - // modTidyDiagnostics computes the differences between the original and tidied // go.mod files to produce diagnostic and suggested fixes. Some diagnostics // may appear on the Go files that import packages from missing modules. -func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []*packageHandle) (diagnostics []*source.Diagnostic, err error) { +func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.ParsedModule, ideal *modfile.File) (diagnostics []*source.Diagnostic, err error) { // First, determine which modules are unused and which are missing from the // original go.mod file. var ( @@ -229,15 +190,25 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc } // Add diagnostics for missing modules anywhere they are imported in the // workspace. - for _, ph := range workspacePkgs { + // TODO(adonovan): opt: opportunities for parallelism abound. + for _, id := range snapshot.workspacePackageIDs() { + m := snapshot.getMetadata(id) + if m == nil { + return nil, fmt.Errorf("no metadata for %s", id) + } + + // Read both lists of files of this package, in parallel. + goFiles, compiledGoFiles, err := readGoFiles(ctx, snapshot, m.Metadata) + if err != nil { + return nil, err + } + missingImports := map[string]*modfile.Require{} // If -mod=readonly is not set we may have successfully imported // packages from missing modules. Otherwise they'll be in // MissingDependencies. Combine both. - importedPkgs := ph.imports(ctx, snapshot) - - for _, imp := range importedPkgs { + for imp := range parseImports(ctx, snapshot, goFiles) { if req, ok := missing[imp]; ok { missingImports[imp] = req break @@ -266,7 +237,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc if len(missingImports) == 0 { continue } - for _, goFile := range ph.compiledGoFiles { + for _, goFile := range compiledGoFiles { pgf, err := snapshot.ParseGo(ctx, goFile, source.ParseHeader) if err != nil { continue @@ -507,3 +478,27 @@ func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Sp } return span.New(m.URI, start, end), nil } + +// parseImports parses the headers of the specified files and returns +// the set of strings that appear in import declarations within +// GoFiles. Errors are ignored. +// +// (We can't simply use ph.m.Metadata.Deps because it contains +// PackageIDs--not import paths--and is based on CompiledGoFiles, +// after cgo processing.) +func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) map[string]bool { + s.mu.Lock() // peekOrParse requires a locked snapshot (!) + defer s.mu.Unlock() + seen := make(map[string]bool) + for _, file := range files { + f, err := peekOrParse(ctx, s, file, source.ParseHeader) + if err != nil { + continue + } + for _, spec := range f.File.Imports { + path, _ := strconv.Unquote(spec.Path.Value) + seen[path] = true + } + } + return seen +} diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 9c1505850c2..2374a528757 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -232,7 +232,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, cancel: cancel, initializeOnce: &sync.Once{}, store: &s.cache.store, - packages: newPackagesMap(), + packages: persistent.NewMap(packageKeyLessInterface), meta: &metadataGraph{}, files: newFilesMap(), isActivePackageCache: newIsActivePackageCacheMap(), diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index a9dd1dfb935..1ba945cf0ed 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -85,7 +85,7 @@ type snapshot struct { files filesMap // parsedGoFiles maps a parseKey to the handle of the future result of parsing it. - parsedGoFiles *persistent.Map // from parseKey to *memoize.Handle + parsedGoFiles *persistent.Map // from parseKey to *memoize.Handle[parseGoResult] // parseKeysByURI records the set of keys of parsedGoFiles that // need to be invalidated for each URI. @@ -95,7 +95,7 @@ type snapshot struct { // symbolizeHandles maps each file URI to a handle for the future // result of computing the symbols declared in that file. - symbolizeHandles *persistent.Map // from span.URI to *memoize.Handle + symbolizeHandles *persistent.Map // from span.URI to *memoize.Handle[symbolizeResult] // packages maps a packageKey to a *packageHandle. // It may be invalidated when a file's content changes. @@ -104,7 +104,7 @@ type snapshot struct { // - packages.Get(id).m.Metadata == meta.metadata[id].Metadata for all ids // - if a package is in packages, then all of its dependencies should also // be in packages, unless there is a missing import - packages packagesMap + packages *persistent.Map // from packageKey to *memoize.Handle[*packageHandle] // isActivePackageCache maps package ID to the cached value if it is active or not. // It may be invalidated when metadata changes or a new file is opened or closed. @@ -123,17 +123,17 @@ type snapshot struct { // parseModHandles keeps track of any parseModHandles for the snapshot. // The handles need not refer to only the view's go.mod file. - parseModHandles *persistent.Map // from span.URI to *memoize.Handle + parseModHandles *persistent.Map // from span.URI to *memoize.Handle[parseModResult] // parseWorkHandles keeps track of any parseWorkHandles for the snapshot. // The handles need not refer to only the view's go.work file. - parseWorkHandles *persistent.Map // from span.URI to *memoize.Handle + parseWorkHandles *persistent.Map // from span.URI to *memoize.Handle[parseWorkResult] // Preserve go.mod-related handles to avoid garbage-collecting the results // of various calls to the go command. The handles need not refer to only // the view's go.mod file. - modTidyHandles *persistent.Map // from span.URI to *memoize.Handle - modWhyHandles *persistent.Map // from span.URI to *memoize.Handle + modTidyHandles *persistent.Map // from span.URI to *memoize.Handle[modTidyResult] + modWhyHandles *persistent.Map // from span.URI to *memoize.Handle[modWhyResult] workspace *workspace // (not guarded by mu) @@ -175,11 +175,6 @@ func (s *snapshot) awaitHandle(ctx context.Context, h *memoize.Handle) (interfac return h.Get(ctx, s) } -type packageKey struct { - mode source.ParseMode - id PackageID -} - type actionKey struct { pkg packageKey analyzer *analysis.Analyzer @@ -603,17 +598,6 @@ func (s *snapshot) buildOverlay() map[string][]byte { return overlays } -func hashUnsavedOverlays(files filesMap) source.Hash { - var unsaved []string - files.Range(func(uri span.URI, fh source.VersionedFileHandle) { - if overlay, ok := fh.(*overlay); ok && !overlay.saved { - unsaved = append(unsaved, uri.Filename()) - } - }) - sort.Strings(unsaved) - return source.Hashf("%s", unsaved) -} - func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]source.Package, error) { ctx = event.Label(ctx, tag.URI.Of(uri)) @@ -791,6 +775,8 @@ func (s *snapshot) getImportedBy(id PackageID) []PackageID { // for the given package key, if it exists. // // An error is returned if the metadata used to build ph is no longer relevant. +// +// TODO(adonovan): inline sole use in buildPackageHandle. func (s *snapshot) addPackageHandle(ph *packageHandle, release func()) (*packageHandle, error) { s.mu.Lock() defer s.mu.Unlock() @@ -801,14 +787,15 @@ func (s *snapshot) addPackageHandle(ph *packageHandle, release func()) (*package // If the package handle has already been cached, // return the cached handle instead of overriding it. - if result, ok := s.packages.Get(ph.packageKey()); ok { + if v, ok := s.packages.Get(ph.packageKey()); ok { + result := v.(*packageHandle) release() if result.m.Metadata != ph.m.Metadata { return nil, bug.Errorf("existing package handle does not match for %s", ph.m.ID) } return result, nil } - s.packages.Set(ph.packageKey(), ph, release) + s.packages.Set(ph.packageKey(), ph, func(_, _ interface{}) { release() }) return ph, nil } @@ -1179,8 +1166,8 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac defer s.mu.Unlock() results := map[string]source.Package{} - s.packages.Range(func(key packageKey, ph *packageHandle) { - cachedPkg, err := ph.cached() + s.packages.Range(func(_, v interface{}) { + cachedPkg, err := v.(*packageHandle).cached() if err != nil { return } @@ -1215,6 +1202,7 @@ func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { return match } +// TODO(adonovan): inline sole use in buildPackageHandle. func (s *snapshot) getPackage(id PackageID, mode source.ParseMode) *packageHandle { s.mu.Lock() defer s.mu.Unlock() @@ -1223,8 +1211,11 @@ func (s *snapshot) getPackage(id PackageID, mode source.ParseMode) *packageHandl id: id, mode: mode, } - ph, _ := s.packages.Get(key) - return ph + v, ok := s.packages.Get(key) + if !ok { + return nil + } + return v.(*packageHandle) } func (s *snapshot) getActionHandle(id PackageID, m source.ParseMode, a *analysis.Analyzer) *actionHandle { diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 0991797b8e4..15a2f90da57 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -76,7 +76,7 @@ type View struct { // snapshots have been destroyed via the destroy method, and snapshotWG may // be waited upon to let these destroy operations complete. snapshotMu sync.Mutex - snapshot *snapshot // latest snapshot + snapshot *snapshot // latest snapshot; nil after shutdown has been called releaseSnapshot func() // called when snapshot is no longer needed snapshotWG sync.WaitGroup // refcount for pending destroy operations diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 6d62ebb0d96..8c921c7e169 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -22,10 +22,12 @@ import ( "golang.org/x/tools/internal/xcontext" ) +// TODO(adonovan): rename Handle to Promise, and present it before Store. + // Store binds keys to functions, returning handles that can be used to access -// the functions results. +// the function's result. type Store struct { - handlesMu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu + handlesMu sync.Mutex handles map[interface{}]*Handle } @@ -71,8 +73,9 @@ const ( // A Handle represents the future result of a call to a function. type Handle struct { - key interface{} - mu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu + debug string // for observability + + mu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu // A Handle starts out IDLE, waiting for something to demand // its evaluation. It then transitions into RUNNING state. @@ -119,9 +122,9 @@ func (store *Store) Handle(key interface{}, function Function) (*Handle, func()) if !ok { // new handle h = &Handle{ - key: key, function: function, refcount: 1, + debug: reflect.TypeOf(key).String(), } if store.handles == nil { @@ -137,7 +140,7 @@ func (store *Store) Handle(key interface{}, function Function) (*Handle, func()) release := func() { if atomic.AddInt32(&h.refcount, -1) == 0 { store.handlesMu.Lock() - delete(store.handles, h.key) + delete(store.handles, key) store.handlesMu.Unlock() } } @@ -170,6 +173,18 @@ func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { } } +// NewHandle returns a handle for the future result of calling the +// specified function. +// +// The debug string is used to classify handles in logs and metrics. +// It should be drawn from a small set. +func NewHandle(debug string, function Function) *Handle { + return &Handle{ + debug: debug, + function: function, + } +} + // Cached returns the value associated with a handle. // // It will never cause the value to be generated. @@ -220,7 +235,7 @@ func (h *Handle) run(ctx context.Context, arg interface{}) (interface{}, error) } go func() { - trace.WithRegion(childCtx, fmt.Sprintf("Handle.run %T", h.key), func() { + trace.WithRegion(childCtx, fmt.Sprintf("Handle.run %s", h.debug), func() { defer release() // Just in case the function does something expensive without checking // the context, double-check we're still alive. From b2eae762671e8ccd6d473aca4239e46ff3a0f108 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 13 Jul 2022 15:05:28 -0400 Subject: [PATCH 101/723] internal/lsp/cache: simplify modwhy cache As with mod tidy in CL 417116, we can rely on invalidation rather than cache keys to avoid reusing stale results, and there's no need to save these handles in the global store. Change-Id: I3763c01fa21c6114248c1d541e3c168fc6a128c9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/417416 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- internal/lsp/cache/check.go | 9 --------- internal/lsp/cache/mod.go | 23 ++--------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index a830cc7f59d..366a7afe133 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -249,15 +249,6 @@ func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata return packageHandleKey(source.HashOf(b.Bytes())) } -// hashEnv returns a hash of the snapshot's configuration. -func hashEnv(s *snapshot) source.Hash { - s.view.optionsMu.Lock() - env := s.view.options.EnvSlice() - s.view.optionsMu.Unlock() - - return source.Hashf("%s", env) -} - // hashConfig returns the hash for the *packages.Config. func hashConfig(config *packages.Config) source.Hash { // TODO(adonovan): opt: don't materialize the bytes; hash them directly. diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index 8e6017648cc..f9d148b7379 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -223,33 +223,14 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string // cache miss? if !hit { - // TODO(adonovan): use a simpler cache of promises that - // is shared across snapshots. See comment at modTidyKey. - // We can then delete hashEnv too. - type modWhyKey struct { - // TODO(rfindley): is sessionID used to identify overlays because modWhy - // looks at overlay state? In that case, I am not sure that this key - // is actually correct. The key should probably just be URI, and - // invalidated in clone when any import changes. - sessionID string - env source.Hash - view string - mod source.FileIdentity - } - key := modWhyKey{ - sessionID: s.view.session.id, - env: hashEnv(s), - mod: fh.FileIdentity(), - view: s.view.rootURI.Filename(), - } - handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} { + handle := memoize.NewHandle("modWhy", func(ctx context.Context, arg interface{}) interface{} { why, err := modWhyImpl(ctx, arg.(*snapshot), fh) return modWhyResult{why, err} }) entry = handle s.mu.Lock() - s.modWhyHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.modWhyHandles.Set(uri, entry, nil) s.mu.Unlock() } From 85173cc4bdf8d8d786177e25534065c10b85c56c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 13 Jul 2022 13:46:53 -0400 Subject: [PATCH 102/723] internal/lsp/cache: follow usual structure for packages, analysis maps All Get/Set operations on the maps now happen within a single function (buildPackageKey, actionHandle). No behavior change. Change-Id: I347dfda578c28657a28538e228ecfb6f0871b94b Reviewed-on: https://go-review.googlesource.com/c/tools/+/417415 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- internal/lsp/cache/analysis.go | 45 ++++++++++++----- internal/lsp/cache/cache.go | 2 +- internal/lsp/cache/check.go | 78 ++++++++++++++++++----------- internal/lsp/cache/snapshot.go | 91 ---------------------------------- 4 files changed, 83 insertions(+), 133 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index a5e34b406da..b277324a0ae 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -58,6 +58,11 @@ func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.A return results, nil } +type actionKey struct { + pkg packageKey + analyzer *analysis.Analyzer +} + type actionHandleKey source.Hash // An action represents one unit of analysis work: the application of @@ -90,6 +95,20 @@ type packageFactKey struct { } func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.Analyzer) (*actionHandle, error) { + const mode = source.ParseFull + key := actionKey{ + pkg: packageKey{id: id, mode: mode}, + analyzer: a, + } + + s.mu.Lock() + entry, hit := s.actions.Get(key) + s.mu.Unlock() + + if hit { + return entry.(*actionHandle), nil + } + // TODO(adonovan): opt: this block of code sequentially loads a package // (and all its dependencies), then sequentially creates action handles // for the direct dependencies (whose packages have by then been loaded @@ -100,15 +119,6 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A // use of concurrency would lead to an exponential amount of duplicated // work. We should instead use an atomically updated future cache // and a parallel graph traversal. - - // TODO(adonovan): in the code below, follow the structure of - // the other handle-map accessors. - - const mode = source.ParseFull - if act := s.getActionHandle(id, mode, a); act != nil { - return act, nil - } - ph, err := s.buildPackageHandle(ctx, id, mode) if err != nil { return nil, err @@ -157,13 +167,24 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A return runAnalysis(ctx, snapshot, a, pkg, results) }) - act := &actionHandle{ + ah := &actionHandle{ analyzer: a, pkg: pkg, handle: handle, } - act = s.addActionHandle(act, release) - return act, nil + + s.mu.Lock() + defer s.mu.Unlock() + + // Check cache again in case another thread got there first. + if result, ok := s.actions.Get(key); ok { + release() + return result.(*actionHandle), nil + } + + s.actions.Set(key, ah, func(_, _ interface{}) { release() }) + + return ah, nil } func (act *actionHandle) analyze(ctx context.Context, snapshot *snapshot) ([]*source.Diagnostic, interface{}, error) { diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index 8ac8b851a25..a59c8908d5a 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -199,7 +199,7 @@ func (c *Cache) PackageStats(withNames bool) template.HTML { c.store.DebugOnlyIterate(func(k, v interface{}) { switch k.(type) { case packageHandleKey: - v := v.(*packageData) + v := v.(typeCheckResult) if v.pkg == nil { break } diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index 366a7afe133..e51f86e5d9e 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -22,6 +22,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/debug/tag" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" @@ -43,10 +44,7 @@ type packageHandleKey source.Hash // A packageHandle is a handle to the future result of type-checking a package. // The resulting package is obtained from the check() method. type packageHandle struct { - handle *memoize.Handle - - // mode is the mode the files will be parsed in. - mode source.ParseMode + handle *memoize.Handle // [typeCheckResult] // m is the metadata associated with the package. m *KnownMetadata @@ -61,15 +59,9 @@ type packageHandle struct { key packageHandleKey } -func (ph *packageHandle) packageKey() packageKey { - return packageKey{ - id: ph.m.ID, - mode: ph.mode, - } -} - -// packageData contains the data produced by type-checking a package. -type packageData struct { +// typeCheckResult contains the result of a call to +// typeCheck, which type-checks a package. +type typeCheckResult struct { pkg *pkg err error } @@ -80,15 +72,21 @@ type packageData struct { // attempt to reload missing or invalid metadata. The caller must reload // metadata if needed. func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode source.ParseMode) (*packageHandle, error) { - if ph := s.getPackage(id, mode); ph != nil { - return ph, nil - } + packageKey := packageKey{id: id, mode: mode} + + s.mu.Lock() + entry, hit := s.packages.Get(packageKey) + m := s.meta.metadata[id] + s.mu.Unlock() - m := s.getMetadata(id) if m == nil { return nil, fmt.Errorf("no metadata for %s", id) } + if hit { + return entry.(*packageHandle), nil + } + // Begin computing the key by getting the depKeys for all dependencies. // This requires reading the transitive closure of dependencies' source files. // @@ -140,10 +138,10 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // All the file reading has now been done. // Create a handle for the result of type checking. experimentalKey := s.View().Options().ExperimentalPackageCacheKey - key := computePackageKey(m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) + phKey := computePackageKey(m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) // TODO(adonovan): extract lambda into a standalone function to // avoid implicit lexical dependencies. - handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} { + handle, release := s.store.Handle(phKey, func(ctx context.Context, arg interface{}) interface{} { snapshot := arg.(*snapshot) // Start type checking of direct dependencies, @@ -167,21 +165,43 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so defer wg.Wait() pkg, err := typeCheck(ctx, snapshot, goFiles, compiledGoFiles, m.Metadata, mode, deps) - return &packageData{pkg, err} + return typeCheckResult{pkg, err} }) ph := &packageHandle{ handle: handle, - mode: mode, m: m, - key: key, + key: phKey, } - // Cache the handle in the snapshot. If a package handle has already - // been cached, addPackage will return the cached value. This is fine, - // since the original package handle above will have no references and be - // garbage collected. - return s.addPackageHandle(ph, release) + s.mu.Lock() + defer s.mu.Unlock() + + // Check that the metadata has not changed + // (which should invalidate this handle). + // + // (In future, handles should form a graph with edges from a + // packageHandle to the handles for parsing its files and the + // handles for type-checking its immediate deps, at which + // point there will be no need to even access s.meta.) + if s.meta.metadata[ph.m.ID].Metadata != ph.m.Metadata { + return nil, fmt.Errorf("stale metadata for %s", ph.m.ID) + } + + // Check cache again in case another thread got there first. + if prev, ok := s.packages.Get(packageKey); ok { + prevPH := prev.(*packageHandle) + release() + if prevPH.m.Metadata != ph.m.Metadata { + return nil, bug.Errorf("existing package handle does not match for %s", ph.m.ID) + } + return prevPH, nil + } + + // Update the map. + s.packages.Set(packageKey, ph, func(_, _ interface{}) { release() }) + + return ph, nil } // readGoFiles reads the content of Metadata.GoFiles and @@ -273,7 +293,7 @@ func (ph *packageHandle) check(ctx context.Context, s *snapshot) (*pkg, error) { if err != nil { return nil, err } - data := v.(*packageData) + data := v.(typeCheckResult) return data.pkg, data.err } @@ -290,7 +310,7 @@ func (ph *packageHandle) cached() (*pkg, error) { if v == nil { return nil, fmt.Errorf("no cached type information for %s", ph.m.PkgPath) } - data := v.(*packageData) + data := v.(typeCheckResult) return data.pkg, data.err } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 1ba945cf0ed..9f26b1e59cc 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -30,7 +30,6 @@ import ( "golang.org/x/mod/module" "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" - "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" @@ -175,11 +174,6 @@ func (s *snapshot) awaitHandle(ctx context.Context, h *memoize.Handle) (interfac return h.Get(ctx, s) } -type actionKey struct { - pkg packageKey - analyzer *analysis.Analyzer -} - // destroy waits for all leases on the snapshot to expire then releases // any resources (reference counts and files) associated with it. // Snapshots being destroyed can be awaited using v.destroyWG. @@ -771,34 +765,6 @@ func (s *snapshot) getImportedBy(id PackageID) []PackageID { return s.meta.importedBy[id] } -// addPackageHandle stores ph in the snapshot, or returns a pre-existing handle -// for the given package key, if it exists. -// -// An error is returned if the metadata used to build ph is no longer relevant. -// -// TODO(adonovan): inline sole use in buildPackageHandle. -func (s *snapshot) addPackageHandle(ph *packageHandle, release func()) (*packageHandle, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if s.meta.metadata[ph.m.ID].Metadata != ph.m.Metadata { - return nil, fmt.Errorf("stale metadata for %s", ph.m.ID) - } - - // If the package handle has already been cached, - // return the cached handle instead of overriding it. - if v, ok := s.packages.Get(ph.packageKey()); ok { - result := v.(*packageHandle) - release() - if result.m.Metadata != ph.m.Metadata { - return nil, bug.Errorf("existing package handle does not match for %s", ph.m.ID) - } - return result, nil - } - s.packages.Set(ph.packageKey(), ph, func(_, _ interface{}) { release() }) - return ph, nil -} - func (s *snapshot) workspacePackageIDs() (ids []PackageID) { s.mu.Lock() defer s.mu.Unlock() @@ -1202,63 +1168,6 @@ func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { return match } -// TODO(adonovan): inline sole use in buildPackageHandle. -func (s *snapshot) getPackage(id PackageID, mode source.ParseMode) *packageHandle { - s.mu.Lock() - defer s.mu.Unlock() - - key := packageKey{ - id: id, - mode: mode, - } - v, ok := s.packages.Get(key) - if !ok { - return nil - } - return v.(*packageHandle) -} - -func (s *snapshot) getActionHandle(id PackageID, m source.ParseMode, a *analysis.Analyzer) *actionHandle { - key := actionKey{ - pkg: packageKey{ - id: id, - mode: m, - }, - analyzer: a, - } - - s.mu.Lock() - defer s.mu.Unlock() - - ah, ok := s.actions.Get(key) - if !ok { - return nil - } - return ah.(*actionHandle) -} - -func (s *snapshot) addActionHandle(ah *actionHandle, release func()) *actionHandle { - key := actionKey{ - analyzer: ah.analyzer, - pkg: packageKey{ - id: ah.pkg.m.ID, - mode: ah.pkg.mode, - }, - } - - s.mu.Lock() - defer s.mu.Unlock() - - // If another thread since cached a different handle, - // return it instead of overriding it. - if result, ok := s.actions.Get(key); ok { - release() - return result.(*actionHandle) - } - s.actions.Set(key, ah, func(_, _ interface{}) { release() }) - return ah -} - func (s *snapshot) getIDsForURI(uri span.URI) []PackageID { s.mu.Lock() defer s.mu.Unlock() From 9b6c01892a361929559f87c2ba1b87825744596c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 1 Jul 2022 13:31:27 -0400 Subject: [PATCH 103/723] internal/lsp/cache: don't trim unexported struct fields The trimming optimization deletes parts of the syntax tree that don't affect the type checking of package-level declarations. It used to remove unexported struct fields, but this had observable consequences: it would affect the offset of later fields, and the size and aligment of structs, causing the 'fieldalignment' analyzer to report incorrect findings. Also, it required a complex workaround in the UI element for hovering over a type to account for the missing parts. This change restores unexported fields. The logic of recordFieldsUses has been inlined and specialized for each case (params+results, struct fields, interface methods) as they are more different than alike. BenchmarkMemStats on k8s shows +4% HeapAlloc: a lot, but a small part of the 32% saving of the trimming optimization as a whole. Also: - trimAST: delete func bodies without visiting them. - minor clarifications. Updates golang/go#51016 Change-Id: Ifae15564a8fb86af3ea186af351a2a92eb9deb22 Reviewed-on: https://go-review.googlesource.com/c/tools/+/415503 gopls-CI: kokoro Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/regtest/bench/bench_test.go | 2 +- internal/lsp/cache/check.go | 28 ++++--- internal/lsp/cache/parse.go | 96 ++++++++++++---------- internal/lsp/cache/parse_test.go | 2 +- 4 files changed, 71 insertions(+), 57 deletions(-) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index dfe41f65b1d..7f0da83fb37 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -201,7 +201,7 @@ func TestBenchmarkDidChange(t *testing.T) { // // Kubernetes example: // -// $ go test -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes +// $ go test -v -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes // TotalAlloc: 5766 MB // HeapAlloc: 1984 MB // diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index e51f86e5d9e..79a6ff3eeb6 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -590,20 +590,24 @@ func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHand // errors, as they may be completely nonsensical. pkg.hasFixedFiles = pkg.hasFixedFiles || pgf.Fixed } - if mode != source.ParseExported { - return nil - } - if astFilter != nil { - var files []*ast.File - for _, cgf := range pkg.compiledGoFiles { - files = append(files, cgf.File) - } - astFilter.Filter(files) - } else { - for _, cgf := range pkg.compiledGoFiles { - trimAST(cgf.File) + + // Optionally remove parts that don't affect the exported API. + if mode == source.ParseExported { + if astFilter != nil { + // aggressive pruning based on reachability + var files []*ast.File + for _, cgf := range pkg.compiledGoFiles { + files = append(files, cgf.File) + } + astFilter.Filter(files) + } else { + // simple trimming of function bodies + for _, cgf := range pkg.compiledGoFiles { + trimAST(cgf.File) + } } } + return nil } diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index 11075195330..62aea2229b4 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -279,6 +279,8 @@ func (f *unexportedFilter) filterSpec(spec ast.Spec) bool { } switch typ := spec.Type.(type) { case *ast.StructType: + // In practice this no longer filters anything; + // see comment at StructType case in recordUses. f.filterFieldList(typ.Fields) case *ast.InterfaceType: f.filterFieldList(typ.Methods) @@ -334,9 +336,19 @@ func (f *unexportedFilter) recordUses(file *ast.File) { case *ast.TypeSpec: switch typ := spec.Type.(type) { case *ast.StructType: - f.recordFieldUses(false, typ.Fields) + // We used to trim unexported fields but this + // had observable consequences. For example, + // the 'fieldalignment' analyzer would compute + // incorrect diagnostics from the size and + // offsets, and the UI hover information for + // types was inaccurate. So now we keep them. + if typ.Fields != nil { + for _, field := range typ.Fields.List { + f.recordIdents(field.Type) + } + } case *ast.InterfaceType: - f.recordFieldUses(false, typ.Methods) + f.recordInterfaceMethodUses(typ.Methods) } } } @@ -385,37 +397,32 @@ func (f *unexportedFilter) recordIdents(x ast.Expr) { } // recordFuncType records the types mentioned by a function type. -func (f *unexportedFilter) recordFuncType(x *ast.FuncType) { - f.recordFieldUses(true, x.Params) - f.recordFieldUses(true, x.Results) -} - -// recordFieldUses records unexported identifiers used in fields, which may be -// struct members, interface members, or function parameter/results. -func (f *unexportedFilter) recordFieldUses(isParams bool, fields *ast.FieldList) { - if fields == nil { - return - } - for _, field := range fields.List { - if isParams { - // Parameter types of retained functions need to be retained. +func (f *unexportedFilter) recordFuncType(fn *ast.FuncType) { + // Parameter and result types of retained functions need to be retained. + if fn.Params != nil { + for _, field := range fn.Params.List { f.recordIdents(field.Type) - continue - } - if ft, ok := field.Type.(*ast.FuncType); ok { - // Function declarations in interfaces need all their types retained. - f.recordFuncType(ft) - continue } - if len(field.Names) == 0 { - // Embedded fields might contribute exported names. + } + if fn.Results != nil { + for _, field := range fn.Results.List { f.recordIdents(field.Type) } - for _, name := range field.Names { - // We only need normal fields if they're exported. - if ast.IsExported(name.Name) { - f.recordIdents(field.Type) - break + } +} + +// recordInterfaceMethodUses records unexported identifiers used in interface methods. +func (f *unexportedFilter) recordInterfaceMethodUses(methods *ast.FieldList) { + if methods != nil { + for _, method := range methods.List { + if len(method.Names) == 0 { + // I, pkg.I, I[T] -- embedded interface: + // may contribute exported names. + f.recordIdents(method.Type) + } else if ft, ok := method.Type.(*ast.FuncType); ok { + // f(T) -- ordinary interface method: + // needs all its types retained. + f.recordFuncType(ft) } } } @@ -442,32 +449,35 @@ func (f *unexportedFilter) ProcessErrors(errors []types.Error) (map[string]bool, } // trimAST clears any part of the AST not relevant to type checking -// expressions at pos. +// the package-level declarations. func trimAST(file *ast.File) { - ast.Inspect(file, func(n ast.Node) bool { - if n == nil { - return false + // Eliminate bodies of top-level functions, methods, inits. + for _, decl := range file.Decls { + if fn, ok := decl.(*ast.FuncDecl); ok { + fn.Body = nil } + } + + // Simplify remaining declarations. + ast.Inspect(file, func(n ast.Node) bool { switch n := n.(type) { - case *ast.FuncDecl: - n.Body = nil - case *ast.BlockStmt: - n.List = nil - case *ast.CaseClause: - n.Body = nil - case *ast.CommClause: - n.Body = nil + case *ast.FuncLit: + // Eliminate bodies of literal functions. + // func() { ... } => func() {} + n.Body.List = nil case *ast.CompositeLit: // types.Info.Types for long slice/array literals are particularly - // expensive. Try to clear them out. + // expensive. Try to clear them out: T{e, ..., e} => T{} at, ok := n.Type.(*ast.ArrayType) if !ok { - // Composite literal. No harm removing all its fields. + // Map or struct literal: no harm removing all its fields. n.Elts = nil break } + // Removing the elements from an ellipsis array changes its type. // Try to set the length explicitly so we can continue. + // [...]T{e, ..., e} => [3]T[]{} if _, ok := at.Len.(*ast.Ellipsis); ok { length, ok := arrayLength(n) if !ok { diff --git a/internal/lsp/cache/parse_test.go b/internal/lsp/cache/parse_test.go index cb620f27432..e8db64530e6 100644 --- a/internal/lsp/cache/parse_test.go +++ b/internal/lsp/cache/parse_test.go @@ -149,7 +149,7 @@ type Exported struct { } var Var = Exported{foo:1} `, - kept: []string{"Exported", "Var"}, + kept: []string{"Exported", "Var", "x"}, }, { name: "drop_function_literals", From a7c53b59a64e59d60bbd08f4d0bd086aaa90b32e Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 13 Jul 2022 17:10:18 -0400 Subject: [PATCH 104/723] internal/analysisinternal: move FindBestMatch to internal/lsp/fuzzy This is used by internal/lsp/analysis/fillreturns and internal/lsp/analysis/fillstruct. This doesn't need to be in this analysisinternal package. This removes the dependency path go/analysis/internal/checker -> internal/analysisinternal -> internal/lsp/fuzzy Change-Id: I5db674ca30eb06ae6ce7021397cf5530a695af4e Reviewed-on: https://go-review.googlesource.com/c/tools/+/417418 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim --- internal/analysisinternal/analysis.go | 29 ------------------- .../lsp/analysis/fillreturns/fillreturns.go | 3 +- .../lsp/analysis/fillstruct/fillstruct.go | 3 +- internal/lsp/fuzzy/matcher.go | 28 ++++++++++++++++++ 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index 3f1e573342f..e32152ac223 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -12,8 +12,6 @@ import ( "go/token" "go/types" "strconv" - - "golang.org/x/tools/internal/lsp/fuzzy" ) // Flag to gate diagnostics for fuzz tests in 1.18. @@ -397,30 +395,3 @@ func equivalentTypes(want, got types.Type) bool { } return types.AssignableTo(want, got) } - -// FindBestMatch employs fuzzy matching to evaluate the similarity of each given identifier to the -// given pattern. We return the identifier whose name is most similar to the pattern. -func FindBestMatch(pattern string, idents []*ast.Ident) ast.Expr { - fuzz := fuzzy.NewMatcher(pattern) - var bestFuzz ast.Expr - highScore := float32(0) // minimum score is 0 (no match) - for _, ident := range idents { - // TODO: Improve scoring algorithm. - score := fuzz.Score(ident.Name) - if score > highScore { - highScore = score - bestFuzz = ident - } else if score == 0 { - // Order matters in the fuzzy matching algorithm. If we find no match - // when matching the target to the identifier, try matching the identifier - // to the target. - revFuzz := fuzzy.NewMatcher(ident.Name) - revScore := revFuzz.Score(pattern) - if revScore > highScore { - highScore = revScore - bestFuzz = ident - } - } - } - return bestFuzz -} diff --git a/internal/lsp/analysis/fillreturns/fillreturns.go b/internal/lsp/analysis/fillreturns/fillreturns.go index 72fe65d79ca..4a30934c63c 100644 --- a/internal/lsp/analysis/fillreturns/fillreturns.go +++ b/internal/lsp/analysis/fillreturns/fillreturns.go @@ -19,6 +19,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/lsp/fuzzy" "golang.org/x/tools/internal/typeparams" ) @@ -191,7 +192,7 @@ outer: // Find the identifier whose name is most similar to the return type. // If we do not find any identifier that matches the pattern, // generate a zero value. - value := analysisinternal.FindBestMatch(retTyp.String(), idents) + value := fuzzy.FindBestMatch(retTyp.String(), idents) if value == nil { value = analysisinternal.ZeroValue(file, pass.Pkg, retTyp) } diff --git a/internal/lsp/analysis/fillstruct/fillstruct.go b/internal/lsp/analysis/fillstruct/fillstruct.go index f160d4422ae..2103a55879e 100644 --- a/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/internal/lsp/analysis/fillstruct/fillstruct.go @@ -21,6 +21,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/lsp/fuzzy" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) @@ -254,7 +255,7 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast // Find the identifier whose name is most similar to the name of the field's key. // If we do not find any identifier that matches the pattern, generate a new value. // NOTE: We currently match on the name of the field key rather than the field type. - value := analysisinternal.FindBestMatch(obj.Field(i).Name(), idents) + value := fuzzy.FindBestMatch(obj.Field(i).Name(), idents) if value == nil { value = populateValue(file, pkg, fieldTyp) } diff --git a/internal/lsp/fuzzy/matcher.go b/internal/lsp/fuzzy/matcher.go index 265cdcf1604..92e1001fd12 100644 --- a/internal/lsp/fuzzy/matcher.go +++ b/internal/lsp/fuzzy/matcher.go @@ -8,6 +8,7 @@ package fuzzy import ( "bytes" "fmt" + "go/ast" ) const ( @@ -405,3 +406,30 @@ func (m *Matcher) poorMatch() bool { } return false } + +// FindBestMatch employs fuzzy matching to evaluate the similarity of each given identifier to the +// given pattern. We return the identifier whose name is most similar to the pattern. +func FindBestMatch(pattern string, idents []*ast.Ident) ast.Expr { + fuzz := NewMatcher(pattern) + var bestFuzz ast.Expr + highScore := float32(0) // minimum score is 0 (no match) + for _, ident := range idents { + // TODO: Improve scoring algorithm. + score := fuzz.Score(ident.Name) + if score > highScore { + highScore = score + bestFuzz = ident + } else if score == 0 { + // Order matters in the fuzzy matching algorithm. If we find no match + // when matching the target to the identifier, try matching the identifier + // to the target. + revFuzz := NewMatcher(ident.Name) + revScore := revFuzz.Score(pattern) + if revScore > highScore { + highScore = revScore + bestFuzz = ident + } + } + } + return bestFuzz +} From db8f89b397771c885c6218de3f383d800d72e62a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 13 Jul 2022 16:34:32 -0400 Subject: [PATCH 105/723] internal/memoize: rename Handle to Promise Also: - add test of NewHandle - update package doc and other doc comments - factor Store.Handle with NewHandle - declare Handle before Store Change-Id: I4bcea2c9debf1e77f973ef7ea9dbe2fd7a373996 Reviewed-on: https://go-review.googlesource.com/c/tools/+/417417 Auto-Submit: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- internal/lsp/cache/analysis.go | 10 +- internal/lsp/cache/check.go | 14 +- internal/lsp/cache/mod.go | 14 +- internal/lsp/cache/mod_tidy.go | 4 +- internal/lsp/cache/parse.go | 6 +- internal/lsp/cache/snapshot.go | 18 +- internal/lsp/cache/symbols.go | 6 +- internal/memoize/memoize.go | 300 ++++++++++++++++--------------- internal/memoize/memoize_test.go | 65 ++++--- 9 files changed, 231 insertions(+), 206 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index b277324a0ae..ee80bbcd529 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -70,7 +70,7 @@ type actionHandleKey source.Hash // package (as different analyzers are applied, either in sequence or // parallel), and across packages (as dependencies are analyzed). type actionHandle struct { - handle *memoize.Handle + promise *memoize.Promise analyzer *analysis.Analyzer pkg *pkg @@ -155,7 +155,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } } - handle, release := s.store.Handle(buildActionKey(a, ph), func(ctx context.Context, arg interface{}) interface{} { + promise, release := s.store.Promise(buildActionKey(a, ph), func(ctx context.Context, arg interface{}) interface{} { snapshot := arg.(*snapshot) // Analyze dependencies first. results, err := execAll(ctx, snapshot, deps) @@ -170,7 +170,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A ah := &actionHandle{ analyzer: a, pkg: pkg, - handle: handle, + promise: promise, } s.mu.Lock() @@ -188,7 +188,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } func (act *actionHandle) analyze(ctx context.Context, snapshot *snapshot) ([]*source.Diagnostic, interface{}, error) { - d, err := snapshot.awaitHandle(ctx, act.handle) + d, err := snapshot.awaitPromise(ctx, act.promise) if err != nil { return nil, nil, err } @@ -218,7 +218,7 @@ func execAll(ctx context.Context, snapshot *snapshot, actions []*actionHandle) ( for _, act := range actions { act := act g.Go(func() error { - v, err := snapshot.awaitHandle(ctx, act.handle) + v, err := snapshot.awaitPromise(ctx, act.promise) if err != nil { return err } diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index 79a6ff3eeb6..abc17245726 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -44,7 +44,7 @@ type packageHandleKey source.Hash // A packageHandle is a handle to the future result of type-checking a package. // The resulting package is obtained from the check() method. type packageHandle struct { - handle *memoize.Handle // [typeCheckResult] + promise *memoize.Promise // [typeCheckResult] // m is the metadata associated with the package. m *KnownMetadata @@ -141,7 +141,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so phKey := computePackageKey(m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) // TODO(adonovan): extract lambda into a standalone function to // avoid implicit lexical dependencies. - handle, release := s.store.Handle(phKey, func(ctx context.Context, arg interface{}) interface{} { + promise, release := s.store.Promise(phKey, func(ctx context.Context, arg interface{}) interface{} { snapshot := arg.(*snapshot) // Start type checking of direct dependencies, @@ -169,9 +169,9 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so }) ph := &packageHandle{ - handle: handle, - m: m, - key: phKey, + promise: promise, + m: m, + key: phKey, } s.mu.Lock() @@ -289,7 +289,7 @@ func hashConfig(config *packages.Config) source.Hash { } func (ph *packageHandle) check(ctx context.Context, s *snapshot) (*pkg, error) { - v, err := s.awaitHandle(ctx, ph.handle) + v, err := s.awaitPromise(ctx, ph.promise) if err != nil { return nil, err } @@ -306,7 +306,7 @@ func (ph *packageHandle) ID() string { } func (ph *packageHandle) cached() (*pkg, error) { - v := ph.handle.Cached() + v := ph.promise.Cached() if v == nil { return nil, fmt.Errorf("no cached type information for %s", ph.m.PkgPath) } diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index f9d148b7379..57fa1e2d0aa 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -39,19 +39,19 @@ func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source. // cache miss? if !hit { - handle, release := s.store.Handle(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { + promise, release := s.store.Promise(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { parsed, err := parseModImpl(ctx, fh) return parseModResult{parsed, err} }) - entry = handle + entry = promise s.mu.Lock() s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() }) s.mu.Unlock() } // Await result. - v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } @@ -116,7 +116,7 @@ func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source // cache miss? if !hit { - handle, release := s.store.Handle(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { + handle, release := s.store.Promise(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { parsed, err := parseWorkImpl(ctx, fh) return parseWorkResult{parsed, err} }) @@ -128,7 +128,7 @@ func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source } // Await result. - v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } @@ -223,7 +223,7 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string // cache miss? if !hit { - handle := memoize.NewHandle("modWhy", func(ctx context.Context, arg interface{}) interface{} { + handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg interface{}) interface{} { why, err := modWhyImpl(ctx, arg.(*snapshot), fh) return modWhyResult{why, err} }) @@ -235,7 +235,7 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string } // Await result. - v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index a04bacf8ee2..361f526ddfb 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -69,7 +69,7 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc return nil, err } - handle := memoize.NewHandle("modTidy", func(ctx context.Context, arg interface{}) interface{} { + handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} { tidied, err := modTidyImpl(ctx, arg.(*snapshot), uri.Filename(), pm) return modTidyResult{tidied, err} @@ -82,7 +82,7 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc } // Await result. - v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index 62aea2229b4..77e893a6681 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -58,7 +58,7 @@ func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode sourc // cache miss? if !hit { - handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} { + handle, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} { parsed, err := parseGoImpl(ctx, arg.(*snapshot).FileSet(), fh, mode) return parseGoResult{parsed, err} }) @@ -76,7 +76,7 @@ func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode sourc } // Await result. - v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } @@ -92,7 +92,7 @@ func (s *snapshot) peekParseGoLocked(fh source.FileHandle, mode source.ParseMode if !hit { return nil, nil // no-one has requested this file } - v := entry.(*memoize.Handle).Cached() + v := entry.(*memoize.Promise).Cached() if v == nil { return nil, nil // parsing is still in progress } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 9f26b1e59cc..9e52cda5aff 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -84,7 +84,7 @@ type snapshot struct { files filesMap // parsedGoFiles maps a parseKey to the handle of the future result of parsing it. - parsedGoFiles *persistent.Map // from parseKey to *memoize.Handle[parseGoResult] + parsedGoFiles *persistent.Map // from parseKey to *memoize.Promise[parseGoResult] // parseKeysByURI records the set of keys of parsedGoFiles that // need to be invalidated for each URI. @@ -94,7 +94,7 @@ type snapshot struct { // symbolizeHandles maps each file URI to a handle for the future // result of computing the symbols declared in that file. - symbolizeHandles *persistent.Map // from span.URI to *memoize.Handle[symbolizeResult] + symbolizeHandles *persistent.Map // from span.URI to *memoize.Promise[symbolizeResult] // packages maps a packageKey to a *packageHandle. // It may be invalidated when a file's content changes. @@ -103,7 +103,7 @@ type snapshot struct { // - packages.Get(id).m.Metadata == meta.metadata[id].Metadata for all ids // - if a package is in packages, then all of its dependencies should also // be in packages, unless there is a missing import - packages *persistent.Map // from packageKey to *memoize.Handle[*packageHandle] + packages *persistent.Map // from packageKey to *memoize.Promise[*packageHandle] // isActivePackageCache maps package ID to the cached value if it is active or not. // It may be invalidated when metadata changes or a new file is opened or closed. @@ -122,17 +122,17 @@ type snapshot struct { // parseModHandles keeps track of any parseModHandles for the snapshot. // The handles need not refer to only the view's go.mod file. - parseModHandles *persistent.Map // from span.URI to *memoize.Handle[parseModResult] + parseModHandles *persistent.Map // from span.URI to *memoize.Promise[parseModResult] // parseWorkHandles keeps track of any parseWorkHandles for the snapshot. // The handles need not refer to only the view's go.work file. - parseWorkHandles *persistent.Map // from span.URI to *memoize.Handle[parseWorkResult] + parseWorkHandles *persistent.Map // from span.URI to *memoize.Promise[parseWorkResult] // Preserve go.mod-related handles to avoid garbage-collecting the results // of various calls to the go command. The handles need not refer to only // the view's go.mod file. - modTidyHandles *persistent.Map // from span.URI to *memoize.Handle[modTidyResult] - modWhyHandles *persistent.Map // from span.URI to *memoize.Handle[modWhyResult] + modTidyHandles *persistent.Map // from span.URI to *memoize.Promise[modTidyResult] + modWhyHandles *persistent.Map // from span.URI to *memoize.Promise[modWhyResult] workspace *workspace // (not guarded by mu) @@ -170,8 +170,8 @@ func (s *snapshot) Acquire() func() { return s.refcount.Done } -func (s *snapshot) awaitHandle(ctx context.Context, h *memoize.Handle) (interface{}, error) { - return h.Get(ctx, s) +func (s *snapshot) awaitPromise(ctx context.Context, p *memoize.Promise) (interface{}, error) { + return p.Get(ctx, s) } // destroy waits for all leases on the snapshot to expire then releases diff --git a/internal/lsp/cache/symbols.go b/internal/lsp/cache/symbols.go index b562d5bbdd8..e98f554969c 100644 --- a/internal/lsp/cache/symbols.go +++ b/internal/lsp/cache/symbols.go @@ -35,12 +35,12 @@ func (s *snapshot) symbolize(ctx context.Context, fh source.FileHandle) ([]sourc if !hit { type symbolHandleKey source.Hash key := symbolHandleKey(fh.FileIdentity().Hash) - handle, release := s.store.Handle(key, func(_ context.Context, arg interface{}) interface{} { + promise, release := s.store.Promise(key, func(_ context.Context, arg interface{}) interface{} { symbols, err := symbolizeImpl(arg.(*snapshot), fh) return symbolizeResult{symbols, err} }) - entry = handle + entry = promise s.mu.Lock() s.symbolizeHandles.Set(uri, entry, func(_, _ interface{}) { release() }) @@ -48,7 +48,7 @@ func (s *snapshot) symbolize(ctx context.Context, fh source.FileHandle) ([]sourc } // Await result. - v, err := s.awaitHandle(ctx, entry.(*memoize.Handle)) + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 8c921c7e169..aa4d58d2f26 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -2,13 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package memoize supports memoizing the return values of functions with -// idempotent results that are expensive to compute. +// Package memoize defines a "promise" abstraction that enables +// memoization of the result of calling an expensive but idempotent +// function. // -// To use this package, create a Store, call its Handle method to -// acquire a handle to (aka a "promise" of) the future result of a -// function, and call Handle.Get to obtain the result. Get may block -// if the function has not finished (or started). +// Call p = NewPromise(f) to obtain a promise for the future result of +// calling f(), and call p.Get() to obtain that result. All calls to +// p.Get return the result of a single call of f(). +// Get blocks if the function has not finished (or started). +// +// A Store is a map of arbitrary keys to promises. Use Store.Promise +// to create a promise in the store. All calls to Handle(k) return the +// same promise as long as it is in the store. These promises are +// reference-counted and must be explicitly released. Once the last +// reference is released, the promise is removed from the store. package memoize import ( @@ -22,22 +29,13 @@ import ( "golang.org/x/tools/internal/xcontext" ) -// TODO(adonovan): rename Handle to Promise, and present it before Store. - -// Store binds keys to functions, returning handles that can be used to access -// the function's result. -type Store struct { - handlesMu sync.Mutex - handles map[interface{}]*Handle -} - // Function is the type of a function that can be memoized. // // If the arg is a RefCounted, its Acquire/Release operations are called. // // The argument must not materially affect the result of the function -// in ways that are not captured by the handle's key, since if -// Handle.Get is called twice concurrently, with the same (implicit) +// in ways that are not captured by the promise's key, since if +// Promise.Get is called twice concurrently, with the same (implicit) // key but different arguments, the Function is called only once but // its result must be suitable for both callers. // @@ -63,21 +61,13 @@ type RefCounted interface { Acquire() func() } -type state int - -const ( - stateIdle = iota // newly constructed, or last waiter was cancelled - stateRunning // start was called and not cancelled - stateCompleted // function call ran to completion -) - -// A Handle represents the future result of a call to a function. -type Handle struct { +// A Promise represents the future result of a call to a function. +type Promise struct { debug string // for observability - mu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu + mu sync.Mutex - // A Handle starts out IDLE, waiting for something to demand + // A Promise starts out IDLE, waiting for something to demand // its evaluation. It then transitions into RUNNING state. // // While RUNNING, waiters tracks the number of Get calls @@ -105,128 +95,78 @@ type Handle struct { refcount int32 // accessed using atomic load/store } -// Handle returns a reference-counted handle for the future result of -// calling the specified function. Calls to Handle with the same key -// return the same handle, and all calls to Handle.Get on a given -// handle return the same result but the function is called at most once. +// NewPromise returns a promise for the future result of calling the +// specified function. // -// The caller must call the returned function to decrement the -// handle's reference count when it is no longer needed. -func (store *Store) Handle(key interface{}, function Function) (*Handle, func()) { +// The debug string is used to classify promises in logs and metrics. +// It should be drawn from a small set. +func NewPromise(debug string, function Function) *Promise { if function == nil { panic("nil function") } - - store.handlesMu.Lock() - h, ok := store.handles[key] - if !ok { - // new handle - h = &Handle{ - function: function, - refcount: 1, - debug: reflect.TypeOf(key).String(), - } - - if store.handles == nil { - store.handles = map[interface{}]*Handle{} - } - store.handles[key] = h - } else { - // existing handle - atomic.AddInt32(&h.refcount, 1) - } - store.handlesMu.Unlock() - - release := func() { - if atomic.AddInt32(&h.refcount, -1) == 0 { - store.handlesMu.Lock() - delete(store.handles, key) - store.handlesMu.Unlock() - } - } - return h, release -} - -// Stats returns the number of each type of value in the store. -func (s *Store) Stats() map[reflect.Type]int { - result := map[reflect.Type]int{} - - s.handlesMu.Lock() - defer s.handlesMu.Unlock() - - for k := range s.handles { - result[reflect.TypeOf(k)]++ - } - return result -} - -// DebugOnlyIterate iterates through all live cache entries and calls f on them. -// It should only be used for debugging purposes. -func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { - s.handlesMu.Lock() - defer s.handlesMu.Unlock() - - for k, h := range s.handles { - if v := h.Cached(); v != nil { - f(k, v) - } - } -} - -// NewHandle returns a handle for the future result of calling the -// specified function. -// -// The debug string is used to classify handles in logs and metrics. -// It should be drawn from a small set. -func NewHandle(debug string, function Function) *Handle { - return &Handle{ + return &Promise{ debug: debug, function: function, } } -// Cached returns the value associated with a handle. +type state int + +const ( + stateIdle = iota // newly constructed, or last waiter was cancelled + stateRunning // start was called and not cancelled + stateCompleted // function call ran to completion +) + +// Cached returns the value associated with a promise. // // It will never cause the value to be generated. // It will return the cached value, if present. -func (h *Handle) Cached() interface{} { - h.mu.Lock() - defer h.mu.Unlock() - if h.state == stateCompleted { - return h.value +func (p *Promise) Cached() interface{} { + p.mu.Lock() + defer p.mu.Unlock() + if p.state == stateCompleted { + return p.value } return nil } -// Get returns the value associated with a handle. +// Get returns the value associated with a promise. +// +// All calls to Promise.Get on a given promise return the +// same result but the function is called (to completion) at most once. // // If the value is not yet ready, the underlying function will be invoked. +// // If ctx is cancelled, Get returns (nil, Canceled). -func (h *Handle) Get(ctx context.Context, arg interface{}) (interface{}, error) { +// If all concurrent calls to Get are cancelled, the context provided +// to the function is cancelled. A later call to Get may attempt to +// call the function again. +func (p *Promise) Get(ctx context.Context, arg interface{}) (interface{}, error) { if ctx.Err() != nil { return nil, ctx.Err() } - h.mu.Lock() - switch h.state { + p.mu.Lock() + switch p.state { case stateIdle: - return h.run(ctx, arg) + return p.run(ctx, arg) case stateRunning: - return h.wait(ctx) + return p.wait(ctx) case stateCompleted: - defer h.mu.Unlock() - return h.value, nil + defer p.mu.Unlock() + return p.value, nil default: panic("unknown state") } } -// run starts h.function and returns the result. h.mu must be locked. -func (h *Handle) run(ctx context.Context, arg interface{}) (interface{}, error) { +// run starts p.function and returns the result. p.mu must be locked. +func (p *Promise) run(ctx context.Context, arg interface{}) (interface{}, error) { childCtx, cancel := context.WithCancel(xcontext.Detach(ctx)) - h.cancel = cancel - h.state = stateRunning - h.done = make(chan struct{}) - function := h.function // Read under the lock + p.cancel = cancel + p.state = stateRunning + p.done = make(chan struct{}) + function := p.function // Read under the lock // Make sure that the argument isn't destroyed while we're running in it. release := func() {} @@ -235,7 +175,7 @@ func (h *Handle) run(ctx context.Context, arg interface{}) (interface{}, error) } go func() { - trace.WithRegion(childCtx, fmt.Sprintf("Handle.run %s", h.debug), func() { + trace.WithRegion(childCtx, fmt.Sprintf("Promise.run %s", p.debug), func() { defer release() // Just in case the function does something expensive without checking // the context, double-check we're still alive. @@ -247,51 +187,115 @@ func (h *Handle) run(ctx context.Context, arg interface{}) (interface{}, error) return } - h.mu.Lock() - defer h.mu.Unlock() - // It's theoretically possible that the handle has been cancelled out + p.mu.Lock() + defer p.mu.Unlock() + // It's theoretically possible that the promise has been cancelled out // of the run that started us, and then started running again since we // checked childCtx above. Even so, that should be harmless, since each // run should produce the same results. - if h.state != stateRunning { + if p.state != stateRunning { return } - h.value = v - h.function = nil // aid GC - h.state = stateCompleted - close(h.done) + p.value = v + p.function = nil // aid GC + p.state = stateCompleted + close(p.done) }) }() - return h.wait(ctx) + return p.wait(ctx) } -// wait waits for the value to be computed, or ctx to be cancelled. h.mu must be locked. -func (h *Handle) wait(ctx context.Context) (interface{}, error) { - h.waiters++ - done := h.done - h.mu.Unlock() +// wait waits for the value to be computed, or ctx to be cancelled. p.mu must be locked. +func (p *Promise) wait(ctx context.Context) (interface{}, error) { + p.waiters++ + done := p.done + p.mu.Unlock() select { case <-done: - h.mu.Lock() - defer h.mu.Unlock() - if h.state == stateCompleted { - return h.value, nil + p.mu.Lock() + defer p.mu.Unlock() + if p.state == stateCompleted { + return p.value, nil } return nil, nil case <-ctx.Done(): - h.mu.Lock() - defer h.mu.Unlock() - h.waiters-- - if h.waiters == 0 && h.state == stateRunning { - h.cancel() - close(h.done) - h.state = stateIdle - h.done = nil - h.cancel = nil + p.mu.Lock() + defer p.mu.Unlock() + p.waiters-- + if p.waiters == 0 && p.state == stateRunning { + p.cancel() + close(p.done) + p.state = stateIdle + p.done = nil + p.cancel = nil } return nil, ctx.Err() } } + +// A Store maps arbitrary keys to reference-counted promises. +type Store struct { + promisesMu sync.Mutex + promises map[interface{}]*Promise +} + +// Promise returns a reference-counted promise for the future result of +// calling the specified function. +// +// Calls to Promise with the same key return the same promise, +// incrementing its reference count. The caller must call the +// returned function to decrement the promise's reference count when +// it is no longer needed. Once the last reference has been released, +// the promise is removed from the store. +func (store *Store) Promise(key interface{}, function Function) (*Promise, func()) { + store.promisesMu.Lock() + p, ok := store.promises[key] + if !ok { + p = NewPromise(reflect.TypeOf(key).String(), function) + if store.promises == nil { + store.promises = map[interface{}]*Promise{} + } + store.promises[key] = p + } + atomic.AddInt32(&p.refcount, 1) + store.promisesMu.Unlock() + + release := func() { + if atomic.AddInt32(&p.refcount, -1) == 0 { + store.promisesMu.Lock() + delete(store.promises, key) + store.promisesMu.Unlock() + } + } + return p, release +} + +// Stats returns the number of each type of key in the store. +func (s *Store) Stats() map[reflect.Type]int { + result := map[reflect.Type]int{} + + s.promisesMu.Lock() + defer s.promisesMu.Unlock() + + for k := range s.promises { + result[reflect.TypeOf(k)]++ + } + return result +} + +// DebugOnlyIterate iterates through the store and, for each completed +// promise, calls f(k, v) for the map key k and function result v. It +// should only be used for debugging purposes. +func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { + s.promisesMu.Lock() + defer s.promisesMu.Unlock() + + for k, p := range s.promises { + if v := p.Cached(); v != nil { + f(k, v) + } + } +} diff --git a/internal/memoize/memoize_test.go b/internal/memoize/memoize_test.go index bde02bf6136..3550f1eb144 100644 --- a/internal/memoize/memoize_test.go +++ b/internal/memoize/memoize_test.go @@ -18,7 +18,7 @@ func TestGet(t *testing.T) { evaled := 0 - h, release := store.Handle("key", func(context.Context, interface{}) interface{} { + h, release := store.Promise("key", func(context.Context, interface{}) interface{} { evaled++ return "res" }) @@ -30,7 +30,7 @@ func TestGet(t *testing.T) { } } -func expectGet(t *testing.T, h *memoize.Handle, wantV interface{}) { +func expectGet(t *testing.T, h *memoize.Promise, wantV interface{}) { t.Helper() gotV, gotErr := h.Get(context.Background(), nil) if gotV != wantV || gotErr != nil { @@ -38,29 +38,50 @@ func expectGet(t *testing.T, h *memoize.Handle, wantV interface{}) { } } -func TestHandleRefCounting(t *testing.T) { +func TestNewPromise(t *testing.T) { + calls := 0 + f := func(context.Context, interface{}) interface{} { + calls++ + return calls + } + + // All calls to Get on the same promise return the same result. + p1 := memoize.NewPromise("debug", f) + expectGet(t, p1, 1) + expectGet(t, p1, 1) + + // A new promise calls the function again. + p2 := memoize.NewPromise("debug", f) + expectGet(t, p2, 2) + expectGet(t, p2, 2) + + // The original promise is unchanged. + expectGet(t, p1, 1) +} + +func TestStoredPromiseRefCounting(t *testing.T) { var store memoize.Store v1 := false v2 := false - h1, release1 := store.Handle("key1", func(context.Context, interface{}) interface{} { + p1, release1 := store.Promise("key1", func(context.Context, interface{}) interface{} { return &v1 }) - h2, release2 := store.Handle("key2", func(context.Context, interface{}) interface{} { + p2, release2 := store.Promise("key2", func(context.Context, interface{}) interface{} { return &v2 }) - expectGet(t, h1, &v1) - expectGet(t, h2, &v2) + expectGet(t, p1, &v1) + expectGet(t, p2, &v2) - expectGet(t, h1, &v1) - expectGet(t, h2, &v2) + expectGet(t, p1, &v1) + expectGet(t, p2, &v2) - h2Copy, release2Copy := store.Handle("key2", func(context.Context, interface{}) interface{} { + p2Copy, release2Copy := store.Promise("key2", func(context.Context, interface{}) interface{} { return &v1 }) - if h2 != h2Copy { - t.Error("NewHandle returned a new value while old is not destroyed yet") + if p2 != p2Copy { + t.Error("Promise returned a new value while old is not destroyed yet") } - expectGet(t, h2Copy, &v2) + expectGet(t, p2Copy, &v2) release2() if got, want := v2, false; got != want { @@ -72,23 +93,23 @@ func TestHandleRefCounting(t *testing.T) { } release1() - h2Copy, release2Copy = store.Handle("key2", func(context.Context, interface{}) interface{} { + p2Copy, release2Copy = store.Promise("key2", func(context.Context, interface{}) interface{} { return &v2 }) - if h2 == h2Copy { - t.Error("NewHandle returned previously destroyed value") + if p2 == p2Copy { + t.Error("Promise returned previously destroyed value") } release2Copy() } -func TestHandleDestroyedWhileRunning(t *testing.T) { - // Test that calls to Handle.Get return even if the handle is destroyed while running. +func TestPromiseDestroyedWhileRunning(t *testing.T) { + // Test that calls to Promise.Get return even if the promise is destroyed while running. var store memoize.Store c := make(chan int) var v int - h, release := store.Handle("key", func(ctx context.Context, _ interface{}) interface{} { + h, release := store.Promise("key", func(ctx context.Context, _ interface{}) interface{} { <-c <-c if err := ctx.Err(); err != nil { @@ -109,9 +130,9 @@ func TestHandleDestroyedWhileRunning(t *testing.T) { wg.Done() }() - c <- 0 // send once to enter the handle function - release() // release before the handle function returns - c <- 0 // let the handle function proceed + c <- 0 // send once to enter the promise function + release() // release before the promise function returns + c <- 0 // let the promise function proceed wg.Wait() From 1a4e02fee4d3f223c0b460ad15d69e630b25c0ec Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Wed, 23 Mar 2022 01:19:43 +0100 Subject: [PATCH 106/723] internal/lsp/analysis/unusedvariable: add analyzer This analyzer suggests fixes for unused variable errors. In declarations it will remove the whole statement if the offending variable is the only one declared in that statement, otherwise it will just delete the offending variable. In assignments it will remove the whole statement if the offending variable is the only one assigned in that statement, otherwise it will rename the offending variable to `_`. If the assignment RHS contains a statement that can cause a side effect (a function call or reading from a channel), the assignment will be removed but RHS will be preserved. Fixes golang/go#48975 Change-Id: I3850f1b0340cd5ae63249931df3a5403d8617080 Reviewed-on: https://go-review.googlesource.com/c/tools/+/394934 Reviewed-by: Alan Donovan Reviewed-by: Robert Findley --- gopls/doc/analyzers.md | 9 + .../unusedvariable/testdata/src/assign/a.go | 74 +++++ .../testdata/src/assign/a.go.golden | 59 ++++ .../unusedvariable/testdata/src/decl/a.go | 30 ++ .../testdata/src/decl/a.go.golden | 24 ++ .../analysis/unusedvariable/unusedvariable.go | 300 ++++++++++++++++++ .../unusedvariable/unusedvariable_test.go | 24 ++ internal/lsp/source/api_json.go | 9 + internal/lsp/source/options.go | 8 + 9 files changed, 537 insertions(+) create mode 100644 internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go create mode 100644 internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden create mode 100644 internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go create mode 100644 internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden create mode 100644 internal/lsp/analysis/unusedvariable/unusedvariable.go create mode 100644 internal/lsp/analysis/unusedvariable/unusedvariable_test.go diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index fd65c3a2a9d..861be1ca62c 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -657,6 +657,15 @@ func <>(inferred parameters) { **Enabled by default.** +## **unusedvariable** + +check for unused variables + +The unusedvariable analyzer suggests fixes for unused variables errors. + + +**Disabled by default. Enable it by setting `"analyses": {"unusedvariable": true}`.** + ## **fillstruct** note incomplete struct initializations diff --git a/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go b/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go new file mode 100644 index 00000000000..eccfe14c3aa --- /dev/null +++ b/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go @@ -0,0 +1,74 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package a + +import ( + "fmt" + "os" +) + +type A struct { + b int +} + +func singleAssignment() { + v := "s" // want `v declared but not used` + + s := []int{ // want `s declared but not used` + 1, + 2, + } + + a := func(s string) bool { // want `a declared but not used` + return false + } + + if 1 == 1 { + s := "v" // want `s declared but not used` + } + + panic("I should survive") +} + +func noOtherStmtsInBlock() { + v := "s" // want `v declared but not used` +} + +func partOfMultiAssignment() { + f, err := os.Open("file") // want `f declared but not used` + panic(err) +} + +func sideEffects(cBool chan bool, cInt chan int) { + b := <-c // want `b declared but not used` + s := fmt.Sprint("") // want `s declared but not used` + a := A{ // want `a declared but not used` + b: func() int { + return 1 + }(), + } + c := A{<-cInt} // want `c declared but not used` + d := fInt() + <-cInt // want `d declared but not used` + e := fBool() && <-cBool // want `e declared but not used` + f := map[int]int{ // want `f declared but not used` + fInt(): <-cInt, + } + g := []int{<-cInt} // want `g declared but not used` + h := func(s string) {} // want `h declared but not used` + i := func(s string) {}() // want `i declared but not used` +} + +func commentAbove() { + // v is a variable + v := "s" // want `v declared but not used` +} + +func fBool() bool { + return true +} + +func fInt() int { + return 1 +} diff --git a/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden b/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden new file mode 100644 index 00000000000..8d6e561fa60 --- /dev/null +++ b/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden @@ -0,0 +1,59 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package a + +import ( + "fmt" + "os" +) + +type A struct { + b int +} + +func singleAssignment() { + if 1 == 1 { + } + + panic("I should survive") +} + +func noOtherStmtsInBlock() { +} + +func partOfMultiAssignment() { + _, err := os.Open("file") // want `f declared but not used` + panic(err) +} + +func sideEffects(cBool chan bool, cInt chan int) { + <-c // want `b declared but not used` + fmt.Sprint("") // want `s declared but not used` + A{ // want `a declared but not used` + b: func() int { + return 1 + }(), + } + A{<-cInt} // want `c declared but not used` + fInt() + <-cInt // want `d declared but not used` + fBool() && <-cBool // want `e declared but not used` + map[int]int{ // want `f declared but not used` + fInt(): <-cInt, + } + []int{<-cInt} // want `g declared but not used` + func(s string) {}() // want `i declared but not used` +} + +func commentAbove() { + // v is a variable +} + +func fBool() bool { + return true +} + +func fInt() int { + return 1 +} diff --git a/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go b/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go new file mode 100644 index 00000000000..024e49db9c4 --- /dev/null +++ b/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go @@ -0,0 +1,30 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package decl + +func a() { + var b, c bool // want `b declared but not used` + panic(c) + + if 1 == 1 { + var s string // want `s declared but not used` + } +} + +func b() { + // b is a variable + var b bool // want `b declared but not used` +} + +func c() { + var ( + d string + + // some comment for c + c bool // want `c declared but not used` + ) + + panic(d) +} diff --git a/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden b/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden new file mode 100644 index 00000000000..a589a47af1f --- /dev/null +++ b/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden @@ -0,0 +1,24 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package decl + +func a() { + var c bool // want `b declared but not used` + panic(c) + + if 1 == 1 { + } +} + +func b() { + // b is a variable +} + +func c() { + var ( + d string + ) + panic(d) +} diff --git a/internal/lsp/analysis/unusedvariable/unusedvariable.go b/internal/lsp/analysis/unusedvariable/unusedvariable.go new file mode 100644 index 00000000000..47564f1f154 --- /dev/null +++ b/internal/lsp/analysis/unusedvariable/unusedvariable.go @@ -0,0 +1,300 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package unusedvariable defines an analyzer that checks for unused variables. +package unusedvariable + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/internal/analysisinternal" +) + +const Doc = `check for unused variables + +The unusedvariable analyzer suggests fixes for unused variables errors. +` + +var Analyzer = &analysis.Analyzer{ + Name: "unusedvariable", + Doc: Doc, + Requires: []*analysis.Analyzer{}, + Run: run, + RunDespiteErrors: true, // an unusedvariable diagnostic is a compile error +} + +type fixesForError map[types.Error][]analysis.SuggestedFix + +const unusedVariableSuffix = " declared but not used" + +func run(pass *analysis.Pass) (interface{}, error) { + for _, typeErr := range analysisinternal.GetTypeErrors(pass) { + if strings.HasSuffix(typeErr.Msg, unusedVariableSuffix) { + varName := strings.TrimSuffix(typeErr.Msg, unusedVariableSuffix) + err := runForError(pass, typeErr, varName) + if err != nil { + return nil, err + } + } + } + + return nil, nil +} + +func runForError(pass *analysis.Pass, err types.Error, name string) error { + var file *ast.File + for _, f := range pass.Files { + if f.Pos() <= err.Pos && err.Pos < f.End() { + file = f + break + } + } + if file == nil { + return nil + } + + path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos) + if len(path) < 2 { + return nil + } + + ident, ok := path[0].(*ast.Ident) + if !ok || ident.Name != name { + return nil + } + + diag := analysis.Diagnostic{ + Pos: ident.Pos(), + End: ident.End(), + Message: err.Msg, + } + + for i := range path { + switch stmt := path[i].(type) { + case *ast.ValueSpec: + // Find GenDecl to which offending ValueSpec belongs. + if decl, ok := path[i+1].(*ast.GenDecl); ok { + fixes := removeVariableFromSpec(pass, path, stmt, decl, ident) + // fixes may be nil + if len(fixes) > 0 { + diag.SuggestedFixes = fixes + pass.Report(diag) + } + } + + case *ast.AssignStmt: + if stmt.Tok != token.DEFINE { + continue + } + + containsIdent := false + for _, expr := range stmt.Lhs { + if expr == ident { + containsIdent = true + } + } + if !containsIdent { + continue + } + + fixes := removeVariableFromAssignment(pass, path, stmt, ident) + // fixes may be nil + if len(fixes) > 0 { + diag.SuggestedFixes = fixes + pass.Report(diag) + } + } + } + + return nil +} + +func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.ValueSpec, decl *ast.GenDecl, ident *ast.Ident) []analysis.SuggestedFix { + newDecl := new(ast.GenDecl) + *newDecl = *decl + newDecl.Specs = nil + + for _, spec := range decl.Specs { + if spec != stmt { + newDecl.Specs = append(newDecl.Specs, spec) + continue + } + + newSpec := new(ast.ValueSpec) + *newSpec = *stmt + newSpec.Names = nil + + for _, n := range stmt.Names { + if n != ident { + newSpec.Names = append(newSpec.Names, n) + } + } + + if len(newSpec.Names) > 0 { + newDecl.Specs = append(newDecl.Specs, newSpec) + } + } + + // decl.End() does not include any comments, so if a comment is present we + // need to account for it when we delete the statement + end := decl.End() + if stmt.Comment != nil && stmt.Comment.End() > end { + end = stmt.Comment.End() + } + + // There are no other specs left in the declaration, the whole statement can + // be deleted + if len(newDecl.Specs) == 0 { + // Find parent DeclStmt and delete it + for _, node := range path { + if declStmt, ok := node.(*ast.DeclStmt); ok { + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: deleteStmtFromBlock(path, declStmt), + }, + } + } + } + } + + var b bytes.Buffer + if err := format.Node(&b, pass.Fset, newDecl); err != nil { + return nil + } + + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: []analysis.TextEdit{ + { + Pos: decl.Pos(), + // Avoid adding a new empty line + End: end + 1, + NewText: b.Bytes(), + }, + }, + }, + } +} + +func removeVariableFromAssignment(pass *analysis.Pass, path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix { + // The only variable in the assignment is unused + if len(stmt.Lhs) == 1 { + // If LHS has only one expression to be valid it has to have 1 expression + // on RHS + // + // RHS may have side effects, preserve RHS + if exprMayHaveSideEffects(stmt.Rhs[0]) { + // Delete until RHS + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: []analysis.TextEdit{ + { + Pos: ident.Pos(), + End: stmt.Rhs[0].Pos(), + }, + }, + }, + } + } + + // RHS does not have any side effects, delete the whole statement + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: deleteStmtFromBlock(path, stmt), + }, + } + } + + // Otherwise replace ident with `_` + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: []analysis.TextEdit{ + { + Pos: ident.Pos(), + End: ident.End(), + NewText: []byte("_"), + }, + }, + }, + } +} + +func suggestedFixMessage(name string) string { + return fmt.Sprintf("Remove variable %s", name) +} + +func deleteStmtFromBlock(path []ast.Node, stmt ast.Stmt) []analysis.TextEdit { + // Find innermost enclosing BlockStmt. + var block *ast.BlockStmt + for i := range path { + if blockStmt, ok := path[i].(*ast.BlockStmt); ok { + block = blockStmt + break + } + } + + nodeIndex := -1 + for i, blockStmt := range block.List { + if blockStmt == stmt { + nodeIndex = i + break + } + } + + // The statement we need to delete was not found in BlockStmt + if nodeIndex == -1 { + return nil + } + + // Delete until the end of the block unless there is another statement after + // the one we are trying to delete + end := block.Rbrace + if nodeIndex < len(block.List)-1 { + end = block.List[nodeIndex+1].Pos() + } + + return []analysis.TextEdit{ + { + Pos: stmt.Pos(), + End: end, + }, + } +} + +// exprMayHaveSideEffects reports whether the expression may have side effects +// (because it contains a function call or channel receive). We disregard +// runtime panics as well written programs should not encounter them. +func exprMayHaveSideEffects(expr ast.Expr) bool { + var mayHaveSideEffects bool + ast.Inspect(expr, func(n ast.Node) bool { + switch n := n.(type) { + case *ast.CallExpr: // possible function call + mayHaveSideEffects = true + return false + case *ast.UnaryExpr: + if n.Op == token.ARROW { // channel receive + mayHaveSideEffects = true + return false + } + case *ast.FuncLit: + return false // evaluating what's inside a FuncLit has no effect + } + return true + }) + + return mayHaveSideEffects +} diff --git a/internal/lsp/analysis/unusedvariable/unusedvariable_test.go b/internal/lsp/analysis/unusedvariable/unusedvariable_test.go new file mode 100644 index 00000000000..e6d7c020f04 --- /dev/null +++ b/internal/lsp/analysis/unusedvariable/unusedvariable_test.go @@ -0,0 +1,24 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package unusedvariable_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/internal/lsp/analysis/unusedvariable" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + + t.Run("decl", func(t *testing.T) { + analysistest.RunWithSuggestedFixes(t, testdata, unusedvariable.Analyzer, "decl") + }) + + t.Run("assign", func(t *testing.T) { + analysistest.RunWithSuggestedFixes(t, testdata, unusedvariable.Analyzer, "assign") + }) +} diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 4e2183cf4e6..2493da25d8c 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -433,6 +433,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "suggested fixes for \"undeclared name: <>\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will either insert a new statement,\nsuch as:\n\n\"<> := \"\n\nor a new function declaration, such as:\n\nfunc <>(inferred parameters) {\n\tpanic(\"implement me!\")\n}\n", Default: "true", }, + { + Name: "\"unusedvariable\"", + Doc: "check for unused variables\n\nThe unusedvariable analyzer suggests fixes for unused variables errors.\n", + Default: "false", + }, { Name: "\"fillstruct\"", Doc: "note incomplete struct initializations\n\nThis analyzer provides diagnostics for any struct literals that do not have\nany fields initialized. Because the suggested fix for this analysis is\nexpensive to compute, callers should compute it separately, using the\nSuggestedFix function below.\n", @@ -1013,6 +1018,10 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "suggested fixes for \"undeclared name: <>\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will either insert a new statement,\nsuch as:\n\n\"<> := \"\n\nor a new function declaration, such as:\n\nfunc <>(inferred parameters) {\n\tpanic(\"implement me!\")\n}\n", Default: true, }, + { + Name: "unusedvariable", + Doc: "check for unused variables\n\nThe unusedvariable analyzer suggests fixes for unused variables errors.\n", + }, { Name: "fillstruct", Doc: "note incomplete struct initializations\n\nThis analyzer provides diagnostics for any struct literals that do not have\nany fields initialized. Because the suggested fix for this analysis is\nexpensive to compute, callers should compute it separately, using the\nSuggestedFix function below.\n", diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 5da14ebfe92..09401bc6ef8 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -61,6 +61,7 @@ import ( "golang.org/x/tools/internal/lsp/analysis/stubmethods" "golang.org/x/tools/internal/lsp/analysis/undeclaredname" "golang.org/x/tools/internal/lsp/analysis/unusedparams" + "golang.org/x/tools/internal/lsp/analysis/unusedvariable" "golang.org/x/tools/internal/lsp/analysis/useany" "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/diff" @@ -800,6 +801,9 @@ func (o *Options) enableAllExperimentMaps() { if _, ok := o.Analyses[unusedparams.Analyzer.Name]; !ok { o.Analyses[unusedparams.Analyzer.Name] = true } + if _, ok := o.Analyses[unusedvariable.Analyzer.Name]; !ok { + o.Analyses[unusedvariable.Analyzer.Name] = true + } } func (o *Options) set(name string, value interface{}, seen map[string]struct{}) OptionResult { @@ -1270,6 +1274,10 @@ func typeErrorAnalyzers() map[string]*Analyzer { Fix: UndeclaredName, Enabled: true, }, + unusedvariable.Analyzer.Name: { + Analyzer: unusedvariable.Analyzer, + Enabled: false, + }, } } From c3af7c2fa9473d5bc80d0f62e3067027afd7ccd7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 15 Jul 2022 08:31:52 -0400 Subject: [PATCH 107/723] internal/lsp/cache: delete workspacePackageHandles (dead code) This should have been in CL 417116. Also: - (related to CL 417415), rename packageHandle.check to await to indicate its blocking nature. - rename typeCheck to typeCheckImpl, following the pattern. - move "prefetch" parallel loop into typeCheckImpl. - add some comments. Change-Id: Iea2c8e1f1f74fb65afd0759b493509147d87a4bb Reviewed-on: https://go-review.googlesource.com/c/tools/+/417581 Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro --- internal/lsp/cache/analysis.go | 2 +- internal/lsp/cache/check.go | 66 ++++++++++++++++++---------------- internal/lsp/cache/snapshot.go | 23 +++--------- 3 files changed, 40 insertions(+), 51 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index ee80bbcd529..ca0e04d64b0 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -123,7 +123,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A if err != nil { return nil, err } - pkg, err := ph.check(ctx, s) + pkg, err := ph.await(ctx, s) if err != nil { return nil, err } diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index abc17245726..4caf4ba6fa7 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -42,7 +42,7 @@ type packageKey struct { type packageHandleKey source.Hash // A packageHandle is a handle to the future result of type-checking a package. -// The resulting package is obtained from the check() method. +// The resulting package is obtained from the await() method. type packageHandle struct { promise *memoize.Promise // [typeCheckResult] @@ -60,7 +60,7 @@ type packageHandle struct { } // typeCheckResult contains the result of a call to -// typeCheck, which type-checks a package. +// typeCheckImpl, which type-checks a package. type typeCheckResult struct { pkg *pkg err error @@ -130,6 +130,12 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so } // Read both lists of files of this package, in parallel. + // + // goFiles aren't presented to the type checker--nor + // are they included in the key, unsoundly--but their + // syntax trees are available from (*pkg).File(URI). + // TODO(adonovan): consider parsing them on demand? + // The need should be rare. goFiles, compiledGoFiles, err := readGoFiles(ctx, s, m.Metadata) if err != nil { return nil, err @@ -139,32 +145,9 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // Create a handle for the result of type checking. experimentalKey := s.View().Options().ExperimentalPackageCacheKey phKey := computePackageKey(m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) - // TODO(adonovan): extract lambda into a standalone function to - // avoid implicit lexical dependencies. promise, release := s.store.Promise(phKey, func(ctx context.Context, arg interface{}) interface{} { - snapshot := arg.(*snapshot) - - // Start type checking of direct dependencies, - // in parallel and asynchronously. - // As the type checker imports each of these - // packages, it will wait for its completion. - var wg sync.WaitGroup - for _, dep := range deps { - wg.Add(1) - go func(dep *packageHandle) { - dep.check(ctx, snapshot) // ignore result - wg.Done() - }(dep) - } - // The 'defer' below is unusual but intentional: - // it is not necessary that each call to dep.check - // complete before type checking begins, as the type - // checker will wait for those it needs. But they do - // need to complete before this function returns and - // the snapshot is possibly destroyed. - defer wg.Wait() - - pkg, err := typeCheck(ctx, snapshot, goFiles, compiledGoFiles, m.Metadata, mode, deps) + + pkg, err := typeCheckImpl(ctx, arg.(*snapshot), goFiles, compiledGoFiles, m.Metadata, mode, deps) return typeCheckResult{pkg, err} }) @@ -288,7 +271,8 @@ func hashConfig(config *packages.Config) source.Hash { return source.HashOf(b.Bytes()) } -func (ph *packageHandle) check(ctx context.Context, s *snapshot) (*pkg, error) { +// await waits for typeCheckImpl to complete and returns its result. +func (ph *packageHandle) await(ctx context.Context, s *snapshot) (*pkg, error) { v, err := s.awaitPromise(ctx, ph.promise) if err != nil { return nil, err @@ -314,10 +298,30 @@ func (ph *packageHandle) cached() (*pkg, error) { return data.pkg, data.err } -// typeCheck type checks the parsed source files in compiledGoFiles. +// typeCheckImpl type checks the parsed source files in compiledGoFiles. // (The resulting pkg also holds the parsed but not type-checked goFiles.) // deps holds the future results of type-checking the direct dependencies. -func typeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle) (*pkg, error) { +func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle) (*pkg, error) { + // Start type checking of direct dependencies, + // in parallel and asynchronously. + // As the type checker imports each of these + // packages, it will wait for its completion. + var wg sync.WaitGroup + for _, dep := range deps { + wg.Add(1) + go func(dep *packageHandle) { + dep.await(ctx, snapshot) // ignore result + wg.Done() + }(dep) + } + // The 'defer' below is unusual but intentional: + // it is not necessary that each call to dep.check + // complete before type checking begins, as the type + // checker will wait for those it needs. But they do + // need to complete before this function returns and + // the snapshot is possibly destroyed. + defer wg.Wait() + var filter *unexportedFilter if mode == source.ParseExported { filter = &unexportedFilter{uses: map[string]bool{}} @@ -522,7 +526,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil if !source.IsValidImport(string(m.PkgPath), string(dep.m.PkgPath)) { return nil, fmt.Errorf("invalid use of internal package %s", pkgPath) } - depPkg, err := dep.check(ctx, snapshot) + depPkg, err := dep.await(ctx, snapshot) if err != nil { return nil, err } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 9e52cda5aff..a516860aafc 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -601,7 +601,7 @@ func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode sourc } var pkgs []source.Package for _, ph := range phs { - pkg, err := ph.check(ctx, s) + pkg, err := ph.await(ctx, s) if err != nil { return nil, err } @@ -639,7 +639,7 @@ func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source return nil, fmt.Errorf("no packages in input") } - return ph.check(ctx, s) + return ph.await(ctx, s) } func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]*packageHandle, error) { @@ -756,7 +756,7 @@ func (s *snapshot) checkedPackage(ctx context.Context, id PackageID, mode source if err != nil { return nil, err } - return ph.check(ctx, s) + return ph.await(ctx, s) } func (s *snapshot) getImportedBy(id PackageID) []PackageID { @@ -996,21 +996,6 @@ func (s *snapshot) knownFilesInDir(ctx context.Context, dir span.URI) []span.URI return files } -func (s *snapshot) workspacePackageHandles(ctx context.Context) ([]*packageHandle, error) { - if err := s.awaitLoaded(ctx); err != nil { - return nil, err - } - var phs []*packageHandle - for _, pkgID := range s.workspacePackageIDs() { - ph, err := s.buildPackageHandle(ctx, pkgID, s.workspaceParseMode(pkgID)) - if err != nil { - return nil, err - } - phs = append(phs, ph) - } - return phs, nil -} - func (s *snapshot) ActivePackages(ctx context.Context) ([]source.Package, error) { phs, err := s.activePackageHandles(ctx) if err != nil { @@ -1018,7 +1003,7 @@ func (s *snapshot) ActivePackages(ctx context.Context) ([]source.Package, error) } var pkgs []source.Package for _, ph := range phs { - pkg, err := ph.check(ctx, s) + pkg, err := ph.await(ctx, s) if err != nil { return nil, err } From 22d149443a6474462b18eb49cbc26bf9b21b87f2 Mon Sep 17 00:00:00 2001 From: David Chase Date: Thu, 16 Jun 2022 16:30:48 -0400 Subject: [PATCH 108/723] internal/gcimporter: add support for reading unified IR export data This does not include writing export data in the unified IR format. Most of this change is an import of code from the Go implementation, with minor tweaks and gaskets added. This does not (yet) address the registry issue mentioned in golang/go#52163. Updates golang/go#52163. Change-Id: I98030e6c9ff35c6ff678b8a7ce9b653b18e65e17 Reviewed-on: https://go-review.googlesource.com/c/tools/+/412821 TryBot-Result: Gopher Robot Run-TryBot: David Chase Reviewed-by: Matthew Dempsky gopls-CI: kokoro --- go/gcexportdata/gcexportdata.go | 28 +- go/internal/gcimporter/gcimporter.go | 28 +- go/internal/gcimporter/gcimporter_test.go | 39 +- go/internal/gcimporter/unified_no.go | 10 + go/internal/gcimporter/unified_yes.go | 10 + go/internal/gcimporter/ureader_no.go | 19 + go/internal/gcimporter/ureader_yes.go | 612 ++++++++++++++++++++++ go/internal/pkgbits/codes.go | 77 +++ go/internal/pkgbits/decoder.go | 433 +++++++++++++++ go/internal/pkgbits/doc.go | 32 ++ go/internal/pkgbits/encoder.go | 379 ++++++++++++++ go/internal/pkgbits/flags.go | 9 + go/internal/pkgbits/frames_go1.go | 21 + go/internal/pkgbits/frames_go17.go | 28 + go/internal/pkgbits/reloc.go | 42 ++ go/internal/pkgbits/support.go | 17 + go/internal/pkgbits/sync.go | 113 ++++ go/internal/pkgbits/syncmarker_string.go | 89 ++++ 18 files changed, 1968 insertions(+), 18 deletions(-) create mode 100644 go/internal/gcimporter/unified_no.go create mode 100644 go/internal/gcimporter/unified_yes.go create mode 100644 go/internal/gcimporter/ureader_no.go create mode 100644 go/internal/gcimporter/ureader_yes.go create mode 100644 go/internal/pkgbits/codes.go create mode 100644 go/internal/pkgbits/decoder.go create mode 100644 go/internal/pkgbits/doc.go create mode 100644 go/internal/pkgbits/encoder.go create mode 100644 go/internal/pkgbits/flags.go create mode 100644 go/internal/pkgbits/frames_go1.go create mode 100644 go/internal/pkgbits/frames_go17.go create mode 100644 go/internal/pkgbits/reloc.go create mode 100644 go/internal/pkgbits/support.go create mode 100644 go/internal/pkgbits/sync.go create mode 100644 go/internal/pkgbits/syncmarker_string.go diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go index ddc276cfbcb..2ed25a75024 100644 --- a/go/gcexportdata/gcexportdata.go +++ b/go/gcexportdata/gcexportdata.go @@ -116,13 +116,29 @@ func Read(in io.Reader, fset *token.FileSet, imports map[string]*types.Package, // The indexed export format starts with an 'i'; the older // binary export format starts with a 'c', 'd', or 'v' // (from "version"). Select appropriate importer. - if len(data) > 0 && data[0] == 'i' { - _, pkg, err := gcimporter.IImportData(fset, imports, data[1:], path) - return pkg, err - } + if len(data) > 0 { + switch data[0] { + case 'i': + _, pkg, err := gcimporter.IImportData(fset, imports, data[1:], path) + return pkg, err + + case 'v', 'c', 'd': + _, pkg, err := gcimporter.BImportData(fset, imports, data, path) + return pkg, err - _, pkg, err := gcimporter.BImportData(fset, imports, data, path) - return pkg, err + case 'u': + _, pkg, err := gcimporter.UImportData(fset, imports, data[1:], path) + return pkg, err + + default: + l := len(data) + if l > 10 { + l = 10 + } + return nil, fmt.Errorf("unexpected export data with prefix %q for path %s", string(data[:l]), path) + } + } + return nil, fmt.Errorf("empty export data for %s", path) } // Write writes encoded type information for the specified package to out. diff --git a/go/internal/gcimporter/gcimporter.go b/go/internal/gcimporter/gcimporter.go index 493bfa03b0f..e96c39600d1 100644 --- a/go/internal/gcimporter/gcimporter.go +++ b/go/internal/gcimporter/gcimporter.go @@ -181,8 +181,9 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func defer rc.Close() var hdr string + var size int64 buf := bufio.NewReader(rc) - if hdr, _, err = FindExportData(buf); err != nil { + if hdr, size, err = FindExportData(buf); err != nil { return } @@ -210,10 +211,27 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func // The indexed export format starts with an 'i'; the older // binary export format starts with a 'c', 'd', or 'v' // (from "version"). Select appropriate importer. - if len(data) > 0 && data[0] == 'i' { - _, pkg, err = IImportData(fset, packages, data[1:], id) - } else { - _, pkg, err = BImportData(fset, packages, data, id) + if len(data) > 0 { + switch data[0] { + case 'i': + _, pkg, err := IImportData(fset, packages, data[1:], id) + return pkg, err + + case 'v', 'c', 'd': + _, pkg, err := BImportData(fset, packages, data, id) + return pkg, err + + case 'u': + _, pkg, err := UImportData(fset, packages, data[1:size], id) + return pkg, err + + default: + l := len(data) + if l > 10 { + l = 10 + } + return nil, fmt.Errorf("unexpected export data with prefix %q for path %s", string(data[:l]), id) + } } default: diff --git a/go/internal/gcimporter/gcimporter_test.go b/go/internal/gcimporter/gcimporter_test.go index 4e992af76b3..66c09269d88 100644 --- a/go/internal/gcimporter/gcimporter_test.go +++ b/go/internal/gcimporter/gcimporter_test.go @@ -45,6 +45,10 @@ func needsCompiler(t *testing.T, compiler string) { // compile runs the compiler on filename, with dirname as the working directory, // and writes the output file to outdirname. func compile(t *testing.T, dirname, filename, outdirname string) string { + return compilePkg(t, dirname, filename, outdirname, "p") +} + +func compilePkg(t *testing.T, dirname, filename, outdirname, pkg string) string { testenv.NeedsGoBuild(t) // filename must end with ".go" @@ -53,12 +57,12 @@ func compile(t *testing.T, dirname, filename, outdirname string) string { } basename := filepath.Base(filename) outname := filepath.Join(outdirname, basename[:len(basename)-2]+"o") - cmd := exec.Command("go", "tool", "compile", "-p=p", "-o", outname, filename) + cmd := exec.Command("go", "tool", "compile", "-p="+pkg, "-o", outname, filename) cmd.Dir = dirname out, err := cmd.CombinedOutput() if err != nil { t.Logf("%s", out) - t.Fatalf("go tool compile %s failed: %s", filename, err) + t.Fatalf("(cd %v && %v) failed: %s", cmd.Dir, cmd, err) } return outname } @@ -140,7 +144,11 @@ func TestImportTestdata(t *testing.T) { // For now, we just test the presence of a few packages // that we know are there for sure. got := fmt.Sprint(pkg.Imports()) - for _, want := range []string{"go/ast", "go/token"} { + wants := []string{"go/ast", "go/token"} + if unifiedIR { + wants = []string{"go/ast"} + } + for _, want := range wants { if !strings.Contains(got, want) { t.Errorf(`Package("exports").Imports() = %s, does not contain %s`, got, want) } @@ -364,6 +372,14 @@ func verifyInterfaceMethodRecvs(t *testing.T, named *types.Named, level int) { return // not an interface } + // The unified IR importer always sets interface method receiver + // parameters to point to the Interface type, rather than the Named. + // See #49906. + var want types.Type = named + if unifiedIR { + want = iface + } + // check explicitly declared methods for i := 0; i < iface.NumExplicitMethods(); i++ { m := iface.ExplicitMethod(i) @@ -372,8 +388,8 @@ func verifyInterfaceMethodRecvs(t *testing.T, named *types.Named, level int) { t.Errorf("%s: missing receiver type", m) continue } - if recv.Type() != named { - t.Errorf("%s: got recv type %s; want %s", m, recv.Type(), named) + if recv.Type() != want { + t.Errorf("%s: got recv type %s; want %s", m, recv.Type(), want) } } @@ -451,7 +467,7 @@ func TestIssue13566(t *testing.T) { if err != nil { t.Fatal(err) } - compile(t, "testdata", "a.go", testoutdir) + compilePkg(t, "testdata", "a.go", testoutdir, apkg(testoutdir)) compile(t, testoutdir, bpath, testoutdir) // import must succeed (test for issue at hand) @@ -611,13 +627,22 @@ func TestIssue51836(t *testing.T) { if err != nil { t.Fatal(err) } - compile(t, dir, "a.go", testoutdir) + compilePkg(t, dir, "a.go", testoutdir, apkg(testoutdir)) compile(t, testoutdir, bpath, testoutdir) // import must succeed (test for issue at hand) _ = importPkg(t, "./testdata/aa", tmpdir) } +// apkg returns the package "a" prefixed by (as a package) testoutdir +func apkg(testoutdir string) string { + apkg := testoutdir + "/a" + if os.PathSeparator != '/' { + apkg = strings.ReplaceAll(apkg, string(os.PathSeparator), "/") + } + return apkg +} + func importPkg(t *testing.T, path, srcDir string) *types.Package { pkg, err := Import(make(map[string]*types.Package), path, srcDir, nil) if err != nil { diff --git a/go/internal/gcimporter/unified_no.go b/go/internal/gcimporter/unified_no.go new file mode 100644 index 00000000000..286bf445483 --- /dev/null +++ b/go/internal/gcimporter/unified_no.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !(go1.18 && goexperiment.unified) +// +build !go1.18 !goexperiment.unified + +package gcimporter + +const unifiedIR = false diff --git a/go/internal/gcimporter/unified_yes.go b/go/internal/gcimporter/unified_yes.go new file mode 100644 index 00000000000..b5d69ffbe68 --- /dev/null +++ b/go/internal/gcimporter/unified_yes.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 && goexperiment.unified +// +build go1.18,goexperiment.unified + +package gcimporter + +const unifiedIR = true diff --git a/go/internal/gcimporter/ureader_no.go b/go/internal/gcimporter/ureader_no.go new file mode 100644 index 00000000000..8eb20729c2a --- /dev/null +++ b/go/internal/gcimporter/ureader_no.go @@ -0,0 +1,19 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.18 +// +build !go1.18 + +package gcimporter + +import ( + "fmt" + "go/token" + "go/types" +) + +func UImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (_ int, pkg *types.Package, err error) { + err = fmt.Errorf("go/tools compiled with a Go version earlier than 1.18 cannot read unified IR export data") + return +} diff --git a/go/internal/gcimporter/ureader_yes.go b/go/internal/gcimporter/ureader_yes.go new file mode 100644 index 00000000000..3c1a4375435 --- /dev/null +++ b/go/internal/gcimporter/ureader_yes.go @@ -0,0 +1,612 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Derived from go/internal/gcimporter/ureader.go + +//go:build go1.18 +// +build go1.18 + +package gcimporter + +import ( + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/go/internal/pkgbits" +) + +// A pkgReader holds the shared state for reading a unified IR package +// description. +type pkgReader struct { + pkgbits.PkgDecoder + + fake fakeFileSet + + ctxt *types.Context + imports map[string]*types.Package // previously imported packages, indexed by path + + // lazily initialized arrays corresponding to the unified IR + // PosBase, Pkg, and Type sections, respectively. + posBases []string // position bases (i.e., file names) + pkgs []*types.Package + typs []types.Type + + // laterFns holds functions that need to be invoked at the end of + // import reading. + laterFns []func() +} + +// later adds a function to be invoked at the end of import reading. +func (pr *pkgReader) later(fn func()) { + pr.laterFns = append(pr.laterFns, fn) +} + +// See cmd/compile/internal/noder.derivedInfo. +type derivedInfo struct { + idx pkgbits.Index + needed bool +} + +// See cmd/compile/internal/noder.typeInfo. +type typeInfo struct { + idx pkgbits.Index + derived bool +} + +func UImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (_ int, pkg *types.Package, err error) { + s := string(data) + s = s[:strings.LastIndex(s, "\n$$\n")] + input := pkgbits.NewPkgDecoder(path, s) + pkg = readUnifiedPackage(fset, nil, imports, input) + return +} + +// readUnifiedPackage reads a package description from the given +// unified IR export data decoder. +func readUnifiedPackage(fset *token.FileSet, ctxt *types.Context, imports map[string]*types.Package, input pkgbits.PkgDecoder) *types.Package { + pr := pkgReader{ + PkgDecoder: input, + + fake: fakeFileSet{ + fset: fset, + files: make(map[string]*fileInfo), + }, + + ctxt: ctxt, + imports: imports, + + posBases: make([]string, input.NumElems(pkgbits.RelocPosBase)), + pkgs: make([]*types.Package, input.NumElems(pkgbits.RelocPkg)), + typs: make([]types.Type, input.NumElems(pkgbits.RelocType)), + } + defer pr.fake.setLines() + + r := pr.newReader(pkgbits.RelocMeta, pkgbits.PublicRootIdx, pkgbits.SyncPublic) + pkg := r.pkg() + r.Bool() // has init + + for i, n := 0, r.Len(); i < n; i++ { + // As if r.obj(), but avoiding the Scope.Lookup call, + // to avoid eager loading of imports. + r.Sync(pkgbits.SyncObject) + assert(!r.Bool()) + r.p.objIdx(r.Reloc(pkgbits.RelocObj)) + assert(r.Len() == 0) + } + + r.Sync(pkgbits.SyncEOF) + + for _, fn := range pr.laterFns { + fn() + } + + pkg.MarkComplete() + return pkg +} + +// A reader holds the state for reading a single unified IR element +// within a package. +type reader struct { + pkgbits.Decoder + + p *pkgReader + + dict *readerDict +} + +// A readerDict holds the state for type parameters that parameterize +// the current unified IR element. +type readerDict struct { + // bounds is a slice of typeInfos corresponding to the underlying + // bounds of the element's type parameters. + bounds []typeInfo + + // tparams is a slice of the constructed TypeParams for the element. + tparams []*types.TypeParam + + // devived is a slice of types derived from tparams, which may be + // instantiated while reading the current element. + derived []derivedInfo + derivedTypes []types.Type // lazily instantiated from derived +} + +func (pr *pkgReader) newReader(k pkgbits.RelocKind, idx pkgbits.Index, marker pkgbits.SyncMarker) *reader { + return &reader{ + Decoder: pr.NewDecoder(k, idx, marker), + p: pr, + } +} + +// @@@ Positions + +func (r *reader) pos() token.Pos { + r.Sync(pkgbits.SyncPos) + if !r.Bool() { + return token.NoPos + } + + // TODO(mdempsky): Delta encoding. + posBase := r.posBase() + line := r.Uint() + col := r.Uint() + return r.p.fake.pos(posBase, int(line), int(col)) +} + +func (r *reader) posBase() string { + return r.p.posBaseIdx(r.Reloc(pkgbits.RelocPosBase)) +} + +func (pr *pkgReader) posBaseIdx(idx pkgbits.Index) string { + if b := pr.posBases[idx]; b != "" { + return b + } + + r := pr.newReader(pkgbits.RelocPosBase, idx, pkgbits.SyncPosBase) + + // Within types2, position bases have a lot more details (e.g., + // keeping track of where //line directives appeared exactly). + // + // For go/types, we just track the file name. + + filename := r.String() + + if r.Bool() { // file base + // Was: "b = token.NewTrimmedFileBase(filename, true)" + } else { // line base + pos := r.pos() + line := r.Uint() + col := r.Uint() + + // Was: "b = token.NewLineBase(pos, filename, true, line, col)" + _, _, _ = pos, line, col + } + + b := filename + pr.posBases[idx] = b + return b +} + +// @@@ Packages + +func (r *reader) pkg() *types.Package { + r.Sync(pkgbits.SyncPkg) + return r.p.pkgIdx(r.Reloc(pkgbits.RelocPkg)) +} + +func (pr *pkgReader) pkgIdx(idx pkgbits.Index) *types.Package { + // TODO(mdempsky): Consider using some non-nil pointer to indicate + // the universe scope, so we don't need to keep re-reading it. + if pkg := pr.pkgs[idx]; pkg != nil { + return pkg + } + + pkg := pr.newReader(pkgbits.RelocPkg, idx, pkgbits.SyncPkgDef).doPkg() + pr.pkgs[idx] = pkg + return pkg +} + +func (r *reader) doPkg() *types.Package { + path := r.String() + switch path { + case "": + path = r.p.PkgPath() + case "builtin": + return nil // universe + case "unsafe": + return types.Unsafe + } + + if pkg := r.p.imports[path]; pkg != nil { + return pkg + } + + name := r.String() + + pkg := types.NewPackage(path, name) + r.p.imports[path] = pkg + + imports := make([]*types.Package, r.Len()) + for i := range imports { + imports[i] = r.pkg() + } + pkg.SetImports(imports) + + return pkg +} + +// @@@ Types + +func (r *reader) typ() types.Type { + return r.p.typIdx(r.typInfo(), r.dict) +} + +func (r *reader) typInfo() typeInfo { + r.Sync(pkgbits.SyncType) + if r.Bool() { + return typeInfo{idx: pkgbits.Index(r.Len()), derived: true} + } + return typeInfo{idx: r.Reloc(pkgbits.RelocType), derived: false} +} + +func (pr *pkgReader) typIdx(info typeInfo, dict *readerDict) types.Type { + idx := info.idx + var where *types.Type + if info.derived { + where = &dict.derivedTypes[idx] + idx = dict.derived[idx].idx + } else { + where = &pr.typs[idx] + } + + if typ := *where; typ != nil { + return typ + } + + r := pr.newReader(pkgbits.RelocType, idx, pkgbits.SyncTypeIdx) + r.dict = dict + + typ := r.doTyp() + assert(typ != nil) + + // See comment in pkgReader.typIdx explaining how this happens. + if prev := *where; prev != nil { + return prev + } + + *where = typ + return typ +} + +func (r *reader) doTyp() (res types.Type) { + switch tag := pkgbits.CodeType(r.Code(pkgbits.SyncType)); tag { + default: + errorf("unhandled type tag: %v", tag) + panic("unreachable") + + case pkgbits.TypeBasic: + return types.Typ[r.Len()] + + case pkgbits.TypeNamed: + obj, targs := r.obj() + name := obj.(*types.TypeName) + if len(targs) != 0 { + t, _ := types.Instantiate(r.p.ctxt, name.Type(), targs, false) + return t + } + return name.Type() + + case pkgbits.TypeTypeParam: + return r.dict.tparams[r.Len()] + + case pkgbits.TypeArray: + len := int64(r.Uint64()) + return types.NewArray(r.typ(), len) + case pkgbits.TypeChan: + dir := types.ChanDir(r.Len()) + return types.NewChan(dir, r.typ()) + case pkgbits.TypeMap: + return types.NewMap(r.typ(), r.typ()) + case pkgbits.TypePointer: + return types.NewPointer(r.typ()) + case pkgbits.TypeSignature: + return r.signature(nil, nil, nil) + case pkgbits.TypeSlice: + return types.NewSlice(r.typ()) + case pkgbits.TypeStruct: + return r.structType() + case pkgbits.TypeInterface: + return r.interfaceType() + case pkgbits.TypeUnion: + return r.unionType() + } +} + +func (r *reader) structType() *types.Struct { + fields := make([]*types.Var, r.Len()) + var tags []string + for i := range fields { + pos := r.pos() + pkg, name := r.selector() + ftyp := r.typ() + tag := r.String() + embedded := r.Bool() + + fields[i] = types.NewField(pos, pkg, name, ftyp, embedded) + if tag != "" { + for len(tags) < i { + tags = append(tags, "") + } + tags = append(tags, tag) + } + } + return types.NewStruct(fields, tags) +} + +func (r *reader) unionType() *types.Union { + terms := make([]*types.Term, r.Len()) + for i := range terms { + terms[i] = types.NewTerm(r.Bool(), r.typ()) + } + return types.NewUnion(terms) +} + +func (r *reader) interfaceType() *types.Interface { + methods := make([]*types.Func, r.Len()) + embeddeds := make([]types.Type, r.Len()) + implicit := len(methods) == 0 && len(embeddeds) == 1 && r.Bool() + + for i := range methods { + pos := r.pos() + pkg, name := r.selector() + mtyp := r.signature(nil, nil, nil) + methods[i] = types.NewFunc(pos, pkg, name, mtyp) + } + + for i := range embeddeds { + embeddeds[i] = r.typ() + } + + iface := types.NewInterfaceType(methods, embeddeds) + if implicit { + iface.MarkImplicit() + } + return iface +} + +func (r *reader) signature(recv *types.Var, rtparams, tparams []*types.TypeParam) *types.Signature { + r.Sync(pkgbits.SyncSignature) + + params := r.params() + results := r.params() + variadic := r.Bool() + + return types.NewSignatureType(recv, rtparams, tparams, params, results, variadic) +} + +func (r *reader) params() *types.Tuple { + r.Sync(pkgbits.SyncParams) + + params := make([]*types.Var, r.Len()) + for i := range params { + params[i] = r.param() + } + + return types.NewTuple(params...) +} + +func (r *reader) param() *types.Var { + r.Sync(pkgbits.SyncParam) + + pos := r.pos() + pkg, name := r.localIdent() + typ := r.typ() + + return types.NewParam(pos, pkg, name, typ) +} + +// @@@ Objects + +func (r *reader) obj() (types.Object, []types.Type) { + r.Sync(pkgbits.SyncObject) + + assert(!r.Bool()) + + pkg, name := r.p.objIdx(r.Reloc(pkgbits.RelocObj)) + obj := pkgScope(pkg).Lookup(name) + + targs := make([]types.Type, r.Len()) + for i := range targs { + targs[i] = r.typ() + } + + return obj, targs +} + +func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { + rname := pr.newReader(pkgbits.RelocName, idx, pkgbits.SyncObject1) + + objPkg, objName := rname.qualifiedIdent() + assert(objName != "") + + tag := pkgbits.CodeObj(rname.Code(pkgbits.SyncCodeObj)) + + if tag == pkgbits.ObjStub { + assert(objPkg == nil || objPkg == types.Unsafe) + return objPkg, objName + } + + if objPkg.Scope().Lookup(objName) == nil { + dict := pr.objDictIdx(idx) + + r := pr.newReader(pkgbits.RelocObj, idx, pkgbits.SyncObject1) + r.dict = dict + + declare := func(obj types.Object) { + objPkg.Scope().Insert(obj) + } + + switch tag { + default: + panic("weird") + + case pkgbits.ObjAlias: + pos := r.pos() + typ := r.typ() + declare(types.NewTypeName(pos, objPkg, objName, typ)) + + case pkgbits.ObjConst: + pos := r.pos() + typ := r.typ() + val := r.Value() + declare(types.NewConst(pos, objPkg, objName, typ, val)) + + case pkgbits.ObjFunc: + pos := r.pos() + tparams := r.typeParamNames() + sig := r.signature(nil, nil, tparams) + declare(types.NewFunc(pos, objPkg, objName, sig)) + + case pkgbits.ObjType: + pos := r.pos() + + obj := types.NewTypeName(pos, objPkg, objName, nil) + named := types.NewNamed(obj, nil, nil) + declare(obj) + + named.SetTypeParams(r.typeParamNames()) + + // TODO(mdempsky): Rewrite receiver types to underlying is an + // Interface? The go/types importer does this (I think because + // unit tests expected that), but cmd/compile doesn't care + // about it, so maybe we can avoid worrying about that here. + rhs := r.typ() + r.p.later(func() { + underlying := rhs.Underlying() + named.SetUnderlying(underlying) + }) + + for i, n := 0, r.Len(); i < n; i++ { + named.AddMethod(r.method()) + } + + case pkgbits.ObjVar: + pos := r.pos() + typ := r.typ() + declare(types.NewVar(pos, objPkg, objName, typ)) + } + } + + return objPkg, objName +} + +func (pr *pkgReader) objDictIdx(idx pkgbits.Index) *readerDict { + r := pr.newReader(pkgbits.RelocObjDict, idx, pkgbits.SyncObject1) + + var dict readerDict + + if implicits := r.Len(); implicits != 0 { + errorf("unexpected object with %v implicit type parameter(s)", implicits) + } + + dict.bounds = make([]typeInfo, r.Len()) + for i := range dict.bounds { + dict.bounds[i] = r.typInfo() + } + + dict.derived = make([]derivedInfo, r.Len()) + dict.derivedTypes = make([]types.Type, len(dict.derived)) + for i := range dict.derived { + dict.derived[i] = derivedInfo{r.Reloc(pkgbits.RelocType), r.Bool()} + } + + // function references follow, but reader doesn't need those + + return &dict +} + +func (r *reader) typeParamNames() []*types.TypeParam { + r.Sync(pkgbits.SyncTypeParamNames) + + // Note: This code assumes it only processes objects without + // implement type parameters. This is currently fine, because + // reader is only used to read in exported declarations, which are + // always package scoped. + + if len(r.dict.bounds) == 0 { + return nil + } + + // Careful: Type parameter lists may have cycles. To allow for this, + // we construct the type parameter list in two passes: first we + // create all the TypeNames and TypeParams, then we construct and + // set the bound type. + + r.dict.tparams = make([]*types.TypeParam, len(r.dict.bounds)) + for i := range r.dict.bounds { + pos := r.pos() + pkg, name := r.localIdent() + + tname := types.NewTypeName(pos, pkg, name, nil) + r.dict.tparams[i] = types.NewTypeParam(tname, nil) + } + + typs := make([]types.Type, len(r.dict.bounds)) + for i, bound := range r.dict.bounds { + typs[i] = r.p.typIdx(bound, r.dict) + } + + // TODO(mdempsky): This is subtle, elaborate further. + // + // We have to save tparams outside of the closure, because + // typeParamNames() can be called multiple times with the same + // dictionary instance. + // + // Also, this needs to happen later to make sure SetUnderlying has + // been called. + // + // TODO(mdempsky): Is it safe to have a single "later" slice or do + // we need to have multiple passes? See comments on CL 386002 and + // go.dev/issue/52104. + tparams := r.dict.tparams + r.p.later(func() { + for i, typ := range typs { + tparams[i].SetConstraint(typ) + } + }) + + return r.dict.tparams +} + +func (r *reader) method() *types.Func { + r.Sync(pkgbits.SyncMethod) + pos := r.pos() + pkg, name := r.selector() + + rparams := r.typeParamNames() + sig := r.signature(r.param(), rparams, nil) + + _ = r.pos() // TODO(mdempsky): Remove; this is a hacker for linker.go. + return types.NewFunc(pos, pkg, name, sig) +} + +func (r *reader) qualifiedIdent() (*types.Package, string) { return r.ident(pkgbits.SyncSym) } +func (r *reader) localIdent() (*types.Package, string) { return r.ident(pkgbits.SyncLocalIdent) } +func (r *reader) selector() (*types.Package, string) { return r.ident(pkgbits.SyncSelector) } + +func (r *reader) ident(marker pkgbits.SyncMarker) (*types.Package, string) { + r.Sync(marker) + return r.pkg(), r.String() +} + +// pkgScope returns pkg.Scope(). +// If pkg is nil, it returns types.Universe instead. +// +// TODO(mdempsky): Remove after x/tools can depend on Go 1.19. +func pkgScope(pkg *types.Package) *types.Scope { + if pkg != nil { + return pkg.Scope() + } + return types.Universe +} diff --git a/go/internal/pkgbits/codes.go b/go/internal/pkgbits/codes.go new file mode 100644 index 00000000000..f0cabde96eb --- /dev/null +++ b/go/internal/pkgbits/codes.go @@ -0,0 +1,77 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgbits + +// A Code is an enum value that can be encoded into bitstreams. +// +// Code types are preferable for enum types, because they allow +// Decoder to detect desyncs. +type Code interface { + // Marker returns the SyncMarker for the Code's dynamic type. + Marker() SyncMarker + + // Value returns the Code's ordinal value. + Value() int +} + +// A CodeVal distinguishes among go/constant.Value encodings. +type CodeVal int + +func (c CodeVal) Marker() SyncMarker { return SyncVal } +func (c CodeVal) Value() int { return int(c) } + +// Note: These values are public and cannot be changed without +// updating the go/types importers. + +const ( + ValBool CodeVal = iota + ValString + ValInt64 + ValBigInt + ValBigRat + ValBigFloat +) + +// A CodeType distinguishes among go/types.Type encodings. +type CodeType int + +func (c CodeType) Marker() SyncMarker { return SyncType } +func (c CodeType) Value() int { return int(c) } + +// Note: These values are public and cannot be changed without +// updating the go/types importers. + +const ( + TypeBasic CodeType = iota + TypeNamed + TypePointer + TypeSlice + TypeArray + TypeChan + TypeMap + TypeSignature + TypeStruct + TypeInterface + TypeUnion + TypeTypeParam +) + +// A CodeObj distinguishes among go/types.Object encodings. +type CodeObj int + +func (c CodeObj) Marker() SyncMarker { return SyncCodeObj } +func (c CodeObj) Value() int { return int(c) } + +// Note: These values are public and cannot be changed without +// updating the go/types importers. + +const ( + ObjAlias CodeObj = iota + ObjConst + ObjType + ObjFunc + ObjVar + ObjStub +) diff --git a/go/internal/pkgbits/decoder.go b/go/internal/pkgbits/decoder.go new file mode 100644 index 00000000000..2bc793668ec --- /dev/null +++ b/go/internal/pkgbits/decoder.go @@ -0,0 +1,433 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgbits + +import ( + "encoding/binary" + "fmt" + "go/constant" + "go/token" + "math/big" + "os" + "runtime" + "strings" +) + +// A PkgDecoder provides methods for decoding a package's Unified IR +// export data. +type PkgDecoder struct { + // version is the file format version. + version uint32 + + // sync indicates whether the file uses sync markers. + sync bool + + // pkgPath is the package path for the package to be decoded. + // + // TODO(mdempsky): Remove; unneeded since CL 391014. + pkgPath string + + // elemData is the full data payload of the encoded package. + // Elements are densely and contiguously packed together. + // + // The last 8 bytes of elemData are the package fingerprint. + elemData string + + // elemEnds stores the byte-offset end positions of element + // bitstreams within elemData. + // + // For example, element I's bitstream data starts at elemEnds[I-1] + // (or 0, if I==0) and ends at elemEnds[I]. + // + // Note: elemEnds is indexed by absolute indices, not + // section-relative indices. + elemEnds []uint32 + + // elemEndsEnds stores the index-offset end positions of relocation + // sections within elemEnds. + // + // For example, section K's end positions start at elemEndsEnds[K-1] + // (or 0, if K==0) and end at elemEndsEnds[K]. + elemEndsEnds [numRelocs]uint32 +} + +// PkgPath returns the package path for the package +// +// TODO(mdempsky): Remove; unneeded since CL 391014. +func (pr *PkgDecoder) PkgPath() string { return pr.pkgPath } + +// SyncMarkers reports whether pr uses sync markers. +func (pr *PkgDecoder) SyncMarkers() bool { return pr.sync } + +// NewPkgDecoder returns a PkgDecoder initialized to read the Unified +// IR export data from input. pkgPath is the package path for the +// compilation unit that produced the export data. +// +// TODO(mdempsky): Remove pkgPath parameter; unneeded since CL 391014. +func NewPkgDecoder(pkgPath, input string) PkgDecoder { + pr := PkgDecoder{ + pkgPath: pkgPath, + } + + // TODO(mdempsky): Implement direct indexing of input string to + // avoid copying the position information. + + r := strings.NewReader(input) + + assert(binary.Read(r, binary.LittleEndian, &pr.version) == nil) + + switch pr.version { + default: + panic(fmt.Errorf("unsupported version: %v", pr.version)) + case 0: + // no flags + case 1: + var flags uint32 + assert(binary.Read(r, binary.LittleEndian, &flags) == nil) + pr.sync = flags&flagSyncMarkers != 0 + } + + assert(binary.Read(r, binary.LittleEndian, pr.elemEndsEnds[:]) == nil) + + pr.elemEnds = make([]uint32, pr.elemEndsEnds[len(pr.elemEndsEnds)-1]) + assert(binary.Read(r, binary.LittleEndian, pr.elemEnds[:]) == nil) + + pos, err := r.Seek(0, os.SEEK_CUR) + assert(err == nil) + + pr.elemData = input[pos:] + assert(len(pr.elemData)-8 == int(pr.elemEnds[len(pr.elemEnds)-1])) + + return pr +} + +// NumElems returns the number of elements in section k. +func (pr *PkgDecoder) NumElems(k RelocKind) int { + count := int(pr.elemEndsEnds[k]) + if k > 0 { + count -= int(pr.elemEndsEnds[k-1]) + } + return count +} + +// TotalElems returns the total number of elements across all sections. +func (pr *PkgDecoder) TotalElems() int { + return len(pr.elemEnds) +} + +// Fingerprint returns the package fingerprint. +func (pr *PkgDecoder) Fingerprint() [8]byte { + var fp [8]byte + copy(fp[:], pr.elemData[len(pr.elemData)-8:]) + return fp +} + +// AbsIdx returns the absolute index for the given (section, index) +// pair. +func (pr *PkgDecoder) AbsIdx(k RelocKind, idx Index) int { + absIdx := int(idx) + if k > 0 { + absIdx += int(pr.elemEndsEnds[k-1]) + } + if absIdx >= int(pr.elemEndsEnds[k]) { + errorf("%v:%v is out of bounds; %v", k, idx, pr.elemEndsEnds) + } + return absIdx +} + +// DataIdx returns the raw element bitstream for the given (section, +// index) pair. +func (pr *PkgDecoder) DataIdx(k RelocKind, idx Index) string { + absIdx := pr.AbsIdx(k, idx) + + var start uint32 + if absIdx > 0 { + start = pr.elemEnds[absIdx-1] + } + end := pr.elemEnds[absIdx] + + return pr.elemData[start:end] +} + +// StringIdx returns the string value for the given string index. +func (pr *PkgDecoder) StringIdx(idx Index) string { + return pr.DataIdx(RelocString, idx) +} + +// NewDecoder returns a Decoder for the given (section, index) pair, +// and decodes the given SyncMarker from the element bitstream. +func (pr *PkgDecoder) NewDecoder(k RelocKind, idx Index, marker SyncMarker) Decoder { + r := pr.NewDecoderRaw(k, idx) + r.Sync(marker) + return r +} + +// NewDecoderRaw returns a Decoder for the given (section, index) pair. +// +// Most callers should use NewDecoder instead. +func (pr *PkgDecoder) NewDecoderRaw(k RelocKind, idx Index) Decoder { + r := Decoder{ + common: pr, + k: k, + Idx: idx, + } + + // TODO(mdempsky) r.data.Reset(...) after #44505 is resolved. + r.Data = *strings.NewReader(pr.DataIdx(k, idx)) + + r.Sync(SyncRelocs) + r.Relocs = make([]RelocEnt, r.Len()) + for i := range r.Relocs { + r.Sync(SyncReloc) + r.Relocs[i] = RelocEnt{RelocKind(r.Len()), Index(r.Len())} + } + + return r +} + +// A Decoder provides methods for decoding an individual element's +// bitstream data. +type Decoder struct { + common *PkgDecoder + + Relocs []RelocEnt + Data strings.Reader + + k RelocKind + Idx Index +} + +func (r *Decoder) checkErr(err error) { + if err != nil { + errorf("unexpected decoding error: %w", err) + } +} + +func (r *Decoder) rawUvarint() uint64 { + x, err := binary.ReadUvarint(&r.Data) + r.checkErr(err) + return x +} + +func (r *Decoder) rawVarint() int64 { + ux := r.rawUvarint() + + // Zig-zag decode. + x := int64(ux >> 1) + if ux&1 != 0 { + x = ^x + } + return x +} + +func (r *Decoder) rawReloc(k RelocKind, idx int) Index { + e := r.Relocs[idx] + assert(e.Kind == k) + return e.Idx +} + +// Sync decodes a sync marker from the element bitstream and asserts +// that it matches the expected marker. +// +// If r.common.sync is false, then Sync is a no-op. +func (r *Decoder) Sync(mWant SyncMarker) { + if !r.common.sync { + return + } + + pos, _ := r.Data.Seek(0, os.SEEK_CUR) // TODO(mdempsky): io.SeekCurrent after #44505 is resolved + mHave := SyncMarker(r.rawUvarint()) + writerPCs := make([]int, r.rawUvarint()) + for i := range writerPCs { + writerPCs[i] = int(r.rawUvarint()) + } + + if mHave == mWant { + return + } + + // There's some tension here between printing: + // + // (1) full file paths that tools can recognize (e.g., so emacs + // hyperlinks the "file:line" text for easy navigation), or + // + // (2) short file paths that are easier for humans to read (e.g., by + // omitting redundant or irrelevant details, so it's easier to + // focus on the useful bits that remain). + // + // The current formatting favors the former, as it seems more + // helpful in practice. But perhaps the formatting could be improved + // to better address both concerns. For example, use relative file + // paths if they would be shorter, or rewrite file paths to contain + // "$GOROOT" (like objabi.AbsFile does) if tools can be taught how + // to reliably expand that again. + + fmt.Printf("export data desync: package %q, section %v, index %v, offset %v\n", r.common.pkgPath, r.k, r.Idx, pos) + + fmt.Printf("\nfound %v, written at:\n", mHave) + if len(writerPCs) == 0 { + fmt.Printf("\t[stack trace unavailable; recompile package %q with -d=syncframes]\n", r.common.pkgPath) + } + for _, pc := range writerPCs { + fmt.Printf("\t%s\n", r.common.StringIdx(r.rawReloc(RelocString, pc))) + } + + fmt.Printf("\nexpected %v, reading at:\n", mWant) + var readerPCs [32]uintptr // TODO(mdempsky): Dynamically size? + n := runtime.Callers(2, readerPCs[:]) + for _, pc := range fmtFrames(readerPCs[:n]...) { + fmt.Printf("\t%s\n", pc) + } + + // We already printed a stack trace for the reader, so now we can + // simply exit. Printing a second one with panic or base.Fatalf + // would just be noise. + os.Exit(1) +} + +// Bool decodes and returns a bool value from the element bitstream. +func (r *Decoder) Bool() bool { + r.Sync(SyncBool) + x, err := r.Data.ReadByte() + r.checkErr(err) + assert(x < 2) + return x != 0 +} + +// Int64 decodes and returns an int64 value from the element bitstream. +func (r *Decoder) Int64() int64 { + r.Sync(SyncInt64) + return r.rawVarint() +} + +// Int64 decodes and returns a uint64 value from the element bitstream. +func (r *Decoder) Uint64() uint64 { + r.Sync(SyncUint64) + return r.rawUvarint() +} + +// Len decodes and returns a non-negative int value from the element bitstream. +func (r *Decoder) Len() int { x := r.Uint64(); v := int(x); assert(uint64(v) == x); return v } + +// Int decodes and returns an int value from the element bitstream. +func (r *Decoder) Int() int { x := r.Int64(); v := int(x); assert(int64(v) == x); return v } + +// Uint decodes and returns a uint value from the element bitstream. +func (r *Decoder) Uint() uint { x := r.Uint64(); v := uint(x); assert(uint64(v) == x); return v } + +// Code decodes a Code value from the element bitstream and returns +// its ordinal value. It's the caller's responsibility to convert the +// result to an appropriate Code type. +// +// TODO(mdempsky): Ideally this method would have signature "Code[T +// Code] T" instead, but we don't allow generic methods and the +// compiler can't depend on generics yet anyway. +func (r *Decoder) Code(mark SyncMarker) int { + r.Sync(mark) + return r.Len() +} + +// Reloc decodes a relocation of expected section k from the element +// bitstream and returns an index to the referenced element. +func (r *Decoder) Reloc(k RelocKind) Index { + r.Sync(SyncUseReloc) + return r.rawReloc(k, r.Len()) +} + +// String decodes and returns a string value from the element +// bitstream. +func (r *Decoder) String() string { + r.Sync(SyncString) + return r.common.StringIdx(r.Reloc(RelocString)) +} + +// Strings decodes and returns a variable-length slice of strings from +// the element bitstream. +func (r *Decoder) Strings() []string { + res := make([]string, r.Len()) + for i := range res { + res[i] = r.String() + } + return res +} + +// Value decodes and returns a constant.Value from the element +// bitstream. +func (r *Decoder) Value() constant.Value { + r.Sync(SyncValue) + isComplex := r.Bool() + val := r.scalar() + if isComplex { + val = constant.BinaryOp(val, token.ADD, constant.MakeImag(r.scalar())) + } + return val +} + +func (r *Decoder) scalar() constant.Value { + switch tag := CodeVal(r.Code(SyncVal)); tag { + default: + panic(fmt.Errorf("unexpected scalar tag: %v", tag)) + + case ValBool: + return constant.MakeBool(r.Bool()) + case ValString: + return constant.MakeString(r.String()) + case ValInt64: + return constant.MakeInt64(r.Int64()) + case ValBigInt: + return constant.Make(r.bigInt()) + case ValBigRat: + num := r.bigInt() + denom := r.bigInt() + return constant.Make(new(big.Rat).SetFrac(num, denom)) + case ValBigFloat: + return constant.Make(r.bigFloat()) + } +} + +func (r *Decoder) bigInt() *big.Int { + v := new(big.Int).SetBytes([]byte(r.String())) + if r.Bool() { + v.Neg(v) + } + return v +} + +func (r *Decoder) bigFloat() *big.Float { + v := new(big.Float).SetPrec(512) + assert(v.UnmarshalText([]byte(r.String())) == nil) + return v +} + +// @@@ Helpers + +// TODO(mdempsky): These should probably be removed. I think they're a +// smell that the export data format is not yet quite right. + +// PeekPkgPath returns the package path for the specified package +// index. +func (pr *PkgDecoder) PeekPkgPath(idx Index) string { + r := pr.NewDecoder(RelocPkg, idx, SyncPkgDef) + path := r.String() + if path == "" { + path = pr.pkgPath + } + return path +} + +// PeekObj returns the package path, object name, and CodeObj for the +// specified object index. +func (pr *PkgDecoder) PeekObj(idx Index) (string, string, CodeObj) { + r := pr.NewDecoder(RelocName, idx, SyncObject1) + r.Sync(SyncSym) + r.Sync(SyncPkg) + path := pr.PeekPkgPath(r.Reloc(RelocPkg)) + name := r.String() + assert(name != "") + + tag := CodeObj(r.Code(SyncCodeObj)) + + return path, name, tag +} diff --git a/go/internal/pkgbits/doc.go b/go/internal/pkgbits/doc.go new file mode 100644 index 00000000000..c8a2796b5e4 --- /dev/null +++ b/go/internal/pkgbits/doc.go @@ -0,0 +1,32 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package pkgbits implements low-level coding abstractions for +// Unified IR's export data format. +// +// At a low-level, a package is a collection of bitstream elements. +// Each element has a "kind" and a dense, non-negative index. +// Elements can be randomly accessed given their kind and index. +// +// Individual elements are sequences of variable-length values (e.g., +// integers, booleans, strings, go/constant values, cross-references +// to other elements). Package pkgbits provides APIs for encoding and +// decoding these low-level values, but the details of mapping +// higher-level Go constructs into elements is left to higher-level +// abstractions. +// +// Elements may cross-reference each other with "relocations." For +// example, an element representing a pointer type has a relocation +// referring to the element type. +// +// Go constructs may be composed as a constellation of multiple +// elements. For example, a declared function may have one element to +// describe the object (e.g., its name, type, position), and a +// separate element to describe its function body. This allows readers +// some flexibility in efficiently seeking or re-reading data (e.g., +// inlining requires re-reading the function body for each inlined +// call, without needing to re-read the object-level details). +// +// This is a copy of internal/pkgbits in the Go implementation. +package pkgbits diff --git a/go/internal/pkgbits/encoder.go b/go/internal/pkgbits/encoder.go new file mode 100644 index 00000000000..c50c838caae --- /dev/null +++ b/go/internal/pkgbits/encoder.go @@ -0,0 +1,379 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgbits + +import ( + "bytes" + "crypto/md5" + "encoding/binary" + "go/constant" + "io" + "math/big" + "runtime" +) + +// currentVersion is the current version number. +// +// - v0: initial prototype +// +// - v1: adds the flags uint32 word +const currentVersion uint32 = 1 + +// A PkgEncoder provides methods for encoding a package's Unified IR +// export data. +type PkgEncoder struct { + // elems holds the bitstream for previously encoded elements. + elems [numRelocs][]string + + // stringsIdx maps previously encoded strings to their index within + // the RelocString section, to allow deduplication. That is, + // elems[RelocString][stringsIdx[s]] == s (if present). + stringsIdx map[string]Index + + // syncFrames is the number of frames to write at each sync + // marker. A negative value means sync markers are omitted. + syncFrames int +} + +// SyncMarkers reports whether pw uses sync markers. +func (pw *PkgEncoder) SyncMarkers() bool { return pw.syncFrames >= 0 } + +// NewPkgEncoder returns an initialized PkgEncoder. +// +// syncFrames is the number of caller frames that should be serialized +// at Sync points. Serializing additional frames results in larger +// export data files, but can help diagnosing desync errors in +// higher-level Unified IR reader/writer code. If syncFrames is +// negative, then sync markers are omitted entirely. +func NewPkgEncoder(syncFrames int) PkgEncoder { + return PkgEncoder{ + stringsIdx: make(map[string]Index), + syncFrames: syncFrames, + } +} + +// DumpTo writes the package's encoded data to out0 and returns the +// package fingerprint. +func (pw *PkgEncoder) DumpTo(out0 io.Writer) (fingerprint [8]byte) { + h := md5.New() + out := io.MultiWriter(out0, h) + + writeUint32 := func(x uint32) { + assert(binary.Write(out, binary.LittleEndian, x) == nil) + } + + writeUint32(currentVersion) + + var flags uint32 + if pw.SyncMarkers() { + flags |= flagSyncMarkers + } + writeUint32(flags) + + // Write elemEndsEnds. + var sum uint32 + for _, elems := range &pw.elems { + sum += uint32(len(elems)) + writeUint32(sum) + } + + // Write elemEnds. + sum = 0 + for _, elems := range &pw.elems { + for _, elem := range elems { + sum += uint32(len(elem)) + writeUint32(sum) + } + } + + // Write elemData. + for _, elems := range &pw.elems { + for _, elem := range elems { + _, err := io.WriteString(out, elem) + assert(err == nil) + } + } + + // Write fingerprint. + copy(fingerprint[:], h.Sum(nil)) + _, err := out0.Write(fingerprint[:]) + assert(err == nil) + + return +} + +// StringIdx adds a string value to the strings section, if not +// already present, and returns its index. +func (pw *PkgEncoder) StringIdx(s string) Index { + if idx, ok := pw.stringsIdx[s]; ok { + assert(pw.elems[RelocString][idx] == s) + return idx + } + + idx := Index(len(pw.elems[RelocString])) + pw.elems[RelocString] = append(pw.elems[RelocString], s) + pw.stringsIdx[s] = idx + return idx +} + +// NewEncoder returns an Encoder for a new element within the given +// section, and encodes the given SyncMarker as the start of the +// element bitstream. +func (pw *PkgEncoder) NewEncoder(k RelocKind, marker SyncMarker) Encoder { + e := pw.NewEncoderRaw(k) + e.Sync(marker) + return e +} + +// NewEncoderRaw returns an Encoder for a new element within the given +// section. +// +// Most callers should use NewEncoder instead. +func (pw *PkgEncoder) NewEncoderRaw(k RelocKind) Encoder { + idx := Index(len(pw.elems[k])) + pw.elems[k] = append(pw.elems[k], "") // placeholder + + return Encoder{ + p: pw, + k: k, + Idx: idx, + } +} + +// An Encoder provides methods for encoding an individual element's +// bitstream data. +type Encoder struct { + p *PkgEncoder + + Relocs []RelocEnt + Data bytes.Buffer // accumulated element bitstream data + + encodingRelocHeader bool + + k RelocKind + Idx Index // index within relocation section +} + +// Flush finalizes the element's bitstream and returns its Index. +func (w *Encoder) Flush() Index { + var sb bytes.Buffer // TODO(mdempsky): strings.Builder after #44505 is resolved + + // Backup the data so we write the relocations at the front. + var tmp bytes.Buffer + io.Copy(&tmp, &w.Data) + + // TODO(mdempsky): Consider writing these out separately so they're + // easier to strip, along with function bodies, so that we can prune + // down to just the data that's relevant to go/types. + if w.encodingRelocHeader { + panic("encodingRelocHeader already true; recursive flush?") + } + w.encodingRelocHeader = true + w.Sync(SyncRelocs) + w.Len(len(w.Relocs)) + for _, rEnt := range w.Relocs { + w.Sync(SyncReloc) + w.Len(int(rEnt.Kind)) + w.Len(int(rEnt.Idx)) + } + + io.Copy(&sb, &w.Data) + io.Copy(&sb, &tmp) + w.p.elems[w.k][w.Idx] = sb.String() + + return w.Idx +} + +func (w *Encoder) checkErr(err error) { + if err != nil { + errorf("unexpected encoding error: %v", err) + } +} + +func (w *Encoder) rawUvarint(x uint64) { + var buf [binary.MaxVarintLen64]byte + n := binary.PutUvarint(buf[:], x) + _, err := w.Data.Write(buf[:n]) + w.checkErr(err) +} + +func (w *Encoder) rawVarint(x int64) { + // Zig-zag encode. + ux := uint64(x) << 1 + if x < 0 { + ux = ^ux + } + + w.rawUvarint(ux) +} + +func (w *Encoder) rawReloc(r RelocKind, idx Index) int { + // TODO(mdempsky): Use map for lookup; this takes quadratic time. + for i, rEnt := range w.Relocs { + if rEnt.Kind == r && rEnt.Idx == idx { + return i + } + } + + i := len(w.Relocs) + w.Relocs = append(w.Relocs, RelocEnt{r, idx}) + return i +} + +func (w *Encoder) Sync(m SyncMarker) { + if !w.p.SyncMarkers() { + return + } + + // Writing out stack frame string references requires working + // relocations, but writing out the relocations themselves involves + // sync markers. To prevent infinite recursion, we simply trim the + // stack frame for sync markers within the relocation header. + var frames []string + if !w.encodingRelocHeader && w.p.syncFrames > 0 { + pcs := make([]uintptr, w.p.syncFrames) + n := runtime.Callers(2, pcs) + frames = fmtFrames(pcs[:n]...) + } + + // TODO(mdempsky): Save space by writing out stack frames as a + // linked list so we can share common stack frames. + w.rawUvarint(uint64(m)) + w.rawUvarint(uint64(len(frames))) + for _, frame := range frames { + w.rawUvarint(uint64(w.rawReloc(RelocString, w.p.StringIdx(frame)))) + } +} + +// Bool encodes and writes a bool value into the element bitstream, +// and then returns the bool value. +// +// For simple, 2-alternative encodings, the idiomatic way to call Bool +// is something like: +// +// if w.Bool(x != 0) { +// // alternative #1 +// } else { +// // alternative #2 +// } +// +// For multi-alternative encodings, use Code instead. +func (w *Encoder) Bool(b bool) bool { + w.Sync(SyncBool) + var x byte + if b { + x = 1 + } + err := w.Data.WriteByte(x) + w.checkErr(err) + return b +} + +// Int64 encodes and writes an int64 value into the element bitstream. +func (w *Encoder) Int64(x int64) { + w.Sync(SyncInt64) + w.rawVarint(x) +} + +// Uint64 encodes and writes a uint64 value into the element bitstream. +func (w *Encoder) Uint64(x uint64) { + w.Sync(SyncUint64) + w.rawUvarint(x) +} + +// Len encodes and writes a non-negative int value into the element bitstream. +func (w *Encoder) Len(x int) { assert(x >= 0); w.Uint64(uint64(x)) } + +// Int encodes and writes an int value into the element bitstream. +func (w *Encoder) Int(x int) { w.Int64(int64(x)) } + +// Len encodes and writes a uint value into the element bitstream. +func (w *Encoder) Uint(x uint) { w.Uint64(uint64(x)) } + +// Reloc encodes and writes a relocation for the given (section, +// index) pair into the element bitstream. +// +// Note: Only the index is formally written into the element +// bitstream, so bitstream decoders must know from context which +// section an encoded relocation refers to. +func (w *Encoder) Reloc(r RelocKind, idx Index) { + w.Sync(SyncUseReloc) + w.Len(w.rawReloc(r, idx)) +} + +// Code encodes and writes a Code value into the element bitstream. +func (w *Encoder) Code(c Code) { + w.Sync(c.Marker()) + w.Len(c.Value()) +} + +// String encodes and writes a string value into the element +// bitstream. +// +// Internally, strings are deduplicated by adding them to the strings +// section (if not already present), and then writing a relocation +// into the element bitstream. +func (w *Encoder) String(s string) { + w.Sync(SyncString) + w.Reloc(RelocString, w.p.StringIdx(s)) +} + +// Strings encodes and writes a variable-length slice of strings into +// the element bitstream. +func (w *Encoder) Strings(ss []string) { + w.Len(len(ss)) + for _, s := range ss { + w.String(s) + } +} + +// Value encodes and writes a constant.Value into the element +// bitstream. +func (w *Encoder) Value(val constant.Value) { + w.Sync(SyncValue) + if w.Bool(val.Kind() == constant.Complex) { + w.scalar(constant.Real(val)) + w.scalar(constant.Imag(val)) + } else { + w.scalar(val) + } +} + +func (w *Encoder) scalar(val constant.Value) { + switch v := constant.Val(val).(type) { + default: + errorf("unhandled %v (%v)", val, val.Kind()) + case bool: + w.Code(ValBool) + w.Bool(v) + case string: + w.Code(ValString) + w.String(v) + case int64: + w.Code(ValInt64) + w.Int64(v) + case *big.Int: + w.Code(ValBigInt) + w.bigInt(v) + case *big.Rat: + w.Code(ValBigRat) + w.bigInt(v.Num()) + w.bigInt(v.Denom()) + case *big.Float: + w.Code(ValBigFloat) + w.bigFloat(v) + } +} + +func (w *Encoder) bigInt(v *big.Int) { + b := v.Bytes() + w.String(string(b)) // TODO: More efficient encoding. + w.Bool(v.Sign() < 0) +} + +func (w *Encoder) bigFloat(v *big.Float) { + b := v.Append(nil, 'p', -1) + w.String(string(b)) // TODO: More efficient encoding. +} diff --git a/go/internal/pkgbits/flags.go b/go/internal/pkgbits/flags.go new file mode 100644 index 00000000000..654222745fa --- /dev/null +++ b/go/internal/pkgbits/flags.go @@ -0,0 +1,9 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgbits + +const ( + flagSyncMarkers = 1 << iota // file format contains sync markers +) diff --git a/go/internal/pkgbits/frames_go1.go b/go/internal/pkgbits/frames_go1.go new file mode 100644 index 00000000000..5294f6a63ed --- /dev/null +++ b/go/internal/pkgbits/frames_go1.go @@ -0,0 +1,21 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.7 +// +build !go1.7 + +// TODO(mdempsky): Remove after #44505 is resolved + +package pkgbits + +import "runtime" + +func walkFrames(pcs []uintptr, visit frameVisitor) { + for _, pc := range pcs { + fn := runtime.FuncForPC(pc) + file, line := fn.FileLine(pc) + + visit(file, line, fn.Name(), pc-fn.Entry()) + } +} diff --git a/go/internal/pkgbits/frames_go17.go b/go/internal/pkgbits/frames_go17.go new file mode 100644 index 00000000000..2324ae7adfe --- /dev/null +++ b/go/internal/pkgbits/frames_go17.go @@ -0,0 +1,28 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.7 +// +build go1.7 + +package pkgbits + +import "runtime" + +// walkFrames calls visit for each call frame represented by pcs. +// +// pcs should be a slice of PCs, as returned by runtime.Callers. +func walkFrames(pcs []uintptr, visit frameVisitor) { + if len(pcs) == 0 { + return + } + + frames := runtime.CallersFrames(pcs) + for { + frame, more := frames.Next() + visit(frame.File, frame.Line, frame.Function, frame.PC-frame.Entry) + if !more { + return + } + } +} diff --git a/go/internal/pkgbits/reloc.go b/go/internal/pkgbits/reloc.go new file mode 100644 index 00000000000..7a8f04ab3fc --- /dev/null +++ b/go/internal/pkgbits/reloc.go @@ -0,0 +1,42 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgbits + +// A RelocKind indicates a particular section within a unified IR export. +type RelocKind int + +// An Index represents a bitstream element index within a particular +// section. +type Index int + +// A relocEnt (relocation entry) is an entry in an element's local +// reference table. +// +// TODO(mdempsky): Rename this too. +type RelocEnt struct { + Kind RelocKind + Idx Index +} + +// Reserved indices within the meta relocation section. +const ( + PublicRootIdx Index = 0 + PrivateRootIdx Index = 1 +) + +const ( + RelocString RelocKind = iota + RelocMeta + RelocPosBase + RelocPkg + RelocName + RelocType + RelocObj + RelocObjExt + RelocObjDict + RelocBody + + numRelocs = iota +) diff --git a/go/internal/pkgbits/support.go b/go/internal/pkgbits/support.go new file mode 100644 index 00000000000..ad26d3b28ca --- /dev/null +++ b/go/internal/pkgbits/support.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgbits + +import "fmt" + +func assert(b bool) { + if !b { + panic("assertion failed") + } +} + +func errorf(format string, args ...interface{}) { + panic(fmt.Errorf(format, args...)) +} diff --git a/go/internal/pkgbits/sync.go b/go/internal/pkgbits/sync.go new file mode 100644 index 00000000000..5bd51ef7170 --- /dev/null +++ b/go/internal/pkgbits/sync.go @@ -0,0 +1,113 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgbits + +import ( + "fmt" + "strings" +) + +// fmtFrames formats a backtrace for reporting reader/writer desyncs. +func fmtFrames(pcs ...uintptr) []string { + res := make([]string, 0, len(pcs)) + walkFrames(pcs, func(file string, line int, name string, offset uintptr) { + // Trim package from function name. It's just redundant noise. + name = strings.TrimPrefix(name, "cmd/compile/internal/noder.") + + res = append(res, fmt.Sprintf("%s:%v: %s +0x%v", file, line, name, offset)) + }) + return res +} + +type frameVisitor func(file string, line int, name string, offset uintptr) + +// SyncMarker is an enum type that represents markers that may be +// written to export data to ensure the reader and writer stay +// synchronized. +type SyncMarker int + +//go:generate stringer -type=SyncMarker -trimprefix=Sync + +const ( + _ SyncMarker = iota + + // Public markers (known to go/types importers). + + // Low-level coding markers. + SyncEOF + SyncBool + SyncInt64 + SyncUint64 + SyncString + SyncValue + SyncVal + SyncRelocs + SyncReloc + SyncUseReloc + + // Higher-level object and type markers. + SyncPublic + SyncPos + SyncPosBase + SyncObject + SyncObject1 + SyncPkg + SyncPkgDef + SyncMethod + SyncType + SyncTypeIdx + SyncTypeParamNames + SyncSignature + SyncParams + SyncParam + SyncCodeObj + SyncSym + SyncLocalIdent + SyncSelector + + // Private markers (only known to cmd/compile). + SyncPrivate + + SyncFuncExt + SyncVarExt + SyncTypeExt + SyncPragma + + SyncExprList + SyncExprs + SyncExpr + SyncExprType + SyncAssign + SyncOp + SyncFuncLit + SyncCompLit + + SyncDecl + SyncFuncBody + SyncOpenScope + SyncCloseScope + SyncCloseAnotherScope + SyncDeclNames + SyncDeclName + + SyncStmts + SyncBlockStmt + SyncIfStmt + SyncForStmt + SyncSwitchStmt + SyncRangeStmt + SyncCaseClause + SyncCommClause + SyncSelectStmt + SyncDecls + SyncLabeledStmt + SyncUseObjLocal + SyncAddLocal + SyncLinkname + SyncStmt1 + SyncStmtsEnd + SyncLabel + SyncOptLabel +) diff --git a/go/internal/pkgbits/syncmarker_string.go b/go/internal/pkgbits/syncmarker_string.go new file mode 100644 index 00000000000..4a5b0ca5f2f --- /dev/null +++ b/go/internal/pkgbits/syncmarker_string.go @@ -0,0 +1,89 @@ +// Code generated by "stringer -type=SyncMarker -trimprefix=Sync"; DO NOT EDIT. + +package pkgbits + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[SyncEOF-1] + _ = x[SyncBool-2] + _ = x[SyncInt64-3] + _ = x[SyncUint64-4] + _ = x[SyncString-5] + _ = x[SyncValue-6] + _ = x[SyncVal-7] + _ = x[SyncRelocs-8] + _ = x[SyncReloc-9] + _ = x[SyncUseReloc-10] + _ = x[SyncPublic-11] + _ = x[SyncPos-12] + _ = x[SyncPosBase-13] + _ = x[SyncObject-14] + _ = x[SyncObject1-15] + _ = x[SyncPkg-16] + _ = x[SyncPkgDef-17] + _ = x[SyncMethod-18] + _ = x[SyncType-19] + _ = x[SyncTypeIdx-20] + _ = x[SyncTypeParamNames-21] + _ = x[SyncSignature-22] + _ = x[SyncParams-23] + _ = x[SyncParam-24] + _ = x[SyncCodeObj-25] + _ = x[SyncSym-26] + _ = x[SyncLocalIdent-27] + _ = x[SyncSelector-28] + _ = x[SyncPrivate-29] + _ = x[SyncFuncExt-30] + _ = x[SyncVarExt-31] + _ = x[SyncTypeExt-32] + _ = x[SyncPragma-33] + _ = x[SyncExprList-34] + _ = x[SyncExprs-35] + _ = x[SyncExpr-36] + _ = x[SyncExprType-37] + _ = x[SyncAssign-38] + _ = x[SyncOp-39] + _ = x[SyncFuncLit-40] + _ = x[SyncCompLit-41] + _ = x[SyncDecl-42] + _ = x[SyncFuncBody-43] + _ = x[SyncOpenScope-44] + _ = x[SyncCloseScope-45] + _ = x[SyncCloseAnotherScope-46] + _ = x[SyncDeclNames-47] + _ = x[SyncDeclName-48] + _ = x[SyncStmts-49] + _ = x[SyncBlockStmt-50] + _ = x[SyncIfStmt-51] + _ = x[SyncForStmt-52] + _ = x[SyncSwitchStmt-53] + _ = x[SyncRangeStmt-54] + _ = x[SyncCaseClause-55] + _ = x[SyncCommClause-56] + _ = x[SyncSelectStmt-57] + _ = x[SyncDecls-58] + _ = x[SyncLabeledStmt-59] + _ = x[SyncUseObjLocal-60] + _ = x[SyncAddLocal-61] + _ = x[SyncLinkname-62] + _ = x[SyncStmt1-63] + _ = x[SyncStmtsEnd-64] + _ = x[SyncLabel-65] + _ = x[SyncOptLabel-66] +} + +const _SyncMarker_name = "EOFBoolInt64Uint64StringValueValRelocsRelocUseRelocPublicPosPosBaseObjectObject1PkgPkgDefMethodTypeTypeIdxTypeParamNamesSignatureParamsParamCodeObjSymLocalIdentSelectorPrivateFuncExtVarExtTypeExtPragmaExprListExprsExprExprTypeAssignOpFuncLitCompLitDeclFuncBodyOpenScopeCloseScopeCloseAnotherScopeDeclNamesDeclNameStmtsBlockStmtIfStmtForStmtSwitchStmtRangeStmtCaseClauseCommClauseSelectStmtDeclsLabeledStmtUseObjLocalAddLocalLinknameStmt1StmtsEndLabelOptLabel" + +var _SyncMarker_index = [...]uint16{0, 3, 7, 12, 18, 24, 29, 32, 38, 43, 51, 57, 60, 67, 73, 80, 83, 89, 95, 99, 106, 120, 129, 135, 140, 147, 150, 160, 168, 175, 182, 188, 195, 201, 209, 214, 218, 226, 232, 234, 241, 248, 252, 260, 269, 279, 296, 305, 313, 318, 327, 333, 340, 350, 359, 369, 379, 389, 394, 405, 416, 424, 432, 437, 445, 450, 458} + +func (i SyncMarker) String() string { + i -= 1 + if i < 0 || i >= SyncMarker(len(_SyncMarker_index)-1) { + return "SyncMarker(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _SyncMarker_name[_SyncMarker_index[i]:_SyncMarker_index[i+1]] +} From 32129bf2c952acf093fdc20f81734e68d68ed8af Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 14 Jul 2022 19:30:14 -0400 Subject: [PATCH 109/723] go/internal/gcimporter: adjust importer to match compiler importer This is a port of CL 288632 to x/tools/go/internal/gcimporter. This logic was ostensibly unported to avoid breaking build compatibility with go1.12. Now that gopls no longer support 1.12, It is safe to make this change. Fixes golang/go#53803 Change-Id: Ic9b4d7a60511076a83d8fa72cf7c4a3bdcab3fce Reviewed-on: https://go-review.googlesource.com/c/tools/+/417580 Reviewed-by: Jamal Carvalho gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- go/internal/gcimporter/iimport.go | 60 +++++++++++-------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/go/internal/gcimporter/iimport.go b/go/internal/gcimporter/iimport.go index 28b91b86567..4caa0f55d9d 100644 --- a/go/internal/gcimporter/iimport.go +++ b/go/internal/gcimporter/iimport.go @@ -17,6 +17,7 @@ import ( "go/token" "go/types" "io" + "math/big" "sort" "strings" @@ -512,7 +513,9 @@ func (r *importReader) value() (typ types.Type, val constant.Value) { val = constant.MakeString(r.string()) case types.IsInteger: - val = r.mpint(b) + var x big.Int + r.mpint(&x, b) + val = constant.Make(&x) case types.IsFloat: val = r.mpfloat(b) @@ -561,8 +564,8 @@ func intSize(b *types.Basic) (signed bool, maxBytes uint) { return } -func (r *importReader) mpint(b *types.Basic) constant.Value { - signed, maxBytes := intSize(b) +func (r *importReader) mpint(x *big.Int, typ *types.Basic) { + signed, maxBytes := intSize(typ) maxSmall := 256 - maxBytes if signed { @@ -581,7 +584,8 @@ func (r *importReader) mpint(b *types.Basic) constant.Value { v = ^v } } - return constant.MakeInt64(v) + x.SetInt64(v) + return } v := -n @@ -591,47 +595,23 @@ func (r *importReader) mpint(b *types.Basic) constant.Value { if v < 1 || uint(v) > maxBytes { errorf("weird decoding: %v, %v => %v", n, signed, v) } - - buf := make([]byte, v) - io.ReadFull(&r.declReader, buf) - - // convert to little endian - // TODO(gri) go/constant should have a more direct conversion function - // (e.g., once it supports a big.Float based implementation) - for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 { - buf[i], buf[j] = buf[j], buf[i] - } - - x := constant.MakeFromBytes(buf) + b := make([]byte, v) + io.ReadFull(&r.declReader, b) + x.SetBytes(b) if signed && n&1 != 0 { - x = constant.UnaryOp(token.SUB, x, 0) + x.Neg(x) } - return x } -func (r *importReader) mpfloat(b *types.Basic) constant.Value { - x := r.mpint(b) - if constant.Sign(x) == 0 { - return x - } - - exp := r.int64() - switch { - case exp > 0: - x = constant.Shift(x, token.SHL, uint(exp)) - // Ensure that the imported Kind is Float, else this constant may run into - // bitsize limits on overlarge integers. Eventually we can instead adopt - // the approach of CL 288632, but that CL relies on go/constant APIs that - // were introduced in go1.13. - // - // TODO(rFindley): sync the logic here with tip Go once we no longer - // support go1.12. - x = constant.ToFloat(x) - case exp < 0: - d := constant.Shift(constant.MakeInt64(1), token.SHL, uint(-exp)) - x = constant.BinaryOp(x, token.QUO, d) +func (r *importReader) mpfloat(typ *types.Basic) constant.Value { + var mant big.Int + r.mpint(&mant, typ) + var f big.Float + f.SetInt(&mant) + if f.Sign() != 0 { + f.SetMantExp(&f, int(r.int64())) } - return x + return constant.Make(&f) } func (r *importReader) ident() string { From ce6ce766265714553035d328a9be4f313c63e5fd Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 15 Jul 2022 11:11:52 -0400 Subject: [PATCH 110/723] internal/lsp/regtest: increase the time allowed for shutdown Now that we await ongoing work during shutdown, we are seeing regtest flakes simply due to outstanding go command invocations. Allow more time for cleanup. If this is insufficient, we can be more aggressive about terminating go command processes when context is cancelled. For golang/go#53820 Change-Id: I3df3c5510dae34cb14a6efeb02c2963a71e64f3a Reviewed-on: https://go-review.googlesource.com/c/tools/+/417583 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Dylan Le Run-TryBot: Robert Findley --- internal/lsp/regtest/runner.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go index 0640e452fb1..b2992e99392 100644 --- a/internal/lsp/regtest/runner.go +++ b/internal/lsp/regtest/runner.go @@ -341,9 +341,13 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio } // For tests that failed due to a timeout, don't fail to shutdown // because ctx is done. - closeCtx, cancel := context.WithTimeout(xcontext.Detach(ctx), 5*time.Second) + // + // golang/go#53820: now that we await the completion of ongoing work in + // shutdown, we must allow a significant amount of time for ongoing go + // command invocations to exit. + ctx, cancel := context.WithTimeout(xcontext.Detach(ctx), 30*time.Second) defer cancel() - if err := env.Editor.Close(closeCtx); err != nil { + if err := env.Editor.Close(ctx); err != nil { pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) t.Errorf("closing editor: %v", err) } From dc45e742f0ab02c7fbaff52e51724f628ae84b84 Mon Sep 17 00:00:00 2001 From: Dylan Le Date: Mon, 27 Jun 2022 00:10:22 -0400 Subject: [PATCH 111/723] internal/lsp: Update FilterDisallow to support matching directories at arbitrary depth. In FilterDisallow, change filter to regex form to match with file paths. Add a unit regtest for FilterDisallow. For golang/go#46438 Change-Id: I7de1986c1cb1b65844828fa618b72b1e6b76b5b9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/414317 Run-TryBot: Dylan Le Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/lsp/cache/load.go | 6 +- internal/lsp/cache/view.go | 36 +++++---- internal/lsp/cache/view_test.go | 7 +- internal/lsp/source/options.go | 30 ++++++- internal/lsp/source/workspace_symbol.go | 73 +++++++++++++---- internal/lsp/source/workspace_symbol_test.go | 85 ++++++++++++++++++++ 6 files changed, 200 insertions(+), 37 deletions(-) diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 8937f934031..d0942b51bfe 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -156,6 +156,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } moduleErrs := make(map[string][]packages.Error) // module path -> errors + filterer := buildFilterer(s.view.rootURI.Filename(), s.view.gomodcache, s.view.options) newMetadata := make(map[PackageID]*KnownMetadata) for _, pkg := range pkgs { // The Go command returns synthetic list results for module queries that @@ -201,7 +202,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf // // TODO(rfindley): why exclude metadata arbitrarily here? It should be safe // to capture all metadata. - if s.view.allFilesExcluded(pkg) { + if s.view.allFilesExcluded(pkg, filterer) { continue } if err := buildMetadata(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, newMetadata, nil); err != nil { @@ -581,10 +582,11 @@ func containsPackageLocked(s *snapshot, m *Metadata) bool { uris[uri] = struct{}{} } + filterFunc := s.view.filterFunc() for uri := range uris { // Don't use view.contains here. go.work files may include modules // outside of the workspace folder. - if !strings.Contains(string(uri), "/vendor/") && !s.view.filters(uri) { + if !strings.Contains(string(uri), "/vendor/") && !filterFunc(uri) { return true } } diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 15a2f90da57..e33e3400451 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -385,13 +385,14 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) { relativeTo := s.view.folder.Filename() searched := 0 + filterer := buildFilterer(dir, s.view.gomodcache, s.view.options) // Change to WalkDir when we move up to 1.16 err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { if err != nil { return err } relpath := strings.TrimPrefix(path, relativeTo) - excluded := pathExcludedByFilter(relpath, dir, s.view.gomodcache, s.view.options) + excluded := pathExcludedByFilter(relpath, filterer) if fileHasExtension(path, suffixes) && !excluded && !fi.IsDir() { k := span.URIFromPath(path) _, err := s.GetVersionedFile(ctx, k) @@ -421,17 +422,20 @@ func (v *View) contains(uri span.URI) bool { return false } - return !v.filters(uri) + return !v.filterFunc()(uri) } -// filters reports whether uri is filtered by the currently configured +// filterFunc returns a func that reports whether uri is filtered by the currently configured // directoryFilters. -func (v *View) filters(uri span.URI) bool { - // Only filter relative to the configured root directory. - if source.InDirLex(v.folder.Filename(), uri.Filename()) { - return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), v.rootURI.Filename(), v.gomodcache, v.Options()) +func (v *View) filterFunc() func(span.URI) bool { + filterer := buildFilterer(v.rootURI.Filename(), v.gomodcache, v.Options()) + return func(uri span.URI) bool { + // Only filter relative to the configured root directory. + if source.InDirLex(v.folder.Filename(), uri.Filename()) { + return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), filterer) + } + return false } - return false } func (v *View) mapFile(uri span.URI, f *fileBase) { @@ -1070,15 +1074,14 @@ func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modConten return vendorEnabled, nil } -func (v *View) allFilesExcluded(pkg *packages.Package) bool { - opts := v.Options() +func (v *View) allFilesExcluded(pkg *packages.Package, filterer *source.Filterer) bool { folder := filepath.ToSlash(v.folder.Filename()) for _, f := range pkg.GoFiles { f = filepath.ToSlash(f) if !strings.HasPrefix(f, folder) { return false } - if !pathExcludedByFilter(strings.TrimPrefix(f, folder), v.rootURI.Filename(), v.gomodcache, opts) { + if !pathExcludedByFilter(strings.TrimPrefix(f, folder), filterer) { return false } } @@ -1086,8 +1089,9 @@ func (v *View) allFilesExcluded(pkg *packages.Package) bool { } func pathExcludedByFilterFunc(root, gomodcache string, opts *source.Options) func(string) bool { + filterer := buildFilterer(root, gomodcache, opts) return func(path string) bool { - return pathExcludedByFilter(path, root, gomodcache, opts) + return pathExcludedByFilter(path, filterer) } } @@ -1097,12 +1101,16 @@ func pathExcludedByFilterFunc(root, gomodcache string, opts *source.Options) fun // TODO(rfindley): passing root and gomodcache here makes it confusing whether // path should be absolute or relative, and has already caused at least one // bug. -func pathExcludedByFilter(path, root, gomodcache string, opts *source.Options) bool { +func pathExcludedByFilter(path string, filterer *source.Filterer) bool { path = strings.TrimPrefix(filepath.ToSlash(path), "/") + return filterer.Disallow(path) +} + +func buildFilterer(root, gomodcache string, opts *source.Options) *source.Filterer { gomodcache = strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(gomodcache, root)), "/") filters := opts.DirectoryFilters if gomodcache != "" { filters = append(filters, "-"+gomodcache) } - return source.FiltersDisallow(path, filters) + return source.NewFilterer(filters) } diff --git a/internal/lsp/cache/view_test.go b/internal/lsp/cache/view_test.go index d76dcda8ed4..59684ea3614 100644 --- a/internal/lsp/cache/view_test.go +++ b/internal/lsp/cache/view_test.go @@ -161,15 +161,14 @@ func TestFilters(t *testing.T) { } for _, tt := range tests { - opts := &source.Options{} - opts.DirectoryFilters = tt.filters + filterer := source.NewFilterer(tt.filters) for _, inc := range tt.included { - if pathExcludedByFilter(inc, "root", "root/gopath/pkg/mod", opts) { + if pathExcludedByFilter(inc, filterer) { t.Errorf("filters %q excluded %v, wanted included", tt.filters, inc) } } for _, exc := range tt.excluded { - if !pathExcludedByFilter(exc, "root", "root/gopath/pkg/mod", opts) { + if !pathExcludedByFilter(exc, filterer) { t.Errorf("filters %q included %v, wanted excluded", tt.filters, exc) } } diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 09401bc6ef8..c386eee45e7 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -806,6 +806,30 @@ func (o *Options) enableAllExperimentMaps() { } } +// validateDirectoryFilter validates if the filter string +// - is not empty +// - start with either + or - +// - doesn't contain currently unsupported glob operators: *, ? +func validateDirectoryFilter(ifilter string) (string, error) { + filter := fmt.Sprint(ifilter) + if filter == "" || (filter[0] != '+' && filter[0] != '-') { + return "", fmt.Errorf("invalid filter %v, must start with + or -", filter) + } + segs := strings.Split(filter, "/") + unsupportedOps := [...]string{"?", "*"} + for _, seg := range segs { + if seg != "**" { + for _, op := range unsupportedOps { + if strings.Contains(seg, op) { + return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue.", filter, op) + } + } + } + } + + return strings.TrimRight(filepath.FromSlash(filter), "/"), nil +} + func (o *Options) set(name string, value interface{}, seen map[string]struct{}) OptionResult { // Flatten the name in case we get options with a hierarchy. split := strings.Split(name, ".") @@ -850,9 +874,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) } var filters []string for _, ifilter := range ifilters { - filter := fmt.Sprint(ifilter) - if filter == "" || (filter[0] != '+' && filter[0] != '-') { - result.errorf("invalid filter %q, must start with + or -", filter) + filter, err := validateDirectoryFilter(fmt.Sprintf("%v", ifilter)) + if err != nil { + result.errorf(err.Error()) return result } filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/")) diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go index 6167c586a9a..6c33effc1ae 100644 --- a/internal/lsp/source/workspace_symbol.go +++ b/internal/lsp/source/workspace_symbol.go @@ -8,7 +8,9 @@ import ( "context" "fmt" "go/types" + "path" "path/filepath" + "regexp" "runtime" "sort" "strings" @@ -305,11 +307,12 @@ func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher roots = append(roots, strings.TrimRight(string(v.Folder()), "/")) filters := v.Options().DirectoryFilters + filterer := NewFilterer(filters) folder := filepath.ToSlash(v.Folder().Filename()) for uri, syms := range snapshot.Symbols(ctx) { norm := filepath.ToSlash(uri.Filename()) nm := strings.TrimPrefix(norm, folder) - if FiltersDisallow(nm, filters) { + if filterer.Disallow(nm) { continue } // Only scan each file once. @@ -358,28 +361,70 @@ func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher return unified.results(), nil } -// FilterDisallow is code from the body of cache.pathExcludedByFilter in cache/view.go -// Exporting and using that function would cause an import cycle. -// Moving it here and exporting it would leave behind view_test.go. -// (This code is exported and used in the body of cache.pathExcludedByFilter) -func FiltersDisallow(path string, filters []string) bool { +type Filterer struct { + // Whether a filter is excluded depends on the operator (first char of the raw filter). + // Slices filters and excluded then should have the same length. + filters []*regexp.Regexp + excluded []bool +} + +// NewFilterer computes regular expression form of all raw filters +func NewFilterer(rawFilters []string) *Filterer { + var f Filterer + for _, filter := range rawFilters { + filter = path.Clean(filepath.ToSlash(filter)) + op, prefix := filter[0], filter[1:] + // convertFilterToRegexp adds "/" at the end of prefix to handle cases where a filter is a prefix of another filter. + // For example, it prevents [+foobar, -foo] from excluding "foobar". + f.filters = append(f.filters, convertFilterToRegexp(filepath.ToSlash(prefix))) + f.excluded = append(f.excluded, op == '-') + } + + return &f +} + +// Disallow return true if the path is excluded from the filterer's filters. +func (f *Filterer) Disallow(path string) bool { path = strings.TrimPrefix(path, "/") var excluded bool - for _, filter := range filters { - op, prefix := filter[0], filter[1:] - // Non-empty prefixes have to be precise directory matches. - if prefix != "" { - prefix = prefix + "/" - path = path + "/" + + for i, filter := range f.filters { + path := path + if !strings.HasSuffix(path, "/") { + path += "/" } - if !strings.HasPrefix(path, prefix) { + if !filter.MatchString(path) { continue } - excluded = op == '-' + excluded = f.excluded[i] } + return excluded } +// convertFilterToRegexp replaces glob-like operator substrings in a string file path to their equivalent regex forms. +// Supporting glob-like operators: +// - **: match zero or more complete path segments +func convertFilterToRegexp(filter string) *regexp.Regexp { + var ret strings.Builder + segs := strings.Split(filter, "/") + for i, seg := range segs { + if seg == "**" { + switch i { + case 0: + ret.WriteString("^.*") + default: + ret.WriteString(".*") + } + } else { + ret.WriteString(regexp.QuoteMeta(seg)) + } + ret.WriteString("/") + } + + return regexp.MustCompile(ret.String()) +} + // symbolFile holds symbol information for a single file. type symbolFile struct { uri span.URI diff --git a/internal/lsp/source/workspace_symbol_test.go b/internal/lsp/source/workspace_symbol_test.go index 314ef785df3..633550ed945 100644 --- a/internal/lsp/source/workspace_symbol_test.go +++ b/internal/lsp/source/workspace_symbol_test.go @@ -44,3 +44,88 @@ func TestParseQuery(t *testing.T) { } } } + +func TestFiltererDisallow(t *testing.T) { + tests := []struct { + filters []string + included []string + excluded []string + }{ + { + []string{"+**/c.go"}, + []string{"a/c.go", "a/b/c.go"}, + []string{}, + }, + { + []string{"+a/**/c.go"}, + []string{"a/b/c.go", "a/b/d/c.go", "a/c.go"}, + []string{}, + }, + { + []string{"-a/c.go", "+a/**"}, + []string{"a/c.go"}, + []string{}, + }, + { + []string{"+a/**/c.go", "-**/c.go"}, + []string{}, + []string{"a/b/c.go"}, + }, + { + []string{"+a/**/c.go", "-a/**"}, + []string{}, + []string{"a/b/c.go"}, + }, + { + []string{"+**/c.go", "-a/**/c.go"}, + []string{}, + []string{"a/b/c.go"}, + }, + { + []string{"+foobar", "-foo"}, + []string{"foobar", "foobar/a"}, + []string{"foo", "foo/a"}, + }, + { + []string{"+", "-"}, + []string{}, + []string{"foobar", "foobar/a", "foo", "foo/a"}, + }, + { + []string{"-", "+"}, + []string{"foobar", "foobar/a", "foo", "foo/a"}, + []string{}, + }, + { + []string{"-a/**/b/**/c.go"}, + []string{}, + []string{"a/x/y/z/b/f/g/h/c.go"}, + }, + // tests for unsupported glob operators + { + []string{"+**/c.go", "-a/*/c.go"}, + []string{"a/b/c.go"}, + []string{}, + }, + { + []string{"+**/c.go", "-a/?/c.go"}, + []string{"a/b/c.go"}, + []string{}, + }, + } + + for _, test := range tests { + filterer := NewFilterer(test.filters) + for _, inc := range test.included { + if filterer.Disallow(inc) { + t.Errorf("Filters %v excluded %v, wanted included", test.filters, inc) + } + } + + for _, exc := range test.excluded { + if !filterer.Disallow(exc) { + t.Errorf("Filters %v included %v, wanted excluded", test.filters, exc) + } + } + } +} From 2eaea86599c644bbe537e9548d6b000d331803a1 Mon Sep 17 00:00:00 2001 From: Zvonimir Pavlinovic Date: Wed, 13 Jul 2022 14:59:25 -0700 Subject: [PATCH 112/723] go/callgraph/vta: do not include interface types during propagation Some interface types could be propagated around type graph. This does not affect precision as the results are ultimately intersected with the initial call graph. However, this types of propagation are not necessary. On very large projects, this can save few seconds. Change-Id: I73d41c082a52734f50669af19fee940725aed662 Reviewed-on: https://go-review.googlesource.com/c/tools/+/417514 Reviewed-by: Tim King gopls-CI: kokoro Run-TryBot: Zvonimir Pavlinovic TryBot-Result: Gopher Robot --- go/callgraph/vta/graph.go | 4 +++- go/callgraph/vta/propagation.go | 12 ++++++++++++ go/callgraph/vta/utils.go | 12 ------------ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 48547a52527..8a338e97d23 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -687,7 +687,9 @@ func (b *builder) nodeFromVal(val ssa.Value) node { // semantically equivalent types can have different implementations, // this method guarantees the same implementation is always used. func (b *builder) representative(n node) node { - if !hasInitialTypes(n) { + if n.Type() == nil { + // panicArg and recoverReturn do not have + // types and are unique by definition. return n } t := canonicalize(n.Type(), &b.canon) diff --git a/go/callgraph/vta/propagation.go b/go/callgraph/vta/propagation.go index 5934ebc2167..b3953601341 100644 --- a/go/callgraph/vta/propagation.go +++ b/go/callgraph/vta/propagation.go @@ -175,6 +175,18 @@ func nodeTypes(nodes []node, builder *trie.Builder, propTypeId func(p propType) return &typeSet } +// hasInitialTypes check if a node can have initial types. +// Returns true iff `n` is not a panic, recover, nestedPtr* +// node, nor a node whose type is an interface. +func hasInitialTypes(n node) bool { + switch n.(type) { + case panicArg, recoverReturn, nestedPtrFunction, nestedPtrInterface: + return false + default: + return !isInterface(n.Type()) + } +} + // getPropType creates a propType for `node` based on its type. // propType.typ is always node.Type(). If node is function, then // propType.val is the underlying function; nil otherwise. diff --git a/go/callgraph/vta/utils.go b/go/callgraph/vta/utils.go index 0531a227f6c..c8f0a47adf6 100644 --- a/go/callgraph/vta/utils.go +++ b/go/callgraph/vta/utils.go @@ -59,18 +59,6 @@ func hasInFlow(n node) bool { return isInterface(t) || isFunction(t) } -// hasInitialTypes check if a node can have initial types. -// Returns true iff `n` is not a panic or recover node as -// those are artificial. -func hasInitialTypes(n node) bool { - switch n.(type) { - case panicArg, recoverReturn: - return false - default: - return true - } -} - func isInterface(t types.Type) bool { _, ok := t.Underlying().(*types.Interface) return ok From 2957e9da5d156b5d2bfb51fded9c29a9fa3358f5 Mon Sep 17 00:00:00 2001 From: Zvonimir Pavlinovic Date: Thu, 14 Jul 2022 15:58:13 -0700 Subject: [PATCH 113/723] go/callgraph/vta: use types.IsInterface instead of our own isInterface Change-Id: I9e5a81e4f59f32e3bfc6baf2348ee3e4db411aae Reviewed-on: https://go-review.googlesource.com/c/tools/+/417674 gopls-CI: kokoro Reviewed-by: Tim King Run-TryBot: Zvonimir Pavlinovic TryBot-Result: Gopher Robot --- go/callgraph/vta/graph.go | 2 +- go/callgraph/vta/propagation.go | 2 +- go/callgraph/vta/propagation_test.go | 2 +- go/callgraph/vta/utils.go | 9 ++------- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 8a338e97d23..4d0387f12bd 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -654,7 +654,7 @@ func (b *builder) addInFlowEdge(s, d node) { // Creates const, pointer, global, func, and local nodes based on register instructions. func (b *builder) nodeFromVal(val ssa.Value) node { - if p, ok := val.Type().(*types.Pointer); ok && !isInterface(p.Elem()) && !isFunction(p.Elem()) { + if p, ok := val.Type().(*types.Pointer); ok && !types.IsInterface(p.Elem()) && !isFunction(p.Elem()) { // Nested pointer to interfaces are modeled as a special // nestedPtrInterface node. if i := interfaceUnderPtr(p.Elem()); i != nil { diff --git a/go/callgraph/vta/propagation.go b/go/callgraph/vta/propagation.go index b3953601341..6127780ac4e 100644 --- a/go/callgraph/vta/propagation.go +++ b/go/callgraph/vta/propagation.go @@ -183,7 +183,7 @@ func hasInitialTypes(n node) bool { case panicArg, recoverReturn, nestedPtrFunction, nestedPtrInterface: return false default: - return !isInterface(n.Type()) + return !types.IsInterface(n.Type()) } } diff --git a/go/callgraph/vta/propagation_test.go b/go/callgraph/vta/propagation_test.go index 00b21277f22..f4a754f9663 100644 --- a/go/callgraph/vta/propagation_test.go +++ b/go/callgraph/vta/propagation_test.go @@ -58,7 +58,7 @@ func newLocal(name string, t types.Type) local { // newNamedType creates a bogus type named `name`. func newNamedType(name string) *types.Named { - return types.NewNamed(types.NewTypeName(token.NoPos, nil, name, nil), nil, nil) + return types.NewNamed(types.NewTypeName(token.NoPos, nil, name, nil), types.Universe.Lookup("int").Type(), nil) } // sccString is a utility for stringifying `nodeToScc`. Every diff --git a/go/callgraph/vta/utils.go b/go/callgraph/vta/utils.go index c8f0a47adf6..c0b5775907f 100644 --- a/go/callgraph/vta/utils.go +++ b/go/callgraph/vta/utils.go @@ -56,12 +56,7 @@ func hasInFlow(n node) bool { return true } - return isInterface(t) || isFunction(t) -} - -func isInterface(t types.Type) bool { - _, ok := t.Underlying().(*types.Interface) - return ok + return types.IsInterface(t) || isFunction(t) } func isFunction(t types.Type) bool { @@ -86,7 +81,7 @@ func interfaceUnderPtr(t types.Type) types.Type { return nil } - if isInterface(p.Elem()) { + if types.IsInterface(p.Elem()) { return p.Elem() } From 79f3242e4b2ee6f1bd987fdd0538e16451f7523e Mon Sep 17 00:00:00 2001 From: aarzilli Date: Thu, 9 Jun 2022 15:19:50 +0200 Subject: [PATCH 114/723] godoc: support go1.19 doc comment syntax Call go/doc.(*Package).HTML on go1.19 instead of the deprecated go/doc.ToHTML. Change-Id: Ie604d9ace7adc179f7c2e345f17a2e0c0365d1a2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411381 Run-TryBot: Alessandro Arzilli gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Jonathan Amsterdam Reviewed-by: Jenny Rakoczy --- godoc/godoc.go | 9 +++++++-- godoc/static/package.html | 22 +++++++++++----------- godoc/static/searchdoc.html | 2 +- godoc/static/static.go | 4 ++-- godoc/tohtml_go119.go | 17 +++++++++++++++++ godoc/tohtml_other.go | 17 +++++++++++++++++ 6 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 godoc/tohtml_go119.go create mode 100644 godoc/tohtml_other.go diff --git a/godoc/godoc.go b/godoc/godoc.go index 7ff2eab6239..6edb8f93ce9 100644 --- a/godoc/godoc.go +++ b/godoc/godoc.go @@ -345,11 +345,16 @@ func isDigit(ch rune) bool { return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch) } -func comment_htmlFunc(comment string) string { +func comment_htmlFunc(info *PageInfo, comment string) string { var buf bytes.Buffer // TODO(gri) Provide list of words (e.g. function parameters) // to be emphasized by ToHTML. - doc.ToHTML(&buf, comment, nil) // does html-escaping + + // godocToHTML is: + // - buf.Write(info.PDoc.HTML(comment)) on go1.19 + // - go/doc.ToHTML(&buf, comment, nil) on other versions + godocToHTML(&buf, info.PDoc, comment) + return buf.String() } diff --git a/godoc/static/package.html b/godoc/static/package.html index 86445df4c08..a04b08b63f5 100644 --- a/godoc/static/package.html +++ b/godoc/static/package.html @@ -17,7 +17,7 @@ {{if $.IsMain}} {{/* command documentation */}} - {{comment_html .Doc}} + {{comment_html $ .Doc}} {{else}} {{/* package documentation */}}
@@ -42,7 +42,7 @@

Overview ▹

Overview ▾

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

Inter {{with .Consts}}

Constants

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

Variables

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

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

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

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

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

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

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

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

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

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

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

{{$key.Name}}

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

{{comment_html .Doc}}

+

{{comment_html $ .Doc}}

{{else}}

No documentation available

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

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

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

Examples

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

Package\x20files

\x0a\x09\x09\x09

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

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

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

\x0a\x09\x09\x09

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

\x0a\x09\x09\x09

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

\x0a\x09\x09\x09

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

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

    Examples

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

    Package\x20files

    \x0a\x09\x09\x09

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

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

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

    \x0a\x09\x09\x09

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

    \x0a\x09\x09\x09

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

    \x0a\x09\x09\x09

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

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

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

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

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

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

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

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

    {{comment_html\x20.Doc}}

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

    No\x20documentation\x20available

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

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

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

    No\x20documentation\x20available

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

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

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

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

    \x0a{{end}}\x0a", diff --git a/godoc/tohtml_go119.go b/godoc/tohtml_go119.go new file mode 100644 index 00000000000..6dbf7212b9a --- /dev/null +++ b/godoc/tohtml_go119.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package godoc + +import ( + "bytes" + "go/doc" +) + +func godocToHTML(buf *bytes.Buffer, pkg *doc.Package, comment string) { + buf.Write(pkg.HTML(comment)) +} diff --git a/godoc/tohtml_other.go b/godoc/tohtml_other.go new file mode 100644 index 00000000000..a1dcf2e195b --- /dev/null +++ b/godoc/tohtml_other.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.19 +// +build !go1.19 + +package godoc + +import ( + "bytes" + "go/doc" +) + +func godocToHTML(buf *bytes.Buffer, pkg *doc.Package, comment string) { + doc.ToHTML(buf, comment, nil) +} From ec1f92440bcb00d4cf10fee76edcbcee7c88b20c Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 19 Jul 2022 17:38:06 -0400 Subject: [PATCH 115/723] internal/lsp: add check for nil results to fillreturns Avoid panicking when allocating an array for a nil results list by returning early. Change-Id: I26953b5cef7832bad3006bd316d59978a5d94cbd Reviewed-on: https://go-review.googlesource.com/c/tools/+/418416 Run-TryBot: Suzy Mueller Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- internal/lsp/analysis/fillreturns/fillreturns.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/lsp/analysis/fillreturns/fillreturns.go b/internal/lsp/analysis/fillreturns/fillreturns.go index 4a30934c63c..705ae124d57 100644 --- a/internal/lsp/analysis/fillreturns/fillreturns.go +++ b/internal/lsp/analysis/fillreturns/fillreturns.go @@ -113,7 +113,7 @@ outer: break } } - if enclosingFunc == nil { + if enclosingFunc == nil || enclosingFunc.Results == nil { continue } From 980cbfeacb418cfa8899193c93fd74d8d4211165 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Fri, 22 Jul 2022 11:11:11 -0400 Subject: [PATCH 116/723] A+C: delete AUTHORS and CONTRIBUTORS In 2009, Google's open-source lawyers asked us to create the AUTHORS file to define "The Go Authors", and the CONTRIBUTORS file was in keeping with open source best practices of the time. Re-reviewing our repos now in 2022, the open-source lawyers are comfortable with source control history taking the place of the AUTHORS file, and most open source projects no longer maintain CONTRIBUTORS files. To ease maintenance, remove AUTHORS and CONTRIBUTORS from all repos. For golang/go#53961. Change-Id: Icaaaf04cc7884b479c7fd1231c53c8bf1f349623 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419105 gopls-CI: kokoro Run-TryBot: Russ Cox Reviewed-by: David Chase TryBot-Result: Gopher Robot --- AUTHORS | 3 --- CONTRIBUTORS | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 AUTHORS delete mode 100644 CONTRIBUTORS diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 15167cd746c..00000000000 --- a/AUTHORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code refers to The Go Authors for copyright purposes. -# The master list of authors is in the main Go distribution, -# visible at http://tip.golang.org/AUTHORS. diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 1c4577e9680..00000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code was written by the Go contributors. -# The master list of contributors is in the main Go distribution, -# visible at http://tip.golang.org/CONTRIBUTORS. From a732e45cc7309582c43c3cfc2db4c73eed18aa76 Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Wed, 20 Jul 2022 18:39:28 +0000 Subject: [PATCH 117/723] gopls: update golang.org/x/vuln For golang/go#53869. Change-Id: If173f0e7e3a06c11cd358ff9917c73b50d2dcb28 Reviewed-on: https://go-review.googlesource.com/c/tools/+/418536 Run-TryBot: Jamal Carvalho gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim --- gopls/go.mod | 2 +- gopls/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gopls/go.mod b/gopls/go.mod index bd118e226bb..020386fca28 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -10,7 +10,7 @@ require ( golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 golang.org/x/sys v0.0.0-20220209214540-3681064d5158 golang.org/x/tools v0.1.11-0.20220523181440-ccb10502d1a5 - golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c + golang.org/x/vuln v0.0.0-20220718121659-b9a3ad919698 honnef.co/go/tools v0.3.2 mvdan.cc/gofumpt v0.3.0 mvdan.cc/xurls/v2 v2.4.0 diff --git a/gopls/go.sum b/gopls/go.sum index 73a55fbad82..f263f9e0d00 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -75,6 +75,8 @@ golang.org/x/vuln v0.0.0-20220503210553-a5481fb0c8be h1:jokAF1mfylAi1iTQx7C44B7v golang.org/x/vuln v0.0.0-20220503210553-a5481fb0c8be/go.mod h1:twca1SxmF6/i2wHY/mj1vLIkkHdp+nil/yA32ZOP4kg= golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c h1:r5bbIROBQtRRgoutV8Q3sFY58VzHW6jMBYl48ANSyS4= golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c/go.mod h1:UZshlUPxXeGUM9I14UOawXQg6yosDE9cr1vKY/DzgWo= +golang.org/x/vuln v0.0.0-20220718121659-b9a3ad919698 h1:9lgpkUgjzoIcZYp7/UPFO/0jIlYcokcEjqWm0hj9pzE= +golang.org/x/vuln v0.0.0-20220718121659-b9a3ad919698/go.mod h1:UZshlUPxXeGUM9I14UOawXQg6yosDE9cr1vKY/DzgWo= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= From 126ef8f8644b6482a6c6d65a4e373fa96bd36bfb Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Wed, 20 Jul 2022 18:40:41 +0000 Subject: [PATCH 118/723] gopls/internal/govulncheck: sync x/vuln@b9a3ad9 For golang/go#53869 Change-Id: I8cf795b792380596be306b2437e26faf990cff8b Reviewed-on: https://go-review.googlesource.com/c/tools/+/418537 Reviewed-by: Hyang-Ah Hana Kim Run-TryBot: Jamal Carvalho TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/govulncheck/README.md | 2 ++ gopls/internal/govulncheck/source.go | 22 ---------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/gopls/internal/govulncheck/README.md b/gopls/internal/govulncheck/README.md index d8339c506f6..bc10d8a2ec1 100644 --- a/gopls/internal/govulncheck/README.md +++ b/gopls/internal/govulncheck/README.md @@ -15,3 +15,5 @@ The `copy.sh` does the copying, after removing all .go files here. To use it: 2. cd to this directory. 3. Run `copy.sh`. + +4. Re-add build tags for go1.18 \ No newline at end of file diff --git a/gopls/internal/govulncheck/source.go b/gopls/internal/govulncheck/source.go index 23028b9eb42..d51fe8c0c2d 100644 --- a/gopls/internal/govulncheck/source.go +++ b/gopls/internal/govulncheck/source.go @@ -8,13 +8,11 @@ package govulncheck import ( - "context" "fmt" "sort" "strings" "golang.org/x/tools/go/packages" - "golang.org/x/vuln/client" "golang.org/x/vuln/vulncheck" ) @@ -57,26 +55,6 @@ func LoadPackages(cfg *packages.Config, patterns ...string) ([]*vulncheck.Packag return vpkgs, err } -// Source calls vulncheck.Source on the Go source in pkgs. It returns the result -// with Vulns trimmed to those that are actually called. -// -// This function is being used by the Go IDE team. -func Source(ctx context.Context, pkgs []*vulncheck.Package, c client.Client) (*vulncheck.Result, error) { - r, err := vulncheck.Source(ctx, pkgs, &vulncheck.Config{Client: c}) - if err != nil { - return nil, err - } - // Keep only the vulns that are called. - var vulns []*vulncheck.Vuln - for _, v := range r.Vulns { - if v.CallSink != 0 { - vulns = append(vulns, v) - } - } - r.Vulns = vulns - return r, nil -} - // CallInfo is information about calls to vulnerable functions. type CallInfo struct { // CallStacks contains all call stacks to vulnerable functions. From 7b605f471d9c0aa0acd165f72e32f0897fc6eece Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Wed, 20 Jul 2022 18:49:20 +0000 Subject: [PATCH 119/723] gopls/internal/vulncheck: pass go version to vulncheck config For golang/go#53869 Change-Id: I8c34adaf81415dafb724ca67fa731912832c3ee5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/418538 Run-TryBot: Jamal Carvalho TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/vulncheck/command.go | 2 +- gopls/internal/vulncheck/util.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 53bf0f03860..a29bc008c7a 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -84,7 +84,7 @@ func (c *cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) log.Printf("analyzing %d packages...\n", len(loadedPkgs)) - r, err := vulncheck.Source(ctx, loadedPkgs, &vulncheck.Config{Client: c.Client}) + r, err := vulncheck.Source(ctx, loadedPkgs, &vulncheck.Config{Client: c.Client, SourceGoVersion: goVersion()}) if err != nil { return nil, err } diff --git a/gopls/internal/vulncheck/util.go b/gopls/internal/vulncheck/util.go index c329461894e..05332d375c3 100644 --- a/gopls/internal/vulncheck/util.go +++ b/gopls/internal/vulncheck/util.go @@ -8,8 +8,11 @@ package vulncheck import ( + "bytes" "fmt" "go/token" + "os" + "os/exec" gvc "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/internal/lsp/protocol" @@ -80,3 +83,16 @@ func posToPosition(pos *token.Position) (p protocol.Position) { } return p } + +func goVersion() string { + if v := os.Getenv("GOVERSION"); v != "" { + // Unlikely to happen in practice, mostly used for testing. + return v + } + out, err := exec.Command("go", "env", "GOVERSION").Output() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to determine go version; skipping stdlib scanning: %v\n", err) + return "" + } + return string(bytes.TrimSpace(out)) +} From a2a24778ba9562d210892fad5bf136c0bdbd233c Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 14 Jul 2022 10:44:30 -0400 Subject: [PATCH 120/723] gopls/internal/regtest: externalize shouldLoad tracking The fundamental bug causing TestChangePackageName to fail has been fixed, yet unskipping it revealed a new bug: tracking whether or not a package should be loaded requires that we actually store that package in s.meta. In cases where we drop metadata, we also lose the information that a package path needs to be reloaded. Fix this by significantly reworking the tracking of pending loads, to simplify the code and separate the reloading logic from the logic of tracking metadata. As a nice side-effect, this eliminates the needless work necessary to mark/unmark packages as needing loading, since this is no longer tracked by the immutable metadata graph. Additionally, eliminate the "shouldLoad" guard inside of snapshot.load. We should never ask for loads that we do not want, and the shouldLoad guard either masks bugs or leads to bugs. For example, we would repeatedly call load from reloadOrphanedFiles for files that are part of a package that needs loading, because we skip loading the file scope. Lift the responsibility for determining if we should load to the callers of load. Along the way, make a few additional minor improvements: - simplify the code where possible - leave TODOs for likely bugs or things that should be simplified in the future - reduce the overly granular locking in getOrLoadIDsForURI, which could lead to strange races - remove a stale comment for a test that is no longer flaky. Updates golang/go#53878 Change-Id: I6d9084806f1fdebc43002c7cc75dc1b94f8514b9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/417576 Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot --- .../regtest/diagnostics/diagnostics_test.go | 15 +- gopls/internal/regtest/misc/vendor_test.go | 10 - internal/lsp/cache/load.go | 15 -- internal/lsp/cache/metadata.go | 12 +- internal/lsp/cache/snapshot.go | 225 +++++++++--------- internal/lsp/source/util.go | 4 + 6 files changed, 129 insertions(+), 152 deletions(-) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index b9dc2d434b2..ae8b4a56cdd 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -703,7 +703,8 @@ func main() { // Test for golang/go#38207. func TestNewModule_Issue38207(t *testing.T) { - testenv.NeedsGo1Point(t, 14) + // Fails at Go 1.14 following CL 417576. Not investigated. + testenv.NeedsGo1Point(t, 15) const emptyFile = ` -- go.mod -- module mod.com @@ -874,7 +875,7 @@ func TestX(t *testing.T) { } func TestChangePackageName(t *testing.T) { - t.Skip("This issue hasn't been fixed yet. See golang.org/issue/41061.") + testenv.NeedsGo1Point(t, 16) // needs native overlay support const mod = ` -- go.mod -- @@ -889,15 +890,11 @@ package foo_ Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("foo/bar_test.go") env.RegexpReplace("foo/bar_test.go", "package foo_", "package foo_test") - env.SaveBuffer("foo/bar_test.go") env.Await( OnceMet( - env.DoneWithSave(), - NoDiagnostics("foo/bar_test.go"), - ), - OnceMet( - env.DoneWithSave(), - NoDiagnostics("foo/foo.go"), + env.DoneWithChange(), + EmptyOrNoDiagnostics("foo/bar_test.go"), + EmptyOrNoDiagnostics("foo/foo.go"), ), ) }) diff --git a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go index 324a8006fa7..4e02799b47a 100644 --- a/gopls/internal/regtest/misc/vendor_test.go +++ b/gopls/internal/regtest/misc/vendor_test.go @@ -27,16 +27,6 @@ var Goodbye error func TestInconsistentVendoring(t *testing.T) { testenv.NeedsGo1Point(t, 14) - // TODO(golang/go#49646): delete this comment once this test is stable. - // - // In golang/go#49646, this test is reported as flaky on Windows. We believe - // this is due to file contention from go mod vendor that should be resolved. - // If this test proves to still be flaky, skip it. - // - // if runtime.GOOS == "windows" { - // t.Skipf("skipping test due to flakiness on Windows: https://golang.org/issue/49646") - // } - const pkgThatUsesVendoring = ` -- go.mod -- module mod.com diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index d0942b51bfe..79789fe60d0 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -7,7 +7,6 @@ package cache import ( "bytes" "context" - "errors" "fmt" "io/ioutil" "os" @@ -41,24 +40,10 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf var query []string var containsDir bool // for logging - // Unless the context was canceled, set "shouldLoad" to false for all - // of the metadata we attempted to load. - defer func() { - if errors.Is(err, context.Canceled) { - return - } - // TODO(rfindley): merge these metadata updates with the updates below, to - // avoid updating the graph twice. - s.clearShouldLoad(scopes...) - }() - // Keep track of module query -> module path so that we can later correlate query // errors with errors. moduleQueries := make(map[string]string) for _, scope := range scopes { - if !s.shouldLoad(scope) { - continue - } switch scope := scope.(type) { case PackagePath: if source.IsCommandLineArguments(string(scope)) { diff --git a/internal/lsp/cache/metadata.go b/internal/lsp/cache/metadata.go index 486035f9390..7d9192f43a2 100644 --- a/internal/lsp/cache/metadata.go +++ b/internal/lsp/cache/metadata.go @@ -29,7 +29,7 @@ type Metadata struct { Name PackageName GoFiles []span.URI CompiledGoFiles []span.URI - ForTest PackagePath + ForTest PackagePath // package path under test, or "" TypesSizes types.Sizes Errors []packages.Error Deps []PackageID // direct dependencies, in string order @@ -94,12 +94,8 @@ type KnownMetadata struct { // PkgFilesChanged reports whether the file set of this metadata has // potentially changed. - PkgFilesChanged bool - - // ShouldLoad is true if the given metadata should be reloaded. // - // Note that ShouldLoad is different from !Valid: when we try to load a - // package, we mark ShouldLoad = false regardless of whether the load - // succeeded, to prevent endless loads. - ShouldLoad bool + // TODO(rfindley): this is used for WorkspacePackages, and looks fishy: we + // should probably only consider valid packages to be workspace packages. + PkgFilesChanged bool } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index a516860aafc..64e7f17c994 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -117,6 +117,13 @@ type snapshot struct { // when the view is created. workspacePackages map[PackageID]PackagePath + // shouldLoad tracks packages that need to be reloaded, mapping a PackageID + // to the package paths that should be used to reload it + // + // When we try to load a package, we clear it from the shouldLoad map + // regardless of whether the load succeeded, to prevent endless loads. + shouldLoad map[PackageID][]PackagePath + // unloadableFiles keeps track of files that we've failed to load. unloadableFiles map[span.URI]struct{} @@ -643,6 +650,10 @@ func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source } func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]*packageHandle, error) { + // TODO(rfindley): why can't/shouldn't we awaitLoaded here? It seems that if + // we ask for package handles for a file, we should wait for pending loads. + // Else we will reload orphaned files before the initial load completes. + // Check if we should reload metadata for the file. We don't invalidate IDs // (though we should), so the IDs will be a better source of truth than the // metadata. If there are no IDs for the file, then we should also reload. @@ -691,13 +702,13 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode } func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]PackageID, error) { - knownIDs := s.getIDsForURI(uri) - reload := len(knownIDs) == 0 - for _, id := range knownIDs { - // Reload package metadata if any of the metadata has missing - // dependencies, in case something has changed since the last time we - // reloaded it. - if s.noValidMetadataForID(id) { + s.mu.Lock() + ids := s.meta.ids[uri] + reload := len(ids) == 0 + for _, id := range ids { + // If the file is part of a package that needs reloading, reload it now to + // improve our responsiveness. + if len(s.shouldLoad[id]) > 0 { reload = true break } @@ -705,20 +716,38 @@ func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]Pack // missing dependencies. This is expensive and results in too many // calls to packages.Load. Determine what we should do instead. } + s.mu.Unlock() + if reload { - err := s.load(ctx, false, fileURI(uri)) + scope := fileURI(uri) + err := s.load(ctx, false, scope) + + // As in reloadWorkspace, we must clear scopes after loading. + // + // TODO(rfindley): simply call reloadWorkspace here, first, to avoid this + // duplication. + if !errors.Is(err, context.Canceled) { + s.clearShouldLoad(scope) + } + // TODO(rfindley): this doesn't look right. If we don't reload, we use + // invalid metadata anyway, but if we DO reload and it fails, we don't? if !s.useInvalidMetadata() && err != nil { return nil, err } + + s.mu.Lock() + ids = s.meta.ids[uri] + s.mu.Unlock() + // We've tried to reload and there are still no known IDs for the URI. // Return the load error, if there was one. - knownIDs = s.getIDsForURI(uri) - if len(knownIDs) == 0 { + if len(ids) == 0 { return nil, err } } - return knownIDs, nil + + return ids, nil } // Only use invalid metadata for Go versions >= 1.13. Go 1.12 and below has @@ -1153,13 +1182,6 @@ func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { return match } -func (s *snapshot) getIDsForURI(uri span.URI) []PackageID { - s.mu.Lock() - defer s.mu.Unlock() - - return s.meta.ids[uri] -} - func (s *snapshot) getMetadata(id PackageID) *KnownMetadata { s.mu.Lock() defer s.mu.Unlock() @@ -1167,78 +1189,34 @@ func (s *snapshot) getMetadata(id PackageID) *KnownMetadata { return s.meta.metadata[id] } -func (s *snapshot) shouldLoad(scope interface{}) bool { - s.mu.Lock() - defer s.mu.Unlock() - - g := s.meta - - switch scope := scope.(type) { - case PackagePath: - var meta *KnownMetadata - for _, m := range g.metadata { - if m.PkgPath != scope { - continue - } - meta = m - } - if meta == nil || meta.ShouldLoad { - return true - } - return false - case fileURI: - uri := span.URI(scope) - ids := g.ids[uri] - if len(ids) == 0 { - return true - } - for _, id := range ids { - m, ok := g.metadata[id] - if !ok || m.ShouldLoad { - return true - } - } - return false - default: - return true - } -} - +// clearShouldLoad clears package IDs that no longer need to be reloaded after +// scopes has been loaded. func (s *snapshot) clearShouldLoad(scopes ...interface{}) { s.mu.Lock() defer s.mu.Unlock() - g := s.meta - - var updates map[PackageID]*KnownMetadata - markLoaded := func(m *KnownMetadata) { - if updates == nil { - updates = make(map[PackageID]*KnownMetadata) - } - next := *m - next.ShouldLoad = false - updates[next.ID] = &next - } for _, scope := range scopes { switch scope := scope.(type) { case PackagePath: - for _, m := range g.metadata { - if m.PkgPath == scope { - markLoaded(m) + var toDelete []PackageID + for id, pkgPaths := range s.shouldLoad { + for _, pkgPath := range pkgPaths { + if pkgPath == scope { + toDelete = append(toDelete, id) + } } } + for _, id := range toDelete { + delete(s.shouldLoad, id) + } case fileURI: uri := span.URI(scope) - ids := g.ids[uri] + ids := s.meta.ids[uri] for _, id := range ids { - if m, ok := g.metadata[id]; ok { - markLoaded(m) - } + delete(s.shouldLoad, id) } } } - s.meta = g.Clone(updates) - s.resetIsActivePackageLocked() } // noValidMetadataForURILocked reports whether there is any valid metadata for @@ -1256,16 +1234,6 @@ func (s *snapshot) noValidMetadataForURILocked(uri span.URI) bool { return true } -// noValidMetadataForID reports whether there is no valid metadata for the -// given ID. -func (s *snapshot) noValidMetadataForID(id PackageID) bool { - s.mu.Lock() - defer s.mu.Unlock() - - m := s.meta.metadata[id] - return m == nil || !m.Valid -} - func (s *snapshot) isWorkspacePackage(id PackageID) bool { s.mu.Lock() defer s.mu.Unlock() @@ -1479,39 +1447,42 @@ func (s *snapshot) AwaitInitialized(ctx context.Context) { // reloadWorkspace reloads the metadata for all invalidated workspace packages. func (s *snapshot) reloadWorkspace(ctx context.Context) error { - // See which of the workspace packages are missing metadata. + var scopes []interface{} + var seen map[PackagePath]bool s.mu.Lock() - missingMetadata := len(s.workspacePackages) == 0 || len(s.meta.metadata) == 0 - pkgPathSet := map[PackagePath]struct{}{} - for id, pkgPath := range s.workspacePackages { - if m, ok := s.meta.metadata[id]; ok && m.Valid { - continue - } - missingMetadata = true - - // Don't try to reload "command-line-arguments" directly. - if source.IsCommandLineArguments(string(pkgPath)) { - continue + for _, pkgPaths := range s.shouldLoad { + for _, pkgPath := range pkgPaths { + if seen == nil { + seen = make(map[PackagePath]bool) + } + if seen[pkgPath] { + continue + } + seen[pkgPath] = true + scopes = append(scopes, pkgPath) } - pkgPathSet[pkgPath] = struct{}{} } s.mu.Unlock() + if len(scopes) == 0 { + return nil + } + // If the view's build configuration is invalid, we cannot reload by // package path. Just reload the directory instead. - if missingMetadata && !s.ValidBuildConfiguration() { - return s.load(ctx, false, viewLoadScope("LOAD_INVALID_VIEW")) + if !s.ValidBuildConfiguration() { + scopes = []interface{}{viewLoadScope("LOAD_INVALID_VIEW")} } - if len(pkgPathSet) == 0 { - return nil - } + err := s.load(ctx, false, scopes...) - var pkgPaths []interface{} - for pkgPath := range pkgPathSet { - pkgPaths = append(pkgPaths, pkgPath) + // Unless the context was canceled, set "shouldLoad" to false for all + // of the metadata we attempted to load. + if !errors.Is(err, context.Canceled) { + s.clearShouldLoad(scopes...) } - return s.load(ctx, false, pkgPaths...) + + return err } func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { @@ -1676,6 +1647,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC release := result.Acquire() // Copy the set of unloadable files. + // + // TODO(rfindley): this looks wrong. Shouldn't we clear unloadableFiles on + // changes to environment or workspace layout, or more generally on any + // metadata change? for k, v := range s.unloadableFiles { result.unloadableFiles[k] = v } @@ -1873,6 +1848,15 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } + // Any packages that need loading in s still need loading in the new + // snapshot. + for k, v := range s.shouldLoad { + if result.shouldLoad == nil { + result.shouldLoad = make(map[PackageID][]PackagePath) + } + result.shouldLoad[k] = v + } + // Compute which metadata updates are required. We only need to invalidate // packages directly containing the affected file, and only if it changed in // a relevant way. @@ -1880,20 +1864,41 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC deleteInvalidMetadata := forceReloadMetadata || workspaceModeChanged for k, v := range s.meta.metadata { invalidateMetadata := idsToInvalidate[k] + + // For metadata that has been newly invalidated, capture package paths + // requiring reloading in the shouldLoad map. + if invalidateMetadata && !source.IsCommandLineArguments(string(v.ID)) { + if result.shouldLoad == nil { + result.shouldLoad = make(map[PackageID][]PackagePath) + } + needsReload := []PackagePath{v.PkgPath} + if v.ForTest != "" && v.ForTest != v.PkgPath { + // When reloading test variants, always reload their ForTest package as + // well. Otherwise, we may miss test variants in the resulting load. + // + // TODO(rfindley): is this actually sufficient? Is it possible that + // other test variants may be invalidated? Either way, we should + // determine exactly what needs to be reloaded here. + needsReload = append(needsReload, v.ForTest) + } + result.shouldLoad[k] = needsReload + } + + // Check whether the metadata should be deleted. if skipID[k] || (invalidateMetadata && deleteInvalidMetadata) { metadataUpdates[k] = nil continue } + + // Check if the metadata has changed. valid := v.Valid && !invalidateMetadata pkgFilesChanged := v.PkgFilesChanged || changedPkgFiles[k] - shouldLoad := v.ShouldLoad || invalidateMetadata - if valid != v.Valid || pkgFilesChanged != v.PkgFilesChanged || shouldLoad != v.ShouldLoad { + if valid != v.Valid || pkgFilesChanged != v.PkgFilesChanged { // Mark invalidated metadata rather than deleting it outright. metadataUpdates[k] = &KnownMetadata{ Metadata: v.Metadata, Valid: valid, PkgFilesChanged: pkgFilesChanged, - ShouldLoad: shouldLoad, } } } diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go index 8d205ee6cee..262447cf36d 100644 --- a/internal/lsp/source/util.go +++ b/internal/lsp/source/util.go @@ -550,6 +550,8 @@ func IsValidImport(pkgPath, importPkgPath string) bool { if i == -1 { return true } + // TODO(rfindley): this looks wrong: IsCommandLineArguments is meant to + // operate on package IDs, not package paths. if IsCommandLineArguments(string(pkgPath)) { return true } @@ -560,6 +562,8 @@ func IsValidImport(pkgPath, importPkgPath string) bool { // "command-line-arguments" package, which is a package with an unknown ID // created by the go command. It can have a test variant, which is why callers // should not check that a value equals "command-line-arguments" directly. +// +// TODO(rfindley): this should accept a PackageID. func IsCommandLineArguments(s string) bool { return strings.Contains(s, "command-line-arguments") } From 3d474c89054e6c6094b99e57fe652ad6d45c2976 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Wed, 30 Mar 2022 14:48:29 -0400 Subject: [PATCH 121/723] internal/lsp/diff: new diff implementation to replace go-diff The new implementation is based on Myers' paper, and is in the package diff/lcs. There is a new option newDiff, that can be set to 'old', 'new', or 'both'. The default is 'both', although that may not be the right choice for a release. See gopls/hooks/diff.go. 'both' runs both the old and new diff algorithm and saves some statistics in a file in os.Tempdir(), When (or if) the new code becomes the default, this logging (and some internal checking) will be removed. The new implementation has internal checking, which currently panics. The code in gopls/hooks/diff.go tries to save an encrypted (for privacy) version of the failing input. The package diff/myers has not been replaced, but it could be. Fixes golang/go#52966 Change-Id: Id38d76ed383c4330d9373580561765b5a2412587 Reviewed-on: https://go-review.googlesource.com/c/tools/+/396855 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Peter Weinberger --- go.mod | 4 +- go.sum | 22 +- gopls/doc/settings.md | 10 + gopls/go.mod | 4 +- gopls/go.sum | 20 +- gopls/internal/hooks/diff.go | 164 +++++++++ gopls/internal/hooks/diff_test.go | 47 ++- gopls/internal/hooks/hooks.go | 10 +- internal/lsp/diff/diff_test.go | 125 +++++++ internal/lsp/diff/lcs/common.go | 184 ++++++++++ internal/lsp/diff/lcs/common_test.go | 140 +++++++ internal/lsp/diff/lcs/doc.go | 156 ++++++++ internal/lsp/diff/lcs/git.sh | 34 ++ internal/lsp/diff/lcs/labels.go | 55 +++ internal/lsp/diff/lcs/old.go | 530 +++++++++++++++++++++++++++ internal/lsp/diff/lcs/old_test.go | 203 ++++++++++ internal/lsp/diff/ndiff.go | 130 +++++++ internal/lsp/source/options.go | 12 +- internal/lsp/tests/tests.go | 1 + 19 files changed, 1804 insertions(+), 47 deletions(-) create mode 100644 internal/lsp/diff/lcs/common.go create mode 100644 internal/lsp/diff/lcs/common_test.go create mode 100644 internal/lsp/diff/lcs/doc.go create mode 100644 internal/lsp/diff/lcs/git.sh create mode 100644 internal/lsp/diff/lcs/labels.go create mode 100644 internal/lsp/diff/lcs/old.go create mode 100644 internal/lsp/diff/lcs/old_test.go create mode 100644 internal/lsp/diff/ndiff.go diff --git a/go.mod b/go.mod index 985b9cc120c..f05aba2b64a 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module golang.org/x/tools -go 1.17 +go 1.18 require ( github.com/yuin/goldmark v1.4.1 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a golang.org/x/text v0.3.7 ) diff --git a/go.sum b/go.sum index 85cf00cab79..efeb68a0ec2 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,12 @@ github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 0ed0e19bb02..6d923b71e36 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -452,6 +452,16 @@ Default: `false`. +#### **newDiff** *string* + +newDiff enables the new diff implementation. If this is "both", +for now both diffs will be run and statistics will be generateted in +a file in $TMPDIR. This is a risky setting; help in trying it +is appreciated. If it is "old" the old implementation is used, +and if it is "new", just the new implementation is used. + +Default: 'old'. + ## Code Lenses These are the code lenses that `gopls` currently supports. They can be enabled diff --git a/gopls/go.mod b/gopls/go.mod index 020386fca28..3e89620b7f6 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -8,7 +8,7 @@ require ( github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - golang.org/x/sys v0.0.0-20220209214540-3681064d5158 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a golang.org/x/tools v0.1.11-0.20220523181440-ccb10502d1a5 golang.org/x/vuln v0.0.0-20220718121659-b9a3ad919698 honnef.co/go/tools v0.3.2 @@ -20,7 +20,7 @@ require ( github.com/BurntSushi/toml v1.0.0 // indirect github.com/google/safehtml v0.0.2 // indirect golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) diff --git a/gopls/go.sum b/gopls/go.sum index f263f9e0d00..4ee977a8c89 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -1,21 +1,14 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/google/go-cmdtest v0.4.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/safehtml v0.0.2 h1:ZOt2VXg4x24bW0m2jtzAOkhoXV0iM8vNKc0paByCZqM= github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/jba/printsrc v0.2.2 h1:9OHK51UT+/iMAEBlQIIXW04qvKyF3/vvLuwW/hL8tDU= @@ -47,8 +40,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -61,18 +52,15 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/vuln v0.0.0-20220503210553-a5481fb0c8be h1:jokAF1mfylAi1iTQx7C44B7vyXUcSEMw8eDv0PzNu8s= -golang.org/x/vuln v0.0.0-20220503210553-a5481fb0c8be/go.mod h1:twca1SxmF6/i2wHY/mj1vLIkkHdp+nil/yA32ZOP4kg= golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c h1:r5bbIROBQtRRgoutV8Q3sFY58VzHW6jMBYl48ANSyS4= golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c/go.mod h1:UZshlUPxXeGUM9I14UOawXQg6yosDE9cr1vKY/DzgWo= golang.org/x/vuln v0.0.0-20220718121659-b9a3ad919698 h1:9lgpkUgjzoIcZYp7/UPFO/0jIlYcokcEjqWm0hj9pzE= @@ -88,14 +76,10 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= -honnef.co/go/tools v0.3.0 h1:2LdYUZ7CIxnYgskbUZfY7FPggmqnh6shBqfWa8Tn3XU= -honnef.co/go/tools v0.3.0/go.mod h1:vlRD9XErLMGT+mDuofSr0mMMquscM/1nQqtRSsh6m70= honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= mvdan.cc/gofumpt v0.3.0 h1:kTojdZo9AcEYbQYhGuLf/zszYthRdhDNDUi2JKTxas4= mvdan.cc/gofumpt v0.3.0/go.mod h1:0+VyGZWleeIj5oostkOex+nDBA0eyavuDnDusAJ8ylo= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio= -mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5/go.mod h1:b8RRCBm0eeiWR8cfN88xeq2G5SG3VKGO+5UPWi5FSOY= mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc= mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg= diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go index a307ba77fd6..e0461a152bb 100644 --- a/gopls/internal/hooks/diff.go +++ b/gopls/internal/hooks/diff.go @@ -5,13 +5,177 @@ package hooks import ( + "crypto/rand" + "encoding/json" "fmt" + "io" + "log" + "math/big" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + "unicode" "github.com/sergi/go-diff/diffmatchpatch" "golang.org/x/tools/internal/lsp/diff" "golang.org/x/tools/internal/span" ) +// structure for saving information about diffs +// while the new code is being rolled out +type diffstat struct { + Before, After int + Oldedits, Newedits int + Oldtime, Newtime time.Duration + Stack string + Msg string `json:",omitempty"` // for errors + Ignored int `json:",omitempty"` // numbr of skipped records with 0 edits +} + +var ( + mu sync.Mutex // serializes writes and protects ignored + difffd io.Writer + ignored int // lots of the diff calls have 0 diffs +) + +var fileonce sync.Once + +func (s *diffstat) save() { + // save log records in a file in os.TempDir(). + // diff is frequently called with identical strings, so + // these are somewhat compressed out + fileonce.Do(func() { + fname := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-diff-%x", os.Getpid())) + fd, err := os.Create(fname) + if err != nil { + // now what? + } + difffd = fd + }) + + mu.Lock() + defer mu.Unlock() + if s.Oldedits == 0 && s.Newedits == 0 { + if ignored < 15 { + // keep track of repeated instances of no diffs + // but only print every 15th + ignored++ + return + } + s.Ignored = ignored + 1 + } else { + s.Ignored = ignored + } + ignored = 0 + // it would be really nice to see why diff was called + _, f, l, ok := runtime.Caller(2) + if ok { + var fname string + fname = filepath.Base(f) // diff is only called from a few places + s.Stack = fmt.Sprintf("%s:%d", fname, l) + } + x, err := json.Marshal(s) + if err != nil { + log.Print(err) // failure to print statistics should not stop gopls + } + fmt.Fprintf(difffd, "%s\n", x) +} + +// save encrypted versions of the broken input and return the file name +// (the saved strings will have the same diff behavior as the user's strings) +func disaster(before, after string) string { + // encrypt before and after for privacy. (randomized monoalphabetic cipher) + // got will contain the substitution cipher + // for the runes in before and after + got := map[rune]rune{} + for _, r := range before { + got[r] = ' ' // value doesn't matter + } + for _, r := range after { + got[r] = ' ' + } + repl := initrepl(len(got)) + i := 0 + for k := range got { // randomized + got[k] = repl[i] + i++ + } + // use got to encrypt before and after + subst := func(r rune) rune { return got[r] } + first := strings.Map(subst, before) + second := strings.Map(subst, after) + + // one failure per session is enough, and more private. + // this saves the last one. + fname := fmt.Sprintf("%s/gopls-failed-%x", os.TempDir(), os.Getpid()) + fd, err := os.Create(fname) + defer fd.Close() + _, err = fd.Write([]byte(fmt.Sprintf("%s\n%s\n", string(first), string(second)))) + if err != nil { + // what do we tell the user? + return "" + } + // ask the user to send us the file, somehow + return fname +} + +func initrepl(n int) []rune { + repl := make([]rune, 0, n) + for r := rune(0); len(repl) < n; r++ { + if unicode.IsLetter(r) || unicode.IsNumber(r) { + repl = append(repl, r) + } + } + // randomize repl + rdr := rand.Reader + lim := big.NewInt(int64(len(repl))) + for i := 1; i < n; i++ { + v, _ := rand.Int(rdr, lim) + k := v.Int64() + repl[i], repl[k] = repl[k], repl[i] + } + return repl +} + +// BothDiffs edits calls both the new and old diffs, checks that the new diffs +// change before into after, and attempts to preserve some statistics. +func BothDiffs(uri span.URI, before, after string) (edits []diff.TextEdit, err error) { + // The new diff code contains a lot of internal checks that panic when they + // fail. This code catches the panics, or other failures, tries to save + // the failing example (and ut wiykd ask the user to send it back to us, and + // changes options.newDiff to 'old', if only we could figure out how.) + stat := diffstat{Before: len(before), After: len(after)} + now := time.Now() + Oldedits, oerr := ComputeEdits(uri, before, after) + if oerr != nil { + stat.Msg += fmt.Sprintf("old:%v", oerr) + } + stat.Oldedits = len(Oldedits) + stat.Oldtime = time.Since(now) + defer func() { + if r := recover(); r != nil { + disaster(before, after) + edits, err = Oldedits, oerr + } + }() + now = time.Now() + Newedits, rerr := diff.NComputeEdits(uri, before, after) + stat.Newedits = len(Newedits) + stat.Newtime = time.Now().Sub(now) + got := diff.ApplyEdits(before, Newedits) + if got != after { + stat.Msg += "FAIL" + disaster(before, after) + stat.save() + return Oldedits, oerr + } + stat.save() + return Newedits, rerr +} + func ComputeEdits(uri span.URI, before, after string) (edits []diff.TextEdit, err error) { // The go-diff library has an unresolved panic (see golang/go#278774). // TODO(rstambler): Remove the recover once the issue has been fixed diff --git a/gopls/internal/hooks/diff_test.go b/gopls/internal/hooks/diff_test.go index d979be78dbe..a9e536728b0 100644 --- a/gopls/internal/hooks/diff_test.go +++ b/gopls/internal/hooks/diff_test.go @@ -2,15 +2,56 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package hooks_test +package hooks import ( + "fmt" + "io/ioutil" + "os" "testing" + "unicode/utf8" - "golang.org/x/tools/gopls/internal/hooks" "golang.org/x/tools/internal/lsp/diff/difftest" ) func TestDiff(t *testing.T) { - difftest.DiffTest(t, hooks.ComputeEdits) + difftest.DiffTest(t, ComputeEdits) +} + +func TestRepl(t *testing.T) { + t.Skip("just for checking repl by looking at it") + repl := initrepl(800) + t.Errorf("%q", string(repl)) + t.Errorf("%d", len(repl)) +} + +func TestDisaster(t *testing.T) { + a := "This is a string,(\u0995) just for basic functionality" + b := "Ths is another string, (\u0996) to see if disaster will store stuff correctly" + fname := disaster(a, b) + buf, err := ioutil.ReadFile(fname) + if err != nil { + t.Errorf("error %v reading %s", err, fname) + } + var x, y string + n, err := fmt.Sscanf(string(buf), "%s\n%s\n", &x, &y) + if n != 2 { + t.Errorf("got %d, expected 2", n) + t.Logf("read %q", string(buf)) + } + if a == x || b == y { + t.Error("failed to encrypt") + } + err = os.Remove(fname) + if err != nil { + t.Errorf("%v removing %s", err, fname) + } + alen, blen := utf8.RuneCount([]byte(a)), utf8.RuneCount([]byte(b)) + xlen, ylen := utf8.RuneCount([]byte(x)), utf8.RuneCount([]byte(y)) + if alen != xlen { + t.Errorf("a; got %d, expected %d", xlen, alen) + } + if blen != ylen { + t.Errorf("b: got %d expected %d", ylen, blen) + } } diff --git a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go index 023aefeab98..b55917e0737 100644 --- a/gopls/internal/hooks/hooks.go +++ b/gopls/internal/hooks/hooks.go @@ -11,6 +11,7 @@ import ( "context" "golang.org/x/tools/gopls/internal/vulncheck" + "golang.org/x/tools/internal/lsp/diff" "golang.org/x/tools/internal/lsp/source" "mvdan.cc/gofumpt/format" "mvdan.cc/xurls/v2" @@ -19,7 +20,14 @@ import ( func Options(options *source.Options) { options.LicensesText = licensesText if options.GoDiff { - options.ComputeEdits = ComputeEdits + switch options.NewDiff { + case "old": + options.ComputeEdits = ComputeEdits + case "new": + options.ComputeEdits = diff.NComputeEdits + default: + options.ComputeEdits = BothDiffs + } } options.URLRegexp = xurls.Relaxed() options.GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { diff --git a/internal/lsp/diff/diff_test.go b/internal/lsp/diff/diff_test.go index dd9414e5d7a..a3699498914 100644 --- a/internal/lsp/diff/diff_test.go +++ b/internal/lsp/diff/diff_test.go @@ -6,6 +6,8 @@ package diff_test import ( "fmt" + "math/rand" + "strings" "testing" "golang.org/x/tools/internal/lsp/diff" @@ -29,6 +31,71 @@ func TestApplyEdits(t *testing.T) { } } +func TestNEdits(t *testing.T) { + for i, tc := range difftest.TestCases { + sp := fmt.Sprintf("file://%s.%d", tc.Name, i) + edits, err := diff.NComputeEdits(span.URI(sp), tc.In, tc.Out) + if err != nil { + t.Fatal(err) + } + got := diff.ApplyEdits(tc.In, edits) + if got != tc.Out { + t.Fatalf("%s: got %q wanted %q", tc.Name, got, tc.Out) + } + if len(edits) < len(tc.Edits) { // should find subline edits + t.Errorf("got %v, expected %v for %#v", edits, tc.Edits, tc) + } + } +} + +func TestNRandom(t *testing.T) { + rand.Seed(1) + for i := 0; i < 1000; i++ { + fname := fmt.Sprintf("file://%x", i) + a := randstr("abω", 16) + b := randstr("abωc", 16) + edits, err := diff.NComputeEdits(span.URI(fname), a, b) + if err != nil { + t.Fatalf("%q,%q %v", a, b, err) + } + got := diff.ApplyEdits(a, edits) + if got != b { + t.Fatalf("%d: got %q, wanted %q, starting with %q", i, got, b, a) + } + } +} + +func TestNLinesRandom(t *testing.T) { + rand.Seed(2) + for i := 0; i < 1000; i++ { + fname := fmt.Sprintf("file://%x", i) + x := randlines("abω", 4) // avg line length is 6, want a change every 3rd line or so + v := []rune(x) + for i := 0; i < len(v); i++ { + if rand.Float64() < .05 { + v[i] = 'N' + } + } + y := string(v) + // occasionally remove the trailing \n + if rand.Float64() < .1 { + x = x[:len(x)-1] + } + if rand.Float64() < .1 { + y = y[:len(y)-1] + } + a, b := strings.SplitAfter(x, "\n"), strings.SplitAfter(y, "\n") + edits, err := diff.NComputeLineEdits(span.URI(fname), a, b) + if err != nil { + t.Fatalf("%q,%q %v", a, b, err) + } + got := diff.ApplyEdits(x, edits) + if got != y { + t.Fatalf("%d: got\n%q, wanted\n%q, starting with %q", i, got, y, a) + } + } +} + func TestLineEdits(t *testing.T) { for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { @@ -63,6 +130,41 @@ func TestUnified(t *testing.T) { } } +func TestRegressionOld001(t *testing.T) { + a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/lsp/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + + b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/lsp/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + diffs, err := diff.NComputeEdits(span.URI("file://one"), a, b) + if err != nil { + t.Error(err) + } + got := diff.ApplyEdits(a, diffs) + if got != b { + i := 0 + for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ { + } + t.Errorf("oops %vd\n%q\n%q", diffs, got, b) + t.Errorf("\n%q\n%q", got[i:], b[i:]) + } +} + +func TestRegressionOld002(t *testing.T) { + a := "n\"\n)\n" + b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n" + diffs, err := diff.NComputeEdits(span.URI("file://two"), a, b) + if err != nil { + t.Error(err) + } + got := diff.ApplyEdits(a, diffs) + if got != b { + i := 0 + for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ { + } + t.Errorf("oops %vd\n%q\n%q", diffs, got, b) + t.Errorf("\n%q\n%q", got[i:], b[i:]) + } +} + func diffEdits(got, want []diff.TextEdit) bool { if len(got) != len(want) { return true @@ -78,3 +180,26 @@ func diffEdits(got, want []diff.TextEdit) bool { } return false } + +// return a random string of length n made of characters from s +func randstr(s string, n int) string { + src := []rune(s) + x := make([]rune, n) + for i := 0; i < n; i++ { + x[i] = src[rand.Intn(len(src))] + } + return string(x) +} + +// return some random lines, all ending with \n +func randlines(s string, n int) string { + src := []rune(s) + var b strings.Builder + for i := 0; i < n; i++ { + for j := 0; j < 4+rand.Intn(4); j++ { + b.WriteRune(src[rand.Intn(len(src))]) + } + b.WriteByte('\n') + } + return b.String() +} diff --git a/internal/lsp/diff/lcs/common.go b/internal/lsp/diff/lcs/common.go new file mode 100644 index 00000000000..e5d08014760 --- /dev/null +++ b/internal/lsp/diff/lcs/common.go @@ -0,0 +1,184 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "log" + "sort" +) + +// lcs is a longest common sequence +type lcs []diag + +// A diag is a piece of the edit graph where A[X+i] == B[Y+i], for 0<=i l[j].Len + }) + return l +} + +// validate that the elements of the lcs do not overlap +// (can only happen when the two-sided algorithm ends early) +// expects the lcs to be sorted +func (l lcs) valid() bool { + for i := 1; i < len(l); i++ { + if l[i-1].X+l[i-1].Len > l[i].X { + return false + } + if l[i-1].Y+l[i-1].Len > l[i].Y { + return false + } + } + return true +} + +// repair overlapping lcs +// only called if two-sided stops early +func (l lcs) fix() lcs { + // from the set of diagonals in l, find a maximal non-conflicting set + // this problem may be NP-complete, but we use a greedy heuristic, + // which is quadratic, but with a better data structure, could be D log D. + // indepedent is not enough: {0,3,1} and {3,0,2} can't both occur in an lcs + // which has to have monotone x and y + if len(l) == 0 { + return nil + } + sort.Slice(l, func(i, j int) bool { return l[i].Len > l[j].Len }) + tmp := make(lcs, 0, len(l)) + tmp = append(tmp, l[0]) + for i := 1; i < len(l); i++ { + var dir direction + nxt := l[i] + for _, in := range tmp { + if dir, nxt = overlap(in, nxt); dir == empty || dir == bad { + break + } + } + if nxt.Len > 0 && dir != bad { + tmp = append(tmp, nxt) + } + } + tmp.sort() + if false && !tmp.valid() { // debug checking + log.Fatalf("here %d", len(tmp)) + } + return tmp +} + +type direction int + +const ( + empty direction = iota // diag is empty (so not in lcs) + leftdown // proposed acceptably to the left and below + rightup // proposed diag is acceptably to the right and above + bad // proposed diag is inconsistent with the lcs so far +) + +// overlap trims the proposed diag prop so it doesn't overlap with +// the existing diag that has already been added to the lcs. +func overlap(exist, prop diag) (direction, diag) { + if prop.X <= exist.X && exist.X < prop.X+prop.Len { + // remove the end of prop where it overlaps with the X end of exist + delta := prop.X + prop.Len - exist.X + prop.Len -= delta + if prop.Len <= 0 { + return empty, prop + } + } + if exist.X <= prop.X && prop.X < exist.X+exist.Len { + // remove the beginning of prop where overlaps with exist + delta := exist.X + exist.Len - prop.X + prop.Len -= delta + if prop.Len <= 0 { + return empty, prop + } + prop.X += delta + prop.Y += delta + } + if prop.Y <= exist.Y && exist.Y < prop.Y+prop.Len { + // remove the end of prop that overlaps (in Y) with exist + delta := prop.Y + prop.Len - exist.Y + prop.Len -= delta + if prop.Len <= 0 { + return empty, prop + } + } + if exist.Y <= prop.Y && prop.Y < exist.Y+exist.Len { + // remove the beginning of peop that overlaps with exist + delta := exist.Y + exist.Len - prop.Y + prop.Len -= delta + if prop.Len <= 0 { + return empty, prop + } + prop.X += delta // no test reaches this code + prop.Y += delta + } + if prop.X+prop.Len <= exist.X && prop.Y+prop.Len <= exist.Y { + return leftdown, prop + } + if exist.X+exist.Len <= prop.X && exist.Y+exist.Len <= prop.Y { + return rightup, prop + } + // prop can't be in an lcs that contains exist + return bad, prop +} + +// manipulating Diag and lcs + +// prependlcs a diagonal (x,y)-(x+1,y+1) segment either to an empty lcs +// or to its first Diag. prependlcs is only called extending diagonals +// the backward direction. +func prependlcs(lcs lcs, x, y int) lcs { + if len(lcs) > 0 { + d := &lcs[0] + if int(d.X) == x+1 && int(d.Y) == y+1 { + // extend the diagonal down and to the left + d.X, d.Y = int(x), int(y) + d.Len++ + return lcs + } + } + + r := diag{X: int(x), Y: int(y), Len: 1} + lcs = append([]diag{r}, lcs...) + return lcs +} + +// appendlcs appends a diagonal, or extends the existing one. +// by adding the edge (x,y)-(x+1.y+1). appendlcs is only called +// while extending diagonals in the forward direction. +func appendlcs(lcs lcs, x, y int) lcs { + if len(lcs) > 0 { + last := &lcs[len(lcs)-1] + // Expand last element if adjoining. + if last.X+last.Len == x && last.Y+last.Len == y { + last.Len++ + return lcs + } + } + + return append(lcs, diag{X: x, Y: y, Len: 1}) +} + +// enforce constraint on d, k +func ok(d, k int) bool { + return d >= 0 && -d <= k && k <= d +} + +type Diff struct { + Start, End int // offsets in A + Text string // replacement text +} diff --git a/internal/lsp/diff/lcs/common_test.go b/internal/lsp/diff/lcs/common_test.go new file mode 100644 index 00000000000..4aa36abc2e8 --- /dev/null +++ b/internal/lsp/diff/lcs/common_test.go @@ -0,0 +1,140 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "log" + "math/rand" + "strings" + "testing" +) + +type Btest struct { + a, b string + lcs []string +} + +var Btests = []Btest{ + {"aaabab", "abaab", []string{"abab", "aaab"}}, + {"aabbba", "baaba", []string{"aaba"}}, + {"cabbx", "cbabx", []string{"cabx", "cbbx"}}, + {"c", "cb", []string{"c"}}, + {"aaba", "bbb", []string{"b"}}, + {"bbaabb", "b", []string{"b"}}, + {"baaabb", "bbaba", []string{"bbb", "baa", "bab"}}, + {"baaabb", "abbab", []string{"abb", "bab", "aab"}}, + {"baaba", "aaabba", []string{"aaba"}}, + {"ca", "cba", []string{"ca"}}, + {"ccbcbc", "abba", []string{"bb"}}, + {"ccbcbc", "aabba", []string{"bb"}}, + {"ccb", "cba", []string{"cb"}}, + {"caef", "axe", []string{"ae"}}, + {"bbaabb", "baabb", []string{"baabb"}}, + // Example from Myers: + {"abcabba", "cbabac", []string{"caba", "baba", "cbba"}}, + {"3456aaa", "aaa", []string{"aaa"}}, + {"aaa", "aaa123", []string{"aaa"}}, + {"aabaa", "aacaa", []string{"aaaa"}}, + {"1a", "a", []string{"a"}}, + {"abab", "bb", []string{"bb"}}, + {"123", "ab", []string{""}}, + {"a", "b", []string{""}}, + {"abc", "123", []string{""}}, + {"aa", "aa", []string{"aa"}}, + {"abcde", "12345", []string{""}}, + {"aaa3456", "aaa", []string{"aaa"}}, + {"abcde", "12345a", []string{"a"}}, + {"ab", "123", []string{""}}, + {"1a2", "a", []string{"a"}}, + // for two-sided + {"babaab", "cccaba", []string{"aba"}}, + {"aabbab", "cbcabc", []string{"bab"}}, + {"abaabb", "bcacab", []string{"baab"}}, + {"abaabb", "abaaaa", []string{"abaa"}}, + {"bababb", "baaabb", []string{"baabb"}}, + {"abbbaa", "cabacc", []string{"aba"}}, + {"aabbaa", "aacaba", []string{"aaaa", "aaba"}}, +} + +func init() { + log.SetFlags(log.Lshortfile) +} + +func check(t *testing.T, str string, lcs lcs, want []string) { + t.Helper() + if !lcs.valid() { + t.Errorf("bad lcs %v", lcs) + } + var got strings.Builder + for _, dd := range lcs { + got.WriteString(str[dd.X : dd.X+dd.Len]) + } + ans := got.String() + for _, w := range want { + if ans == w { + return + } + } + t.Fatalf("str=%q lcs=%v want=%q got=%q", str, lcs, want, ans) +} + +func checkDiffs(t *testing.T, before string, diffs []Diff, after string) { + t.Helper() + var ans strings.Builder + sofar := 0 // index of position in before + for _, d := range diffs { + if sofar < d.Start { + ans.WriteString(before[sofar:d.Start]) + } + ans.WriteString(d.Text) + sofar = d.End + } + ans.WriteString(before[sofar:]) + if ans.String() != after { + t.Fatalf("diff %v took %q to %q, not to %q", diffs, before, ans.String(), after) + } +} + +func lcslen(l lcs) int { + ans := 0 + for _, d := range l { + ans += int(d.Len) + } + return ans +} + +// return a random string of length n made of characters from s +func randstr(s string, n int) string { + src := []rune(s) + x := make([]rune, n) + for i := 0; i < n; i++ { + x[i] = src[rand.Intn(len(src))] + } + return string(x) +} + +func TestLcsFix(t *testing.T) { + tests := []struct{ before, after lcs }{ + {lcs{diag{0, 0, 3}, diag{2, 2, 5}, diag{3, 4, 5}, diag{8, 9, 4}}, lcs{diag{0, 0, 2}, diag{2, 2, 1}, diag{3, 4, 5}, diag{8, 9, 4}}}, + {lcs{diag{1, 1, 6}, diag{6, 12, 3}}, lcs{diag{1, 1, 5}, diag{6, 12, 3}}}, + {lcs{diag{0, 0, 4}, diag{3, 5, 4}}, lcs{diag{0, 0, 3}, diag{3, 5, 4}}}, + {lcs{diag{0, 20, 1}, diag{0, 0, 3}, diag{1, 20, 4}}, lcs{diag{0, 0, 3}, diag{3, 22, 2}}}, + {lcs{diag{0, 0, 4}, diag{1, 1, 2}}, lcs{diag{0, 0, 4}}}, + {lcs{diag{0, 0, 4}}, lcs{diag{0, 0, 4}}}, + {lcs{}, lcs{}}, + {lcs{diag{0, 0, 4}, diag{1, 1, 6}, diag{3, 3, 2}}, lcs{diag{0, 0, 1}, diag{1, 1, 6}}}, + } + for n, x := range tests { + got := x.before.fix() + if len(got) != len(x.after) { + t.Errorf("got %v, expected %v, for %v", got, x.after, x.before) + } + olen := lcslen(x.after) + glen := lcslen(got) + if olen != glen { + t.Errorf("%d: lens(%d,%d) differ, %v, %v, %v", n, glen, olen, got, x.after, x.before) + } + } +} diff --git a/internal/lsp/diff/lcs/doc.go b/internal/lsp/diff/lcs/doc.go new file mode 100644 index 00000000000..dc779f38a01 --- /dev/null +++ b/internal/lsp/diff/lcs/doc.go @@ -0,0 +1,156 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// package lcs contains code to find longest-common-subsequences +// (and diffs) +package lcs + +/* +Compute longest-common-subsequences of two slices A, B using +algorithms from Myers' paper. A longest-common-subsequence +(LCS from now on) of A and B is a maximal set of lexically increasing +pairs of subscripts (x,y) with A[x]==B[y]. There may be many LCS, but +they all have the same length. An LCS determines a sequence of edits +that changes A into B. + +The key concept is the edit graph of A and B. +If A has length N and B has length M, then the edit graph has +vertices v[i][j] for 0 <= i <= N, 0 <= j <= M. There is a +horizontal edge from v[i][j] to v[i+1][j] whenever both are in +the graph, and a vertical edge from v[i][j] to f[i][j+1] similarly. +When A[i] == B[j] there is a diagonal edge from v[i][j] to v[i+1][j+1]. + +A path between in the graph between (0,0) and (N,M) determines a sequence +of edits converting A into B: each horizontal edge corresponds to removing +an element of A, and each vertical edge corresponds to inserting an +element of B. + +A vertex (x,y) is on (forward) diagonal k if x-y=k. A path in the graph +is of length D if it has D non-diagonal edges. The algorithms generate +forward paths (in which at least one of x,y increases at each edge), +or backward paths (in which at least one of x,y decreases at each edge), +or a combination. (Note that the orientation is the traditional mathematical one, +with the origin in the lower-left corner.) + +Here is the edit graph for A:"aabbaa", B:"aacaba". (I know the diagonals look weird.) + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + b | | | ___/‾‾‾ | ___/‾‾‾ | | | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + c | | | | | | | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a a b b a a + + +The algorithm labels a vertex (x,y) with D,k if it is on diagonal k and at +the end of a maximal path of length D. (Because x-y=k it suffices to remember +only the x coordinate of the vertex.) + +The forward algorithm: Find the longest diagonal starting at (0,0) and +label its end with D=0,k=0. From that vertex take a vertical step and +then follow the longest diagonal (up and to the right), and label that vertex +with D=1,k=-1. From the D=0,k=0 point take a horizontal step and the follow +the longest diagonal (up and to the right) and label that vertex +D=1,k=1. In the same way, having labelled all the D vertices, +from a vertex labelled D,k find two vertices +tentatively labelled D+1,k-1 and D+1,k+1. There may be two on the same +diagonal, in which case take the one with the larger x. + +Eventually the path gets to (N,M), and the diagonals on it are the LCS. + +Here is the edit graph with the ends of D-paths labelled. (So, for instance, +0/2,2 indicates that x=2,y=2 is labelled with 0, as it should be, since the first +step is to go up the longest diagonal from (0,0).) +A:"aabbaa", B:"aacaba" + ⊙ ------- ⊙ ------- ⊙ -------(3/3,6)------- ⊙ -------(3/5,6)-------(4/6,6) + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ -------(2/3,5)------- ⊙ ------- ⊙ ------- ⊙ + b | | | ___/‾‾‾ | ___/‾‾‾ | | | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ -------(3/5,4)------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ -------(1/2,3)-------(2/3,3)------- ⊙ ------- ⊙ ------- ⊙ + c | | | | | | | + ⊙ ------- ⊙ -------(0/2,2)-------(1/3,2)-------(2/4,2)-------(3/5,2)-------(4/6,2) + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a a b b a a + +The 4-path is reconstructed starting at (4/6,6), horizontal to (3/5,6), diagonal to (3,4), vertical +to (2/3,3), horizontal to (1/2,3), vertical to (0/2,2), and diagonal to (0,0). As expected, +there are 4 non-diagonal steps, and the diagonals form an LCS. + +There is a symmetric backward algorithm, which gives (backwards labels are prefixed with a colon): +A:"aabbaa", B:"aacaba" + ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ + a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ | + ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ --------(:0/5,5)-------- ⊙ + b | | | ____/‾‾‾ | ____/‾‾‾ | | | + ⊙ -------- ⊙ -------- ⊙ --------(:1/3,4)-------- ⊙ -------- ⊙ -------- ⊙ + a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ | + (:3/0,3)--------(:2/1,3)-------- ⊙ --------(:2/3,3)--------(:1/4,3)-------- ⊙ -------- ⊙ + c | | | | | | | + ⊙ -------- ⊙ -------- ⊙ --------(:3/3,2)--------(:2/4,2)-------- ⊙ -------- ⊙ + a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ | + (:3/0,1)-------- ⊙ -------- ⊙ -------- ⊙ --------(:3/4,1)-------- ⊙ -------- ⊙ + a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ | + (:4/0,0)-------- ⊙ -------- ⊙ -------- ⊙ --------(:4/4,0)-------- ⊙ -------- ⊙ + a a b b a a + +Neither of these is ideal for use in an editor, where it is undesirable to send very long diffs to the +front end. It's tricky to decide exactly what 'very long diffs' means, as "replace A by B" is very short. +We want to control how big D can be, by stopping when it gets too large. The forward algorithm then +privileges common prefixes, and the backward algorithm privileges common suffixes. Either is an undesirable +asymmetry. + +Fortunately there is a two-sided algorithm, implied by results in Myers' paper. Here's what the labels in +the edit graph look like. +A:"aabbaa", B:"aacaba" + ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ + a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ | + ⊙ --------- ⊙ --------- ⊙ --------- (2/3,5) --------- ⊙ --------- (:0/5,5)--------- ⊙ + b | | | ____/‾‾‾‾ | ____/‾‾‾‾ | | | + ⊙ --------- ⊙ --------- ⊙ --------- (:1/3,4)--------- ⊙ --------- ⊙ --------- ⊙ + a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ | + ⊙ --------- (:2/1,3)--------- (1/2,3) ---------(2:2/3,3)--------- (:1/4,3)--------- ⊙ --------- ⊙ + c | | | | | | | + ⊙ --------- ⊙ --------- (0/2,2) --------- (1/3,2) ---------(2:2/4,2)--------- ⊙ --------- ⊙ + a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ | + ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ + a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ | + ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ + a a b b a a + +The algorithm stopped when it saw the backwards 2-path ending at (1,3) and the forwards 2-path ending at (3,5). The criterion +is a backwards path ending at (u,v) and a forward path ending at (x,y), where u <= x and the two points are on the same +diagonal. (Here the edgegraph has a diagonal, but the criterion is x-y=u-v.) Myers proves there is a forward +2-path from (0,0) to (1,3), and that together with the backwards 2-path ending at (1,3) gives the expected 4-path. +Unfortunately the forward path has to be constructed by another run of the forward algorithm; it can't be found from the +computed labels. That is the worst case. Had the code noticed (x,y)=(u,v)=(3,3) the whole path could be reconstructed +from the edgegraph. The implementation looks for a number of special cases to try to avoid computing an extra forward path. + +If the two-sided algorithm has stop early (because D has become too large) it will have found a forward LCS and a +backwards LCS. Ideally these go with disjoint prefixes and suffixes of A and B, but disjointness may fail and the two +computed LCS may conflict. (An easy example is where A is a suffix of B, and shares a short prefix. The backwards LCS +is all of A, and the forward LCS is a prefix of A.) The algorithm combines the two +to form a best-effort LCS. In the worst case the forward partial LCS may have to +be recomputed. +*/ + +/* Eugene Myers paper is titled +"An O(ND) Difference Algorithm and Its Variations" +and can be found at +http://www.xmailserver.org/diff2.pdf + +(There is a generic implementation of the algorithm the the repository with git hash +b9ad7e4ade3a686d608e44475390ad428e60e7fc) +*/ diff --git a/internal/lsp/diff/lcs/git.sh b/internal/lsp/diff/lcs/git.sh new file mode 100644 index 00000000000..caa4c424198 --- /dev/null +++ b/internal/lsp/diff/lcs/git.sh @@ -0,0 +1,34 @@ + +#!/bin/bash +# +# Copyright 2022 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +# +# Creates a zip file containing all numbered versions +# of the commit history of a large source file, for use +# as input data for the tests of the diff algorithm. +# +# Run script from root of the x/tools repo. + +set -eu + +# WARNING: This script will install the latest version of $file +# The largest real source file in the x/tools repo. +# file=internal/lsp/source/completion/completion.go +# file=internal/lsp/source/diagnostics.go +file=internal/lsp/protocol/tsprotocol.go + +tmp=$(mktemp -d) +git log $file | + awk '/^commit / {print $2}' | + nl -ba -nrz | + while read n hash; do + git checkout --quiet $hash $file + cp -f $file $tmp/$n + done +(cd $tmp && zip -q - *) > testdata.zip +rm -fr $tmp +git restore --staged $file +git restore $file +echo "Created testdata.zip" diff --git a/internal/lsp/diff/lcs/labels.go b/internal/lsp/diff/lcs/labels.go new file mode 100644 index 00000000000..0689f1ed700 --- /dev/null +++ b/internal/lsp/diff/lcs/labels.go @@ -0,0 +1,55 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "fmt" +) + +// For each D, vec[D] has length D+1, +// and the label for (D, k) is stored in vec[D][(D+k)/2]. +type label struct { + vec [][]int +} + +// Temporary checking DO NOT COMMIT true TO PRODUCTION CODE +const debug = false + +// debugging. check that the (d,k) pair is valid +// (that is, -d<=k<=d and d+k even) +func checkDK(D, k int) { + if k >= -D && k <= D && (D+k)%2 == 0 { + return + } + panic(fmt.Sprintf("out of range, d=%d,k=%d", D, k)) +} + +func (t *label) set(D, k, x int) { + if debug { + checkDK(D, k) + } + for len(t.vec) <= D { + t.vec = append(t.vec, nil) + } + if t.vec[D] == nil { + t.vec[D] = make([]int, D+1) + } + t.vec[D][(D+k)/2] = x // known that D+k is even +} + +func (t *label) get(d, k int) int { + if debug { + checkDK(d, k) + } + return int(t.vec[d][(d+k)/2]) +} + +func newtriang(limit int) label { + if limit < 100 { + // Preallocate if limit is not large. + return label{vec: make([][]int, limit)} + } + return label{} +} diff --git a/internal/lsp/diff/lcs/old.go b/internal/lsp/diff/lcs/old.go new file mode 100644 index 00000000000..a091edd5501 --- /dev/null +++ b/internal/lsp/diff/lcs/old.go @@ -0,0 +1,530 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "fmt" + "strings" +) + +// non generic code. The names have Old at the end to indicate they are the +// the implementation that doesn't use generics. + +// Compute the Diffs and the lcs. +func Compute(a, b interface{}, limit int) ([]Diff, lcs) { + var ans lcs + g := newegraph(a, b, limit) + ans = g.twosided() + diffs := g.fromlcs(ans) + return diffs, ans +} + +// editGraph carries the information for computing the lcs for []byte, []rune, or []string. +type editGraph struct { + eq eq // how to compare elements of A, B, and convert slices to strings + vf, vb label // forward and backward labels + + limit int // maximal value of D + // the bounding rectangle of the current edit graph + lx, ly, ux, uy int + delta int // common subexpression: (ux-lx)-(uy-ly) +} + +// abstraction in place of generic +type eq interface { + eq(i, j int) bool + substr(i, j int) string // string from b[i:j] + lena() int + lenb() int +} + +type byteeq struct { + a, b []byte // the input was ascii. perhaps these could be strings +} + +func (x *byteeq) eq(i, j int) bool { return x.a[i] == x.b[j] } +func (x *byteeq) substr(i, j int) string { return string(x.b[i:j]) } +func (x *byteeq) lena() int { return int(len(x.a)) } +func (x *byteeq) lenb() int { return int(len(x.b)) } + +type runeeq struct { + a, b []rune +} + +func (x *runeeq) eq(i, j int) bool { return x.a[i] == x.b[j] } +func (x *runeeq) substr(i, j int) string { return string(x.b[i:j]) } +func (x *runeeq) lena() int { return int(len(x.a)) } +func (x *runeeq) lenb() int { return int(len(x.b)) } + +type lineeq struct { + a, b []string +} + +func (x *lineeq) eq(i, j int) bool { return x.a[i] == x.b[j] } +func (x *lineeq) substr(i, j int) string { return strings.Join(x.b[i:j], "") } +func (x *lineeq) lena() int { return int(len(x.a)) } +func (x *lineeq) lenb() int { return int(len(x.b)) } + +func neweq(a, b interface{}) eq { + switch x := a.(type) { + case []byte: + return &byteeq{a: x, b: b.([]byte)} + case []rune: + return &runeeq{a: x, b: b.([]rune)} + case []string: + return &lineeq{a: x, b: b.([]string)} + default: + panic(fmt.Sprintf("unexpected type %T in neweq", x)) + } +} + +func (g *editGraph) fromlcs(lcs lcs) []Diff { + var ans []Diff + var pa, pb int // offsets in a, b + for _, l := range lcs { + if pa < l.X && pb < l.Y { + ans = append(ans, Diff{pa, l.X, g.eq.substr(pb, l.Y)}) + } else if pa < l.X { + ans = append(ans, Diff{pa, l.X, ""}) + } else if pb < l.Y { + ans = append(ans, Diff{pa, l.X, g.eq.substr(pb, l.Y)}) + } + pa = l.X + l.Len + pb = l.Y + l.Len + } + if pa < g.eq.lena() && pb < g.eq.lenb() { + ans = append(ans, Diff{pa, g.eq.lena(), g.eq.substr(pb, g.eq.lenb())}) + } else if pa < g.eq.lena() { + ans = append(ans, Diff{pa, g.eq.lena(), ""}) + } else if pb < g.eq.lenb() { + ans = append(ans, Diff{pa, g.eq.lena(), g.eq.substr(pb, g.eq.lenb())}) + } + return ans +} + +func newegraph(a, b interface{}, limit int) *editGraph { + if limit <= 0 { + limit = 1 << 25 // effectively infinity + } + var alen, blen int + switch a := a.(type) { + case []byte: + alen, blen = len(a), len(b.([]byte)) + case []rune: + alen, blen = len(a), len(b.([]rune)) + case []string: + alen, blen = len(a), len(b.([]string)) + default: + panic(fmt.Sprintf("unexpected type %T in newegraph", a)) + } + ans := &editGraph{eq: neweq(a, b), vf: newtriang(limit), vb: newtriang(limit), limit: int(limit), + ux: alen, uy: blen, delta: alen - blen} + return ans +} + +// --- FORWARD --- +// fdone decides if the forwward path has reached the upper right +// corner of the rectangele. If so, it also returns the computed lcs. +func (e *editGraph) fdone(D, k int) (bool, lcs) { + // x, y, k are relative to the rectangle + x := e.vf.get(D, k) + y := x - k + if x == e.ux && y == e.uy { + return true, e.forwardlcs(D, k) + } + return false, nil +} + +// run the forward algorithm, until success or up to the limit on D. +func (e *editGraph) forward() lcs { + e.setForward(0, 0, e.lx) + if ok, ans := e.fdone(0, 0); ok { + return ans + } + // from D to D+1 + for D := 0; D < e.limit; D++ { + e.setForward(D+1, -(D + 1), e.getForward(D, -D)) + if ok, ans := e.fdone(D+1, -(D + 1)); ok { + return ans + } + e.setForward(D+1, D+1, e.getForward(D, D)+1) + if ok, ans := e.fdone(D+1, D+1); ok { + return ans + } + for k := -D + 1; k <= D-1; k += 2 { + // these are tricky and easy to get backwards + lookv := e.lookForward(k, e.getForward(D, k-1)+1) + lookh := e.lookForward(k, e.getForward(D, k+1)) + if lookv > lookh { + e.setForward(D+1, k, lookv) + } else { + e.setForward(D+1, k, lookh) + } + if ok, ans := e.fdone(D+1, k); ok { + return ans + } + } + } + // D is too large + // find the D path with maximal x+y inside the rectangle and + // use that to compute the found part of the lcs + kmax := -e.limit - 1 + diagmax := -1 + for k := -e.limit; k <= e.limit; k += 2 { + x := e.getForward(e.limit, k) + y := x - k + if x+y > diagmax && x <= e.ux && y <= e.uy { + diagmax, kmax = x+y, k + } + } + return e.forwardlcs(e.limit, kmax) +} + +// recover the lcs by backtracking from the farthest point reached +func (e *editGraph) forwardlcs(D, k int) lcs { + var ans lcs + for x := e.getForward(D, k); x != 0 || x-k != 0; { + if ok(D-1, k-1) && x-1 == e.getForward(D-1, k-1) { + // if (x-1,y) is labelled D-1, x--,D--,k--,continue + D, k, x = D-1, k-1, x-1 + continue + } else if ok(D-1, k+1) && x == e.getForward(D-1, k+1) { + // if (x,y-1) is labelled D-1, x, D--,k++, continue + D, k = D-1, k+1 + continue + } + // if (x-1,y-1)--(x,y) is a diagonal, prepend,x--,y--, continue + y := x - k + realx, realy := x+e.lx, y+e.ly + if e.eq.eq(realx-1, realy-1) { + ans = prependlcs(ans, realx-1, realy-1) + x-- + } else { + panic("broken path") + } + } + return ans +} + +// start at (x,y), go up the diagonal as far as possible, +// and label the result with d +func (e *editGraph) lookForward(k, relx int) int { + rely := relx - k + x, y := relx+e.lx, rely+e.ly + for x < e.ux && y < e.uy && e.eq.eq(x, y) { + x++ + y++ + } + return x +} + +func (e *editGraph) setForward(d, k, relx int) { + x := e.lookForward(k, relx) + e.vf.set(d, k, x-e.lx) +} + +func (e *editGraph) getForward(d, k int) int { + x := e.vf.get(d, k) + return x +} + +// --- BACKWARD --- +// bdone decides if the backward path has reached the lower left corner +func (e *editGraph) bdone(D, k int) (bool, lcs) { + // x, y, k are relative to the rectangle + x := e.vb.get(D, k) + y := x - (k + e.delta) + if x == 0 && y == 0 { + return true, e.backwardlcs(D, k) + } + return false, nil +} + +// run the backward algorithm, until success or up to the limit on D. +func (e *editGraph) backward() lcs { + e.setBackward(0, 0, e.ux) + if ok, ans := e.bdone(0, 0); ok { + return ans + } + // from D to D+1 + for D := 0; D < e.limit; D++ { + e.setBackward(D+1, -(D + 1), e.getBackward(D, -D)-1) + if ok, ans := e.bdone(D+1, -(D + 1)); ok { + return ans + } + e.setBackward(D+1, D+1, e.getBackward(D, D)) + if ok, ans := e.bdone(D+1, D+1); ok { + return ans + } + for k := -D + 1; k <= D-1; k += 2 { + // these are tricky and easy to get wrong + lookv := e.lookBackward(k, e.getBackward(D, k-1)) + lookh := e.lookBackward(k, e.getBackward(D, k+1)-1) + if lookv < lookh { + e.setBackward(D+1, k, lookv) + } else { + e.setBackward(D+1, k, lookh) + } + if ok, ans := e.bdone(D+1, k); ok { + return ans + } + } + } + + // D is too large + // find the D path with minimal x+y inside the rectangle and + // use that to compute the part of the lcs found + kmax := -e.limit - 1 + diagmin := 1 << 25 + for k := -e.limit; k <= e.limit; k += 2 { + x := e.getBackward(e.limit, k) + y := x - (k + e.delta) + if x+y < diagmin && x >= 0 && y >= 0 { + diagmin, kmax = x+y, k + } + } + if kmax < -e.limit { + panic(fmt.Sprintf("no paths when limit=%d?", e.limit)) + } + return e.backwardlcs(e.limit, kmax) +} + +// recover the lcs by backtracking +func (e *editGraph) backwardlcs(D, k int) lcs { + var ans lcs + for x := e.getBackward(D, k); x != e.ux || x-(k+e.delta) != e.uy; { + if ok(D-1, k-1) && x == e.getBackward(D-1, k-1) { + // D--, k--, x unchanged + D, k = D-1, k-1 + continue + } else if ok(D-1, k+1) && x+1 == e.getBackward(D-1, k+1) { + // D--, k++, x++ + D, k, x = D-1, k+1, x+1 + continue + } + y := x - (k + e.delta) + realx, realy := x+e.lx, y+e.ly + if e.eq.eq(realx, realy) { + ans = appendlcs(ans, realx, realy) + x++ + } else { + panic("broken path") + } + } + return ans +} + +// start at (x,y), go down the diagonal as far as possible, +func (e *editGraph) lookBackward(k, relx int) int { + rely := relx - (k + e.delta) // forward k = k + e.delta + x, y := relx+e.lx, rely+e.ly + for x > 0 && y > 0 && e.eq.eq(x-1, y-1) { + x-- + y-- + } + return x +} + +// convert to rectangle, and label the result with d +func (e *editGraph) setBackward(d, k, relx int) { + x := e.lookBackward(k, relx) + e.vb.set(d, k, x-e.lx) +} + +func (e *editGraph) getBackward(d, k int) int { + x := e.vb.get(d, k) + return x +} + +// -- TWOSIDED --- + +func (e *editGraph) twosided() lcs { + // The termination condition could be improved, as either the forward + // or backward pass could succeed before Myers' Lemma applies. + // Aside from questions of efficiency (is the extra testing cost-effective) + // this is more likely to matter when e.limit is reached. + e.setForward(0, 0, e.lx) + e.setBackward(0, 0, e.ux) + + // from D to D+1 + for D := 0; D < e.limit; D++ { + // just finished a backwards pass, so check + if got, ok := e.twoDone(D, D); ok { + return e.twolcs(D, D, got) + } + // do a forwards pass (D to D+1) + e.setForward(D+1, -(D + 1), e.getForward(D, -D)) + e.setForward(D+1, D+1, e.getForward(D, D)+1) + for k := -D + 1; k <= D-1; k += 2 { + // these are tricky and easy to get backwards + lookv := e.lookForward(k, e.getForward(D, k-1)+1) + lookh := e.lookForward(k, e.getForward(D, k+1)) + if lookv > lookh { + e.setForward(D+1, k, lookv) + } else { + e.setForward(D+1, k, lookh) + } + } + // just did a forward pass, so check + if got, ok := e.twoDone(D+1, D); ok { + return e.twolcs(D+1, D, got) + } + // do a backward pass, D to D+1 + e.setBackward(D+1, -(D + 1), e.getBackward(D, -D)-1) + e.setBackward(D+1, D+1, e.getBackward(D, D)) + for k := -D + 1; k <= D-1; k += 2 { + // these are tricky and easy to get wrong + lookv := e.lookBackward(k, e.getBackward(D, k-1)) + lookh := e.lookBackward(k, e.getBackward(D, k+1)-1) + if lookv < lookh { + e.setBackward(D+1, k, lookv) + } else { + e.setBackward(D+1, k, lookh) + } + } + } + + // D too large. combine a forward and backward partial lcs + // first, a forward one + kmax := -e.limit - 1 + diagmax := -1 + for k := -e.limit; k <= e.limit; k += 2 { + x := e.getForward(e.limit, k) + y := x - k + if x+y > diagmax && x <= e.ux && y <= e.uy { + diagmax, kmax = x+y, k + } + } + if kmax < -e.limit { + panic(fmt.Sprintf("no forward paths when limit=%d?", e.limit)) + } + lcs := e.forwardlcs(e.limit, kmax) + // now a backward one + // find the D path with minimal x+y inside the rectangle and + // use that to compute the lcs + diagmin := 1 << 25 // infinity + for k := -e.limit; k <= e.limit; k += 2 { + x := e.getBackward(e.limit, k) + y := x - (k + e.delta) + if x+y < diagmin && x >= 0 && y >= 0 { + diagmin, kmax = x+y, k + } + } + if kmax < -e.limit { + panic(fmt.Sprintf("no backward paths when limit=%d?", e.limit)) + } + lcs = append(lcs, e.backwardlcs(e.limit, kmax)...) + // These may overlap (e.forwardlcs and e.backwardlcs return sorted lcs) + ans := lcs.fix() + return ans +} + +// Does Myers' Lemma apply? +func (e *editGraph) twoDone(df, db int) (int, bool) { + if (df+db+e.delta)%2 != 0 { + return 0, false // diagonals cannot overlap + } + kmin := -db + e.delta + if -df > kmin { + kmin = -df + } + kmax := db + e.delta + if df < kmax { + kmax = df + } + for k := kmin; k <= kmax; k += 2 { + x := e.vf.get(df, k) + u := e.vb.get(db, k-e.delta) + if u <= x { + // is it worth looking at all the other k? + for l := k; l <= kmax; l += 2 { + x := e.vf.get(df, l) + y := x - l + u := e.vb.get(db, l-e.delta) + v := u - l + if x == u || u == 0 || v == 0 || y == e.uy || x == e.ux { + return l, true + } + } + return k, true + } + } + return 0, false +} + +func (e *editGraph) twolcs(df, db, kf int) lcs { + // db==df || db+1==df + x := e.vf.get(df, kf) + y := x - kf + kb := kf - e.delta + u := e.vb.get(db, kb) + v := u - kf + + // Myers proved there is a df-path from (0,0) to (u,v) + // and a db-path from (x,y) to (N,M). + // In the first case the overall path is the forward path + // to (u,v) followed by the backward path to (N,M). + // In the second case the path is the backward path to (x,y) + // followed by the forward path to (x,y) from (0,0). + + // Look for some special cases to avoid computing either of these paths. + if x == u { + // "babaab" "cccaba" + // already patched together + lcs := e.forwardlcs(df, kf) + lcs = append(lcs, e.backwardlcs(db, kb)...) + return lcs.sort() + } + + // is (u-1,v) or (u,v-1) labelled df-1? + // if so, that forward df-1-path plus a horizontal or vertical edge + // is the df-path to (u,v), then plus the db-path to (N,M) + if u > 0 && ok(df-1, u-1-v) && e.vf.get(df-1, u-1-v) == u-1 { + // "aabbab" "cbcabc" + lcs := e.forwardlcs(df-1, u-1-v) + lcs = append(lcs, e.backwardlcs(db, kb)...) + return lcs.sort() + } + if v > 0 && ok(df-1, (u-(v-1))) && e.vf.get(df-1, u-(v-1)) == u { + // "abaabb" "bcacab" + lcs := e.forwardlcs(df-1, u-(v-1)) + lcs = append(lcs, e.backwardlcs(db, kb)...) + return lcs.sort() + } + + // The path can't possibly contribute to the lcs because it + // is all horizontal or vertical edges + if u == 0 || v == 0 || x == e.ux || y == e.uy { + // "abaabb" "abaaaa" + if u == 0 || v == 0 { + return e.backwardlcs(db, kb) + } + return e.forwardlcs(df, kf) + } + + // is (x+1,y) or (x,y+1) labelled db-1? + if x+1 <= e.ux && ok(db-1, x+1-y-e.delta) && e.vb.get(db-1, x+1-y-e.delta) == x+1 { + // "bababb" "baaabb" + lcs := e.backwardlcs(db-1, kb+1) + lcs = append(lcs, e.forwardlcs(df, kf)...) + return lcs.sort() + } + if y+1 <= e.uy && ok(db-1, x-(y+1)-e.delta) && e.vb.get(db-1, x-(y+1)-e.delta) == x { + // "abbbaa" "cabacc" + lcs := e.backwardlcs(db-1, kb-1) + lcs = append(lcs, e.forwardlcs(df, kf)...) + return lcs.sort() + } + + // need to compute another path + // "aabbaa" "aacaba" + lcs := e.backwardlcs(db, kb) + oldx, oldy := e.ux, e.uy + e.ux = u + e.uy = v + lcs = append(lcs, e.forward()...) + e.ux, e.uy = oldx, oldy + return lcs.sort() +} diff --git a/internal/lsp/diff/lcs/old_test.go b/internal/lsp/diff/lcs/old_test.go new file mode 100644 index 00000000000..ba22fe6f461 --- /dev/null +++ b/internal/lsp/diff/lcs/old_test.go @@ -0,0 +1,203 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "math/rand" + "testing" +) + +func TestForwardOld(t *testing.T) { + for _, tx := range Btests { + lim := len(tx.a) + len(tx.b) + left, right := []byte(tx.a), []byte(tx.b) + g := newegraph(left, right, lim) + lcs := g.forward() + diffs := g.fromlcs(lcs) + check(t, tx.a, lcs, tx.lcs) + checkDiffs(t, tx.a, diffs, tx.b) + + g = newegraph(right, left, lim) + lcs = g.forward() + diffs = g.fromlcs(lcs) + check(t, tx.b, lcs, tx.lcs) + checkDiffs(t, tx.b, diffs, tx.a) + } +} + +func TestBackwardOld(t *testing.T) { + for _, tx := range Btests { + lim := len(tx.a) + len(tx.b) + left, right := []byte(tx.a), []byte(tx.b) + g := newegraph(left, right, lim) + lcs := g.backward() + check(t, tx.a, lcs, tx.lcs) + diffs := g.fromlcs(lcs) + checkDiffs(t, tx.a, diffs, tx.b) + + g = newegraph(right, left, lim) + lcs = g.backward() + diffs = g.fromlcs(lcs) + check(t, tx.b, lcs, tx.lcs) + checkDiffs(t, tx.b, diffs, tx.a) + } +} + +func TestTwosidedOld(t *testing.T) { + // test both (a,b) and (b,a) + for _, tx := range Btests { + left, right := []byte(tx.a), []byte(tx.b) + lim := len(tx.a) + len(tx.b) + diffs, lcs := Compute(left, right, lim) + check(t, tx.a, lcs, tx.lcs) + checkDiffs(t, tx.a, diffs, tx.b) + diffs, lcs = Compute(right, left, lim) + check(t, tx.b, lcs, tx.lcs) + checkDiffs(t, tx.b, diffs, tx.a) + } +} + +func TestIntOld(t *testing.T) { + // need to avoid any characters in btests + lfill, rfill := "AAAAAAAAAAAA", "BBBBBBBBBBBB" + for _, tx := range Btests { + if len(tx.a) < 2 || len(tx.b) < 2 { + continue + } + left := []byte(tx.a + lfill) + right := []byte(tx.b + rfill) + lim := len(tx.a) + len(tx.b) + diffs, lcs := Compute(left, right, lim) + check(t, string(left), lcs, tx.lcs) + checkDiffs(t, string(left), diffs, string(right)) + diffs, lcs = Compute(right, left, lim) + check(t, string(right), lcs, tx.lcs) + checkDiffs(t, string(right), diffs, string(left)) + + left = []byte(lfill + tx.a) + right = []byte(rfill + tx.b) + diffs, lcs = Compute(left, right, lim) + check(t, string(left), lcs, tx.lcs) + checkDiffs(t, string(left), diffs, string(right)) + diffs, lcs = Compute(right, left, lim) + check(t, string(right), lcs, tx.lcs) + checkDiffs(t, string(right), diffs, string(left)) + } +} + +func TestSpecialOld(t *testing.T) { // needs lcs.fix + a := []byte("golang.org/x/tools/intern") + b := []byte("github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/intern") + diffs, lcs := Compute(a, b, 4) + if !lcs.valid() { + t.Errorf("%d,%v", len(diffs), lcs) + } +} + +func TestRegressionOld001(t *testing.T) { + a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/lsp/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + + b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/lsp/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + for i := 1; i < len(b); i++ { + diffs, lcs := Compute([]byte(a), []byte(b), int(i)) // 14 from gopls + if !lcs.valid() { + t.Errorf("%d,%v", len(diffs), lcs) + } + checkDiffs(t, a, diffs, b) + } +} + +func TestRegressionOld002(t *testing.T) { + a := "n\"\n)\n" + b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n" + for i := 1; i <= len(b); i++ { + diffs, lcs := Compute([]byte(a), []byte(b), int(i)) + if !lcs.valid() { + t.Errorf("%d,%v", len(diffs), lcs) + } + checkDiffs(t, a, diffs, b) + } +} + +func TestRegressionOld003(t *testing.T) { + a := "golang.org/x/hello v1.0.0\nrequire golang.org/x/unused v1" + b := "golang.org/x/hello v1" + for i := 1; i <= len(a); i++ { + diffs, lcs := Compute([]byte(a), []byte(b), int(i)) + if !lcs.valid() { + t.Errorf("%d,%v", len(diffs), lcs) + } + checkDiffs(t, a, diffs, b) + } +} + +func TestRandOld(t *testing.T) { + rand.Seed(1) + for i := 0; i < 1000; i++ { + a := []rune(randstr("abω", 16)) + b := []rune(randstr("abωc", 16)) + g := newegraph(a, b, 24) // large enough to get true lcs + two := g.twosided() + forw := g.forward() + back := g.backward() + if lcslen(two) != lcslen(forw) || lcslen(forw) != lcslen(back) { + t.Logf("\n%v\n%v\n%v", forw, back, two) + t.Fatalf("%d forw:%d back:%d two:%d", i, lcslen(forw), lcslen(back), lcslen(two)) + } + if !two.valid() || !forw.valid() || !back.valid() { + t.Errorf("check failure") + } + } +} + +func BenchmarkTwoOld(b *testing.B) { + tests := genBench("abc", 96) + for i := 0; i < b.N; i++ { + for _, tt := range tests { + _, two := Compute([]byte(tt.before), []byte(tt.after), 100) + if !two.valid() { + b.Error("check failed") + } + } + } +} + +func BenchmarkForwOld(b *testing.B) { + tests := genBench("abc", 96) + for i := 0; i < b.N; i++ { + for _, tt := range tests { + _, two := Compute([]byte(tt.before), []byte(tt.after), 100) + if !two.valid() { + b.Error("check failed") + } + } + } +} + +func genBench(set string, n int) []struct{ before, after string } { + // before and after for benchmarks. 24 strings of length n with + // before and after differing at least once, and about 5% + rand.Seed(3) + var ans []struct{ before, after string } + for i := 0; i < 24; i++ { + // maybe b should have an approximately known number of diffs + a := randstr(set, n) + cnt := 0 + bb := make([]rune, 0, n) + for _, r := range a { + if rand.Float64() < .05 { + cnt++ + r = 'N' + } + bb = append(bb, r) + } + if cnt == 0 { + // avoid == shortcut + bb[n/2] = 'N' + } + ans = append(ans, struct{ before, after string }{a, string(bb)}) + } + return ans +} diff --git a/internal/lsp/diff/ndiff.go b/internal/lsp/diff/ndiff.go new file mode 100644 index 00000000000..8f7732dd019 --- /dev/null +++ b/internal/lsp/diff/ndiff.go @@ -0,0 +1,130 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diff + +import ( + "strings" + "unicode/utf8" + + "golang.org/x/tools/internal/lsp/diff/lcs" + "golang.org/x/tools/internal/span" +) + +// maxDiffs is a limit on how deeply the lcs algorithm should search +// the value is just a guess +const maxDiffs = 30 + +// NComputeEdits computes TextEdits for strings +// (both it and the diff in the myers package have type ComputeEdits, which +// is why the arguments are strings, not []bytes.) +func NComputeEdits(uri span.URI, before, after string) ([]TextEdit, error) { + if before == after { + // very frequently true + return nil, nil + } + // the diffs returned by the lcs package use indexes into whatever slice + // was passed in. TextEdits need a span.Span which is computed with + // byte offsets, so rune or line offsets need to be converted. + if needrunes(before) || needrunes(after) { + diffs, _ := lcs.Compute([]rune(before), []rune(after), maxDiffs/2) + diffs = runeOffsets(diffs, []rune(before)) + ans, err := convertDiffs(uri, diffs, []byte(before)) + return ans, err + } else { + diffs, _ := lcs.Compute([]byte(before), []byte(after), maxDiffs/2) + ans, err := convertDiffs(uri, diffs, []byte(before)) + return ans, err + } +} + +// NComputeLineEdits computes TextEdits for []strings +func NComputeLineEdits(uri span.URI, before, after []string) ([]TextEdit, error) { + diffs, _ := lcs.Compute(before, after, maxDiffs/2) + diffs = lineOffsets(diffs, before) + ans, err := convertDiffs(uri, diffs, []byte(strJoin(before))) + // the code is not coping with possible missing \ns at the ends + return ans, err +} + +// convert diffs with byte offsets into diffs with line and column +func convertDiffs(uri span.URI, diffs []lcs.Diff, src []byte) ([]TextEdit, error) { + ans := make([]TextEdit, len(diffs)) + tf := span.NewTokenFile(uri.Filename(), src) + for i, d := range diffs { + s := newSpan(uri, d.Start, d.End) + s, err := s.WithPosition(tf) + if err != nil { + return nil, err + } + ans[i] = TextEdit{s, d.Text} + } + return ans, nil +} + +// convert diffs with rune offsets into diffs with byte offsets +func runeOffsets(diffs []lcs.Diff, src []rune) []lcs.Diff { + var idx int + var tmp strings.Builder // string because []byte([]rune) is illegal + for i, d := range diffs { + tmp.WriteString(string(src[idx:d.Start])) + v := tmp.Len() + tmp.WriteString(string(src[d.Start:d.End])) + d.Start = v + idx = d.End + d.End = tmp.Len() + diffs[i] = d + } + return diffs +} + +// convert diffs with line offsets into diffs with byte offsets +func lineOffsets(diffs []lcs.Diff, src []string) []lcs.Diff { + var idx int + var tmp strings.Builder // bytes/ + for i, d := range diffs { + tmp.WriteString(strJoin(src[idx:d.Start])) + v := tmp.Len() + tmp.WriteString(strJoin(src[d.Start:d.End])) + d.Start = v + idx = d.End + d.End = tmp.Len() + diffs[i] = d + } + return diffs +} + +// join lines. (strings.Join doesn't add a trailing separator) +func strJoin(elems []string) string { + if len(elems) == 0 { + return "" + } + n := 0 + for i := 0; i < len(elems); i++ { + n += len(elems[i]) + } + + var b strings.Builder + b.Grow(n) + for _, s := range elems { + b.WriteString(s) + //b.WriteByte('\n') + } + return b.String() +} + +func newSpan(uri span.URI, left, right int) span.Span { + return span.New(uri, span.NewPoint(0, 0, left), span.NewPoint(0, 0, right)) +} + +// need runes is true if the string needs to be converted to []rune +// for random access +func needrunes(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] >= utf8.RuneSelf { + return true + } + } + return false +} diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index c386eee45e7..8fa78ae2610 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -477,7 +477,7 @@ type Hooks struct { // LicensesText holds third party licenses for software used by gopls. LicensesText string - // TODO(rfindley): is this even necessary? + // GoDiff is used in gopls/hooks to get Myers' diff GoDiff bool // Whether staticcheck is supported. @@ -562,6 +562,13 @@ type InternalOptions struct { // on the server. // This option applies only during initialization. ShowBugReports bool + + // NewDiff controls the choice of the new diff implementation. + // It can be 'new', 'checked', or 'old' which is the default. + // 'checked' computes diffs with both algorithms, checks + // that the new algorithm has worked, and write some summary + // statistics to a file in os.TmpDir() + NewDiff string } type ImportShortcut string @@ -1059,6 +1066,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) // This setting should be handled before all of the other options are // processed, so do nothing here. + case "newDiff": + result.setString(&o.NewDiff) + // Replaced settings. case "experimentalDisabledAnalyses": result.deprecated("analyses") diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index ec804e5e79e..b60fbf03866 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -265,6 +265,7 @@ func DefaultOptions(o *source.Options) { o.HierarchicalDocumentSymbolSupport = true o.ExperimentalWorkspaceModule = true o.SemanticTokens = true + o.InternalOptions.NewDiff = "both" } func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*testing.T, *Data)) { From 1cfe623ebfa0ec9b232510b6b504dce8a0942a9a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 22 Jul 2022 11:40:02 -0400 Subject: [PATCH 122/723] gopls/internal/regtest: unskip TestQuickFixEmptyFiles This test ran slowly sometimes. Now that we have improved performance, elimininated arbitrary timeouts, and improved cacheability of computed results when running with -short, I suspect this test should no longer flake. If it does, we can reduce its cost in other ways rather than turning it off entirely. Updates golang/go#48773 Updates golang/go#53878 Change-Id: I878e78117df5a1a25f4ac5f72e02f28fc078ec73 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419106 Run-TryBot: Robert Findley Reviewed-by: Bryan Mills gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/regtest/diagnostics/diagnostics_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index ae8b4a56cdd..d404b65f4de 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -964,8 +964,6 @@ const C = a.A // This is a copy of the scenario_default/quickfix_empty_files.txt test from // govim. Reproduces golang/go#39646. func TestQuickFixEmptyFiles(t *testing.T) { - t.Skip("too flaky: golang/go#48773") - testenv.NeedsGo1Point(t, 15) const mod = ` From 178fdf98da63010b55ff90c04614e0ca9a69a2cd Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 21 Jul 2022 22:47:23 -0400 Subject: [PATCH 123/723] gopls/internal/regtest: unskip Test_Issue38211 This test was originally skipped due to deadline exceeded errors. In the time since, we've made performance improvements, fixed races, and altered the regtests to remove arbitrary deadlines. Unskip it to see if it still flakes. For golang/go#44098 For golang/go#53878 Change-Id: I06530f2bc9c6883f415dc9147cfcbf260abb2a00 Reviewed-on: https://go-review.googlesource.com/c/tools/+/418898 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Bryan Mills --- gopls/internal/regtest/diagnostics/diagnostics_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index d404b65f4de..9c9ad368cf3 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -636,8 +636,6 @@ var ErrHelpWanted error // Test for golang/go#38211. func Test_Issue38211(t *testing.T) { - t.Skipf("Skipping flaky test: https://golang.org/issue/44098") - testenv.NeedsGo1Point(t, 14) const ardanLabs = ` -- go.mod -- From 6ec939a616607e6443b9037c89557109eedbddfd Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 22 Jul 2022 13:51:59 -0400 Subject: [PATCH 124/723] internal/span: fix incorrect bounds check in ToOffset token.File.LineStart panics if line < 1, but we were checking line < 0. Surprising that this was not hit more often: it looks like we pre-validate input except for parsed positions coming from the Go command. It is possible that an older version of the Go command returned invalid positions. Also report a bug for one error condition in ToOffset: the position returned by LineStart should always be valid. Fixes golang/go#54006 Change-Id: I5965af9c62976b3e00b023512df334a8de943a3d Reviewed-on: https://go-review.googlesource.com/c/tools/+/419109 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro --- internal/span/token.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/internal/span/token.go b/internal/span/token.go index cae696db757..c35a512c158 100644 --- a/internal/span/token.go +++ b/internal/span/token.go @@ -176,15 +176,16 @@ func ToPosition(tf *token.File, offset int) (int, int, error) { return line, col, err } -// ToOffset converts a 1-base line and utf-8 column index into a byte offset in -// the file corresponding to tf. +// ToOffset converts a 1-based line and utf-8 column index into a byte offset +// in the file corresponding to tf. func ToOffset(tf *token.File, line, col int) (int, error) { - if line < 0 { - return -1, fmt.Errorf("line is not valid") + if line < 1 { // token.File.LineStart panics if line < 1 + return -1, fmt.Errorf("invalid line: %d", line) } + lineMax := tf.LineCount() + 1 if line > lineMax { - return -1, fmt.Errorf("line is beyond end of file %v", lineMax) + return -1, fmt.Errorf("line %d is beyond end of file %v", line, lineMax) } else if line == lineMax { if col > 1 { return -1, fmt.Errorf("column is beyond end of file") @@ -194,7 +195,9 @@ func ToOffset(tf *token.File, line, col int) (int, error) { } pos := tf.LineStart(line) if !pos.IsValid() { - return -1, fmt.Errorf("line is not in file") + // bug.Errorf here because LineStart panics on out-of-bound input, and so + // should never return invalid positions. + return -1, bug.Errorf("line is not in file") } // we assume that column is in bytes here, and that the first byte of a // line is at column 1 From 04bd0878179c06f3950f7440743a458185f2f961 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Thu, 30 Jun 2022 16:45:30 -0400 Subject: [PATCH 125/723] internal/lsp: enable fillstruct for generics This enables some fill struct code actions for instances of structs with type parameters. This additionally adds a filtering mechanism to the suggested fixes in order to account for multiple suggested fixes in the same location. Change-Id: I98866b462b026f4c5a4897bc278f704381623f25 Reviewed-on: https://go-review.googlesource.com/c/tools/+/418415 Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Suzy Mueller --- .../lsp/analysis/fillstruct/fillstruct.go | 8 - .../testdata/src/typeparams/typeparams.go | 12 +- internal/lsp/cmd/test/suggested_fix.go | 8 +- internal/lsp/lsp_test.go | 16 +- internal/lsp/source/source_test.go | 14 +- .../extract_variable/extract_basic_lit.go | 4 +- .../extract_basic_lit.go.golden | 8 +- .../extract_variable/extract_func_call.go | 4 +- .../extract_func_call.go.golden | 12 +- .../extract/extract_variable/extract_scope.go | 4 +- .../extract_variable/extract_scope.go.golden | 8 +- internal/lsp/testdata/fillstruct/a.go | 8 +- internal/lsp/testdata/fillstruct/a.go.golden | 32 +- internal/lsp/testdata/fillstruct/a2.go | 8 +- internal/lsp/testdata/fillstruct/a2.go.golden | 32 +- internal/lsp/testdata/fillstruct/a3.go | 10 +- internal/lsp/testdata/fillstruct/a3.go.golden | 50 +-- internal/lsp/testdata/fillstruct/a4.go | 8 +- internal/lsp/testdata/fillstruct/a4.go.golden | 32 +- .../lsp/testdata/fillstruct/fill_struct.go | 8 +- .../testdata/fillstruct/fill_struct.go.golden | 32 +- .../testdata/fillstruct/fill_struct_anon.go | 2 +- .../fillstruct/fill_struct_anon.go.golden | 2 +- .../testdata/fillstruct/fill_struct_nested.go | 2 +- .../fillstruct/fill_struct_nested.go.golden | 2 +- .../fillstruct/fill_struct_package.go | 4 +- .../fillstruct/fill_struct_package.go.golden | 8 +- .../fillstruct/fill_struct_partial.go | 4 +- .../fillstruct/fill_struct_partial.go.golden | 8 +- .../testdata/fillstruct/fill_struct_spaces.go | 2 +- .../fillstruct/fill_struct_spaces.go.golden | 2 +- .../testdata/fillstruct/fill_struct_unsafe.go | 2 +- .../fillstruct/fill_struct_unsafe.go.golden | 2 +- .../lsp/testdata/fillstruct/typeparams.go | 38 ++ .../testdata/fillstruct/typeparams.go.golden | 328 ++++++++++++++++++ .../lsp/testdata/missingfunction/channels.go | 2 +- .../missingfunction/channels.go.golden | 2 +- .../missingfunction/consecutive_params.go | 2 +- .../consecutive_params.go.golden | 2 +- .../testdata/missingfunction/error_param.go | 2 +- .../missingfunction/error_param.go.golden | 2 +- .../lsp/testdata/missingfunction/literals.go | 2 +- .../missingfunction/literals.go.golden | 4 +- .../lsp/testdata/missingfunction/operation.go | 2 +- .../missingfunction/operation.go.golden | 4 +- .../lsp/testdata/missingfunction/selector.go | 2 +- .../missingfunction/selector.go.golden | 2 +- .../lsp/testdata/missingfunction/slice.go | 2 +- .../testdata/missingfunction/slice.go.golden | 2 +- .../lsp/testdata/missingfunction/tuple.go | 2 +- .../testdata/missingfunction/tuple.go.golden | 2 +- .../testdata/missingfunction/unique_params.go | 2 +- .../missingfunction/unique_params.go.golden | 4 +- .../lsp/testdata/stub/stub_add_selector.go | 2 +- .../testdata/stub/stub_add_selector.go.golden | 2 +- internal/lsp/testdata/stub/stub_assign.go | 2 +- .../lsp/testdata/stub/stub_assign.go.golden | 2 +- .../testdata/stub/stub_assign_multivars.go | 2 +- .../stub/stub_assign_multivars.go.golden | 2 +- internal/lsp/testdata/stub/stub_call_expr.go | 2 +- .../testdata/stub/stub_call_expr.go.golden | 2 +- internal/lsp/testdata/stub/stub_embedded.go | 2 +- .../lsp/testdata/stub/stub_embedded.go.golden | 2 +- internal/lsp/testdata/stub/stub_err.go | 2 +- internal/lsp/testdata/stub/stub_err.go.golden | 2 +- .../lsp/testdata/stub/stub_function_return.go | 2 +- .../stub/stub_function_return.go.golden | 2 +- .../testdata/stub/stub_generic_receiver.go | 2 +- .../stub/stub_generic_receiver.go.golden | 2 +- .../lsp/testdata/stub/stub_ignored_imports.go | 2 +- .../stub/stub_ignored_imports.go.golden | 2 +- internal/lsp/testdata/stub/stub_multi_var.go | 2 +- .../testdata/stub/stub_multi_var.go.golden | 2 +- internal/lsp/testdata/stub/stub_pointer.go | 2 +- .../lsp/testdata/stub/stub_pointer.go.golden | 2 +- .../lsp/testdata/stub/stub_renamed_import.go | 2 +- .../stub/stub_renamed_import.go.golden | 2 +- .../stub/stub_renamed_import_iface.go | 2 +- .../stub/stub_renamed_import_iface.go.golden | 2 +- internal/lsp/testdata/stub/stub_stdlib.go | 2 +- .../lsp/testdata/stub/stub_stdlib.go.golden | 2 +- .../suggestedfix/has_suggested_fix.go | 2 +- .../suggestedfix/has_suggested_fix.go.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- .../lsp/testdata/typeerrors/noresultvalues.go | 4 +- .../typeerrors/noresultvalues.go.golden | 8 +- internal/lsp/testdata/undeclared/var.go | 6 +- .../lsp/testdata/undeclared/var.go.golden | 18 +- internal/lsp/tests/README.md | 2 +- internal/lsp/tests/tests.go | 15 +- 90 files changed, 624 insertions(+), 257 deletions(-) create mode 100644 internal/lsp/testdata/fillstruct/typeparams.go create mode 100644 internal/lsp/testdata/fillstruct/typeparams.go.golden diff --git a/internal/lsp/analysis/fillstruct/fillstruct.go b/internal/lsp/analysis/fillstruct/fillstruct.go index 2103a55879e..2c0084ff6f5 100644 --- a/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/internal/lsp/analysis/fillstruct/fillstruct.go @@ -68,14 +68,6 @@ func run(pass *analysis.Pass) (interface{}, error) { return } - // Ignore types that have type parameters for now. - // TODO: support type params. - if typ, ok := typ.(*types.Named); ok { - if tparams := typeparams.ForNamed(typ); tparams != nil && tparams.Len() > 0 { - return - } - } - // Find reference to the type declaration of the struct being initialized. for { p, ok := typ.Underlying().(*types.Pointer) diff --git a/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go b/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go index 90290613d87..7972bd3e12b 100644 --- a/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go +++ b/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go @@ -12,18 +12,16 @@ type basicStruct[T any] struct { foo T } -var _ = basicStruct[int]{} - -type fooType[T any] T +var _ = basicStruct[int]{} // want "" type twoArgStruct[F, B any] struct { - foo fooType[F] - bar fooType[B] + foo F + bar B } -var _ = twoArgStruct[string, int]{} +var _ = twoArgStruct[string, int]{} // want "" -var _ = twoArgStruct[int, string]{ +var _ = twoArgStruct[int, string]{ // want "" bar: "bar", } diff --git a/internal/lsp/cmd/test/suggested_fix.go b/internal/lsp/cmd/test/suggested_fix.go index c819e051735..db401350fb1 100644 --- a/internal/lsp/cmd/test/suggested_fix.go +++ b/internal/lsp/cmd/test/suggested_fix.go @@ -12,14 +12,16 @@ import ( "golang.org/x/tools/internal/span" ) -func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, expectedActions int) { +func (r *runner) SuggestedFix(t *testing.T, spn span.Span, suggestedFixes []tests.SuggestedFix, expectedActions int) { uri := spn.URI() filename := uri.Filename() args := []string{"fix", "-a", fmt.Sprintf("%s", spn)} - for _, kind := range actionKinds { - if kind == "refactor.rewrite" { + var actionKinds []string + for _, sf := range suggestedFixes { + if sf.ActionKind == "refactor.rewrite" { t.Skip("refactor.rewrite is not yet supported on the command line") } + actionKinds = append(actionKinds, sf.ActionKind) } args = append(args, actionKinds...) got, stderr := r.NormalizeGoplsCmd(t, args...) diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index e8febec93f3..8e0e628dfea 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -487,7 +487,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) { } } -func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, expectedActions int) { +func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.SuggestedFix, expectedActions int) { uri := spn.URI() view, err := r.server.session.ViewOf(uri) if err != nil { @@ -516,9 +516,9 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, } codeActionKinds := []protocol.CodeActionKind{} for _, k := range actionKinds { - codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k)) + codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k.ActionKind)) } - actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ + allActions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.URIFromSpanURI(uri), }, @@ -531,6 +531,16 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, if err != nil { t.Fatalf("CodeAction %s failed: %v", spn, err) } + var actions []protocol.CodeAction + for _, action := range allActions { + for _, fix := range actionKinds { + if strings.Contains(action.Title, fix.Title) { + actions = append(actions, action) + break + } + } + + } if len(actions) != expectedActions { // Hack: We assume that we only get one code action per range. var cmds []string diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index 8beb8a5dde0..c670bdeb0b2 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -968,14 +968,12 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa } // These are pure LSP features, no source level functionality to be tested. -func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {} - -func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, expectedActions int) { -} -func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {} -func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) {} -func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {} -func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {} +func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {} +func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actions []tests.SuggestedFix, want int) {} +func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {} +func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) {} +func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {} +func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {} func spanToRange(data *tests.Data, spn span.Span) (*protocol.ColumnMapper, protocol.Range, error) { m, err := data.Mapper(spn.URI()) diff --git a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go b/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go index c49e5d6a017..cbb70a04cd1 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go +++ b/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go @@ -1,6 +1,6 @@ package extract func _() { - var _ = 1 + 2 //@suggestedfix("1", "refactor.extract") - var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract") + var _ = 1 + 2 //@suggestedfix("1", "refactor.extract", "") + var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract", "") } diff --git a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden b/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden index 00ee7b4f94d..3fd9b328711 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden +++ b/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden @@ -3,16 +3,16 @@ package extract func _() { x := 1 - var _ = x + 2 //@suggestedfix("1", "refactor.extract") - var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract") + var _ = x + 2 //@suggestedfix("1", "refactor.extract", "") + var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract", "") } -- suggestedfix_extract_basic_lit_5_10 -- package extract func _() { - var _ = 1 + 2 //@suggestedfix("1", "refactor.extract") + var _ = 1 + 2 //@suggestedfix("1", "refactor.extract", "") x := 3 + 4 - var _ = x //@suggestedfix("3 + 4", "refactor.extract") + var _ = x //@suggestedfix("3 + 4", "refactor.extract", "") } diff --git a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go b/internal/lsp/testdata/extract/extract_variable/extract_func_call.go index badc010dce4..a20b45f5869 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go +++ b/internal/lsp/testdata/extract/extract_variable/extract_func_call.go @@ -3,7 +3,7 @@ package extract import "strconv" func _() { - x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract") + x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") str := "1" - b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract") + b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") } diff --git a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden b/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden index 74df67ee65f..4423fc92770 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden +++ b/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden @@ -5,9 +5,9 @@ import "strconv" func _() { x0 := append([]int{}, 1) - a := x0 //@suggestedfix("append([]int{}, 1)", "refactor.extract") + a := x0 //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") str := "1" - b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract") + b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") } -- suggestedfix_extract_func_call_6_8 -- @@ -17,9 +17,9 @@ import "strconv" func _() { x := append([]int{}, 1) - x0 := x //@suggestedfix("append([]int{}, 1)", "refactor.extract") + x0 := x //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") str := "1" - b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract") + b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") } -- suggestedfix_extract_func_call_8_12 -- @@ -28,9 +28,9 @@ package extract import "strconv" func _() { - x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract") + x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") str := "1" x, x1 := strconv.Atoi(str) - b, err := x, x1 //@suggestedfix("strconv.Atoi(str)", "refactor.extract") + b, err := x, x1 //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") } diff --git a/internal/lsp/testdata/extract/extract_variable/extract_scope.go b/internal/lsp/testdata/extract/extract_variable/extract_scope.go index 5dfcc36203b..c14ad709212 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_scope.go +++ b/internal/lsp/testdata/extract/extract_variable/extract_scope.go @@ -5,9 +5,9 @@ import "go/ast" func _() { x0 := 0 if true { - y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract") + y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract", "") } if true { - x1 := !false //@suggestedfix("!false", "refactor.extract") + x1 := !false //@suggestedfix("!false", "refactor.extract", "") } } diff --git a/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden b/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden index e0e6464b59a..1c2f64b7df7 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden +++ b/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden @@ -6,11 +6,11 @@ import "go/ast" func _() { x0 := 0 if true { - y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract") + y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract", "") } if true { x := !false - x1 := x //@suggestedfix("!false", "refactor.extract") + x1 := x //@suggestedfix("!false", "refactor.extract", "") } } @@ -23,10 +23,10 @@ func _() { x0 := 0 if true { x := ast.CompositeLit{} - y := x //@suggestedfix("ast.CompositeLit{}", "refactor.extract") + y := x //@suggestedfix("ast.CompositeLit{}", "refactor.extract", "") } if true { - x1 := !false //@suggestedfix("!false", "refactor.extract") + x1 := !false //@suggestedfix("!false", "refactor.extract", "") } } diff --git a/internal/lsp/testdata/fillstruct/a.go b/internal/lsp/testdata/fillstruct/a.go index 5c6df6c4a7c..4fb855d06b5 100644 --- a/internal/lsp/testdata/fillstruct/a.go +++ b/internal/lsp/testdata/fillstruct/a.go @@ -8,20 +8,20 @@ type basicStruct struct { foo int } -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type nestedStruct struct { bar string basic basicStruct } -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite") +var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a.go.golden b/internal/lsp/testdata/fillstruct/a.go.golden index 5d6dbceb279..76789f0fc26 100644 --- a/internal/lsp/testdata/fillstruct/a.go.golden +++ b/internal/lsp/testdata/fillstruct/a.go.golden @@ -11,23 +11,23 @@ type basicStruct struct { var _ = basicStruct{ foo: 0, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type nestedStruct struct { bar string basic basicStruct } -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite") +var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a_18_22 -- package fillstruct @@ -40,7 +40,7 @@ type basicStruct struct { foo int } -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type twoArgStruct struct { foo int @@ -50,16 +50,16 @@ type twoArgStruct struct { var _ = twoArgStruct{ foo: 0, bar: "", -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type nestedStruct struct { bar string basic basicStruct } -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite") +var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a_25_22 -- package fillstruct @@ -72,14 +72,14 @@ type basicStruct struct { foo int } -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type nestedStruct struct { bar string @@ -89,9 +89,9 @@ type nestedStruct struct { var _ = nestedStruct{ bar: "", basic: basicStruct{}, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite") +var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a_27_16 -- package fillstruct @@ -104,23 +104,23 @@ type basicStruct struct { foo int } -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type nestedStruct struct { bar string basic basicStruct } -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = data.B{ ExportedInt: 0, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a2.go b/internal/lsp/testdata/fillstruct/a2.go index 8e12a6b54ba..b5e30a84f1e 100644 --- a/internal/lsp/testdata/fillstruct/a2.go +++ b/internal/lsp/testdata/fillstruct/a2.go @@ -8,22 +8,22 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a2.go.golden b/internal/lsp/testdata/fillstruct/a2.go.golden index 78a6ee2b691..2eca3e349a1 100644 --- a/internal/lsp/testdata/fillstruct/a2.go.golden +++ b/internal/lsp/testdata/fillstruct/a2.go.golden @@ -15,25 +15,25 @@ var _ = typedStruct{ c: make(chan int), c1: make(<-chan int), a: [2]string{}, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a2_17_19 -- package fillstruct @@ -46,7 +46,7 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int @@ -55,19 +55,19 @@ type funStruct struct { var _ = funStruct{ fn: func(i int) int { }, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a2_23_25 -- package fillstruct @@ -80,13 +80,13 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) @@ -95,13 +95,13 @@ type funStructCompex struct { var _ = funStructCompex{ fn: func(i int, s string) (string, int) { }, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a2_29_24 -- package fillstruct @@ -114,19 +114,19 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() @@ -135,5 +135,5 @@ type funStructEmpty struct { var _ = funStructEmpty{ fn: func() { }, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a3.go b/internal/lsp/testdata/fillstruct/a3.go index 730db305423..59cd9fa28b5 100644 --- a/internal/lsp/testdata/fillstruct/a3.go +++ b/internal/lsp/testdata/fillstruct/a3.go @@ -14,7 +14,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -25,7 +25,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -33,10 +33,10 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a3.go.golden b/internal/lsp/testdata/fillstruct/a3.go.golden index 1d8672927d9..a7c7baa8d27 100644 --- a/internal/lsp/testdata/fillstruct/a3.go.golden +++ b/internal/lsp/testdata/fillstruct/a3.go.golden @@ -18,7 +18,7 @@ type Bar struct { var _ = Bar{ X: &Foo{}, Y: &Foo{}, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -29,7 +29,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -37,13 +37,13 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a3_28_24 -- package fillstruct @@ -62,7 +62,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -81,7 +81,7 @@ var _ = importedStruct{ fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { }, st: ast.CompositeLit{}, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -89,13 +89,13 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a3_36_30 -- package fillstruct @@ -114,7 +114,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -125,7 +125,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -137,13 +137,13 @@ var _ = pointerBuiltinStruct{ b: new(bool), s: new(string), i: new(int), -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a3_39_3 -- package fillstruct @@ -162,7 +162,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -173,7 +173,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -181,17 +181,17 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ { ValuePos: 0, Kind: 0, Value: "", - }, //@suggestedfix("}", "refactor.rewrite") + }, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a3_42_25 -- package fillstruct @@ -210,7 +210,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -221,7 +221,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -229,15 +229,15 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } var _ = []ast.BasicLit{{ ValuePos: 0, Kind: 0, Value: "", -}} //@suggestedfix("}", "refactor.rewrite") +}} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a4.go b/internal/lsp/testdata/fillstruct/a4.go index 7833d338c64..5f52a55fa72 100644 --- a/internal/lsp/testdata/fillstruct/a4.go +++ b/internal/lsp/testdata/fillstruct/a4.go @@ -22,18 +22,18 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite") + var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/a4.go.golden b/internal/lsp/testdata/fillstruct/a4.go.golden index 109c6b5ea47..b1e376f05f1 100644 --- a/internal/lsp/testdata/fillstruct/a4.go.golden +++ b/internal/lsp/testdata/fillstruct/a4.go.golden @@ -25,20 +25,20 @@ func fill() { var x int var _ = iStruct{ X: x, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite") + var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_a4_28_18 -- @@ -66,22 +66,22 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var s string var _ = sStruct{ str: s, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite") + var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_a4_35_20 -- @@ -109,10 +109,10 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} @@ -123,10 +123,10 @@ func fill() { num: n, strin: s, arr: []int{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_a4_38_23 -- @@ -154,21 +154,21 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite") + var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit var _ = assignStruct{ n: node, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct.go b/internal/lsp/testdata/fillstruct/fill_struct.go index fccec135321..3da904741d0 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct.go +++ b/internal/lsp/testdata/fillstruct/fill_struct.go @@ -17,10 +17,10 @@ type StructA3 struct { } func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite") + a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") + b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") + c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite") + _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") } } diff --git a/internal/lsp/testdata/fillstruct/fill_struct.go.golden b/internal/lsp/testdata/fillstruct/fill_struct.go.golden index 8d997031516..de01a40f052 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct.go.golden +++ b/internal/lsp/testdata/fillstruct/fill_struct.go.golden @@ -24,11 +24,11 @@ func fill() { MapA: map[int]string{}, Array: []int{}, StructB: StructB{}, - } //@suggestedfix("}", "refactor.rewrite") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") + b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") + c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite") + _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") } } @@ -52,13 +52,13 @@ type StructA3 struct { } func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite") + a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") b := StructA2{ B: &StructB{}, - } //@suggestedfix("}", "refactor.rewrite") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") + c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite") + _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") } } @@ -82,13 +82,13 @@ type StructA3 struct { } func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite") + a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") + b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") c := StructA3{ B: StructB{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite") + _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") } } @@ -112,13 +112,13 @@ type StructA3 struct { } func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite") + a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") + b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") + c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") if true { _ = StructA3{ B: StructB{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_anon.go b/internal/lsp/testdata/fillstruct/fill_struct_anon.go index b5d2337fd9d..2c099a80ea7 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_anon.go +++ b/internal/lsp/testdata/fillstruct/fill_struct_anon.go @@ -10,5 +10,5 @@ type StructAnon struct { } func fill() { - _ := StructAnon{} //@suggestedfix("}", "refactor.rewrite") + _ := StructAnon{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden b/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden index eb6ffd66136..7cc9ac23d02 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden +++ b/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden @@ -15,6 +15,6 @@ func fill() { a: struct{}{}, b: map[string]interface{}{}, c: map[string]struct{d int; e bool}{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_nested.go b/internal/lsp/testdata/fillstruct/fill_struct_nested.go index 79eb84b7478..ab7be5a7b58 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_nested.go +++ b/internal/lsp/testdata/fillstruct/fill_struct_nested.go @@ -10,6 +10,6 @@ type StructC struct { func nested() { c := StructB{ - StructC: StructC{}, //@suggestedfix("}", "refactor.rewrite") + StructC: StructC{}, //@suggestedfix("}", "refactor.rewrite", "Fill") } } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden b/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden index 30061a5d72a..c902ee7f12b 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden +++ b/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden @@ -13,7 +13,7 @@ func nested() { c := StructB{ StructC: StructC{ unexportedInt: 0, - }, //@suggestedfix("}", "refactor.rewrite") + }, //@suggestedfix("}", "refactor.rewrite", "Fill") } } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_package.go b/internal/lsp/testdata/fillstruct/fill_struct_package.go index 71f124858b3..edb88c48675 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_package.go +++ b/internal/lsp/testdata/fillstruct/fill_struct_package.go @@ -7,6 +7,6 @@ import ( ) func unexported() { - a := data.B{} //@suggestedfix("}", "refactor.rewrite") - _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite") + a := data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") + _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden b/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden index 13c85702527..57b261329dc 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden +++ b/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden @@ -10,8 +10,8 @@ import ( func unexported() { a := data.B{ ExportedInt: 0, - } //@suggestedfix("}", "refactor.rewrite") - _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") + _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_fill_struct_package_11_16 -- @@ -24,13 +24,13 @@ import ( ) func unexported() { - a := data.B{} //@suggestedfix("}", "refactor.rewrite") + a := data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") _ = h2.Client{ Transport: nil, CheckRedirect: func(req *h2.Request, via []*h2.Request) error { }, Jar: nil, Timeout: 0, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_partial.go b/internal/lsp/testdata/fillstruct/fill_struct_partial.go index 97b517dcdc3..5de1722c783 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_partial.go +++ b/internal/lsp/testdata/fillstruct/fill_struct_partial.go @@ -14,11 +14,11 @@ type StructPartialB struct { func fill() { a := StructPartialA{ PrefilledInt: 5, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") b := StructPartialB{ /* this comment should disappear */ PrefilledInt: 7, // This comment should be blown away. /* As should this one */ - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden b/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden index 2d063c14d39..3aa437a0334 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden +++ b/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden @@ -17,13 +17,13 @@ func fill() { PrefilledInt: 5, UnfilledInt: 0, StructPartialB: StructPartialB{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") b := StructPartialB{ /* this comment should disappear */ PrefilledInt: 7, // This comment should be blown away. /* As should this one */ - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_fill_struct_partial_23_2 -- @@ -43,10 +43,10 @@ type StructPartialB struct { func fill() { a := StructPartialA{ PrefilledInt: 5, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") b := StructPartialB{ PrefilledInt: 7, UnfilledInt: 0, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go b/internal/lsp/testdata/fillstruct/fill_struct_spaces.go index d5d1bbba5c3..6a468cd544c 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go +++ b/internal/lsp/testdata/fillstruct/fill_struct_spaces.go @@ -5,5 +5,5 @@ type StructD struct { } func spaces() { - d := StructD{} //@suggestedfix("}", "refactor.rewrite") + d := StructD{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden b/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden index 0d755334c99..590c91611d0 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden +++ b/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden @@ -8,6 +8,6 @@ type StructD struct { func spaces() { d := StructD{ ExportedIntField: 0, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go b/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go index 50877e9005c..f5e42a4f2fe 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go +++ b/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go @@ -8,5 +8,5 @@ type unsafeStruct struct { } func fill() { - _ := unsafeStruct{} //@suggestedfix("}", "refactor.rewrite") + _ := unsafeStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden b/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden index 99369544373..7e8e1952f86 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden +++ b/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden @@ -12,6 +12,6 @@ func fill() { _ := unsafeStruct{ x: 0, p: nil, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/typeparams.go b/internal/lsp/testdata/fillstruct/typeparams.go new file mode 100644 index 00000000000..c60cd68ada5 --- /dev/null +++ b/internal/lsp/testdata/fillstruct/typeparams.go @@ -0,0 +1,38 @@ +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} + +func _[T any]() { + type S struct{ t T } + x := S{} + _ = x +} diff --git a/internal/lsp/testdata/fillstruct/typeparams.go.golden b/internal/lsp/testdata/fillstruct/typeparams.go.golden new file mode 100644 index 00000000000..9b2b90c12ee --- /dev/null +++ b/internal/lsp/testdata/fillstruct/typeparams.go.golden @@ -0,0 +1,328 @@ +-- suggestedfix_typeparams_11_40 -- +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{ + foo: 0, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} + +func _[T any]() { + type S struct{ t T } + x := S{} + var _ = basicStructWithTypeParams[T]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + _ = x +} + +-- suggestedfix_typeparams_14_40 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{ + foo: 0, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} + +func _[T any]() { + type S struct{ t T } + x := S{} + _ = x +} + +-- suggestedfix_typeparams_18_49 -- +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{ + foo: "", + bar: 0, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} + +func _[T any]() { + type S struct{ t T } + x := S{} + var _ = basicStructWithTypeParams[T]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + _ = x +} + +-- suggestedfix_typeparams_20_49 -- +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = basicStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{ + foo: "", + bar: 0, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} + +func _[T any]() { + type S struct{ t T } + x := S{} + _ = x +} + +-- suggestedfix_typeparams_21_49 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{ + foo: "", + bar: 0, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} + +func _[T any]() { + type S struct{ t T } + x := S{} + _ = x +} + +-- suggestedfix_typeparams_22_1 -- +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + foo: 0, + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} + +func _[T any]() { + type S struct{ t T } + x := S{} + var _ = basicStructWithTypeParams[T]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + _ = x +} + +-- suggestedfix_typeparams_24_1 -- +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = basicStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + foo: 0, + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} + +func _[T any]() { + type S struct{ t T } + x := S{} + _ = x +} + +-- suggestedfix_typeparams_25_1 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + foo: 0, + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} + +func _[T any]() { + type S struct{ t T } + x := S{} + _ = x +} + diff --git a/internal/lsp/testdata/missingfunction/channels.go b/internal/lsp/testdata/missingfunction/channels.go index 436491c1949..303770cd7aa 100644 --- a/internal/lsp/testdata/missingfunction/channels.go +++ b/internal/lsp/testdata/missingfunction/channels.go @@ -1,7 +1,7 @@ package missingfunction func channels(s string) { - undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix") + undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix", "") } func c() (<-chan string, chan string) { diff --git a/internal/lsp/testdata/missingfunction/channels.go.golden b/internal/lsp/testdata/missingfunction/channels.go.golden index f5078fed17a..998ce589e1d 100644 --- a/internal/lsp/testdata/missingfunction/channels.go.golden +++ b/internal/lsp/testdata/missingfunction/channels.go.golden @@ -2,7 +2,7 @@ package missingfunction func channels(s string) { - undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix") + undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix", "") } func undefinedChannels(ch1 <-chan string, ch2 chan string) { diff --git a/internal/lsp/testdata/missingfunction/consecutive_params.go b/internal/lsp/testdata/missingfunction/consecutive_params.go index d2ec3be3232..f2fb3c04132 100644 --- a/internal/lsp/testdata/missingfunction/consecutive_params.go +++ b/internal/lsp/testdata/missingfunction/consecutive_params.go @@ -2,5 +2,5 @@ package missingfunction func consecutiveParams() { var s string - undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix") + undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/consecutive_params.go.golden b/internal/lsp/testdata/missingfunction/consecutive_params.go.golden index 14a766496fb..4b852ce141b 100644 --- a/internal/lsp/testdata/missingfunction/consecutive_params.go.golden +++ b/internal/lsp/testdata/missingfunction/consecutive_params.go.golden @@ -3,7 +3,7 @@ package missingfunction func consecutiveParams() { var s string - undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix") + undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix", "") } func undefinedConsecutiveParams(s1, s2 string) { diff --git a/internal/lsp/testdata/missingfunction/error_param.go b/internal/lsp/testdata/missingfunction/error_param.go index 9fd943ffb6d..d0484f0ff56 100644 --- a/internal/lsp/testdata/missingfunction/error_param.go +++ b/internal/lsp/testdata/missingfunction/error_param.go @@ -2,5 +2,5 @@ package missingfunction func errorParam() { var err error - undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix") + undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/error_param.go.golden b/internal/lsp/testdata/missingfunction/error_param.go.golden index 2e12711817d..de78646a5f1 100644 --- a/internal/lsp/testdata/missingfunction/error_param.go.golden +++ b/internal/lsp/testdata/missingfunction/error_param.go.golden @@ -3,7 +3,7 @@ package missingfunction func errorParam() { var err error - undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix") + undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix", "") } func undefinedErrorParam(err error) { diff --git a/internal/lsp/testdata/missingfunction/literals.go b/internal/lsp/testdata/missingfunction/literals.go index e276eae79ec..0099b1a08ad 100644 --- a/internal/lsp/testdata/missingfunction/literals.go +++ b/internal/lsp/testdata/missingfunction/literals.go @@ -3,5 +3,5 @@ package missingfunction type T struct{} func literals() { - undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix") + undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/literals.go.golden b/internal/lsp/testdata/missingfunction/literals.go.golden index 04782b9bf50..599f020a75b 100644 --- a/internal/lsp/testdata/missingfunction/literals.go.golden +++ b/internal/lsp/testdata/missingfunction/literals.go.golden @@ -8,7 +8,7 @@ package missingfunction type T struct{} func literals() { - undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix") + undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix", "") } func undefinedLiterals(s string, t1 T, t2 *T) { @@ -20,7 +20,7 @@ package missingfunction type T struct{} func literals() { - undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix") + undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix", "") } func undefinedLiterals(s string, t1 T, t2 *T) { diff --git a/internal/lsp/testdata/missingfunction/operation.go b/internal/lsp/testdata/missingfunction/operation.go index 0408219fe37..a4913ec10b2 100644 --- a/internal/lsp/testdata/missingfunction/operation.go +++ b/internal/lsp/testdata/missingfunction/operation.go @@ -3,5 +3,5 @@ package missingfunction import "time" func operation() { - undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix") + undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/operation.go.golden b/internal/lsp/testdata/missingfunction/operation.go.golden index 5e35f300534..fce69b11d85 100644 --- a/internal/lsp/testdata/missingfunction/operation.go.golden +++ b/internal/lsp/testdata/missingfunction/operation.go.golden @@ -8,7 +8,7 @@ package missingfunction import "time" func operation() { - undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix") + undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix", "") } func undefinedOperation(duration time.Duration) { @@ -20,7 +20,7 @@ package missingfunction import "time" func operation() { - undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix") + undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix", "") } func undefinedOperation(duration time.Duration) { diff --git a/internal/lsp/testdata/missingfunction/selector.go b/internal/lsp/testdata/missingfunction/selector.go index afd1ab61f3a..93a04027138 100644 --- a/internal/lsp/testdata/missingfunction/selector.go +++ b/internal/lsp/testdata/missingfunction/selector.go @@ -2,5 +2,5 @@ package missingfunction func selector() { m := map[int]bool{} - undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix") + undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/selector.go.golden b/internal/lsp/testdata/missingfunction/selector.go.golden index c48691c4ed5..44e2dde3aa7 100644 --- a/internal/lsp/testdata/missingfunction/selector.go.golden +++ b/internal/lsp/testdata/missingfunction/selector.go.golden @@ -3,7 +3,7 @@ package missingfunction func selector() { m := map[int]bool{} - undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix") + undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix", "") } func undefinedSelector(b bool) { diff --git a/internal/lsp/testdata/missingfunction/slice.go b/internal/lsp/testdata/missingfunction/slice.go index 4a562a2e762..48b1a52b3f3 100644 --- a/internal/lsp/testdata/missingfunction/slice.go +++ b/internal/lsp/testdata/missingfunction/slice.go @@ -1,5 +1,5 @@ package missingfunction func slice() { - undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix") + undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/slice.go.golden b/internal/lsp/testdata/missingfunction/slice.go.golden index 0ccb8611b6c..2a05d9a0f54 100644 --- a/internal/lsp/testdata/missingfunction/slice.go.golden +++ b/internal/lsp/testdata/missingfunction/slice.go.golden @@ -2,7 +2,7 @@ package missingfunction func slice() { - undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix") + undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix", "") } func undefinedSlice(i []int) { diff --git a/internal/lsp/testdata/missingfunction/tuple.go b/internal/lsp/testdata/missingfunction/tuple.go index 1c4782c15dd..4059ced983a 100644 --- a/internal/lsp/testdata/missingfunction/tuple.go +++ b/internal/lsp/testdata/missingfunction/tuple.go @@ -1,7 +1,7 @@ package missingfunction func tuple() { - undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix") + undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix", "") } func b() (string, error) { diff --git a/internal/lsp/testdata/missingfunction/tuple.go.golden b/internal/lsp/testdata/missingfunction/tuple.go.golden index 1e12bb70860..e1118a3f348 100644 --- a/internal/lsp/testdata/missingfunction/tuple.go.golden +++ b/internal/lsp/testdata/missingfunction/tuple.go.golden @@ -2,7 +2,7 @@ package missingfunction func tuple() { - undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix") + undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix", "") } func undefinedTuple(s string, err error) { diff --git a/internal/lsp/testdata/missingfunction/unique_params.go b/internal/lsp/testdata/missingfunction/unique_params.go index ffaba3f9cb9..00479bf7554 100644 --- a/internal/lsp/testdata/missingfunction/unique_params.go +++ b/internal/lsp/testdata/missingfunction/unique_params.go @@ -3,5 +3,5 @@ package missingfunction func uniqueArguments() { var s string var i int - undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix") + undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/unique_params.go.golden b/internal/lsp/testdata/missingfunction/unique_params.go.golden index 74fb91a8eb2..4797b3ba784 100644 --- a/internal/lsp/testdata/missingfunction/unique_params.go.golden +++ b/internal/lsp/testdata/missingfunction/unique_params.go.golden @@ -8,7 +8,7 @@ package missingfunction func uniqueArguments() { var s string var i int - undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix") + undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix", "") } func undefinedUniqueArguments(s1 string, i int, s2 string) { @@ -21,7 +21,7 @@ package missingfunction func uniqueArguments() { var s string var i int - undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix") + undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix", "") } func undefinedUniqueArguments(s1 string, i int, s2 string) { diff --git a/internal/lsp/testdata/stub/stub_add_selector.go b/internal/lsp/testdata/stub/stub_add_selector.go index a15afd7c244..4037b7ad3a0 100644 --- a/internal/lsp/testdata/stub/stub_add_selector.go +++ b/internal/lsp/testdata/stub/stub_add_selector.go @@ -7,6 +7,6 @@ import "io" // then our implementation must add the import/package selector // in the concrete method if the concrete type is outside of the interface // package -var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "refactor.rewrite") +var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "refactor.rewrite", "") type readerFrom struct{} diff --git a/internal/lsp/testdata/stub/stub_add_selector.go.golden b/internal/lsp/testdata/stub/stub_add_selector.go.golden index e885483eaaf..8f08ca1efe2 100644 --- a/internal/lsp/testdata/stub/stub_add_selector.go.golden +++ b/internal/lsp/testdata/stub/stub_add_selector.go.golden @@ -8,7 +8,7 @@ import "io" // then our implementation must add the import/package selector // in the concrete method if the concrete type is outside of the interface // package -var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "refactor.rewrite") +var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "refactor.rewrite", "") type readerFrom struct{} diff --git a/internal/lsp/testdata/stub/stub_assign.go b/internal/lsp/testdata/stub/stub_assign.go index 9336361d009..d3f09313f25 100644 --- a/internal/lsp/testdata/stub/stub_assign.go +++ b/internal/lsp/testdata/stub/stub_assign.go @@ -4,7 +4,7 @@ import "io" func main() { var br io.ByteWriter - br = &byteWriter{} //@suggestedfix("&", "refactor.rewrite") + br = &byteWriter{} //@suggestedfix("&", "refactor.rewrite", "") } type byteWriter struct{} diff --git a/internal/lsp/testdata/stub/stub_assign.go.golden b/internal/lsp/testdata/stub/stub_assign.go.golden index a52a8236798..f1535424114 100644 --- a/internal/lsp/testdata/stub/stub_assign.go.golden +++ b/internal/lsp/testdata/stub/stub_assign.go.golden @@ -5,7 +5,7 @@ import "io" func main() { var br io.ByteWriter - br = &byteWriter{} //@suggestedfix("&", "refactor.rewrite") + br = &byteWriter{} //@suggestedfix("&", "refactor.rewrite", "") } type byteWriter struct{} diff --git a/internal/lsp/testdata/stub/stub_assign_multivars.go b/internal/lsp/testdata/stub/stub_assign_multivars.go index 01b330fda54..bd36d6833d1 100644 --- a/internal/lsp/testdata/stub/stub_assign_multivars.go +++ b/internal/lsp/testdata/stub/stub_assign_multivars.go @@ -5,7 +5,7 @@ import "io" func main() { var br io.ByteWriter var i int - i, br = 1, &multiByteWriter{} //@suggestedfix("&", "refactor.rewrite") + i, br = 1, &multiByteWriter{} //@suggestedfix("&", "refactor.rewrite", "") } type multiByteWriter struct{} diff --git a/internal/lsp/testdata/stub/stub_assign_multivars.go.golden b/internal/lsp/testdata/stub/stub_assign_multivars.go.golden index e1e71adbd50..425d11746a5 100644 --- a/internal/lsp/testdata/stub/stub_assign_multivars.go.golden +++ b/internal/lsp/testdata/stub/stub_assign_multivars.go.golden @@ -6,7 +6,7 @@ import "io" func main() { var br io.ByteWriter var i int - i, br = 1, &multiByteWriter{} //@suggestedfix("&", "refactor.rewrite") + i, br = 1, &multiByteWriter{} //@suggestedfix("&", "refactor.rewrite", "") } type multiByteWriter struct{} diff --git a/internal/lsp/testdata/stub/stub_call_expr.go b/internal/lsp/testdata/stub/stub_call_expr.go index 775b0e5545e..0c309466524 100644 --- a/internal/lsp/testdata/stub/stub_call_expr.go +++ b/internal/lsp/testdata/stub/stub_call_expr.go @@ -1,7 +1,7 @@ package stub func main() { - check(&callExpr{}) //@suggestedfix("&", "refactor.rewrite") + check(&callExpr{}) //@suggestedfix("&", "refactor.rewrite", "") } func check(err error) { diff --git a/internal/lsp/testdata/stub/stub_call_expr.go.golden b/internal/lsp/testdata/stub/stub_call_expr.go.golden index 2d12f8651f3..c82d22440f1 100644 --- a/internal/lsp/testdata/stub/stub_call_expr.go.golden +++ b/internal/lsp/testdata/stub/stub_call_expr.go.golden @@ -2,7 +2,7 @@ package stub func main() { - check(&callExpr{}) //@suggestedfix("&", "refactor.rewrite") + check(&callExpr{}) //@suggestedfix("&", "refactor.rewrite", "") } func check(err error) { diff --git a/internal/lsp/testdata/stub/stub_embedded.go b/internal/lsp/testdata/stub/stub_embedded.go index 6d6a986bf24..f66989e9f0f 100644 --- a/internal/lsp/testdata/stub/stub_embedded.go +++ b/internal/lsp/testdata/stub/stub_embedded.go @@ -5,7 +5,7 @@ import ( "sort" ) -var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "refactor.rewrite") +var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "refactor.rewrite", "") type embeddedConcrete struct{} diff --git a/internal/lsp/testdata/stub/stub_embedded.go.golden b/internal/lsp/testdata/stub/stub_embedded.go.golden index c258ebaf46c..3c5347e8c01 100644 --- a/internal/lsp/testdata/stub/stub_embedded.go.golden +++ b/internal/lsp/testdata/stub/stub_embedded.go.golden @@ -6,7 +6,7 @@ import ( "sort" ) -var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "refactor.rewrite") +var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "refactor.rewrite", "") type embeddedConcrete struct{} diff --git a/internal/lsp/testdata/stub/stub_err.go b/internal/lsp/testdata/stub/stub_err.go index 908c7d3152f..121f0e794d7 100644 --- a/internal/lsp/testdata/stub/stub_err.go +++ b/internal/lsp/testdata/stub/stub_err.go @@ -1,7 +1,7 @@ package stub func main() { - var br error = &customErr{} //@suggestedfix("&", "refactor.rewrite") + var br error = &customErr{} //@suggestedfix("&", "refactor.rewrite", "") } type customErr struct{} diff --git a/internal/lsp/testdata/stub/stub_err.go.golden b/internal/lsp/testdata/stub/stub_err.go.golden index 717aed86293..0b441bdaab1 100644 --- a/internal/lsp/testdata/stub/stub_err.go.golden +++ b/internal/lsp/testdata/stub/stub_err.go.golden @@ -2,7 +2,7 @@ package stub func main() { - var br error = &customErr{} //@suggestedfix("&", "refactor.rewrite") + var br error = &customErr{} //@suggestedfix("&", "refactor.rewrite", "") } type customErr struct{} diff --git a/internal/lsp/testdata/stub/stub_function_return.go b/internal/lsp/testdata/stub/stub_function_return.go index bbf05885af2..41f17645e9c 100644 --- a/internal/lsp/testdata/stub/stub_function_return.go +++ b/internal/lsp/testdata/stub/stub_function_return.go @@ -5,7 +5,7 @@ import ( ) func newCloser() io.Closer { - return closer{} //@suggestedfix("c", "refactor.rewrite") + return closer{} //@suggestedfix("c", "refactor.rewrite", "") } type closer struct{} diff --git a/internal/lsp/testdata/stub/stub_function_return.go.golden b/internal/lsp/testdata/stub/stub_function_return.go.golden index f80874d2b94..e90712e6973 100644 --- a/internal/lsp/testdata/stub/stub_function_return.go.golden +++ b/internal/lsp/testdata/stub/stub_function_return.go.golden @@ -6,7 +6,7 @@ import ( ) func newCloser() io.Closer { - return closer{} //@suggestedfix("c", "refactor.rewrite") + return closer{} //@suggestedfix("c", "refactor.rewrite", "") } type closer struct{} diff --git a/internal/lsp/testdata/stub/stub_generic_receiver.go b/internal/lsp/testdata/stub/stub_generic_receiver.go index 64e90fcf6a7..1c00569ea1c 100644 --- a/internal/lsp/testdata/stub/stub_generic_receiver.go +++ b/internal/lsp/testdata/stub/stub_generic_receiver.go @@ -7,7 +7,7 @@ import "io" // This file tests that that the stub method generator accounts for concrete // types that have type parameters defined. -var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite") +var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite", "Implement io.ReaderFrom") type genReader[T, Y any] struct { T T diff --git a/internal/lsp/testdata/stub/stub_generic_receiver.go.golden b/internal/lsp/testdata/stub/stub_generic_receiver.go.golden index 1fc7157b463..97935d47eb3 100644 --- a/internal/lsp/testdata/stub/stub_generic_receiver.go.golden +++ b/internal/lsp/testdata/stub/stub_generic_receiver.go.golden @@ -8,7 +8,7 @@ import "io" // This file tests that that the stub method generator accounts for concrete // types that have type parameters defined. -var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite") +var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite", "Implement io.ReaderFrom") type genReader[T, Y any] struct { T T diff --git a/internal/lsp/testdata/stub/stub_ignored_imports.go b/internal/lsp/testdata/stub/stub_ignored_imports.go index 8f6ec73de1b..ca95d2a7120 100644 --- a/internal/lsp/testdata/stub/stub_ignored_imports.go +++ b/internal/lsp/testdata/stub/stub_ignored_imports.go @@ -12,7 +12,7 @@ import ( var ( _ Reader - _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "refactor.rewrite") + _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "refactor.rewrite", "") ) type ignoredResetter struct{} diff --git a/internal/lsp/testdata/stub/stub_ignored_imports.go.golden b/internal/lsp/testdata/stub/stub_ignored_imports.go.golden index a0ddc179353..33aba532662 100644 --- a/internal/lsp/testdata/stub/stub_ignored_imports.go.golden +++ b/internal/lsp/testdata/stub/stub_ignored_imports.go.golden @@ -14,7 +14,7 @@ import ( var ( _ Reader - _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "refactor.rewrite") + _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "refactor.rewrite", "") ) type ignoredResetter struct{} diff --git a/internal/lsp/testdata/stub/stub_multi_var.go b/internal/lsp/testdata/stub/stub_multi_var.go index 4276b799429..06702b22204 100644 --- a/internal/lsp/testdata/stub/stub_multi_var.go +++ b/internal/lsp/testdata/stub/stub_multi_var.go @@ -6,6 +6,6 @@ import "io" // has multiple values on the same line can still be // analyzed correctly to target the interface implementation // diagnostic. -var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "refactor.rewrite") +var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "refactor.rewrite", "") type multiVar struct{} diff --git a/internal/lsp/testdata/stub/stub_multi_var.go.golden b/internal/lsp/testdata/stub/stub_multi_var.go.golden index b9ac4236766..804c7eec65c 100644 --- a/internal/lsp/testdata/stub/stub_multi_var.go.golden +++ b/internal/lsp/testdata/stub/stub_multi_var.go.golden @@ -7,7 +7,7 @@ import "io" // has multiple values on the same line can still be // analyzed correctly to target the interface implementation // diagnostic. -var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "refactor.rewrite") +var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "refactor.rewrite", "") type multiVar struct{} diff --git a/internal/lsp/testdata/stub/stub_pointer.go b/internal/lsp/testdata/stub/stub_pointer.go index 2b3681b8357..e9d8bc688fc 100644 --- a/internal/lsp/testdata/stub/stub_pointer.go +++ b/internal/lsp/testdata/stub/stub_pointer.go @@ -3,7 +3,7 @@ package stub import "io" func getReaderFrom() io.ReaderFrom { - return &pointerImpl{} //@suggestedfix("&", "refactor.rewrite") + return &pointerImpl{} //@suggestedfix("&", "refactor.rewrite", "") } type pointerImpl struct{} diff --git a/internal/lsp/testdata/stub/stub_pointer.go.golden b/internal/lsp/testdata/stub/stub_pointer.go.golden index c4133d7a44d..a4d765dd457 100644 --- a/internal/lsp/testdata/stub/stub_pointer.go.golden +++ b/internal/lsp/testdata/stub/stub_pointer.go.golden @@ -4,7 +4,7 @@ package stub import "io" func getReaderFrom() io.ReaderFrom { - return &pointerImpl{} //@suggestedfix("&", "refactor.rewrite") + return &pointerImpl{} //@suggestedfix("&", "refactor.rewrite", "") } type pointerImpl struct{} diff --git a/internal/lsp/testdata/stub/stub_renamed_import.go b/internal/lsp/testdata/stub/stub_renamed_import.go index eaebe251018..54dd598013d 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import.go +++ b/internal/lsp/testdata/stub/stub_renamed_import.go @@ -5,7 +5,7 @@ import ( myio "io" ) -var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "refactor.rewrite") +var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "refactor.rewrite", "") var _ myio.Reader type myIO struct{} diff --git a/internal/lsp/testdata/stub/stub_renamed_import.go.golden b/internal/lsp/testdata/stub/stub_renamed_import.go.golden index 48ff4f1537f..8182d2b3675 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import.go.golden +++ b/internal/lsp/testdata/stub/stub_renamed_import.go.golden @@ -6,7 +6,7 @@ import ( myio "io" ) -var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "refactor.rewrite") +var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "refactor.rewrite", "") var _ myio.Reader type myIO struct{} diff --git a/internal/lsp/testdata/stub/stub_renamed_import_iface.go b/internal/lsp/testdata/stub/stub_renamed_import_iface.go index 96caf540d60..26142d0a893 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import_iface.go +++ b/internal/lsp/testdata/stub/stub_renamed_import_iface.go @@ -8,6 +8,6 @@ import ( // method references an import from its own package // that the concrete type does not yet import, and that import happens // to be renamed, then we prefer the renaming of the interface. -var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "refactor.rewrite") +var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "refactor.rewrite", "") type otherInterfaceImpl struct{} diff --git a/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden b/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden index 9ba2cb440e8..134c24bf88a 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden +++ b/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden @@ -11,7 +11,7 @@ import ( // method references an import from its own package // that the concrete type does not yet import, and that import happens // to be renamed, then we prefer the renaming of the interface. -var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "refactor.rewrite") +var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "refactor.rewrite", "") type otherInterfaceImpl struct{} diff --git a/internal/lsp/testdata/stub/stub_stdlib.go b/internal/lsp/testdata/stub/stub_stdlib.go index 0d54a6daadf..463cf78a344 100644 --- a/internal/lsp/testdata/stub/stub_stdlib.go +++ b/internal/lsp/testdata/stub/stub_stdlib.go @@ -4,6 +4,6 @@ import ( "io" ) -var _ io.Writer = writer{} //@suggestedfix("w", "refactor.rewrite") +var _ io.Writer = writer{} //@suggestedfix("w", "refactor.rewrite", "") type writer struct{} diff --git a/internal/lsp/testdata/stub/stub_stdlib.go.golden b/internal/lsp/testdata/stub/stub_stdlib.go.golden index 8636cead414..55592501a07 100644 --- a/internal/lsp/testdata/stub/stub_stdlib.go.golden +++ b/internal/lsp/testdata/stub/stub_stdlib.go.golden @@ -5,7 +5,7 @@ import ( "io" ) -var _ io.Writer = writer{} //@suggestedfix("w", "refactor.rewrite") +var _ io.Writer = writer{} //@suggestedfix("w", "refactor.rewrite", "") type writer struct{} diff --git a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go b/internal/lsp/testdata/suggestedfix/has_suggested_fix.go index e06dce0a846..7ff524479b4 100644 --- a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go +++ b/internal/lsp/testdata/suggestedfix/has_suggested_fix.go @@ -6,6 +6,6 @@ import ( func goodbye() { s := "hiiiiiii" - s = s //@suggestedfix("s = s", "quickfix") + s = s //@suggestedfix("s = s", "quickfix", "") log.Print(s) } diff --git a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden b/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden index 9ccaa199468..e7e84fc227d 100644 --- a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden +++ b/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden @@ -7,7 +7,7 @@ import ( func goodbye() { s := "hiiiiiii" - //@suggestedfix("s = s", "quickfix") + //@suggestedfix("s = s", "quickfix", "") log.Print(s) } diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 9fadf634090..668d5fb82e5 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -13,7 +13,7 @@ FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 SemanticTokenCount = 3 -SuggestedFixCount = 64 +SuggestedFixCount = 67 FunctionExtractionCount = 25 MethodExtractionCount = 6 DefinitionsCount = 108 diff --git a/internal/lsp/testdata/typeerrors/noresultvalues.go b/internal/lsp/testdata/typeerrors/noresultvalues.go index 84234c4b93a..729e7bbccd4 100644 --- a/internal/lsp/testdata/typeerrors/noresultvalues.go +++ b/internal/lsp/testdata/typeerrors/noresultvalues.go @@ -1,5 +1,5 @@ package typeerrors -func x() { return nil } //@suggestedfix("nil", "quickfix") +func x() { return nil } //@suggestedfix("nil", "quickfix", "") -func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix") +func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix", "") diff --git a/internal/lsp/testdata/typeerrors/noresultvalues.go.golden b/internal/lsp/testdata/typeerrors/noresultvalues.go.golden index 07c54d44553..48409a0b7dd 100644 --- a/internal/lsp/testdata/typeerrors/noresultvalues.go.golden +++ b/internal/lsp/testdata/typeerrors/noresultvalues.go.golden @@ -1,14 +1,14 @@ -- suggestedfix_noresultvalues_3_19 -- package typeerrors -func x() { return } //@suggestedfix("nil", "quickfix") +func x() { return } //@suggestedfix("nil", "quickfix", "") -func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix") +func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix", "") -- suggestedfix_noresultvalues_5_19 -- package typeerrors -func x() { return nil } //@suggestedfix("nil", "quickfix") +func x() { return nil } //@suggestedfix("nil", "quickfix", "") -func y() { return } //@suggestedfix("nil", "quickfix") +func y() { return } //@suggestedfix("nil", "quickfix", "") diff --git a/internal/lsp/testdata/undeclared/var.go b/internal/lsp/testdata/undeclared/var.go index b5f9287d48d..e27a733d942 100644 --- a/internal/lsp/testdata/undeclared/var.go +++ b/internal/lsp/testdata/undeclared/var.go @@ -1,13 +1,13 @@ package undeclared func m() int { - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix") + z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix", "") if 100 < 90 { z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix") + } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix", "") z = 4 } - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix") + for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix", "") } r() //@diag("r", "compiler", "undeclared name: r", "error") return z diff --git a/internal/lsp/testdata/undeclared/var.go.golden b/internal/lsp/testdata/undeclared/var.go.golden index 74adbe8ffde..a266df7c0c7 100644 --- a/internal/lsp/testdata/undeclared/var.go.golden +++ b/internal/lsp/testdata/undeclared/var.go.golden @@ -2,14 +2,14 @@ package undeclared func m() int { - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix") + z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix", "") if 100 < 90 { z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix") + } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix", "") z = 4 } i := - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix") + for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix", "") } r() //@diag("r", "compiler", "undeclared name: r", "error") return z @@ -20,13 +20,13 @@ package undeclared func m() int { y := - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix") + z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix", "") if 100 < 90 { z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix") + } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix", "") z = 4 } - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix") + for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix", "") } r() //@diag("r", "compiler", "undeclared name: r", "error") return z @@ -36,14 +36,14 @@ func m() int { package undeclared func m() int { - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix") + z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix", "") n := if 100 < 90 { z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix") + } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix", "") z = 4 } - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix") + for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix", "") } r() //@diag("r", "compiler", "undeclared name: r", "error") return z diff --git a/internal/lsp/tests/README.md b/internal/lsp/tests/README.md index 2c18675f7e5..64ced79702e 100644 --- a/internal/lsp/tests/README.md +++ b/internal/lsp/tests/README.md @@ -11,7 +11,7 @@ file, like `internal/lsp/testdata/foo/bar.go.golden`. The former is the "input" and the latter is the expected output. Each input file contains annotations like -`//@suggestedfix("}", "refactor.rewrite")`. These annotations are interpreted by +`//@suggestedfix("}", "refactor.rewrite", "Fill anonymous struct")`. These annotations are interpreted by test runners to perform certain actions. The expected output after those actions is encoded in the golden file. diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index b60fbf03866..4c3201ab8bc 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -69,7 +69,7 @@ type FoldingRanges []span.Span type Formats []span.Span type Imports []span.Span type SemanticTokens []span.Span -type SuggestedFixes map[span.Span][]string +type SuggestedFixes map[span.Span][]SuggestedFix type FunctionExtractions map[span.Span]span.Span type MethodExtractions map[span.Span]span.Span type Definitions map[span.Span]Definition @@ -152,7 +152,7 @@ type Tests interface { Format(*testing.T, span.Span) Import(*testing.T, span.Span) SemanticTokens(*testing.T, span.Span) - SuggestedFix(*testing.T, span.Span, []string, int) + SuggestedFix(*testing.T, span.Span, []SuggestedFix, int) FunctionExtraction(*testing.T, span.Span, span.Span) MethodExtraction(*testing.T, span.Span, span.Span) Definition(*testing.T, span.Span, Definition) @@ -232,6 +232,10 @@ type Link struct { NotePosition token.Position } +type SuggestedFix struct { + ActionKind, Title string +} + type Golden struct { Filename string Archive *txtar.Archive @@ -1198,11 +1202,8 @@ func (data *Data) collectSemanticTokens(spn span.Span) { data.SemanticTokens = append(data.SemanticTokens, spn) } -func (data *Data) collectSuggestedFixes(spn span.Span, actionKind string) { - if _, ok := data.SuggestedFixes[spn]; !ok { - data.SuggestedFixes[spn] = []string{} - } - data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], actionKind) +func (data *Data) collectSuggestedFixes(spn span.Span, actionKind, fix string) { + data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], SuggestedFix{actionKind, fix}) } func (data *Data) collectFunctionExtractions(start span.Span, end span.Span) { From 005c07ac5af6b5cabf2a1cb1ab49993c5b3bcb4d Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 30 Jun 2022 15:41:43 -0400 Subject: [PATCH 126/723] gopls/internal/vulncheck: adjust logging Report # of findings Change-Id: Ib10d18a23280a8644b9c08a8d51d6e288ae761db Reviewed-on: https://go-review.googlesource.com/c/tools/+/415496 Reviewed-by: Suzy Mueller --- gopls/internal/vulncheck/command.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index a29bc008c7a..8c88cf3d51b 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -80,7 +80,6 @@ func (c *cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) log.Printf("package load failed: %v", err) return nil, err } - log.Printf("loaded %d packages\n", len(loadedPkgs)) log.Printf("analyzing %d packages...\n", len(loadedPkgs)) @@ -88,9 +87,12 @@ func (c *cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) if err != nil { return nil, err } + + log.Printf("selecting affecting vulnerabilities from %d findings...\n", len(r.Vulns)) unaffectedMods := filterUnaffected(r.Vulns) r.Vulns = filterCalled(r) + log.Printf("found %d vulnerabilities.\n", len(r.Vulns)) callInfo := gvc.GetCallInfo(r, loadedPkgs) return toVulns(callInfo, unaffectedMods) // TODO: add import graphs. From 4375b29f44fb3bfb45abe6d2e1de68c34fcd9ec5 Mon Sep 17 00:00:00 2001 From: Abirdcfly Date: Thu, 21 Jul 2022 16:54:42 +0000 Subject: [PATCH 127/723] cmd/auth/cookieauth: delete unreachable os.Exit Change-Id: I5c60eeb8667423544b2bc8b9cf5f51279b0a941d GitHub-Last-Rev: cb0eca5ff34755a0185729e5c81bf08eb92fab39 GitHub-Pull-Request: golang/tools#388 Reviewed-on: https://go-review.googlesource.com/c/tools/+/418854 TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills Reviewed-by: Bryan Mills Auto-Submit: Bryan Mills gopls-CI: kokoro Reviewed-by: Cherry Mui --- cmd/auth/cookieauth/cookieauth.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/auth/cookieauth/cookieauth.go b/cmd/auth/cookieauth/cookieauth.go index feefaff0b6e..8b0ff17664b 100644 --- a/cmd/auth/cookieauth/cookieauth.go +++ b/cmd/auth/cookieauth/cookieauth.go @@ -40,7 +40,6 @@ func main() { f, err := os.Open(os.Args[1]) if err != nil { log.Fatalf("failed to read cookie file: %v\n", os.Args[1]) - os.Exit(1) } defer f.Close() From 2a6393fe54b36af368a3d319f92cd3b1efee7741 Mon Sep 17 00:00:00 2001 From: Dylan Le Date: Thu, 21 Jul 2022 14:30:30 -0400 Subject: [PATCH 128/723] internal/lsp: Refactor to share logic with rename If a package is being operated on, getPackage returns that package information; otherwise nil. Change-Id: I881056510b8d6862c274a7532fdfbc840c938468 Reviewed-on: https://go-review.googlesource.com/c/tools/+/418791 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Dylan Le Reviewed-by: Robert Findley --- internal/lsp/source/references.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/internal/lsp/source/references.go b/internal/lsp/source/references.go index a1643fbec6c..2bbdc0741ca 100644 --- a/internal/lsp/source/references.go +++ b/internal/lsp/source/references.go @@ -18,7 +18,6 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" "golang.org/x/tools/internal/span" ) @@ -32,6 +31,18 @@ type ReferenceInfo struct { isDeclaration bool } +// isInPackageName reports whether the file's package name surrounds the +// given position pp (e.g. "foo" surrounds the cursor in "package foo"). +func isInPackageName(ctx context.Context, s Snapshot, f FileHandle, pgf *ParsedGoFile, pp protocol.Position) (bool, error) { + // Find position of the package name declaration + cursorPos, err := pgf.Mapper.Pos(pp) + if err != nil { + return false, err + } + + return pgf.File.Name.Pos() <= cursorPos && cursorPos <= pgf.File.Name.End(), nil +} + // References returns a list of references for a given identifier within the packages // containing i.File. Declarations appear first in the result. func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) { @@ -44,23 +55,13 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit return nil, err } - cursorOffset, err := pgf.Mapper.Offset(pp) - if err != nil { - return nil, err - } - packageName := pgf.File.Name.Name // from package decl - packageNameStart, err := safetoken.Offset(pgf.Tok, pgf.File.Name.Pos()) - if err != nil { - return nil, err - } - - packageNameEnd, err := safetoken.Offset(pgf.Tok, pgf.File.Name.End()) + inPackageName, err := isInPackageName(ctx, s, f, pgf, pp) if err != nil { return nil, err } - if packageNameStart <= cursorOffset && cursorOffset < packageNameEnd { + if inPackageName { renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckAll, NarrowestPackage) if err != nil { return nil, err From 76004542dc1955c2c789d856b2e2ded6002412cf Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 25 Jul 2022 16:09:28 -0400 Subject: [PATCH 129/723] gopls: update dependencies Update all dependencies, except sergi/go-diff. Also tidy x/tools with -compat=1.16, as it had recently been broken at 1.16. Change-Id: I2e6c9bf48c6bedb2dff0fa418bf588dd07918866 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419494 gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim --- go.sum | 16 ++++++++++++++++ gopls/go.mod | 17 ++++++++--------- gopls/go.sum | 48 +++++++++++++++++++++++++++--------------------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/go.sum b/go.sum index efeb68a0ec2..7498b1467ed 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,28 @@ github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gopls/go.mod b/gopls/go.mod index 3e89620b7f6..006bfe23224 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -3,26 +3,25 @@ module golang.org/x/tools/gopls go 1.18 require ( - github.com/google/go-cmp v0.5.7 + github.com/google/go-cmp v0.5.8 github.com/jba/printsrc v0.2.2 github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a - golang.org/x/tools v0.1.11-0.20220523181440-ccb10502d1a5 - golang.org/x/vuln v0.0.0-20220718121659-b9a3ad919698 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f + golang.org/x/tools v0.1.11 + golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df honnef.co/go/tools v0.3.2 - mvdan.cc/gofumpt v0.3.0 + mvdan.cc/gofumpt v0.3.1 mvdan.cc/xurls/v2 v2.4.0 ) require ( - github.com/BurntSushi/toml v1.0.0 // indirect + github.com/BurntSushi/toml v1.2.0 // indirect github.com/google/safehtml v0.0.2 // indirect - golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 golang.org/x/text v0.3.7 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) replace golang.org/x/tools => ../ diff --git a/gopls/go.sum b/gopls/go.sum index 4ee977a8c89..cced7df8f56 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -1,14 +1,22 @@ -github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= -github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= +github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmdtest v0.4.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/safehtml v0.0.2 h1:ZOt2VXg4x24bW0m2jtzAOkhoXV0iM8vNKc0paByCZqM= github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/jba/printsrc v0.2.2 h1:9OHK51UT+/iMAEBlQIIXW04qvKyF3/vvLuwW/hL8tDU= @@ -34,40 +42,36 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e h1:7Xs2YCOpMlNqSQSmrrnhlzBXIE/bpMecZplbLePTJvE= +golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c h1:r5bbIROBQtRRgoutV8Q3sFY58VzHW6jMBYl48ANSyS4= -golang.org/x/vuln v0.0.0-20220613164644-4eb5ba49563c/go.mod h1:UZshlUPxXeGUM9I14UOawXQg6yosDE9cr1vKY/DzgWo= -golang.org/x/vuln v0.0.0-20220718121659-b9a3ad919698 h1:9lgpkUgjzoIcZYp7/UPFO/0jIlYcokcEjqWm0hj9pzE= -golang.org/x/vuln v0.0.0-20220718121659-b9a3ad919698/go.mod h1:UZshlUPxXeGUM9I14UOawXQg6yosDE9cr1vKY/DzgWo= +golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df h1:BkeW9/QJhcigekDUPS9N9bIb0v7gPKKmLYeczVAqr2s= +golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df/go.mod h1:UZshlUPxXeGUM9I14UOawXQg6yosDE9cr1vKY/DzgWo= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -76,10 +80,12 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= -mvdan.cc/gofumpt v0.3.0 h1:kTojdZo9AcEYbQYhGuLf/zszYthRdhDNDUi2JKTxas4= -mvdan.cc/gofumpt v0.3.0/go.mod h1:0+VyGZWleeIj5oostkOex+nDBA0eyavuDnDusAJ8ylo= +mvdan.cc/gofumpt v0.3.1 h1:avhhrOmv0IuvQVK7fvwV91oFSGAk5/6Po8GXTzICeu8= +mvdan.cc/gofumpt v0.3.1/go.mod h1:w3ymliuxvzVx8DAutBnVyDqYb1Niy/yCJt/lk821YCE= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio= +mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5/go.mod h1:b8RRCBm0eeiWR8cfN88xeq2G5SG3VKGO+5UPWi5FSOY= mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc= mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg= From 8b47d4e187e3f98d5c096495bcea329f33e99055 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 25 Jul 2022 16:11:43 -0400 Subject: [PATCH 130/723] all: update dependencies Update all x/tools dependencies. Change-Id: I0a81f9821a9267bef9057f294efc3ac1c13b59ad Reviewed-on: https://go-review.googlesource.com/c/tools/+/419495 Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- go.mod | 8 ++++---- go.sum | 18 +++++++++--------- gopls/go.sum | 7 +++---- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index f05aba2b64a..272a6d2eaa7 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module golang.org/x/tools go 1.18 require ( - github.com/yuin/goldmark v1.4.1 + github.com/yuin/goldmark v1.4.13 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a + golang.org/x/net v0.0.0-20220722155237-a158d28d115b + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f golang.org/x/text v0.3.7 ) diff --git a/go.sum b/go.sum index 7498b1467ed..f603000deb2 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,26 @@ -github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/gopls/go.sum b/gopls/go.sum index cced7df8f56..ecd3f4dd363 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -41,7 +41,7 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e h1:7Xs2YCOpMlNqSQSmrrnhlzBXIE/bpMecZplbLePTJvE= @@ -50,12 +50,11 @@ golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -64,8 +63,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df h1:BkeW9/QJhcigekDUPS9N9bIb0v7gPKKmLYeczVAqr2s= From c83f42da700064452a622c90d97093e13e8d236b Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 26 Jul 2022 12:16:16 -0400 Subject: [PATCH 131/723] internal/lsp: update inlay hints documentation to include go snippets Update the markdown documentation for inlay hints and fix a couple of typos in the examples. Change-Id: I114502a81999bc5e4f25384ab619888f3e31a731 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419496 Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro Run-TryBot: Suzy Mueller TryBot-Result: Gopher Robot --- gopls/doc/inlayHints.md | 27 ++++++++++------ internal/lsp/source/api_json.go | 28 ++++++++-------- internal/lsp/source/inlay_hint.go | 53 ++++++++----------------------- 3 files changed, 45 insertions(+), 63 deletions(-) diff --git a/gopls/doc/inlayHints.md b/gopls/doc/inlayHints.md index 15957b52ede..2ae9a2828af 100644 --- a/gopls/doc/inlayHints.md +++ b/gopls/doc/inlayHints.md @@ -6,67 +6,74 @@ This document describes the inlay hints that `gopls` uses inside the editor. ## **assignVariableTypes** Enable/disable inlay hints for variable types in assign statements: - - i/* int/*, j/* int/* := 0, len(r)-1 +```go + i/* int*/, j/* int*/ := 0, len(r)-1 +``` **Disabled by default. Enable it by setting `"hints": {"assignVariableTypes": true}`.** ## **compositeLiteralFields** Enable/disable inlay hints for composite literal field names: - - {in: "Hello, world", want: "dlrow ,olleH"} +```go + {/*in: */"Hello, world", /*want: */"dlrow ,olleH"} +``` **Disabled by default. Enable it by setting `"hints": {"compositeLiteralFields": true}`.** ## **compositeLiteralTypes** Enable/disable inlay hints for composite literal types: - +```go for _, c := range []struct { in, want string }{ /*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"}, } +``` **Disabled by default. Enable it by setting `"hints": {"compositeLiteralTypes": true}`.** ## **constantValues** Enable/disable inlay hints for constant values: - +```go const ( KindNone Kind = iota/* = 0*/ KindPrint/* = 1*/ KindPrintf/* = 2*/ KindErrorf/* = 3*/ ) +``` **Disabled by default. Enable it by setting `"hints": {"constantValues": true}`.** ## **functionTypeParameters** Enable/disable inlay hints for implicit type parameters on generic functions: - +```go myFoo/*[int, string]*/(1, "hello") +``` **Disabled by default. Enable it by setting `"hints": {"functionTypeParameters": true}`.** ## **parameterNames** Enable/disable inlay hints for parameter names: - +```go parseInt(/* str: */ "123", /* radix: */ 8) +``` **Disabled by default. Enable it by setting `"hints": {"parameterNames": true}`.** ## **rangeVariableTypes** Enable/disable inlay hints for variable types in range statements: - - for k/* int*/, v/* string/* := range []string{} { +```go + for k/* int*/, v/* string*/ := range []string{} { fmt.Println(k, v) } +``` **Disabled by default. Enable it by setting `"hints": {"rangeVariableTypes": true}`.** diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 2493da25d8c..94d0f12b4a8 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -517,37 +517,37 @@ var GeneratedAPIJSON = &APIJSON{ EnumKeys: EnumKeys{Keys: []EnumKey{ { Name: "\"assignVariableTypes\"", - Doc: "Enable/disable inlay hints for variable types in assign statements:\n\n\ti/* int/*, j/* int/* := 0, len(r)-1", + Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", Default: "false", }, { Name: "\"compositeLiteralFields\"", - Doc: "Enable/disable inlay hints for composite literal field names:\n\n\t{in: \"Hello, world\", want: \"dlrow ,olleH\"}", + Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", Default: "false", }, { Name: "\"compositeLiteralTypes\"", - Doc: "Enable/disable inlay hints for composite literal types:\n\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}", + Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", Default: "false", }, { Name: "\"constantValues\"", - Doc: "Enable/disable inlay hints for constant values:\n\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)", + Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", Default: "false", }, { Name: "\"functionTypeParameters\"", - Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n\n\tmyFoo/*[int, string]*/(1, \"hello\")", + Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", Default: "false", }, { Name: "\"parameterNames\"", - Doc: "Enable/disable inlay hints for parameter names:\n\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)", + Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", Default: "false", }, { Name: "\"rangeVariableTypes\"", - Doc: "Enable/disable inlay hints for variable types in range statements:\n\n\tfor k/* int*/, v/* string/* := range []string{} {\n\t\tfmt.Println(k, v)\n\t}", + Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", Default: "false", }, }}, @@ -1036,31 +1036,31 @@ var GeneratedAPIJSON = &APIJSON{ Hints: []*HintJSON{ { Name: "assignVariableTypes", - Doc: "Enable/disable inlay hints for variable types in assign statements:\n\n\ti/* int/*, j/* int/* := 0, len(r)-1", + Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", }, { Name: "compositeLiteralFields", - Doc: "Enable/disable inlay hints for composite literal field names:\n\n\t{in: \"Hello, world\", want: \"dlrow ,olleH\"}", + Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", }, { Name: "compositeLiteralTypes", - Doc: "Enable/disable inlay hints for composite literal types:\n\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}", + Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", }, { Name: "constantValues", - Doc: "Enable/disable inlay hints for constant values:\n\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)", + Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", }, { Name: "functionTypeParameters", - Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n\n\tmyFoo/*[int, string]*/(1, \"hello\")", + Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", }, { Name: "parameterNames", - Doc: "Enable/disable inlay hints for parameter names:\n\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)", + Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", }, { Name: "rangeVariableTypes", - Doc: "Enable/disable inlay hints for variable types in range statements:\n\n\tfor k/* int*/, v/* string/* := range []string{} {\n\t\tfmt.Println(k, v)\n\t}", + Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", }, }, } diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 967752b5c51..6ca51930a7f 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -44,63 +44,38 @@ const ( var AllInlayHints = map[string]*Hint{ AssignVariableTypes: { Name: AssignVariableTypes, - Doc: `Enable/disable inlay hints for variable types in assign statements: - - i/* int/*, j/* int/* := 0, len(r)-1`, - Run: assignVariableTypes, + Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", + Run: assignVariableTypes, }, ParameterNames: { Name: ParameterNames, - Doc: `Enable/disable inlay hints for parameter names: - - parseInt(/* str: */ "123", /* radix: */ 8)`, - Run: parameterNames, + Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", + Run: parameterNames, }, ConstantValues: { Name: ConstantValues, - Doc: `Enable/disable inlay hints for constant values: - - const ( - KindNone Kind = iota/* = 0*/ - KindPrint/* = 1*/ - KindPrintf/* = 2*/ - KindErrorf/* = 3*/ - )`, - Run: constantValues, + Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", + Run: constantValues, }, RangeVariableTypes: { Name: RangeVariableTypes, - Doc: `Enable/disable inlay hints for variable types in range statements: - - for k/* int*/, v/* string/* := range []string{} { - fmt.Println(k, v) - }`, - Run: rangeVariableTypes, + Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", + Run: rangeVariableTypes, }, CompositeLiteralTypes: { Name: CompositeLiteralTypes, - Doc: `Enable/disable inlay hints for composite literal types: - - for _, c := range []struct { - in, want string - }{ - /*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"}, - }`, - Run: compositeLiteralTypes, + Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", + Run: compositeLiteralTypes, }, CompositeLiteralFieldNames: { Name: CompositeLiteralFieldNames, - Doc: `Enable/disable inlay hints for composite literal field names: - - {in: "Hello, world", want: "dlrow ,olleH"}`, - Run: compositeLiteralFields, + Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", + Run: compositeLiteralFields, }, FunctionTypeParameters: { Name: FunctionTypeParameters, - Doc: `Enable/disable inlay hints for implicit type parameters on generic functions: - - myFoo/*[int, string]*/(1, "hello")`, - Run: funcTypeParams, + Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", + Run: funcTypeParams, }, } From 6c8a6c40933532ebe1271a3a5c8ff53bfdb8f1d5 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 26 Jul 2022 13:16:49 -0400 Subject: [PATCH 132/723] internal/lsp: suppress parameter hint when argument matches parameter Suppress the parameter hint when it would present redundant information. Fixes golang/go#2361 Change-Id: I4340a903046f212f8a035eab847da665e2692f1a Reviewed-on: https://go-review.googlesource.com/c/tools/+/419497 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Suzy Mueller Reviewed-by: Robert Findley --- internal/lsp/source/inlay_hint.go | 15 +++++++++++---- .../lsp/testdata/inlay_hint/parameter_names.go | 5 +++++ .../testdata/inlay_hint/parameter_names.go.golden | 5 +++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go index 6ca51930a7f..4fb1cfb44f8 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/internal/lsp/source/inlay_hint.go @@ -154,17 +154,24 @@ func parameterNames(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ if i > params.Len()-1 { break } - value := params.At(i).Name() + param := params.At(i) // param.Name is empty for built-ins like append - if value == "" { + if param.Name() == "" { continue } + // Skip the parameter name hint if the arg matches the + // the parameter name. + if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() { + continue + } + + label := param.Name() if signature.Variadic() && i == params.Len()-1 { - value = value + "..." + label = label + "..." } hints = append(hints, protocol.InlayHint{ Position: &start, - Label: buildLabel(value + ":"), + Label: buildLabel(label + ":"), Kind: protocol.Parameter, PaddingRight: true, }) diff --git a/internal/lsp/testdata/inlay_hint/parameter_names.go b/internal/lsp/testdata/inlay_hint/parameter_names.go index 6fba23530aa..0d930e5d426 100644 --- a/internal/lsp/testdata/inlay_hint/parameter_names.go +++ b/internal/lsp/testdata/inlay_hint/parameter_names.go @@ -42,4 +42,9 @@ func foobar() { kipp("a", "b", "c") plex("a", "b", "c") tars("a", "b", "c") + foo, bar, baz := "a", "b", "c" + kipp(foo, bar, baz) + plex("a", bar, baz) + tars(foo+foo, (bar), "c") + } diff --git a/internal/lsp/testdata/inlay_hint/parameter_names.go.golden b/internal/lsp/testdata/inlay_hint/parameter_names.go.golden index 46d3ea4e9bf..4e93a4f9268 100644 --- a/internal/lsp/testdata/inlay_hint/parameter_names.go.golden +++ b/internal/lsp/testdata/inlay_hint/parameter_names.go.golden @@ -43,5 +43,10 @@ func foobar() { kipp("a", "b", "c") plex("a", "b", "c") tars("a", "b", "c") + foo< string>, bar< string>, baz< string> := "a", "b", "c" + kipp(foo, bar, baz) + plex("a", bar, baz) + tars(foo+foo, (bar), "c") + } From 8ccb25c9a3d7c598b204d612763dc0192a01952c Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 26 Jul 2022 14:55:33 -0400 Subject: [PATCH 133/723] internal/lsp: treat struct tags as string type For golang/go#54066 Change-Id: Ia4f0bf0b4d76743a7f4fafc375859db7184753fb Reviewed-on: https://go-review.googlesource.com/c/tools/+/419498 Reviewed-by: Peter Weinberger TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim --- internal/lsp/semantic.go | 5 ----- internal/lsp/testdata/semantic/a.go.golden | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/lsp/semantic.go b/internal/lsp/semantic.go index 429dc0660b2..f0c4a11a4e9 100644 --- a/internal/lsp/semantic.go +++ b/internal/lsp/semantic.go @@ -299,11 +299,6 @@ func (e *encoded) inspector(n ast.Node) bool { what := tokNumber if x.Kind == token.STRING { what = tokString - if _, ok := e.stack[len(e.stack)-2].(*ast.Field); ok { - // struct tags (this is probably pointless, as the - // TextMate grammar will treat all the other comments the same) - what = tokComment - } } e.token(x.Pos(), ln, what, nil) case *ast.BinaryExpr: diff --git a/internal/lsp/testdata/semantic/a.go.golden b/internal/lsp/testdata/semantic/a.go.golden index 19dd412407d..071dd171c84 100644 --- a/internal/lsp/testdata/semantic/a.go.golden +++ b/internal/lsp/testdata/semantic/a.go.golden @@ -27,7 +27,7 @@ ) /*⇒4,keyword,[]*/type /*⇒1,type,[definition]*/A /*⇒6,keyword,[]*/struct { - /*⇒1,variable,[definition]*/X /*⇒3,type,[defaultLibrary]*/int /*⇒6,comment,[]*/`foof` + /*⇒1,variable,[definition]*/X /*⇒3,type,[defaultLibrary]*/int /*⇒6,string,[]*/`foof` } /*⇒4,keyword,[]*/type /*⇒1,type,[definition]*/B /*⇒9,keyword,[]*/interface { /*⇒1,type,[]*/A From f157068c1bcac39caec6a43bfd495c40845cf542 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 15 Jul 2022 14:51:24 -0400 Subject: [PATCH 134/723] internal/lsp/regtest: allow sharing memoized results across regtests Each regtest does a significant amount of extra work re-doing things like parsing and type-checking the runtime package. We can share this work across regtests by using a shared cache, significantly speeding them up at the cost of potentially hiding bugs related to timing. Sharing this work still retains most of the benefit of the regtests, so implement this in the default mode (formerly called "singleton" and now renamed to "default"). In a subsequent CL, modes will be cleaned up so that "default" is the only mode that runs with -short. Making this change actually revealed a caching bug: our cached package stores error messages extracted from go/packages errors, but does not include these errors in the cache key. Fix this by hashing all metadata errors into the package cache key. Updates golang/go#39384 Change-Id: I37ab9604149d34c9a79fc02b0e1bc23fcb17c454 Reviewed-on: https://go-review.googlesource.com/c/tools/+/417587 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Bryan Mills --- gopls/internal/regtest/bench/bench_test.go | 2 +- gopls/internal/regtest/debug/debug_test.go | 2 +- .../regtest/diagnostics/diagnostics_test.go | 14 +- .../regtest/misc/semantictokens_test.go | 2 +- gopls/internal/regtest/misc/vendor_test.go | 2 +- .../internal/regtest/modfile/modfile_test.go | 8 +- .../regtest/workspace/workspace_test.go | 4 +- gopls/internal/vulncheck/command_test.go | 2 +- internal/lsp/cache/cache.go | 33 ++++- internal/lsp/cache/check.go | 10 ++ internal/lsp/cache/session.go | 2 +- internal/lsp/cmd/capabilities_test.go | 2 +- internal/lsp/cmd/cmd.go | 2 +- internal/lsp/cmd/serve.go | 2 +- internal/lsp/cmd/test/cmdtest.go | 2 +- internal/lsp/lsp_test.go | 2 +- internal/lsp/lsprpc/lsprpc_test.go | 6 +- internal/lsp/mod/mod_test.go | 2 +- internal/lsp/regtest/regtest.go | 14 +- internal/lsp/regtest/runner.go | 121 ++++++++++++------ internal/lsp/source/source_test.go | 2 +- internal/memoize/memoize.go | 26 +++- 22 files changed, 182 insertions(+), 80 deletions(-) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index 7f0da83fb37..090a51cc968 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -33,7 +33,7 @@ func benchmarkOptions(dir string) []RunOption { // Skip logs as they buffer up memory unnaturally. SkipLogs(), // The Debug server only makes sense if running in singleton mode. - Modes(Singleton), + Modes(Default), // Remove the default timeout. Individual tests should control their // own graceful termination. NoDefaultTimeout(), diff --git a/gopls/internal/regtest/debug/debug_test.go b/gopls/internal/regtest/debug/debug_test.go index d01d44ed980..bae14802eef 100644 --- a/gopls/internal/regtest/debug/debug_test.go +++ b/gopls/internal/regtest/debug/debug_test.go @@ -20,7 +20,7 @@ func TestBugNotification(t *testing.T) { // Verify that a properly configured session gets notified of a bug on the // server. WithOptions( - Modes(Singleton), // must be in-process to receive the bug report below + Modes(Default), // must be in-process to receive the bug report below Settings{"showBugReports": true}, ).Run(t, "", func(t *testing.T, env *Env) { const desc = "got a bug" diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 9c9ad368cf3..ac5307e5534 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -298,7 +298,7 @@ func Hello() { t.Run("without workspace module", func(t *testing.T) { WithOptions( - Modes(Singleton), + Modes(Default), ).Run(t, noMod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), @@ -1678,7 +1678,7 @@ import ( WithOptions( InGOPATH(), EnvVars{"GO111MODULE": "off"}, - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexpWithMessage("main.go", `"nosuchpkg"`, `cannot find package "nosuchpkg" in any of`), @@ -1705,7 +1705,7 @@ package b for _, go111module := range []string{"on", "auto"} { t.Run("GO111MODULE="+go111module, func(t *testing.T) { WithOptions( - Modes(Singleton), + Modes(Default), EnvVars{"GO111MODULE": go111module}, ).Run(t, modules, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") @@ -1722,7 +1722,7 @@ package b // Expect no warning if GO111MODULE=auto in a directory in GOPATH. t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) { WithOptions( - Modes(Singleton), + Modes(Default), EnvVars{"GO111MODULE": "auto"}, InGOPATH(), ).Run(t, modules, func(t *testing.T, env *Env) { @@ -1784,7 +1784,7 @@ func helloHelper() {} ` WithOptions( ProxyFiles(proxy), - Modes(Singleton), + Modes(Default), ).Run(t, nested, func(t *testing.T, env *Env) { // Expect a diagnostic in a nested module. env.OpenFile("nested/hello/hello.go") @@ -1996,7 +1996,7 @@ func Hello() {} ` WithOptions( Settings{"experimentalUseInvalidMetadata": true}, - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.RegexpReplace("go.mod", "module mod.com", "modul mod.com") // break the go.mod file @@ -2052,7 +2052,7 @@ func _() {} Settings{"experimentalUseInvalidMetadata": true}, // ExperimentalWorkspaceModule has a different failure mode for this // case. - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( OnceMet( diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index dca2b8e7514..4437d402d46 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -25,7 +25,7 @@ func main() {} ` WithOptions( - Modes(Singleton), + Modes(Default), Settings{"allExperiments": true}, ).Run(t, src, func(t *testing.T, env *Env) { params := &protocol.SemanticTokensParams{} diff --git a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go index 4e02799b47a..b0f507aaf32 100644 --- a/gopls/internal/regtest/misc/vendor_test.go +++ b/gopls/internal/regtest/misc/vendor_test.go @@ -49,7 +49,7 @@ func _() { } ` WithOptions( - Modes(Singleton), + Modes(Default), ProxyFiles(basicProxy), ).Run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) { env.OpenFile("a/a1.go") diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index c0bef833f44..e6f76d4d514 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -742,7 +742,7 @@ func main() { WithOptions( EnvVars{"GOFLAGS": "-mod=readonly"}, ProxyFiles(proxy), - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") original := env.ReadWorkspaceFile("go.mod") @@ -922,7 +922,7 @@ func hello() {} // TODO(rFindley) this doesn't work in multi-module workspace mode, because // it keeps around the last parsing modfile. Update this test to also // exercise the workspace module. - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.Await(env.DoneWithOpen()) @@ -1090,7 +1090,7 @@ func main() { ` WithOptions( ProxyFiles(workspaceProxy), - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") params := &protocol.PublishDiagnosticsParams{} @@ -1159,7 +1159,7 @@ func main() { ` WithOptions( ProxyFiles(proxy), - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") d := &protocol.PublishDiagnosticsParams{} diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 7eafaf191dd..e4a1c4b4494 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1205,7 +1205,7 @@ package main ` WithOptions( EnvVars{"GOPATH": filepath.FromSlash("$SANDBOX_WORKDIR/gopath")}, - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( // Confirm that the build configuration is seen as valid, @@ -1236,7 +1236,7 @@ package main func main() {} ` WithOptions( - Modes(Singleton), + Modes(Default), ).Run(t, nomod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.OpenFile("b/main.go") diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index e7bf7085f88..71eaf4a580f 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -293,7 +293,7 @@ func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Co t.Fatal(err) } - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) session := cache.NewSession(ctx) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index a59c8908d5a..c002850653b 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -28,23 +28,46 @@ import ( "golang.org/x/tools/internal/span" ) -func New(options func(*source.Options)) *Cache { +// New Creates a new cache for gopls operation results, using the given file +// set, shared store, and session options. +// +// All of the fset, store and options may be nil, but if store is non-nil so +// must be fset (and they must always be used together), otherwise it may be +// possible to get cached data referencing token.Pos values not mapped by the +// FileSet. +func New(fset *token.FileSet, store *memoize.Store, options func(*source.Options)) *Cache { index := atomic.AddInt64(&cacheIndex, 1) + + if store != nil && fset == nil { + panic("non-nil store with nil fset") + } + if fset == nil { + fset = token.NewFileSet() + } + if store == nil { + store = &memoize.Store{} + } + c := &Cache{ id: strconv.FormatInt(index, 10), - fset: token.NewFileSet(), + fset: fset, options: options, + store: store, fileContent: map[span.URI]*fileHandle{}, } return c } type Cache struct { - id string - fset *token.FileSet + id string + fset *token.FileSet + + // TODO(rfindley): it doesn't make sense that cache accepts LSP options, just + // so that it can create a session: the cache does not (and should not) + // depend on options. Invert this relationship to remove options from Cache. options func(*source.Options) - store memoize.Store + store *memoize.Store fileMu sync.Mutex fileContent map[span.URI]*fileHandle diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index 4caf4ba6fa7..6c02d5348f8 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -249,6 +249,16 @@ func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata for _, file := range files { b.WriteString(file.FileIdentity().String()) } + // Metadata errors are interpreted and memoized on the computed package, so + // we must hash them into the key here. + // + // TODO(rfindley): handle metadata diagnostics independently from + // type-checking diagnostics. + for _, err := range m.Errors { + b.WriteString(err.Msg) + b.WriteString(err.Pos) + b.WriteRune(rune(err.Kind)) + } return packageHandleKey(source.HashOf(b.Bytes())) } diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 2374a528757..a27efe334fd 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -231,7 +231,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, backgroundCtx: backgroundCtx, cancel: cancel, initializeOnce: &sync.Once{}, - store: &s.cache.store, + store: s.cache.store, packages: persistent.NewMap(packageKeyLessInterface), meta: &metadataGraph{}, files: newFilesMap(), diff --git a/internal/lsp/cmd/capabilities_test.go b/internal/lsp/cmd/capabilities_test.go index 1d01b4bd0d7..930621bf307 100644 --- a/internal/lsp/cmd/capabilities_test.go +++ b/internal/lsp/cmd/capabilities_test.go @@ -43,7 +43,7 @@ func TestCapabilities(t *testing.T) { params.Capabilities.Workspace.Configuration = true // Send an initialize request to the server. - c.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), c.Client) + c.Server = lsp.NewServer(cache.New(nil, nil, app.options).NewSession(ctx), c.Client) result, err := c.Server.Initialize(ctx, params) if err != nil { t.Fatal(err) diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go index a81eb839535..5911f97d1c1 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -286,7 +286,7 @@ func (app *Application) connect(ctx context.Context) (*connection, error) { switch { case app.Remote == "": connection := newConnection(app) - connection.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), connection.Client) + connection.Server = lsp.NewServer(cache.New(nil, nil, app.options).NewSession(ctx), connection.Client) ctx = protocol.WithClient(ctx, connection.Client) return connection, connection.initialize(ctx, app.options) case strings.HasPrefix(app.Remote, "internal@"): diff --git a/internal/lsp/cmd/serve.go b/internal/lsp/cmd/serve.go index 1c229a422b4..10730fd89cd 100644 --- a/internal/lsp/cmd/serve.go +++ b/internal/lsp/cmd/serve.go @@ -101,7 +101,7 @@ func (s *Serve) Run(ctx context.Context, args ...string) error { return fmt.Errorf("creating forwarder: %w", err) } } else { - ss = lsprpc.NewStreamServer(cache.New(s.app.options), isDaemon) + ss = lsprpc.NewStreamServer(cache.New(nil, nil, s.app.options), isDaemon) } var network, addr string diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go index ff0461b333f..5342e9b7faf 100644 --- a/internal/lsp/cmd/test/cmdtest.go +++ b/internal/lsp/cmd/test/cmdtest.go @@ -50,7 +50,7 @@ func TestCommandLine(t *testing.T, testdata string, options func(*source.Options func NewTestServer(ctx context.Context, options func(*source.Options)) *servertest.TCPServer { ctx = debug.WithInstance(ctx, "", "") - cache := cache.New(options) + cache := cache.New(nil, nil, options) ss := lsprpc.NewStreamServer(cache, false) return servertest.NewTCPServer(ctx, ss, nil) } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 8e0e628dfea..53890dc616b 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -49,7 +49,7 @@ type runner struct { func testLSP(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) session := cache.NewSession(ctx) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) diff --git a/internal/lsp/lsprpc/lsprpc_test.go b/internal/lsp/lsprpc/lsprpc_test.go index cde641c920b..498566d1bbd 100644 --- a/internal/lsp/lsprpc/lsprpc_test.go +++ b/internal/lsp/lsprpc/lsprpc_test.go @@ -58,7 +58,7 @@ func TestClientLogging(t *testing.T) { client := FakeClient{Logs: make(chan string, 10)} ctx = debug.WithInstance(ctx, "", "") - ss := NewStreamServer(cache.New(nil), false) + ss := NewStreamServer(cache.New(nil, nil, nil), false) ss.serverForTest = server ts := servertest.NewPipeServer(ss, nil) defer checkClose(t, ts.Close) @@ -121,7 +121,7 @@ func checkClose(t *testing.T, closer func() error) { func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (direct, forwarded servertest.Connector, cleanup func()) { t.Helper() serveCtx := debug.WithInstance(ctx, "", "") - ss := NewStreamServer(cache.New(nil), false) + ss := NewStreamServer(cache.New(nil, nil, nil), false) ss.serverForTest = s tsDirect := servertest.NewTCPServer(serveCtx, ss, nil) @@ -216,7 +216,7 @@ func TestDebugInfoLifecycle(t *testing.T) { clientCtx := debug.WithInstance(baseCtx, "", "") serverCtx := debug.WithInstance(baseCtx, "", "") - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) ss := NewStreamServer(cache, false) tsBackend := servertest.NewTCPServer(serverCtx, ss, nil) diff --git a/internal/lsp/mod/mod_test.go b/internal/lsp/mod/mod_test.go index 09a182d16d7..56af9860b9d 100644 --- a/internal/lsp/mod/mod_test.go +++ b/internal/lsp/mod/mod_test.go @@ -26,7 +26,7 @@ func TestModfileRemainsUnchanged(t *testing.T) { testenv.NeedsGo1Point(t, 14) ctx := tests.Context(t) - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) session := cache.NewSession(ctx) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) diff --git a/internal/lsp/regtest/regtest.go b/internal/lsp/regtest/regtest.go index 9ebc673f8c0..b3a543d531e 100644 --- a/internal/lsp/regtest/regtest.go +++ b/internal/lsp/regtest/regtest.go @@ -8,6 +8,7 @@ import ( "context" "flag" "fmt" + "go/token" "io/ioutil" "os" "runtime" @@ -16,6 +17,7 @@ import ( "golang.org/x/tools/internal/lsp/cmd" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/tool" ) @@ -87,9 +89,11 @@ var slowGOOS = map[string]bool{ } func DefaultModes() Mode { - normal := Singleton | Experimental + // TODO(rfindley): these modes should *not* depend on GOOS. Depending on + // testing.Short() should be sufficient. + normal := Default | Experimental if slowGOOS[runtime.GOOS] && testing.Short() { - normal = Singleton + normal = Default } if *runSubprocessTests { return normal | SeparateProcess @@ -116,6 +120,8 @@ func Main(m *testing.M, hook func(*source.Options)) { PrintGoroutinesOnFailure: *printGoroutinesOnFailure, SkipCleanup: *skipCleanup, OptionsHook: hook, + fset: token.NewFileSet(), + store: memoize.NewStore(memoize.NeverEvict), } if *runSubprocessTests { goplsPath := *goplsBinaryPath @@ -126,13 +132,13 @@ func Main(m *testing.M, hook func(*source.Options)) { panic(fmt.Sprintf("finding test binary path: %v", err)) } } - runner.GoplsPath = goplsPath + runner.goplsPath = goplsPath } dir, err := ioutil.TempDir("", "gopls-regtest-") if err != nil { panic(fmt.Errorf("creating regtest temp directory: %v", err)) } - runner.TempDir = dir + runner.tempDir = dir code := m.Run() if err := runner.Close(); err != nil { diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go index b2992e99392..6c96e61dbc6 100644 --- a/internal/lsp/regtest/runner.go +++ b/internal/lsp/regtest/runner.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "fmt" + "go/token" "io" "io/ioutil" "net" @@ -29,24 +30,58 @@ import ( "golang.org/x/tools/internal/lsp/lsprpc" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/xcontext" ) // Mode is a bitmask that defines for which execution modes a test should run. +// +// Each mode controls several aspects of gopls' configuration: +// - Which server options to use for gopls sessions +// - Whether to use a shared cache +// - Whether to use a shared server +// - Whether to run the server in-process or in a separate process +// +// The behavior of each mode with respect to these aspects is summarized below. +// TODO(rfindley, cleanup): rather than using arbitrary names for these modes, +// we can compose them explicitly out of the features described here, allowing +// individual tests more freedom in constructing problematic execution modes. +// For example, a test could assert on a certain behavior when running with +// experimental options on a separate process. Moreover, we could unify 'Modes' +// with 'Options', and use RunMultiple rather than a hard-coded loop through +// modes. +// +// Mode | Options | Shared Cache? | Shared Server? | In-process? +// --------------------------------------------------------------------------- +// Default | Default | Y | N | Y +// Forwarded | Default | Y | Y | Y +// SeparateProcess | Default | Y | Y | N +// Experimental | Experimental | N | N | Y type Mode int const ( - // Singleton mode uses a separate in-process gopls instance for each test, - // and communicates over pipes to mimic the gopls sidecar execution mode, - // which communicates over stdin/stderr. - Singleton Mode = 1 << iota - // Forwarded forwards connections to a shared in-process gopls instance. + // Default mode runs gopls with the default options, communicating over pipes + // to emulate the lsp sidecar execution mode, which communicates over + // stdin/stdout. + // + // It uses separate servers for each test, but a shared cache, to avoid + // duplicating work when processing GOROOT. + Default Mode = 1 << iota + + // Forwarded uses the default options, but forwards connections to a shared + // in-process gopls server. Forwarded - // SeparateProcess forwards connection to a shared separate gopls process. + + // SeparateProcess uses the default options, but forwards connection to an + // external gopls daemon. SeparateProcess + // Experimental enables all of the experimental configurations that are - // being developed. + // being developed, and runs gopls in sidecar mode. + // + // It uses a separate cache for each test, to exercise races that may only + // appear with cache misses. Experimental ) @@ -55,14 +90,20 @@ const ( // remote), any tests that execute on the same Runner will share the same // state. type Runner struct { - DefaultModes Mode - Timeout time.Duration - GoplsPath string - PrintGoroutinesOnFailure bool - TempDir string - SkipCleanup bool - OptionsHook func(*source.Options) - + // Configuration + DefaultModes Mode // modes to run for each test + Timeout time.Duration // per-test timeout, if set + PrintGoroutinesOnFailure bool // whether to dump goroutines on test failure + SkipCleanup bool // if set, don't delete test data directories when the test exits + OptionsHook func(*source.Options) // if set, use these options when creating gopls sessions + + // Immutable state shared across test invocations + goplsPath string // path to the gopls executable (for SeparateProcess mode) + tempDir string // shared parent temp directory + fset *token.FileSet // shared FileSet + store *memoize.Store // shared store + + // Lazily allocated resources mu sync.Mutex ts *servertest.TCPServer socketDir string @@ -193,7 +234,7 @@ func InGOPATH() RunOption { } // DebugAddress configures a debug server bound to addr. This option is -// currently only supported when executing in Singleton mode. It is intended to +// currently only supported when executing in Default mode. It is intended to // be used for long-running stress tests. func DebugAddress(addr string) RunOption { return optionSetter(func(opts *runConfig) { @@ -252,10 +293,10 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio mode Mode getServer func(*testing.T, func(*source.Options)) jsonrpc2.StreamServer }{ - {"singleton", Singleton, singletonServer}, + {"default", Default, r.defaultServer}, {"forwarded", Forwarded, r.forwardedServer}, {"separate_process", SeparateProcess, r.separateProcessServer}, - {"experimental", Experimental, experimentalServer}, + {"experimental", Experimental, r.experimentalServer}, } for _, tc := range tests { @@ -267,10 +308,10 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio if config.modes&tc.mode == 0 { continue } - if config.debugAddr != "" && tc.mode != Singleton { + if config.debugAddr != "" && tc.mode != Default { // Debugging is useful for running stress tests, but since the daemon has // likely already been started, it would be too late to debug. - t.Fatalf("debugging regtest servers only works in Singleton mode, "+ + t.Fatalf("debugging regtest servers only works in Default mode, "+ "got debug addr %q and mode %v", config.debugAddr, tc.mode) } @@ -298,7 +339,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio di.MonitorMemory(ctx) } - rootDir := filepath.Join(r.TempDir, filepath.FromSlash(t.Name())) + rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name())) if err := os.MkdirAll(rootDir, 0755); err != nil { t.Fatal(err) } @@ -434,11 +475,13 @@ func (s *loggingFramer) printBuffers(testname string, w io.Writer) { fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname) } -func singletonServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { - return lsprpc.NewStreamServer(cache.New(optsHook), false) +// defaultServer handles the Default execution mode. +func (r *Runner) defaultServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { + return lsprpc.NewStreamServer(cache.New(r.fset, r.store, optsHook), false) } -func experimentalServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { +// experimentalServer handles the Experimental execution mode. +func (r *Runner) experimentalServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { options := func(o *source.Options) { optsHook(o) o.EnableAllExperiments() @@ -446,28 +489,23 @@ func experimentalServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.S // source.Options.EnableAllExperiments, but we want to test it. o.ExperimentalWorkspaceModule = true } - return lsprpc.NewStreamServer(cache.New(options), false) -} - -func (r *Runner) forwardedServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { - ts := r.getTestServer(optsHook) - return newForwarder("tcp", ts.Addr) + return lsprpc.NewStreamServer(cache.New(nil, nil, options), false) } -// getTestServer gets the shared test server instance to connect to, or creates -// one if it doesn't exist. -func (r *Runner) getTestServer(optsHook func(*source.Options)) *servertest.TCPServer { - r.mu.Lock() - defer r.mu.Unlock() +// forwardedServer handles the Forwarded execution mode. +func (r *Runner) forwardedServer(_ *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { if r.ts == nil { + r.mu.Lock() ctx := context.Background() ctx = debug.WithInstance(ctx, "", "off") - ss := lsprpc.NewStreamServer(cache.New(optsHook), false) + ss := lsprpc.NewStreamServer(cache.New(nil, nil, optsHook), false) r.ts = servertest.NewTCPServer(ctx, ss, nil) + r.mu.Unlock() } - return r.ts + return newForwarder("tcp", r.ts.Addr) } +// separateProcessServer handles the SeparateProcess execution mode. func (r *Runner) separateProcessServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { // TODO(rfindley): can we use the autostart behavior here, instead of // pre-starting the remote? @@ -498,21 +536,22 @@ func (r *Runner) getRemoteSocket(t *testing.T) string { return filepath.Join(r.socketDir, daemonFile) } - if r.GoplsPath == "" { + if r.goplsPath == "" { t.Fatal("cannot run tests with a separate process unless a path to a gopls binary is configured") } var err error - r.socketDir, err = ioutil.TempDir(r.TempDir, "gopls-regtest-socket") + r.socketDir, err = ioutil.TempDir(r.tempDir, "gopls-regtest-socket") if err != nil { t.Fatalf("creating tempdir: %v", err) } socket := filepath.Join(r.socketDir, daemonFile) args := []string{"serve", "-listen", "unix;" + socket, "-listen.timeout", "10s"} - cmd := exec.Command(r.GoplsPath, args...) + cmd := exec.Command(r.goplsPath, args...) cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true") var stderr bytes.Buffer cmd.Stderr = &stderr go func() { + // TODO(rfindley): this is racy; we're returning before we know that the command is running. if err := cmd.Run(); err != nil { panic(fmt.Sprintf("error running external gopls: %v\nstderr:\n%s", err, stderr.String())) } @@ -537,7 +576,7 @@ func (r *Runner) Close() error { } } if !r.SkipCleanup { - if err := os.RemoveAll(r.TempDir); err != nil { + if err := os.RemoveAll(r.tempDir); err != nil { errmsgs = append(errmsgs, err.Error()) } } diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index c670bdeb0b2..5fdcc0f86ea 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -49,7 +49,7 @@ type runner struct { func testSource(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) session := cache.NewSession(ctx) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index aa4d58d2f26..150c79af852 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -236,12 +236,34 @@ func (p *Promise) wait(ctx context.Context) (interface{}, error) { } } +// An EvictionPolicy controls the eviction behavior of keys in a Store when +// they no longer have any references. +type EvictionPolicy int + +const ( + // ImmediatelyEvict evicts keys as soon as they no longer have references. + ImmediatelyEvict EvictionPolicy = iota + + // NeverEvict does not evict keys. + NeverEvict +) + // A Store maps arbitrary keys to reference-counted promises. +// +// The zero value is a valid Store, though a store may also be created via +// NewStore if a custom EvictionPolicy is required. type Store struct { + evictionPolicy EvictionPolicy + promisesMu sync.Mutex promises map[interface{}]*Promise } +// NewStore creates a new store with the given eviction policy. +func NewStore(policy EvictionPolicy) *Store { + return &Store{evictionPolicy: policy} +} + // Promise returns a reference-counted promise for the future result of // calling the specified function. // @@ -264,7 +286,9 @@ func (store *Store) Promise(key interface{}, function Function) (*Promise, func( store.promisesMu.Unlock() release := func() { - if atomic.AddInt32(&p.refcount, -1) == 0 { + // TODO(rfindley): this looks racy: it's possible that the refcount is + // incremented before we grab the lock. + if atomic.AddInt32(&p.refcount, -1) == 0 && store.evictionPolicy != NeverEvict { store.promisesMu.Lock() delete(store.promises, key) store.promisesMu.Unlock() From 39a4e36475d99fd719f5d62f870e662e057c7ad2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 15 Jul 2022 14:58:24 -0400 Subject: [PATCH 135/723] internal/lsp/regtest: only run /default tests with -short Due to shared caching, the "default" tests can run faster than other execution modes and still retain most of the test coverage. Update the test runner to only run the singleton tests if testing.Short() is true, independent of GOOS. On the other hand, we lost noticeable coverage when disabling the Forwarded testing mode. Now that the regtests are lighter weight in general, reenable it on the longtests builders. While at it, clean up tests that used the server-side Options hook to instead use Settings, since clients would configure their server via Settings and Options can't work with a shared daemon. Updates golang/go#39384 Change-Id: I33e8b746188d795e88841727e6b7116cd4851dc2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/418774 Reviewed-by: Bryan Mills TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- .../completion/postfix_snippet_test.go | 9 +-- gopls/internal/regtest/misc/shared_test.go | 33 ++++++----- .../regtest/workspace/workspace_test.go | 43 +++++--------- internal/lsp/regtest/regtest.go | 23 +++----- internal/lsp/regtest/runner.go | 58 +++++++++++-------- 5 files changed, 79 insertions(+), 87 deletions(-) diff --git a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go index 5a7ffb80d26..54860474e73 100644 --- a/gopls/internal/regtest/completion/postfix_snippet_test.go +++ b/gopls/internal/regtest/completion/postfix_snippet_test.go @@ -9,7 +9,6 @@ import ( "testing" . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/source" ) func TestPostfixSnippetCompletion(t *testing.T) { @@ -433,9 +432,11 @@ func foo() string { }, } - r := WithOptions(Options(func(o *source.Options) { - o.ExperimentalPostfixCompletions = true - })) + r := WithOptions( + Settings{ + "experimentalPostfixCompletions": true, + }, + ) r.Run(t, mod, func(t *testing.T, env *Env) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index 6b5acd02f71..170e164c94c 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -24,7 +24,10 @@ func main() { fmt.Println("Hello World.") }` -func runShared(t *testing.T, testFunc func(env1 *Env, env2 *Env)) { +// runShared is a helper to run a test in the same directory using both the +// original env, and an additional other environment connected to the same +// server. +func runShared(t *testing.T, testFunc func(origEnv *Env, otherEnv *Env)) { // Only run these tests in forwarded modes. modes := DefaultModes() & (Forwarded | SeparateProcess) WithOptions(Modes(modes)).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { @@ -38,28 +41,32 @@ func runShared(t *testing.T, testFunc func(env1 *Env, env2 *Env)) { } func TestSimultaneousEdits(t *testing.T) { - runShared(t, func(env1 *Env, env2 *Env) { + runShared(t, func(origEnv *Env, otherEnv *Env) { // In editor #1, break fmt.Println as before. - env1.OpenFile("main.go") - env1.RegexpReplace("main.go", "Printl(n)", "") + origEnv.OpenFile("main.go") + origEnv.RegexpReplace("main.go", "Printl(n)", "") // In editor #2 remove the closing brace. - env2.OpenFile("main.go") - env2.RegexpReplace("main.go", "\\)\n(})", "") + otherEnv.OpenFile("main.go") + otherEnv.RegexpReplace("main.go", "\\)\n(})", "") // Now check that we got different diagnostics in each environment. - env1.Await(env1.DiagnosticAtRegexp("main.go", "Printl")) - env2.Await(env2.DiagnosticAtRegexp("main.go", "$")) + origEnv.Await(origEnv.DiagnosticAtRegexp("main.go", "Printl")) + otherEnv.Await(otherEnv.DiagnosticAtRegexp("main.go", "$")) }) } func TestShutdown(t *testing.T) { - runShared(t, func(env1 *Env, env2 *Env) { - if err := env1.Editor.Close(env1.Ctx); err != nil { + runShared(t, func(origEnv *Env, otherEnv *Env) { + // Close otherEnv, and verify that operation in the original environment is + // unaffected. Note: 'otherEnv' must be the environment being closed here. + // If we were to instead close 'env' here, we'd run into a duplicate + // shutdown when the test runner closes the original env. + if err := otherEnv.Editor.Close(otherEnv.Ctx); err != nil { t.Errorf("closing first editor: %v", err) } // Now make an edit in editor #2 to trigger diagnostics. - env2.OpenFile("main.go") - env2.RegexpReplace("main.go", "\\)\n(})", "") - env2.Await(env2.DiagnosticAtRegexp("main.go", "$")) + origEnv.OpenFile("main.go") + origEnv.RegexpReplace("main.go", "\\)\n(})", "") + origEnv.Await(origEnv.DiagnosticAtRegexp("main.go", "$")) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index e4a1c4b4494..deb8a83695f 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/testenv" . "golang.org/x/tools/internal/lsp/regtest" @@ -138,36 +137,22 @@ func TestReferences(t *testing.T) { } } -// make sure that directory filters work -func TestFilters(t *testing.T) { - for _, tt := range []struct { - name, rootPath string - }{ - { - name: "module root", - rootPath: "pkg", +func TestDirectoryFilters(t *testing.T) { + WithOptions( + ProxyFiles(workspaceProxy), + WorkspaceFolders("pkg"), + Settings{ + "directoryFilters": []string{"-inner"}, }, - } { - t.Run(tt.name, func(t *testing.T) { - opts := []RunOption{ProxyFiles(workspaceProxy)} - if tt.rootPath != "" { - opts = append(opts, WorkspaceFolders(tt.rootPath)) - } - f := func(o *source.Options) { - o.DirectoryFilters = append(o.DirectoryFilters, "-inner") + ).Run(t, workspaceModule, func(t *testing.T, env *Env) { + syms := env.WorkspaceSymbol("Hi") + sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) + for _, s := range syms { + if strings.Contains(s.ContainerName, "inner") { + t.Errorf("WorkspaceSymbol: found symbol %q with container %q, want \"inner\" excluded", s.Name, s.ContainerName) } - opts = append(opts, Options(f)) - WithOptions(opts...).Run(t, workspaceModule, func(t *testing.T, env *Env) { - syms := env.WorkspaceSymbol("Hi") - sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) - for i, s := range syms { - if strings.Contains(s.ContainerName, "/inner") { - t.Errorf("%s %v %s %s %d\n", s.Name, s.Kind, s.ContainerName, tt.name, i) - } - } - }) - }) - } + } + }) } // Make sure that analysis diagnostics are cleared for the whole package when diff --git a/internal/lsp/regtest/regtest.go b/internal/lsp/regtest/regtest.go index b3a543d531e..cfd999dfa59 100644 --- a/internal/lsp/regtest/regtest.go +++ b/internal/lsp/regtest/regtest.go @@ -79,26 +79,17 @@ func (r RunMultiple) Run(t *testing.T, files string, f TestFunc) { } } -// The regtests run significantly slower on these operating systems, due to (we -// believe) kernel locking behavior. Only run in singleton mode on these -// operating system when using -short. -var slowGOOS = map[string]bool{ - "darwin": true, - "openbsd": true, - "plan9": true, -} - +// DefaultModes returns the default modes to run for each regression test (they +// may be reconfigured by the tests themselves). func DefaultModes() Mode { - // TODO(rfindley): these modes should *not* depend on GOOS. Depending on - // testing.Short() should be sufficient. - normal := Default | Experimental - if slowGOOS[runtime.GOOS] && testing.Short() { - normal = Default + modes := Default + if !testing.Short() { + modes |= Experimental | Forwarded } if *runSubprocessTests { - return normal | SeparateProcess + modes |= SeparateProcess } - return normal + return modes } // Main sets up and tears down the shared regtest state. diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go index 6c96e61dbc6..1d22aaa9d85 100644 --- a/internal/lsp/regtest/runner.go +++ b/internal/lsp/regtest/runner.go @@ -85,6 +85,21 @@ const ( Experimental ) +func (m Mode) String() string { + switch m { + case Default: + return "default" + case Forwarded: + return "forwarded" + case SeparateProcess: + return "separate process" + case Experimental: + return "experimental" + default: + return "unknown mode" + } +} + // A Runner runs tests in gopls execution environments, as specified by its // modes. For modes that share state (for example, a shared cache or common // remote), any tests that execute on the same Runner will share the same @@ -117,14 +132,6 @@ type runConfig struct { debugAddr string skipLogs bool skipHooks bool - optionsHook func(*source.Options) -} - -func (r *Runner) defaultConfig() *runConfig { - return &runConfig{ - modes: r.DefaultModes, - optionsHook: r.OptionsHook, - } } // A RunOption augments the behavior of the test runner. @@ -155,22 +162,16 @@ func ProxyFiles(txt string) RunOption { } // Modes configures the execution modes that the test should run in. +// +// By default, modes are configured by the test runner. If this option is set, +// it overrides the set of default modes and the test runs in exactly these +// modes. func Modes(modes Mode) RunOption { return optionSetter(func(opts *runConfig) { - opts.modes = modes - }) -} - -// Options configures the various server and user options. -func Options(hook func(*source.Options)) RunOption { - return optionSetter(func(opts *runConfig) { - old := opts.optionsHook - opts.optionsHook = func(o *source.Options) { - if old != nil { - old(o) - } - hook(o) + if opts.modes != 0 { + panic("modes set more than once") } + opts.modes = modes }) } @@ -301,13 +302,18 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio for _, tc := range tests { tc := tc - config := r.defaultConfig() + var config runConfig for _, opt := range opts { - opt.set(config) + opt.set(&config) } - if config.modes&tc.mode == 0 { + modes := r.DefaultModes + if config.modes != 0 { + modes = config.modes + } + if modes&tc.mode == 0 { continue } + if config.debugAddr != "" && tc.mode != Default { // Debugging is useful for running stress tests, but since the daemon has // likely already been started, it would be too late to debug. @@ -364,7 +370,9 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio } } }() - ss := tc.getServer(t, config.optionsHook) + + ss := tc.getServer(t, r.OptionsHook) + framer := jsonrpc2.NewRawStream ls := &loggingFramer{} if !config.skipLogs { From b3b5c13b291f9653da6f31b95db100a2e26bd186 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 26 Jul 2022 17:01:06 -0400 Subject: [PATCH 136/723] internal/lsp/cache: invalidate packages with missing deps when files are added Add logic to invalidate any packages with missing dependencies when a file is added. Also fix a latent bug overwriting 'anyFileOpenenedOrClosed' for each loop iteration. Fixes golang/go#54073 Change-Id: I000ceb354885bd4863a1dfdda09e4d5f0e5481f3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419501 Run-TryBot: Robert Findley Reviewed-by: Suzy Mueller gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/regtest/watch/watch_test.go | 4 +--- internal/lsp/cache/snapshot.go | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 04414f6b744..2890f401a90 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -199,14 +199,12 @@ func _() { } ` Run(t, missing, func(t *testing.T, env *Env) { - t.Skip("the initial workspace load fails and never retries") - env.Await( env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""), ) env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) env.Await( - EmptyDiagnostics("c/c.go"), + EmptyDiagnostics("a/a.go"), ) }) } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 64e7f17c994..b183bc5f88f 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1695,8 +1695,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Compute invalidations based on file changes. changedPkgFiles := map[PackageID]bool{} // packages whose file set may have changed - anyImportDeleted := false - anyFileOpenedOrClosed := false + anyImportDeleted := false // import deletions can resolve cycles + anyFileOpenedOrClosed := false // opened files affect workspace packages + anyFileAdded := false // adding a file can resolve missing dependencies + for uri, change := range changes { // Maybe reinitialize the view if we see a change in the vendor // directory. @@ -1709,7 +1711,8 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC var originalOpen, newOpen bool _, originalOpen = originalFH.(*overlay) _, newOpen = change.fileHandle.(*overlay) - anyFileOpenedOrClosed = originalOpen != newOpen + anyFileOpenedOrClosed = anyFileOpenedOrClosed || (originalOpen != newOpen) + anyFileAdded = anyFileAdded || (originalFH == nil && change.fileHandle != nil) // If uri is a Go file, check if it has changed in a way that would // invalidate metadata. Note that we can't use s.view.FileKind here, @@ -1779,6 +1782,19 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } + // Adding a file can resolve missing dependencies from existing packages. + // + // We could be smart here and try to guess which packages may have been + // fixed, but until that proves necessary, just invalidate metadata for any + // package with missing dependencies. + if anyFileAdded { + for id, metadata := range s.meta.metadata { + if len(metadata.MissingDeps) > 0 { + directIDs[id] = true + } + } + } + // Invalidate reverse dependencies too. // idsToInvalidate keeps track of transitive reverse dependencies. // If an ID is present in the map, invalidate its types. From f1bb5ca08f1023d773c2ef3f28ffacaa6017861a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 25 Jul 2022 09:57:53 -0400 Subject: [PATCH 137/723] internal/lsp/cache: report a critical error when go.work is invalid When a go.work file fails to validate, the workspace is left in an invalid state: we will detect that the workspace is defined by the go.work, but will not actually parse any active modules. This should be a critical error. Fix this by adding allowing the workspace to surface critical errors via a new cache.workspace.criticalError method. Additionally: - only build the workspace mod file in workspace.build if the mode is fileSystemWorkspace (in all other modes the modfile is already determined) - rename workspace.invalidate to workspace.Clone, to be consistent with other data structures - rename CriticalError.DiagList to CriticalError.Diagnostics - add several TODOs for observations while reading the code - create a new file for regtests related to broken workspaces - make the regtest sandbox panic when duplicate paths are present in the sandbox file set (an error I made while writing the test) Updates golang/go#53933 Change-Id: If8625ab190129bc9c57e784314bc9cc92644c955 Reviewed-on: https://go-review.googlesource.com/c/tools/+/417593 Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- .../regtest/diagnostics/diagnostics_test.go | 2 +- .../internal/regtest/workspace/broken_test.go | 120 ++++++++++++++++++ .../regtest/workspace/workspace_test.go | 4 - internal/lsp/cache/load.go | 6 +- internal/lsp/cache/mod_tidy.go | 3 +- internal/lsp/cache/session.go | 15 ++- internal/lsp/cache/snapshot.go | 35 +++-- internal/lsp/cache/view.go | 21 ++- internal/lsp/cache/workspace.go | 53 +++++++- internal/lsp/cache/workspace_test.go | 2 +- internal/lsp/diagnostics.go | 2 +- internal/lsp/fake/sandbox.go | 3 + internal/lsp/regtest/expectation.go | 2 +- internal/lsp/source/view.go | 8 +- 14 files changed, 234 insertions(+), 42 deletions(-) create mode 100644 gopls/internal/regtest/workspace/broken_test.go diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index ac5307e5534..e1b55b5b131 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -543,7 +543,7 @@ func _() { // Expect a module/GOPATH error if there is an error in the file at startup. // Tests golang/go#37279. -func TestShowCriticalError_Issue37279(t *testing.T) { +func TestBrokenWorkspace_OutsideModule(t *testing.T) { const noModule = ` -- a.go -- package foo diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go new file mode 100644 index 00000000000..a818fe40bcf --- /dev/null +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -0,0 +1,120 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package workspace + +import ( + "testing" + + "golang.org/x/tools/internal/lsp" + . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" +) + +// This file holds various tests for UX with respect to broken workspaces. +// +// TODO: consolidate other tests here. + +// Test for golang/go#53933 +func TestBrokenWorkspace_DuplicateModules(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + + // This proxy module content is replaced by the workspace, but is still + // required for module resolution to function in the Go command. + const proxy = ` +-- example.com/foo@v0.0.1/go.mod -- +module example.com/foo + +go 1.12 +-- example.com/foo@v1.2.3/foo.go -- +package foo +` + + const src = ` +-- go.work -- +go 1.18 + +use ( + ./package1 + ./package1/vendor/example.com/foo + ./package2 + ./package2/vendor/example.com/foo +) + +-- package1/go.mod -- +module mod.test + +go 1.18 + +require example.com/foo v0.0.1 +-- package1/main.go -- +package main + +import "example.com/foo" + +func main() { + _ = foo.CompleteMe +} +-- package1/vendor/example.com/foo/go.mod -- +module example.com/foo + +go 1.18 +-- package1/vendor/example.com/foo/foo.go -- +package foo + +const CompleteMe = 111 +-- package2/go.mod -- +module mod2.test + +go 1.18 + +require example.com/foo v0.0.1 +-- package2/main.go -- +package main + +import "example.com/foo" + +func main() { + _ = foo.CompleteMe +} +-- package2/vendor/example.com/foo/go.mod -- +module example.com/foo + +go 1.18 +-- package2/vendor/example.com/foo/foo.go -- +package foo + +const CompleteMe = 222 +` + + WithOptions( + ProxyFiles(proxy), + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("package1/main.go") + env.Await( + OutstandingWork(lsp.WorkspaceLoadFailure, `found module "example.com/foo" multiple times in the workspace`), + ) + + /* TODO(golang/go#54069): once we allow network when reinitializing the + * workspace, we should be able to fix the error here. + + // Remove the redundant vendored copy of example.com. + env.WriteWorkspaceFile("go.work", `go 1.18 + use ( + ./package1 + ./package2 + ./package2/vendor/example.com/foo + ) + `) + env.Await(NoOutstandingWork()) + + // Check that definitions in package1 go to the copy vendored in package2. + location, _ := env.GoToDefinition("package1/main.go", env.RegexpSearch("package1/main.go", "CompleteMe")) + const wantLocation = "package2/vendor/example.com/foo/foo.go" + if !strings.HasSuffix(location, wantLocation) { + t.Errorf("got definition of CompleteMe at %q, want %q", location, wantLocation) + } + */ + }) +} diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index deb8a83695f..bc576226e84 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -290,10 +290,6 @@ func Hello() {} module b.com go 1.12 --- b.com@v1.2.4/b/b.go -- -package b - -func Hello() {} ` const multiModule = ` -- go.mod -- diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 79789fe60d0..9d84891911f 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -310,8 +310,8 @@ You can work with multiple modules by opening each one as a workspace folder. Improvements to this workflow will be coming soon, and you can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` return &source.CriticalError{ - MainError: fmt.Errorf(msg), - DiagList: s.applyCriticalErrorToFiles(ctx, msg, openFiles), + MainError: fmt.Errorf(msg), + Diagnostics: s.applyCriticalErrorToFiles(ctx, msg, openFiles), } } @@ -349,7 +349,7 @@ You can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/w MainError: fmt.Errorf(`You are working in a nested module. Please open it as a separate workspace folder. Learn more: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`), - DiagList: srcDiags, + Diagnostics: srcDiags, } } } diff --git a/internal/lsp/cache/mod_tidy.go b/internal/lsp/cache/mod_tidy.go index 361f526ddfb..704e1a63a46 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/internal/lsp/cache/mod_tidy.go @@ -61,7 +61,7 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc if criticalErr := s.GetCriticalError(ctx); criticalErr != nil { return &source.TidiedModule{ - Diagnostics: criticalErr.DiagList, + Diagnostics: criticalErr.Diagnostics, }, nil } @@ -70,7 +70,6 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc } handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} { - tidied, err := modTidyImpl(ctx, arg.(*snapshot), uri.Filename(), pm) return modTidyResult{tidied, err} }) diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index a27efe334fd..88690648286 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -180,21 +180,26 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, s.cache.options(options) } - // Set the module-specific information. - ws, err := s.getWorkspaceInformation(ctx, folder, options) + // Get immutable workspace configuration. + // + // TODO(rfindley): this info isn't actually immutable. For example, GOWORK + // could be changed, or a user's environment could be modified. + // We need a mechanism to invalidate it. + wsInfo, err := s.getWorkspaceInformation(ctx, folder, options) if err != nil { return nil, nil, func() {}, err } + root := folder if options.ExpandWorkspaceToModule { - root, err = findWorkspaceRoot(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), ws.gomodcache, options), options.ExperimentalWorkspaceModule) + root, err = findWorkspaceRoot(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), options.ExperimentalWorkspaceModule) if err != nil { return nil, nil, func() {}, err } } // Build the gopls workspace, collecting active modules in the view. - workspace, err := newWorkspace(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), ws.gomodcache, options), ws.userGo111Module == off, options.ExperimentalWorkspaceModule) + workspace, err := newWorkspace(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), wsInfo.userGo111Module == off, options.ExperimentalWorkspaceModule) if err != nil { return nil, nil, func() {}, err } @@ -217,7 +222,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, filesByURI: map[span.URI]*fileBase{}, filesByBase: map[string][]*fileBase{}, rootURI: root, - workspaceInformation: *ws, + workspaceInformation: *wsInfo, } v.importsState = &importsState{ ctx: backgroundCtx, diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index b183bc5f88f..7c143fe81ed 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1337,6 +1337,10 @@ func (s *snapshot) awaitLoaded(ctx context.Context) error { } func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { + if wsErr := s.workspace.criticalError(ctx, s); wsErr != nil { + return wsErr + } + loadErr := s.awaitLoadedAllErrors(ctx) if loadErr != nil && errors.Is(loadErr.MainError, context.Canceled) { return nil @@ -1396,32 +1400,45 @@ func (s *snapshot) awaitLoadedAllErrors(ctx context.Context) *source.CriticalErr // Do not return results until the snapshot's view has been initialized. s.AwaitInitialized(ctx) - // TODO(rstambler): Should we be more careful about returning the + // TODO(rfindley): Should we be more careful about returning the // initialization error? Is it possible for the initialization error to be // corrected without a successful reinitialization? s.mu.Lock() initializedErr := s.initializedErr s.mu.Unlock() + if initializedErr != nil { return initializedErr } + // TODO(rfindley): revisit this handling. Calling reloadWorkspace with a + // cancelled context should have the same effect, so this preemptive handling + // should not be necessary. + // + // Also: GetCriticalError ignores context cancellation errors. Should we be + // returning nil here? if ctx.Err() != nil { return &source.CriticalError{MainError: ctx.Err()} } + // TODO(rfindley): reloading is not idempotent: if we try to reload or load + // orphaned files below and fail, we won't try again. For that reason, we + // could get different results from subsequent calls to this function, which + // may cause critical errors to be suppressed. + if err := s.reloadWorkspace(ctx); err != nil { diags := s.extractGoCommandErrors(ctx, err) return &source.CriticalError{ - MainError: err, - DiagList: diags, + MainError: err, + Diagnostics: diags, } } + if err := s.reloadOrphanedFiles(ctx); err != nil { diags := s.extractGoCommandErrors(ctx, err) return &source.CriticalError{ - MainError: err, - DiagList: diags, + MainError: err, + Diagnostics: diags, } } return nil @@ -1607,7 +1624,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC defer done() var vendorChanged bool - newWorkspace, workspaceChanged, workspaceReload := s.workspace.invalidate(ctx, changes, &unappliedChanges{ + newWorkspace, workspaceChanged, workspaceReload := s.workspace.Clone(ctx, changes, &unappliedChanges{ originalSnapshot: s, changes: changes, }) @@ -2235,7 +2252,7 @@ func (s *snapshot) BuildGoplsMod(ctx context.Context) (*modfile.File, error) { return buildWorkspaceModFile(ctx, allModules, s) } -// TODO(rfindley): move this to workspacemodule.go +// TODO(rfindley): move this to workspace.go func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, fs source.FileSource) (*modfile.File, error) { file := &modfile.File{} file.AddModuleStmt("gopls-workspace") @@ -2273,8 +2290,8 @@ func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, goVersion = parsed.Go.Version } path := parsed.Module.Mod.Path - if _, ok := paths[path]; ok { - return nil, fmt.Errorf("found module %q twice in the workspace", path) + if seen, ok := paths[path]; ok { + return nil, fmt.Errorf("found module %q multiple times in the workspace, at:\n\t%q\n\t%q", path, seen, modURI) } paths[path] = modURI // If the module's path includes a major version, we expect it to have diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index e33e3400451..71d27ab909c 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -112,6 +112,9 @@ type workspaceInformation struct { environmentVariables // userGo111Module is the user's value of GO111MODULE. + // + // TODO(rfindley): is there really value in memoizing this variable? It seems + // simpler to make this a function/method. userGo111Module go111module // The value of GO111MODULE we want to run with. @@ -703,18 +706,18 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { event.Error(ctx, "initial workspace load failed", err) extractedDiags := s.extractGoCommandErrors(ctx, err) criticalErr = &source.CriticalError{ - MainError: err, - DiagList: append(modDiagnostics, extractedDiags...), + MainError: err, + Diagnostics: append(modDiagnostics, extractedDiags...), } case len(modDiagnostics) == 1: criticalErr = &source.CriticalError{ - MainError: fmt.Errorf(modDiagnostics[0].Message), - DiagList: modDiagnostics, + MainError: fmt.Errorf(modDiagnostics[0].Message), + Diagnostics: modDiagnostics, } case len(modDiagnostics) > 1: criticalErr = &source.CriticalError{ - MainError: fmt.Errorf("error loading module names"), - DiagList: modDiagnostics, + MainError: fmt.Errorf("error loading module names"), + Diagnostics: modDiagnostics, } } @@ -941,6 +944,12 @@ func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, go for k := range vars { args = append(args, k) } + // TODO(rfindley): GOWORK is not a property of the session. It may change + // when a workfile is added or removed. + // + // We need to distinguish between GOWORK values that are set by the GOWORK + // environment variable, and GOWORK values that are computed based on the + // location of a go.work file in the directory hierarchy. args = append(args, "GOWORK") inv := gocommand.Invocation{ diff --git a/internal/lsp/cache/workspace.go b/internal/lsp/cache/workspace.go index 669ce9290c9..0091be9d1e6 100644 --- a/internal/lsp/cache/workspace.go +++ b/internal/lsp/cache/workspace.go @@ -28,7 +28,7 @@ const ( legacyWorkspace = iota // non-module or single module mode goplsModWorkspace // modules provided by a gopls.mod file goWorkWorkspace // modules provided by a go.work file - fileSystemWorkspace // modules scanned from the filesystem + fileSystemWorkspace // modules found by walking the filesystem ) func (s workspaceSource) String() string { @@ -95,7 +95,12 @@ type workspace struct { // // If there is no active workspace file (a gopls.mod or go.work), newWorkspace // scans the filesystem to find modules. -func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff bool, useWsModule bool) (*workspace, error) { +// +// TODO(rfindley): newWorkspace should perhaps never fail, relying instead on +// the criticalError method to surface problems in the workspace. +// TODO(rfindley): this function should accept the GOWORK value, if specified +// by the user. +func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff, useWsModule bool) (*workspace, error) { ws := &workspace{ root: root, excludePath: excludePath, @@ -178,6 +183,28 @@ func (w *workspace) getActiveModFiles() map[span.URI]struct{} { return w.activeModFiles } +// criticalError returns a critical error related to the workspace setup. +func (w *workspace) criticalError(ctx context.Context, fs source.FileSource) (res *source.CriticalError) { + // For now, we narrowly report errors related to `go.work` files. + // + // TODO(rfindley): investigate whether other workspace validation errors + // can be consolidated here. + if w.moduleSource == goWorkWorkspace { + // We should have already built the modfile, but build here to be + // consistent about accessing w.mod after w.build. + // + // TODO(rfindley): build eagerly. Building lazily is a premature + // optimization that poses a significant burden on the code. + w.build(ctx, fs) + if w.buildErr != nil { + return &source.CriticalError{ + MainError: w.buildErr, + } + } + } + return nil +} + // modFile gets the workspace modfile associated with this workspace, // computing it if it doesn't exist. // @@ -207,9 +234,10 @@ func (w *workspace) build(ctx context.Context, fs source.FileSource) { // would not be obvious to the user how to recover. ctx = xcontext.Detach(ctx) - // If our module source is not gopls.mod, try to build the workspace module - // from modules. Fall back on the pre-existing mod file if parsing fails. - if w.moduleSource != goplsModWorkspace { + // If the module source is from the filesystem, try to build the workspace + // module from active modules discovered by scanning the filesystem. Fall + // back on the pre-existing mod file if parsing fails. + if w.moduleSource == fileSystemWorkspace { file, err := buildWorkspaceModFile(ctx, w.activeModFiles, fs) switch { case err == nil: @@ -222,6 +250,7 @@ func (w *workspace) build(ctx context.Context, fs source.FileSource) { w.buildErr = err } } + if w.mod != nil { w.wsDirs = map[span.URI]struct{}{ w.root: {}, @@ -235,18 +264,21 @@ func (w *workspace) build(ctx context.Context, fs source.FileSource) { w.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{} } } + // Ensure that there is always at least the root dir. if len(w.wsDirs) == 0 { w.wsDirs = map[span.URI]struct{}{ w.root: {}, } } + sum, err := buildWorkspaceSumFile(ctx, w.activeModFiles, fs) if err == nil { w.sum = sum } else { event.Error(ctx, "building workspace sum file", err) } + w.built = true } @@ -263,7 +295,7 @@ func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI { return dirs } -// invalidate returns a (possibly) new workspace after invalidating the changed +// Clone returns a (possibly) new workspace after invalidating the changed // files. If w is still valid in the presence of changedURIs, it returns itself // unmodified. // @@ -271,7 +303,10 @@ func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI { // Some workspace changes may affect workspace contents without requiring a // reload of metadata (for example, unsaved changes to a go.mod or go.sum // file). -func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileChange, fs source.FileSource) (_ *workspace, changed, reload bool) { +// +// TODO(rfindley): it looks wrong that we return 'reload' here. The caller +// should determine whether to reload. +func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, fs source.FileSource) (_ *workspace, changed, reload bool) { // Prevent races to w.modFile or w.wsDirs below, if w has not yet been built. w.buildMu.Lock() defer w.buildMu.Unlock() @@ -501,6 +536,10 @@ func parseGoWork(ctx context.Context, root, uri span.URI, contents []byte, fs so modURI := span.URIFromPath(filepath.Join(dir.Path, "go.mod")) modFiles[modURI] = struct{}{} } + + // TODO(rfindley): we should either not build the workspace modfile here, or + // not fail so hard. A failure in building the workspace modfile should not + // invalidate the active module paths extracted above. modFile, err := buildWorkspaceModFile(ctx, modFiles, fs) if err != nil { return nil, nil, err diff --git a/internal/lsp/cache/workspace_test.go b/internal/lsp/cache/workspace_test.go index b809ad196a6..f8651fd650b 100644 --- a/internal/lsp/cache/workspace_test.go +++ b/internal/lsp/cache/workspace_test.go @@ -298,7 +298,7 @@ replace gopls.test => ../../gopls.test2`, false}, t.Fatal(err) } } - got, gotChanged, gotReload := w.invalidate(ctx, changes, fs) + got, gotChanged, gotReload := w.Clone(ctx, changes, fs) if gotChanged != test.wantChanged { t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged) } diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 59fe1716b9b..460659f30d2 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -414,7 +414,7 @@ func (s *Server) showCriticalErrorStatus(ctx context.Context, snapshot source.Sn var errMsg string if err != nil { event.Error(ctx, "errors loading workspace", err.MainError, tag.Snapshot.Of(snapshot.ID()), tag.Directory.Of(snapshot.View().Folder())) - for _, d := range err.DiagList { + for _, d := range err.Diagnostics { s.storeDiagnostics(snapshot, d.URI, modSource, []*source.Diagnostic{d}) } errMsg = strings.ReplaceAll(err.MainError.Error(), "\n", " ") diff --git a/internal/lsp/fake/sandbox.go b/internal/lsp/fake/sandbox.go index 72b01217ae7..7efbdb3ae58 100644 --- a/internal/lsp/fake/sandbox.go +++ b/internal/lsp/fake/sandbox.go @@ -159,6 +159,9 @@ func UnpackTxt(txt string) map[string][]byte { dataMap := make(map[string][]byte) archive := txtar.Parse([]byte(txt)) for _, f := range archive.Files { + if _, ok := dataMap[f.Name]; ok { + panic(fmt.Sprintf("found file %q twice", f.Name)) + } dataMap[f.Name] = f.Data } return dataMap diff --git a/internal/lsp/regtest/expectation.go b/internal/lsp/regtest/expectation.go index 737f83da89c..32538851ee1 100644 --- a/internal/lsp/regtest/expectation.go +++ b/internal/lsp/regtest/expectation.go @@ -308,7 +308,7 @@ func OutstandingWork(title, msg string) SimpleExpectation { } return SimpleExpectation{ check: check, - description: fmt.Sprintf("outstanding work: %s", title), + description: fmt.Sprintf("outstanding work: %q containing %q", title, msg), } } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index c067fe5a4f6..f0d22c7c414 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -648,11 +648,15 @@ type Package interface { ParseMode() ParseMode } +// A CriticalError is a workspace-wide error that generally prevents gopls from +// functioning correctly. In the presence of critical errors, other diagnostics +// in the workspace may not make sense. type CriticalError struct { // MainError is the primary error. Must be non-nil. MainError error - // DiagList contains any supplemental (structured) diagnostics. - DiagList []*Diagnostic + + // Diagnostics contains any supplemental (structured) diagnostics. + Diagnostics []*Diagnostic } // An Diagnostic corresponds to an LSP Diagnostic. From b52794acc2200247cbde87ae7dfc00998c47bbd9 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 26 Jul 2022 15:28:51 -0400 Subject: [PATCH 138/723] internal/lsp/cache: simplify snapshot.Clone reinitialization logic The logic to detect whether the snapshot should be reinitialized is complicated, and had at least a couple latent bugs: - It depends on workspace.Clone to learn whether relevant mod/work files have been saved. - It naively checks for changes in vendor directories, and reinitializes if there was an initialization failure. - It copies the initializeOnce field. Pragmatically this may not matter (due to context cancellation, for example), but as a matter of best practices we should not copy this field. It could lead to missing an initialization on the new snapshot. - Writing initializeOnce was technically racy. Furthermore, due to vendored file handling being interleaved with other changes, it was possible that we detect that the snapshot should be reinitialied only after we have processed other changes, and that processing of other changes depended on knowing whether the snapshot will be reinitialized. Simplify this by - Replacing 'initializeOnce' with an 'initialized' bool, which is guarded with snapshot.mu. We were relying on the view initialization semaphore to prevent races anyway, so there is no need for a sync.Once to prevent multiple in-flight initializations. - Removing the 'changed' result from workspace.Clone: it shouldn't actualy matter for snapshot.Clone (only reinit matters), and in any case can be derived by comparing the new workspace with the old. - Detect changes to vendored files before processing the rest of the changes. Change-Id: I3624a46d4be9c054a6f719f20549599986b64cbd Reviewed-on: https://go-review.googlesource.com/c/tools/+/419499 Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/cache/session.go | 1 - internal/lsp/cache/snapshot.go | 58 ++++++++++++++++------------ internal/lsp/cache/view.go | 25 +++++++++--- internal/lsp/cache/workspace.go | 29 ++++++++------ internal/lsp/cache/workspace_test.go | 7 ++-- 5 files changed, 74 insertions(+), 46 deletions(-) diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 88690648286..372e8e97956 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -235,7 +235,6 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, view: v, backgroundCtx: backgroundCtx, cancel: cancel, - initializeOnce: &sync.Once{}, store: s.cache.store, packages: persistent.NewMap(packageKeyLessInterface), meta: &metadataGraph{}, diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 7c143fe81ed..6d3ead5c371 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -55,12 +55,11 @@ type snapshot struct { refcount sync.WaitGroup // number of references destroyedBy *string // atomically set to non-nil in Destroy once refcount = 0 - // The snapshot's initialization state is controlled by the fields below. - // - // initializeOnce guards snapshot initialization. Each snapshot is - // initialized at most once: reinitialization is triggered on later snapshots - // by invalidating this field. - initializeOnce *sync.Once + // initialized reports whether the snapshot has been initialized. Concurrent + // initialization is guarded by the view.initializationSema. Each snapshot is + // initialized at most once: concurrent initialization is guarded by + // view.initializationSema. + initialized bool // initializedErr holds the last error resulting from initialization. If // initialization fails, we only retry when the the workspace modules change, // to avoid too many go/packages calls. @@ -1593,6 +1592,13 @@ func contains(views []*View, view *View) bool { return false } +// TODO(golang/go#53756): this function needs to consider more than just the +// absolute URI, for example: +// - the position of /vendor/ with respect to the relevant module root +// - whether or not go.work is in use (as vendoring isn't supported in workspace mode) +// +// Most likely, each call site of inVendor needs to be reconsidered to +// understand and correctly implement the desired behavior. func inVendor(uri span.URI) bool { if !strings.Contains(string(uri), "/vendor/") { return false @@ -1623,8 +1629,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC ctx, done := event.Start(ctx, "snapshot.clone") defer done() - var vendorChanged bool - newWorkspace, workspaceChanged, workspaceReload := s.workspace.Clone(ctx, changes, &unappliedChanges{ + newWorkspace, reinit := s.workspace.Clone(ctx, changes, &unappliedChanges{ originalSnapshot: s, changes: changes, }) @@ -1632,6 +1637,17 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC s.mu.Lock() defer s.mu.Unlock() + // If there is an initialization error and a vendor directory changed, try to + // reinit. + if s.initializedErr != nil { + for uri := range changes { + if inVendor(uri) { + reinit = true + break + } + } + } + bgCtx, cancel := context.WithCancel(bgCtx) result := &snapshot{ id: s.id + 1, @@ -1640,7 +1656,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC backgroundCtx: bgCtx, cancel: cancel, builtin: s.builtin, - initializeOnce: s.initializeOnce, + initialized: s.initialized, initializedErr: s.initializedErr, packages: s.packages.Clone(), isActivePackageCache: s.isActivePackageCache.Clone(), @@ -1658,6 +1674,13 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC knownSubdirs: s.knownSubdirs.Clone(), workspace: newWorkspace, } + + // The snapshot should be initialized if either s was uninitialized, or we've + // detected a change that triggers reinitialization. + if reinit { + result.initialized = false + } + // Create a lease on the new snapshot. // (Best to do this early in case the code below hides an // incref/decref operation that might destroy it prematurely.) @@ -1704,7 +1727,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC directIDs := map[PackageID]bool{} // Invalidate all package metadata if the workspace module has changed. - if workspaceReload { + if reinit { for k := range s.meta.metadata { directIDs[k] = true } @@ -1717,12 +1740,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC anyFileAdded := false // adding a file can resolve missing dependencies for uri, change := range changes { - // Maybe reinitialize the view if we see a change in the vendor - // directory. - if inVendor(uri) { - vendorChanged = true - } - // The original FileHandle for this URI is cached on the snapshot. originalFH, _ := s.files.Get(uri) var originalOpen, newOpen bool @@ -1740,7 +1757,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC invalidateMetadata, pkgFileChanged, importDeleted = metadataChanges(ctx, s, originalFH, change.fileHandle) } - invalidateMetadata = invalidateMetadata || forceReloadMetadata || workspaceReload + invalidateMetadata = invalidateMetadata || forceReloadMetadata || reinit anyImportDeleted = anyImportDeleted || importDeleted // Mark all of the package IDs containing the given file. @@ -1961,13 +1978,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC if workspaceModeChanged { result.workspacePackages = map[PackageID]PackagePath{} } - - // The snapshot may need to be reinitialized. - if workspaceReload || vendorChanged { - if workspaceChanged || result.initializedErr != nil { - result.initializeOnce = &sync.Once{} - } - } result.dumpWorkspace("clone") return result, release } diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 71d27ab909c..6073e932303 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -88,6 +88,10 @@ type View struct { // initializationSema is used limit concurrent initialization of snapshots in // the view. We use a channel instead of a mutex to avoid blocking when a // context is canceled. + // + // This field (along with snapshot.initialized) guards against duplicate + // initialization of snapshots. Do not change it without adjusting snapshot + // accordingly. initializationSema chan struct{} // rootURI is the rootURI directory of this view. If we are in GOPATH mode, this @@ -630,18 +634,23 @@ func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) { <-s.view.initializationSema }() - if s.initializeOnce == nil { + s.mu.Lock() + initialized := s.initialized + s.mu.Unlock() + + if initialized { return } - s.initializeOnce.Do(func() { - s.loadWorkspace(ctx, firstAttempt) - s.collectAllKnownSubdirs(ctx) - }) + + s.loadWorkspace(ctx, firstAttempt) + s.collectAllKnownSubdirs(ctx) } func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { defer func() { - s.initializeOnce = nil + s.mu.Lock() + s.initialized = true + s.mu.Unlock() if firstAttempt { close(s.view.initialWorkspaceLoad) } @@ -658,7 +667,11 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { Message: err.Error(), }) } + + // TODO(rFindley): we should only locate template files on the first attempt, + // or guard it via a different mechanism. s.locateTemplateFiles(ctx) + if len(s.workspace.getActiveModFiles()) > 0 { for modURI := range s.workspace.getActiveModFiles() { fh, err := s.GetFile(ctx, modURI) diff --git a/internal/lsp/cache/workspace.go b/internal/lsp/cache/workspace.go index 0091be9d1e6..9182cb9ad58 100644 --- a/internal/lsp/cache/workspace.go +++ b/internal/lsp/cache/workspace.go @@ -299,19 +299,19 @@ func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI { // files. If w is still valid in the presence of changedURIs, it returns itself // unmodified. // -// The returned changed and reload flags control the level of invalidation. -// Some workspace changes may affect workspace contents without requiring a -// reload of metadata (for example, unsaved changes to a go.mod or go.sum -// file). +// The returned needReinit flag indicates to the caller that the workspace +// needs to be reinitialized (because a relevant go.mod or go.work file has +// been changed). // -// TODO(rfindley): it looks wrong that we return 'reload' here. The caller -// should determine whether to reload. -func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, fs source.FileSource) (_ *workspace, changed, reload bool) { +// TODO(rfindley): it looks wrong that we return 'needReinit' here. The caller +// should determine whether to re-initialize.. +func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, fs source.FileSource) (_ *workspace, needReinit bool) { // Prevent races to w.modFile or w.wsDirs below, if w has not yet been built. w.buildMu.Lock() defer w.buildMu.Unlock() // Clone the workspace. This may be discarded if nothing changed. + changed := false result := &workspace{ root: w.root, moduleSource: w.moduleSource, @@ -335,7 +335,7 @@ func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, // determine which modules we care about. If go.work/gopls.mod has changed // we need to either re-read it if it exists or walk the filesystem if it // has been deleted. go.work should override the gopls.mod if both exist. - changed, reload = handleWorkspaceFileChanges(ctx, result, changes, fs) + changed, needReinit = handleWorkspaceFileChanges(ctx, result, changes, fs) // Next, handle go.mod changes that could affect our workspace. for uri, change := range changes { // Otherwise, we only care about go.mod files in the workspace directory. @@ -344,7 +344,7 @@ func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, } changed = true active := result.moduleSource != legacyWorkspace || source.CompareURI(modURI(w.root), uri) == 0 - reload = reload || (active && change.fileHandle.Saved()) + needReinit = needReinit || (active && change.fileHandle.Saved()) // Don't mess with the list of mod files if using go.work or gopls.mod. if result.moduleSource == goplsModWorkspace || result.moduleSource == goWorkWorkspace { continue @@ -374,14 +374,14 @@ func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, // Only changes to active go.sum files actually cause the workspace to // change. changed = true - reload = reload || change.fileHandle.Saved() + needReinit = needReinit || change.fileHandle.Saved() } if !changed { - return w, false, false + return w, false } - return result, changed, reload + return result, needReinit } // handleWorkspaceFileChanges handles changes related to a go.work or gopls.mod @@ -416,6 +416,11 @@ func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[ // An unparseable file should not invalidate the workspace: // nothing good could come from changing the workspace in // this case. + // + // TODO(rfindley): well actually, it could potentially lead to a better + // critical error. Evaluate whether we can unify this case with the + // error returned by newWorkspace, without needlessly invalidating + // metadata. event.Error(ctx, fmt.Sprintf("parsing %s", filepath.Base(uri.Filename())), err) } else { // only update the modfile if it parsed. diff --git a/internal/lsp/cache/workspace_test.go b/internal/lsp/cache/workspace_test.go index f8651fd650b..871e4bb9b98 100644 --- a/internal/lsp/cache/workspace_test.go +++ b/internal/lsp/cache/workspace_test.go @@ -298,12 +298,13 @@ replace gopls.test => ../../gopls.test2`, false}, t.Fatal(err) } } - got, gotChanged, gotReload := w.Clone(ctx, changes, fs) + got, gotReinit := w.Clone(ctx, changes, fs) + gotChanged := got != w if gotChanged != test.wantChanged { t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged) } - if gotReload != test.wantReload { - t.Errorf("w.invalidate(): got reload %t, want %t", gotReload, test.wantReload) + if gotReinit != test.wantReload { + t.Errorf("w.invalidate(): got reload %t, want %t", gotReinit, test.wantReload) } checkState(ctx, t, fs, rel, got, test.finalState) } From e02e98a0378f7ab2dc934b10d293d532b9d90e9b Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 26 Jul 2022 16:26:18 -0400 Subject: [PATCH 139/723] internal/lsp/cache: allow network whenever reloading the workspace Per the explanation at golang/go#54069, allow network access whenever loading the workspace. Enable one test that exercises this behavior. More tests will be added in subsequent CLs. Updates golang/go#47540 Updates golang/go#53676 Updates golang/go#54069 Change-Id: I9c3bb19d36702bc6b8051bee6b7cddaec5b97c0c Reviewed-on: https://go-review.googlesource.com/c/tools/+/419500 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro --- .../internal/regtest/modfile/modfile_test.go | 4 ++- .../internal/regtest/workspace/broken_test.go | 25 +++++++--------- .../regtest/workspace/workspace_test.go | 29 ++++++------------- internal/lsp/cache/view.go | 2 +- 4 files changed, 24 insertions(+), 36 deletions(-) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index e6f76d4d514..79441c1fe79 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -576,7 +576,9 @@ func TestUnknownRevision(t *testing.T) { t.Skipf("skipping test that fails for unknown reasons on plan9; see https://go.dev/issue/50477") } - testenv.NeedsGo1Point(t, 14) + // This test fails at go1.14 and go1.15 due to differing Go command behavior. + // This was not significantly investigated. + testenv.NeedsGo1Point(t, 16) const unknown = ` -- a/go.mod -- diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index a818fe40bcf..cd5127ff3a8 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -5,6 +5,7 @@ package workspace import ( + "strings" "testing" "golang.org/x/tools/internal/lsp" @@ -96,25 +97,21 @@ const CompleteMe = 222 OutstandingWork(lsp.WorkspaceLoadFailure, `found module "example.com/foo" multiple times in the workspace`), ) - /* TODO(golang/go#54069): once we allow network when reinitializing the - * workspace, we should be able to fix the error here. - - // Remove the redundant vendored copy of example.com. - env.WriteWorkspaceFile("go.work", `go 1.18 + // Remove the redundant vendored copy of example.com. + env.WriteWorkspaceFile("go.work", `go 1.18 use ( ./package1 ./package2 ./package2/vendor/example.com/foo ) `) - env.Await(NoOutstandingWork()) - - // Check that definitions in package1 go to the copy vendored in package2. - location, _ := env.GoToDefinition("package1/main.go", env.RegexpSearch("package1/main.go", "CompleteMe")) - const wantLocation = "package2/vendor/example.com/foo/foo.go" - if !strings.HasSuffix(location, wantLocation) { - t.Errorf("got definition of CompleteMe at %q, want %q", location, wantLocation) - } - */ + env.Await(NoOutstandingWork()) + + // Check that definitions in package1 go to the copy vendored in package2. + location, _ := env.GoToDefinition("package1/main.go", env.RegexpSearch("package1/main.go", "CompleteMe")) + const wantLocation = "package2/vendor/example.com/foo/foo.go" + if !strings.HasSuffix(location, wantLocation) { + t.Errorf("got definition of CompleteMe at %q, want %q", location, wantLocation) + } }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index bc576226e84..d7a0c7f7e1d 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -576,21 +576,16 @@ require ( replace a.com => %s/moda/a replace b.com => %s/modb `, workdir, workdir)) - env.Await(env.DoneWithChangeWatchedFiles()) - // Check that go.mod diagnostics picked up the newly active mod file. - // The local version of modb has an extra dependency we need to download. - env.OpenFile("modb/go.mod") - env.Await(env.DoneWithOpen()) - var d protocol.PublishDiagnosticsParams + // As of golang/go#54069, writing a gopls.mod to the workspace triggers a + // workspace reload. env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("modb/go.mod", `require example.com v1.2.3`, "has not been downloaded"), - ReadDiagnostics("modb/go.mod", &d), + env.DoneWithChangeWatchedFiles(), + env.DiagnosticAtRegexp("modb/b/b.go", "x"), ), ) - env.ApplyQuickFixes("modb/go.mod", d.Diagnostics) - env.Await(env.DiagnosticAtRegexp("modb/b/b.go", "x")) + // Jumping to definition should now go to b.com in the workspace. if err := checkHelloLocation("modb/b/b.go"); err != nil { t.Fatal(err) @@ -717,21 +712,15 @@ use ( ./modb ) `) - env.Await(env.DoneWithChangeWatchedFiles()) - // Check that go.mod diagnostics picked up the newly active mod file. - // The local version of modb has an extra dependency we need to download. - env.OpenFile("modb/go.mod") - env.Await(env.DoneWithOpen()) - var d protocol.PublishDiagnosticsParams + // As of golang/go#54069, writing go.work to the workspace triggers a + // workspace reload. env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("modb/go.mod", `require example.com v1.2.3`, "has not been downloaded"), - ReadDiagnostics("modb/go.mod", &d), + env.DoneWithChangeWatchedFiles(), + env.DiagnosticAtRegexp("modb/b/b.go", "x"), ), ) - env.ApplyQuickFixes("modb/go.mod", d.Diagnostics) - env.Await(env.DiagnosticAtRegexp("modb/b/b.go", "x")) // Jumping to definition should now go to b.com in the workspace. if err := checkHelloLocation("modb/b/b.go"); err != nil { diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 6073e932303..f974cfcf571 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -700,7 +700,7 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { if len(scopes) > 0 { scopes = append(scopes, PackagePath("builtin")) } - err := s.load(ctx, firstAttempt, scopes...) + err := s.load(ctx, true, scopes...) // If the context is canceled on the first attempt, loading has failed // because the go command has timed out--that should be a critical error. From 98bfcd1bee36f151b1083a3d3c8f9fe55bd90dfe Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 27 Jul 2022 15:59:52 -0400 Subject: [PATCH 140/723] internal/memoize: fix race in Store.Promise When releasing a promise, there was a theoretical race whereby a promise's refcount could be incremented before Store.promisesMu was acquired and the promise deleted. We could fix this by double-checking after acquiring Store.promisesMu, but it seemed simpler to just guard Promise.refcount with Store.promisesMu, and skip using atomics. We already lock promisesMu when acquiring the promise, and so locking when releasing should not significantly affect our level of contention. Additionally, make it a panic to call the returned release function more than once, and document this behavior. Change-Id: I1135b558b1f13f2b063dcaad129a432c22da0b28 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419504 Reviewed-by: Bryan Mills Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/memoize/memoize.go | 36 ++++++++++++++++++++------------ internal/memoize/memoize_test.go | 21 +++++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 150c79af852..e56af3bb45b 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -65,6 +65,10 @@ type RefCounted interface { type Promise struct { debug string // for observability + // refcount is the reference count in the containing Store, used by + // Store.Promise. It is guarded by Store.promisesMu on the containing Store. + refcount int32 + mu sync.Mutex // A Promise starts out IDLE, waiting for something to demand @@ -91,8 +95,6 @@ type Promise struct { function Function // value is set in completed state. value interface{} - - refcount int32 // accessed using atomic load/store } // NewPromise returns a promise for the future result of calling the @@ -267,11 +269,13 @@ func NewStore(policy EvictionPolicy) *Store { // Promise returns a reference-counted promise for the future result of // calling the specified function. // -// Calls to Promise with the same key return the same promise, -// incrementing its reference count. The caller must call the -// returned function to decrement the promise's reference count when -// it is no longer needed. Once the last reference has been released, -// the promise is removed from the store. +// Calls to Promise with the same key return the same promise, incrementing its +// reference count. The caller must call the returned function to decrement +// the promise's reference count when it is no longer needed. The returned +// function must not be called more than once. +// +// Once the last reference has been released, the promise is removed from the +// store. func (store *Store) Promise(key interface{}, function Function) (*Promise, func()) { store.promisesMu.Lock() p, ok := store.promises[key] @@ -282,18 +286,24 @@ func (store *Store) Promise(key interface{}, function Function) (*Promise, func( } store.promises[key] = p } - atomic.AddInt32(&p.refcount, 1) + p.refcount++ store.promisesMu.Unlock() + var released int32 release := func() { - // TODO(rfindley): this looks racy: it's possible that the refcount is - // incremented before we grab the lock. - if atomic.AddInt32(&p.refcount, -1) == 0 && store.evictionPolicy != NeverEvict { - store.promisesMu.Lock() + if !atomic.CompareAndSwapInt32(&released, 0, 1) { + panic("release called more than once") + } + store.promisesMu.Lock() + + p.refcount-- + if p.refcount == 0 && store.evictionPolicy != NeverEvict { + // Inv: if p.refcount > 0, then store.promises[key] == p. delete(store.promises, key) - store.promisesMu.Unlock() } + store.promisesMu.Unlock() } + return p, release } diff --git a/internal/memoize/memoize_test.go b/internal/memoize/memoize_test.go index 3550f1eb144..c54572d59ca 100644 --- a/internal/memoize/memoize_test.go +++ b/internal/memoize/memoize_test.go @@ -143,3 +143,24 @@ func TestPromiseDestroyedWhileRunning(t *testing.T) { t.Errorf("Get() = %v, want %v", got, v) } } + +func TestDoubleReleasePanics(t *testing.T) { + var store memoize.Store + _, release := store.Promise("key", func(ctx context.Context, _ interface{}) interface{} { return 0 }) + + panicked := false + + func() { + defer func() { + if recover() != nil { + panicked = true + } + }() + release() + release() + }() + + if !panicked { + t.Errorf("calling release() twice did not panic") + } +} From d01bb2ff912c348cc37095f3722bf518cba26078 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 28 Jul 2022 13:28:39 -0400 Subject: [PATCH 141/723] internal/lsp/source: document the handling of GOPRIVATE for linkTarget Document that modules matching GOPRIVATE will not be linked. Updates golang/vscode-go#2362 Change-Id: I7e2447bb50a2cd0d7d394f8589ccd4498f889048 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419979 Run-TryBot: Robert Findley gopls-CI: kokoro Auto-Submit: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim --- gopls/doc/settings.md | 3 +++ internal/lsp/source/api_json.go | 2 +- internal/lsp/source/options.go | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 6d923b71e36..e04436a34d7 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -363,6 +363,9 @@ It might be one of: If company chooses to use its own `godoc.org`, its address can be used as well. +Modules matching the GOPRIVATE environment variable will not have +documentation links in hover. + Default: `"pkg.go.dev"`. ##### **linksInHover** *bool* diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 94d0f12b4a8..807f496a6bb 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -116,7 +116,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "linkTarget", Type: "string", - Doc: "linkTarget controls where documentation links go.\nIt might be one of:\n\n* `\"godoc.org\"`\n* `\"pkg.go.dev\"`\n\nIf company chooses to use its own `godoc.org`, its address can be used as well.\n", + Doc: "linkTarget controls where documentation links go.\nIt might be one of:\n\n* `\"godoc.org\"`\n* `\"pkg.go.dev\"`\n\nIf company chooses to use its own `godoc.org`, its address can be used as well.\n\nModules matching the GOPRIVATE environment variable will not have\ndocumentation links in hover.\n", Default: "\"pkg.go.dev\"", Hierarchy: "ui.documentation", }, diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 8fa78ae2610..1bb135dd574 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -350,6 +350,9 @@ type DocumentationOptions struct { // * `"pkg.go.dev"` // // If company chooses to use its own `godoc.org`, its address can be used as well. + // + // Modules matching the GOPRIVATE environment variable will not have + // documentation links in hover. LinkTarget string // LinksInHover toggles the presence of links to documentation in hover. From 8ea56879871ae7f8d2168cf4eab1bb57e7fe98b2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 27 Jul 2022 14:57:02 -0400 Subject: [PATCH 142/723] internal/lsp/regtest: remove arbitrary timeout for closing the editor Following up on some post-submit feedback in CL 417583. Fixes golang/go#53819 Change-Id: Iebb1e6496ab1d6fde8961d8617d0b63e19c7033b Reviewed-on: https://go-review.googlesource.com/c/tools/+/419503 Reviewed-by: Bryan Mills TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- internal/lsp/regtest/runner.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go index 1d22aaa9d85..df1cc05e7e8 100644 --- a/internal/lsp/regtest/runner.go +++ b/internal/lsp/regtest/runner.go @@ -391,13 +391,11 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio // For tests that failed due to a timeout, don't fail to shutdown // because ctx is done. // - // golang/go#53820: now that we await the completion of ongoing work in - // shutdown, we must allow a significant amount of time for ongoing go - // command invocations to exit. - ctx, cancel := context.WithTimeout(xcontext.Detach(ctx), 30*time.Second) - defer cancel() - if err := env.Editor.Close(ctx); err != nil { - pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) + // There is little point to setting an arbitrary timeout for closing + // the editor: in general we want to clean up before proceeding to the + // next test, and if there is a deadlock preventing closing it will + // eventually be handled by the `go test` timeout. + if err := env.Editor.Close(xcontext.Detach(ctx)); err != nil { t.Errorf("closing editor: %v", err) } }() From f560bc877f30412e048ba6da827d032b46f8302f Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 26 Jul 2022 17:58:48 -0400 Subject: [PATCH 143/723] internal/lsp/cache: don't set context cancellation as a critical err Fix a tricky race in gopls/internal/regtest/diagnostics.Test_Issue38211: When reloading the workspace, we can encounter context cancellation if the snapshot is cancelled, and can write this cancellation as a critical error *before* the context is cloned, leading to a state where there is a critical error that won't go away. This should resolve test flakes reported in golang/go#44098. For golang/go#44098 Change-Id: I41c0f49b2fe999131f4c31166e69b2cde85470b7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419502 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Bryan Mills --- internal/lsp/cache/view.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index f974cfcf571..0a64b76306e 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -656,9 +656,14 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { } }() - // If we have multiple modules, we need to load them by paths. - var scopes []interface{} - var modDiagnostics []*source.Diagnostic + // TODO(rFindley): we should only locate template files on the first attempt, + // or guard it via a different mechanism. + s.locateTemplateFiles(ctx) + + // Collect module paths to load by parsing go.mod files. If a module fails to + // parse, capture the parsing failure as a critical diagnostic. + var scopes []interface{} // scopes to load + var modDiagnostics []*source.Diagnostic // diagnostics for broken go.mod files addError := func(uri span.URI, err error) { modDiagnostics = append(modDiagnostics, &source.Diagnostic{ URI: uri, @@ -668,20 +673,22 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { }) } - // TODO(rFindley): we should only locate template files on the first attempt, - // or guard it via a different mechanism. - s.locateTemplateFiles(ctx) - if len(s.workspace.getActiveModFiles()) > 0 { for modURI := range s.workspace.getActiveModFiles() { + // Be careful not to add context cancellation errors as critical module + // errors. fh, err := s.GetFile(ctx, modURI) if err != nil { - addError(modURI, err) + if ctx.Err() == nil { + addError(modURI, err) + } continue } parsed, err := s.ParseMod(ctx, fh) if err != nil { - addError(modURI, err) + if ctx.Err() == nil { + addError(modURI, err) + } continue } if parsed.File == nil || parsed.File.Module == nil { From 9580c84d5753ec8548a100560e8a2d94aeb99972 Mon Sep 17 00:00:00 2001 From: Dylan Le Date: Thu, 21 Jul 2022 15:23:17 -0400 Subject: [PATCH 144/723] internal/lsp: Check if user's editor support rename operation Change-Id: Iadda768e93eda1d53fa00a5ff8a28013a575ef57 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419774 Run-TryBot: Dylan Le Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/regtest/misc/rename_test.go | 40 ++++++++++++++++++++++ internal/lsp/fake/editor.go | 20 +++++------ internal/lsp/source/options.go | 4 +++ internal/lsp/source/rename.go | 23 +++++++++++++ 4 files changed, 77 insertions(+), 10 deletions(-) diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 121b70725b4..65a9ecb52eb 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -8,9 +8,49 @@ import ( "strings" "testing" + "golang.org/x/tools/internal/lsp/protocol" . "golang.org/x/tools/internal/lsp/regtest" ) +func TestPrepareRenamePackage(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- main.go -- +package main + +import ( + "fmt" +) + +func main() { + fmt.Println(1) +} +` + const wantErr = "can't rename packages: LSP client does not support file renaming" + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + pos := env.RegexpSearch("main.go", `main`) + tdpp := protocol.TextDocumentPositionParams{ + TextDocument: env.Editor.TextDocumentIdentifier("main.go"), + Position: pos.ToProtocolPosition(), + } + params := &protocol.PrepareRenameParams{ + TextDocumentPositionParams: tdpp, + } + _, err := env.Editor.Server.PrepareRename(env.Ctx, params) + if err == nil { + t.Errorf("missing can't rename package error from PrepareRename") + } + + if err.Error() != wantErr { + t.Errorf("got %v, want %v", err.Error(), wantErr) + } + }) +} + // Test for golang/go#47564. func TestRenameInTestVariant(t *testing.T) { const files = ` diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index 240e35cffa3..a07e9e7c661 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -428,7 +428,7 @@ func (e *Editor) CloseBuffer(ctx context.Context, path string) error { if e.Server != nil { if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), }); err != nil { return fmt.Errorf("DidClose: %w", err) } @@ -439,7 +439,7 @@ func (e *Editor) CloseBuffer(ctx context.Context, path string) error { return nil } -func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier { +func (e *Editor) TextDocumentIdentifier(path string) protocol.TextDocumentIdentifier { return protocol.TextDocumentIdentifier{ URI: e.sandbox.Workdir.URI(path), } @@ -471,7 +471,7 @@ func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) erro includeText = syncOptions.Save.IncludeText } - docID := e.textDocumentIdentifier(buf.path) + docID := e.TextDocumentIdentifier(buf.path) if e.Server != nil { if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{ TextDocument: docID, @@ -693,7 +693,7 @@ func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty params := &protocol.DidChangeTextDocumentParams{ TextDocument: protocol.VersionedTextDocumentIdentifier{ Version: int32(buf.version), - TextDocumentIdentifier: e.textDocumentIdentifier(buf.path), + TextDocumentIdentifier: e.TextDocumentIdentifier(buf.path), }, ContentChanges: evts, } @@ -1008,7 +1008,7 @@ func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens return nil, fmt.Errorf("buffer %q is not open", path) } params := &protocol.CodeLensParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), } lens, err := e.Server.CodeLens(ctx, params) if err != nil { @@ -1030,7 +1030,7 @@ func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protoco } params := &protocol.CompletionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), Position: pos.ToProtocolPosition(), }, } @@ -1080,7 +1080,7 @@ func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHi return nil, fmt.Errorf("buffer %q is not open", path) } params := &protocol.InlayHintParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), } hints, err := e.Server.InlayHint(ctx, params) if err != nil { @@ -1102,7 +1102,7 @@ func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protoc } params := &protocol.ReferenceParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), Position: pos.ToProtocolPosition(), }, Context: protocol.ReferenceContext{ @@ -1121,7 +1121,7 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin return nil } params := &protocol.RenameParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), Position: pos.ToProtocolPosition(), NewName: newName, } @@ -1195,7 +1195,7 @@ func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Rang return nil, fmt.Errorf("buffer %q is not open", path) } params := &protocol.CodeActionParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), Context: protocol.CodeActionContext{ Diagnostics: diagnostics, }, diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 1bb135dd574..11ba1576632 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -204,6 +204,7 @@ type ClientOptions struct { RelatedInformationSupported bool CompletionTags bool CompletionDeprecated bool + SupportedResourceOperations []protocol.ResourceOperationKind } // ServerOptions holds LSP-specific configuration that is provided by the @@ -701,6 +702,9 @@ func SetOptions(options *Options, opts interface{}) OptionResults { func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) { // Check if the client supports snippets in completion items. + if caps.Workspace.WorkspaceEdit != nil { + o.SupportedResourceOperations = caps.Workspace.WorkspaceEdit.ResourceOperations + } if c := caps.TextDocument.Completion; c.CompletionItem.SnippetSupport { o.InsertTextFormat = protocol.SnippetTextFormat } diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go index 503422aa906..b6f0e6531e2 100644 --- a/internal/lsp/source/rename.go +++ b/internal/lsp/source/rename.go @@ -49,6 +49,29 @@ 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 Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) { + fileRenameSupported := false + for _, op := range snapshot.View().Options().SupportedResourceOperations { + if op == protocol.Rename { + fileRenameSupported = true + break + } + } + + // Find position of the package name declaration + pgf, err := snapshot.ParseGo(ctx, f, ParseFull) + if err != nil { + return nil, err, err + } + inPackageName, err := isInPackageName(ctx, snapshot, f, pgf, pp) + if err != nil { + return nil, err, err + } + + if inPackageName && !fileRenameSupported { + err := errors.New("can't rename packages: LSP client does not support file renaming") + return nil, err, err + } + ctx, done := event.Start(ctx, "source.PrepareRename") defer done() From 9f65685098805b05089601f777596720ebe45ef4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 28 Jul 2022 15:01:48 -0400 Subject: [PATCH 145/723] internal/lsp/source: enable the new diff with allExperiments When enabling all experiments (done by VS Code nightly), switch to the checked version of the new diff implementation. Also remove some experimental settings that are now on by default. Updates golang/go#52967 Change-Id: Id272c4a646006a739e49d48f0f09b2f8b0982bab Reviewed-on: https://go-review.googlesource.com/c/tools/+/419981 gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Peter Weinberger TryBot-Result: Gopher Robot --- internal/lsp/source/options.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 11ba1576632..a695feaf264 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -802,10 +802,9 @@ func (o *Options) AddStaticcheckAnalyzer(a *analysis.Analyzer, enabled bool, sev // should be enabled in enableAllExperimentMaps. func (o *Options) EnableAllExperiments() { o.SemanticTokens = true - o.ExperimentalPostfixCompletions = true o.ExperimentalUseInvalidMetadata = true o.ExperimentalWatchedFileDelay = 50 * time.Millisecond - o.SymbolMatcher = SymbolFastFuzzy + o.NewDiff = "checked" } func (o *Options) enableAllExperimentMaps() { From bd3f524777d3d52754343e6fee7528e4d5b2295a Mon Sep 17 00:00:00 2001 From: Dylan Le Date: Fri, 29 Jul 2022 16:50:30 -0400 Subject: [PATCH 146/723] internal/lsp: rename all the package names in the renamed package This CL contains a partial implementation of package renaming. Currently gopls is only able to rename references to the renaming package within the renaming package itself. prepareRename is still expected to return an error for renaming a package. For golang/go#41567 Change-Id: I3683a0a7128bba7620ef30db528f45b753e6c08f Reviewed-on: https://go-review.googlesource.com/c/tools/+/420219 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Dylan Le --- gopls/internal/regtest/misc/rename_test.go | 36 ++++++++++++++++++++++ internal/lsp/source/rename.go | 36 ++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 65a9ecb52eb..1d980d90aa9 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -10,6 +10,7 @@ import ( "golang.org/x/tools/internal/lsp/protocol" . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" ) func TestPrepareRenamePackage(t *testing.T) { @@ -51,6 +52,41 @@ func main() { }) } +func TestRenamePackageInRenamedPackage(t *testing.T) { + // Failed at Go 1.13; not investigated + testenv.NeedsGo1Point(t, 14) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- main.go -- +package main + +import ( + "fmt" + "a.go" +) + +func main() { + fmt.Println(a.C) +} +-- a.go -- +package main + +const C = 1 +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + pos := env.RegexpSearch("main.go", "main") + env.Rename("main.go", pos, "pkg") + + // Check if the new package name exists. + env.RegexpSearch("main.go", "package pkg") + env.RegexpSearch("a.go", "package pkg") + }) +} + // Test for golang/go#47564. func TestRenameInTestVariant(t *testing.T) { const files = ` diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go index b6f0e6531e2..6bbe91afae6 100644 --- a/internal/lsp/source/rename.go +++ b/internal/lsp/source/rename.go @@ -118,6 +118,41 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, ctx, done := event.Start(ctx, "source.Rename") defer done() + pgf, err := s.ParseGo(ctx, f, ParseFull) + if err != nil { + return nil, err + } + inPackageName, err := isInPackageName(ctx, s, f, pgf, pp) + if err != nil { + return nil, err + } + + if inPackageName { + renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckAll, NarrowestPackage) + if err != nil { + return nil, err + } + + result := make(map[span.URI][]protocol.TextEdit) + // Rename internal references to the package in the renaming package + // Todo(dle): need more investigation on case when pkg.GoFiles != pkg.CompiledGoFiles if using cgo. + for _, f := range renamingPkg.CompiledGoFiles() { + pkgNameMappedRange := NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()) + rng, err := pkgNameMappedRange.Range() + if err != nil { + return nil, err + } + result[f.URI] = []protocol.TextEdit{ + { + Range: rng, + NewText: newName, + }, + } + } + + return result, nil + } + qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) if err != nil { return nil, err @@ -182,6 +217,7 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, if err != nil { return nil, err } + result := make(map[span.URI][]protocol.TextEdit) for uri, edits := range changes { // These edits should really be associated with FileHandles for maximal correctness. From c7f11917cb9729a0dbe91f07206f2e12864fa550 Mon Sep 17 00:00:00 2001 From: David Chase Date: Mon, 1 Aug 2022 10:23:05 -0400 Subject: [PATCH 147/723] go/internal/gcimporter: set underlying types in proper order; flatten imports These are copied from go/internal, for processing export data from unified IR compilation. The underlying types order problem appeared in google-internal testing. If the run-later functions are run in the wrong order, type definitions won't resolve properly. Flatten imports makes the unified IR export/import match the behavior of export data from older compilers, though it's not clear that this behavior was intended, it is now expected. See https://go.dev/cl/419996 and https://go.dev/cl/419596 Change-Id: I4197fe9e93ee07eb7f24597ba9157ce083a1d086 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420534 TryBot-Result: Gopher Robot Reviewed-by: Matthew Dempsky Run-TryBot: David Chase gopls-CI: kokoro --- go/internal/gcimporter/ureader_yes.go | 47 +++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/go/internal/gcimporter/ureader_yes.go b/go/internal/gcimporter/ureader_yes.go index 3c1a4375435..add7bf8e3e3 100644 --- a/go/internal/gcimporter/ureader_yes.go +++ b/go/internal/gcimporter/ureader_yes.go @@ -36,6 +36,8 @@ type pkgReader struct { // laterFns holds functions that need to be invoked at the end of // import reading. laterFns []func() + // laterFors is used in case of 'type A B' to ensure that B is processed before A. + laterFors map[types.Type]int } // later adds a function to be invoked at the end of import reading. @@ -63,6 +65,15 @@ func UImportData(fset *token.FileSet, imports map[string]*types.Package, data [] return } +// laterFor adds a function to be invoked at the end of import reading, and records the type that function is finishing. +func (pr *pkgReader) laterFor(t types.Type, fn func()) { + if pr.laterFors == nil { + pr.laterFors = make(map[types.Type]int) + } + pr.laterFors[t] = len(pr.laterFns) + pr.laterFns = append(pr.laterFns, fn) +} + // readUnifiedPackage reads a package description from the given // unified IR export data decoder. func readUnifiedPackage(fset *token.FileSet, ctxt *types.Context, imports map[string]*types.Package, input pkgbits.PkgDecoder) *types.Package { @@ -231,11 +242,35 @@ func (r *reader) doPkg() *types.Package { for i := range imports { imports[i] = r.pkg() } - pkg.SetImports(imports) + pkg.SetImports(flattenImports(imports)) return pkg } +// flattenImports returns the transitive closure of all imported +// packages rooted from pkgs. +func flattenImports(pkgs []*types.Package) []*types.Package { + var res []*types.Package + + seen := make(map[*types.Package]bool) + var add func(pkg *types.Package) + add = func(pkg *types.Package) { + if seen[pkg] { + return + } + seen[pkg] = true + res = append(res, pkg) + for _, imp := range pkg.Imports() { + add(imp) + } + } + + for _, pkg := range pkgs { + add(pkg) + } + return res +} + // @@@ Types func (r *reader) typ() types.Type { @@ -482,7 +517,15 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { // unit tests expected that), but cmd/compile doesn't care // about it, so maybe we can avoid worrying about that here. rhs := r.typ() - r.p.later(func() { + pk := r.p + pk.laterFor(named, func() { + // First be sure that the rhs is initialized, if it needs to be initialized. + delete(pk.laterFors, named) // prevent cycles + if i, ok := pk.laterFors[rhs]; ok { + f := pk.laterFns[i] + pk.laterFns[i] = func() {} // function is running now, so replace it with a no-op + f() // initialize RHS + } underlying := rhs.Underlying() named.SetUnderlying(underlying) }) From 21861e6be56e68c27f11d9e6932c02be7f46284a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 28 Jul 2022 16:49:14 -0400 Subject: [PATCH 148/723] gopls/internal/regtest/bench: put feature benchmarks in their own file Purely for consistent organization. No functional changes. Change-Id: Id26d55d6496523827c154f8a2a17b3660f6081eb Reviewed-on: https://go-review.googlesource.com/c/tools/+/419982 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Peter Weinberger Run-TryBot: Robert Findley --- gopls/internal/regtest/bench/bench_test.go | 185 +----------------- ...etion_bench_test.go => completion_test.go} | 0 .../internal/regtest/bench/didchange_test.go | 79 ++++++++ gopls/internal/regtest/bench/iwl_test.go | 39 ++++ gopls/internal/regtest/bench/mem_test.go | 41 ++++ .../regtest/bench/workspace_symbols_test.go | 74 +++++++ 6 files changed, 234 insertions(+), 184 deletions(-) rename gopls/internal/regtest/bench/{completion_bench_test.go => completion_test.go} (100%) create mode 100644 gopls/internal/regtest/bench/didchange_test.go create mode 100644 gopls/internal/regtest/bench/iwl_test.go create mode 100644 gopls/internal/regtest/bench/mem_test.go create mode 100644 gopls/internal/regtest/bench/workspace_symbols_test.go diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index 090a51cc968..fc1fa7496b0 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -5,19 +5,13 @@ package bench import ( - "flag" "fmt" - "os" - "runtime" - "runtime/pprof" "testing" "golang.org/x/tools/gopls/internal/hooks" "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/fake" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/internal/lsp/regtest" ) func TestMain(m *testing.M) { @@ -46,180 +40,3 @@ func benchmarkOptions(dir string) []RunOption { func printBenchmarkResults(result testing.BenchmarkResult) { fmt.Printf("BenchmarkStatistics\t%s\t%s\n", result.String(), result.MemString()) } - -var iwlOptions struct { - workdir string -} - -func init() { - flag.StringVar(&iwlOptions.workdir, "iwl_workdir", "", "if set, run IWL benchmark in this directory") -} - -func TestBenchmarkIWL(t *testing.T) { - if iwlOptions.workdir == "" { - t.Skip("-iwl_workdir not configured") - } - - opts := stressTestOptions(iwlOptions.workdir) - // Don't skip hooks, so that we can wait for IWL. - opts = append(opts, SkipHooks(false)) - - results := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {}) - - } - }) - - printBenchmarkResults(results) -} - -var symbolOptions struct { - workdir, query, matcher, style string - printResults bool -} - -func init() { - flag.StringVar(&symbolOptions.workdir, "symbol_workdir", "", "if set, run symbol benchmark in this directory") - flag.StringVar(&symbolOptions.query, "symbol_query", "test", "symbol query to use in benchmark") - flag.StringVar(&symbolOptions.matcher, "symbol_matcher", "", "symbol matcher to use in benchmark") - flag.StringVar(&symbolOptions.style, "symbol_style", "", "symbol style to use in benchmark") - flag.BoolVar(&symbolOptions.printResults, "symbol_print_results", false, "whether to print symbol query results") -} - -func TestBenchmarkSymbols(t *testing.T) { - if symbolOptions.workdir == "" { - t.Skip("-symbol_workdir not configured") - } - - opts := benchmarkOptions(symbolOptions.workdir) - settings := make(Settings) - if symbolOptions.matcher != "" { - settings["symbolMatcher"] = symbolOptions.matcher - } - if symbolOptions.style != "" { - settings["symbolStyle"] = symbolOptions.style - } - opts = append(opts, settings) - - WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) { - // We can't Await in this test, since we have disabled hooks. Instead, run - // one symbol request to completion to ensure all necessary cache entries - // are populated. - symbols, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{ - Query: symbolOptions.query, - }) - if err != nil { - t.Fatal(err) - } - - if symbolOptions.printResults { - fmt.Println("Results:") - for i := 0; i < len(symbols); i++ { - fmt.Printf("\t%d. %s (%s)\n", i, symbols[i].Name, symbols[i].ContainerName) - } - } - - results := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - if _, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{ - Query: symbolOptions.query, - }); err != nil { - t.Fatal(err) - } - } - }) - printBenchmarkResults(results) - }) -} - -var ( - benchDir = flag.String("didchange_dir", "", "If set, run benchmarks in this dir. Must also set didchange_file.") - benchFile = flag.String("didchange_file", "", "The file to modify") - benchProfile = flag.String("didchange_cpuprof", "", "file to write cpu profiling data to") -) - -// TestBenchmarkDidChange benchmarks modifications of a single file by making -// synthetic modifications in a comment. It controls pacing by waiting for the -// server to actually start processing the didChange notification before -// proceeding. Notably it does not wait for diagnostics to complete. -// -// Run it by passing -didchange_dir and -didchange_file, where -didchange_dir -// is the path to a workspace root, and -didchange_file is the -// workspace-relative path to a file to modify. e.g.: -// -// go test -run=TestBenchmarkDidChange \ -// -didchange_dir=path/to/kubernetes \ -// -didchange_file=pkg/util/hash/hash.go -func TestBenchmarkDidChange(t *testing.T) { - if *benchDir == "" { - t.Skip("-didchange_dir is not set") - } - if *benchFile == "" { - t.Fatal("-didchange_file must be set if -didchange_dir is set") - } - - opts := benchmarkOptions(*benchDir) - WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { - env.OpenFile(*benchFile) - env.Await(env.DoneWithOpen()) - // Insert the text we'll be modifying at the top of the file. - env.EditBuffer(*benchFile, fake.Edit{Text: "// __REGTEST_PLACEHOLDER_0__\n"}) - - // Run the profiler after the initial load, - // across all benchmark iterations. - if *benchProfile != "" { - profile, err := os.Create(*benchProfile) - if err != nil { - t.Fatal(err) - } - defer profile.Close() - if err := pprof.StartCPUProfile(profile); err != nil { - t.Fatal(err) - } - defer pprof.StopCPUProfile() - } - - result := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - env.EditBuffer(*benchFile, fake.Edit{ - Start: fake.Pos{Line: 0, Column: 0}, - End: fake.Pos{Line: 1, Column: 0}, - // Increment - Text: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1), - }) - env.Await(StartedChange(uint64(i + 1))) - } - }) - printBenchmarkResults(result) - }) -} - -// TestPrintMemStats measures the memory usage of loading a project. -// It uses the same -didchange_dir flag as above. -// Always run it in isolation since it measures global heap usage. -// -// Kubernetes example: -// -// $ go test -v -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes -// TotalAlloc: 5766 MB -// HeapAlloc: 1984 MB -// -// Both figures exhibit variance of less than 1%. -func TestPrintMemStats(t *testing.T) { - if *benchDir == "" { - t.Skip("-didchange_dir is not set") - } - - // Load the program... - opts := benchmarkOptions(*benchDir) - WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { - // ...and print the memory usage. - runtime.GC() - runtime.GC() - var mem runtime.MemStats - runtime.ReadMemStats(&mem) - t.Logf("TotalAlloc:\t%d MB", mem.TotalAlloc/1e6) - t.Logf("HeapAlloc:\t%d MB", mem.HeapAlloc/1e6) - }) -} diff --git a/gopls/internal/regtest/bench/completion_bench_test.go b/gopls/internal/regtest/bench/completion_test.go similarity index 100% rename from gopls/internal/regtest/bench/completion_bench_test.go rename to gopls/internal/regtest/bench/completion_test.go diff --git a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go new file mode 100644 index 00000000000..76f40be6e68 --- /dev/null +++ b/gopls/internal/regtest/bench/didchange_test.go @@ -0,0 +1,79 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bench + +import ( + "flag" + "fmt" + "os" + "runtime/pprof" + "testing" + + "golang.org/x/tools/internal/lsp/fake" + + . "golang.org/x/tools/internal/lsp/regtest" +) + +var ( + benchDir = flag.String("didchange_dir", "", "If set, run benchmarks in this dir. Must also set didchange_file.") + benchFile = flag.String("didchange_file", "", "The file to modify") + benchProfile = flag.String("didchange_cpuprof", "", "file to write cpu profiling data to") +) + +// TestBenchmarkDidChange benchmarks modifications of a single file by making +// synthetic modifications in a comment. It controls pacing by waiting for the +// server to actually start processing the didChange notification before +// proceeding. Notably it does not wait for diagnostics to complete. +// +// Run it by passing -didchange_dir and -didchange_file, where -didchange_dir +// is the path to a workspace root, and -didchange_file is the +// workspace-relative path to a file to modify. e.g.: +// +// go test -run=TestBenchmarkDidChange \ +// -didchange_dir=path/to/kubernetes \ +// -didchange_file=pkg/util/hash/hash.go +func TestBenchmarkDidChange(t *testing.T) { + if *benchDir == "" { + t.Skip("-didchange_dir is not set") + } + if *benchFile == "" { + t.Fatal("-didchange_file must be set if -didchange_dir is set") + } + + opts := benchmarkOptions(*benchDir) + WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { + env.OpenFile(*benchFile) + env.Await(env.DoneWithOpen()) + // Insert the text we'll be modifying at the top of the file. + env.EditBuffer(*benchFile, fake.Edit{Text: "// __REGTEST_PLACEHOLDER_0__\n"}) + + // Run the profiler after the initial load, + // across all benchmark iterations. + if *benchProfile != "" { + profile, err := os.Create(*benchProfile) + if err != nil { + t.Fatal(err) + } + defer profile.Close() + if err := pprof.StartCPUProfile(profile); err != nil { + t.Fatal(err) + } + defer pprof.StopCPUProfile() + } + + result := testing.Benchmark(func(b *testing.B) { + for i := 0; i < b.N; i++ { + env.EditBuffer(*benchFile, fake.Edit{ + Start: fake.Pos{Line: 0, Column: 0}, + End: fake.Pos{Line: 1, Column: 0}, + // Increment + Text: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1), + }) + env.Await(StartedChange(uint64(i + 1))) + } + }) + printBenchmarkResults(result) + }) +} diff --git a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go new file mode 100644 index 00000000000..62faa34fac0 --- /dev/null +++ b/gopls/internal/regtest/bench/iwl_test.go @@ -0,0 +1,39 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bench + +import ( + "flag" + "testing" + + . "golang.org/x/tools/internal/lsp/regtest" +) + +var iwlOptions struct { + workdir string +} + +func init() { + flag.StringVar(&iwlOptions.workdir, "iwl_workdir", "", "if set, run IWL benchmark in this directory") +} + +func TestBenchmarkIWL(t *testing.T) { + if iwlOptions.workdir == "" { + t.Skip("-iwl_workdir not configured") + } + + opts := stressTestOptions(iwlOptions.workdir) + // Don't skip hooks, so that we can wait for IWL. + opts = append(opts, SkipHooks(false)) + + results := testing.Benchmark(func(b *testing.B) { + for i := 0; i < b.N; i++ { + WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {}) + + } + }) + + printBenchmarkResults(results) +} diff --git a/gopls/internal/regtest/bench/mem_test.go b/gopls/internal/regtest/bench/mem_test.go new file mode 100644 index 00000000000..f48b60b0b6f --- /dev/null +++ b/gopls/internal/regtest/bench/mem_test.go @@ -0,0 +1,41 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bench + +import ( + "runtime" + "testing" + + . "golang.org/x/tools/internal/lsp/regtest" +) + +// TestPrintMemStats measures the memory usage of loading a project. +// It uses the same -didchange_dir flag as above. +// Always run it in isolation since it measures global heap usage. +// +// Kubernetes example: +// +// $ go test -v -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes +// TotalAlloc: 5766 MB +// HeapAlloc: 1984 MB +// +// Both figures exhibit variance of less than 1%. +func TestPrintMemStats(t *testing.T) { + if *benchDir == "" { + t.Skip("-didchange_dir is not set") + } + + // Load the program... + opts := benchmarkOptions(*benchDir) + WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { + // ...and print the memory usage. + runtime.GC() + runtime.GC() + var mem runtime.MemStats + runtime.ReadMemStats(&mem) + t.Logf("TotalAlloc:\t%d MB", mem.TotalAlloc/1e6) + t.Logf("HeapAlloc:\t%d MB", mem.HeapAlloc/1e6) + }) +} diff --git a/gopls/internal/regtest/bench/workspace_symbols_test.go b/gopls/internal/regtest/bench/workspace_symbols_test.go new file mode 100644 index 00000000000..ad258dce54a --- /dev/null +++ b/gopls/internal/regtest/bench/workspace_symbols_test.go @@ -0,0 +1,74 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bench + +import ( + "flag" + "fmt" + "testing" + + "golang.org/x/tools/internal/lsp/protocol" + + . "golang.org/x/tools/internal/lsp/regtest" +) + +var symbolOptions struct { + workdir, query, matcher, style string + printResults bool +} + +func init() { + flag.StringVar(&symbolOptions.workdir, "symbol_workdir", "", "if set, run symbol benchmark in this directory") + flag.StringVar(&symbolOptions.query, "symbol_query", "test", "symbol query to use in benchmark") + flag.StringVar(&symbolOptions.matcher, "symbol_matcher", "", "symbol matcher to use in benchmark") + flag.StringVar(&symbolOptions.style, "symbol_style", "", "symbol style to use in benchmark") + flag.BoolVar(&symbolOptions.printResults, "symbol_print_results", false, "whether to print symbol query results") +} + +func TestBenchmarkSymbols(t *testing.T) { + if symbolOptions.workdir == "" { + t.Skip("-symbol_workdir not configured") + } + + opts := benchmarkOptions(symbolOptions.workdir) + settings := make(Settings) + if symbolOptions.matcher != "" { + settings["symbolMatcher"] = symbolOptions.matcher + } + if symbolOptions.style != "" { + settings["symbolStyle"] = symbolOptions.style + } + opts = append(opts, settings) + + WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) { + // We can't Await in this test, since we have disabled hooks. Instead, run + // one symbol request to completion to ensure all necessary cache entries + // are populated. + symbols, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{ + Query: symbolOptions.query, + }) + if err != nil { + t.Fatal(err) + } + + if symbolOptions.printResults { + fmt.Println("Results:") + for i := 0; i < len(symbols); i++ { + fmt.Printf("\t%d. %s (%s)\n", i, symbols[i].Name, symbols[i].ContainerName) + } + } + + results := testing.Benchmark(func(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{ + Query: symbolOptions.query, + }); err != nil { + t.Fatal(err) + } + } + }) + printBenchmarkResults(results) + }) +} From 310ea71b71739fd393f8828d091f07d19cae9690 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 1 Aug 2022 12:07:41 -0400 Subject: [PATCH 149/723] gopls/internal/regtest: add a test that ignoring a file resolves errors Add a test that when a user adds a "//go:build ignore" to a file, gopls correctly invalidates diagnostics. This used to be broken, but was fixed by CL 417576. Fixes golang/go#54147 Change-Id: I554fcfc0a56b72f657e19b3c0ae53a66d1a99a76 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420537 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Suzy Mueller Run-TryBot: Robert Findley --- .../regtest/workspace/metadata_test.go | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index 28291a2e23d..4c3f46b0a93 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -41,3 +41,63 @@ const C = 42 )) }) } + +// Test that moving ignoring a file via build constraints causes diagnostics to +// be resolved. +func TestIgnoreFile(t *testing.T) { + testenv.NeedsGo1Point(t, 16) // needs native overlays + + const src = ` +-- go.mod -- +module mod.test + +go 1.12 +-- foo.go -- +package main + +func main() {} +-- bar.go -- +package main + +func main() {} + ` + + WithOptions( + // TODO(golang/go#54180): we don't run in 'experimental' mode here, because + // with "experimentalUseInvalidMetadata", this test fails because the + // orphaned bar.go is diagnosed using stale metadata, and then not + // re-diagnosed when new metadata arrives. + // + // We could fix this by re-running diagnostics after a load, but should + // consider whether that is worthwhile. + Modes(Default), + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("foo.go") + env.OpenFile("bar.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("foo.go", "func (main)"), + env.DiagnosticAtRegexp("bar.go", "func (main)"), + ), + ) + // Ignore bar.go. This should resolve diagnostics. + env.RegexpReplace("bar.go", "package main", "// +build ignore\n\npackage main") + + // To make this test pass with experimentalUseInvalidMetadata, we could make + // an arbitrary edit that invalidates the snapshot, at which point the + // orphaned diagnostics will be invalidated. + // + // But of course, this should not be necessary: we should invalidate stale + // information when fresh metadata arrives. + // env.RegexpReplace("foo.go", "package main", "package main // test") + env.Await( + OnceMet( + env.DoneWithChange(), + EmptyDiagnostics("foo.go"), + env.DiagnosticAtRegexpWithMessage("bar.go", "package (main)", "No packages"), + env.NoDiagnosticAtRegexp("bar.go", "func (main)"), + ), + ) + }) +} From 4d0b3834583013776f95cae07b2edbfc451c1898 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 2 Aug 2022 10:54:01 -0400 Subject: [PATCH 150/723] internal/lsp/regtest: minor cleanup for magic regtest envvar For some reason, we were not reusing the constant runTestAsGoplsEnvVar in the regtest Main function. Fix this, add a bit more commentary, and check the envvar before doing anything else. Also fix the threading of hooks to the created gopls server. Tested manually via -enable_gopls_subprocess_tests. Change-Id: Ieb5329aa5850e845f4d9e3868703bfa16387bce3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420716 Reviewed-by: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- internal/lsp/regtest/regtest.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/lsp/regtest/regtest.go b/internal/lsp/regtest/regtest.go index cfd999dfa59..b041891702a 100644 --- a/internal/lsp/regtest/regtest.go +++ b/internal/lsp/regtest/regtest.go @@ -94,16 +94,19 @@ func DefaultModes() Mode { // Main sets up and tears down the shared regtest state. func Main(m *testing.M, hook func(*source.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:]) + os.Exit(0) + } + testenv.ExitIfSmallMachine() // Disable GOPACKAGESDRIVER, as it can cause spurious test failures. os.Setenv("GOPACKAGESDRIVER", "off") flag.Parse() - if os.Getenv("_GOPLS_TEST_BINARY_RUN_AS_GOPLS") == "true" { - tool.Main(context.Background(), cmd.New("gopls", "", nil, nil), os.Args[1:]) - os.Exit(0) - } runner = &Runner{ DefaultModes: DefaultModes(), From 10cb4353f96af992b4bb996817f505a89e198feb Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 28 Jul 2022 10:46:55 -0400 Subject: [PATCH 151/723] internal/lsp/regtest: improvements for shared execution modes Following up on CL 417587, make several improvements to the regtest runner related to shared execution modes: - guard lazily-allocated resources with sync.Once rather than a common mutex. - for simplicity, always set Runner.goplsPath - start the separate process server synchronously, to ensure that it is running before test execution - cancel the separate process server when the test runner exits - remove the testing.T argument from server constructors - close the regtest runner in a deferred function Tested manually via -enable_gopls_subprocess_tests. Change-Id: Ide3972a94c129bcce554c10dd167df01c3040d31 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419954 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Bryan Mills gopls-CI: kokoro --- internal/lsp/regtest/regtest.go | 42 +++++------ internal/lsp/regtest/runner.go | 124 ++++++++++++++++---------------- 2 files changed, 85 insertions(+), 81 deletions(-) diff --git a/internal/lsp/regtest/regtest.go b/internal/lsp/regtest/regtest.go index b041891702a..d499bde79b4 100644 --- a/internal/lsp/regtest/regtest.go +++ b/internal/lsp/regtest/regtest.go @@ -117,33 +117,35 @@ func Main(m *testing.M, hook func(*source.Options)) { fset: token.NewFileSet(), store: memoize.NewStore(memoize.NeverEvict), } - if *runSubprocessTests { - goplsPath := *goplsBinaryPath - if goplsPath == "" { - var err error - goplsPath, err = os.Executable() - if err != nil { - panic(fmt.Sprintf("finding test binary path: %v", err)) - } + + runner.goplsPath = *goplsBinaryPath + if runner.goplsPath == "" { + var err error + runner.goplsPath, err = os.Executable() + if err != nil { + panic(fmt.Sprintf("finding test binary path: %v", err)) } - runner.goplsPath = goplsPath } + dir, err := ioutil.TempDir("", "gopls-regtest-") if err != nil { panic(fmt.Errorf("creating regtest temp directory: %v", err)) } runner.tempDir = dir - code := m.Run() - if err := runner.Close(); err != nil { - fmt.Fprintf(os.Stderr, "closing test runner: %v\n", err) - // Regtest cleanup is broken in go1.12 and earlier, and sometimes flakes on - // Windows due to file locking, but this is OK for our CI. - // - // Fail on go1.13+, except for windows and android which have shutdown problems. - if testenv.Go1Point() >= 13 && runtime.GOOS != "windows" && runtime.GOOS != "android" { - os.Exit(1) + var code int + defer func() { + if err := runner.Close(); err != nil { + fmt.Fprintf(os.Stderr, "closing test runner: %v\n", err) + // Regtest cleanup is broken in go1.12 and earlier, and sometimes flakes on + // Windows due to file locking, but this is OK for our CI. + // + // Fail on go1.13+, except for windows and android which have shutdown problems. + if testenv.Go1Point() >= 13 && runtime.GOOS != "windows" && runtime.GOOS != "android" { + os.Exit(1) + } } - } - os.Exit(code) + os.Exit(code) + }() + code = m.Run() } diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go index df1cc05e7e8..12fd323abf5 100644 --- a/internal/lsp/regtest/runner.go +++ b/internal/lsp/regtest/runner.go @@ -14,6 +14,7 @@ import ( "net" "os" "path/filepath" + "runtime" "runtime/pprof" "strings" "sync" @@ -75,6 +76,8 @@ const ( // SeparateProcess uses the default options, but forwards connection to an // external gopls daemon. + // + // Only supported on GOOS=linux. SeparateProcess // Experimental enables all of the experimental configurations that are @@ -119,9 +122,13 @@ type Runner struct { store *memoize.Store // shared store // Lazily allocated resources - mu sync.Mutex - ts *servertest.TCPServer - socketDir string + tsOnce sync.Once + ts *servertest.TCPServer // shared in-process test server ("forwarded" mode) + + startRemoteOnce sync.Once + remoteSocket string // unix domain socket for shared daemon ("separate process" mode) + remoteErr error + cancelRemote func() } type runConfig struct { @@ -292,7 +299,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio tests := []struct { name string mode Mode - getServer func(*testing.T, func(*source.Options)) jsonrpc2.StreamServer + getServer func(func(*source.Options)) jsonrpc2.StreamServer }{ {"default", Default, r.defaultServer}, {"forwarded", Forwarded, r.forwardedServer}, @@ -371,7 +378,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio } }() - ss := tc.getServer(t, r.OptionsHook) + ss := tc.getServer(r.OptionsHook) framer := jsonrpc2.NewRawStream ls := &loggingFramer{} @@ -482,12 +489,12 @@ func (s *loggingFramer) printBuffers(testname string, w io.Writer) { } // defaultServer handles the Default execution mode. -func (r *Runner) defaultServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { +func (r *Runner) defaultServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { return lsprpc.NewStreamServer(cache.New(r.fset, r.store, optsHook), false) } // experimentalServer handles the Experimental execution mode. -func (r *Runner) experimentalServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { +func (r *Runner) experimentalServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { options := func(o *source.Options) { optsHook(o) o.EnableAllExperiments() @@ -499,24 +506,61 @@ func (r *Runner) experimentalServer(t *testing.T, optsHook func(*source.Options) } // forwardedServer handles the Forwarded execution mode. -func (r *Runner) forwardedServer(_ *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { - if r.ts == nil { - r.mu.Lock() +func (r *Runner) forwardedServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { + r.tsOnce.Do(func() { ctx := context.Background() ctx = debug.WithInstance(ctx, "", "off") ss := lsprpc.NewStreamServer(cache.New(nil, nil, optsHook), false) r.ts = servertest.NewTCPServer(ctx, ss, nil) - r.mu.Unlock() - } + }) return newForwarder("tcp", r.ts.Addr) } +// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running +// tests. It's a trick to allow tests to find a binary to use to start a gopls +// subprocess. +const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS" + // separateProcessServer handles the SeparateProcess execution mode. -func (r *Runner) separateProcessServer(t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { - // TODO(rfindley): can we use the autostart behavior here, instead of - // pre-starting the remote? - socket := r.getRemoteSocket(t) - return newForwarder("unix", socket) +func (r *Runner) separateProcessServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { + if runtime.GOOS != "linux" { + panic("separate process execution mode is only supported on linux") + } + + r.startRemoteOnce.Do(func() { + socketDir, err := ioutil.TempDir(r.tempDir, "gopls-regtest-socket") + if err != nil { + r.remoteErr = err + return + } + r.remoteSocket = filepath.Join(socketDir, "gopls-test-daemon") + + // The server should be killed by when the test runner exits, but to be + // conservative also set a listen timeout. + args := []string{"serve", "-listen", "unix;" + r.remoteSocket, "-listen.timeout", "1m"} + + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, r.goplsPath, args...) + cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true") + + // Start the external gopls process. This is still somewhat racy, as we + // don't know when gopls binds to the socket, but the gopls forwarder + // client has built-in retry behavior that should mostly mitigate this + // problem (and if it doesn't, we probably want to improve the retry + // behavior). + if err := cmd.Start(); err != nil { + cancel() + r.remoteSocket = "" + r.remoteErr = err + } else { + r.cancelRemote = cancel + // Spin off a goroutine to wait, so that we free up resources when the + // server exits. + go cmd.Wait() + } + }) + + return newForwarder("unix", r.remoteSocket) } func newForwarder(network, address string) *lsprpc.Forwarder { @@ -528,58 +572,16 @@ func newForwarder(network, address string) *lsprpc.Forwarder { return server } -// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running -// tests. It's a trick to allow tests to find a binary to use to start a gopls -// subprocess. -const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS" - -func (r *Runner) getRemoteSocket(t *testing.T) string { - t.Helper() - r.mu.Lock() - defer r.mu.Unlock() - const daemonFile = "gopls-test-daemon" - if r.socketDir != "" { - return filepath.Join(r.socketDir, daemonFile) - } - - if r.goplsPath == "" { - t.Fatal("cannot run tests with a separate process unless a path to a gopls binary is configured") - } - var err error - r.socketDir, err = ioutil.TempDir(r.tempDir, "gopls-regtest-socket") - if err != nil { - t.Fatalf("creating tempdir: %v", err) - } - socket := filepath.Join(r.socketDir, daemonFile) - args := []string{"serve", "-listen", "unix;" + socket, "-listen.timeout", "10s"} - cmd := exec.Command(r.goplsPath, args...) - cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true") - var stderr bytes.Buffer - cmd.Stderr = &stderr - go func() { - // TODO(rfindley): this is racy; we're returning before we know that the command is running. - if err := cmd.Run(); err != nil { - panic(fmt.Sprintf("error running external gopls: %v\nstderr:\n%s", err, stderr.String())) - } - }() - return socket -} - // Close cleans up resource that have been allocated to this workspace. func (r *Runner) Close() error { - r.mu.Lock() - defer r.mu.Unlock() - var errmsgs []string if r.ts != nil { if err := r.ts.Close(); err != nil { errmsgs = append(errmsgs, err.Error()) } } - if r.socketDir != "" { - if err := os.RemoveAll(r.socketDir); err != nil { - errmsgs = append(errmsgs, err.Error()) - } + if r.cancelRemote != nil { + r.cancelRemote() } if !r.SkipCleanup { if err := os.RemoveAll(r.tempDir); err != nil { From d025cced83580ce6aa84134a94d6a32ec301c041 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 2 Aug 2022 10:32:11 -0400 Subject: [PATCH 152/723] internal/lsp/source: don't crash requesting gc_details for an empty file Requesting gc_details for an empty file never worked, but in the past it would inadvertently get handled by our forgiving position helpers. With https://go.dev/cl/409935, it became a crasher. Fix the crash by adding handling when the package name position is invalid. Also move gc_details related codelens to their own file. Fixes golang/go#54199 Change-Id: I7339b3903abd1315f1fea3dd9929d81855a8264a Reviewed-on: https://go-review.googlesource.com/c/tools/+/420715 gopls-CI: kokoro Reviewed-by: Suzy Mueller Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- .../regtest/codelens/codelens_test.go | 71 --------- .../regtest/codelens/gcdetails_test.go | 137 ++++++++++++++++++ internal/lsp/source/code_lens.go | 4 + 3 files changed, 141 insertions(+), 71 deletions(-) create mode 100644 gopls/internal/regtest/codelens/gcdetails_test.go diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 12a75754152..4d2be8e74d7 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -6,8 +6,6 @@ package codelens import ( "fmt" - "runtime" - "strings" "testing" "golang.org/x/tools/gopls/internal/hooks" @@ -15,7 +13,6 @@ import ( . "golang.org/x/tools/internal/lsp/regtest" "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/tests" "golang.org/x/tools/internal/testenv" @@ -284,71 +281,3 @@ func Foo() { env.Await(EmptyDiagnostics("cgo.go")) }) } - -func TestGCDetails(t *testing.T) { - testenv.NeedsGo1Point(t, 15) - if runtime.GOOS == "android" { - t.Skipf("the gc details code lens doesn't work on Android") - } - - const mod = ` --- go.mod -- -module mod.com - -go 1.15 --- main.go -- -package main - -import "fmt" - -func main() { - fmt.Println(42) -} -` - WithOptions( - Settings{ - "codelenses": map[string]bool{ - "gc_details": true, - }, - }, - ).Run(t, mod, func(t *testing.T, env *Env) { - env.OpenFile("main.go") - env.ExecuteCodeLensCommand("main.go", command.GCDetails) - d := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - DiagnosticAt("main.go", 5, 13), - ReadDiagnostics("main.go", d), - ), - ) - // Confirm that the diagnostics come from the gc details code lens. - var found bool - for _, d := range d.Diagnostics { - if d.Severity != protocol.SeverityInformation { - t.Fatalf("unexpected diagnostic severity %v, wanted Information", d.Severity) - } - if strings.Contains(d.Message, "42 escapes") { - found = true - } - } - if !found { - t.Fatalf(`expected to find diagnostic with message "escape(42 escapes to heap)", found none`) - } - - // Editing a buffer should cause gc_details diagnostics to disappear, since - // they only apply to saved buffers. - env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, "\n\n")) - env.Await(EmptyDiagnostics("main.go")) - - // Saving a buffer should re-format back to the original state, and - // re-enable the gc_details diagnostics. - env.SaveBuffer("main.go") - env.Await(DiagnosticAt("main.go", 5, 13)) - - // Toggle the GC details code lens again so now it should be off. - env.ExecuteCodeLensCommand("main.go", command.GCDetails) - env.Await( - EmptyDiagnostics("main.go"), - ) - }) -} diff --git a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go new file mode 100644 index 00000000000..3764888d74e --- /dev/null +++ b/gopls/internal/regtest/codelens/gcdetails_test.go @@ -0,0 +1,137 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package codelens + +import ( + "runtime" + "strings" + "testing" + + "golang.org/x/tools/internal/testenv" + + "golang.org/x/tools/internal/lsp/bug" + "golang.org/x/tools/internal/lsp/command" + "golang.org/x/tools/internal/lsp/fake" + "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/internal/lsp/regtest" +) + +func TestGCDetails_Toggle(t *testing.T) { + testenv.NeedsGo1Point(t, 15) + if runtime.GOOS == "android" { + t.Skipf("the gc details code lens doesn't work on Android") + } + + const mod = ` +-- go.mod -- +module mod.com + +go 1.15 +-- main.go -- +package main + +import "fmt" + +func main() { + fmt.Println(42) +} +` + WithOptions( + Settings{ + "codelenses": map[string]bool{ + "gc_details": true, + }, + }, + ).Run(t, mod, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + env.ExecuteCodeLensCommand("main.go", command.GCDetails) + d := &protocol.PublishDiagnosticsParams{} + env.Await( + OnceMet( + DiagnosticAt("main.go", 5, 13), + ReadDiagnostics("main.go", d), + ), + ) + // Confirm that the diagnostics come from the gc details code lens. + var found bool + for _, d := range d.Diagnostics { + if d.Severity != protocol.SeverityInformation { + t.Fatalf("unexpected diagnostic severity %v, wanted Information", d.Severity) + } + if strings.Contains(d.Message, "42 escapes") { + found = true + } + } + if !found { + t.Fatalf(`expected to find diagnostic with message "escape(42 escapes to heap)", found none`) + } + + // Editing a buffer should cause gc_details diagnostics to disappear, since + // they only apply to saved buffers. + env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, "\n\n")) + env.Await(EmptyDiagnostics("main.go")) + + // Saving a buffer should re-format back to the original state, and + // re-enable the gc_details diagnostics. + env.SaveBuffer("main.go") + env.Await(DiagnosticAt("main.go", 5, 13)) + + // Toggle the GC details code lens again so now it should be off. + env.ExecuteCodeLensCommand("main.go", command.GCDetails) + env.Await( + EmptyDiagnostics("main.go"), + ) + }) +} + +// Test for the crasher in golang/go#54199 +func TestGCDetails_NewFile(t *testing.T) { + bug.PanicOnBugs = false + // It appears that older Go versions don't even see p.go from the initial + // workspace load. + testenv.NeedsGo1Point(t, 15) + const src = ` +-- go.mod -- +module mod.test + +go 1.12 +` + + WithOptions( + Settings{ + "codelenses": map[string]bool{ + "gc_details": true, + }, + }, + ).Run(t, src, func(t *testing.T, env *Env) { + env.CreateBuffer("p_test.go", "") + + const gcDetailsCommand = "gopls." + string(command.GCDetails) + + hasGCDetails := func() bool { + lenses := env.CodeLens("p_test.go") // should not crash + for _, lens := range lenses { + if lens.Command.Command == gcDetailsCommand { + return true + } + } + return false + } + + // With an empty file, we shouldn't get the gc_details codelens because + // there is nowhere to position it (it needs a package name). + if hasGCDetails() { + t.Errorf("got the gc_details codelens for an empty file") + } + + // Edit to provide a package name. + env.EditBuffer("p_test.go", fake.NewEdit(0, 0, 0, 0, "package p")) + + // Now we should get the gc_details codelens. + if !hasGCDetails() { + t.Errorf("didn't get the gc_details codelens for a valid non-empty Go file") + } + }) +} diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go index 0e9453a662b..85a0a2f522d 100644 --- a/internal/lsp/source/code_lens.go +++ b/internal/lsp/source/code_lens.go @@ -231,6 +231,10 @@ func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle if err != nil { return nil, err } + if !pgf.File.Package.IsValid() { + // Without a package name we have nowhere to put the codelens, so give up. + return nil, nil + } rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() if err != nil { return nil, err From 0d04f65da922c811c0d443e902ea62bbc27e5fd9 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 1 Aug 2022 17:30:10 -0400 Subject: [PATCH 153/723] internal/lsp: re-send diagnostics on file events Fix golang/go#50267 by ensuring that diagnostics are re-sent following didOpen or didClose events. Additionally, introduce a new hidden 'chattyDiagnostics' option that causes diagnostics to be resent on *every* file change event. This latter option is for LSP clients that get confused when diagnostics are not re-sent for later file versions. For now, be conservative and only force diagnostic publication on didOpen and didClose. Update tests whose 'NoDiagnostics' assertions were broken by the new behavior. Fixes golang/go#50267 Change-Id: I6332d66a1851e0d8261599d37020a03b4c598f7d Reviewed-on: https://go-review.googlesource.com/c/tools/+/420539 Run-TryBot: Robert Findley Reviewed-by: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro --- .../regtest/diagnostics/diagnostics_test.go | 21 +-- .../regtest/diagnostics/invalidation_test.go | 126 ++++++++++++++++++ .../regtest/misc/configuration_test.go | 2 +- .../regtest/template/template_test.go | 2 +- gopls/internal/regtest/watch/watch_test.go | 18 +-- internal/lsp/cache/session.go | 2 + internal/lsp/diagnostics.go | 61 +++++++-- internal/lsp/source/options.go | 14 ++ internal/lsp/text_synchronization.go | 19 +++ 9 files changed, 238 insertions(+), 27 deletions(-) create mode 100644 gopls/internal/regtest/diagnostics/invalidation_test.go diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index e1b55b5b131..f915fccbd14 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -384,7 +384,7 @@ func main() {} // completed. OnceMet( env.DoneWithChange(), - NoDiagnostics("a.go"), + EmptyDiagnostics("a.go"), ), ) }) @@ -757,15 +757,20 @@ func _() { env.OpenFile("a/a1.go") env.CreateBuffer("a/a2.go", ``) env.SaveBufferWithoutActions("a/a2.go") + // We can't use OnceMet here (at least, not easily) because the didSave + // races with the didChangeWatchedFiles. + // + // TODO(rfindley): add an AllOf expectation combinator, or an expectation + // that all notifications have been processed. env.Await( - OnceMet( - env.DoneWithSave(), - NoDiagnostics("a/a1.go"), - ), + EmptyDiagnostics("a/a1.go"), ) env.EditBuffer("a/a2.go", fake.NewEdit(0, 0, 0, 0, `package a`)) env.Await( - OnceMet(env.DoneWithChange(), NoDiagnostics("a/a1.go")), + OnceMet( + env.DoneWithChange(), + EmptyDiagnostics("a/a1.go"), + ), ) }) } @@ -914,7 +919,7 @@ var _ = foo.Bar env.Await( OnceMet( env.DoneWithOpen(), - NoDiagnostics("_foo/x.go"), + EmptyDiagnostics("_foo/x.go"), )) }) } @@ -1730,7 +1735,7 @@ package b env.Await( OnceMet( env.DoneWithOpen(), - NoDiagnostics("a/a.go"), + EmptyDiagnostics("a/a.go"), ), NoOutstandingWork(), ) diff --git a/gopls/internal/regtest/diagnostics/invalidation_test.go b/gopls/internal/regtest/diagnostics/invalidation_test.go new file mode 100644 index 00000000000..ea65037644c --- /dev/null +++ b/gopls/internal/regtest/diagnostics/invalidation_test.go @@ -0,0 +1,126 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diagnostics + +import ( + "fmt" + "testing" + + "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/internal/lsp/regtest" +) + +// Test for golang/go#50267: diagnostics should be re-sent after a file is +// opened. +func TestDiagnosticsAreResentAfterCloseOrOpen(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.16 +-- main.go -- +package main + +func _() { + x := 2 +} +` + Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. + env.OpenFile("main.go") + var afterOpen protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithOpen(), + ReadDiagnostics("main.go", &afterOpen), + ), + ) + env.CloseBuffer("main.go") + var afterClose protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithClose(), + ReadDiagnostics("main.go", &afterClose), + ), + ) + if afterOpen.Version == afterClose.Version { + t.Errorf("publishDiagnostics: got the same version after closing (%d) as after opening", afterOpen.Version) + } + env.OpenFile("main.go") + var afterReopen protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithOpen(), + ReadDiagnostics("main.go", &afterReopen), + ), + ) + if afterReopen.Version == afterClose.Version { + t.Errorf("pubslishDiagnostics: got the same version after reopening (%d) as after closing", afterClose.Version) + } + }) +} + +// Test for the "chattyDiagnostics" setting: we should get re-published +// diagnostics after every file change, even if diagnostics did not change. +func TestChattyDiagnostics(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.16 +-- main.go -- +package main + +func _() { + x := 2 +} + +// Irrelevant comment #0 +` + + WithOptions( + Settings{ + "chattyDiagnostics": true, + }, + ).Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. + + env.OpenFile("main.go") + var d protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithOpen(), + ReadDiagnostics("main.go", &d), + ), + ) + + if len(d.Diagnostics) != 1 { + t.Fatalf("len(Diagnostics) = %d, want 1", len(d.Diagnostics)) + } + msg := d.Diagnostics[0].Message + + for i := 0; i < 5; i++ { + before := d.Version + env.RegexpReplace("main.go", "Irrelevant comment #.", fmt.Sprintf("Irrelevant comment #%d", i)) + env.Await( + OnceMet( + env.DoneWithChange(), + ReadDiagnostics("main.go", &d), + ), + ) + + if d.Version == before { + t.Errorf("after change, got version %d, want new version", d.Version) + } + + // As a sanity check, make sure we have the same diagnostic. + if len(d.Diagnostics) != 1 { + t.Fatalf("len(Diagnostics) = %d, want 1", len(d.Diagnostics)) + } + newMsg := d.Diagnostics[0].Message + if newMsg != msg { + t.Errorf("after change, got message %q, want %q", newMsg, msg) + } + } + }) +} diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index 9629a2382ee..433f96e8555 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -37,7 +37,7 @@ var FooErr = errors.New("foo") env.OpenFile("a/a.go") env.Await( env.DoneWithOpen(), - NoDiagnostics("a/a.go"), + EmptyDiagnostics("a/a.go"), ) cfg := env.Editor.Config() cfg.Settings = map[string]interface{}{ diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index 0fdc3bda6ff..292088ce59f 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -133,7 +133,7 @@ go 1.12 env.Await( OnceMet( env.DoneWithOpen(), - NoDiagnostics("hello.tmpl"), // Don't get spurious errors for empty templates. + EmptyDiagnostics("hello.tmpl"), // Don't get spurious errors for empty templates. ), ) env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}") diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 2890f401a90..31655955e23 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -139,7 +139,7 @@ func _() { }) env.Await( EmptyDiagnostics("a/a.go"), - NoDiagnostics("b/b.go"), + EmptyOrNoDiagnostics("b/b.go"), ) }) } @@ -341,12 +341,12 @@ func _() { env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a.go"), + EmptyOrNoDiagnostics("a/a.go"), ), ) env.WriteWorkspaceFile("b/b.go", newMethod) env.Await( - NoDiagnostics("a/a.go"), + EmptyOrNoDiagnostics("a/a.go"), ) }) }) @@ -360,9 +360,9 @@ func _() { env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a.go"), + EmptyOrNoDiagnostics("a/a.go"), ), - NoDiagnostics("b/b.go"), + EmptyOrNoDiagnostics("b/b.go"), ) }) }) @@ -715,11 +715,11 @@ func TestAll(t *testing.T) { env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a.go"), + EmptyOrNoDiagnostics("a/a.go"), ), OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a_test.go"), + EmptyOrNoDiagnostics("a/a_test.go"), ), ) // Now, add a new file to the test variant and use its symbol in the @@ -747,11 +747,11 @@ func TestSomething(t *testing.T) {} env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a_test.go"), + EmptyOrNoDiagnostics("a/a_test.go"), ), OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a2_test.go"), + EmptyOrNoDiagnostics("a/a2_test.go"), ), ) }) diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 372e8e97956..a77deb62118 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -413,6 +413,8 @@ func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModifica return err } +// TODO(rfindley): fileChange seems redundant with source.FileModification. +// De-dupe into a common representation for changes. type fileChange struct { content []byte exists bool diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 460659f30d2..642957cbadc 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -41,17 +41,42 @@ const ( // A diagnosticReport holds results for a single diagnostic source. type diagnosticReport struct { - snapshotID uint64 - publishedHash string + snapshotID uint64 // snapshot ID on which the report was computed + publishedHash string // last published hash for this (URI, source) diags map[string]*source.Diagnostic } // fileReports holds a collection of diagnostic reports for a single file, as // well as the hash of the last published set of diagnostics. type fileReports struct { - snapshotID uint64 + // publishedSnapshotID is the last snapshot ID for which we have "published" + // diagnostics (though the publishDiagnostics notification may not have + // actually been sent, if nothing changed). + // + // Specifically, publishedSnapshotID is updated to a later snapshot ID when + // we either: + // (1) publish diagnostics for the file for a snapshot, or + // (2) determine that published diagnostics are valid for a new snapshot. + // + // Notably publishedSnapshotID may not match the snapshot id on individual reports in + // the reports map: + // - we may have published partial diagnostics from only a subset of + // diagnostic sources for which new results have been computed, or + // - we may have started computing reports for an even new snapshot, but not + // yet published. + // + // This prevents gopls from publishing stale diagnostics. + publishedSnapshotID uint64 + + // publishedHash is a hash of the latest diagnostics published for the file. publishedHash string - reports map[diagnosticSource]diagnosticReport + + // If set, mustPublish marks diagnostics as needing publication, independent + // of whether their publishedHash has changed. + mustPublish bool + + // The last stored diagnostics for each diagnostic source. + reports map[diagnosticSource]diagnosticReport } func (d diagnosticSource) String() string { @@ -358,6 +383,24 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg } } +// mustPublishDiagnostics marks the uri as needing publication, independent of +// whether the published contents have changed. +// +// This can be used for ensuring gopls publishes diagnostics after certain file +// events. +func (s *Server) mustPublishDiagnostics(uri span.URI) { + s.diagnosticsMu.Lock() + defer s.diagnosticsMu.Unlock() + + if s.diagnostics[uri] == nil { + s.diagnostics[uri] = &fileReports{ + publishedHash: hashDiagnostics(), // Hash for 0 diagnostics. + reports: map[diagnosticSource]diagnosticReport{}, + } + } + s.diagnostics[uri].mustPublish = true +} + // storeDiagnostics stores results from a single diagnostic source. If merge is // true, it merges results into any existing results for this snapshot. func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsource diagnosticSource, diags []*source.Diagnostic) { @@ -367,6 +410,7 @@ func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsourc if fh == nil { return } + s.diagnosticsMu.Lock() defer s.diagnosticsMu.Unlock() if s.diagnostics[uri] == nil { @@ -512,7 +556,7 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so // If we've already delivered diagnostics for a future snapshot for this // file, do not deliver them. - if r.snapshotID > snapshot.ID() { + if r.publishedSnapshotID > snapshot.ID() { continue } anyReportsChanged := false @@ -541,10 +585,10 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so } source.SortDiagnostics(diags) hash := hashDiagnostics(diags...) - if hash == r.publishedHash { + if hash == r.publishedHash && !r.mustPublish { // Update snapshotID to be the latest snapshot for which this diagnostic // hash is valid. - r.snapshotID = snapshot.ID() + r.publishedSnapshotID = snapshot.ID() continue } var version int32 @@ -558,7 +602,8 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so }); err == nil { published++ r.publishedHash = hash - r.snapshotID = snapshot.ID() + r.mustPublish = false // diagnostics have been successfully published + r.publishedSnapshotID = snapshot.ID() for dsource, hash := range reportHashes { report := r.reports[dsource] report.publishedHash = hash diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index a695feaf264..8abcc7452c8 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -573,6 +573,16 @@ type InternalOptions struct { // that the new algorithm has worked, and write some summary // statistics to a file in os.TmpDir() NewDiff string + + // ChattyDiagnostics controls whether to report file diagnostics for each + // file change. If unset, gopls only reports diagnostics when they change, or + // when a file is opened or closed. + // + // TODO(rfindley): is seems that for many clients this should be true by + // default. For example, coc.nvim seems to get confused if diagnostics are + // not re-published. Switch the default to true after some period of internal + // testing. + ChattyDiagnostics bool } type ImportShortcut string @@ -805,6 +815,7 @@ func (o *Options) EnableAllExperiments() { o.ExperimentalUseInvalidMetadata = true o.ExperimentalWatchedFileDelay = 50 * time.Millisecond o.NewDiff = "checked" + o.ChattyDiagnostics = true } func (o *Options) enableAllExperimentMaps() { @@ -1075,6 +1086,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "newDiff": result.setString(&o.NewDiff) + case "chattyDiagnostics": + result.setBool(&o.ChattyDiagnostics) + // Replaced settings. case "experimentalDisabledAnalyses": result.deprecated("analyses") diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go index 514834b077c..dd6714553c1 100644 --- a/internal/lsp/text_synchronization.go +++ b/internal/lsp/text_synchronization.go @@ -286,16 +286,35 @@ func (s *Server) processModifications(ctx context.Context, modifications []sourc return errors.New("server is shut down") } s.stateMu.Unlock() + // If the set of changes included directories, expand those directories // to their files. modifications = s.session.ExpandModificationsToDirectories(ctx, modifications) + // Build a lookup map for file modifications, so that we can later join + // with the snapshot file associations. + modMap := make(map[span.URI]source.FileModification) + for _, mod := range modifications { + modMap[mod.URI] = mod + } + snapshots, release, err := s.session.DidModifyFiles(ctx, modifications) if err != nil { close(diagnoseDone) return err } + // golang/go#50267: diagnostics should be re-sent after an open or close. For + // some clients, it may be helpful to re-send after each change. + for snapshot, uris := range snapshots { + for _, uri := range uris { + mod := modMap[uri] + if snapshot.View().Options().ChattyDiagnostics || mod.Action == source.Open || mod.Action == source.Close { + s.mustPublishDiagnostics(uri) + } + } + } + go func() { s.diagnoseSnapshots(snapshots, onDisk) release() From ddb90ecd31bf8b6db4ddb50ba6aac035403fc914 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 2 Aug 2022 22:22:57 -0400 Subject: [PATCH 154/723] internal/lsp/cache: fix data races to view.options Use the concurrency-safe view.Options method. Fixes golang/go#54214 Change-Id: If75a544ae477ee7361540c3933a18e3366d8ffd7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420954 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Dylan Le --- internal/lsp/cache/load.go | 2 +- internal/lsp/cache/view.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 9d84891911f..ca906c82877 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -141,7 +141,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } moduleErrs := make(map[string][]packages.Error) // module path -> errors - filterer := buildFilterer(s.view.rootURI.Filename(), s.view.gomodcache, s.view.options) + filterer := buildFilterer(s.view.rootURI.Filename(), s.view.gomodcache, s.view.Options()) newMetadata := make(map[PackageID]*KnownMetadata) for _, pkg := range pkgs { // The Go command returns synthetic list results for module queries that diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 0a64b76306e..cddd4fa5d63 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -392,7 +392,7 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) { relativeTo := s.view.folder.Filename() searched := 0 - filterer := buildFilterer(dir, s.view.gomodcache, s.view.options) + filterer := buildFilterer(dir, s.view.gomodcache, s.view.Options()) // Change to WalkDir when we move up to 1.16 err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { if err != nil { From d08f5dc9fbfc126ed1a4386b2ef120822334924f Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 22 Jul 2022 17:55:31 -0400 Subject: [PATCH 155/723] gopls/internal/regtest: unskip all of TestModFileModification I believe the races described in the issue have been fixed: we should invalidate mod tidy results on any metadata change. If this invalidation doesn't work due to a race, we want to know about it. Update the test to wait for file-related events to complete before removing files, in an attempt to avoid windows file-locking issues. For golang/go#40269 For golang/go#53878 Change-Id: I91f0cb4969851010b34904a0b78ab9bd2808f92e Reviewed-on: https://go-review.googlesource.com/c/tools/+/420718 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller --- .../internal/regtest/modfile/modfile_test.go | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 79441c1fe79..91c8005be98 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -95,7 +95,10 @@ func main() { goModContent := env.ReadWorkspaceFile("a/go.mod") env.OpenFile("a/main.go") env.Await( - env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), + ), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got)) @@ -114,26 +117,43 @@ func main() { // Reproduce golang/go#40269 by deleting and recreating main.go. t.Run("delete main.go", func(t *testing.T) { - t.Skip("This test will be flaky until golang/go#40269 is resolved.") - runner.Run(t, untidyModule, func(t *testing.T, env *Env) { goModContent := env.ReadWorkspaceFile("a/go.mod") mainContent := env.ReadWorkspaceFile("a/main.go") env.OpenFile("a/main.go") env.SaveBuffer("a/main.go") + // Ensure that we're done processing all the changes caused by opening + // and saving above. If not, we may run into a file locking issue on + // windows. + // + // If this proves insufficient, env.RemoveWorkspaceFile can be updated to + // retry file lock errors on windows. + env.Await( + env.DoneWithOpen(), + env.DoneWithSave(), + env.DoneWithChangeWatchedFiles(), + ) env.RemoveWorkspaceFile("a/main.go") + + // TODO(rfindley): awaiting here shouldn't really be necessary. We should + // be consistent eventually. + // + // Probably this was meant to exercise a race with the change below. env.Await( env.DoneWithOpen(), env.DoneWithSave(), env.DoneWithChangeWatchedFiles(), ) - env.WriteWorkspaceFile("main.go", mainContent) + env.WriteWorkspaceFile("a/main.go", mainContent) env.Await( - env.DiagnosticAtRegexp("main.go", "\"example.com/blah\""), + OnceMet( + env.DoneWithChangeWatchedFiles(), + env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), + ), ) - if got := env.ReadWorkspaceFile("go.mod"); got != goModContent { + if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got)) } }) From 371fc67d3b2466ba157ed3dbd9cbb2249c848eff Mon Sep 17 00:00:00 2001 From: Erik Dubbelboer Date: Fri, 29 Jul 2022 19:27:57 +0000 Subject: [PATCH 156/723] go/tools: add check for time formats with 2006-02-01 yyyy-dd-mm is a time format that isn't really used anywhere [1]. It is much more likely that the user intended to use yyyy-mm-dd instead and made a mistake. This happens quite often [2] because of the unusual way to handle time formatting and parsing in Go. Since the mistake is Go specific and happens so often a vet check will be useful. 1. https://stackoverflow.com/questions/2254014/are-there-locales-or-common-programs-that-use-yyyy-dd-mm-as-the-date-format 2. https://github.com/search?l=&p=1&q=%222006-02-01%22+language%3AGo&type=Code Updates golang/go#48801 Change-Id: I20960c93710766f20a7df90873bff960dea41b28 GitHub-Last-Rev: 496b9917b5eda0525cb75d04a3487d174ccf8fea GitHub-Pull-Request: golang/tools#342 Reviewed-on: https://go-review.googlesource.com/c/tools/+/354010 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Tim King Run-TryBot: Tim King Reviewed-by: Robert Findley --- .../passes/timeformat/testdata/src/a/a.go | 50 +++++++ .../timeformat/testdata/src/a/a.go.golden | 50 +++++++ .../passes/timeformat/testdata/src/b/b.go | 11 ++ go/analysis/passes/timeformat/timeformat.go | 131 ++++++++++++++++++ .../passes/timeformat/timeformat_test.go | 17 +++ gopls/doc/analyzers.md | 11 ++ internal/lsp/source/api_json.go | 10 ++ internal/lsp/source/options.go | 2 + 8 files changed, 282 insertions(+) create mode 100644 go/analysis/passes/timeformat/testdata/src/a/a.go create mode 100644 go/analysis/passes/timeformat/testdata/src/a/a.go.golden create mode 100644 go/analysis/passes/timeformat/testdata/src/b/b.go create mode 100644 go/analysis/passes/timeformat/timeformat.go create mode 100644 go/analysis/passes/timeformat/timeformat_test.go diff --git a/go/analysis/passes/timeformat/testdata/src/a/a.go b/go/analysis/passes/timeformat/testdata/src/a/a.go new file mode 100644 index 00000000000..98481446e55 --- /dev/null +++ b/go/analysis/passes/timeformat/testdata/src/a/a.go @@ -0,0 +1,50 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains tests for the timeformat checker. + +package a + +import ( + "time" + + "b" +) + +func hasError() { + a, _ := time.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00") // want `2006-02-01 should be 2006-01-02` + a.Format(`2006-02-01`) // want `2006-02-01 should be 2006-01-02` + a.Format("2006-02-01 15:04:05") // want `2006-02-01 should be 2006-01-02` + + const c = "2006-02-01" + a.Format(c) // want `2006-02-01 should be 2006-01-02` +} + +func notHasError() { + a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00") + a.Format("2006-01-02") + + const c = "2006-01-02" + a.Format(c) + + v := "2006-02-01" + a.Format(v) // Allowed though variables. + + m := map[string]string{ + "y": "2006-02-01", + } + a.Format(m["y"]) + + s := []string{"2006-02-01"} + a.Format(s[0]) + + a.Format(badFormat()) + + o := b.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00") + o.Format("2006-02-01") +} + +func badFormat() string { + return "2006-02-01" +} diff --git a/go/analysis/passes/timeformat/testdata/src/a/a.go.golden b/go/analysis/passes/timeformat/testdata/src/a/a.go.golden new file mode 100644 index 00000000000..9eccded63b4 --- /dev/null +++ b/go/analysis/passes/timeformat/testdata/src/a/a.go.golden @@ -0,0 +1,50 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains tests for the timeformat checker. + +package a + +import ( + "time" + + "b" +) + +func hasError() { + a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00") // want `2006-02-01 should be 2006-01-02` + a.Format(`2006-01-02`) // want `2006-02-01 should be 2006-01-02` + a.Format("2006-01-02 15:04:05") // want `2006-02-01 should be 2006-01-02` + + const c = "2006-02-01" + a.Format(c) // want `2006-02-01 should be 2006-01-02` +} + +func notHasError() { + a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00") + a.Format("2006-01-02") + + const c = "2006-01-02" + a.Format(c) + + v := "2006-02-01" + a.Format(v) // Allowed though variables. + + m := map[string]string{ + "y": "2006-02-01", + } + a.Format(m["y"]) + + s := []string{"2006-02-01"} + a.Format(s[0]) + + a.Format(badFormat()) + + o := b.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00") + o.Format("2006-02-01") +} + +func badFormat() string { + return "2006-02-01" +} diff --git a/go/analysis/passes/timeformat/testdata/src/b/b.go b/go/analysis/passes/timeformat/testdata/src/b/b.go new file mode 100644 index 00000000000..de5690863c9 --- /dev/null +++ b/go/analysis/passes/timeformat/testdata/src/b/b.go @@ -0,0 +1,11 @@ +package b + +type B struct { +} + +func Parse(string, string) B { + return B{} +} + +func (b B) Format(string) { +} diff --git a/go/analysis/passes/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go new file mode 100644 index 00000000000..9147826b4a1 --- /dev/null +++ b/go/analysis/passes/timeformat/timeformat.go @@ -0,0 +1,131 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package timeformat defines an Analyzer that checks for the use +// of time.Format or time.Parse calls with a bad format. +package timeformat + +import ( + "fmt" + "go/ast" + "go/constant" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" +) + +const badFormat = "2006-02-01" +const goodFormat = "2006-01-02" + +const Doc = `check for calls of (time.Time).Format or time.Parse with 2006-02-01 + +The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm) +format. Internationally, "yyyy-dd-mm" does not occur in common calendar date +standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended. +` + +var Analyzer = &analysis.Analyzer{ + Name: "timeformat", + Doc: Doc, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + +func run(pass *analysis.Pass) (interface{}, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), + } + inspect.Preorder(nodeFilter, func(n ast.Node) { + call := n.(*ast.CallExpr) + fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func) + if !ok { + return + } + if !isTimeDotFormat(fn) && !isTimeDotParse(fn) { + return + } + if len(call.Args) > 0 { + arg := call.Args[0] + badAt := badFormatAt(pass.TypesInfo, arg) + + if badAt > -1 { + // Check if it's a literal string, otherwise we can't suggest a fix. + if _, ok := arg.(*ast.BasicLit); ok { + fmt.Printf("%#v\n", arg) + pos := int(arg.Pos()) + badAt + 1 // +1 to skip the " or ` + end := pos + len(badFormat) + + pass.Report(analysis.Diagnostic{ + Pos: token.Pos(pos), + End: token.Pos(end), + Message: badFormat + " should be " + goodFormat, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace " + badFormat + " with " + goodFormat, + TextEdits: []analysis.TextEdit{{ + Pos: token.Pos(pos), + End: token.Pos(end), + NewText: []byte(goodFormat), + }}, + }}, + }) + } else { + pass.Reportf(arg.Pos(), badFormat+" should be "+goodFormat) + } + } + } + }) + return nil, nil +} + +func isTimeDotFormat(f *types.Func) bool { + if f.Name() != "Format" || f.Pkg().Path() != "time" { + return false + } + sig, ok := f.Type().(*types.Signature) + if !ok { + return false + } + // Verify that the receiver is time.Time. + recv := sig.Recv() + if recv == nil { + return false + } + named, ok := recv.Type().(*types.Named) + return ok && named.Obj().Name() == "Time" +} + +func isTimeDotParse(f *types.Func) bool { + if f.Name() != "Parse" || f.Pkg().Path() != "time" { + return false + } + // Verify that there is no receiver. + sig, ok := f.Type().(*types.Signature) + return ok && sig.Recv() == nil +} + +// badFormatAt return the start of a bad format in e or -1 if no bad format is found. +func badFormatAt(info *types.Info, e ast.Expr) int { + tv, ok := info.Types[e] + if !ok { // no type info, assume good + return -1 + } + + t, ok := tv.Type.(*types.Basic) + if !ok || t.Info()&types.IsString == 0 { + return -1 + } + + if tv.Value == nil { + return -1 + } + + return strings.Index(constant.StringVal(tv.Value), badFormat) +} diff --git a/go/analysis/passes/timeformat/timeformat_test.go b/go/analysis/passes/timeformat/timeformat_test.go new file mode 100644 index 00000000000..86bbe1bb3fb --- /dev/null +++ b/go/analysis/passes/timeformat/timeformat_test.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package timeformat_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/timeformat" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.RunWithSuggestedFixes(t, testdata, timeformat.Analyzer, "a") +} diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 861be1ca62c..90a5118a4b5 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -495,6 +495,17 @@ identifiers. Please see the documentation for package testing in golang.org/pkg/testing for the conventions that are enforced for Tests, Benchmarks, and Examples. +**Enabled by default.** + +## **timeformat** + +check for calls of (time.Time).Format or time.Parse with 2006-02-01 + +The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm) +format. Internationally, "yyyy-dd-mm" does not occur in common calendar date +standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended. + + **Enabled by default.** ## **unmarshal** diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 807f496a6bb..e20b8a671d9 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -378,6 +378,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.", Default: "true", }, + { + Name: "\"timeformat\"", + Doc: "check for calls of (time.Time).Format or time.Parse with 2006-02-01\n\nThe timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)\nformat. Internationally, \"yyyy-dd-mm\" does not occur in common calendar date\nstandards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.\n", + Default: "true", + }, { Name: "\"unmarshal\"", Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.", @@ -966,6 +971,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.", Default: true, }, + { + Name: "timeformat", + Doc: "check for calls of (time.Time).Format or time.Parse with 2006-02-01\n\nThe timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)\nformat. Internationally, \"yyyy-dd-mm\" does not occur in common calendar date\nstandards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.\n", + Default: true, + }, { Name: "unmarshal", Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.", diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 8abcc7452c8..6881a7cb82b 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -43,6 +43,7 @@ import ( "golang.org/x/tools/go/analysis/passes/structtag" "golang.org/x/tools/go/analysis/passes/testinggoroutine" "golang.org/x/tools/go/analysis/passes/tests" + "golang.org/x/tools/go/analysis/passes/timeformat" "golang.org/x/tools/go/analysis/passes/unmarshal" "golang.org/x/tools/go/analysis/passes/unreachable" "golang.org/x/tools/go/analysis/passes/unsafeptr" @@ -1393,6 +1394,7 @@ func defaultAnalyzers() map[string]*Analyzer { useany.Analyzer.Name: {Analyzer: useany.Analyzer, Enabled: false}, infertypeargs.Analyzer.Name: {Analyzer: infertypeargs.Analyzer, Enabled: true}, embeddirective.Analyzer.Name: {Analyzer: embeddirective.Analyzer, Enabled: true}, + timeformat.Analyzer.Name: {Analyzer: timeformat.Analyzer, Enabled: true}, // gofmt -s suite: simplifycompositelit.Analyzer.Name: { From 8b9a1fbdf5c32612d9e258d98eb059b27ad15bfa Mon Sep 17 00:00:00 2001 From: Zvonimir Pavlinovic Date: Tue, 2 Aug 2022 14:48:24 -0700 Subject: [PATCH 157/723] go/callgraph/vta: do not assume that recovers cannot be deferred Otherwise, one has a panic. Change-Id: I850d99ad373ac877bfbc2a8c2ef0c8ac98992dff Reviewed-on: https://go-review.googlesource.com/c/tools/+/420914 Run-TryBot: Zvonimir Pavlinovic TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Tim King --- go/callgraph/vta/graph.go | 4 +++- go/callgraph/vta/testdata/src/panic.go | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 4d0387f12bd..2ad0f89fd83 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -568,7 +568,9 @@ func (b *builder) panic(p *ssa.Panic) { func (b *builder) call(c ssa.CallInstruction) { // When c is r := recover() call register instruction, we add Recover -> r. if bf, ok := c.Common().Value.(*ssa.Builtin); ok && bf.Name() == "recover" { - b.addInFlowEdge(recoverReturn{}, b.nodeFromVal(c.(*ssa.Call))) + if v, ok := c.(ssa.Value); ok { + b.addInFlowEdge(recoverReturn{}, b.nodeFromVal(v)) + } return } diff --git a/go/callgraph/vta/testdata/src/panic.go b/go/callgraph/vta/testdata/src/panic.go index 2d39c70ea89..5ef3548577b 100644 --- a/go/callgraph/vta/testdata/src/panic.go +++ b/go/callgraph/vta/testdata/src/panic.go @@ -27,12 +27,12 @@ func recover2() { func Baz(a A) { defer recover1() + defer recover() panic(a) } // Relevant SSA: // func recover1(): -// 0: // t0 = print("only this recover...":string) // t1 = recover() // t2 = typeassert,ok t1.(I) @@ -53,6 +53,7 @@ func Baz(a A) { // t0 = local A (a) // *t0 = a // defer recover1() +// defer recover() // t1 = *t0 // t2 = make interface{} <- A (t1) // panic t2 From 87f47bbfb4b87de63e0d128b0b43ccba047408bc Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 22 Jul 2022 15:38:01 -0400 Subject: [PATCH 158/723] gopls/internal/regtest/bench: refactor and improve benchmarks Significantly refactor the gopls benchmarks to turn them into proper benchmarks, eliminate the need for passing flags, and allow running them on external gopls processes so that they may be used to test older gopls versions. Doing this required decoupling the benchmarks themselves from the regtest.Runner. Instead, they just create their own regtest.Env to use for scripting operations. In order to facilitate this, I tried to redefine Env as a convenience wrapper around other primitives. By using a separate environment setup for benchmarks, I was able to eliminate a lot of regtest.Options that existed only to prevent the regtest runner from adding instrumentation that would affect benchmarking. This also helped clean up Runner.Run somewhat, though it is still too complicated. Also eliminate the unused AnyDiagnosticAtCurrentVersion, and make a few other TODOs about future cleanup. For golang/go#53992 For golang/go#53538 Change-Id: Idbf923178d4256900c3c05bc8999c0c9839a3c07 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419988 gopls-CI: kokoro Reviewed-by: Peter Weinberger Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/regtest/bench/bench_test.go | 205 ++++++++++++-- .../internal/regtest/bench/completion_test.go | 132 ++++----- .../internal/regtest/bench/didchange_test.go | 77 ++---- gopls/internal/regtest/bench/iwl_test.go | 46 ++-- gopls/internal/regtest/bench/mem_test.go | 32 ++- gopls/internal/regtest/bench/stress_test.go | 101 ++++--- .../regtest/bench/workspace_symbols_test.go | 71 ++--- .../regtest/diagnostics/diagnostics_test.go | 4 +- .../regtest/diagnostics/undeclared_test.go | 4 +- gopls/internal/regtest/misc/failures_test.go | 2 +- gopls/internal/regtest/misc/shared_test.go | 16 +- .../internal/regtest/modfile/modfile_test.go | 2 +- .../regtest/template/template_test.go | 2 +- internal/jsonrpc2/servertest/servertest.go | 2 +- internal/lsp/diagnostics.go | 1 + internal/lsp/fake/editor.go | 16 +- internal/lsp/fake/sandbox.go | 4 + internal/lsp/lsprpc/lsprpc.go | 8 +- internal/lsp/lsprpc/lsprpc_test.go | 6 +- internal/lsp/regtest/env.go | 252 +++++++++--------- internal/lsp/regtest/env_test.go | 12 +- internal/lsp/regtest/expectation.go | 18 -- internal/lsp/regtest/runner.go | 101 ++----- 23 files changed, 578 insertions(+), 536 deletions(-) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index fc1fa7496b0..a3780f02d89 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -5,38 +5,209 @@ package bench import ( + "context" + "flag" "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "sync" "testing" + "time" "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/fakenet" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/lsp/bug" + "golang.org/x/tools/internal/lsp/cache" + "golang.org/x/tools/internal/lsp/fake" + "golang.org/x/tools/internal/lsp/lsprpc" + "golang.org/x/tools/internal/lsp/regtest" . "golang.org/x/tools/internal/lsp/regtest" ) +// This package implements benchmarks that share a common editor session. +// +// It is a work-in-progress. +// +// Remaining TODO(rfindley): +// - add detailed documentation for how to write a benchmark, as a package doc +// - add benchmarks for more features +// - eliminate flags, and just run benchmarks on with a predefined set of +// arguments + func TestMain(m *testing.M) { bug.PanicOnBugs = true - Main(m, hooks.Options) + event.SetExporter(nil) // don't log to stderr + code := doMain(m) + os.Exit(code) +} + +func doMain(m *testing.M) (code int) { + defer func() { + if editor != nil { + if err := editor.Close(context.Background()); err != nil { + fmt.Fprintf(os.Stderr, "closing editor: %v", err) + if code == 0 { + code = 1 + } + } + } + if tempDir != "" { + if err := os.RemoveAll(tempDir); err != nil { + fmt.Fprintf(os.Stderr, "cleaning temp dir: %v", err) + if code == 0 { + code = 1 + } + } + } + }() + return m.Run() } -func benchmarkOptions(dir string) []RunOption { - return []RunOption{ - // Run in an existing directory, since we're trying to simulate known cases - // that cause gopls memory problems. - InExistingDir(dir), - // Skip logs as they buffer up memory unnaturally. - SkipLogs(), - // The Debug server only makes sense if running in singleton mode. - Modes(Default), - // Remove the default timeout. Individual tests should control their - // own graceful termination. - NoDefaultTimeout(), +var ( + workdir = flag.String("workdir", "", "if set, working directory to use for benchmarks; overrides -repo and -commit") + repo = flag.String("repo", "https://go.googlesource.com/tools", "if set (and -workdir is unset), run benchmarks in this repo") + file = flag.String("file", "go/ast/astutil/util.go", "active file, for benchmarks that operate on a file") + commitish = flag.String("commit", "gopls/v0.9.0", "if set (and -workdir is unset), run benchmarks at this commit") + + goplsPath = flag.String("gopls", "", "if set, use this gopls for testing") + + // If non-empty, tempDir is a temporary working dir that was created by this + // test suite. + setupDirOnce sync.Once + tempDir string + + setupEditorOnce sync.Once + sandbox *fake.Sandbox + editor *fake.Editor + awaiter *regtest.Awaiter +) + +// benchmarkDir returns the directory to use for benchmarks. +// +// If -workdir is set, just use that directory. Otherwise, check out a shallow +// copy of -repo at the given -commit, and clean up when the test suite exits. +func benchmarkDir() string { + if *workdir != "" { + return *workdir + } + setupDirOnce.Do(func() { + if *repo == "" { + log.Fatal("-repo must be provided") + } + + if *commitish == "" { + log.Fatal("-commit must be provided") + } + + var err error + tempDir, err = ioutil.TempDir("", "gopls-bench") + if err != nil { + log.Fatal(err) + } + fmt.Printf("checking out %s@%s to %s\n", *repo, *commitish, tempDir) + + // Set a timeout for git fetch. If this proves flaky, it can be removed. + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + // Use a shallow fetch to download just the releveant commit. + shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", *repo, *commitish) + initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit) + initCmd.Dir = tempDir + if err := initCmd.Run(); err != nil { + log.Fatalf("checking out %s: %v", *repo, err) + } + }) + return tempDir +} + +// benchmarkEnv returns a shared benchmark environment +func benchmarkEnv(tb testing.TB) *Env { + setupEditorOnce.Do(func() { + dir := benchmarkDir() + + var err error + sandbox, editor, awaiter, err = connectEditor(dir) + if err != nil { + log.Fatalf("connecting editor: %v", err) + } + + if err := awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil { + panic(err) + } + }) - // Use the actual proxy, since we want our builds to succeed. - GOPROXY("https://proxy.golang.org"), + return &Env{ + T: tb, + Ctx: context.Background(), + Editor: editor, + Sandbox: sandbox, + Awaiter: awaiter, } } -func printBenchmarkResults(result testing.BenchmarkResult) { - fmt.Printf("BenchmarkStatistics\t%s\t%s\n", result.String(), result.MemString()) +// connectEditor connects a fake editor session in the given dir, using the +// given editor config. +func connectEditor(dir string) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) { + s, err := fake.NewSandbox(&fake.SandboxConfig{ + Workdir: dir, + GOPROXY: "https://proxy.golang.org", + }) + if err != nil { + return nil, nil, nil, err + } + + a := regtest.NewAwaiter(s.Workdir) + ts := getServer() + e, err := fake.NewEditor(s, fake.EditorConfig{}).Connect(context.Background(), ts, a.Hooks()) + if err != nil { + return nil, nil, nil, err + } + return s, e, a, nil +} + +// getServer returns a server connector that either starts a new in-process +// server, or starts a separate gopls process. +func getServer() servertest.Connector { + if *goplsPath != "" { + return &SidecarServer{*goplsPath} + } + server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false) + return servertest.NewPipeServer(server, jsonrpc2.NewRawStream) +} + +// A SidecarServer starts (and connects to) a separate gopls process at the +// given path. +type SidecarServer struct { + goplsPath string +} + +// Connect creates new io.Pipes and binds them to the underlying StreamServer. +func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn { + cmd := exec.CommandContext(ctx, *goplsPath, "serve") + + stdin, err := cmd.StdinPipe() + if err != nil { + log.Fatal(err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Fatal(err) + } + cmd.Stderr = os.Stdout + if err := cmd.Start(); err != nil { + log.Fatalf("starting gopls: %v", err) + } + + go cmd.Wait() // to free resources; error is ignored + + clientStream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", stdout, stdin)) + clientConn := jsonrpc2.NewConn(clientStream) + return clientConn } diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go index f9b8445891d..cdafb080924 100644 --- a/gopls/internal/regtest/bench/completion_test.go +++ b/gopls/internal/regtest/bench/completion_test.go @@ -5,7 +5,7 @@ package bench import ( - "flag" + "context" "fmt" "strings" "testing" @@ -15,63 +15,63 @@ import ( "golang.org/x/tools/internal/lsp/fake" ) -// dummyCompletionFunction to test manually configured completion using CLI. -func dummyCompletionFunction() { const s = "placeholder"; fmt.Printf("%s", s) } - type completionBenchOptions struct { - workdir, file, locationRegexp string - printResults bool - // hook to run edits before initial completion, not supported for manually - // configured completions. + file, locationRegexp string + + // hook to run edits before initial completion preCompletionEdits func(*Env) } -var completionOptions = completionBenchOptions{} - -func init() { - flag.StringVar(&completionOptions.workdir, "completion_workdir", "", "directory to run completion benchmarks in") - flag.StringVar(&completionOptions.file, "completion_file", "", "relative path to the file to complete in") - flag.StringVar(&completionOptions.locationRegexp, "completion_regexp", "", "regexp location to complete at") - flag.BoolVar(&completionOptions.printResults, "completion_print_results", false, "whether to print completion results") -} +func benchmarkCompletion(options completionBenchOptions, b *testing.B) { + dir := benchmarkDir() -func benchmarkCompletion(options completionBenchOptions, t *testing.T) { - if completionOptions.workdir == "" { - t.Skip("-completion_workdir not configured, skipping benchmark") + // Use a new environment for each test, to avoid any existing state from the + // previous session. + sandbox, editor, awaiter, err := connectEditor(dir) + if err != nil { + b.Fatal(err) } + ctx := context.Background() + defer func() { + if err := editor.Close(ctx); err != nil { + b.Errorf("closing editor: %v", err) + } + }() + + env := &Env{ + T: b, + Ctx: ctx, + Editor: editor, + Sandbox: sandbox, + Awaiter: awaiter, + } + env.OpenFile(options.file) - opts := stressTestOptions(options.workdir) - - // Completion gives bad results if IWL is not yet complete, so we must await - // it first (and therefore need hooks). - opts = append(opts, SkipHooks(false)) + // Run edits required for this completion. + if options.preCompletionEdits != nil { + options.preCompletionEdits(env) + } - WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) { - env.OpenFile(options.file) + // Run a completion to make sure the system is warm. + pos := env.RegexpSearch(options.file, options.locationRegexp) + completions := env.Completion(options.file, pos) - // Run edits required for this completion. - if options.preCompletionEdits != nil { - options.preCompletionEdits(env) + if testing.Verbose() { + fmt.Println("Results:") + for i := 0; i < len(completions.Items); i++ { + fmt.Printf("\t%d. %v\n", i, completions.Items[i]) } + } - // Run a completion to make sure the system is warm. - pos := env.RegexpSearch(options.file, options.locationRegexp) - completions := env.Completion(options.file, pos) + b.ResetTimer() - if options.printResults { - fmt.Println("Results:") - for i := 0; i < len(completions.Items); i++ { - fmt.Printf("\t%d. %v\n", i, completions.Items[i]) - } + // Use a subtest to ensure that benchmarkCompletion does not itself get + // executed multiple times (as it is doing expensive environment + // initialization). + b.Run("completion", func(b *testing.B) { + for i := 0; i < b.N; i++ { + env.Completion(options.file, pos) } - - results := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - env.Completion(options.file, pos) - } - }) - - printBenchmarkResults(results) }) } @@ -88,26 +88,8 @@ func endPosInBuffer(env *Env, name string) fake.Pos { } } -// Benchmark completion at a specified file and location. When no CLI options -// are specified, this test is skipped. -// To Run (from x/tools/gopls) against the dummy function above: -// -// go test -v ./internal/regtest/bench -run=TestBenchmarkConfiguredCompletion -// -completion_workdir="$HOME/Developer/tools" -// -completion_file="gopls/internal/regtest/completion_bench_test.go" -// -completion_regexp="dummyCompletionFunction.*fmt\.Printf\(\"%s\", s(\))" -func TestBenchmarkConfiguredCompletion(t *testing.T) { - benchmarkCompletion(completionOptions, t) -} - -// To run (from x/tools/gopls): -// go test -v ./internal/regtest/bench -run TestBenchmark<>Completion -// -completion_workdir="$HOME/Developer/tools" -// where <> is one of the tests below. completion_workdir should be path to -// x/tools on your system. - // Benchmark struct completion in tools codebase. -func TestBenchmarkStructCompletion(t *testing.T) { +func BenchmarkStructCompletion(b *testing.B) { file := "internal/lsp/cache/session.go" preCompletionEdits := func(env *Env) { @@ -120,26 +102,22 @@ func TestBenchmarkStructCompletion(t *testing.T) { } benchmarkCompletion(completionBenchOptions{ - workdir: completionOptions.workdir, file: file, locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`, preCompletionEdits: preCompletionEdits, - printResults: completionOptions.printResults, - }, t) + }, b) } // Benchmark import completion in tools codebase. -func TestBenchmarkImportCompletion(t *testing.T) { +func BenchmarkImportCompletion(b *testing.B) { benchmarkCompletion(completionBenchOptions{ - workdir: completionOptions.workdir, file: "internal/lsp/source/completion/completion.go", locationRegexp: `go\/()`, - printResults: completionOptions.printResults, - }, t) + }, b) } // Benchmark slice completion in tools codebase. -func TestBenchmarkSliceCompletion(t *testing.T) { +func BenchmarkSliceCompletion(b *testing.B) { file := "internal/lsp/cache/session.go" preCompletionEdits := func(env *Env) { @@ -152,16 +130,14 @@ func TestBenchmarkSliceCompletion(t *testing.T) { } benchmarkCompletion(completionBenchOptions{ - workdir: completionOptions.workdir, file: file, locationRegexp: `var testVariable \[\]byte (=)`, preCompletionEdits: preCompletionEdits, - printResults: completionOptions.printResults, - }, t) + }, b) } // Benchmark deep completion in function call in tools codebase. -func TestBenchmarkFuncDeepCompletion(t *testing.T) { +func BenchmarkFuncDeepCompletion(b *testing.B) { file := "internal/lsp/source/completion/completion.go" fileContent := ` func (c *completer) _() { @@ -178,10 +154,8 @@ func (c *completer) _() { } benchmarkCompletion(completionBenchOptions{ - workdir: completionOptions.workdir, file: file, locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, preCompletionEdits: preCompletionEdits, - printResults: completionOptions.printResults, - }, t) + }, b) } diff --git a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go index 76f40be6e68..8fcf25362be 100644 --- a/gopls/internal/regtest/bench/didchange_test.go +++ b/gopls/internal/regtest/bench/didchange_test.go @@ -5,10 +5,7 @@ package bench import ( - "flag" "fmt" - "os" - "runtime/pprof" "testing" "golang.org/x/tools/internal/lsp/fake" @@ -16,64 +13,28 @@ import ( . "golang.org/x/tools/internal/lsp/regtest" ) -var ( - benchDir = flag.String("didchange_dir", "", "If set, run benchmarks in this dir. Must also set didchange_file.") - benchFile = flag.String("didchange_file", "", "The file to modify") - benchProfile = flag.String("didchange_cpuprof", "", "file to write cpu profiling data to") -) - -// TestBenchmarkDidChange benchmarks modifications of a single file by making +// BenchmarkDidChange benchmarks modifications of a single file by making // synthetic modifications in a comment. It controls pacing by waiting for the // server to actually start processing the didChange notification before // proceeding. Notably it does not wait for diagnostics to complete. // -// Run it by passing -didchange_dir and -didchange_file, where -didchange_dir -// is the path to a workspace root, and -didchange_file is the -// workspace-relative path to a file to modify. e.g.: -// -// go test -run=TestBenchmarkDidChange \ -// -didchange_dir=path/to/kubernetes \ -// -didchange_file=pkg/util/hash/hash.go -func TestBenchmarkDidChange(t *testing.T) { - if *benchDir == "" { - t.Skip("-didchange_dir is not set") - } - if *benchFile == "" { - t.Fatal("-didchange_file must be set if -didchange_dir is set") - } - - opts := benchmarkOptions(*benchDir) - WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { - env.OpenFile(*benchFile) - env.Await(env.DoneWithOpen()) - // Insert the text we'll be modifying at the top of the file. - env.EditBuffer(*benchFile, fake.Edit{Text: "// __REGTEST_PLACEHOLDER_0__\n"}) - - // Run the profiler after the initial load, - // across all benchmark iterations. - if *benchProfile != "" { - profile, err := os.Create(*benchProfile) - if err != nil { - t.Fatal(err) - } - defer profile.Close() - if err := pprof.StartCPUProfile(profile); err != nil { - t.Fatal(err) - } - defer pprof.StopCPUProfile() - } - - result := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - env.EditBuffer(*benchFile, fake.Edit{ - Start: fake.Pos{Line: 0, Column: 0}, - End: fake.Pos{Line: 1, Column: 0}, - // Increment - Text: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1), - }) - env.Await(StartedChange(uint64(i + 1))) - } +// Uses -workdir and -file to control where the edits occur. +func BenchmarkDidChange(b *testing.B) { + env := benchmarkEnv(b) + env.OpenFile(*file) + env.Await(env.DoneWithOpen()) + + // Insert the text we'll be modifying at the top of the file. + env.EditBuffer(*file, fake.Edit{Text: "// __REGTEST_PLACEHOLDER_0__\n"}) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + env.EditBuffer(*file, fake.Edit{ + Start: fake.Pos{Line: 0, Column: 0}, + End: fake.Pos{Line: 1, Column: 0}, + // Increment the placeholder text, to ensure cache misses. + Text: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1), }) - printBenchmarkResults(result) - }) + env.Await(StartedChange(uint64(i + 1))) + } } diff --git a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go index 62faa34fac0..e262a398f1d 100644 --- a/gopls/internal/regtest/bench/iwl_test.go +++ b/gopls/internal/regtest/bench/iwl_test.go @@ -5,35 +5,31 @@ package bench import ( - "flag" + "context" "testing" . "golang.org/x/tools/internal/lsp/regtest" ) -var iwlOptions struct { - workdir string -} - -func init() { - flag.StringVar(&iwlOptions.workdir, "iwl_workdir", "", "if set, run IWL benchmark in this directory") -} - -func TestBenchmarkIWL(t *testing.T) { - if iwlOptions.workdir == "" { - t.Skip("-iwl_workdir not configured") - } - - opts := stressTestOptions(iwlOptions.workdir) - // Don't skip hooks, so that we can wait for IWL. - opts = append(opts, SkipHooks(false)) - - results := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {}) - +// BenchmarkIWL benchmarks the initial workspace load time for a new editing +// session. +func BenchmarkIWL(b *testing.B) { + dir := benchmarkDir() + b.ResetTimer() + + ctx := context.Background() + for i := 0; i < b.N; i++ { + _, editor, awaiter, err := connectEditor(dir) + if err != nil { + b.Fatal(err) } - }) - - printBenchmarkResults(results) + if err := awaiter.Await(ctx, InitialWorkspaceLoad); err != nil { + b.Fatal(err) + } + b.StopTimer() + if err := editor.Close(ctx); err != nil { + b.Fatal(err) + } + b.StartTimer() + } } diff --git a/gopls/internal/regtest/bench/mem_test.go b/gopls/internal/regtest/bench/mem_test.go index f48b60b0b6f..19626785acc 100644 --- a/gopls/internal/regtest/bench/mem_test.go +++ b/gopls/internal/regtest/bench/mem_test.go @@ -7,8 +7,6 @@ package bench import ( "runtime" "testing" - - . "golang.org/x/tools/internal/lsp/regtest" ) // TestPrintMemStats measures the memory usage of loading a project. @@ -17,25 +15,25 @@ import ( // // Kubernetes example: // -// $ go test -v -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes +// $ go test -v -run=TestPrintMemStats -workdir=$HOME/w/kubernetes // TotalAlloc: 5766 MB // HeapAlloc: 1984 MB // // Both figures exhibit variance of less than 1%. func TestPrintMemStats(t *testing.T) { - if *benchDir == "" { - t.Skip("-didchange_dir is not set") - } + // This test only makes sense when run in isolation, so for now it is + // manually skipped. + // + // TODO(rfindley): figure out a better way to capture memstats as a benchmark + // metric. + t.Skip("unskip to run this test manually") + + _ = benchmarkEnv(t) - // Load the program... - opts := benchmarkOptions(*benchDir) - WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { - // ...and print the memory usage. - runtime.GC() - runtime.GC() - var mem runtime.MemStats - runtime.ReadMemStats(&mem) - t.Logf("TotalAlloc:\t%d MB", mem.TotalAlloc/1e6) - t.Logf("HeapAlloc:\t%d MB", mem.HeapAlloc/1e6) - }) + runtime.GC() + runtime.GC() + var mem runtime.MemStats + runtime.ReadMemStats(&mem) + t.Logf("TotalAlloc:\t%d MB", mem.TotalAlloc/1e6) + t.Logf("HeapAlloc:\t%d MB", mem.HeapAlloc/1e6) } diff --git a/gopls/internal/regtest/bench/stress_test.go b/gopls/internal/regtest/bench/stress_test.go index f7e59faf97f..a410c3049c0 100644 --- a/gopls/internal/regtest/bench/stress_test.go +++ b/gopls/internal/regtest/bench/stress_test.go @@ -11,56 +11,83 @@ import ( "testing" "time" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" + "golang.org/x/tools/internal/lsp/cache" + "golang.org/x/tools/internal/lsp/fake" + "golang.org/x/tools/internal/lsp/lsprpc" ) -// Pilosa is a repository that has historically caused significant memory -// problems for Gopls. We use it for a simple stress test that types -// arbitrarily in a file with lots of dependents. +// github.com/pilosa/pilosa is a repository that has historically caused +// significant memory problems for Gopls. We use it for a simple stress test +// that types arbitrarily in a file with lots of dependents. var pilosaPath = flag.String("pilosa_path", "", "Path to a directory containing "+ "github.com/pilosa/pilosa, for stress testing. Do not set this unless you "+ "know what you're doing!") -func stressTestOptions(dir string) []RunOption { - opts := benchmarkOptions(dir) - opts = append(opts, SkipHooks(true), DebugAddress(":8087")) - return opts -} - func TestPilosaStress(t *testing.T) { + // TODO(rfindley): revisit this test and make it is hermetic: it should check + // out pilosa into a directory. + // + // Note: This stress test has not been run recently, and may no longer + // function properly. if *pilosaPath == "" { t.Skip("-pilosa_path not configured") } - opts := stressTestOptions(*pilosaPath) - WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { - files := []string{ - "cmd.go", - "internal/private.pb.go", - "roaring/roaring.go", - "roaring/roaring_internal_test.go", - "server/handler_test.go", - } - for _, file := range files { - env.OpenFile(file) + sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ + Workdir: *pilosaPath, + GOPROXY: "https://proxy.golang.org", + }) + if err != nil { + t.Fatal(err) + } + + server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false) + ts := servertest.NewPipeServer(server, jsonrpc2.NewRawStream) + ctx := context.Background() + + editor, err := fake.NewEditor(sandbox, fake.EditorConfig{}).Connect(ctx, ts, fake.ClientHooks{}) + if err != nil { + t.Fatal(err) + } + + files := []string{ + "cmd.go", + "internal/private.pb.go", + "roaring/roaring.go", + "roaring/roaring_internal_test.go", + "server/handler_test.go", + } + for _, file := range files { + if err := editor.OpenFile(ctx, file); err != nil { + t.Fatal(err) } - ctx, cancel := context.WithTimeout(env.Ctx, 10*time.Minute) - defer cancel() + } + ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) + defer cancel() - i := 1 - // MagicNumber is an identifier that occurs in roaring.go. Just change it - // arbitrarily. - env.RegexpReplace("roaring/roaring.go", "MagicNumber", fmt.Sprintf("MagicNumber%d", 1)) - for { - select { - case <-ctx.Done(): - return - default: - } - env.RegexpReplace("roaring/roaring.go", fmt.Sprintf("MagicNumber%d", i), fmt.Sprintf("MagicNumber%d", i+1)) - time.Sleep(20 * time.Millisecond) - i++ + i := 1 + // MagicNumber is an identifier that occurs in roaring.go. Just change it + // arbitrarily. + if err := editor.RegexpReplace(ctx, "roaring/roaring.go", "MagicNumber", fmt.Sprintf("MagicNumber%d", 1)); err != nil { + t.Fatal(err) + } + for { + select { + case <-ctx.Done(): + return + default: } - }) + if err := editor.RegexpReplace(ctx, "roaring/roaring.go", fmt.Sprintf("MagicNumber%d", i), fmt.Sprintf("MagicNumber%d", i+1)); err != nil { + t.Fatal(err) + } + // Simulate (very fast) typing. + // + // Typing 80 wpm ~150ms per keystroke. + time.Sleep(150 * time.Millisecond) + i++ + } } diff --git a/gopls/internal/regtest/bench/workspace_symbols_test.go b/gopls/internal/regtest/bench/workspace_symbols_test.go index ad258dce54a..fccc8182997 100644 --- a/gopls/internal/regtest/bench/workspace_symbols_test.go +++ b/gopls/internal/regtest/bench/workspace_symbols_test.go @@ -8,67 +8,28 @@ import ( "flag" "fmt" "testing" - - "golang.org/x/tools/internal/lsp/protocol" - - . "golang.org/x/tools/internal/lsp/regtest" ) -var symbolOptions struct { - workdir, query, matcher, style string - printResults bool -} +var symbolQuery = flag.String("symbol_query", "test", "symbol query to use in benchmark") -func init() { - flag.StringVar(&symbolOptions.workdir, "symbol_workdir", "", "if set, run symbol benchmark in this directory") - flag.StringVar(&symbolOptions.query, "symbol_query", "test", "symbol query to use in benchmark") - flag.StringVar(&symbolOptions.matcher, "symbol_matcher", "", "symbol matcher to use in benchmark") - flag.StringVar(&symbolOptions.style, "symbol_style", "", "symbol style to use in benchmark") - flag.BoolVar(&symbolOptions.printResults, "symbol_print_results", false, "whether to print symbol query results") -} +// BenchmarkWorkspaceSymbols benchmarks the time to execute a workspace symbols +// request (controlled by the -symbol_query flag). +func BenchmarkWorkspaceSymbols(b *testing.B) { + env := benchmarkEnv(b) -func TestBenchmarkSymbols(t *testing.T) { - if symbolOptions.workdir == "" { - t.Skip("-symbol_workdir not configured") - } + // Make an initial symbol query to warm the cache. + symbols := env.WorkspaceSymbol(*symbolQuery) - opts := benchmarkOptions(symbolOptions.workdir) - settings := make(Settings) - if symbolOptions.matcher != "" { - settings["symbolMatcher"] = symbolOptions.matcher - } - if symbolOptions.style != "" { - settings["symbolStyle"] = symbolOptions.style + if testing.Verbose() { + fmt.Println("Results:") + for i := 0; i < len(symbols); i++ { + fmt.Printf("\t%d. %s (%s)\n", i, symbols[i].Name, symbols[i].ContainerName) + } } - opts = append(opts, settings) - WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) { - // We can't Await in this test, since we have disabled hooks. Instead, run - // one symbol request to completion to ensure all necessary cache entries - // are populated. - symbols, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{ - Query: symbolOptions.query, - }) - if err != nil { - t.Fatal(err) - } + b.ResetTimer() - if symbolOptions.printResults { - fmt.Println("Results:") - for i := 0; i < len(symbols); i++ { - fmt.Printf("\t%d. %s (%s)\n", i, symbols[i].Name, symbols[i].ContainerName) - } - } - - results := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - if _, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{ - Query: symbolOptions.query, - }); err != nil { - t.Fatal(err) - } - } - }) - printBenchmarkResults(results) - }) + for i := 0; i < b.N; i++ { + env.WorkspaceSymbol(*symbolQuery) + } } diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index f915fccbd14..d7246ae7df6 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1296,7 +1296,7 @@ func main() {} Run(t, dir, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OpenFile("other.go") - x := env.DiagnosticsFor("main.go") + x := env.Awaiter.DiagnosticsFor("main.go") if x == nil { t.Fatalf("expected 1 diagnostic, got none") } @@ -1304,7 +1304,7 @@ func main() {} t.Fatalf("main.go, got %d diagnostics, expected 1", len(x.Diagnostics)) } keep := x.Diagnostics[0] - y := env.DiagnosticsFor("other.go") + y := env.Awaiter.DiagnosticsFor("other.go") if len(y.Diagnostics) != 1 { t.Fatalf("other.go: got %d diagnostics, expected 1", len(y.Diagnostics)) } diff --git a/gopls/internal/regtest/diagnostics/undeclared_test.go b/gopls/internal/regtest/diagnostics/undeclared_test.go index 79f7d42675b..ed2b1d0a630 100644 --- a/gopls/internal/regtest/diagnostics/undeclared_test.go +++ b/gopls/internal/regtest/diagnostics/undeclared_test.go @@ -45,7 +45,7 @@ func _() int { // 'x' is undeclared, but still necessary. env.OpenFile("a/a.go") env.Await(env.DiagnosticAtRegexp("a/a.go", "x")) - diags := env.DiagnosticsFor("a/a.go") + diags := env.Awaiter.DiagnosticsFor("a/a.go") if got := len(diags.Diagnostics); got != 1 { t.Errorf("len(Diagnostics) = %d, want 1", got) } @@ -56,7 +56,7 @@ func _() int { // 'y = y' is pointless, and should be detected as unnecessary. env.OpenFile("b/b.go") env.Await(env.DiagnosticAtRegexp("b/b.go", "y = y")) - diags = env.DiagnosticsFor("b/b.go") + diags = env.Awaiter.DiagnosticsFor("b/b.go") if got := len(diags.Diagnostics); got != 1 { t.Errorf("len(Diagnostics) = %d, want 1", got) } diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index 23fccfd628d..86c9b223c94 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -29,7 +29,7 @@ func main() { var err error err.Error() }` - WithOptions(SkipLogs()).Run(t, mod, func(t *testing.T, env *Env) { + Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error")) // without the //line comment content would be non-nil diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index 170e164c94c..e433f4bd4e2 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -7,6 +7,7 @@ package misc import ( "testing" + "golang.org/x/tools/internal/lsp/fake" . "golang.org/x/tools/internal/lsp/regtest" ) @@ -33,8 +34,19 @@ func runShared(t *testing.T, testFunc func(origEnv *Env, otherEnv *Env)) { WithOptions(Modes(modes)).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { // Create a second test session connected to the same workspace and server // as the first. - env2, cleanup := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config(), true) - defer cleanup() + awaiter := NewAwaiter(env1.Sandbox.Workdir) + editor, err := fake.NewEditor(env1.Sandbox, env1.Editor.Config()).Connect(env1.Ctx, env1.Server, awaiter.Hooks()) + if err != nil { + t.Fatal(err) + } + env2 := &Env{ + T: t, + Ctx: env1.Ctx, + Sandbox: env1.Sandbox, + Server: env1.Server, + Editor: editor, + Awaiter: awaiter, + } env2.Await(InitialWorkspaceLoad) testFunc(env1, env2) }) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 91c8005be98..a32a06a32fc 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -572,7 +572,7 @@ var _ = blah.Name env.DiagnosticAtRegexpWithMessage("a/main.go", `"example.com/blah/v2"`, "cannot find module providing"), env.DiagnosticAtRegexpWithMessage("a/go.mod", `require example.com/blah/v2`, "cannot find module providing"), ) - env.ApplyQuickFixes("a/go.mod", env.DiagnosticsFor("a/go.mod").Diagnostics) + env.ApplyQuickFixes("a/go.mod", env.Awaiter.DiagnosticsFor("a/go.mod").Diagnostics) const want = `module mod.com go 1.12 diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index 292088ce59f..ade9ac93068 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -71,7 +71,7 @@ Hello {{}} <-- missing body ).Run(t, files, func(t *testing.T, env *Env) { // TODO: can we move this diagnostic onto {{}}? env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) - d := env.DiagnosticsFor("hello.tmpl").Diagnostics // issue 50786: check for Source + d := env.Awaiter.DiagnosticsFor("hello.tmpl").Diagnostics // issue 50786: check for Source if len(d) != 1 { t.Errorf("expected 1 diagnostic, got %d", len(d)) return diff --git a/internal/jsonrpc2/servertest/servertest.go b/internal/jsonrpc2/servertest/servertest.go index b879ebdf181..37f8475bee2 100644 --- a/internal/jsonrpc2/servertest/servertest.go +++ b/internal/jsonrpc2/servertest/servertest.go @@ -50,7 +50,7 @@ func NewTCPServer(ctx context.Context, server jsonrpc2.StreamServer, framer json // Connect dials the test server and returns a jsonrpc2 Connection that is // ready for use. -func (s *TCPServer) Connect(ctx context.Context) jsonrpc2.Conn { +func (s *TCPServer) Connect(_ context.Context) jsonrpc2.Conn { netConn, err := net.Dial("tcp", s.Addr) if err != nil { panic(fmt.Sprintf("servertest: failed to connect to test instance: %v", err)) diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 642957cbadc..19776140659 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -545,6 +545,7 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so s.diagnosticsMu.Lock() defer s.diagnosticsMu.Unlock() + // TODO(rfindley): remove this noisy (and not useful) logging. published := 0 defer func() { log.Trace.Logf(ctx, "published %d diagnostics", published) diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index a07e9e7c661..bc2cb2fe9b1 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -17,9 +17,11 @@ import ( "sync" "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/xcontext" ) // Editor is a fake editor client. It keeps track of client state and can be @@ -29,6 +31,7 @@ type Editor struct { // Server, client, and sandbox are concurrency safe and written only // at construction time, so do not require synchronization. Server protocol.Server + cancelConn func() serverConn jsonrpc2.Conn client *Client sandbox *Sandbox @@ -119,14 +122,19 @@ func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { // It returns the editor, so that it may be called as follows: // // editor, err := NewEditor(s).Connect(ctx, conn) -func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) { +func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, hooks ClientHooks) (*Editor, error) { + bgCtx, cancelConn := context.WithCancel(xcontext.Detach(ctx)) + conn := connector.Connect(bgCtx) + e.cancelConn = cancelConn + e.serverConn = conn e.Server = protocol.ServerDispatcher(conn) e.client = &Client{editor: e, hooks: hooks} - conn.Go(ctx, + conn.Go(bgCtx, protocol.Handlers( protocol.ClientHandler(e.client, jsonrpc2.MethodNotFound))) + if err := e.initialize(ctx, e.config.WorkspaceFolders); err != nil { return nil, err } @@ -170,6 +178,10 @@ func (e *Editor) Close(ctx context.Context) error { if err := e.Exit(ctx); err != nil { return err } + defer func() { + e.cancelConn() + }() + // called close on the editor should result in the connection closing select { case <-e.serverConn.Done(): diff --git a/internal/lsp/fake/sandbox.go b/internal/lsp/fake/sandbox.go index 7efbdb3ae58..72b846cdc99 100644 --- a/internal/lsp/fake/sandbox.go +++ b/internal/lsp/fake/sandbox.go @@ -70,6 +70,10 @@ type SandboxConfig struct { // If rootDir is non-empty, it will be used as the root of temporary // directories created for the sandbox. Otherwise, a new temporary directory // will be used as root. +// +// TODO(rfindley): the sandbox abstraction doesn't seem to carry its weight. +// Sandboxes should be composed out of their building-blocks, rather than via a +// monolithic configuration. func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) { if config == nil { config = new(SandboxConfig) diff --git a/internal/lsp/lsprpc/lsprpc.go b/internal/lsp/lsprpc/lsprpc.go index a85e7914219..7e37229b1fd 100644 --- a/internal/lsp/lsprpc/lsprpc.go +++ b/internal/lsp/lsprpc/lsprpc.go @@ -56,7 +56,9 @@ func (s *StreamServer) Binder() *ServerBinder { server := s.serverForTest if server == nil { server = lsp.NewServer(session, client) - debug.GetInstance(ctx).AddService(server, session) + if instance := debug.GetInstance(ctx); instance != nil { + instance.AddService(server, session) + } } return server } @@ -71,7 +73,9 @@ func (s *StreamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) erro server := s.serverForTest if server == nil { server = lsp.NewServer(session, client) - debug.GetInstance(ctx).AddService(server, session) + if instance := debug.GetInstance(ctx); instance != nil { + instance.AddService(server, session) + } } // Clients may or may not send a shutdown message. Make sure the server is // shut down. diff --git a/internal/lsp/lsprpc/lsprpc_test.go b/internal/lsp/lsprpc/lsprpc_test.go index 498566d1bbd..b43629b19e5 100644 --- a/internal/lsp/lsprpc/lsprpc_test.go +++ b/internal/lsp/lsprpc/lsprpc_test.go @@ -226,14 +226,12 @@ func TestDebugInfoLifecycle(t *testing.T) { } tsForwarder := servertest.NewPipeServer(forwarder, nil) - conn1 := tsForwarder.Connect(clientCtx) - ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, conn1, fake.ClientHooks{}) + ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, tsForwarder, fake.ClientHooks{}) if err != nil { t.Fatal(err) } defer ed1.Close(clientCtx) - conn2 := tsBackend.Connect(baseCtx) - ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, conn2, fake.ClientHooks{}) + ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, tsBackend, fake.ClientHooks{}) if err != nil { t.Fatal(err) } diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go index 8960a0dc913..502636a8503 100644 --- a/internal/lsp/regtest/env.go +++ b/internal/lsp/regtest/env.go @@ -14,25 +14,36 @@ import ( "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/xcontext" ) -// Env holds an initialized fake Editor, Workspace, and Server, which may be -// used for writing tests. It also provides adapter methods that call t.Fatal -// on any error, so that tests for the happy path may be written without -// checking errors. +// Env holds the building blocks of an editor testing environment, providing +// wrapper methods that hide the boilerplate of plumbing contexts and checking +// errors. type Env struct { - T testing.TB + T testing.TB // TODO(rfindley): rename to TB Ctx context.Context // Most tests should not need to access the scratch area, editor, server, or // connection, but they are available if needed. Sandbox *fake.Sandbox - Editor *fake.Editor Server servertest.Connector - // mu guards the fields below, for the purpose of checking conditions on - // every change to diagnostics. + // Editor is owned by the Env, and shut down + Editor *fake.Editor + + Awaiter *Awaiter +} + +// An Awaiter keeps track of relevant LSP state, so that it may be asserted +// upon with Expectations. +// +// Wire it into a fake.Editor using Awaiter.Hooks(). +// +// TODO(rfindley): consider simply merging Awaiter with the fake.Editor. It +// probably is not worth its own abstraction. +type Awaiter struct { + workdir *fake.Workdir + mu sync.Mutex // For simplicity, each waiter gets a unique ID. nextWaiterID int @@ -40,6 +51,32 @@ type Env struct { waiters map[int]*condition } +func NewAwaiter(workdir *fake.Workdir) *Awaiter { + return &Awaiter{ + workdir: workdir, + state: State{ + diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), + outstandingWork: make(map[protocol.ProgressToken]*workProgress), + startedWork: make(map[string]uint64), + completedWork: make(map[string]uint64), + }, + waiters: make(map[int]*condition), + } +} + +func (a *Awaiter) Hooks() fake.ClientHooks { + return fake.ClientHooks{ + OnDiagnostics: a.onDiagnostics, + OnLogMessage: a.onLogMessage, + OnWorkDoneProgressCreate: a.onWorkDoneProgressCreate, + OnProgress: a.onProgress, + OnShowMessage: a.onShowMessage, + OnShowMessageRequest: a.onShowMessageRequest, + OnRegistration: a.onRegistration, + OnUnregistration: a.onUnregistration, + } +} + // State encapsulates the server state TODO: explain more type State struct { // diagnostics are a map of relative path->diagnostics params @@ -108,103 +145,55 @@ type condition struct { verdict chan Verdict } -// NewEnv creates a new test environment using the given scratch environment -// and gopls server. -// -// The resulting cleanup func must be called to close the jsonrpc2 connection. -// -// TODO(rfindley): this function provides questionable value. Consider -// refactoring to move things like creating the server outside of this -// constructor. -func NewEnv(ctx context.Context, tb testing.TB, sandbox *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig, withHooks bool) (_ *Env, cleanup func()) { - tb.Helper() - - bgCtx, cleanupConn := context.WithCancel(xcontext.Detach(ctx)) - conn := ts.Connect(bgCtx) - - env := &Env{ - T: tb, - Ctx: ctx, - Sandbox: sandbox, - Server: ts, - state: State{ - diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), - outstandingWork: make(map[protocol.ProgressToken]*workProgress), - startedWork: make(map[string]uint64), - completedWork: make(map[string]uint64), - }, - waiters: make(map[int]*condition), - } - var hooks fake.ClientHooks - if withHooks { - hooks = fake.ClientHooks{ - OnDiagnostics: env.onDiagnostics, - OnLogMessage: env.onLogMessage, - OnWorkDoneProgressCreate: env.onWorkDoneProgressCreate, - OnProgress: env.onProgress, - OnShowMessage: env.onShowMessage, - OnShowMessageRequest: env.onShowMessageRequest, - OnRegistration: env.onRegistration, - OnUnregistration: env.onUnregistration, - } - } - editor, err := fake.NewEditor(sandbox, editorConfig).Connect(bgCtx, conn, hooks) - if err != nil { - tb.Fatal(err) - } - env.Editor = editor - return env, cleanupConn -} - -func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error { + a.mu.Lock() + defer a.mu.Unlock() - pth := e.Sandbox.Workdir.URIToPath(d.URI) - e.state.diagnostics[pth] = d - e.checkConditionsLocked() + pth := a.workdir.URIToPath(d.URI) + a.state.diagnostics[pth] = d + a.checkConditionsLocked() return nil } -func (e *Env) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.showMessage = append(e.state.showMessage, m) - e.checkConditionsLocked() + a.state.showMessage = append(a.state.showMessage, m) + a.checkConditionsLocked() return nil } -func (e *Env) onShowMessageRequest(_ context.Context, m *protocol.ShowMessageRequestParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onShowMessageRequest(_ context.Context, m *protocol.ShowMessageRequestParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.showMessageRequest = append(e.state.showMessageRequest, m) - e.checkConditionsLocked() + a.state.showMessageRequest = append(a.state.showMessageRequest, m) + a.checkConditionsLocked() return nil } -func (e *Env) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.logs = append(e.state.logs, m) - e.checkConditionsLocked() + a.state.logs = append(a.state.logs, m) + a.checkConditionsLocked() return nil } -func (e *Env) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.outstandingWork[m.Token] = &workProgress{} + a.state.outstandingWork[m.Token] = &workProgress{} return nil } -func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error { - e.mu.Lock() - defer e.mu.Unlock() - work, ok := e.state.outstandingWork[m.Token] +func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) error { + a.mu.Lock() + defer a.mu.Unlock() + work, ok := a.state.outstandingWork[m.Token] if !ok { panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m)) } @@ -212,7 +201,7 @@ func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error { switch kind := v["kind"]; kind { case "begin": work.title = v["title"].(string) - e.state.startedWork[work.title] = e.state.startedWork[work.title] + 1 + a.state.startedWork[work.title] = a.state.startedWork[work.title] + 1 if msg, ok := v["message"]; ok { work.msg = msg.(string) } @@ -224,36 +213,36 @@ func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error { work.msg = msg.(string) } case "end": - title := e.state.outstandingWork[m.Token].title - e.state.completedWork[title] = e.state.completedWork[title] + 1 - delete(e.state.outstandingWork, m.Token) + title := a.state.outstandingWork[m.Token].title + a.state.completedWork[title] = a.state.completedWork[title] + 1 + delete(a.state.outstandingWork, m.Token) } - e.checkConditionsLocked() + a.checkConditionsLocked() return nil } -func (e *Env) onRegistration(_ context.Context, m *protocol.RegistrationParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onRegistration(_ context.Context, m *protocol.RegistrationParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.registrations = append(e.state.registrations, m) - e.checkConditionsLocked() + a.state.registrations = append(a.state.registrations, m) + a.checkConditionsLocked() return nil } -func (e *Env) onUnregistration(_ context.Context, m *protocol.UnregistrationParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onUnregistration(_ context.Context, m *protocol.UnregistrationParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.unregistrations = append(e.state.unregistrations, m) - e.checkConditionsLocked() + a.state.unregistrations = append(a.state.unregistrations, m) + a.checkConditionsLocked() return nil } -func (e *Env) checkConditionsLocked() { - for id, condition := range e.waiters { - if v, _ := checkExpectations(e.state, condition.expectations); v != Unmet { - delete(e.waiters, id) +func (a *Awaiter) checkConditionsLocked() { + for id, condition := range a.waiters { + if v, _ := checkExpectations(a.state, condition.expectations); v != Unmet { + delete(a.waiters, id) condition.verdict <- v } } @@ -276,53 +265,62 @@ func checkExpectations(s State, expectations []Expectation) (Verdict, string) { // DiagnosticsFor returns the current diagnostics for the file. It is useful // after waiting on AnyDiagnosticAtCurrentVersion, when the desired diagnostic // is not simply described by DiagnosticAt. -func (e *Env) DiagnosticsFor(name string) *protocol.PublishDiagnosticsParams { - e.mu.Lock() - defer e.mu.Unlock() - return e.state.diagnostics[name] +// +// TODO(rfindley): this method is inherently racy. Replace usages of this +// method with the atomic OnceMet(..., ReadDiagnostics) pattern. +func (a *Awaiter) DiagnosticsFor(name string) *protocol.PublishDiagnosticsParams { + a.mu.Lock() + defer a.mu.Unlock() + return a.state.diagnostics[name] +} + +func (e *Env) Await(expectations ...Expectation) { + if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil { + e.T.Fatal(err) + } } // Await waits for all expectations to simultaneously be met. It should only be // called from the main test goroutine. -func (e *Env) Await(expectations ...Expectation) { - e.T.Helper() - e.mu.Lock() +func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error { + a.mu.Lock() // Before adding the waiter, we check if the condition is currently met or // failed to avoid a race where the condition was realized before Await was // called. - switch verdict, summary := checkExpectations(e.state, expectations); verdict { + switch verdict, summary := checkExpectations(a.state, expectations); verdict { case Met: - e.mu.Unlock() - return + a.mu.Unlock() + return nil case Unmeetable: - failure := fmt.Sprintf("unmeetable expectations:\n%s\nstate:\n%v", summary, e.state) - e.mu.Unlock() - e.T.Fatal(failure) + err := fmt.Errorf("unmeetable expectations:\n%s\nstate:\n%v", summary, a.state) + a.mu.Unlock() + return err } cond := &condition{ expectations: expectations, verdict: make(chan Verdict), } - e.waiters[e.nextWaiterID] = cond - e.nextWaiterID++ - e.mu.Unlock() + a.waiters[a.nextWaiterID] = cond + a.nextWaiterID++ + a.mu.Unlock() var err error select { - case <-e.Ctx.Done(): - err = e.Ctx.Err() + case <-ctx.Done(): + err = ctx.Err() case v := <-cond.verdict: if v != Met { err = fmt.Errorf("condition has final verdict %v", v) } } - e.mu.Lock() - defer e.mu.Unlock() - _, summary := checkExpectations(e.state, expectations) + a.mu.Lock() + defer a.mu.Unlock() + _, summary := checkExpectations(a.state, expectations) // Debugging an unmet expectation can be tricky, so we put some effort into // nicely formatting the failure. if err != nil { - e.T.Fatalf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, e.state) + return fmt.Errorf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, a.state) } + return nil } diff --git a/internal/lsp/regtest/env_test.go b/internal/lsp/regtest/env_test.go index fe5864ca77c..f54f7f29dc4 100644 --- a/internal/lsp/regtest/env_test.go +++ b/internal/lsp/regtest/env_test.go @@ -13,7 +13,7 @@ import ( ) func TestProgressUpdating(t *testing.T) { - e := &Env{ + a := &Awaiter{ state: State{ outstandingWork: make(map[protocol.ProgressToken]*workProgress), startedWork: make(map[string]uint64), @@ -21,12 +21,12 @@ func TestProgressUpdating(t *testing.T) { }, } ctx := context.Background() - if err := e.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ + if err := a.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ Token: "foo", }); err != nil { t.Fatal(err) } - if err := e.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ + if err := a.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ Token: "bar", }); err != nil { t.Fatal(err) @@ -53,14 +53,14 @@ func TestProgressUpdating(t *testing.T) { if err := json.Unmarshal(data, &unmarshaled); err != nil { t.Fatal(err) } - if err := e.onProgress(ctx, &unmarshaled); err != nil { + if err := a.onProgress(ctx, &unmarshaled); err != nil { t.Fatal(err) } } - if _, ok := e.state.outstandingWork["foo"]; ok { + if _, ok := a.state.outstandingWork["foo"]; ok { t.Error("got work entry for \"foo\", want none") } - got := *e.state.outstandingWork["bar"] + got := *a.state.outstandingWork["bar"] want := workProgress{title: "bar work", percent: 42} if got != want { t.Errorf("work progress for \"bar\": %v, want %v", got, want) diff --git a/internal/lsp/regtest/expectation.go b/internal/lsp/regtest/expectation.go index 32538851ee1..a0a7d529aae 100644 --- a/internal/lsp/regtest/expectation.go +++ b/internal/lsp/regtest/expectation.go @@ -617,24 +617,6 @@ func NoDiagnostics(name string) Expectation { } } -// AnyDiagnosticAtCurrentVersion asserts that there is a diagnostic report for -// the current edited version of the buffer corresponding to the given -// workdir-relative pathname. -func (e *Env) AnyDiagnosticAtCurrentVersion(name string) Expectation { - version := e.Editor.BufferVersion(name) - check := func(s State) Verdict { - diags, ok := s.diagnostics[name] - if ok && diags.Version == int32(version) { - return Met - } - return Unmet - } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("any diagnostics at version %d", version), - } -} - // DiagnosticAtRegexp expects that there is a diagnostic entry at the start // position matching the regexp search string re in the buffer specified by // name. Note that this currently ignores the end position. diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go index 12fd323abf5..93bb1397eb2 100644 --- a/internal/lsp/regtest/runner.go +++ b/internal/lsp/regtest/runner.go @@ -132,13 +132,10 @@ type Runner struct { } type runConfig struct { - editor fake.EditorConfig - sandbox fake.SandboxConfig - modes Mode - noDefaultTimeout bool - debugAddr string - skipLogs bool - skipHooks bool + editor fake.EditorConfig + sandbox fake.SandboxConfig + modes Mode + skipHooks bool } // A RunOption augments the behavior of the test runner. @@ -152,15 +149,6 @@ func (f optionSetter) set(opts *runConfig) { f(opts) } -// NoDefaultTimeout removes the timeout set by the -regtest_timeout flag, for -// individual tests that are expected to run longer than is reasonable for -// ordinary regression tests. -func NoDefaultTimeout() RunOption { - return optionSetter(func(opts *runConfig) { - opts.noDefaultTimeout = true - }) -} - // ProxyFiles configures a file proxy using the given txtar-encoded string. func ProxyFiles(txt string) RunOption { return optionSetter(func(opts *runConfig) { @@ -241,50 +229,6 @@ func InGOPATH() RunOption { }) } -// DebugAddress configures a debug server bound to addr. This option is -// currently only supported when executing in Default mode. It is intended to -// be used for long-running stress tests. -func DebugAddress(addr string) RunOption { - return optionSetter(func(opts *runConfig) { - opts.debugAddr = addr - }) -} - -// SkipLogs skips the buffering of logs during test execution. It is intended -// for long-running stress tests. -func SkipLogs() RunOption { - return optionSetter(func(opts *runConfig) { - opts.skipLogs = true - }) -} - -// InExistingDir runs the test in a pre-existing directory. If set, no initial -// files may be passed to the runner. It is intended for long-running stress -// tests. -func InExistingDir(dir string) RunOption { - return optionSetter(func(opts *runConfig) { - opts.sandbox.Workdir = dir - }) -} - -// SkipHooks allows for disabling the test runner's client hooks that are used -// for instrumenting expectations (tracking diagnostics, logs, work done, -// etc.). It is intended for performance-sensitive stress tests or benchmarks. -func SkipHooks(skip bool) RunOption { - return optionSetter(func(opts *runConfig) { - opts.skipHooks = skip - }) -} - -// GOPROXY configures the test environment to have an explicit proxy value. -// This is intended for stress tests -- to ensure their isolation, regtests -// should instead use WithProxyFiles. -func GOPROXY(goproxy string) RunOption { - return optionSetter(func(opts *runConfig) { - opts.sandbox.GOPROXY = goproxy - }) -} - type TestFunc func(t *testing.T, env *Env) // Run executes the test function in the default configured gopls execution @@ -321,20 +265,13 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio continue } - if config.debugAddr != "" && tc.mode != Default { - // Debugging is useful for running stress tests, but since the daemon has - // likely already been started, it would be too late to debug. - t.Fatalf("debugging regtest servers only works in Default mode, "+ - "got debug addr %q and mode %v", config.debugAddr, tc.mode) - } - t.Run(tc.name, func(t *testing.T) { // TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak // goroutines in this test function. // stacktest.NoLeak(t) ctx := context.Background() - if r.Timeout != 0 && !config.noDefaultTimeout { + if r.Timeout != 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, r.Timeout) defer cancel() @@ -345,12 +282,8 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio defer cancel() } + // TODO(rfindley): do we need an instance at all? Can it be removed? ctx = debug.WithInstance(ctx, "", "off") - if config.debugAddr != "" { - di := debug.GetInstance(ctx) - di.Serve(ctx, config.debugAddr) - di.MonitorMemory(ctx) - } rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name())) if err := os.MkdirAll(rootDir, 0755); err != nil { @@ -382,12 +315,22 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio framer := jsonrpc2.NewRawStream ls := &loggingFramer{} - if !config.skipLogs { - framer = ls.framer(jsonrpc2.NewRawStream) - } + framer = ls.framer(jsonrpc2.NewRawStream) ts := servertest.NewPipeServer(ss, framer) - env, cleanup := NewEnv(ctx, t, sandbox, ts, config.editor, !config.skipHooks) - defer cleanup() + + awaiter := NewAwaiter(sandbox.Workdir) + editor, err := fake.NewEditor(sandbox, config.editor).Connect(ctx, ts, awaiter.Hooks()) + if err != nil { + t.Fatal(err) + } + env := &Env{ + T: t, + Ctx: ctx, + Sandbox: sandbox, + Editor: editor, + Server: ts, + Awaiter: awaiter, + } defer func() { if t.Failed() && r.PrintGoroutinesOnFailure { pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) @@ -402,7 +345,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio // the editor: in general we want to clean up before proceeding to the // next test, and if there is a deadlock preventing closing it will // eventually be handled by the `go test` timeout. - if err := env.Editor.Close(xcontext.Detach(ctx)); err != nil { + if err := editor.Close(xcontext.Detach(ctx)); err != nil { t.Errorf("closing editor: %v", err) } }() From 3e0a5031e3cec09527c92842fdf38af0fb48c252 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 3 Aug 2022 17:21:23 -0400 Subject: [PATCH 159/723] internal/lsp: use directoryFilters in import scanning Based on dle8's CL 414454, wire in directoryFilters into the goimports ModuleResolver scan. A few changes/fixes/additions from that CL: - Fix a bug in filter validation that was causing -**/a not to validate. - Fix a bug in regex building where -a was not treated as an excluded prefix (it was matching 'a' anywhere). - Use absolute paths in the SkipPathInScan, so that we can evaluate directory filters relative to the workspace folder. - Add several regression tests. - Consolidate directoryFilters regression tests under a new directoryfilters_test.go file. - Add several TODOs. Fixes golang/go#46438 Change-Id: I55cd3d6410905216cc8cfbdf528f301d67c2bbac Reviewed-on: https://go-review.googlesource.com/c/tools/+/420959 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Dylan Le --- .../workspace/directoryfilters_test.go | 252 ++++++++++++++++++ .../regtest/workspace/workspace_test.go | 117 -------- internal/imports/fix.go | 5 + internal/imports/mod.go | 10 + internal/lsp/cache/imports.go | 20 +- internal/lsp/cache/session.go | 12 + internal/lsp/cache/view.go | 2 + internal/lsp/source/options.go | 2 +- internal/lsp/source/workspace_symbol.go | 13 +- internal/lsp/source/workspace_symbol_test.go | 5 + 10 files changed, 305 insertions(+), 133 deletions(-) create mode 100644 gopls/internal/regtest/workspace/directoryfilters_test.go diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go new file mode 100644 index 00000000000..bdc60a06ee3 --- /dev/null +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -0,0 +1,252 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package workspace + +import ( + "sort" + "strings" + "testing" + + . "golang.org/x/tools/internal/lsp/regtest" +) + +// This file contains regression tests for the directoryFilters setting. +// +// TODO: +// - consolidate some of these tests into a single test +// - add more tests for changing directory filters + +func TestDirectoryFilters(t *testing.T) { + WithOptions( + ProxyFiles(workspaceProxy), + WorkspaceFolders("pkg"), + Settings{ + "directoryFilters": []string{"-inner"}, + }, + ).Run(t, workspaceModule, func(t *testing.T, env *Env) { + syms := env.WorkspaceSymbol("Hi") + sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) + for _, s := range syms { + if strings.Contains(s.ContainerName, "inner") { + t.Errorf("WorkspaceSymbol: found symbol %q with container %q, want \"inner\" excluded", s.Name, s.ContainerName) + } + } + }) +} + +func TestDirectoryFiltersLoads(t *testing.T) { + // exclude, and its error, should be excluded from the workspace. + const files = ` +-- go.mod -- +module example.com + +go 1.12 +-- exclude/exclude.go -- +package exclude + +const _ = Nonexistant +` + + WithOptions( + Settings{"directoryFilters": []string{"-exclude"}}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.Await(NoDiagnostics("exclude/x.go")) + }) +} + +func TestDirectoryFiltersTransitiveDep(t *testing.T) { + // Even though exclude is excluded from the workspace, it should + // still be importable as a non-workspace package. + const files = ` +-- go.mod -- +module example.com + +go 1.12 +-- include/include.go -- +package include +import "example.com/exclude" + +const _ = exclude.X +-- exclude/exclude.go -- +package exclude + +const _ = Nonexistant // should be ignored, since this is a non-workspace package +const X = 1 +` + + WithOptions( + Settings{"directoryFilters": []string{"-exclude"}}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.Await( + NoDiagnostics("exclude/exclude.go"), // filtered out + NoDiagnostics("include/include.go"), // successfully builds + ) + }) +} + +func TestDirectoryFiltersWorkspaceModules(t *testing.T) { + // Define a module include.com which should be in the workspace, plus a + // module exclude.com which should be excluded and therefore come from + // the proxy. + const files = ` +-- include/go.mod -- +module include.com + +go 1.12 + +require exclude.com v1.0.0 + +-- include/go.sum -- +exclude.com v1.0.0 h1:Q5QSfDXY5qyNCBeUiWovUGqcLCRZKoTs9XdBeVz+w1I= +exclude.com v1.0.0/go.mod h1:hFox2uDlNB2s2Jfd9tHlQVfgqUiLVTmh6ZKat4cvnj4= + +-- include/include.go -- +package include + +import "exclude.com" + +var _ = exclude.X // satisfied only by the workspace version +-- exclude/go.mod -- +module exclude.com + +go 1.12 +-- exclude/exclude.go -- +package exclude + +const X = 1 +` + const proxy = ` +-- exclude.com@v1.0.0/go.mod -- +module exclude.com + +go 1.12 +-- exclude.com@v1.0.0/exclude.go -- +package exclude +` + WithOptions( + Modes(Experimental), + ProxyFiles(proxy), + Settings{"directoryFilters": []string{"-exclude"}}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`)) + }) +} + +// Test for golang/go#46438: support for '**' in directory filters. +func TestDirectoryFilters_Wildcard(t *testing.T) { + filters := []string{"-**/bye"} + WithOptions( + ProxyFiles(workspaceProxy), + WorkspaceFolders("pkg"), + Settings{ + "directoryFilters": filters, + }, + ).Run(t, workspaceModule, func(t *testing.T, env *Env) { + syms := env.WorkspaceSymbol("Bye") + sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) + for _, s := range syms { + if strings.Contains(s.ContainerName, "bye") { + t.Errorf("WorkspaceSymbol: found symbol %q with container %q with filters %v", s.Name, s.ContainerName, filters) + } + } + }) +} + +// Test for golang/go#52993: wildcard directoryFilters should apply to +// goimports scanning as well. +func TestDirectoryFilters_ImportScanning(t *testing.T) { + const files = ` +-- go.mod -- +module mod.test + +go 1.12 +-- main.go -- +package main + +func main() { + bye.Goodbye() +} +-- p/bye/bye.go -- +package bye + +func Goodbye() {} +` + + WithOptions( + Settings{ + "directoryFilters": []string{"-**/bye"}, + }, + // This test breaks in 'Experimental' mode, because with + // experimentalWorkspaceModule set we the goimports scan behaves + // differently. + // + // Since this feature is going away (golang/go#52897), don't investigate. + Modes(Default), + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + beforeSave := env.Editor.BufferText("main.go") + env.OrganizeImports("main.go") + got := env.Editor.BufferText("main.go") + if got != beforeSave { + t.Errorf("after organizeImports code action, got modified buffer:\n%s", got) + } + }) +} + +// Test for golang/go#52993: non-wildcard directoryFilters should still be +// applied relative to the workspace folder, not the module root. +func TestDirectoryFilters_MultiRootImportScanning(t *testing.T) { + const files = ` +-- go.work -- +go 1.18 + +use ( + a + b +) +-- a/go.mod -- +module mod1.test + +go 1.18 +-- a/main.go -- +package main + +func main() { + hi.Hi() +} +-- a/hi/hi.go -- +package hi + +func Hi() {} +-- b/go.mod -- +module mod2.test + +go 1.18 +-- b/main.go -- +package main + +func main() { + hi.Hi() +} +-- b/hi/hi.go -- +package hi + +func Hi() {} +` + + WithOptions( + Settings{ + "directoryFilters": []string{"-hi"}, // this test fails with -**/hi + }, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("a/main.go") + beforeSave := env.Editor.BufferText("a/main.go") + env.OrganizeImports("a/main.go") + got := env.Editor.BufferText("a/main.go") + if got == beforeSave { + t.Errorf("after organizeImports code action, got identical buffer:\n%s", got) + } + }) +} diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index d7a0c7f7e1d..86da9d1c938 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -7,7 +7,6 @@ package workspace import ( "fmt" "path/filepath" - "sort" "strings" "testing" @@ -137,24 +136,6 @@ func TestReferences(t *testing.T) { } } -func TestDirectoryFilters(t *testing.T) { - WithOptions( - ProxyFiles(workspaceProxy), - WorkspaceFolders("pkg"), - Settings{ - "directoryFilters": []string{"-inner"}, - }, - ).Run(t, workspaceModule, func(t *testing.T, env *Env) { - syms := env.WorkspaceSymbol("Hi") - sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) - for _, s := range syms { - if strings.Contains(s.ContainerName, "inner") { - t.Errorf("WorkspaceSymbol: found symbol %q with container %q, want \"inner\" excluded", s.Name, s.ContainerName) - } - } - }) -} - // Make sure that analysis diagnostics are cleared for the whole package when // the only opened file is closed. This test was inspired by the experience in // VS Code, where clicking on a reference result triggers a @@ -994,104 +975,6 @@ func main() { }) } -func TestDirectoryFiltersLoads(t *testing.T) { - // exclude, and its error, should be excluded from the workspace. - const files = ` --- go.mod -- -module example.com - -go 1.12 --- exclude/exclude.go -- -package exclude - -const _ = Nonexistant -` - - WithOptions( - Settings{"directoryFilters": []string{"-exclude"}}, - ).Run(t, files, func(t *testing.T, env *Env) { - env.Await(NoDiagnostics("exclude/x.go")) - }) -} - -func TestDirectoryFiltersTransitiveDep(t *testing.T) { - // Even though exclude is excluded from the workspace, it should - // still be importable as a non-workspace package. - const files = ` --- go.mod -- -module example.com - -go 1.12 --- include/include.go -- -package include -import "example.com/exclude" - -const _ = exclude.X --- exclude/exclude.go -- -package exclude - -const _ = Nonexistant // should be ignored, since this is a non-workspace package -const X = 1 -` - - WithOptions( - Settings{"directoryFilters": []string{"-exclude"}}, - ).Run(t, files, func(t *testing.T, env *Env) { - env.Await( - NoDiagnostics("exclude/exclude.go"), // filtered out - NoDiagnostics("include/include.go"), // successfully builds - ) - }) -} - -func TestDirectoryFiltersWorkspaceModules(t *testing.T) { - // Define a module include.com which should be in the workspace, plus a - // module exclude.com which should be excluded and therefore come from - // the proxy. - const files = ` --- include/go.mod -- -module include.com - -go 1.12 - -require exclude.com v1.0.0 - --- include/go.sum -- -exclude.com v1.0.0 h1:Q5QSfDXY5qyNCBeUiWovUGqcLCRZKoTs9XdBeVz+w1I= -exclude.com v1.0.0/go.mod h1:hFox2uDlNB2s2Jfd9tHlQVfgqUiLVTmh6ZKat4cvnj4= - --- include/include.go -- -package include - -import "exclude.com" - -var _ = exclude.X // satisfied only by the workspace version --- exclude/go.mod -- -module exclude.com - -go 1.12 --- exclude/exclude.go -- -package exclude - -const X = 1 -` - const proxy = ` --- exclude.com@v1.0.0/go.mod -- -module exclude.com - -go 1.12 --- exclude.com@v1.0.0/exclude.go -- -package exclude -` - WithOptions( - Modes(Experimental), - ProxyFiles(proxy), - Settings{"directoryFilters": []string{"-exclude"}}, - ).Run(t, files, func(t *testing.T, env *Env) { - env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`)) - }) -} - // Confirm that a fix for a tidy module will correct all modules in the // workspace. func TestMultiModule_OneBrokenModule(t *testing.T) { diff --git a/internal/imports/fix.go b/internal/imports/fix.go index 9e373d64ebc..45a492ef039 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -807,6 +807,11 @@ type ProcessEnv struct { ModFlag string ModFile string + // SkipPathInScan returns true if the path should be skipped from scans of + // the RootCurrentModule root type. The function argument is a clean, + // absolute path. + SkipPathInScan func(string) bool + // Env overrides the OS environment, and can be used to specify // GOPROXY, GO111MODULE, etc. PATH cannot be set here, because // exec.Command will not honor it. diff --git a/internal/imports/mod.go b/internal/imports/mod.go index 46693f24339..dec388bc099 100644 --- a/internal/imports/mod.go +++ b/internal/imports/mod.go @@ -466,6 +466,16 @@ func (r *ModuleResolver) scan(ctx context.Context, callback *scanCallback) error // We assume cached directories are fully cached, including all their // children, and have not changed. We can skip them. skip := func(root gopathwalk.Root, dir string) bool { + if r.env.SkipPathInScan != nil && root.Type == gopathwalk.RootCurrentModule { + if root.Path == dir { + return false + } + + if r.env.SkipPathInScan(filepath.Clean(dir)) { + return true + } + } + info, ok := r.cacheLoad(dir) if !ok { return false diff --git a/internal/lsp/cache/imports.go b/internal/lsp/cache/imports.go index a08953db646..6510bbd5734 100644 --- a/internal/lsp/cache/imports.go +++ b/internal/lsp/cache/imports.go @@ -23,13 +23,14 @@ import ( type importsState struct { ctx context.Context - mu sync.Mutex - processEnv *imports.ProcessEnv - cleanupProcessEnv func() - cacheRefreshDuration time.Duration - cacheRefreshTimer *time.Timer - cachedModFileHash source.Hash - cachedBuildFlags []string + mu sync.Mutex + processEnv *imports.ProcessEnv + cleanupProcessEnv func() + cacheRefreshDuration time.Duration + cacheRefreshTimer *time.Timer + cachedModFileHash source.Hash + cachedBuildFlags []string + cachedDirectoryFilters []string } func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(*imports.Options) error) error { @@ -70,9 +71,11 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot snapshot.view.optionsMu.Lock() localPrefix := snapshot.view.options.Local currentBuildFlags := snapshot.view.options.BuildFlags + currentDirectoryFilters := snapshot.view.options.DirectoryFilters changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) || snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) || - modFileHash != s.cachedModFileHash + modFileHash != s.cachedModFileHash || + !reflect.DeepEqual(snapshot.view.options.DirectoryFilters, s.cachedDirectoryFilters) snapshot.view.optionsMu.Unlock() // If anything relevant to imports has changed, clear caches and @@ -92,6 +95,7 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot } s.cachedModFileHash = modFileHash s.cachedBuildFlags = currentBuildFlags + s.cachedDirectoryFilters = currentDirectoryFilters var err error s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot) if err != nil { diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index a77deb62118..984e8c1e75b 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "strconv" + "strings" "sync" "sync/atomic" @@ -228,6 +229,17 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, ctx: backgroundCtx, processEnv: &imports.ProcessEnv{ GocmdRunner: s.gocmdRunner, + SkipPathInScan: func(dir string) bool { + prefix := strings.TrimSuffix(string(v.folder), "/") + "/" + uri := strings.TrimSuffix(string(span.URIFromPath(dir)), "/") + if !strings.HasPrefix(uri+"/", prefix) { + return false + } + filterer := source.NewFilterer(options.DirectoryFilters) + rel := strings.TrimPrefix(uri, prefix) + disallow := filterer.Disallow(rel) + return disallow + }, }, } v.snapshot = &snapshot{ diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index cddd4fa5d63..61501098c07 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -1136,6 +1136,8 @@ func pathExcludedByFilter(path string, filterer *source.Filterer) bool { } func buildFilterer(root, gomodcache string, opts *source.Options) *source.Filterer { + // TODO(rfindley): this looks wrong. If gomodcache isn't actually nested + // under root, this will do the wrong thing. gomodcache = strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(gomodcache, root)), "/") filters := opts.DirectoryFilters if gomodcache != "" { diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 6881a7cb82b..971fa069d4a 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -840,7 +840,7 @@ func validateDirectoryFilter(ifilter string) (string, error) { if filter == "" || (filter[0] != '+' && filter[0] != '-') { return "", fmt.Errorf("invalid filter %v, must start with + or -", filter) } - segs := strings.Split(filter, "/") + segs := strings.Split(filter[1:], "/") unsupportedOps := [...]string{"?", "*"} for _, seg := range segs { if seg != "**" { diff --git a/internal/lsp/source/workspace_symbol.go b/internal/lsp/source/workspace_symbol.go index 6c33effc1ae..e9da569d02c 100644 --- a/internal/lsp/source/workspace_symbol.go +++ b/internal/lsp/source/workspace_symbol.go @@ -406,16 +406,15 @@ func (f *Filterer) Disallow(path string) bool { // Supporting glob-like operators: // - **: match zero or more complete path segments func convertFilterToRegexp(filter string) *regexp.Regexp { + if filter == "" { + return regexp.MustCompile(".*") + } var ret strings.Builder + ret.WriteString("^") segs := strings.Split(filter, "/") - for i, seg := range segs { + for _, seg := range segs { if seg == "**" { - switch i { - case 0: - ret.WriteString("^.*") - default: - ret.WriteString(".*") - } + ret.WriteString(".*") } else { ret.WriteString(regexp.QuoteMeta(seg)) } diff --git a/internal/lsp/source/workspace_symbol_test.go b/internal/lsp/source/workspace_symbol_test.go index 633550ed945..24fb8b45210 100644 --- a/internal/lsp/source/workspace_symbol_test.go +++ b/internal/lsp/source/workspace_symbol_test.go @@ -112,6 +112,11 @@ func TestFiltererDisallow(t *testing.T) { []string{"a/b/c.go"}, []string{}, }, + { + []string{"-b"}, // should only filter paths prefixed with the "b" directory + []string{"a/b/c.go", "bb"}, + []string{"b/c/d.go", "b"}, + }, } for _, test := range tests { From bceee4b059478407879662c536527cffdadac0d5 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 2 Aug 2022 12:19:22 -0400 Subject: [PATCH 160/723] internal/lsp/command: let RunVulncheckExp call gopls vulncheck By making gopls.run_vulncheck_exp (RunVulncheckExp implements) call `gopls vulncheck`, we achieve - gopls.run_vulncheck_exp can run asynchronously and be cancellable - log information can be forwarded as progress messages - isolate any failures during vulncheck execution In this CL, we also changed not to include test files in the analysis (match the default of govulncheck). We will add an option in the future. TODO: - prevent concurrent gopls.run_vulncheck_exp - convert the gopls vulncheck output to diagnostics and publish it - remove timestamps from the `gopls vulncheck` log messages for simplify progress messages - add test to check vulnerability in third-party dependencies Change-Id: I21592e03794cd9e9d96ed3989973a2ab7d75c538 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420717 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Reviewed-by: Suzy Mueller Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro --- gopls/doc/commands.md | 20 ------ .../testdata/vulndb/golang.org/x/crypto.json | 1 + .../testdata/vulndb/golang.org/x/text.json | 1 + .../regtest/misc/testdata/vulndb/stdlib.json | 1 + gopls/internal/regtest/misc/vuln_test.go | 56 +++++++++++++++ internal/lsp/command.go | 72 +++++++++++++++---- internal/lsp/command/command_gen.go | 2 +- internal/lsp/command/interface.go | 5 +- internal/lsp/source/api_json.go | 9 ++- 9 files changed, 123 insertions(+), 44 deletions(-) create mode 100644 gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json create mode 100644 gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json create mode 100644 gopls/internal/regtest/misc/testdata/vulndb/stdlib.json diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index f868a48936e..37d8dcbdcca 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -281,26 +281,6 @@ Args: } ``` -Result: - -``` -{ - "Vuln": []{ - "ID": string, - "Details": string, - "Aliases": []string, - "Symbol": string, - "PkgPath": string, - "ModPath": string, - "URL": string, - "CurrentVersion": string, - "FixedVersion": string, - "CallStacks": [][]golang.org/x/tools/internal/lsp/command.StackEntry, - "CallStackSummaries": []string, - }, -} -``` - ### **Start the gopls debug server** Identifier: `gopls.start_debugging` diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json new file mode 100644 index 00000000000..e0a279129dd --- /dev/null +++ b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json @@ -0,0 +1 @@ +[{"id":"GO-2020-0012","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2020-9283"],"details":"An attacker can craft an ssh-ed25519 or sk-ssh-ed25519@openssh.com public\nkey, such that the library will panic when trying to verify a signature\nwith it. If verifying signatures using user supplied public keys, this\nmay be used as a denial of service vector.\n","affected":[{"package":{"name":"golang.org/x/crypto/ssh","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20200220183623-bac4c82f6975"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0012"},"ecosystem_specific":{"symbols":["parseED25519","ed25519PublicKey.Verify","parseSKEd25519","skEd25519PublicKey.Verify","NewPublicKey"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/crypto/+/220357"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/bac4c82f69751a6dd76e702d54b3ceb88adab236"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/3L45YRc91SY"}]},{"id":"GO-2020-0013","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2017-3204"],"details":"By default host key verification is disabled which allows for\nman-in-the-middle attacks against SSH clients if\nClientConfig.HostKeyCallback is not set.\n","affected":[{"package":{"name":"golang.org/x/crypto/ssh","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20170330155735-e4e2799dd7aa"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0013"},"ecosystem_specific":{"symbols":["NewClientConn"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/38701"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/e4e2799dd7aab89f583e1d898300d96367750991"},{"type":"WEB","url":"https://go.dev/issue/19767"},{"type":"WEB","url":"https://bridge.grumpy-troll.org/2017/04/golang-ssh-security/"}]},{"id":"GO-2021-0227","published":"2022-02-17T17:35:32Z","modified":"2022-02-17T17:35:32Z","aliases":["CVE-2020-29652"],"details":"Clients can cause a panic in SSH servers. An attacker can craft\nan authentication request message for the “gssapi-with-mic” method\nwhich will cause NewServerConn to panic via a nil pointer dereference\nif ServerConfig.GSSAPIWithMICConfig is nil.\n","affected":[{"package":{"name":"golang.org/x/crypto/ssh","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20201216223049-8b5274cf687f"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0227"},"ecosystem_specific":{"symbols":["connection.serverAuthenticate"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/crypto/+/278852"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/8b5274cf687fd9316b4108863654cc57385531e8"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/ouZIlBimOsE?pli=1"}]}] \ No newline at end of file diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json new file mode 100644 index 00000000000..eee052ffcd3 --- /dev/null +++ b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json @@ -0,0 +1 @@ +[{"id":"GO-2020-0015","published":"2021-04-14T20:04:52Z","modified":"2021-06-07T12:00:00Z","aliases":["CVE-2020-14040"],"details":"An attacker could provide a single byte to a UTF16 decoder instantiated with\nUseBOM or ExpectBOM to trigger an infinite loop if the String function on\nthe Decoder is called, or the Decoder is passed to transform.String.\nIf used to parse user supplied input, this may be used as a denial of service\nvector.\n","affected":[{"package":{"name":"golang.org/x/text/encoding/unicode","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.3"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0015"},"ecosystem_specific":{"symbols":["utf16Decoder.Transform","bomOverride.Transform"]}},{"package":{"name":"golang.org/x/text/transform","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.3"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0015"},"ecosystem_specific":{"symbols":["Transform"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/text/+/238238"},{"type":"FIX","url":"https://go.googlesource.com/text/+/23ae387dee1f90d29a23c0e87ee0b46038fbed0e"},{"type":"WEB","url":"https://go.dev/issue/39491"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/bXVeAmGOqz0"}]},{"id":"GO-2021-0113","published":"2021-10-06T17:51:21Z","modified":"2021-10-06T17:51:21Z","aliases":["CVE-2021-38561"],"details":"Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n","affected":[{"package":{"name":"golang.org/x/text/language","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.7"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0113"},"ecosystem_specific":{"symbols":["Parse","MatchStrings","MustParse","ParseAcceptLanguage"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/text/+/340830"},{"type":"FIX","url":"https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f"}]}] \ No newline at end of file diff --git a/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json b/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json new file mode 100644 index 00000000000..240ee494309 --- /dev/null +++ b/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json @@ -0,0 +1 @@ +[{"id":"STD","affected":[{"package":{"name":"archive/zip"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.18.0"}]}],"ecosystem_specific":{"symbols":["OpenReader"]}}]}] diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 94fde715c77..d39fd74ef57 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -5,11 +5,14 @@ package misc import ( + "os" + "path/filepath" "testing" "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/protocol" . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" ) func TestRunVulncheckExpError(t *testing.T) { @@ -41,3 +44,56 @@ package foo } }) } + +func TestRunVulncheckExp(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- main.go -- +package main + +import ( + "archive/zip" + "fmt" +) + +func main() { + _, err := zip.OpenReader("file.zip") // vulnerable. + fmt.Println(err) +} +` + + cwd, _ := os.Getwd() + WithOptions( + EnvVars{ + // Let the analyzer read vulnerabilities data from the testdata/vulndb. + "GOVULNDB": "file://" + filepath.Join(cwd, "testdata", "vulndb"), + // When fetchinging stdlib package vulnerability info, + // behave as if our go version is go1.18 for this testing. + // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). + // See gopls/internal/vulncheck.goVersion + // which follows the convention used in golang.org/x/vuln/cmd/govulncheck. + "GOVERSION": "go1.18", + "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", + }, + ).Run(t, files, func(t *testing.T, env *Env) { + cmd, err := command.NewRunVulncheckExpCommand("Run Vulncheck Exp", command.VulncheckArgs{ + Dir: env.Sandbox.Workdir.RootURI(), + }) + if err != nil { + t.Fatal(err) + } + + env.ExecuteCommand(&protocol.ExecuteCommandParams{ + Command: command.RunVulncheckExp.ID(), + Arguments: cmd.Arguments, + }, nil) + env.Await( + CompletedWork("Checking vulnerability", 1, true), + // TODO(hyangah): once the diagnostics are published, wait for diagnostics. + ) + }) +} diff --git a/internal/lsp/command.go b/internal/lsp/command.go index c173ef23543..23ade896a25 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -13,13 +13,13 @@ import ( "io" "io/ioutil" "os" + "os/exec" "path/filepath" "sort" "strings" "golang.org/x/mod/modfile" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/lsp/command" @@ -790,9 +790,29 @@ func (c *commandHandler) StartDebugging(ctx context.Context, args command.Debugg return result, nil } -func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) (result command.VulncheckResult, _ error) { +// Copy of pkgLoadConfig defined in internal/lsp/cmd/vulncheck.go +// TODO(hyangah): decide where to define this. +type pkgLoadConfig struct { + // BuildFlags is a list of command-line flags to be passed through to + // the build system's query tool. + BuildFlags []string + + // Env is the environment to use when invoking the build system's query tool. + // If Env is nil, the current environment is used. + // TODO: This seems unnecessary. Delete. + Env []string + + // If Tests is set, the loader includes related test packages. + Tests bool +} + +func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) error { + if args.Dir == "" { + return errors.New("VulncheckArgs is missing Dir field") + } err := c.run(ctx, commandConfig{ - progress: "Running vulncheck", + async: true, // need to be async to be cancellable + progress: "Checking vulnerability", requireSave: true, forURI: args.Dir, // Will dir work? }, func(ctx context.Context, deps commandDeps) error { @@ -802,22 +822,44 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc return errors.New("vulncheck feature is not available") } - buildFlags := opts.BuildFlags // XXX: is session.Options equivalent to view.Options? + cmd := exec.Command(os.Args[0], "vulncheck", "-config", args.Pattern) + cmd.Dir = args.Dir.SpanURI().Filename() + var viewEnv []string if e := opts.EnvSlice(); e != nil { viewEnv = append(os.Environ(), e...) } - cfg := &packages.Config{ - Context: ctx, - Tests: true, // TODO(hyangah): add a field in args. - BuildFlags: buildFlags, - Env: viewEnv, - Dir: args.Dir.SpanURI().Filename(), - // TODO(hyangah): configure overlay + cmd.Env = viewEnv + + // stdin: gopls vulncheck expects JSON-encoded configuration from STDIN when -config flag is set. + var stdin bytes.Buffer + cmd.Stdin = &stdin + + if err := json.NewEncoder(&stdin).Encode(pkgLoadConfig{ + BuildFlags: opts.BuildFlags, + // TODO(hyangah): add `tests` flag in command.VulncheckArgs + }); err != nil { + return fmt.Errorf("failed to pass package load config: %v", err) } - var err error - result, err = opts.Hooks.Govulncheck(ctx, cfg, args) - return err + + // stderr: stream gopls vulncheck's STDERR as progress reports + er := progress.NewEventWriter(ctx, "vulncheck") + stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work)) + cmd.Stderr = stderr + // TODO: can we stream stdout? + stdout, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to run govulncheck: %v", err) + } + + var vulns command.VulncheckResult + if err := json.Unmarshal(stdout, &vulns); err != nil { + // TODO: for easy debugging, log the failed stdout somewhere? + return fmt.Errorf("failed to parse govulncheck output: %v", err) + } + + // TODO(hyangah): convert the results to diagnostics & code actions. + return nil }) - return result, err + return err } diff --git a/internal/lsp/command/command_gen.go b/internal/lsp/command/command_gen.go index 22cfeff5bad..207def42e64 100644 --- a/internal/lsp/command/command_gen.go +++ b/internal/lsp/command/command_gen.go @@ -159,7 +159,7 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte if err := UnmarshalArgs(params.Arguments, &a0); err != nil { return nil, err } - return s.RunVulncheckExp(ctx, a0) + return nil, s.RunVulncheckExp(ctx, a0) case "gopls.start_debugging": var a0 DebuggingArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go index 1f3b092faba..280fd616ebb 100644 --- a/internal/lsp/command/interface.go +++ b/internal/lsp/command/interface.go @@ -147,7 +147,7 @@ type Interface interface { // RunVulncheckExp: Run vulncheck (experimental) // // Run vulnerability check (`govulncheck`). - RunVulncheckExp(context.Context, VulncheckArgs) (VulncheckResult, error) + RunVulncheckExp(context.Context, VulncheckArgs) error } type RunTestsArgs struct { @@ -320,8 +320,7 @@ type VulncheckArgs struct { // Package pattern. E.g. "", ".", "./...". Pattern string - // TODO: Flag []string (flags accepted by govulncheck, e.g., -tests) - // TODO: Format string (json, text) + // TODO: -tests } type VulncheckResult struct { diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index e20b8a671d9..a6130063f45 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -726,11 +726,10 @@ var GeneratedAPIJSON = &APIJSON{ ArgDoc: "{\n\t// The test file containing the tests to run.\n\t\"URI\": string,\n\t// Specific test names to run, e.g. TestFoo.\n\t\"Tests\": []string,\n\t// Specific benchmarks to run, e.g. BenchmarkFoo.\n\t\"Benchmarks\": []string,\n}", }, { - Command: "gopls.run_vulncheck_exp", - Title: "Run vulncheck (experimental)", - Doc: "Run vulnerability check (`govulncheck`).", - ArgDoc: "{\n\t// Dir is the directory from which vulncheck will run from.\n\t\"Dir\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", - ResultDoc: "{\n\t\"Vuln\": []{\n\t\t\"ID\": string,\n\t\t\"Details\": string,\n\t\t\"Aliases\": []string,\n\t\t\"Symbol\": string,\n\t\t\"PkgPath\": string,\n\t\t\"ModPath\": string,\n\t\t\"URL\": string,\n\t\t\"CurrentVersion\": string,\n\t\t\"FixedVersion\": string,\n\t\t\"CallStacks\": [][]golang.org/x/tools/internal/lsp/command.StackEntry,\n\t\t\"CallStackSummaries\": []string,\n\t},\n}", + Command: "gopls.run_vulncheck_exp", + Title: "Run vulncheck (experimental)", + Doc: "Run vulnerability check (`govulncheck`).", + ArgDoc: "{\n\t// Dir is the directory from which vulncheck will run from.\n\t\"Dir\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", }, { Command: "gopls.start_debugging", From bd68922a854dc2bccaa454ef93f76c013e98fba1 Mon Sep 17 00:00:00 2001 From: pjw Date: Thu, 4 Aug 2022 11:40:49 -0400 Subject: [PATCH 161/723] internal/lsp: new options to disable certain kinds of semantic tokens Semantic tokens, as defined by the LSP, have no way of marking parts of strings or numbers, for instance, to emphasize escape character. But if gopls returns no semantic tokens for strings, then the editor will use its coloring for strings, which may be more useful (and similarly for components of numbers). This change introduces boolean flags noSemanticString and noSemanticNumber that can be set to true to suppress the semantic token and let the editor's formatting shine through. Fixes: Fixes golang/go#45753 Change-Id: Ibae880a08fb9a67daa73aa172375a1c949431e11 Reviewed-on: https://go-review.googlesource.com/c/tools/+/421256 Reviewed-by: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Peter Weinberger --- gopls/doc/settings.md | 16 +++++++++++++ internal/lsp/semantic.go | 42 +++++++++++++++++++++------------ internal/lsp/source/api_json.go | 16 +++++++++++++ internal/lsp/source/options.go | 12 ++++++++++ 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index e04436a34d7..9bf6e075a0a 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -215,6 +215,22 @@ semantic tokens to the client. Default: `false`. +#### **noSemanticString** *bool* + +**This setting is experimental and may be deleted.** + +noSemanticString turns off the sending of the semantic token 'string' + +Default: `false`. + +#### **noSemanticNumber** *bool* + +**This setting is experimental and may be deleted.** + +noSemanticNumber turns off the sending of the semantic token 'number' + +Default: `false`. + #### Completion ##### **usePlaceholders** *bool* diff --git a/internal/lsp/semantic.go b/internal/lsp/semantic.go index f0c4a11a4e9..648d5c446f1 100644 --- a/internal/lsp/semantic.go +++ b/internal/lsp/semantic.go @@ -107,14 +107,16 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu return nil, err } e := &encoded{ - ctx: ctx, - pgf: pgf, - rng: rng, - ti: pkg.GetTypesInfo(), - pkg: pkg, - fset: snapshot.FileSet(), - tokTypes: s.session.Options().SemanticTypes, - tokMods: s.session.Options().SemanticMods, + ctx: ctx, + pgf: pgf, + rng: rng, + ti: pkg.GetTypesInfo(), + pkg: pkg, + fset: snapshot.FileSet(), + tokTypes: s.session.Options().SemanticTypes, + tokMods: s.session.Options().SemanticMods, + noStrings: vv.Options().NoSemanticString, + noNumbers: vv.Options().NoSemanticNumber, } if err := e.init(); err != nil { // e.init should never return an error, unless there's some @@ -223,6 +225,9 @@ type encoded struct { // the generated data items []semItem + noStrings bool + noNumbers bool + ctx context.Context tokTypes, tokMods []string pgf *source.ParsedGoFile @@ -827,29 +832,36 @@ func (e *encoded) Data() []uint32 { var j int var last semItem for i := 0; i < len(e.items); i++ { - typ, ok := typeMap[e.items[i].typeStr] + item := e.items[i] + typ, ok := typeMap[item.typeStr] if !ok { continue // client doesn't want typeStr } + if item.typeStr == tokString && e.noStrings { + continue + } + if item.typeStr == tokNumber && e.noNumbers { + continue + } if j == 0 { x[0] = e.items[0].line } else { - x[j] = e.items[i].line - last.line + x[j] = item.line - last.line } - x[j+1] = e.items[i].start + x[j+1] = item.start if j > 0 && x[j] == 0 { - x[j+1] = e.items[i].start - last.start + x[j+1] = item.start - last.start } - x[j+2] = e.items[i].len + x[j+2] = item.len x[j+3] = uint32(typ) mask := 0 - for _, s := range e.items[i].mods { + for _, s := range item.mods { // modMap[s] is 0 if the client doesn't want this modifier mask |= modMap[s] } x[j+4] = uint32(mask) j += 5 - last = e.items[i] + last = item } return x[:j] } diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index a6130063f45..41f66d7c940 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -615,6 +615,22 @@ var GeneratedAPIJSON = &APIJSON{ Status: "experimental", Hierarchy: "ui", }, + { + Name: "noSemanticString", + Type: "bool", + Doc: "noSemanticString turns off the sending of the semantic token 'string'\n", + Default: "false", + Status: "experimental", + Hierarchy: "ui", + }, + { + Name: "noSemanticNumber", + Type: "bool", + Doc: "noSemanticNumber turns off the sending of the semantic token 'number'\n", + Default: "false", + Status: "experimental", + Hierarchy: "ui", + }, { Name: "local", Type: "string", diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 971fa069d4a..9073586939a 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -317,6 +317,12 @@ type UIOptions struct { // SemanticTokens controls whether the LSP server will send // semantic tokens to the client. SemanticTokens bool `status:"experimental"` + + // NoSemanticString turns off the sending of the semantic token 'string' + NoSemanticString bool `status:"experimental"` + + // NoSemanticNumber turns off the sending of the semantic token 'number' + NoSemanticNumber bool `status:"experimental"` } type CompletionOptions struct { @@ -1033,6 +1039,12 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "semanticTokens": result.setBool(&o.SemanticTokens) + case "noSemanticString": + result.setBool(&o.NoSemanticString) + + case "noSemanticNumber": + result.setBool(&o.NoSemanticNumber) + case "expandWorkspaceToModule": result.setBool(&o.ExpandWorkspaceToModule) From 01c9ff053696e40c9604de2bbd7960e82f6bc6df Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 3 Aug 2022 13:07:09 -0400 Subject: [PATCH 162/723] internal/lsp/cache: invalid packages should not be workspace packages To support the experimentalUseInvalidMetadata mode, we keep around invalid metadata. This can help gopls features work when, for example, the go.mod file is broken. It is debatable whether this feature is worth supporting (see golang/go#54180), but in the meantime there is a very negative side-effect when module paths are changed in the go.mod file: we keep around a bunch of workspace packages with a stale module path. As a result we can be left with a lots of extra type-checked packages in memory, and many inaccurate diagnostics. Fix this by skipping packages with invalid metadata when computing workspace packages. While we may want to use invalid metadata when finding the best package for a file, it does not make sense to re- type-check and diagnose all those stale packages. Fixes golang/go#43186 Change-Id: Id73b47ea138ec80a9de63b03dae41d4e509b8d5a Reviewed-on: https://go-review.googlesource.com/c/tools/+/420956 Run-TryBot: Robert Findley Reviewed-by: Suzy Mueller gopls-CI: kokoro TryBot-Result: Gopher Robot --- .../internal/regtest/workspace/broken_test.go | 52 +++++++++++++++++++ internal/lsp/cache/check.go | 1 + internal/lsp/cache/load.go | 6 +++ internal/lsp/cache/snapshot.go | 23 +++++--- internal/lsp/source/util.go | 2 + 5 files changed, 78 insertions(+), 6 deletions(-) diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index cd5127ff3a8..e88b98b50e3 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -115,3 +115,55 @@ const CompleteMe = 222 } }) } + +// Test for golang/go#43186: correcting the module path should fix errors +// without restarting gopls. +func TestBrokenWorkspace_WrongModulePath(t *testing.T) { + const files = ` +-- go.mod -- +module mod.testx + +go 1.18 +-- p/internal/foo/foo.go -- +package foo + +const C = 1 +-- p/internal/bar/bar.go -- +package bar + +import "mod.test/p/internal/foo" + +const D = foo.C + 1 +-- p/internal/bar/bar_test.go -- +package bar_test + +import ( + "mod.test/p/internal/foo" + . "mod.test/p/internal/bar" +) + +const E = D + foo.C +-- p/internal/baz/baz_test.go -- +package baz_test + +import ( + named "mod.test/p/internal/bar" +) + +const F = named.D - 3 +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("p/internal/bar/bar.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\""), + ), + ) + env.OpenFile("go.mod") + env.RegexpReplace("go.mod", "mod.testx", "mod.test") + env.SaveBuffer("go.mod") // saving triggers a reload + env.Await(NoOutstandingDiagnostics()) + }) +} diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index 6c02d5348f8..6beee1f7bed 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -355,6 +355,7 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF missing, unexpected = filter.ProcessErrors(pkg.typeErrors) } if len(unexpected) != 0 || len(missing) != 0 { + // TODO(rfindley): remove this distracting log event.Log(ctx, fmt.Sprintf("falling back to safe trimming due to type errors: %v or still-missing identifiers: %v", unexpected, missing), tag.Package.Of(string(m.ID))) pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, nil) if err != nil { diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index ca906c82877..8afbb2e4ff3 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -635,6 +635,12 @@ func containsFileInWorkspaceLocked(s *snapshot, m *Metadata) bool { func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[PackageID]PackagePath { workspacePackages := make(map[PackageID]PackagePath) for _, m := range meta.metadata { + // Don't consider invalid packages to be workspace packages. Doing so can + // result in type-checking and diagnosing packages that no longer exist, + // which can lead to memory leaks and confusing errors. + if !m.Valid { + continue + } if !containsPackageLocked(s, m.Metadata) { continue } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 6d3ead5c371..5a51ae13e01 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1874,13 +1874,8 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC result.actions.Delete(key) } - // If the workspace mode has changed, we must delete all metadata, as it - // is unusable and may produce confusing or incorrect diagnostics. // If a file has been deleted, we must delete metadata for all packages // containing that file. - workspaceModeChanged := s.workspaceMode() != result.workspaceMode() - - // Don't keep package metadata for packages that have lost files. // // TODO(rfindley): why not keep invalid metadata in this case? If we // otherwise allow operate on invalid metadata, why not continue to do so, @@ -1907,11 +1902,27 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC result.shouldLoad[k] = v } + // TODO(rfindley): consolidate the this workspace mode detection with + // workspace invalidation. + workspaceModeChanged := s.workspaceMode() != result.workspaceMode() + + // We delete invalid metadata in the following cases: + // - If we are forcing a reload of metadata. + // - If the workspace mode has changed, as stale metadata may produce + // confusing or incorrect diagnostics. + // + // TODO(rfindley): we should probably also clear metadata if we are + // reinitializing the workspace, as otherwise we could leave around a bunch + // of irrelevant and duplicate metadata (for example, if the module path + // changed). However, this breaks the "experimentalUseInvalidMetadata" + // feature, which relies on stale metadata when, for example, a go.mod file + // is broken via invalid syntax. + deleteInvalidMetadata := forceReloadMetadata || workspaceModeChanged + // Compute which metadata updates are required. We only need to invalidate // packages directly containing the affected file, and only if it changed in // a relevant way. metadataUpdates := make(map[PackageID]*KnownMetadata) - deleteInvalidMetadata := forceReloadMetadata || workspaceModeChanged for k, v := range s.meta.metadata { invalidateMetadata := idsToInvalidate[k] diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go index 262447cf36d..78448af4176 100644 --- a/internal/lsp/source/util.go +++ b/internal/lsp/source/util.go @@ -555,6 +555,8 @@ func IsValidImport(pkgPath, importPkgPath string) bool { if IsCommandLineArguments(string(pkgPath)) { return true } + // TODO(rfindley): this is wrong. mod.testx/p should not be able to + // import mod.test/internal: https://go.dev/play/p/-Ca6P-E4V4q return strings.HasPrefix(string(pkgPath), string(importPkgPath[:i])) } From 99fd76f9c05e943c92ec0de2dbed41257f93daff Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 4 Aug 2022 12:30:47 -0400 Subject: [PATCH 163/723] internal/lsp/cache: delete KnownMetadata.PkgFilesChanged It is no longer needed, now that we don't consider invalid packages to be workspace packages. Change-Id: I6155a2609ab07046b9507dc04717eea7b974f1b6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/421257 gopls-CI: kokoro Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- internal/lsp/cache/load.go | 10 ---------- internal/lsp/cache/metadata.go | 7 ------- internal/lsp/cache/snapshot.go | 20 ++++++-------------- 3 files changed, 6 insertions(+), 31 deletions(-) diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 8afbb2e4ff3..0952fc679be 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -644,16 +644,6 @@ func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[Packag if !containsPackageLocked(s, m.Metadata) { continue } - if m.PkgFilesChanged { - // If a package name has changed, it's possible that the package no - // longer exists. Leaving it as a workspace package can result in - // persistent stale diagnostics. - // - // If there are still valid files in the package, it will be reloaded. - // - // There may be more precise heuristics. - continue - } if source.IsCommandLineArguments(string(m.ID)) { // If all the files contained in m have a real package, we don't need to diff --git a/internal/lsp/cache/metadata.go b/internal/lsp/cache/metadata.go index 7d9192f43a2..668d0829a13 100644 --- a/internal/lsp/cache/metadata.go +++ b/internal/lsp/cache/metadata.go @@ -91,11 +91,4 @@ type KnownMetadata struct { // Valid is true if the given metadata is Valid. // Invalid metadata can still be used if a metadata reload fails. Valid bool - - // PkgFilesChanged reports whether the file set of this metadata has - // potentially changed. - // - // TODO(rfindley): this is used for WorkspacePackages, and looks fishy: we - // should probably only consider valid packages to be workspace packages. - PkgFilesChanged bool } diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 5a51ae13e01..da2d7b52e15 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1734,10 +1734,9 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // Compute invalidations based on file changes. - changedPkgFiles := map[PackageID]bool{} // packages whose file set may have changed - anyImportDeleted := false // import deletions can resolve cycles - anyFileOpenedOrClosed := false // opened files affect workspace packages - anyFileAdded := false // adding a file can resolve missing dependencies + anyImportDeleted := false // import deletions can resolve cycles + anyFileOpenedOrClosed := false // opened files affect workspace packages + anyFileAdded := false // adding a file can resolve missing dependencies for uri, change := range changes { // The original FileHandle for this URI is cached on the snapshot. @@ -1762,11 +1761,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Mark all of the package IDs containing the given file. filePackageIDs := invalidatedPackageIDs(uri, s.meta.ids, pkgFileChanged) - if pkgFileChanged { - for id := range filePackageIDs { - changedPkgFiles[id] = true - } - } for id := range filePackageIDs { directIDs[id] = directIDs[id] || invalidateMetadata } @@ -1953,13 +1947,11 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Check if the metadata has changed. valid := v.Valid && !invalidateMetadata - pkgFilesChanged := v.PkgFilesChanged || changedPkgFiles[k] - if valid != v.Valid || pkgFilesChanged != v.PkgFilesChanged { + if valid != v.Valid { // Mark invalidated metadata rather than deleting it outright. metadataUpdates[k] = &KnownMetadata{ - Metadata: v.Metadata, - Valid: valid, - PkgFilesChanged: pkgFilesChanged, + Metadata: v.Metadata, + Valid: valid, } } } From b5fd08821a5cee019211bfd5a3fe2340cd0462bb Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 2 Aug 2022 22:39:32 -0400 Subject: [PATCH 164/723] internal/lsp/command: replace VulncheckArgs Dir with URI commandHandler.run expects a valid file for forURI and forURI is necessary to fully populate commandDeps. Our use of directory URI does not work. We plan to use this custom command triggered through codelenses on documents (e.g. go.mod), so we use the document's URI. Change-Id: I4de6d488dec5a4b71716499e7fc5e8328ecf3e49 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420994 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Reviewed-by: Suzy Mueller Run-TryBot: Hyang-Ah Hana Kim --- gopls/doc/commands.md | 4 ++-- gopls/internal/regtest/misc/vuln_test.go | 5 +++-- gopls/internal/vulncheck/command.go | 8 ++++---- gopls/internal/vulncheck/vulncheck.go | 2 +- internal/lsp/cmd/vulncheck.go | 8 ++------ internal/lsp/command.go | 10 ++++++---- internal/lsp/command/interface.go | 4 ++-- internal/lsp/source/api_json.go | 2 +- internal/lsp/source/options.go | 2 +- 9 files changed, 22 insertions(+), 23 deletions(-) diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 37d8dcbdcca..c202b51a2ca 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -274,8 +274,8 @@ Args: ``` { - // Dir is the directory from which vulncheck will run from. - "Dir": string, + // Any document in the directory from which govulncheck will run. + "URI": string, // Package pattern. E.g. "", ".", "./...". "Pattern": string, } diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index d39fd74ef57..9de68b61c7a 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -26,7 +26,7 @@ package foo ` Run(t, files, func(t *testing.T, env *Env) { cmd, err := command.NewRunVulncheckExpCommand("Run Vulncheck Exp", command.VulncheckArgs{ - Dir: "/invalid/file/url", // invalid arg + URI: "/invalid/file/url", // invalid arg }) if err != nil { t.Fatal(err) @@ -81,7 +81,8 @@ func main() { }, ).Run(t, files, func(t *testing.T, env *Env) { cmd, err := command.NewRunVulncheckExpCommand("Run Vulncheck Exp", command.VulncheckArgs{ - Dir: env.Sandbox.Workdir.RootURI(), + URI: protocol.URIFromPath(env.Sandbox.Workdir.AbsPath("go.mod")), + Pattern: "./...", }) if err != nil { t.Fatal(err) diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 8c88cf3d51b..b84daa5d9f7 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -26,9 +26,9 @@ func init() { Govulncheck = govulncheck } -func govulncheck(ctx context.Context, cfg *packages.Config, args command.VulncheckArgs) (res command.VulncheckResult, _ error) { - if args.Pattern == "" { - args.Pattern = "." +func govulncheck(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) { + if patterns == "" { + patterns = "." } dbClient, err := client.NewClient(findGOVULNDB(cfg), client.Options{HTTPCache: gvc.DefaultCache()}) @@ -37,7 +37,7 @@ func govulncheck(ctx context.Context, cfg *packages.Config, args command.Vulnche } c := cmd{Client: dbClient} - vulns, err := c.Run(ctx, cfg, args.Pattern) + vulns, err := c.Run(ctx, cfg, patterns) if err != nil { return res, err } diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go index 2c4d0d2978d..7fc05ae423b 100644 --- a/gopls/internal/vulncheck/vulncheck.go +++ b/gopls/internal/vulncheck/vulncheck.go @@ -18,6 +18,6 @@ import ( // Govulncheck runs the in-process govulncheck implementation. // With go1.18+, this is swapped with the real implementation. -var Govulncheck = func(ctx context.Context, cfg *packages.Config, args command.VulncheckArgs) (res command.VulncheckResult, _ error) { +var Govulncheck = func(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) { return res, errors.New("not implemented") } diff --git a/internal/lsp/cmd/vulncheck.go b/internal/lsp/cmd/vulncheck.go index 4d245cecb60..19b3466a533 100644 --- a/internal/lsp/cmd/vulncheck.go +++ b/internal/lsp/cmd/vulncheck.go @@ -12,8 +12,6 @@ import ( "os" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/tool" ) @@ -90,12 +88,10 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { Tests: cfg.Tests, BuildFlags: cfg.BuildFlags, Env: cfg.Env, + Dir: cwd, } - res, err := opts.Hooks.Govulncheck(ctx, loadCfg, command.VulncheckArgs{ - Dir: protocol.URIFromPath(cwd), - Pattern: pattern, - }) + res, err := opts.Hooks.Govulncheck(ctx, loadCfg, pattern) if err != nil { return tool.CommandLineErrorf("govulncheck failed: %v", err) } diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 23ade896a25..7348c174740 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -807,14 +807,14 @@ type pkgLoadConfig struct { } func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) error { - if args.Dir == "" { - return errors.New("VulncheckArgs is missing Dir field") + if args.URI == "" { + return errors.New("VulncheckArgs is missing URI field") } err := c.run(ctx, commandConfig{ async: true, // need to be async to be cancellable progress: "Checking vulnerability", requireSave: true, - forURI: args.Dir, // Will dir work? + forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { view := deps.snapshot.View() opts := view.Options() @@ -823,7 +823,9 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc } cmd := exec.Command(os.Args[0], "vulncheck", "-config", args.Pattern) - cmd.Dir = args.Dir.SpanURI().Filename() + // TODO(hyangah): if args.URI is not go.mod file, we need to + // adjust the directory accordingly. + cmd.Dir = filepath.Dir(args.URI.SpanURI().Filename()) var viewEnv []string if e := opts.EnvSlice(); e != nil { diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go index 280fd616ebb..4a4498a77ea 100644 --- a/internal/lsp/command/interface.go +++ b/internal/lsp/command/interface.go @@ -314,8 +314,8 @@ type DebuggingResult struct { } type VulncheckArgs struct { - // Dir is the directory from which vulncheck will run from. - Dir protocol.DocumentURI + // Any document in the directory from which govulncheck will run. + URI protocol.DocumentURI // Package pattern. E.g. "", ".", "./...". Pattern string diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 41f66d7c940..cf75792ff2b 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -745,7 +745,7 @@ var GeneratedAPIJSON = &APIJSON{ Command: "gopls.run_vulncheck_exp", Title: "Run vulncheck (experimental)", Doc: "Run vulnerability check (`govulncheck`).", - ArgDoc: "{\n\t// Dir is the directory from which vulncheck will run from.\n\t\"Dir\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", + ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", }, { Command: "gopls.start_debugging", diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 9073586939a..126752bae76 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -514,7 +514,7 @@ type Hooks struct { StaticcheckAnalyzers map[string]*Analyzer // Govulncheck is the implementation of the Govulncheck gopls command. - Govulncheck func(context.Context, *packages.Config, command.VulncheckArgs) (command.VulncheckResult, error) + Govulncheck func(context.Context, *packages.Config, string) (command.VulncheckResult, error) } // InternalOptions contains settings that are not intended for use by the From fc3b24a4566080377579c9a1fda12e0884fbf4c9 Mon Sep 17 00:00:00 2001 From: David Chase Date: Thu, 4 Aug 2022 11:39:16 -0400 Subject: [PATCH 165/723] go/internal/gcimporter: rewrite interface receiver parameters Tracking changes in go repo for unified IR. See CL 421355. Change-Id: Idc0d2afeb6f2241f3608cbdb0fbc128f8755ec55 Reviewed-on: https://go-review.googlesource.com/c/tools/+/421255 Run-TryBot: David Chase TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Matthew Dempsky --- go/internal/gcimporter/gcimporter_test.go | 12 ++--------- go/internal/gcimporter/ureader_yes.go | 26 +++++++++++++++++++---- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/go/internal/gcimporter/gcimporter_test.go b/go/internal/gcimporter/gcimporter_test.go index 66c09269d88..5e1ca4bebcc 100644 --- a/go/internal/gcimporter/gcimporter_test.go +++ b/go/internal/gcimporter/gcimporter_test.go @@ -372,14 +372,6 @@ func verifyInterfaceMethodRecvs(t *testing.T, named *types.Named, level int) { return // not an interface } - // The unified IR importer always sets interface method receiver - // parameters to point to the Interface type, rather than the Named. - // See #49906. - var want types.Type = named - if unifiedIR { - want = iface - } - // check explicitly declared methods for i := 0; i < iface.NumExplicitMethods(); i++ { m := iface.ExplicitMethod(i) @@ -388,8 +380,8 @@ func verifyInterfaceMethodRecvs(t *testing.T, named *types.Named, level int) { t.Errorf("%s: missing receiver type", m) continue } - if recv.Type() != want { - t.Errorf("%s: got recv type %s; want %s", m, recv.Type(), want) + if recv.Type() != named { + t.Errorf("%s: got recv type %s; want %s", m, recv.Type(), named) } } diff --git a/go/internal/gcimporter/ureader_yes.go b/go/internal/gcimporter/ureader_yes.go index add7bf8e3e3..1a0ce647264 100644 --- a/go/internal/gcimporter/ureader_yes.go +++ b/go/internal/gcimporter/ureader_yes.go @@ -512,10 +512,6 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { named.SetTypeParams(r.typeParamNames()) - // TODO(mdempsky): Rewrite receiver types to underlying is an - // Interface? The go/types importer does this (I think because - // unit tests expected that), but cmd/compile doesn't care - // about it, so maybe we can avoid worrying about that here. rhs := r.typ() pk := r.p pk.laterFor(named, func() { @@ -527,6 +523,28 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { f() // initialize RHS } underlying := rhs.Underlying() + + // If the underlying type is an interface, we need to + // duplicate its methods so we can replace the receiver + // parameter's type (#49906). + if iface, ok := underlying.(*types.Interface); ok && iface.NumExplicitMethods() != 0 { + methods := make([]*types.Func, iface.NumExplicitMethods()) + for i := range methods { + fn := iface.ExplicitMethod(i) + sig := fn.Type().(*types.Signature) + + recv := types.NewVar(fn.Pos(), fn.Pkg(), "", named) + methods[i] = types.NewFunc(fn.Pos(), fn.Pkg(), fn.Name(), types.NewSignature(recv, sig.Params(), sig.Results(), sig.Variadic())) + } + + embeds := make([]types.Type, iface.NumEmbeddeds()) + for i := range embeds { + embeds[i] = iface.EmbeddedType(i) + } + + underlying = types.NewInterfaceType(methods, embeds) + } + named.SetUnderlying(underlying) }) From 763f65c3d22e3a1a3a65c334e6728e923524b360 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 4 Aug 2022 15:13:10 -0400 Subject: [PATCH 166/723] gopls/internal/regtest/misc: simplify shared edit tests In order to avoid shutdown races we must always close the second editor created for simultaneous edit tests. Rather than introduce additional indirection, just merge the two tests into one. For golang/go#54241 Change-Id: I6604141baa77d07f6281d3a90efa13c02b94079a Reviewed-on: https://go-review.googlesource.com/c/tools/+/421258 gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot --- gopls/internal/regtest/misc/shared_test.go | 58 +++++++++------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index e433f4bd4e2..64e07208a59 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -11,7 +11,9 @@ import ( . "golang.org/x/tools/internal/lsp/regtest" ) -const sharedProgram = ` +// Smoke test that simultaneous editing sessions in the same workspace works. +func TestSimultaneousEdits(t *testing.T) { + const sharedProgram = ` -- go.mod -- module mod @@ -25,13 +27,9 @@ func main() { fmt.Println("Hello World.") }` -// runShared is a helper to run a test in the same directory using both the -// original env, and an additional other environment connected to the same -// server. -func runShared(t *testing.T, testFunc func(origEnv *Env, otherEnv *Env)) { - // Only run these tests in forwarded modes. - modes := DefaultModes() & (Forwarded | SeparateProcess) - WithOptions(Modes(modes)).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { + WithOptions( + Modes(DefaultModes()&(Forwarded|SeparateProcess)), + ).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { // Create a second test session connected to the same workspace and server // as the first. awaiter := NewAwaiter(env1.Sandbox.Workdir) @@ -48,37 +46,29 @@ func runShared(t *testing.T, testFunc func(origEnv *Env, otherEnv *Env)) { Awaiter: awaiter, } env2.Await(InitialWorkspaceLoad) - testFunc(env1, env2) - }) -} - -func TestSimultaneousEdits(t *testing.T) { - runShared(t, func(origEnv *Env, otherEnv *Env) { // In editor #1, break fmt.Println as before. - origEnv.OpenFile("main.go") - origEnv.RegexpReplace("main.go", "Printl(n)", "") + env1.OpenFile("main.go") + env1.RegexpReplace("main.go", "Printl(n)", "") // In editor #2 remove the closing brace. - otherEnv.OpenFile("main.go") - otherEnv.RegexpReplace("main.go", "\\)\n(})", "") + env2.OpenFile("main.go") + env2.RegexpReplace("main.go", "\\)\n(})", "") // Now check that we got different diagnostics in each environment. - origEnv.Await(origEnv.DiagnosticAtRegexp("main.go", "Printl")) - otherEnv.Await(otherEnv.DiagnosticAtRegexp("main.go", "$")) - }) -} + env1.Await(env1.DiagnosticAtRegexp("main.go", "Printl")) + env2.Await(env2.DiagnosticAtRegexp("main.go", "$")) -func TestShutdown(t *testing.T) { - runShared(t, func(origEnv *Env, otherEnv *Env) { - // Close otherEnv, and verify that operation in the original environment is - // unaffected. Note: 'otherEnv' must be the environment being closed here. - // If we were to instead close 'env' here, we'd run into a duplicate - // shutdown when the test runner closes the original env. - if err := otherEnv.Editor.Close(otherEnv.Ctx); err != nil { - t.Errorf("closing first editor: %v", err) + // Now close editor #2, and verify that operation in editor #1 is + // unaffected. + if err := env2.Editor.Close(env2.Ctx); err != nil { + t.Errorf("closing second editor: %v", err) } - // Now make an edit in editor #2 to trigger diagnostics. - origEnv.OpenFile("main.go") - origEnv.RegexpReplace("main.go", "\\)\n(})", "") - origEnv.Await(origEnv.DiagnosticAtRegexp("main.go", "$")) + + env1.RegexpReplace("main.go", "Printl", "Println") + env1.Await( + OnceMet( + env1.DoneWithChange(), + EmptyDiagnostics("main.go"), + ), + ) }) } From 6c27717f2a25662511c3b534b21f9b2bd160dd56 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 2 Aug 2022 23:15:52 -0400 Subject: [PATCH 167/723] internal/lsp/mod/code_lens: add "run govulncheck" codelens And, make gopls.run_vulncheck_exp show an information/error message popup after a successful run. This is temporary. We plan to publish the results as diagnostics and quick-fix. Finally, changed the stdlib vulnerability info id in testdata to GO-0000-0001 which looks more like a vulnerability ID than STD. Changed TestRunVulncheckExp to include tests on codelens and use the command included in the codelens, instead of directly calling the gopls.run_vulncheck_exp command. Change-Id: Iaf91e4e61b2dfc1e050b887946a69efd3e3785b0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420995 Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot --- gopls/doc/settings.md | 5 +++ .../regtest/misc/testdata/vulndb/stdlib.json | 2 +- gopls/internal/regtest/misc/vuln_test.go | 37 ++++++++++++++----- internal/lsp/command.go | 30 +++++++++++++-- internal/lsp/mod/code_lens.go | 27 ++++++++++++++ internal/lsp/source/api_json.go | 10 +++++ internal/lsp/source/options.go | 1 + 7 files changed, 97 insertions(+), 15 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 9bf6e075a0a..890a0a3444f 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -503,6 +503,11 @@ Runs `go generate` for a given directory. Identifier: `regenerate_cgo` Regenerates cgo definitions. +### **Run vulncheck (experimental)** + +Identifier: `run_vulncheck_exp` + +Run vulnerability check (`govulncheck`). ### **Run test(s) (legacy)** Identifier: `test` diff --git a/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json b/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json index 240ee494309..7cbfafcd103 100644 --- a/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json +++ b/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json @@ -1 +1 @@ -[{"id":"STD","affected":[{"package":{"name":"archive/zip"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.18.0"}]}],"ecosystem_specific":{"symbols":["OpenReader"]}}]}] +[{"id":"GO-0000-001","affected":[{"package":{"name":"archive/zip"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.18.0"}]}],"ecosystem_specific":{"symbols":["OpenReader"]}}]}] diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 9de68b61c7a..78c193efa09 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -61,7 +61,7 @@ import ( ) func main() { - _, err := zip.OpenReader("file.zip") // vulnerable. + _, err := zip.OpenReader("file.zip") // vulnerability GO-0000-001 fmt.Println(err) } ` @@ -79,22 +79,39 @@ func main() { "GOVERSION": "go1.18", "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", }, + Settings{ + "codelenses": map[string]bool{ + "run_vulncheck_exp": true, + }, + }, ).Run(t, files, func(t *testing.T, env *Env) { - cmd, err := command.NewRunVulncheckExpCommand("Run Vulncheck Exp", command.VulncheckArgs{ - URI: protocol.URIFromPath(env.Sandbox.Workdir.AbsPath("go.mod")), - Pattern: "./...", - }) - if err != nil { - t.Fatal(err) - } + env.OpenFile("go.mod") + // Test CodeLens is present. + lenses := env.CodeLens("go.mod") + + const wantCommand = "gopls." + string(command.RunVulncheckExp) + var gotCodelens = false + var lens protocol.CodeLens + for _, l := range lenses { + if l.Command.Command == wantCommand { + gotCodelens = true + lens = l + break + } + } + if !gotCodelens { + t.Fatal("got no vulncheck codelens") + } + // Run Command included in the codelens. env.ExecuteCommand(&protocol.ExecuteCommandParams{ - Command: command.RunVulncheckExp.ID(), - Arguments: cmd.Arguments, + Command: lens.Command.Command, + Arguments: lens.Command.Arguments, }, nil) env.Await( CompletedWork("Checking vulnerability", 1, true), // TODO(hyangah): once the diagnostics are published, wait for diagnostics. + ShownMessage("Found GO-0000-001"), ) }) } diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 7348c174740..6df909bdb10 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -823,8 +823,6 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc } cmd := exec.Command(os.Args[0], "vulncheck", "-config", args.Pattern) - // TODO(hyangah): if args.URI is not go.mod file, we need to - // adjust the directory accordingly. cmd.Dir = filepath.Dir(args.URI.SpanURI().Filename()) var viewEnv []string @@ -860,8 +858,32 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc return fmt.Errorf("failed to parse govulncheck output: %v", err) } - // TODO(hyangah): convert the results to diagnostics & code actions. - return nil + // TODO(jamalc,suzmue): convert the results to diagnostics & code actions. + // Or should we just write to a file (*.vulncheck.json) or text format + // and send "Show Document" request? If *.vulncheck.json is open, + // VSCode Go extension will open its custom editor. + set := make(map[string]bool) + for _, v := range vulns.Vuln { + if len(v.CallStackSummaries) > 0 { + set[v.ID] = true + } + } + if len(set) == 0 { + return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ + Type: protocol.Info, + Message: "No vulnerabilities found", + }) + } + + list := make([]string, 0, len(set)) + for k := range set { + list = append(list, k) + } + sort.Strings(list) + return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ + Type: protocol.Warning, + Message: fmt.Sprintf("Found %v", strings.Join(list, ", ")), + }) }) return err } diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go index b26bae75c47..1de25c2f8f4 100644 --- a/internal/lsp/mod/code_lens.go +++ b/internal/lsp/mod/code_lens.go @@ -22,6 +22,7 @@ func LensFuncs() map[command.Command]source.LensFunc { command.UpgradeDependency: upgradeLenses, command.Tidy: tidyLens, command.Vendor: vendorLens, + command.RunVulncheckExp: vulncheckLenses, } } @@ -151,3 +152,29 @@ func firstRequireRange(fh source.FileHandle, pm *source.ParsedModule) (protocol. } return source.LineToRange(pm.Mapper, fh.URI(), start, end) } + +func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { + pm, err := snapshot.ParseMod(ctx, fh) + if err != nil || pm.File == nil { + return nil, err + } + // Place the codelenses near the module statement. + // A module may not have the require block, + // but vulnerabilities can exist in standard libraries. + uri := protocol.URIFromSpanURI(fh.URI()) + rng, err := moduleStmtRange(fh, pm) + if err != nil { + return nil, err + } + + vulncheck, err := command.NewRunVulncheckExpCommand("Run govulncheck", command.VulncheckArgs{ + URI: uri, + Pattern: "./...", + }) + if err != nil { + return nil, err + } + return []protocol.CodeLens{ + {Range: rng, Command: vulncheck}, + }, nil +} diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index cf75792ff2b..0b3b3d117a7 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -582,6 +582,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Regenerates cgo definitions.", Default: "true", }, + { + Name: "\"run_vulncheck_exp\"", + Doc: "Run vulnerability check (`govulncheck`).", + Default: "false", + }, { Name: "\"test\"", Doc: "Runs `go test` for a specific set of test or benchmark functions.", @@ -807,6 +812,11 @@ var GeneratedAPIJSON = &APIJSON{ Title: "Regenerate cgo", Doc: "Regenerates cgo definitions.", }, + { + Lens: "run_vulncheck_exp", + Title: "Run vulncheck (experimental)", + Doc: "Run vulnerability check (`govulncheck`).", + }, { Lens: "test", Title: "Run test(s) (legacy)", diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 126752bae76..2f40b59ccf7 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -155,6 +155,7 @@ func DefaultOptions() *Options { string(command.GCDetails): false, string(command.UpgradeDependency): true, string(command.Vendor): true, + // TODO(hyangah): enable command.RunVulncheckExp. }, }, }, From 3519aa25b86f6f26353801ead2372efc783d75e3 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 2 Aug 2022 23:24:30 -0400 Subject: [PATCH 168/723] internal/lsp/cmd: remove unused Env from pkgLoadConfig Instead rely on the process env vars the `gopls vulncheck` command runs with. Change-Id: I313a035d9bb7dbbdf2199474e0864cdb591e15ab Reviewed-on: https://go-review.googlesource.com/c/tools/+/420996 TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Robert Findley gopls-CI: kokoro --- internal/lsp/cmd/vulncheck.go | 12 +----------- internal/lsp/command.go | 5 ----- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/internal/lsp/cmd/vulncheck.go b/internal/lsp/cmd/vulncheck.go index 19b3466a533..d5b05a92427 100644 --- a/internal/lsp/cmd/vulncheck.go +++ b/internal/lsp/cmd/vulncheck.go @@ -27,10 +27,6 @@ type pkgLoadConfig struct { // the build system's query tool. BuildFlags []string - // Env is the environment to use when invoking the build system's query tool. - // If Env is nil, the current environment is used. - Env []string - // If Tests is set, the loader includes related test packages. Tests bool } @@ -65,11 +61,6 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { if len(args) == 1 { pattern = args[0] } - - cwd, err := os.Getwd() - if err != nil { - return tool.CommandLineErrorf("failed to get current directory: %v", err) - } var cfg pkgLoadConfig if v.Config { if err := json.NewDecoder(os.Stdin).Decode(&cfg); err != nil { @@ -87,8 +78,7 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { Context: ctx, Tests: cfg.Tests, BuildFlags: cfg.BuildFlags, - Env: cfg.Env, - Dir: cwd, + // inherit the current process's cwd and env. } res, err := opts.Hooks.Govulncheck(ctx, loadCfg, pattern) diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 6df909bdb10..f830a97c983 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -797,11 +797,6 @@ type pkgLoadConfig struct { // the build system's query tool. BuildFlags []string - // Env is the environment to use when invoking the build system's query tool. - // If Env is nil, the current environment is used. - // TODO: This seems unnecessary. Delete. - Env []string - // If Tests is set, the loader includes related test packages. Tests bool } From af2a0a8167d54582088696545694d804da88f18e Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 2 Aug 2022 23:28:18 -0400 Subject: [PATCH 169/723] internal/lsp: use exec.CommandContext when running vulncheck That, hopefully, handles process termination upon context cancellation. (not 100% guaranteed) Change-Id: I79441afb794c37e9e55f710f124871a82aa4effe Reviewed-on: https://go-review.googlesource.com/c/tools/+/420997 Reviewed-by: Suzy Mueller Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro --- internal/lsp/command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/lsp/command.go b/internal/lsp/command.go index f830a97c983..1d6c973212e 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -817,7 +817,7 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc return errors.New("vulncheck feature is not available") } - cmd := exec.Command(os.Args[0], "vulncheck", "-config", args.Pattern) + cmd := exec.CommandContext(ctx, os.Args[0], "vulncheck", "-config", args.Pattern) cmd.Dir = filepath.Dir(args.URI.SpanURI().Filename()) var viewEnv []string From 81c7dc4e4efa77b1f401e403365b23ca06422ab7 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 3 Aug 2022 00:01:48 -0400 Subject: [PATCH 170/723] internal/lsp: polish vulncheck progress messages gopls.run_vulncheck_exp runs `gopls vulncheck` (fork of govulncheck) and pipes its stderr (logs) as progress messages. The default log format includes timestamp and that is too long for progress message. Tell gopls vulncheck to omit timestamp in the log message. Use "govulncheck" as the progress message prefix, instead of the long "Checking vulnerabilities". Change-Id: I92fe9958b20d0260711a42af9b5f9f399e267587 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420998 Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Suzy Mueller Reviewed-by: Jamal Carvalho gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/regtest/misc/vuln_test.go | 2 +- gopls/internal/vulncheck/command.go | 11 ++++++----- internal/lsp/command.go | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 78c193efa09..91fef3f88bc 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -109,7 +109,7 @@ func main() { Arguments: lens.Command.Arguments, }, nil) env.Await( - CompletedWork("Checking vulnerability", 1, true), + CompletedWork("govulncheck", 1, true), // TODO(hyangah): once the diagnostics are published, wait for diagnostics. ShownMessage("Found GO-0000-001"), ) diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index b84daa5d9f7..60d582ca318 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -70,29 +70,30 @@ type cmd struct { // Run runs the govulncheck after loading packages using the provided packages.Config. func (c *cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) (_ []Vuln, err error) { + logger := log.New(log.Default().Writer(), "", 0) cfg.Mode |= packages.NeedModule | packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps - log.Println("loading packages...") + logger.Println("loading packages...") loadedPkgs, err := gvc.LoadPackages(cfg, patterns...) if err != nil { - log.Printf("package load failed: %v", err) + logger.Printf("package load failed: %v", err) return nil, err } - log.Printf("analyzing %d packages...\n", len(loadedPkgs)) + logger.Printf("analyzing %d packages...\n", len(loadedPkgs)) r, err := vulncheck.Source(ctx, loadedPkgs, &vulncheck.Config{Client: c.Client, SourceGoVersion: goVersion()}) if err != nil { return nil, err } - log.Printf("selecting affecting vulnerabilities from %d findings...\n", len(r.Vulns)) + logger.Printf("selecting affecting vulnerabilities from %d findings...\n", len(r.Vulns)) unaffectedMods := filterUnaffected(r.Vulns) r.Vulns = filterCalled(r) - log.Printf("found %d vulnerabilities.\n", len(r.Vulns)) + logger.Printf("found %d vulnerabilities.\n", len(r.Vulns)) callInfo := gvc.GetCallInfo(r, loadedPkgs) return toVulns(callInfo, unaffectedMods) // TODO: add import graphs. diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 1d6c973212e..35bc0e43287 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -807,7 +807,7 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc } err := c.run(ctx, commandConfig{ async: true, // need to be async to be cancellable - progress: "Checking vulnerability", + progress: "govulncheck", requireSave: true, forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { From 06d96ee8fcfeb1083837cc5c3459f85e0c225fa2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 29 Jul 2022 17:27:52 -0400 Subject: [PATCH 171/723] gopls/internal/regtest/bench: add a test for completion following edits For golang/go#53992 Change-Id: Ia1f1e27663992707eef9226273b152117ee977ac Reviewed-on: https://go-review.googlesource.com/c/tools/+/420220 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Peter Weinberger --- gopls/internal/regtest/bench/bench_test.go | 6 +- .../internal/regtest/bench/completion_test.go | 81 ++++++++++++++----- gopls/internal/regtest/bench/iwl_test.go | 3 +- internal/lsp/fake/edit.go | 2 + 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index a3780f02d89..cfe4db6648e 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -133,7 +133,7 @@ func benchmarkEnv(tb testing.TB) *Env { dir := benchmarkDir() var err error - sandbox, editor, awaiter, err = connectEditor(dir) + sandbox, editor, awaiter, err = connectEditor(dir, fake.EditorConfig{}) if err != nil { log.Fatalf("connecting editor: %v", err) } @@ -154,7 +154,7 @@ func benchmarkEnv(tb testing.TB) *Env { // connectEditor connects a fake editor session in the given dir, using the // given editor config. -func connectEditor(dir string) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) { +func connectEditor(dir string, config fake.EditorConfig) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) { s, err := fake.NewSandbox(&fake.SandboxConfig{ Workdir: dir, GOPROXY: "https://proxy.golang.org", @@ -165,7 +165,7 @@ func connectEditor(dir string) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, e a := regtest.NewAwaiter(s.Workdir) ts := getServer() - e, err := fake.NewEditor(s, fake.EditorConfig{}).Connect(context.Background(), ts, a.Hooks()) + e, err := fake.NewEditor(s, config).Connect(context.Background(), ts, a.Hooks()) if err != nil { return nil, nil, nil, err } diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go index cdafb080924..a8725cee3d3 100644 --- a/gopls/internal/regtest/bench/completion_test.go +++ b/gopls/internal/regtest/bench/completion_test.go @@ -18,8 +18,9 @@ import ( type completionBenchOptions struct { file, locationRegexp string - // hook to run edits before initial completion - preCompletionEdits func(*Env) + // Hooks to run edits before initial completion + setup func(*Env) // run before the benchmark starts + beforeCompletion func(*Env) // run before each completion } func benchmarkCompletion(options completionBenchOptions, b *testing.B) { @@ -27,7 +28,11 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) { // Use a new environment for each test, to avoid any existing state from the // previous session. - sandbox, editor, awaiter, err := connectEditor(dir) + sandbox, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{ + Settings: map[string]interface{}{ + "completionBudget": "1m", // arbitrary long completion budget + }, + }) if err != nil { b.Fatal(err) } @@ -45,11 +50,10 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) { Sandbox: sandbox, Awaiter: awaiter, } - env.OpenFile(options.file) // Run edits required for this completion. - if options.preCompletionEdits != nil { - options.preCompletionEdits(env) + if options.setup != nil { + options.setup(env) } // Run a completion to make sure the system is warm. @@ -70,6 +74,9 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) { // initialization). b.Run("completion", func(b *testing.B) { for i := 0; i < b.N; i++ { + if options.beforeCompletion != nil { + options.beforeCompletion(env) + } env.Completion(options.file, pos) } }) @@ -92,7 +99,7 @@ func endPosInBuffer(env *Env, name string) fake.Pos { func BenchmarkStructCompletion(b *testing.B) { file := "internal/lsp/cache/session.go" - preCompletionEdits := func(env *Env) { + setup := func(env *Env) { env.OpenFile(file) originalBuffer := env.Editor.BufferText(file) env.EditBuffer(file, fake.Edit{ @@ -102,17 +109,19 @@ func BenchmarkStructCompletion(b *testing.B) { } benchmarkCompletion(completionBenchOptions{ - file: file, - locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`, - preCompletionEdits: preCompletionEdits, + file: file, + locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`, + setup: setup, }, b) } // Benchmark import completion in tools codebase. func BenchmarkImportCompletion(b *testing.B) { + const file = "internal/lsp/source/completion/completion.go" benchmarkCompletion(completionBenchOptions{ - file: "internal/lsp/source/completion/completion.go", + file: file, locationRegexp: `go\/()`, + setup: func(env *Env) { env.OpenFile(file) }, }, b) } @@ -120,7 +129,7 @@ func BenchmarkImportCompletion(b *testing.B) { func BenchmarkSliceCompletion(b *testing.B) { file := "internal/lsp/cache/session.go" - preCompletionEdits := func(env *Env) { + setup := func(env *Env) { env.OpenFile(file) originalBuffer := env.Editor.BufferText(file) env.EditBuffer(file, fake.Edit{ @@ -130,9 +139,9 @@ func BenchmarkSliceCompletion(b *testing.B) { } benchmarkCompletion(completionBenchOptions{ - file: file, - locationRegexp: `var testVariable \[\]byte (=)`, - preCompletionEdits: preCompletionEdits, + file: file, + locationRegexp: `var testVariable \[\]byte (=)`, + setup: setup, }, b) } @@ -144,7 +153,7 @@ func (c *completer) _() { c.inference.kindMatches(c.) } ` - preCompletionEdits := func(env *Env) { + setup := func(env *Env) { env.OpenFile(file) originalBuffer := env.Editor.BufferText(file) env.EditBuffer(file, fake.Edit{ @@ -154,8 +163,42 @@ func (c *completer) _() { } benchmarkCompletion(completionBenchOptions{ - file: file, - locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, - preCompletionEdits: preCompletionEdits, + file: file, + locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, + setup: setup, + }, b) +} + +// Benchmark completion following an arbitrary edit. +// +// Edits force type-checked packages to be invalidated, so we want to measure +// how long it takes before completion results are available. +func BenchmarkCompletionFollowingEdit(b *testing.B) { + file := "internal/lsp/source/completion/completion2.go" + fileContent := ` +package completion + +func (c *completer) _() { + c.inference.kindMatches(c.) + // __MAGIC_STRING_1 +} +` + setup := func(env *Env) { + env.CreateBuffer(file, fileContent) + } + + n := 1 + beforeCompletion := func(env *Env) { + old := fmt.Sprintf("__MAGIC_STRING_%d", n) + new := fmt.Sprintf("__MAGIC_STRING_%d", n+1) + n++ + env.RegexpReplace(file, old, new) + } + + benchmarkCompletion(completionBenchOptions{ + file: file, + locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, + setup: setup, + beforeCompletion: beforeCompletion, }, b) } diff --git a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go index e262a398f1d..b223e336dbe 100644 --- a/gopls/internal/regtest/bench/iwl_test.go +++ b/gopls/internal/regtest/bench/iwl_test.go @@ -8,6 +8,7 @@ import ( "context" "testing" + "golang.org/x/tools/internal/lsp/fake" . "golang.org/x/tools/internal/lsp/regtest" ) @@ -19,7 +20,7 @@ func BenchmarkIWL(b *testing.B) { ctx := context.Background() for i := 0; i < b.N; i++ { - _, editor, awaiter, err := connectEditor(dir) + _, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{}) if err != nil { b.Fatal(err) } diff --git a/internal/lsp/fake/edit.go b/internal/lsp/fake/edit.go index 8b04c390fc5..579c3a18de9 100644 --- a/internal/lsp/fake/edit.go +++ b/internal/lsp/fake/edit.go @@ -108,6 +108,8 @@ func inText(p Pos, content []string) bool { // editContent implements a simplistic, inefficient algorithm for applying text // edits to our buffer representation. It returns an error if the edit is // invalid for the current content. +// +// TODO(rfindley): this function does not handle non-ascii text correctly. func editContent(content []string, edits []Edit) ([]string, error) { newEdits := make([]Edit, len(edits)) copy(newEdits, edits) From 9b608524258a803e0ea4326a871a2d35b198a4e6 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 4 Aug 2022 16:19:07 -0400 Subject: [PATCH 172/723] gopls/internal/regtest: move TestMultipleModules_Warning to ./workspace This test is really about workspace setup, not diagnostics. Move it in advance of changes to this feature. Pure code move, no other changes. Change-Id: Ib78f1fe5ce701673f5aa071f399da11f208874df Reviewed-on: https://go-review.googlesource.com/c/tools/+/421498 Reviewed-by: Suzy Mueller --- .../regtest/diagnostics/diagnostics_test.go | 51 ------------------- .../internal/regtest/workspace/broken_test.go | 51 +++++++++++++++++++ 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index d7246ae7df6..b377668e876 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1692,57 +1692,6 @@ import ( }) } -func TestMultipleModules_Warning(t *testing.T) { - const modules = ` --- a/go.mod -- -module a.com - -go 1.12 --- a/a.go -- -package a --- b/go.mod -- -module b.com - -go 1.12 --- b/b.go -- -package b -` - for _, go111module := range []string{"on", "auto"} { - t.Run("GO111MODULE="+go111module, func(t *testing.T) { - WithOptions( - Modes(Default), - EnvVars{"GO111MODULE": go111module}, - ).Run(t, modules, func(t *testing.T, env *Env) { - env.OpenFile("a/a.go") - env.OpenFile("b/go.mod") - env.Await( - env.DiagnosticAtRegexp("a/a.go", "package a"), - env.DiagnosticAtRegexp("b/go.mod", "module b.com"), - OutstandingWork(lsp.WorkspaceLoadFailure, "gopls requires a module at the root of your workspace."), - ) - }) - }) - } - - // Expect no warning if GO111MODULE=auto in a directory in GOPATH. - t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) { - WithOptions( - Modes(Default), - EnvVars{"GO111MODULE": "auto"}, - InGOPATH(), - ).Run(t, modules, func(t *testing.T, env *Env) { - env.OpenFile("a/a.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - EmptyDiagnostics("a/a.go"), - ), - NoOutstandingWork(), - ) - }) - }) -} - func TestNestedModules(t *testing.T) { const proxy = ` -- nested.com@v1.0.0/go.mod -- diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index e88b98b50e3..c4ac905bd0d 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -167,3 +167,54 @@ const F = named.D - 3 env.Await(NoOutstandingDiagnostics()) }) } + +func TestMultipleModules_Warning(t *testing.T) { + const modules = ` +-- a/go.mod -- +module a.com + +go 1.12 +-- a/a.go -- +package a +-- b/go.mod -- +module b.com + +go 1.12 +-- b/b.go -- +package b +` + for _, go111module := range []string{"on", "auto"} { + t.Run("GO111MODULE="+go111module, func(t *testing.T) { + WithOptions( + Modes(Default), + EnvVars{"GO111MODULE": go111module}, + ).Run(t, modules, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + env.OpenFile("b/go.mod") + env.Await( + env.DiagnosticAtRegexp("a/a.go", "package a"), + env.DiagnosticAtRegexp("b/go.mod", "module b.com"), + OutstandingWork(lsp.WorkspaceLoadFailure, "gopls requires a module at the root of your workspace."), + ) + }) + }) + } + + // Expect no warning if GO111MODULE=auto in a directory in GOPATH. + t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) { + WithOptions( + Modes(Default), + EnvVars{"GO111MODULE": "auto"}, + InGOPATH(), + ).Run(t, modules, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + EmptyDiagnostics("a/a.go"), + ), + NoOutstandingWork(), + ) + }) + }) +} From fff6d6d39f5d4992e910a17bd6c47a9858f1c709 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 5 Aug 2022 10:25:57 -0400 Subject: [PATCH 173/723] internal/lsp: update the broken workspace message to mention go.work For users of Go >= 1.18, update the error message presented to users with broken workspaces to suggest using a go.work. For older Go versions, suggest updating to 1.18+. Also: - add support for changing workspace folders from the fake editor - update the test to confirm that changing workspace folders resolves the error message - inline validBuildConfiguration, and make a few TODOs for how the code could be further cleaned up Fixes golang/go#53882 Change-Id: Ia03dcfec59569b1a3ac941dc40d079b9c2593825 Reviewed-on: https://go-review.googlesource.com/c/tools/+/421499 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Dylan Le Reviewed-by: Suzy Mueller --- .../internal/regtest/workspace/broken_test.go | 31 +++++- internal/lsp/cache/load.go | 22 +++- internal/lsp/cache/snapshot.go | 21 +++- internal/lsp/cache/view.go | 21 ---- internal/lsp/fake/client.go | 7 +- internal/lsp/fake/editor.go | 101 ++++++++++++++---- internal/lsp/fake/sandbox.go | 46 +++++--- internal/lsp/progress/progress.go | 2 +- internal/lsp/regtest/wrappers.go | 20 ++++ internal/span/uri.go | 4 +- 10 files changed, 208 insertions(+), 67 deletions(-) diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index c4ac905bd0d..b1f35b38d33 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -169,6 +169,14 @@ const F = named.D - 3 } func TestMultipleModules_Warning(t *testing.T) { + msgForVersion := func(ver int) string { + if ver >= 18 { + return `gopls was not able to find modules in your workspace.` + } else { + return `gopls requires a module at the root of your workspace.` + } + } + const modules = ` -- a/go.mod -- module a.com @@ -189,13 +197,34 @@ package b Modes(Default), EnvVars{"GO111MODULE": go111module}, ).Run(t, modules, func(t *testing.T, env *Env) { + ver := env.GoVersion() + msg := msgForVersion(ver) env.OpenFile("a/a.go") env.OpenFile("b/go.mod") env.Await( env.DiagnosticAtRegexp("a/a.go", "package a"), env.DiagnosticAtRegexp("b/go.mod", "module b.com"), - OutstandingWork(lsp.WorkspaceLoadFailure, "gopls requires a module at the root of your workspace."), + OutstandingWork(lsp.WorkspaceLoadFailure, msg), ) + + // Changing the workspace folders to the valid modules should resolve + // the workspace error. + env.ChangeWorkspaceFolders("a", "b") + env.Await(NoOutstandingWork()) + + env.ChangeWorkspaceFolders(".") + + // TODO(rfindley): when GO111MODULE=auto, we need to open or change a + // file here in order to detect a critical error. This is because gopls + // has forgotten about a/a.go, and therefor doesn't hit the heuristic + // "all packages are command-line-arguments". + // + // This is broken, and could be fixed by adjusting the heuristic to + // account for the scenario where there are *no* workspace packages, or + // (better) trying to get workspace packages for each open file. See + // also golang/go#54261. + env.OpenFile("b/b.go") + env.Await(OutstandingWork(lsp.WorkspaceLoadFailure, msg)) }) }) } diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 0952fc679be..7f30939e1c8 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -283,15 +283,20 @@ func (m *moduleErrorMap) Error() string { // workspaceLayoutErrors returns a diagnostic for every open file, as well as // an error message if there are no open files. func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError { + // TODO(rfindley): do we really not want to show a critical error if the user + // has no go.mod files? if len(s.workspace.getKnownModFiles()) == 0 { return nil } + + // TODO(rfindley): both of the checks below should be delegated to the workspace. if s.view.userGo111Module == off { return nil } if s.workspace.moduleSource != legacyWorkspace { return nil } + // If the user has one module per view, there is nothing to warn about. if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 { return nil @@ -305,10 +310,21 @@ func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalErr // that the user has opened a directory that contains multiple modules. // Check for that an warn about it. if !s.ValidBuildConfiguration() { - msg := `gopls requires a module at the root of your workspace. -You can work with multiple modules by opening each one as a workspace folder. -Improvements to this workflow will be coming soon, and you can learn more here: + var msg string + if s.view.goversion >= 18 { + msg = `gopls was not able to find modules in your workspace. +When outside of GOPATH, gopls needs to know which modules you are working on. +You can fix this by opening your workspace to a folder inside a Go module, or +by using a go.work file to specify multiple modules. +See the documentation for more information on setting up your workspace: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` + } else { + msg = `gopls requires a module at the root of your workspace. +You can work with multiple modules by upgrading to Go 1.18 or later, and using +go workspaces (go.work files). +See the documentation for more information on setting up your workspace: +https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` + } return &source.CriticalError{ MainError: fmt.Errorf(msg), Diagnostics: s.applyCriticalErrorToFiles(ctx, msg, openFiles), diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index da2d7b52e15..da4fe6265a7 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -274,7 +274,24 @@ func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle { } func (s *snapshot) ValidBuildConfiguration() bool { - return validBuildConfiguration(s.view.rootURI, &s.view.workspaceInformation, s.workspace.getActiveModFiles()) + // Since we only really understand the `go` command, if the user has a + // different GOPACKAGESDRIVER, assume that their configuration is valid. + if s.view.hasGopackagesDriver { + return true + } + // Check if the user is working within a module or if we have found + // multiple modules in the workspace. + if len(s.workspace.getActiveModFiles()) > 0 { + return true + } + // The user may have a multiple directories in their GOPATH. + // Check if the workspace is within any of them. + for _, gp := range filepath.SplitList(s.view.gopath) { + if source.InDir(filepath.Join(gp, "src"), s.view.rootURI.Filename()) { + return true + } + } + return false } // workspaceMode describes the way in which the snapshot's workspace should @@ -1358,6 +1375,8 @@ func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { // with the user's workspace layout. Workspace packages that only have the // ID "command-line-arguments" are usually a symptom of a bad workspace // configuration. + // + // TODO(rfindley): re-evaluate this heuristic. if containsCommandLineArguments(wsPkgs) { return s.workspaceLayoutError(ctx) } diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 61501098c07..1aca36e7d40 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -926,27 +926,6 @@ func defaultCheckPathCase(path string) error { return nil } -func validBuildConfiguration(folder span.URI, ws *workspaceInformation, modFiles map[span.URI]struct{}) bool { - // Since we only really understand the `go` command, if the user has a - // different GOPACKAGESDRIVER, assume that their configuration is valid. - if ws.hasGopackagesDriver { - return true - } - // Check if the user is working within a module or if we have found - // multiple modules in the workspace. - if len(modFiles) > 0 { - return true - } - // The user may have a multiple directories in their GOPATH. - // Check if the workspace is within any of them. - for _, gp := range filepath.SplitList(ws.gopath) { - if source.InDir(filepath.Join(gp, "src"), folder.Filename()) { - return true - } - } - return false -} - // getGoEnv gets the view's various GO* values. func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, go111module string, configEnv []string) (environmentVariables, map[string]string, error) { envVars := environmentVariables{} diff --git a/internal/lsp/fake/client.go b/internal/lsp/fake/client.go index 4c5f2a2e1bd..bb258f29f50 100644 --- a/internal/lsp/fake/client.go +++ b/internal/lsp/fake/client.go @@ -74,10 +74,11 @@ func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { results := make([]interface{}, len(p.Items)) for i, item := range p.Items { - if item.Section != "gopls" { - continue + if item.Section == "gopls" { + c.editor.mu.Lock() + results[i] = c.editor.settingsLocked() + c.editor.mu.Unlock() } - results[i] = c.editor.settings() } return results, nil } diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index bc2cb2fe9b1..db4326011d9 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -121,7 +121,7 @@ func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { // // It returns the editor, so that it may be called as follows: // -// editor, err := NewEditor(s).Connect(ctx, conn) +// editor, err := NewEditor(s).Connect(ctx, conn, hooks) func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, hooks ClientHooks) (*Editor, error) { bgCtx, cancelConn := context.WithCancel(xcontext.Detach(ctx)) conn := connector.Connect(bgCtx) @@ -135,7 +135,7 @@ func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, ho protocol.ClientHandler(e.client, jsonrpc2.MethodNotFound))) - if err := e.initialize(ctx, e.config.WorkspaceFolders); err != nil { + if err := e.initialize(ctx); err != nil { return nil, err } e.sandbox.Workdir.AddWatcher(e.onFileChanges) @@ -197,11 +197,10 @@ func (e *Editor) Client() *Client { return e.client } -// settings builds the settings map for use in LSP settings -// RPCs. -func (e *Editor) settings() map[string]interface{} { - e.mu.Lock() - defer e.mu.Unlock() +// settingsLocked builds the settings map for use in LSP settings RPCs. +// +// e.mu must be held while calling this function. +func (e *Editor) settingsLocked() map[string]interface{} { env := make(map[string]string) for k, v := range e.defaultEnv { env[k] = v @@ -240,26 +239,19 @@ func (e *Editor) settings() map[string]interface{} { return settings } -func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error { +func (e *Editor) initialize(ctx context.Context) error { params := &protocol.ParamInitialize{} params.ClientInfo.Name = "fakeclient" params.ClientInfo.Version = "v1.0.0" - - if workspaceFolders == nil { - workspaceFolders = []string{string(e.sandbox.Workdir.RelativeTo)} - } - for _, folder := range workspaceFolders { - params.WorkspaceFolders = append(params.WorkspaceFolders, protocol.WorkspaceFolder{ - URI: string(e.sandbox.Workdir.URI(folder)), - Name: filepath.Base(folder), - }) - } - + e.mu.Lock() + params.WorkspaceFolders = e.makeWorkspaceFoldersLocked() + params.InitializationOptions = e.settingsLocked() + e.mu.Unlock() params.Capabilities.Workspace.Configuration = true params.Capabilities.Window.WorkDoneProgress = true + // TODO: set client capabilities params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated} - params.InitializationOptions = e.settings() params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true @@ -296,6 +288,27 @@ func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) erro return nil } +// makeWorkspaceFoldersLocked creates a slice of workspace folders to use for +// this editing session, based on the editor configuration. +// +// e.mu must be held while calling this function. +func (e *Editor) makeWorkspaceFoldersLocked() (folders []protocol.WorkspaceFolder) { + paths := e.config.WorkspaceFolders + if len(paths) == 0 { + paths = append(paths, string(e.sandbox.Workdir.RelativeTo)) + } + + for _, path := range paths { + uri := string(e.sandbox.Workdir.URI(path)) + folders = append(folders, protocol.WorkspaceFolder{ + URI: uri, + Name: filepath.Base(uri), + }) + } + + return folders +} + // onFileChanges is registered to be called by the Workdir on any writes that // go through the Workdir API. It is called synchronously by the Workdir. func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) { @@ -1195,6 +1208,54 @@ func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig return nil } +// ChangeWorkspaceFolders sets the new workspace folders, and sends a +// didChangeWorkspaceFolders notification to the server. +// +// The given folders must all be unique. +func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error { + // capture existing folders so that we can compute the change. + e.mu.Lock() + oldFolders := e.makeWorkspaceFoldersLocked() + e.config.WorkspaceFolders = folders + newFolders := e.makeWorkspaceFoldersLocked() + e.mu.Unlock() + + if e.Server == nil { + return nil + } + + var params protocol.DidChangeWorkspaceFoldersParams + + // Keep track of old workspace folders that must be removed. + toRemove := make(map[protocol.URI]protocol.WorkspaceFolder) + for _, folder := range oldFolders { + toRemove[folder.URI] = folder + } + + // Sanity check: if we see a folder twice the algorithm below doesn't work, + // so track seen folders to ensure that we panic in that case. + seen := make(map[protocol.URI]protocol.WorkspaceFolder) + for _, folder := range newFolders { + if _, ok := seen[folder.URI]; ok { + panic(fmt.Sprintf("folder %s seen twice", folder.URI)) + } + + // If this folder already exists, we don't want to remove it. + // Otherwise, we need to add it. + if _, ok := toRemove[folder.URI]; ok { + delete(toRemove, folder.URI) + } else { + params.Event.Added = append(params.Event.Added, folder) + } + } + + for _, v := range toRemove { + params.Event.Removed = append(params.Event.Removed, v) + } + + return e.Server.DidChangeWorkspaceFolders(ctx, ¶ms) +} + // CodeAction executes a codeAction request on the server. func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { if e.Server == nil { diff --git a/internal/lsp/fake/sandbox.go b/internal/lsp/fake/sandbox.go index 72b846cdc99..e0e113f4914 100644 --- a/internal/lsp/fake/sandbox.go +++ b/internal/lsp/fake/sandbox.go @@ -23,10 +23,11 @@ import ( // Sandbox holds a collection of temporary resources to use for working with Go // code in tests. type Sandbox struct { - gopath string - rootdir string - goproxy string - Workdir *Workdir + gopath string + rootdir string + goproxy string + Workdir *Workdir + goCommandRunner gocommand.Runner } // SandboxConfig controls the behavior of a test sandbox. The zero value @@ -229,30 +230,36 @@ func (sb *Sandbox) GoEnv() map[string]string { return vars } -// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is -// true, the sandbox scans the working directory and emits file change events -// for any file changes it finds. -func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string, checkForFileChanges bool) error { +// goCommandInvocation returns a new gocommand.Invocation initialized with the +// sandbox environment variables and working directory. +func (sb *Sandbox) goCommandInvocation() gocommand.Invocation { var vars []string for k, v := range sb.GoEnv() { vars = append(vars, fmt.Sprintf("%s=%s", k, v)) } inv := gocommand.Invocation{ - Verb: verb, - Args: args, - Env: vars, + Env: vars, } - // Use the provided directory for the working directory, if available. // sb.Workdir may be nil if we exited the constructor with errors (we call // Close to clean up any partial state from the constructor, which calls // RunGoCommand). + if sb.Workdir != nil { + inv.WorkingDir = string(sb.Workdir.RelativeTo) + } + return inv +} + +// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is +// true, the sandbox scans the working directory and emits file change events +// for any file changes it finds. +func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string, checkForFileChanges bool) error { + inv := sb.goCommandInvocation() + inv.Verb = verb + inv.Args = args if dir != "" { inv.WorkingDir = sb.Workdir.AbsPath(dir) - } else if sb.Workdir != nil { - inv.WorkingDir = string(sb.Workdir.RelativeTo) } - gocmdRunner := &gocommand.Runner{} - stdout, stderr, _, err := gocmdRunner.RunRaw(ctx, inv) + stdout, stderr, _, err := sb.goCommandRunner.RunRaw(ctx, inv) if err != nil { return fmt.Errorf("go command failed (stdout: %s) (stderr: %s): %v", stdout.String(), stderr.String(), err) } @@ -269,6 +276,13 @@ func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []st return nil } +// GoVersion checks the version of the go command. +// It returns the X in Go 1.X. +func (sb *Sandbox) GoVersion(ctx context.Context) (int, error) { + inv := sb.goCommandInvocation() + return gocommand.GoVersion(ctx, inv, &sb.goCommandRunner) +} + // Close removes all state associated with the sandbox. func (sb *Sandbox) Close() error { var goCleanErr error diff --git a/internal/lsp/progress/progress.go b/internal/lsp/progress/progress.go index 8b0d1c6a224..ee63bb76cde 100644 --- a/internal/lsp/progress/progress.go +++ b/internal/lsp/progress/progress.go @@ -118,7 +118,7 @@ func (t *Tracker) Start(ctx context.Context, title, message string, token protoc }, }) if err != nil { - event.Error(ctx, "generate progress begin", err) + event.Error(ctx, "progress begin", err) } return wd } diff --git a/internal/lsp/regtest/wrappers.go b/internal/lsp/regtest/wrappers.go index d8c080c0fec..e8f49a68dfe 100644 --- a/internal/lsp/regtest/wrappers.go +++ b/internal/lsp/regtest/wrappers.go @@ -277,6 +277,17 @@ func (e *Env) RunGoCommandInDir(dir, verb string, args ...string) { } } +// GoVersion checks the version of the go command. +// It returns the X in Go 1.X. +func (e *Env) GoVersion() int { + e.T.Helper() + v, err := e.Sandbox.GoVersion(e.Ctx) + if err != nil { + e.T.Fatal(err) + } + return v +} + // DumpGoSum prints the correct go.sum contents for dir in txtar format, // for use in creating regtests. func (e *Env) DumpGoSum(dir string) { @@ -433,3 +444,12 @@ func (e *Env) ChangeConfiguration(newConfig fake.EditorConfig) { e.T.Fatal(err) } } + +// ChangeWorkspaceFolders updates the editor workspace folders, calling t.Fatal +// on any error. +func (e *Env) ChangeWorkspaceFolders(newFolders ...string) { + e.T.Helper() + if err := e.Editor.ChangeWorkspaceFolders(e.Ctx, newFolders); err != nil { + e.T.Fatal(err) + } +} diff --git a/internal/span/uri.go b/internal/span/uri.go index 8132665d75f..ce874e70318 100644 --- a/internal/span/uri.go +++ b/internal/span/uri.go @@ -144,7 +144,9 @@ func equalURI(a, b URI) bool { } // URIFromPath returns a span URI for the supplied file path. -// It will always have the file scheme. +// +// For empty paths, URIFromPath returns the empty URI "". +// For non-empty paths, URIFromPath returns a uri with the file:// scheme. func URIFromPath(path string) URI { if path == "" { return "" From 98aef77998df89514a36ea2d918ae52f85c52f71 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 5 Aug 2022 15:59:27 -0400 Subject: [PATCH 174/723] internal/lsp/cache: track explicit go.work files outside the workspace In order to correctly process changes to the go.work file, the workspace must know about GOWORK settings configured in the users environment. Compute this when initializing the view, and thread this through to the workspace. At this point, workspace information is spread around in a few places. Add some TODOs to clean this up. Also remove some module data that was not used in TestBrokenWorkspace_DuplicateModules. Updates golang/go#53631 Change-Id: Ie0577d702c8a229304387bc7fe53a8befb544acb Reviewed-on: https://go-review.googlesource.com/c/tools/+/421500 Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- .../internal/regtest/workspace/broken_test.go | 6 +- .../regtest/workspace/fromenv_test.go | 54 ++++++++++++++++ internal/lsp/cache/session.go | 10 ++- internal/lsp/cache/snapshot.go | 6 ++ internal/lsp/cache/view.go | 14 ++++- internal/lsp/cache/workspace.go | 63 ++++++++++++------- internal/lsp/cache/workspace_test.go | 4 +- 7 files changed, 129 insertions(+), 28 deletions(-) create mode 100644 gopls/internal/regtest/workspace/fromenv_test.go diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index b1f35b38d33..fbc41de1615 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -16,6 +16,10 @@ import ( // This file holds various tests for UX with respect to broken workspaces. // // TODO: consolidate other tests here. +// +// TODO: write more tests: +// - an explicit GOWORK value that doesn't exist +// - using modules and/or GOWORK inside of GOPATH? // Test for golang/go#53933 func TestBrokenWorkspace_DuplicateModules(t *testing.T) { @@ -28,8 +32,6 @@ func TestBrokenWorkspace_DuplicateModules(t *testing.T) { module example.com/foo go 1.12 --- example.com/foo@v1.2.3/foo.go -- -package foo ` const src = ` diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go new file mode 100644 index 00000000000..1d95160eaa0 --- /dev/null +++ b/gopls/internal/regtest/workspace/fromenv_test.go @@ -0,0 +1,54 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package workspace + +import ( + "testing" + + . "golang.org/x/tools/internal/lsp/regtest" +) + +// Test that setting go.work via environment variables or settings works. +func TestUseGoWorkOutsideTheWorkspace(t *testing.T) { + const files = ` +-- work/a/go.mod -- +module a.com + +go 1.12 +-- work/a/a.go -- +package a +-- work/b/go.mod -- +module b.com + +go 1.12 +-- work/b/b.go -- +package b + +func _() { + x := 1 // unused +} +-- config/go.work -- +go 1.18 + +use ( + $SANDBOX_WORKDIR/work/a + $SANDBOX_WORKDIR/work/b +) +` + + WithOptions( + EnvVars{"GOWORK": "$SANDBOX_WORKDIR/config/go.work"}, + ).Run(t, files, func(t *testing.T, env *Env) { + // Even though work/b is not open, we should get its diagnostics as it is + // included in the workspace. + env.OpenFile("work/a/a.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexpWithMessage("work/b/b.go", "x := 1", "not used"), + ), + ) + }) +} diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index 984e8c1e75b..d11c06d1e7e 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -7,6 +7,7 @@ package cache import ( "context" "fmt" + "os" "strconv" "strings" "sync" @@ -199,8 +200,14 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, } } + explicitGowork := os.Getenv("GOWORK") + if v, ok := options.Env["GOWORK"]; ok { + explicitGowork = v + } + goworkURI := span.URIFromPath(explicitGowork) + // Build the gopls workspace, collecting active modules in the view. - workspace, err := newWorkspace(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), wsInfo.userGo111Module == off, options.ExperimentalWorkspaceModule) + workspace, err := newWorkspace(ctx, root, goworkURI, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), wsInfo.userGo111Module == off, options.ExperimentalWorkspaceModule) if err != nil { return nil, nil, func() {}, err } @@ -223,6 +230,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, filesByURI: map[span.URI]*fileBase{}, filesByBase: map[string][]*fileBase{}, rootURI: root, + explicitGowork: goworkURI, workspaceInformation: *wsInfo, } v.importsState = &importsState{ diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index da4fe6265a7..6ed6fe5360f 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -436,6 +436,12 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat s.view.optionsMu.Lock() allowModfileModificationOption := s.view.options.AllowModfileModifications allowNetworkOption := s.view.options.AllowImplicitNetworkAccess + + // TODO(rfindley): this is very hard to follow, and may not even be doing the + // right thing: should inv.Env really trample view.options? Do we ever invoke + // this with a non-empty inv.Env? + // + // We should refactor to make it clearer that the correct env is being used. inv.Env = append(append(append(os.Environ(), s.view.options.EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.effectiveGo111Module) inv.BuildFlags = append([]string{}, s.view.options.BuildFlags...) s.view.optionsMu.Unlock() diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 1aca36e7d40..b23ed614f6d 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -27,6 +27,7 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/span" @@ -98,6 +99,13 @@ type View struct { // is just the folder. If we are in module mode, this is the module rootURI. rootURI span.URI + // explicitGowork is, if non-empty, the URI for the explicit go.work file + // provided via the users environment. + // + // TODO(rfindley): this is duplicated in the workspace type. Refactor to + // eliminate this duplication. + explicitGowork span.URI + // workspaceInformation tracks various details about this view's // environment variables, go version, and use of modules. workspaceInformation @@ -469,7 +477,7 @@ func (v *View) relevantChange(c source.FileModification) bool { // TODO(rstambler): Make sure the go.work/gopls.mod files are always known // to the view. for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - if c.URI == uriForSource(v.rootURI, src) { + if c.URI == uriForSource(v.rootURI, v.explicitGowork, src) { return true } } @@ -813,9 +821,13 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, } // The value of GOPACKAGESDRIVER is not returned through the go command. gopackagesdriver := os.Getenv("GOPACKAGESDRIVER") + // TODO(rfindley): this looks wrong, or at least overly defensive. If the + // value of GOPACKAGESDRIVER is not returned from the go command... why do we + // look it up here? for _, s := range env { split := strings.SplitN(s, "=", 2) if split[0] == "GOPACKAGESDRIVER" { + bug.Reportf("found GOPACKAGESDRIVER from the go command") // see note above gopackagesdriver = split[1] } } diff --git a/internal/lsp/cache/workspace.go b/internal/lsp/cache/workspace.go index 9182cb9ad58..f04fbe8eec0 100644 --- a/internal/lsp/cache/workspace.go +++ b/internal/lsp/cache/workspace.go @@ -46,6 +46,19 @@ func (s workspaceSource) String() string { } } +// workspaceCommon holds immutable information about the workspace setup. +// +// TODO(rfindley): there is some redundancy here with workspaceInformation. +// Reconcile these two types. +type workspaceCommon struct { + root span.URI + excludePath func(string) bool + + // explicitGowork is, if non-empty, the URI for the explicit go.work file + // provided via the user's environment. + explicitGowork span.URI +} + // workspace tracks go.mod files in the workspace, along with the // gopls.mod file, to provide support for multi-module workspaces. // @@ -58,8 +71,8 @@ func (s workspaceSource) String() string { // This type is immutable (or rather, idempotent), so that it may be shared // across multiple snapshots. type workspace struct { - root span.URI - excludePath func(string) bool + workspaceCommon + moduleSource workspaceSource // activeModFiles holds the active go.mod files. @@ -98,17 +111,21 @@ type workspace struct { // // TODO(rfindley): newWorkspace should perhaps never fail, relying instead on // the criticalError method to surface problems in the workspace. -// TODO(rfindley): this function should accept the GOWORK value, if specified -// by the user. -func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff, useWsModule bool) (*workspace, error) { +func newWorkspace(ctx context.Context, root, explicitGowork span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff, useWsModule bool) (*workspace, error) { ws := &workspace{ - root: root, - excludePath: excludePath, + workspaceCommon: workspaceCommon{ + root: root, + explicitGowork: explicitGowork, + excludePath: excludePath, + }, } // The user may have a gopls.mod or go.work file that defines their // workspace. - if err := loadExplicitWorkspaceFile(ctx, ws, fs); err == nil { + // + // TODO(rfindley): if GO111MODULE=off, this looks wrong, though there are + // probably other problems. + if err := ws.loadExplicitWorkspaceFile(ctx, fs); err == nil { return ws, nil } @@ -140,15 +157,15 @@ func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excl // loadExplicitWorkspaceFile loads workspace information from go.work or // gopls.mod files, setting the active modules, mod file, and module source // accordingly. -func loadExplicitWorkspaceFile(ctx context.Context, ws *workspace, fs source.FileSource) error { +func (ws *workspace) loadExplicitWorkspaceFile(ctx context.Context, fs source.FileSource) error { for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - fh, err := fs.GetFile(ctx, uriForSource(ws.root, src)) + fh, err := fs.GetFile(ctx, uriForSource(ws.root, ws.explicitGowork, src)) if err != nil { return err } contents, err := fh.Read() if err != nil { - continue + continue // TODO(rfindley): is it correct to proceed here? } var file *modfile.File var activeModFiles map[span.URI]struct{} @@ -313,15 +330,14 @@ func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, // Clone the workspace. This may be discarded if nothing changed. changed := false result := &workspace{ - root: w.root, - moduleSource: w.moduleSource, - knownModFiles: make(map[span.URI]struct{}), - activeModFiles: make(map[span.URI]struct{}), - workFile: w.workFile, - mod: w.mod, - sum: w.sum, - wsDirs: w.wsDirs, - excludePath: w.excludePath, + workspaceCommon: w.workspaceCommon, + moduleSource: w.moduleSource, + knownModFiles: make(map[span.URI]struct{}), + activeModFiles: make(map[span.URI]struct{}), + workFile: w.workFile, + mod: w.mod, + sum: w.sum, + wsDirs: w.wsDirs, } for k, v := range w.knownModFiles { result.knownModFiles[k] = v @@ -391,7 +407,7 @@ func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[ // exists or walk the filesystem if it has been deleted. // go.work should override the gopls.mod if both exist. for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - uri := uriForSource(ws.root, src) + uri := uriForSource(ws.root, ws.explicitGowork, src) // File opens/closes are just no-ops. change, ok := changes[uri] if !ok { @@ -460,12 +476,15 @@ func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[ } // goplsModURI returns the URI for the gopls.mod file contained in root. -func uriForSource(root span.URI, src workspaceSource) span.URI { +func uriForSource(root, explicitGowork span.URI, src workspaceSource) span.URI { var basename string switch src { case goplsModWorkspace: basename = "gopls.mod" case goWorkWorkspace: + if explicitGowork != "" { + return explicitGowork + } basename = "go.work" default: return "" diff --git a/internal/lsp/cache/workspace_test.go b/internal/lsp/cache/workspace_test.go index 871e4bb9b98..f1cd00b7119 100644 --- a/internal/lsp/cache/workspace_test.go +++ b/internal/lsp/cache/workspace_test.go @@ -280,7 +280,7 @@ replace gopls.test => ../../gopls.test2`, false}, fs := &osFileSource{} excludeNothing := func(string) bool { return false } - w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode) + w, err := newWorkspace(ctx, root, "", fs, excludeNothing, false, !test.legacyMode) if err != nil { t.Fatal(err) } @@ -325,7 +325,7 @@ func workspaceFromTxtar(t *testing.T, files string) (*workspace, func(), error) fs := &osFileSource{} excludeNothing := func(string) bool { return false } - workspace, err := newWorkspace(ctx, root, fs, excludeNothing, false, false) + workspace, err := newWorkspace(ctx, root, "", fs, excludeNothing, false, false) return workspace, cleanup, err } From 92d58ea4e734e02be542fe27eef6fb1a600e50f4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 5 Aug 2022 18:08:11 -0400 Subject: [PATCH 175/723] internal/lsp/cache: register a file watcher for explicit GOWORK values When the go.work file is set by the GOWORK environment variable, we must create an additional file watching pattern. Fixes golang/go#53631 Change-Id: I2d78c5a9ee8a71551d5274db7eb4e6c623d8db74 Reviewed-on: https://go-review.googlesource.com/c/tools/+/421501 gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller --- .../regtest/diagnostics/diagnostics_test.go | 7 +- .../regtest/workspace/fromenv_test.go | 2 + internal/lsp/cache/snapshot.go | 4 + internal/lsp/regtest/env.go | 11 +- internal/lsp/regtest/expectation.go | 102 ++++++++++-------- 5 files changed, 77 insertions(+), 49 deletions(-) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index b377668e876..209e015e0ef 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1548,7 +1548,7 @@ func Hello() { } -- go.mod -- module mod.com --- main.go -- +-- cmd/main.go -- package main import "mod.com/bob" @@ -1558,11 +1558,12 @@ func main() { } ` Run(t, mod, func(t *testing.T, env *Env) { + env.Await(FileWatchMatching("bob")) env.RemoveWorkspaceFile("bob") env.Await( - env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), + env.DiagnosticAtRegexp("cmd/main.go", `"mod.com/bob"`), EmptyDiagnostics("bob/bob.go"), - RegistrationMatching("didChangeWatchedFiles"), + NoFileWatchMatching("bob"), ) }) } diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go index 1d95160eaa0..8a77867cf44 100644 --- a/gopls/internal/regtest/workspace/fromenv_test.go +++ b/gopls/internal/regtest/workspace/fromenv_test.go @@ -41,6 +41,8 @@ use ( WithOptions( EnvVars{"GOWORK": "$SANDBOX_WORKDIR/config/go.work"}, ).Run(t, files, func(t *testing.T, env *Env) { + // When we have an explicit GOWORK set, we should get a file watch request. + env.Await(FileWatchMatching(`config.go\.work`)) // Even though work/b is not open, we should get its diagnostics as it is // included in the workspace. env.OpenFile("work/a/a.go") diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 6ed6fe5360f..0fa670cf20f 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -887,6 +887,10 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru fmt.Sprintf("**/*.{%s}", extensions): {}, } + if s.view.explicitGowork != "" { + patterns[s.view.explicitGowork.Filename()] = struct{}{} + } + // Add a pattern for each Go module in the workspace that is not within the view. dirs := s.workspace.dirs(ctx, s) for _, dir := range dirs { diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go index 502636a8503..f8a68b34190 100644 --- a/internal/lsp/regtest/env.go +++ b/internal/lsp/regtest/env.go @@ -85,8 +85,9 @@ type State struct { showMessage []*protocol.ShowMessageParams showMessageRequest []*protocol.ShowMessageRequestParams - registrations []*protocol.RegistrationParams - unregistrations []*protocol.UnregistrationParams + registrations []*protocol.RegistrationParams + registeredCapabilities map[string]protocol.Registration + unregistrations []*protocol.UnregistrationParams // outstandingWork is a map of token->work summary. All tokens are assumed to // be string, though the spec allows for numeric tokens as well. When work @@ -226,6 +227,12 @@ func (a *Awaiter) onRegistration(_ context.Context, m *protocol.RegistrationPara defer a.mu.Unlock() a.state.registrations = append(a.state.registrations, m) + if a.state.registeredCapabilities == nil { + a.state.registeredCapabilities = make(map[string]protocol.Registration) + } + for _, reg := range m.Registrations { + a.state.registeredCapabilities[reg.Method] = reg + } a.checkConditionsLocked() return nil } diff --git a/internal/lsp/regtest/expectation.go b/internal/lsp/regtest/expectation.go index a0a7d529aae..7867af980b0 100644 --- a/internal/lsp/regtest/expectation.go +++ b/internal/lsp/regtest/expectation.go @@ -394,32 +394,66 @@ func NoLogMatching(typ protocol.MessageType, re string) LogExpectation { } } -// RegistrationExpectation is an expectation on the capability registrations -// received by the editor from gopls. -type RegistrationExpectation struct { - check func([]*protocol.RegistrationParams) Verdict - description string +// FileWatchMatching expects that a file registration matches re. +func FileWatchMatching(re string) SimpleExpectation { + return SimpleExpectation{ + check: checkFileWatch(re, Met, Unmet), + description: fmt.Sprintf("file watch matching %q", re), + } } -// Check implements the Expectation interface. -func (e RegistrationExpectation) Check(s State) Verdict { - return e.check(s.registrations) +// NoFileWatchMatching expects that no file registration matches re. +func NoFileWatchMatching(re string) SimpleExpectation { + return SimpleExpectation{ + check: checkFileWatch(re, Unmet, Met), + description: fmt.Sprintf("no file watch matching %q", re), + } } -// Description implements the Expectation interface. -func (e RegistrationExpectation) Description() string { - return e.description +func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict { + rec := regexp.MustCompile(re) + return func(s State) Verdict { + r := s.registeredCapabilities["workspace/didChangeWatchedFiles"] + watchers := jsonProperty(r.RegisterOptions, "watchers").([]interface{}) + for _, watcher := range watchers { + pattern := jsonProperty(watcher, "globPattern").(string) + if rec.MatchString(pattern) { + return onMatch + } + } + return onNoMatch + } +} + +// jsonProperty extracts a value from a path of JSON property names, assuming +// the default encoding/json unmarshaling to the empty interface (i.e.: that +// JSON objects are unmarshalled as map[string]interface{}) +// +// For example, if obj is unmarshalled from the following json: +// +// { +// "foo": { "bar": 3 } +// } +// +// Then jsonProperty(obj, "foo", "bar") will be 3. +func jsonProperty(obj interface{}, path ...string) interface{} { + if len(path) == 0 || obj == nil { + return obj + } + m := obj.(map[string]interface{}) + return jsonProperty(m[path[0]], path[1:]...) } // RegistrationMatching asserts that the client has received a capability // registration matching the given regexp. -func RegistrationMatching(re string) RegistrationExpectation { - rec, err := regexp.Compile(re) - if err != nil { - panic(err) - } - check := func(params []*protocol.RegistrationParams) Verdict { - for _, p := range params { +// +// TODO(rfindley): remove this once TestWatchReplaceTargets has been revisited. +// +// Deprecated: use (No)FileWatchMatching +func RegistrationMatching(re string) SimpleExpectation { + rec := regexp.MustCompile(re) + check := func(s State) Verdict { + for _, p := range s.registrations { for _, r := range p.Registrations { if rec.Match([]byte(r.Method)) { return Met @@ -428,38 +462,18 @@ func RegistrationMatching(re string) RegistrationExpectation { } return Unmet } - return RegistrationExpectation{ + return SimpleExpectation{ check: check, description: fmt.Sprintf("registration matching %q", re), } } -// UnregistrationExpectation is an expectation on the capability -// unregistrations received by the editor from gopls. -type UnregistrationExpectation struct { - check func([]*protocol.UnregistrationParams) Verdict - description string -} - -// Check implements the Expectation interface. -func (e UnregistrationExpectation) Check(s State) Verdict { - return e.check(s.unregistrations) -} - -// Description implements the Expectation interface. -func (e UnregistrationExpectation) Description() string { - return e.description -} - // UnregistrationMatching asserts that the client has received an // unregistration whose ID matches the given regexp. -func UnregistrationMatching(re string) UnregistrationExpectation { - rec, err := regexp.Compile(re) - if err != nil { - panic(err) - } - check := func(params []*protocol.UnregistrationParams) Verdict { - for _, p := range params { +func UnregistrationMatching(re string) SimpleExpectation { + rec := regexp.MustCompile(re) + check := func(s State) Verdict { + for _, p := range s.unregistrations { for _, r := range p.Unregisterations { if rec.Match([]byte(r.Method)) { return Met @@ -468,7 +482,7 @@ func UnregistrationMatching(re string) UnregistrationExpectation { } return Unmet } - return UnregistrationExpectation{ + return SimpleExpectation{ check: check, description: fmt.Sprintf("unregistration matching %q", re), } From 4ff08b404476193815dcf79395282a60d18d4781 Mon Sep 17 00:00:00 2001 From: Antoine Cotten Date: Sat, 6 Aug 2022 15:08:18 +0200 Subject: [PATCH 176/723] gopls: Improve auto-imports example for NeoVim LSP Follow Lua Style Guide for function names. Query the client's offset encoding, with a fallback to utf-16. Omit handling of r.command since the source.organizeImports code action doesn't return any. Omit passing a value for wait_ms. It is optional and defaults to 1000 (see NeoVim help for vim.lsp.buf_request_sync). Change-Id: I5e5eb6cee3a46fee1edc1e6d15b40ad88498a26c Reviewed-on: https://go-review.googlesource.com/c/tools/+/421295 Reviewed-by: Dylan Le Reviewed-by: Robert Findley Reviewed-by: Jamal Carvalho --- gopls/doc/vim.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gopls/doc/vim.md b/gopls/doc/vim.md index d9b33ac34dc..af54a7e088e 100644 --- a/gopls/doc/vim.md +++ b/gopls/doc/vim.md @@ -175,23 +175,22 @@ a helper function in Lua: lua < Date: Mon, 8 Aug 2022 16:36:42 -0400 Subject: [PATCH 177/723] gopls/internal/regtest: fix TestFailingDiagnosticClearingOnEdit This test was asserting immediately on the non-presence of published diagnostics, and therefore was racing with the diagnostics calculation pass. Following https://go.dev/cl/420539 this became a flake, because once gopls has calculated diagnostics for the open event, it will actually publish empty diagnostics for the opened file. Update the test to use a OnceMet so that we only evaluate the expectation once we've finished the diagnostics pass. As a result, the test deterministically failed, which was fixed by using the EmptyOrNoDiagnostics expectation. Fixes golang/go#54271 Change-Id: Id3f220ce44c7996132699a724b6c627f034e367f Reviewed-on: https://go-review.googlesource.com/c/tools/+/422136 gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Peter Weinberger TryBot-Result: Gopher Robot --- gopls/internal/regtest/misc/failures_test.go | 19 ++++++++++++++----- internal/lsp/regtest/env.go | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index 86c9b223c94..185a68cfc8b 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -39,9 +39,12 @@ func main() { }) } -// badPackageDup contains a duplicate definition of the 'a' const. -// this is from diagnostics_test.go, -const badPackageDup = ` +// This test demonstrates a case where gopls is confused by line directives, +// and fails to surface type checking errors. +func TestFailingDiagnosticClearingOnEdit(t *testing.T) { + // badPackageDup contains a duplicate definition of the 'a' const. + // this is from diagnostics_test.go, + const badPackageDup = ` -- go.mod -- module mod.com @@ -56,11 +59,17 @@ package consts const a = 2 ` -func TestFailingDiagnosticClearingOnEdit(t *testing.T) { Run(t, badPackageDup, func(t *testing.T, env *Env) { env.OpenFile("b.go") + // no diagnostics for any files, but there should be - env.Await(NoDiagnostics("a.go"), NoDiagnostics("b.go")) + env.Await( + OnceMet( + env.DoneWithOpen(), + EmptyOrNoDiagnostics("a.go"), + EmptyOrNoDiagnostics("b.go"), + ), + ) // Fix the error by editing the const name in b.go to `b`. env.RegexpReplace("b.go", "(a) = 2", "b") diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go index f8a68b34190..77744ae614a 100644 --- a/internal/lsp/regtest/env.go +++ b/internal/lsp/regtest/env.go @@ -282,6 +282,7 @@ func (a *Awaiter) DiagnosticsFor(name string) *protocol.PublishDiagnosticsParams } func (e *Env) Await(expectations ...Expectation) { + e.T.Helper() if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil { e.T.Fatal(err) } From 6fa767d87cd94c5237da8321ab5586b653185bd5 Mon Sep 17 00:00:00 2001 From: Dylan Le Date: Tue, 9 Aug 2022 12:16:47 -0400 Subject: [PATCH 178/723] internal/lsp: update documentation for directoryFilters setting and default value Add `**` usage to directoryFilters documentation. Change directoryFilters default value to `-**/node_modules` For golang/go#46438 Change-Id: I3ea14ad8a20893d19df4cf8d584a7c7f9b213aab Reviewed-on: https://go-review.googlesource.com/c/tools/+/422356 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/doc/settings.md | 8 ++++++-- internal/lsp/source/api_json.go | 4 ++-- internal/lsp/source/options.go | 8 ++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 890a0a3444f..f71bbebdce9 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -63,15 +63,19 @@ relative to the workspace folder. They are evaluated in order, and the last filter that applies to a path controls whether it is included. The path prefix can be empty, so an initial `-` excludes everything. +DirectoryFilters also supports the `**` operator to match 0 or more directories. + Examples: -Exclude node_modules: `-node_modules` +Exclude node_modules at current depth: `-node_modules` + +Exclude node_modules at any depth: `-**/node_modules` Include only project_a: `-` (exclude everything), `+project_a` Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules` -Default: `["-node_modules"]`. +Default: `["-**/node_modules"]`. #### **templateExtensions** *[]string* diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 0b3b3d117a7..f6833fe3df8 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -22,8 +22,8 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "directoryFilters", Type: "[]string", - Doc: "directoryFilters can be used to exclude unwanted directories from the\nworkspace. By default, all directories are included. Filters are an\noperator, `+` to include and `-` to exclude, followed by a path prefix\nrelative to the workspace folder. They are evaluated in order, and\nthe last filter that applies to a path controls whether it is included.\nThe path prefix can be empty, so an initial `-` excludes everything.\n\nExamples:\n\nExclude node_modules: `-node_modules`\n\nInclude only project_a: `-` (exclude everything), `+project_a`\n\nInclude only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`\n", - Default: "[\"-node_modules\"]", + Doc: "directoryFilters can be used to exclude unwanted directories from the\nworkspace. By default, all directories are included. Filters are an\noperator, `+` to include and `-` to exclude, followed by a path prefix\nrelative to the workspace folder. They are evaluated in order, and\nthe last filter that applies to a path controls whether it is included.\nThe path prefix can be empty, so an initial `-` excludes everything.\n\nDirectoryFilters also supports the `**` operator to match 0 or more directories.\n\nExamples:\n\nExclude node_modules at current depth: `-node_modules`\n\nExclude node_modules at any depth: `-**/node_modules`\n\nInclude only project_a: `-` (exclude everything), `+project_a`\n\nInclude only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`\n", + Default: "[\"-**/node_modules\"]", Hierarchy: "build", }, { diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 2f40b59ccf7..096f1acf9a4 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -119,7 +119,7 @@ func DefaultOptions() *Options { ExpandWorkspaceToModule: true, ExperimentalPackageCacheKey: true, MemoryMode: ModeNormal, - DirectoryFilters: []string{"-node_modules"}, + DirectoryFilters: []string{"-**/node_modules"}, TemplateExtensions: []string{}, }, UIOptions: UIOptions{ @@ -232,9 +232,13 @@ type BuildOptions struct { // the last filter that applies to a path controls whether it is included. // The path prefix can be empty, so an initial `-` excludes everything. // + // DirectoryFilters also supports the `**` operator to match 0 or more directories. + // // Examples: // - // Exclude node_modules: `-node_modules` + // Exclude node_modules at current depth: `-node_modules` + // + // Exclude node_modules at any depth: `-**/node_modules` // // Include only project_a: `-` (exclude everything), `+project_a` // From 3950865150171b52ce6b7f79f0db6adc2dcc7b60 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 9 Aug 2022 16:37:38 -0400 Subject: [PATCH 179/723] gopls/internal/regtest/bench: add a -gopls_version flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To facilitate testing specific gopls versions, add a -gopls_version flag that installs and uses a specified gopls version. Also fix a bug in the DidChange benchmark that causes it to be inaccurate when the benchmark function is invoked multiple times (by the benchmark runner or via -count): because the server environment is shared across invocations, we can't use the benchmark iteration to derive the expected number of changes: we must use our count of actual DidChange notifications from the server. Change-Id: I1ad733cb3e695d382e81dfb7e31346162d9b7635 Reviewed-on: https://go-review.googlesource.com/c/tools/+/422368 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Nooras Saba‎ --- gopls/internal/regtest/bench/bench_test.go | 104 ++++++++++++++---- .../internal/regtest/bench/didchange_test.go | 4 +- internal/lsp/regtest/expectation.go | 9 +- 3 files changed, 87 insertions(+), 30 deletions(-) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index cfe4db6648e..2bee4205cb5 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -12,6 +12,7 @@ import ( "log" "os" "os/exec" + "path/filepath" "sync" "testing" "time" @@ -25,7 +26,6 @@ import ( "golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/lsp/lsprpc" - "golang.org/x/tools/internal/lsp/regtest" . "golang.org/x/tools/internal/lsp/regtest" ) @@ -75,19 +75,37 @@ var ( file = flag.String("file", "go/ast/astutil/util.go", "active file, for benchmarks that operate on a file") commitish = flag.String("commit", "gopls/v0.9.0", "if set (and -workdir is unset), run benchmarks at this commit") - goplsPath = flag.String("gopls", "", "if set, use this gopls for testing") + goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_version") + goplsVersion = flag.String("gopls_version", "", "if set, install and use gopls at this version for testing; incompatible with -gopls_path") // If non-empty, tempDir is a temporary working dir that was created by this // test suite. - setupDirOnce sync.Once - tempDir string + // + // The sync.Once variables guard various modifications of the temp directory. + makeTempDirOnce sync.Once + checkoutRepoOnce sync.Once + installGoplsOnce sync.Once + tempDir string setupEditorOnce sync.Once sandbox *fake.Sandbox editor *fake.Editor - awaiter *regtest.Awaiter + awaiter *Awaiter ) +// getTempDir returns the temporary directory to use for benchmark files, +// creating it if necessary. +func getTempDir() string { + makeTempDirOnce.Do(func() { + var err error + tempDir, err = ioutil.TempDir("", "gopls-bench") + if err != nil { + log.Fatal(err) + } + }) + return tempDir +} + // benchmarkDir returns the directory to use for benchmarks. // // If -workdir is set, just use that directory. Otherwise, check out a shallow @@ -96,21 +114,19 @@ func benchmarkDir() string { if *workdir != "" { return *workdir } - setupDirOnce.Do(func() { - if *repo == "" { - log.Fatal("-repo must be provided") - } + if *repo == "" { + log.Fatal("-repo must be provided if -workdir is unset") + } + if *commitish == "" { + log.Fatal("-commit must be provided if -workdir is unset") + } - if *commitish == "" { - log.Fatal("-commit must be provided") + dir := filepath.Join(getTempDir(), "repo") + checkoutRepoOnce.Do(func() { + if err := os.Mkdir(dir, 0750); err != nil { + log.Fatalf("creating repo dir: %v", err) } - - var err error - tempDir, err = ioutil.TempDir("", "gopls-bench") - if err != nil { - log.Fatal(err) - } - fmt.Printf("checking out %s@%s to %s\n", *repo, *commitish, tempDir) + log.Printf("checking out %s@%s to %s\n", *repo, *commitish, dir) // Set a timeout for git fetch. If this proves flaky, it can be removed. ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) @@ -119,12 +135,12 @@ func benchmarkDir() string { // Use a shallow fetch to download just the releveant commit. shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", *repo, *commitish) initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit) - initCmd.Dir = tempDir + initCmd.Dir = dir if err := initCmd.Run(); err != nil { log.Fatalf("checking out %s: %v", *repo, err) } }) - return tempDir + return dir } // benchmarkEnv returns a shared benchmark environment @@ -154,7 +170,7 @@ func benchmarkEnv(tb testing.TB) *Env { // connectEditor connects a fake editor session in the given dir, using the // given editor config. -func connectEditor(dir string, config fake.EditorConfig) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) { +func connectEditor(dir string, config fake.EditorConfig) (*fake.Sandbox, *fake.Editor, *Awaiter, error) { s, err := fake.NewSandbox(&fake.SandboxConfig{ Workdir: dir, GOPROXY: "https://proxy.golang.org", @@ -163,7 +179,7 @@ func connectEditor(dir string, config fake.EditorConfig) (*fake.Sandbox, *fake.E return nil, nil, nil, err } - a := regtest.NewAwaiter(s.Workdir) + a := NewAwaiter(s.Workdir) ts := getServer() e, err := fake.NewEditor(s, config).Connect(context.Background(), ts, a.Hooks()) if err != nil { @@ -175,13 +191,55 @@ func connectEditor(dir string, config fake.EditorConfig) (*fake.Sandbox, *fake.E // getServer returns a server connector that either starts a new in-process // server, or starts a separate gopls process. func getServer() servertest.Connector { + if *goplsPath != "" && *goplsVersion != "" { + panic("can't set both -gopls_path and -gopls_version") + } if *goplsPath != "" { return &SidecarServer{*goplsPath} } + if *goplsVersion != "" { + path := getInstalledGopls(*goplsVersion) + return &SidecarServer{path} + } server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false) return servertest.NewPipeServer(server, jsonrpc2.NewRawStream) } +func getInstalledGopls(version string) string { + // Use a temp GOPATH to while installing gopls, to avoid overwriting gopls in + // the user's PATH. + gopath := filepath.Join(getTempDir(), "gopath") + goplsPath := filepath.Join(gopath, "bin", "gopls") + installGoplsOnce.Do(func() { + if err := os.Mkdir(gopath, 0755); err != nil { + log.Fatalf("creating temp GOPATH: %v", err) + } + env := append(os.Environ(), "GOPATH="+gopath) + + // Install gopls. + log.Printf("installing gopls@%s\n", version) + cmd := exec.Command("go", "install", fmt.Sprintf("golang.org/x/tools/gopls@%s", version)) + cmd.Env = env + if output, err := cmd.CombinedOutput(); err != nil { + log.Fatalf("installing gopls: %v:\n%s", err, string(output)) + } + + // Clean the modcache, otherwise we'll get errors when trying to remove the + // temp directory. + cleanCmd := exec.Command("go", "clean", "-modcache") + cleanCmd.Env = env + if output, err := cleanCmd.CombinedOutput(); err != nil { + log.Fatalf("cleaning up temp GOPATH: %v\n%s", err, string(output)) + } + + // Confirm that the resulting path now exists. + if _, err := os.Stat(goplsPath); err != nil { + log.Fatalf("os.Stat(goplsPath): %v", err) + } + }) + return goplsPath +} + // A SidecarServer starts (and connects to) a separate gopls process at the // given path. type SidecarServer struct { @@ -190,7 +248,7 @@ type SidecarServer struct { // Connect creates new io.Pipes and binds them to the underlying StreamServer. func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn { - cmd := exec.CommandContext(ctx, *goplsPath, "serve") + cmd := exec.CommandContext(ctx, s.goplsPath, "serve") stdin, err := cmd.StdinPipe() if err != nil { diff --git a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go index 8fcf25362be..00048f854e4 100644 --- a/gopls/internal/regtest/bench/didchange_test.go +++ b/gopls/internal/regtest/bench/didchange_test.go @@ -9,8 +9,6 @@ import ( "testing" "golang.org/x/tools/internal/lsp/fake" - - . "golang.org/x/tools/internal/lsp/regtest" ) // BenchmarkDidChange benchmarks modifications of a single file by making @@ -35,6 +33,6 @@ func BenchmarkDidChange(b *testing.B) { // Increment the placeholder text, to ensure cache misses. Text: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1), }) - env.Await(StartedChange(uint64(i + 1))) + env.Await(env.StartedChange()) } } diff --git a/internal/lsp/regtest/expectation.go b/internal/lsp/regtest/expectation.go index 7867af980b0..eb88a4c8584 100644 --- a/internal/lsp/regtest/expectation.go +++ b/internal/lsp/regtest/expectation.go @@ -223,10 +223,11 @@ func (e *Env) DoneWithOpen() Expectation { return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), opens, true) } -// StartedChange expects there to have been i work items started for -// processing didChange notifications. -func StartedChange(i uint64) Expectation { - return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), i) +// StartedChange expects that the server has at least started processing all +// didChange notifications sent from the client. +func (e *Env) StartedChange() Expectation { + changes := e.Editor.Stats().DidChange + return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes) } // DoneWithChange expects all didChange notifications currently sent by the From 0ad49fdeb95584e7c8ee43817c2832e894f5a524 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 10 Aug 2022 13:31:23 -0400 Subject: [PATCH 180/723] internal/imports: update stdlib index for 1.19 Updates golang/go#38706 Change-Id: I361c8862b4dcd5215ea0574715908f552f937e6c Reviewed-on: https://go-review.googlesource.com/c/tools/+/422654 TryBot-Result: Gopher Robot Reviewed-by: Heschi Kreinick Run-TryBot: Robert Findley gopls-CI: kokoro --- internal/imports/mkstdlib.go | 1 + internal/imports/zstdlib.go | 110 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/internal/imports/mkstdlib.go b/internal/imports/mkstdlib.go index 47714bf0719..b1670227a24 100644 --- a/internal/imports/mkstdlib.go +++ b/internal/imports/mkstdlib.go @@ -71,6 +71,7 @@ func main() { mustOpen(api("go1.16.txt")), mustOpen(api("go1.17.txt")), mustOpen(api("go1.18.txt")), + mustOpen(api("go1.19.txt")), // The API of the syscall/js package needs to be computed explicitly, // because it's not included in the GOROOT/api/go1.*.txt files at this time. diff --git a/internal/imports/zstdlib.go b/internal/imports/zstdlib.go index 437fbb78dbd..116a149ba14 100644 --- a/internal/imports/zstdlib.go +++ b/internal/imports/zstdlib.go @@ -588,6 +588,7 @@ var stdlib = map[string][]string{ "ParsePKCS1PublicKey", "ParsePKCS8PrivateKey", "ParsePKIXPublicKey", + "ParseRevocationList", "PublicKeyAlgorithm", "PureEd25519", "RSA", @@ -1238,6 +1239,7 @@ var stdlib = map[string][]string{ "EM_L10M", "EM_LANAI", "EM_LATTICEMICO32", + "EM_LOONGARCH", "EM_M16C", "EM_M32", "EM_M32C", @@ -1820,6 +1822,57 @@ var stdlib = map[string][]string{ "R_ARM_XPC25", "R_INFO", "R_INFO32", + "R_LARCH", + "R_LARCH_32", + "R_LARCH_64", + "R_LARCH_ADD16", + "R_LARCH_ADD24", + "R_LARCH_ADD32", + "R_LARCH_ADD64", + "R_LARCH_ADD8", + "R_LARCH_COPY", + "R_LARCH_IRELATIVE", + "R_LARCH_JUMP_SLOT", + "R_LARCH_MARK_LA", + "R_LARCH_MARK_PCREL", + "R_LARCH_NONE", + "R_LARCH_RELATIVE", + "R_LARCH_SOP_ADD", + "R_LARCH_SOP_AND", + "R_LARCH_SOP_ASSERT", + "R_LARCH_SOP_IF_ELSE", + "R_LARCH_SOP_NOT", + "R_LARCH_SOP_POP_32_S_0_10_10_16_S2", + "R_LARCH_SOP_POP_32_S_0_5_10_16_S2", + "R_LARCH_SOP_POP_32_S_10_12", + "R_LARCH_SOP_POP_32_S_10_16", + "R_LARCH_SOP_POP_32_S_10_16_S2", + "R_LARCH_SOP_POP_32_S_10_5", + "R_LARCH_SOP_POP_32_S_5_20", + "R_LARCH_SOP_POP_32_U", + "R_LARCH_SOP_POP_32_U_10_12", + "R_LARCH_SOP_PUSH_ABSOLUTE", + "R_LARCH_SOP_PUSH_DUP", + "R_LARCH_SOP_PUSH_GPREL", + "R_LARCH_SOP_PUSH_PCREL", + "R_LARCH_SOP_PUSH_PLT_PCREL", + "R_LARCH_SOP_PUSH_TLS_GD", + "R_LARCH_SOP_PUSH_TLS_GOT", + "R_LARCH_SOP_PUSH_TLS_TPREL", + "R_LARCH_SOP_SL", + "R_LARCH_SOP_SR", + "R_LARCH_SOP_SUB", + "R_LARCH_SUB16", + "R_LARCH_SUB24", + "R_LARCH_SUB32", + "R_LARCH_SUB64", + "R_LARCH_SUB8", + "R_LARCH_TLS_DTPMOD32", + "R_LARCH_TLS_DTPMOD64", + "R_LARCH_TLS_DTPREL32", + "R_LARCH_TLS_DTPREL64", + "R_LARCH_TLS_TPREL32", + "R_LARCH_TLS_TPREL64", "R_MIPS", "R_MIPS_16", "R_MIPS_26", @@ -2459,11 +2512,18 @@ var stdlib = map[string][]string{ }, "debug/pe": []string{ "COFFSymbol", + "COFFSymbolAuxFormat5", "COFFSymbolSize", "DataDirectory", "File", "FileHeader", "FormatError", + "IMAGE_COMDAT_SELECT_ANY", + "IMAGE_COMDAT_SELECT_ASSOCIATIVE", + "IMAGE_COMDAT_SELECT_EXACT_MATCH", + "IMAGE_COMDAT_SELECT_LARGEST", + "IMAGE_COMDAT_SELECT_NODUPLICATES", + "IMAGE_COMDAT_SELECT_SAME_SIZE", "IMAGE_DIRECTORY_ENTRY_ARCHITECTURE", "IMAGE_DIRECTORY_ENTRY_BASERELOC", "IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT", @@ -2508,6 +2568,8 @@ var stdlib = map[string][]string{ "IMAGE_FILE_MACHINE_EBC", "IMAGE_FILE_MACHINE_I386", "IMAGE_FILE_MACHINE_IA64", + "IMAGE_FILE_MACHINE_LOONGARCH32", + "IMAGE_FILE_MACHINE_LOONGARCH64", "IMAGE_FILE_MACHINE_M32R", "IMAGE_FILE_MACHINE_MIPS16", "IMAGE_FILE_MACHINE_MIPSFPU", @@ -2527,6 +2589,14 @@ var stdlib = map[string][]string{ "IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP", "IMAGE_FILE_SYSTEM", "IMAGE_FILE_UP_SYSTEM_ONLY", + "IMAGE_SCN_CNT_CODE", + "IMAGE_SCN_CNT_INITIALIZED_DATA", + "IMAGE_SCN_CNT_UNINITIALIZED_DATA", + "IMAGE_SCN_LNK_COMDAT", + "IMAGE_SCN_MEM_DISCARDABLE", + "IMAGE_SCN_MEM_EXECUTE", + "IMAGE_SCN_MEM_READ", + "IMAGE_SCN_MEM_WRITE", "IMAGE_SUBSYSTEM_EFI_APPLICATION", "IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER", "IMAGE_SUBSYSTEM_EFI_ROM", @@ -2647,6 +2717,9 @@ var stdlib = map[string][]string{ "URLEncoding", }, "encoding/binary": []string{ + "AppendByteOrder", + "AppendUvarint", + "AppendVarint", "BigEndian", "ByteOrder", "LittleEndian", @@ -2822,6 +2895,7 @@ var stdlib = map[string][]string{ "Set", "String", "StringVar", + "TextVar", "Uint", "Uint64", "Uint64Var", @@ -2834,6 +2908,9 @@ var stdlib = map[string][]string{ "VisitAll", }, "fmt": []string{ + "Append", + "Appendf", + "Appendln", "Errorf", "Formatter", "Fprint", @@ -3054,6 +3131,24 @@ var stdlib = map[string][]string{ "Type", "Value", }, + "go/doc/comment": []string{ + "Block", + "Code", + "DefaultLookupPackage", + "Doc", + "DocLink", + "Heading", + "Italic", + "Link", + "LinkDef", + "List", + "ListItem", + "Paragraph", + "Parser", + "Plain", + "Printer", + "Text", + }, "go/format": []string{ "Node", "Source", @@ -3390,9 +3485,11 @@ var stdlib = map[string][]string{ "New64a", }, "hash/maphash": []string{ + "Bytes", "Hash", "MakeSeed", "Seed", + "String", }, "html": []string{ "EscapeString", @@ -4168,6 +4265,7 @@ var stdlib = map[string][]string{ "ListenAndServe", "ListenAndServeTLS", "LocalAddrContextKey", + "MaxBytesError", "MaxBytesHandler", "MaxBytesReader", "MethodConnect", @@ -4448,6 +4546,7 @@ var stdlib = map[string][]string{ "Error", "EscapeError", "InvalidHostError", + "JoinPath", "Parse", "ParseQuery", "ParseRequestURI", @@ -4581,6 +4680,7 @@ var stdlib = map[string][]string{ "Cmd", "Command", "CommandContext", + "ErrDot", "ErrNotFound", "Error", "ExitError", @@ -4759,6 +4859,7 @@ var stdlib = map[string][]string{ "ErrMissingBracket", "ErrMissingParen", "ErrMissingRepeatArgument", + "ErrNestingDepth", "ErrTrailingBackslash", "ErrUnexpectedParen", "Error", @@ -4878,6 +4979,7 @@ var stdlib = map[string][]string{ "SetGCPercent", "SetMaxStack", "SetMaxThreads", + "SetMemoryLimit", "SetPanicOnFault", "SetTraceback", "Stack", @@ -4925,6 +5027,7 @@ var stdlib = map[string][]string{ "WithRegion", }, "sort": []string{ + "Find", "Float64Slice", "Float64s", "Float64sAreSorted", @@ -5058,18 +5161,22 @@ var stdlib = map[string][]string{ "AddUint32", "AddUint64", "AddUintptr", + "Bool", "CompareAndSwapInt32", "CompareAndSwapInt64", "CompareAndSwapPointer", "CompareAndSwapUint32", "CompareAndSwapUint64", "CompareAndSwapUintptr", + "Int32", + "Int64", "LoadInt32", "LoadInt64", "LoadPointer", "LoadUint32", "LoadUint64", "LoadUintptr", + "Pointer", "StoreInt32", "StoreInt64", "StorePointer", @@ -5082,6 +5189,9 @@ var stdlib = map[string][]string{ "SwapUint32", "SwapUint64", "SwapUintptr", + "Uint32", + "Uint64", + "Uintptr", "Value", }, "syscall": []string{ From b2156b5c9a229153568044fa1c175bb61e912fb7 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 10 Aug 2022 15:21:33 -0400 Subject: [PATCH 181/723] gopls: update dependencies Update all dependencies, except sergi/go-diff. Fixes golang/go#54294 Change-Id: I4f60a7563bf214a76594fef3f566c714fb59133c Reviewed-on: https://go-review.googlesource.com/c/tools/+/422655 Reviewed-by: Peter Weinberger Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/go.mod | 8 ++++---- gopls/go.sum | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/gopls/go.mod b/gopls/go.mod index 006bfe23224..49f5804442a 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -8,10 +8,10 @@ require ( github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f - golang.org/x/tools v0.1.11 - golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df - honnef.co/go/tools v0.3.2 + golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 + golang.org/x/tools v0.1.13-0.20220810174125-0ad49fdeb955 + golang.org/x/vuln v0.0.0-20220809164104-12ff722659c1 + honnef.co/go/tools v0.3.3 mvdan.cc/gofumpt v0.3.1 mvdan.cc/xurls/v2 v2.4.0 ) diff --git a/gopls/go.sum b/gopls/go.sum index ecd3f4dd363..2b9d61584da 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -60,15 +60,16 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs= +golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df h1:BkeW9/QJhcigekDUPS9N9bIb0v7gPKKmLYeczVAqr2s= -golang.org/x/vuln v0.0.0-20220725105440-4151a5aca1df/go.mod h1:UZshlUPxXeGUM9I14UOawXQg6yosDE9cr1vKY/DzgWo= +golang.org/x/vuln v0.0.0-20220809164104-12ff722659c1 h1:wxIK8Hnmd3ervTxk4aON+gAbfWbb2hToeKSTQd0eXgo= +golang.org/x/vuln v0.0.0-20220809164104-12ff722659c1/go.mod h1:t0tyWMAuNGUOL2N4il/aj/M5LLt4ktPpOGuTBNUqmiM= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -80,8 +81,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= -honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= -honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= +honnef.co/go/tools v0.3.3 h1:oDx7VAwstgpYpb3wv0oxiZlxY+foCpRAwY7Vk6XpAgA= +honnef.co/go/tools v0.3.3/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= mvdan.cc/gofumpt v0.3.1 h1:avhhrOmv0IuvQVK7fvwV91oFSGAk5/6Po8GXTzICeu8= mvdan.cc/gofumpt v0.3.1/go.mod h1:w3ymliuxvzVx8DAutBnVyDqYb1Niy/yCJt/lk821YCE= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio= From 38074195235bdd653de752af11c4a3ca1b77aec3 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 11 Aug 2022 09:33:31 -0400 Subject: [PATCH 182/723] internal/lsp/cache: validate the range for critical errors in go files This avoids the panic reported in golang/go#54395. Fixes golang/go#54395 Change-Id: Ief35985a503d3cc13971499dc6f4e9c1d1d63ea3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/422894 TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller gopls-CI: kokoro Run-TryBot: Robert Findley --- gopls/internal/regtest/workspace/broken_test.go | 3 +++ internal/lsp/cache/load.go | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index fbc41de1615..b06c15580c0 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -186,6 +186,8 @@ module a.com go 1.12 -- a/a.go -- package a +-- a/empty.go -- +// an empty file -- b/go.mod -- module b.com @@ -202,6 +204,7 @@ package b ver := env.GoVersion() msg := msgForVersion(ver) env.OpenFile("a/a.go") + env.OpenFile("a/empty.go") env.OpenFile("b/go.mod") env.Await( env.DiagnosticAtRegexp("a/a.go", "package a"), diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 7f30939e1c8..b15c5374e93 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -380,9 +380,12 @@ func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, fi switch s.view.FileKind(fh) { case source.Go: if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil { - pkgDecl := span.NewRange(pgf.Tok, pgf.File.Package, pgf.File.Name.End()) - if spn, err := pkgDecl.Span(); err == nil { - rng, _ = pgf.Mapper.Range(spn) + // Check that we have a valid `package foo` range to use for positioning the error. + if pgf.File.Package.IsValid() && pgf.File.Name != nil && pgf.File.Name.End().IsValid() { + pkgDecl := span.NewRange(pgf.Tok, pgf.File.Package, pgf.File.Name.End()) + if spn, err := pkgDecl.Span(); err == nil { + rng, _ = pgf.Mapper.Range(spn) + } } } case source.Mod: From 37a81b68a061ab2fad895d022a5a665d87c44e40 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Thu, 30 Jun 2022 17:09:03 -0400 Subject: [PATCH 183/723] internal/lsp: add unnecessary tags for unused vars and imports Diagnostic Tags add another hint for the client for how to display diagnostics. The Unnecessary tags allows clients to fade out the diagnostic instead of adding a squiggle. This adds this tag to unused import errors as well as unused vars. For golang/vscode-go#2285 Change-Id: I0c93b28a6a2ef4eed314dcf30a37c27dd65940ac Reviewed-on: https://go-review.googlesource.com/c/tools/+/415499 Reviewed-by: Robert Findley --- internal/lsp/cache/errors.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/lsp/cache/errors.go b/internal/lsp/cache/errors.go index a1aecb35c7b..80ae5451c8d 100644 --- a/internal/lsp/cache/errors.go +++ b/internal/lsp/cache/errors.go @@ -123,6 +123,10 @@ func typeErrorDiagnostics(snapshot *snapshot, pkg *pkg, e extendedError) ([]*sou diag.Code = code.String() diag.CodeHref = typesCodeHref(snapshot, code) } + switch code { + case typesinternal.UnusedVar, typesinternal.UnusedImport: + diag.Tags = append(diag.Tags, protocol.Unnecessary) + } for _, secondary := range e.secondaries { _, secondarySpan, err := typeErrorData(snapshot.FileSet(), pkg, secondary) From c4ec74a5c9f258918d2266ea281f3e344c6ea35d Mon Sep 17 00:00:00 2001 From: David Chase Date: Thu, 11 Aug 2022 13:48:54 -0400 Subject: [PATCH 184/723] go/internal/pkgbits: fix performance of rawReloc Tracking changes in go repo for unified IR. See CL 422297. Change-Id: If05aba7693c70cac4969721e5dd8a5bf197670b4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/422899 Reviewed-by: Matthew Dempsky Run-TryBot: David Chase gopls-CI: kokoro TryBot-Result: Gopher Robot --- go/internal/pkgbits/encoder.go | 18 +++++++++++------- go/internal/pkgbits/reloc.go | 4 ++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/go/internal/pkgbits/encoder.go b/go/internal/pkgbits/encoder.go index c50c838caae..e98e41171e1 100644 --- a/go/internal/pkgbits/encoder.go +++ b/go/internal/pkgbits/encoder.go @@ -147,8 +147,9 @@ func (pw *PkgEncoder) NewEncoderRaw(k RelocKind) Encoder { type Encoder struct { p *PkgEncoder - Relocs []RelocEnt - Data bytes.Buffer // accumulated element bitstream data + Relocs []RelocEnt + RelocMap map[RelocEnt]uint32 + Data bytes.Buffer // accumulated element bitstream data encodingRelocHeader bool @@ -210,15 +211,18 @@ func (w *Encoder) rawVarint(x int64) { } func (w *Encoder) rawReloc(r RelocKind, idx Index) int { - // TODO(mdempsky): Use map for lookup; this takes quadratic time. - for i, rEnt := range w.Relocs { - if rEnt.Kind == r && rEnt.Idx == idx { - return i + e := RelocEnt{r, idx} + if w.RelocMap != nil { + if i, ok := w.RelocMap[e]; ok { + return int(i) } + } else { + w.RelocMap = make(map[RelocEnt]uint32) } i := len(w.Relocs) - w.Relocs = append(w.Relocs, RelocEnt{r, idx}) + w.RelocMap[e] = uint32(i) + w.Relocs = append(w.Relocs, e) return i } diff --git a/go/internal/pkgbits/reloc.go b/go/internal/pkgbits/reloc.go index 7a8f04ab3fc..fcdfb97ca99 100644 --- a/go/internal/pkgbits/reloc.go +++ b/go/internal/pkgbits/reloc.go @@ -5,11 +5,11 @@ package pkgbits // A RelocKind indicates a particular section within a unified IR export. -type RelocKind int +type RelocKind int32 // An Index represents a bitstream element index within a particular // section. -type Index int +type Index int32 // A relocEnt (relocation entry) is an entry in an element's local // reference table. From 88d981ef8f8158b15938bd1bc77e47cea16fe5f0 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Thu, 11 Aug 2022 13:23:58 +0200 Subject: [PATCH 185/723] printf analyzer: link to fmt#Printing for verb/type docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folks are reporting the current error message as not useful, remarking that they don’t know which formatting verb goes with which Go type, see e.g. b/227741360. Change-Id: I367bc8c4df3521f0726dc4defa4e563532706148 Reviewed-on: https://go-review.googlesource.com/c/tools/+/422854 Reviewed-by: Nicolas Hillegeer TryBot-Result: Gopher Robot Reviewed-by: Than McIntosh Run-TryBot: Than McIntosh gopls-CI: kokoro --- go/analysis/passes/printf/printf.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index c4ccc95b4fb..f56b9b7beb7 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -915,7 +915,7 @@ func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, state *formatState) (o if reason != "" { details = " (" + reason + ")" } - pass.ReportRangef(call, "%s format %s has arg %s of wrong type %s%s", state.name, state.format, analysisutil.Format(pass.Fset, arg), typeString, details) + pass.ReportRangef(call, "%s format %s has arg %s of wrong type %s%s, see also https://pkg.go.dev/fmt#hdr-Printing", state.name, state.format, analysisutil.Format(pass.Fset, arg), typeString, details) return false } if v.typ&argString != 0 && v.verb != 'T' && !bytes.Contains(state.flags, []byte{'#'}) { From bebd890374898b8c6c9fb163f02a62bd41eec577 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 12 Aug 2022 13:01:36 -0400 Subject: [PATCH 186/723] go/analysis: remove stray print statement in the timeformat analyzer A debugging print statement was left in the analyzer, which breaks gopls' communication over stdin/stdout. Fix this, and add tests. Fixes golang/vscode-go#2406 Change-Id: I1b785fa09e66eae2f1b1e03806e5b59d2015e75e Reviewed-on: https://go-review.googlesource.com/c/tools/+/422902 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Tim King --- go/analysis/passes/timeformat/timeformat.go | 2 - .../regtest/diagnostics/analysis_test.go | 54 +++++++++++++++++++ internal/lsp/testdata/analyzer/bad_test.go | 6 +++ internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 gopls/internal/regtest/diagnostics/analysis_test.go diff --git a/go/analysis/passes/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go index 9147826b4a1..acb198f95c4 100644 --- a/go/analysis/passes/timeformat/timeformat.go +++ b/go/analysis/passes/timeformat/timeformat.go @@ -7,7 +7,6 @@ package timeformat import ( - "fmt" "go/ast" "go/constant" "go/token" @@ -59,7 +58,6 @@ func run(pass *analysis.Pass) (interface{}, error) { if badAt > -1 { // Check if it's a literal string, otherwise we can't suggest a fix. if _, ok := arg.(*ast.BasicLit); ok { - fmt.Printf("%#v\n", arg) pos := int(arg.Pos()) + badAt + 1 // +1 to skip the " or ` end := pos + len(badFormat) diff --git a/gopls/internal/regtest/diagnostics/analysis_test.go b/gopls/internal/regtest/diagnostics/analysis_test.go new file mode 100644 index 00000000000..fbebf602dc9 --- /dev/null +++ b/gopls/internal/regtest/diagnostics/analysis_test.go @@ -0,0 +1,54 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diagnostics + +import ( + "testing" + + "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/internal/lsp/regtest" +) + +// Test for the timeformat analyzer, following golang/vscode-go#2406. +// +// This test checks that applying the suggested fix from the analyzer resolves +// the diagnostic warning. +func TestTimeFormatAnalyzer(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- main.go -- +package main + +import ( + "fmt" + "time" +) + +func main() { + now := time.Now() + fmt.Println(now.Format("2006-02-01")) +}` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + + var d protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("main.go", "2006-02-01"), + ReadDiagnostics("main.go", &d), + ), + ) + + env.ApplyQuickFixes("main.go", d.Diagnostics) + env.Await( + EmptyDiagnostics("main.go"), + ) + }) +} diff --git a/internal/lsp/testdata/analyzer/bad_test.go b/internal/lsp/testdata/analyzer/bad_test.go index c819cbc0111..3d89101fc02 100644 --- a/internal/lsp/testdata/analyzer/bad_test.go +++ b/internal/lsp/testdata/analyzer/bad_test.go @@ -4,6 +4,7 @@ import ( "fmt" "sync" "testing" + "time" ) func Testbad(t *testing.T) { //@diag("", "tests", "Testbad has malformed name: first letter after 'Test' must not be lowercase", "warning") @@ -16,3 +17,8 @@ func Testbad(t *testing.T) { //@diag("", "tests", "Testbad has malformed name: f func printfWrapper(format string, args ...interface{}) { fmt.Printf(format, args...) } + +func _() { + now := time.Now() + fmt.Println(now.Format("2006-02-01")) //@diag("2006-02-01", "timeformat", "2006-02-01 should be 2006-01-02", "warning") +} diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index b6c6c07b15d..9324833570f 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -8,7 +8,7 @@ DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 164 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 37 +DiagnosticsCount = 38 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 668d5fb82e5..9ec3c79d3c3 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -8,7 +8,7 @@ DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 174 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 37 +DiagnosticsCount = 38 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 From 35f806b175ca991e33e7ae31a3c0e8ede44b8309 Mon Sep 17 00:00:00 2001 From: Leonardo Henrique <50160686+henrikkudesu@users.noreply.github.com> Date: Fri, 12 Aug 2022 22:44:49 +0000 Subject: [PATCH 187/723] gopls/doc/workspace: correct grammar Fixed double "the the". Change-Id: Id431cdb2eaf1452453047818393324a30e21f971 GitHub-Last-Rev: 20c8438eeb504ac6b92e49e73a2acd2a97fe930e GitHub-Pull-Request: golang/tools#380 Reviewed-on: https://go-review.googlesource.com/c/tools/+/402236 Reviewed-by: Dmitri Shuralyov Run-TryBot: Dmitri Shuralyov gopls-CI: kokoro Reviewed-by: Peter Weinberger Auto-Submit: Dmitri Shuralyov Reviewed-by: Dmitri Shuralyov TryBot-Result: Gopher Robot --- gopls/doc/workspace.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/doc/workspace.md b/gopls/doc/workspace.md index 610afbe6184..734eaddbab9 100644 --- a/gopls/doc/workspace.md +++ b/gopls/doc/workspace.md @@ -47,7 +47,7 @@ go work use tools tools/gopls #### Experimental workspace module (Go 1.17 and earlier) With earlier versions of Go, `gopls` can simulate multi-module workspaces by -creating a synthetic module requiring the the modules in the workspace root. +creating a synthetic module requiring the modules in the workspace root. See [the design document](https://github.com/golang/proposal/blob/master/design/37720-gopls-workspaces.md) for more information. From 5a26068387a160307e60111e4f6c176f2fb24cc8 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 12 Aug 2022 12:33:49 -0400 Subject: [PATCH 188/723] internal/lsp/source: remove utm_source from pkgsite links This query parameter is not needed. Change-Id: Id45d7be0b1cbe5d383bcc6768ef20df26de3e7b3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/422901 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley Auto-Submit: Robert Findley --- gopls/internal/regtest/misc/link_test.go | 4 +- internal/lsp/source/hover.go | 5 +- .../lsp/testdata/cgo/declarecgo.go.golden | 6 +- .../lsp/testdata/cgoimport/usecgo.go.golden | 6 +- internal/lsp/testdata/godef/a/a.go.golden | 22 ++--- internal/lsp/testdata/godef/a/d.go.golden | 30 +++---- .../lsp/testdata/godef/a/random.go.golden | 6 +- internal/lsp/testdata/godef/b/b.go.golden | 80 +++++++++---------- internal/lsp/testdata/godef/b/c.go.golden | 12 +-- internal/lsp/testdata/godef/b/e.go.golden | 24 +++--- internal/lsp/testdata/godef/b/h.go.golden | 2 +- .../godef/hover_generics/hover.go.golden | 2 +- internal/lsp/testdata/links/links.go | 6 +- 13 files changed, 101 insertions(+), 104 deletions(-) diff --git a/gopls/internal/regtest/misc/link_test.go b/gopls/internal/regtest/misc/link_test.go index 1005de9a24f..3dc195a2d8f 100644 --- a/gopls/internal/regtest/misc/link_test.go +++ b/gopls/internal/regtest/misc/link_test.go @@ -53,8 +53,8 @@ const Hello = "Hello" env.OpenFile("main.go") env.OpenFile("go.mod") - modLink := "https://pkg.go.dev/mod/import.test@v1.2.3?utm_source=gopls" - pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg?utm_source=gopls" + modLink := "https://pkg.go.dev/mod/import.test@v1.2.3" + pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg" // First, check that we get the expected links via hover and documentLink. content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello")) diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go index b2524c499e4..d2f80c793be 100644 --- a/internal/lsp/source/hover.go +++ b/internal/lsp/source/hover.go @@ -841,9 +841,6 @@ func formatLink(h *HoverJSON, options *Options) string { // BuildLink constructs a link with the given target, path, and anchor. func BuildLink(target, path, anchor string) string { link := fmt.Sprintf("https://%s/%s", target, path) - if target == "pkg.go.dev" { - link += "?utm_source=gopls" - } if anchor == "" { return link } @@ -879,7 +876,7 @@ func anyNonEmpty(x []string) bool { // or Ellipsis.Elt, the field is returned, along with the innermost // enclosing Decl, which could be only loosely related---consider: // -// var decl = f( func(field int) {} ) +// var decl = f( func(field int) {} ) // // It returns (nil, nil) if no Field or Decl is found at pos. func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *ast.Field) { diff --git a/internal/lsp/testdata/cgo/declarecgo.go.golden b/internal/lsp/testdata/cgo/declarecgo.go.golden index b6d94d0c6c6..a032761ffd0 100644 --- a/internal/lsp/testdata/cgo/declarecgo.go.golden +++ b/internal/lsp/testdata/cgo/declarecgo.go.golden @@ -3,7 +3,7 @@ cgo/declarecgo.go:18:6-13: defined here as ```go func Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example) -- funccgoexample-definition-json -- { "span": { @@ -19,7 +19,7 @@ func Example() "offset": 158 } }, - "description": "```go\nfunc Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example)" + "description": "```go\nfunc Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)" } -- funccgoexample-hoverdef -- @@ -27,4 +27,4 @@ func Example() func Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example) diff --git a/internal/lsp/testdata/cgoimport/usecgo.go.golden b/internal/lsp/testdata/cgoimport/usecgo.go.golden index f33f94f84a6..6f638360a92 100644 --- a/internal/lsp/testdata/cgoimport/usecgo.go.golden +++ b/internal/lsp/testdata/cgoimport/usecgo.go.golden @@ -3,7 +3,7 @@ cgo/declarecgo.go:18:6-13: defined here as ```go func cgo.Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example) -- funccgoexample-definition-json -- { "span": { @@ -19,7 +19,7 @@ func cgo.Example() "offset": 158 } }, - "description": "```go\nfunc cgo.Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example)" + "description": "```go\nfunc cgo.Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)" } -- funccgoexample-hoverdef -- @@ -27,4 +27,4 @@ func cgo.Example() func cgo.Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example) diff --git a/internal/lsp/testdata/godef/a/a.go.golden b/internal/lsp/testdata/godef/a/a.go.golden index 9f67a147d14..819262672a0 100644 --- a/internal/lsp/testdata/godef/a/a.go.golden +++ b/internal/lsp/testdata/godef/a/a.go.golden @@ -5,7 +5,7 @@ func (*sync.Mutex).Lock() Lock locks m\. -[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync?utm_source=gopls#Mutex.Lock) +[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock) -- Name-hoverdef -- ```go func (*types.object).Name() string @@ -13,13 +13,13 @@ func (*types.object).Name() string Name returns the object\'s \(package\-local, unqualified\) name\. -[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types?utm_source=gopls#TypeName.Name) +[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name) -- Random-definition -- godef/a/random.go:3:6-12: defined here as ```go func Random() int ``` -[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random) +[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random) -- Random-definition-json -- { "span": { @@ -35,7 +35,7 @@ func Random() int "offset": 22 } }, - "description": "```go\nfunc Random() int\n```\n\n[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random)" + "description": "```go\nfunc Random() int\n```\n\n[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random)" } -- Random-hoverdef -- @@ -43,13 +43,13 @@ func Random() int func Random() int ``` -[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random) +[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random) -- Random2-definition -- godef/a/random.go:8:6-13: defined here as ```go func Random2(y int) int ``` -[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random2) +[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2) -- Random2-definition-json -- { "span": { @@ -65,7 +65,7 @@ func Random2(y int) int "offset": 78 } }, - "description": "```go\nfunc Random2(y int) int\n```\n\n[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random2)" + "description": "```go\nfunc Random2(y int) int\n```\n\n[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2)" } -- Random2-hoverdef -- @@ -73,7 +73,7 @@ func Random2(y int) int func Random2(y int) int ``` -[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random2) +[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2) -- aPackage-hoverdef -- Package a is a package for testing go to definition\. -- declBlockA-hoverdef -- @@ -161,7 +161,7 @@ func make(t Type, size ...int) Type The make built\-in function allocates and initializes an object of type slice, map, or chan \(only\)\. -[`make` on pkg.go.dev](https://pkg.go.dev/builtin?utm_source=gopls#make) +[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make) -- string-hoverdef -- ```go type string string @@ -169,13 +169,13 @@ type string string string is the set of all strings of 8\-bit bytes, conventionally but not necessarily representing UTF\-8\-encoded text\. -[`string` on pkg.go.dev](https://pkg.go.dev/builtin?utm_source=gopls#string) +[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string) -- typesImport-hoverdef -- ```go package types ("go/types") ``` -[`types` on pkg.go.dev](https://pkg.go.dev/go/types?utm_source=gopls) +[`types` on pkg.go.dev](https://pkg.go.dev/go/types) -- x-hoverdef -- ```go var x string diff --git a/internal/lsp/testdata/godef/a/d.go.golden b/internal/lsp/testdata/godef/a/d.go.golden index 47723b0453c..7577b556b8d 100644 --- a/internal/lsp/testdata/godef/a/d.go.golden +++ b/internal/lsp/testdata/godef/a/d.go.golden @@ -5,7 +5,7 @@ field Member string \@Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) -- Member-definition-json -- { "span": { @@ -21,7 +21,7 @@ field Member string "offset": 96 } }, - "description": "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member)" + "description": "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)" } -- Member-hoverdef -- @@ -31,13 +31,13 @@ field Member string \@Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) -- Method-definition -- godef/a/d.go:15:16-22: defined here as ```go func (Thing).Method(i int) string ``` -[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Method) +[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method) -- Method-definition-json -- { "span": { @@ -53,7 +53,7 @@ func (Thing).Method(i int) string "offset": 225 } }, - "description": "```go\nfunc (Thing).Method(i int) string\n```\n\n[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Method)" + "description": "```go\nfunc (Thing).Method(i int) string\n```\n\n[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method)" } -- Method-hoverdef -- @@ -61,7 +61,7 @@ func (Thing).Method(i int) string func (Thing).Method(i int) string ``` -[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Method) +[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method) -- Other-definition -- godef/a/d.go:9:5-10: defined here as ```go var Other Thing @@ -69,7 +69,7 @@ var Other Thing \@Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) -- Other-definition-json -- { "span": { @@ -85,7 +85,7 @@ var Other Thing "offset": 126 } }, - "description": "```go\nvar Other Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other)" + "description": "```go\nvar Other Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)" } -- Other-hoverdef -- @@ -95,7 +95,7 @@ var Other Thing \@Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) -- Thing-definition -- godef/a/d.go:5:6-11: defined here as ```go type Thing struct { @@ -103,7 +103,7 @@ type Thing struct { } ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing) -- Thing-definition-json -- { "span": { @@ -119,7 +119,7 @@ type Thing struct { "offset": 70 } }, - "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing)" + "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)" } -- Thing-hoverdef -- @@ -129,13 +129,13 @@ type Thing struct { } ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing) -- Things-definition -- godef/a/d.go:11:6-12: defined here as ```go func Things(val []string) []Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things) -- Things-definition-json -- { "span": { @@ -151,7 +151,7 @@ func Things(val []string) []Thing "offset": 154 } }, - "description": "```go\nfunc Things(val []string) []Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things)" + "description": "```go\nfunc Things(val []string) []Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)" } -- Things-hoverdef -- @@ -159,6 +159,6 @@ func Things(val []string) []Thing func Things(val []string) []Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things) -- a-hoverdef -- Package a is a package for testing go to definition\. diff --git a/internal/lsp/testdata/godef/a/random.go.golden b/internal/lsp/testdata/godef/a/random.go.golden index 381a11acee8..e8b180a4b43 100644 --- a/internal/lsp/testdata/godef/a/random.go.golden +++ b/internal/lsp/testdata/godef/a/random.go.golden @@ -3,7 +3,7 @@ godef/a/random.go:24:15-18: defined here as ```go func (*Pos).Sum() int ``` -[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Pos.Sum) +[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum) -- PosSum-definition-json -- { "span": { @@ -19,7 +19,7 @@ func (*Pos).Sum() int "offset": 416 } }, - "description": "```go\nfunc (*Pos).Sum() int\n```\n\n[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Pos.Sum)" + "description": "```go\nfunc (*Pos).Sum() int\n```\n\n[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum)" } -- PosSum-hoverdef -- @@ -27,7 +27,7 @@ func (*Pos).Sum() int func (*Pos).Sum() int ``` -[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Pos.Sum) +[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum) -- PosX-definition -- godef/a/random.go:13:2-3: defined here as ```go field x int diff --git a/internal/lsp/testdata/godef/b/b.go.golden b/internal/lsp/testdata/godef/b/b.go.golden index 5f7669b77ca..a30b3f70b55 100644 --- a/internal/lsp/testdata/godef/b/b.go.golden +++ b/internal/lsp/testdata/godef/b/b.go.golden @@ -5,7 +5,7 @@ func (a.I).B() \@mark\(AB, \"B\"\) -[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#I.B) +[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#I.B) -- AField-hoverdef -- ```go field Field int @@ -13,7 +13,7 @@ field Field int \@mark\(AField, \"Field\"\) -[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#S.Field) +[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#S.Field) -- AField2-hoverdef -- ```go field Field2 int @@ -21,7 +21,7 @@ field Field2 int \@mark\(AField2, \"Field2\"\) -[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#R.Field2) +[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#R.Field2) -- AGoodbye-hoverdef -- ```go func (a.H).Goodbye() @@ -29,7 +29,7 @@ func (a.H).Goodbye() \@mark\(AGoodbye, \"Goodbye\"\) -[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#H.Goodbye) +[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#H.Goodbye) -- AHello-hoverdef -- ```go func (a.J).Hello() @@ -37,25 +37,25 @@ func (a.J).Hello() \@mark\(AHello, \"Hello\"\) -[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#J.Hello) +[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#J.Hello) -- AHey-hoverdef -- ```go func (a.R).Hey() ``` -[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#R.Hey) +[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#R.Hey) -- AHi-hoverdef -- ```go func (a.A).Hi() ``` -[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#A.Hi) +[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A.Hi) -- AImport-definition -- godef/b/b.go:5:2-43: defined here as ```go package a ("golang.org/x/tools/internal/lsp/godef/a") ``` -[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls) +[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a) -- AImport-definition-json -- { "span": { @@ -71,7 +71,7 @@ package a ("golang.org/x/tools/internal/lsp/godef/a") "offset": 153 } }, - "description": "```go\npackage a (\"golang.org/x/tools/internal/lsp/godef/a\")\n```\n\n[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls)" + "description": "```go\npackage a (\"golang.org/x/tools/internal/lsp/godef/a\")\n```\n\n[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a)" } -- AImport-hoverdef -- @@ -79,7 +79,7 @@ package a ("golang.org/x/tools/internal/lsp/godef/a") package a ("golang.org/x/tools/internal/lsp/godef/a") ``` -[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls) +[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a) -- AString-definition -- godef/a/a.go:26:6-7: defined here as ```go type A string @@ -87,7 +87,7 @@ type A string \@mark\(AString, \"A\"\) -[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#A) +[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A) -- AString-definition-json -- { "span": { @@ -103,7 +103,7 @@ type A string "offset": 468 } }, - "description": "```go\ntype A string\n```\n\n\\@mark\\(AString, \\\"A\\\"\\)\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#A)" + "description": "```go\ntype A string\n```\n\n\\@mark\\(AString, \\\"A\\\"\\)\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)" } -- AString-hoverdef -- @@ -113,13 +113,13 @@ type A string \@mark\(AString, \"A\"\) -[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#A) +[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A) -- AStuff-definition -- godef/a/a.go:28:6-12: defined here as ```go func a.AStuff() ``` -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff) +[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff) -- AStuff-definition-json -- { "span": { @@ -135,7 +135,7 @@ func a.AStuff() "offset": 510 } }, - "description": "```go\nfunc a.AStuff()\n```\n\n[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff)" + "description": "```go\nfunc a.AStuff()\n```\n\n[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff)" } -- AStuff-hoverdef -- @@ -143,7 +143,7 @@ func a.AStuff() func a.AStuff() ``` -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff) +[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff) -- S1-definition -- godef/b/b.go:27:6-8: defined here as ```go type S1 struct { @@ -154,7 +154,7 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1) -- S1-definition-json -- { "span": { @@ -170,7 +170,7 @@ type S1 struct { "offset": 589 } }, - "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1)" + "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)" } -- S1-hoverdef -- @@ -183,7 +183,7 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1) -- S1F1-definition -- godef/b/b.go:28:2-4: defined here as ```go field F1 int @@ -191,7 +191,7 @@ field F1 int \@mark\(S1F1, \"F1\"\) -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1) +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) -- S1F1-definition-json -- { "span": { @@ -207,7 +207,7 @@ field F1 int "offset": 608 } }, - "description": "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1)" + "description": "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)" } -- S1F1-hoverdef -- @@ -217,7 +217,7 @@ field F1 int \@mark\(S1F1, \"F1\"\) -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1) +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) -- S1S2-definition -- godef/b/b.go:29:2-4: defined here as ```go field S2 S2 @@ -225,7 +225,7 @@ field S2 S2 \@godef\(\"S2\", S2\),mark\(S1S2, \"S2\"\) -[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.S2) +[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2) -- S1S2-definition-json -- { "span": { @@ -241,7 +241,7 @@ field S2 S2 "offset": 640 } }, - "description": "```go\nfield S2 S2\n```\n\n\\@godef\\(\\\"S2\\\", S2\\),mark\\(S1S2, \\\"S2\\\"\\)\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.S2)" + "description": "```go\nfield S2 S2\n```\n\n\\@godef\\(\\\"S2\\\", S2\\),mark\\(S1S2, \\\"S2\\\"\\)\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)" } -- S1S2-hoverdef -- @@ -251,7 +251,7 @@ field S2 S2 \@godef\(\"S2\", S2\),mark\(S1S2, \"S2\"\) -[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.S2) +[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2) -- S2-definition -- godef/b/b.go:34:6-8: defined here as ```go type S2 struct { @@ -261,7 +261,7 @@ type S2 struct { } ``` -[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2) +[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2) -- S2-definition-json -- { "span": { @@ -277,7 +277,7 @@ type S2 struct { "offset": 764 } }, - "description": "```go\ntype S2 struct {\n\tF1 string //@mark(S2F1, \"F1\")\n\tF2 int //@mark(S2F2, \"F2\")\n\t*a.A //@godef(\"A\", AString),godef(\"a\",AImport)\n}\n```\n\n[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2)" + "description": "```go\ntype S2 struct {\n\tF1 string //@mark(S2F1, \"F1\")\n\tF2 int //@mark(S2F2, \"F2\")\n\t*a.A //@godef(\"A\", AString),godef(\"a\",AImport)\n}\n```\n\n[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2)" } -- S2-hoverdef -- @@ -289,7 +289,7 @@ type S2 struct { } ``` -[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2) +[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2) -- S2F1-definition -- godef/b/b.go:35:2-4: defined here as ```go field F1 string @@ -297,7 +297,7 @@ field F1 string \@mark\(S2F1, \"F1\"\) -[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F1) +[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1) -- S2F1-definition-json -- { "span": { @@ -313,7 +313,7 @@ field F1 string "offset": 783 } }, - "description": "```go\nfield F1 string\n```\n\n\\@mark\\(S2F1, \\\"F1\\\"\\)\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F1)" + "description": "```go\nfield F1 string\n```\n\n\\@mark\\(S2F1, \\\"F1\\\"\\)\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)" } -- S2F1-hoverdef -- @@ -323,7 +323,7 @@ field F1 string \@mark\(S2F1, \"F1\"\) -[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F1) +[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1) -- S2F2-definition -- godef/b/b.go:36:2-4: defined here as ```go field F2 int @@ -331,7 +331,7 @@ field F2 int \@mark\(S2F2, \"F2\"\) -[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F2) +[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2) -- S2F2-definition-json -- { "span": { @@ -347,7 +347,7 @@ field F2 int "offset": 816 } }, - "description": "```go\nfield F2 int\n```\n\n\\@mark\\(S2F2, \\\"F2\\\"\\)\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F2)" + "description": "```go\nfield F2 int\n```\n\n\\@mark\\(S2F2, \\\"F2\\\"\\)\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)" } -- S2F2-hoverdef -- @@ -357,7 +357,7 @@ field F2 int \@mark\(S2F2, \"F2\"\) -[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F2) +[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2) -- aAlias-definition -- godef/b/b.go:25:6-12: defined here as ```go type aAlias = a.A @@ -395,7 +395,7 @@ const X untyped int = 0 \@mark\(bX, \"X\"\),godef\(\"X\", bX\) -[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#X) +[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X) -- bX-definition-json -- { "span": { @@ -411,7 +411,7 @@ const X untyped int = 0 "offset": 1250 } }, - "description": "```go\nconst X untyped int = 0\n```\n\n\\@mark\\(bX, \\\"X\\\"\\),godef\\(\\\"X\\\", bX\\)\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#X)" + "description": "```go\nconst X untyped int = 0\n```\n\n\\@mark\\(bX, \\\"X\\\"\\),godef\\(\\\"X\\\", bX\\)\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)" } -- bX-hoverdef -- @@ -421,13 +421,13 @@ const X untyped int = 0 \@mark\(bX, \"X\"\),godef\(\"X\", bX\) -[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#X) +[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X) -- myFoo-definition -- godef/b/b.go:4:2-7: defined here as ```go package myFoo ("golang.org/x/tools/internal/lsp/foo") ``` -[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo?utm_source=gopls) +[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo) -- myFoo-definition-json -- { "span": { @@ -443,7 +443,7 @@ package myFoo ("golang.org/x/tools/internal/lsp/foo") "offset": 26 } }, - "description": "```go\npackage myFoo (\"golang.org/x/tools/internal/lsp/foo\")\n```\n\n[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo?utm_source=gopls)" + "description": "```go\npackage myFoo (\"golang.org/x/tools/internal/lsp/foo\")\n```\n\n[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo)" } -- myFoo-hoverdef -- @@ -451,4 +451,4 @@ package myFoo ("golang.org/x/tools/internal/lsp/foo") package myFoo ("golang.org/x/tools/internal/lsp/foo") ``` -[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo?utm_source=gopls) +[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo) diff --git a/internal/lsp/testdata/godef/b/c.go.golden b/internal/lsp/testdata/godef/b/c.go.golden index e6205b7265c..fddf3d273ce 100644 --- a/internal/lsp/testdata/godef/b/c.go.golden +++ b/internal/lsp/testdata/godef/b/c.go.golden @@ -8,7 +8,7 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1) -- S1-definition-json -- { "span": { @@ -24,7 +24,7 @@ type S1 struct { "offset": 589 } }, - "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1)" + "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)" } -- S1-hoverdef -- @@ -37,7 +37,7 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1) -- S1F1-definition -- godef/b/b.go:28:2-4: defined here as ```go field F1 int @@ -45,7 +45,7 @@ field F1 int \@mark\(S1F1, \"F1\"\) -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1) +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) -- S1F1-definition-json -- { "span": { @@ -61,7 +61,7 @@ field F1 int "offset": 608 } }, - "description": "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1)" + "description": "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)" } -- S1F1-hoverdef -- @@ -71,4 +71,4 @@ field F1 int \@mark\(S1F1, \"F1\"\) -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1) +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) diff --git a/internal/lsp/testdata/godef/b/e.go.golden b/internal/lsp/testdata/godef/b/e.go.golden index f9af7b74317..5a26caa4fe6 100644 --- a/internal/lsp/testdata/godef/b/e.go.golden +++ b/internal/lsp/testdata/godef/b/e.go.golden @@ -5,7 +5,7 @@ field Member string \@Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) -- Member-definition-json -- { "span": { @@ -21,7 +21,7 @@ field Member string "offset": 96 } }, - "description": "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member)" + "description": "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)" } -- Member-hoverdef -- @@ -31,7 +31,7 @@ field Member string \@Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) -- Other-definition -- godef/a/d.go:9:5-10: defined here as ```go var a.Other a.Thing @@ -39,7 +39,7 @@ var a.Other a.Thing \@Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) -- Other-definition-json -- { "span": { @@ -55,7 +55,7 @@ var a.Other a.Thing "offset": 126 } }, - "description": "```go\nvar a.Other a.Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other)" + "description": "```go\nvar a.Other a.Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)" } -- Other-hoverdef -- @@ -65,7 +65,7 @@ var a.Other a.Thing \@Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) -- Thing-definition -- godef/a/d.go:5:6-11: defined here as ```go type Thing struct { @@ -73,7 +73,7 @@ type Thing struct { } ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing) -- Thing-definition-json -- { "span": { @@ -89,7 +89,7 @@ type Thing struct { "offset": 70 } }, - "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing)" + "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)" } -- Thing-hoverdef -- @@ -99,13 +99,13 @@ type Thing struct { } ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing) -- Things-definition -- godef/a/d.go:11:6-12: defined here as ```go func a.Things(val []string) []a.Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things) -- Things-definition-json -- { "span": { @@ -121,7 +121,7 @@ func a.Things(val []string) []a.Thing "offset": 154 } }, - "description": "```go\nfunc a.Things(val []string) []a.Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things)" + "description": "```go\nfunc a.Things(val []string) []a.Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)" } -- Things-hoverdef -- @@ -129,7 +129,7 @@ func a.Things(val []string) []a.Thing func a.Things(val []string) []a.Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things) -- eInt-hoverdef -- ```go var x int diff --git a/internal/lsp/testdata/godef/b/h.go.golden b/internal/lsp/testdata/godef/b/h.go.golden index f32f0264f8f..73593c63da2 100644 --- a/internal/lsp/testdata/godef/b/h.go.golden +++ b/internal/lsp/testdata/godef/b/h.go.golden @@ -3,7 +3,7 @@ func AStuff() ``` -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff) +[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff) -- AVariable-hoverdef -- ```go var _ A diff --git a/internal/lsp/testdata/godef/hover_generics/hover.go.golden b/internal/lsp/testdata/godef/hover_generics/hover.go.golden index cfebcc472c9..91981a11779 100644 --- a/internal/lsp/testdata/godef/hover_generics/hover.go.golden +++ b/internal/lsp/testdata/godef/hover_generics/hover.go.golden @@ -13,7 +13,7 @@ field Q int \@mark\(ValueQfield, \"Q\"\),hoverdef\(\"Q\", ValueQfield\) -[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/hover_generics?utm_source=gopls#Value.Q) +[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/hover_generics#Value.Q) -- ValueTdecl-hoverdef -- ```go type parameter T any diff --git a/internal/lsp/testdata/links/links.go b/internal/lsp/testdata/links/links.go index 89492bafebf..06ab290e84a 100644 --- a/internal/lsp/testdata/links/links.go +++ b/internal/lsp/testdata/links/links.go @@ -1,11 +1,11 @@ package links import ( - "fmt" //@link(`fmt`,"https://pkg.go.dev/fmt?utm_source=gopls") + "fmt" //@link(`fmt`,"https://pkg.go.dev/fmt") - "golang.org/x/tools/internal/lsp/foo" //@link(`golang.org/x/tools/internal/lsp/foo`,`https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo?utm_source=gopls`) + "golang.org/x/tools/internal/lsp/foo" //@link(`golang.org/x/tools/internal/lsp/foo`,`https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo`) - _ "database/sql" //@link(`database/sql`, `https://pkg.go.dev/database/sql?utm_source=gopls`) + _ "database/sql" //@link(`database/sql`, `https://pkg.go.dev/database/sql`) ) var ( From 987de349f4bd5c62d77fde99c97cd4f1b83691a0 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Sat, 13 Aug 2022 15:02:08 -0400 Subject: [PATCH 189/723] internal/lsp/completion: don't use Type.String for checking identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completion is very performance sensitive, and building a string to check for *testing.F has a significant cost. StructCompletion-8 20.7ms ±14% 16.8ms ± 1% -18.59% (p=0.000 n=10+10) ImportCompletion-8 1.36ms ± 5% 1.05ms ±18% -22.55% (p=0.000 n=9+10) SliceCompletion-8 23.5ms ± 2% 19.3ms ±18% -17.85% (p=0.000 n=7+10) FuncDeepCompletion-8 17.6ms ± 2% 15.5ms ± 2% -11.82% (p=0.000 n=8+8) CompletionFollowingEdit-8 81.2ms ± 8% 74.2ms ± 5% -8.60% (p=0.000 n=9+9) For golang/go#53992 For golang/go#53798 Change-Id: Ia138cbadce142a424caabe8259bda05bcc536055 Reviewed-on: https://go-review.googlesource.com/c/tools/+/422906 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley --- internal/lsp/source/completion/completion.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/internal/lsp/source/completion/completion.go b/internal/lsp/source/completion/completion.go index be613d3e3e3..e230fc58e58 100644 --- a/internal/lsp/source/completion/completion.go +++ b/internal/lsp/source/completion/completion.go @@ -1245,7 +1245,7 @@ func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *impo c.methodSetCache[methodSetKey{typ, addressable}] = mset } - if typ.String() == "*testing.F" && addressable { + if isStarTestingDotF(typ) && addressable { // is that a sufficient test? (or is more care needed?) if c.fuzz(typ, mset, imp, cb, c.snapshot.FileSet()) { return @@ -1272,6 +1272,19 @@ func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *impo }) } +// isStarTestingDotF reports whether typ is *testing.F. +func isStarTestingDotF(typ types.Type) bool { + ptr, _ := typ.(*types.Pointer) + if ptr == nil { + return false + } + named, _ := ptr.Elem().(*types.Named) + if named == nil { + return false + } + return named.Obj() != nil && named.Obj().Pkg().Path() == "testing" && named.Obj().Name() == "F" +} + // lexical finds completions in the lexical environment. func (c *completer) lexical(ctx context.Context) error { var ( From 8c830569a8eabb55cf0049da6df43fab638c5c53 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 15 Aug 2022 14:24:47 -0400 Subject: [PATCH 190/723] gopls/internal/regtest: unskip TestSumUpdateFixesDiagnostics Metadata reloading has been significantly refactored recently. Unskip this test to see if it still flakes. For golang/go#51352 For golang/go#53878 Change-Id: I9f2ae1835bbace1b5095c2d45db082c4e709437b Reviewed-on: https://go-review.googlesource.com/c/tools/+/423974 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Peter Weinberger --- gopls/internal/regtest/modfile/modfile_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index a32a06a32fc..8dabc43f6a4 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -877,8 +877,6 @@ func main() {} } func TestSumUpdateFixesDiagnostics(t *testing.T) { - t.Skipf("Skipping known-flaky test; see https://go.dev/issue/51352.") - testenv.NeedsGo1Point(t, 14) const mod = ` From e8507bed929e6f2a933ea6d4162eab0a51d746b0 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 15 Aug 2022 11:41:17 -0400 Subject: [PATCH 191/723] gopls/internal/regtest/bench: replace -gopls_version with -gopls_commit The go command can't always resolve arbitrary commits as versions, and some commits have an x/tools replace directive that must be honored, so use 'git clone' with an arbitrary commit ref, instead of 'go install' with a Go module version, to install gopls. Also rename BenchmarkIWL to BenchmarkInitialWorkspaceLoad. For golang/go#53538 Change-Id: Ic3a08e4c023e0292f6595cc5b2ab59954d073546 Reviewed-on: https://go-review.googlesource.com/c/tools/+/422908 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/regtest/bench/bench_test.go | 94 ++++++++++++---------- gopls/internal/regtest/bench/iwl_test.go | 6 +- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index 2bee4205cb5..c7901806ed3 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -75,8 +75,8 @@ var ( file = flag.String("file", "go/ast/astutil/util.go", "active file, for benchmarks that operate on a file") commitish = flag.String("commit", "gopls/v0.9.0", "if set (and -workdir is unset), run benchmarks at this commit") - goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_version") - goplsVersion = flag.String("gopls_version", "", "if set, install and use gopls at this version for testing; incompatible with -gopls_path") + goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_commit") + goplsCommit = flag.String("gopls_commit", "", "if set, install and use gopls at this commit for testing; incompatible with -gopls_path") // If non-empty, tempDir is a temporary working dir that was created by this // test suite. @@ -123,26 +123,37 @@ func benchmarkDir() string { dir := filepath.Join(getTempDir(), "repo") checkoutRepoOnce.Do(func() { - if err := os.Mkdir(dir, 0750); err != nil { - log.Fatalf("creating repo dir: %v", err) - } - log.Printf("checking out %s@%s to %s\n", *repo, *commitish, dir) - - // Set a timeout for git fetch. If this proves flaky, it can be removed. - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) - defer cancel() - - // Use a shallow fetch to download just the releveant commit. - shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", *repo, *commitish) - initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit) - initCmd.Dir = dir - if err := initCmd.Run(); err != nil { - log.Fatalf("checking out %s: %v", *repo, err) + log.Printf("creating working dir: checking out %s@%s to %s\n", *repo, *commitish, dir) + if err := shallowClone(dir, *repo, *commitish); err != nil { + log.Fatal(err) } }) return dir } +// shallowClone performs a shallow clone of repo into dir at the given +// 'commitish' ref (any commit reference understood by git). +// +// The directory dir must not already exist. +func shallowClone(dir, repo, commitish string) error { + if err := os.Mkdir(dir, 0750); err != nil { + return fmt.Errorf("creating dir for %s: %v", repo, err) + } + + // Set a timeout for git fetch. If this proves flaky, it can be removed. + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + // Use a shallow fetch to download just the relevant commit. + shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", repo, commitish) + initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit) + initCmd.Dir = dir + if output, err := initCmd.CombinedOutput(); err != nil { + return fmt.Errorf("checking out %s: %v\n%s", repo, err, output) + } + return nil +} + // benchmarkEnv returns a shared benchmark environment func benchmarkEnv(tb testing.TB) *Env { setupEditorOnce.Do(func() { @@ -191,50 +202,45 @@ func connectEditor(dir string, config fake.EditorConfig) (*fake.Sandbox, *fake.E // getServer returns a server connector that either starts a new in-process // server, or starts a separate gopls process. func getServer() servertest.Connector { - if *goplsPath != "" && *goplsVersion != "" { - panic("can't set both -gopls_path and -gopls_version") + if *goplsPath != "" && *goplsCommit != "" { + panic("can't set both -gopls_path and -gopls_commit") } if *goplsPath != "" { return &SidecarServer{*goplsPath} } - if *goplsVersion != "" { - path := getInstalledGopls(*goplsVersion) + if *goplsCommit != "" { + path := getInstalledGopls() return &SidecarServer{path} } server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false) return servertest.NewPipeServer(server, jsonrpc2.NewRawStream) } -func getInstalledGopls(version string) string { - // Use a temp GOPATH to while installing gopls, to avoid overwriting gopls in - // the user's PATH. - gopath := filepath.Join(getTempDir(), "gopath") - goplsPath := filepath.Join(gopath, "bin", "gopls") +// getInstalledGopls builds gopls at the given -gopls_commit, returning the +// path to the gopls binary. +func getInstalledGopls() string { + if *goplsCommit == "" { + panic("must provide -gopls_commit") + } + toolsDir := filepath.Join(getTempDir(), "tools") + goplsPath := filepath.Join(toolsDir, "gopls", "gopls") + installGoplsOnce.Do(func() { - if err := os.Mkdir(gopath, 0755); err != nil { - log.Fatalf("creating temp GOPATH: %v", err) - } - env := append(os.Environ(), "GOPATH="+gopath) - - // Install gopls. - log.Printf("installing gopls@%s\n", version) - cmd := exec.Command("go", "install", fmt.Sprintf("golang.org/x/tools/gopls@%s", version)) - cmd.Env = env - if output, err := cmd.CombinedOutput(); err != nil { - log.Fatalf("installing gopls: %v:\n%s", err, string(output)) + log.Printf("installing gopls: checking out x/tools@%s\n", *goplsCommit) + if err := shallowClone(toolsDir, "https://go.googlesource.com/tools", *goplsCommit); err != nil { + log.Fatal(err) } - // Clean the modcache, otherwise we'll get errors when trying to remove the - // temp directory. - cleanCmd := exec.Command("go", "clean", "-modcache") - cleanCmd.Env = env - if output, err := cleanCmd.CombinedOutput(); err != nil { - log.Fatalf("cleaning up temp GOPATH: %v\n%s", err, string(output)) + log.Println("installing gopls: building...") + bld := exec.Command("go", "build", ".") + bld.Dir = filepath.Join(getTempDir(), "tools", "gopls") + if output, err := bld.CombinedOutput(); err != nil { + log.Fatalf("building gopls: %v\n%s", err, output) } // Confirm that the resulting path now exists. if _, err := os.Stat(goplsPath); err != nil { - log.Fatalf("os.Stat(goplsPath): %v", err) + log.Fatalf("os.Stat(%s): %v", goplsPath, err) } }) return goplsPath diff --git a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go index b223e336dbe..b11173a8ff2 100644 --- a/gopls/internal/regtest/bench/iwl_test.go +++ b/gopls/internal/regtest/bench/iwl_test.go @@ -12,9 +12,9 @@ import ( . "golang.org/x/tools/internal/lsp/regtest" ) -// BenchmarkIWL benchmarks the initial workspace load time for a new editing -// session. -func BenchmarkIWL(b *testing.B) { +// BenchmarkInitialWorkspaceLoad benchmarks the initial workspace load time for +// a new editing session. +func BenchmarkInitialWorkspaceLoad(b *testing.B) { dir := benchmarkDir() b.ResetTimer() From 938e162bcf3a5ba07fe4dfcac14cb525260db9ed Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 15 Aug 2022 14:12:52 -0400 Subject: [PATCH 192/723] gopls/internal/regtest: unskip TestDeleteModule_Interdependent Reloading has been significantly refactored recently. Unskip this test to see if it flakes: - If it does not flake, that is a useful signal. - If it does flake, that is also a useful signal. Notably, following CL 419500 we allow network when reloading the workspace, and so do not need to apply quick-fixes in order to download the new module from the proxy. For golang/go#46375 For golang/go#53878 Change-Id: Idde7195730c32bdb434a26b28aac82649dd1b5ac Reviewed-on: https://go-review.googlesource.com/c/tools/+/422910 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Peter Weinberger gopls-CI: kokoro --- gopls/internal/regtest/workspace/workspace_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 86da9d1c938..3e980de95d8 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -302,8 +302,6 @@ func main() { // This change tests that the version of the module used changes after it has // been deleted from the workspace. func TestDeleteModule_Interdependent(t *testing.T) { - t.Skip("Skipping due to golang/go#46375: race due to orphaned file reloading") - const multiModule = ` -- moda/a/go.mod -- module a.com @@ -353,15 +351,6 @@ func Hello() int { env.DoneWithChangeWatchedFiles(), ) - d := protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - env.DiagnosticAtRegexpWithMessage("moda/a/go.mod", "require b.com v1.2.3", "b.com@v1.2.3 has not been downloaded"), - ReadDiagnostics("moda/a/go.mod", &d), - ), - ) - env.ApplyQuickFixes("moda/a/go.mod", d.Diagnostics) - env.Await(env.DoneWithChangeWatchedFiles()) got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { t.Errorf("expected %s, got %v", want, got) From b3851a823f19f7bfb6b79e3987f5abf87a935d7f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 15 Aug 2022 16:25:59 -0400 Subject: [PATCH 193/723] internal/lsp/cache: tweaks to metadata graph - ignore error from buildPackageHandle, with rationale. - remove unnessary optimization in call to Clone(updates={}): Clone does the same check internally. - more comments. Change-Id: I4551cf560aea722d972fb6da404aed71a79f4037 Reviewed-on: https://go-review.googlesource.com/c/tools/+/416217 Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan --- internal/lsp/cache/graph.go | 8 ++------ internal/lsp/cache/load.go | 15 ++++++--------- internal/lsp/cache/snapshot.go | 8 +------- internal/lsp/cache/view.go | 5 +++-- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go index c1beff82866..d0f80cc4014 100644 --- a/internal/lsp/cache/graph.go +++ b/internal/lsp/cache/graph.go @@ -11,12 +11,8 @@ import ( "golang.org/x/tools/internal/span" ) -// A metadataGraph holds information about a transtively closed import graph of -// Go packages, as obtained from go/packages. -// -// Currently a new metadata graph is created for each snapshot. -// TODO(rfindley): make this type immutable, so that it may be shared across -// snapshots. +// A metadataGraph is an immutable and transitively closed import +// graph of Go packages, as obtained from go/packages. type metadataGraph struct { // metadata maps package IDs to their associated metadata. metadata map[PackageID]*KnownMetadata diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index b15c5374e93..6d9c598606b 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -93,9 +93,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf if s.view.Options().VerboseWorkDoneProgress { work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading query=%s", query), nil, nil) - defer func() { - work.End(ctx, "Done.") - }() + defer work.End(ctx, "Done.") } ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query)) @@ -241,14 +239,13 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf s.dumpWorkspace("load") s.mu.Unlock() - // Rebuild the workspace package handle for any packages we invalidated. + // Recompute the workspace package handle for any packages we invalidated. // - // TODO(rfindley): what's the point of returning an error here? Probably we - // can simply remove this step: The package handle will be rebuilt as needed. + // This is (putatively) an optimization since handle + // construction prefetches the content of all Go source files. + // It is safe to ignore errors, or omit this step entirely. for _, m := range updates { - if _, err := s.buildPackageHandle(ctx, m.ID, s.workspaceParseMode(m.ID)); err != nil { - return err - } + s.buildPackageHandle(ctx, m.ID, s.workspaceParseMode(m.ID)) // ignore error } if len(moduleErrs) > 0 { diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 0fa670cf20f..87ef595c334 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -1986,13 +1986,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // Update metadata, if necessary. - if len(metadataUpdates) > 0 { - result.meta = s.meta.Clone(metadataUpdates) - } else { - // No metadata changes. Since metadata is only updated by cloning, it is - // safe to re-use the existing metadata here. - result.meta = s.meta - } + result.meta = s.meta.Clone(metadataUpdates) // Update workspace and active packages, if necessary. if result.meta != s.meta || anyFileOpenedOrClosed { diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index b23ed614f6d..9d1ee558103 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -710,8 +710,9 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { scopes = append(scopes, viewLoadScope("LOAD_VIEW")) } - // If we're loading anything, ensure we also load builtin. - // TODO(rstambler): explain the rationale for this. + // If we're loading anything, ensure we also load builtin, + // since it provides fake definitions (and documentation) + // for types like int that are used everywhere. if len(scopes) > 0 { scopes = append(scopes, PackagePath("builtin")) } From a3cac118817940baca8af6903d385033558681b3 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Fri, 12 Mar 2021 13:54:13 -0500 Subject: [PATCH 194/723] godoc/redirect: delete golang.org-specific code This logic is now in x/website. Delete from here to avoid confusion. Change-Id: Iec8efaa3490fa471a4ebd7e1fb34b4927a39062d Reviewed-on: https://go-review.googlesource.com/c/tools/+/301309 Run-TryBot: Russ Cox Reviewed-by: Dmitri Shuralyov gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Dmitri Shuralyov Auto-Submit: Russ Cox --- godoc/redirect/hash.go | 138 ---- godoc/redirect/redirect.go | 272 +------- godoc/redirect/redirect_test.go | 51 +- godoc/redirect/rietveld.go | 1093 ------------------------------- 4 files changed, 4 insertions(+), 1550 deletions(-) delete mode 100644 godoc/redirect/hash.go delete mode 100644 godoc/redirect/rietveld.go diff --git a/godoc/redirect/hash.go b/godoc/redirect/hash.go deleted file mode 100644 index d5a1e3eb67b..00000000000 --- a/godoc/redirect/hash.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2014 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This file provides a compact encoding of -// a map of Mercurial hashes to Git hashes. - -package redirect - -import ( - "encoding/binary" - "fmt" - "io" - "os" - "sort" - "strconv" - "strings" -) - -// hashMap is a map of Mercurial hashes to Git hashes. -type hashMap struct { - file *os.File - entries int -} - -// newHashMap takes a file handle that contains a map of Mercurial to Git -// hashes. The file should be a sequence of pairs of little-endian encoded -// uint32s, representing a hgHash and a gitHash respectively. -// The sequence must be sorted by hgHash. -// The file must remain open for as long as the returned hashMap is used. -func newHashMap(f *os.File) (*hashMap, error) { - fi, err := f.Stat() - if err != nil { - return nil, err - } - return &hashMap{file: f, entries: int(fi.Size() / 8)}, nil -} - -// Lookup finds an hgHash in the map that matches the given prefix, and returns -// its corresponding gitHash. The prefix must be at least 8 characters long. -func (m *hashMap) Lookup(s string) gitHash { - if m == nil { - return 0 - } - hg, err := hgHashFromString(s) - if err != nil { - return 0 - } - var git gitHash - b := make([]byte, 8) - sort.Search(m.entries, func(i int) bool { - n, err := m.file.ReadAt(b, int64(i*8)) - if err != nil { - panic(err) - } - if n != 8 { - panic(io.ErrUnexpectedEOF) - } - v := hgHash(binary.LittleEndian.Uint32(b[:4])) - if v == hg { - git = gitHash(binary.LittleEndian.Uint32(b[4:])) - } - return v >= hg - }) - return git -} - -// hgHash represents the lower (leftmost) 32 bits of a Mercurial hash. -type hgHash uint32 - -func (h hgHash) String() string { - return intToHash(int64(h)) -} - -func hgHashFromString(s string) (hgHash, error) { - if len(s) < 8 { - return 0, fmt.Errorf("string too small: len(s) = %d", len(s)) - } - hash := s[:8] - i, err := strconv.ParseInt(hash, 16, 64) - if err != nil { - return 0, err - } - return hgHash(i), nil -} - -// gitHash represents the leftmost 28 bits of a Git hash in its upper 28 bits, -// and it encodes hash's repository in the lower 4 bits. -type gitHash uint32 - -func (h gitHash) Hash() string { - return intToHash(int64(h))[:7] -} - -func (h gitHash) Repo() string { - return repo(h & 0xF).String() -} - -func intToHash(i int64) string { - s := strconv.FormatInt(i, 16) - if len(s) < 8 { - s = strings.Repeat("0", 8-len(s)) + s - } - return s -} - -// repo represents a Go Git repository. -type repo byte - -const ( - repoGo repo = iota - repoBlog - repoCrypto - repoExp - repoImage - repoMobile - repoNet - repoSys - repoTalks - repoText - repoTools -) - -func (r repo) String() string { - return map[repo]string{ - repoGo: "go", - repoBlog: "blog", - repoCrypto: "crypto", - repoExp: "exp", - repoImage: "image", - repoMobile: "mobile", - repoNet: "net", - repoSys: "sys", - repoTalks: "talks", - repoText: "text", - repoTools: "tools", - }[r] -} diff --git a/godoc/redirect/redirect.go b/godoc/redirect/redirect.go index 57d779ccb41..d0145ee183b 100644 --- a/godoc/redirect/redirect.go +++ b/godoc/redirect/redirect.go @@ -3,147 +3,22 @@ // license that can be found in the LICENSE file. // Package redirect provides hooks to register HTTP handlers that redirect old -// godoc paths to their new equivalents and assist in accessing the issue -// tracker, wiki, code review system, etc. +// godoc paths to their new equivalents. package redirect // import "golang.org/x/tools/godoc/redirect" import ( - "context" - "fmt" - "html/template" "net/http" - "os" "regexp" - "strconv" - "strings" - "sync" - "time" - - "golang.org/x/net/context/ctxhttp" ) -// Register registers HTTP handlers that redirect old godoc paths to their new -// equivalents and assist in accessing the issue tracker, wiki, code review -// system, etc. If mux is nil it uses http.DefaultServeMux. +// Register registers HTTP handlers that redirect old godoc paths to their new equivalents. +// If mux is nil it uses http.DefaultServeMux. func Register(mux *http.ServeMux) { if mux == nil { mux = http.DefaultServeMux } - handlePathRedirects(mux, pkgRedirects, "/pkg/") - handlePathRedirects(mux, cmdRedirects, "/cmd/") - for prefix, redirect := range prefixHelpers { - p := "/" + prefix + "/" - mux.Handle(p, PrefixHandler(p, redirect)) - } - for path, redirect := range redirects { - mux.Handle(path, Handler(redirect)) - } // NB: /src/pkg (sans trailing slash) is the index of packages. mux.HandleFunc("/src/pkg/", srcPkgHandler) - mux.HandleFunc("/cl/", clHandler) - mux.HandleFunc("/change/", changeHandler) - mux.HandleFunc("/design/", designHandler) -} - -func handlePathRedirects(mux *http.ServeMux, redirects map[string]string, prefix string) { - for source, target := range redirects { - h := Handler(prefix + target + "/") - p := prefix + source - mux.Handle(p, h) - mux.Handle(p+"/", h) - } -} - -// Packages that were renamed between r60 and go1. -var pkgRedirects = map[string]string{ - "asn1": "encoding/asn1", - "big": "math/big", - "cmath": "math/cmplx", - "csv": "encoding/csv", - "exec": "os/exec", - "exp/template/html": "html/template", - "gob": "encoding/gob", - "http": "net/http", - "http/cgi": "net/http/cgi", - "http/fcgi": "net/http/fcgi", - "http/httptest": "net/http/httptest", - "http/pprof": "net/http/pprof", - "json": "encoding/json", - "mail": "net/mail", - "rand": "math/rand", - "rpc": "net/rpc", - "rpc/jsonrpc": "net/rpc/jsonrpc", - "scanner": "text/scanner", - "smtp": "net/smtp", - "tabwriter": "text/tabwriter", - "template": "text/template", - "template/parse": "text/template/parse", - "url": "net/url", - "utf16": "unicode/utf16", - "utf8": "unicode/utf8", - "xml": "encoding/xml", -} - -// Commands that were renamed between r60 and go1. -var cmdRedirects = map[string]string{ - "gofix": "fix", - "goinstall": "go", - "gopack": "pack", - "gotest": "go", - "govet": "vet", - "goyacc": "yacc", -} - -var redirects = map[string]string{ - "/blog": "/blog/", - "/build": "http://build.golang.org", - "/change": "https://go.googlesource.com/go", - "/cl": "https://go-review.googlesource.com", - "/cmd/godoc/": "https://pkg.go.dev/golang.org/x/tools/cmd/godoc", - "/issue": "https://github.com/golang/go/issues", - "/issue/new": "https://github.com/golang/go/issues/new", - "/issues": "https://github.com/golang/go/issues", - "/issues/new": "https://github.com/golang/go/issues/new", - "/play": "http://play.golang.org", - "/design": "https://go.googlesource.com/proposal/+/master/design", - - // In Go 1.2 the references page is part of /doc/. - "/ref": "/doc/#references", - // This next rule clobbers /ref/spec and /ref/mem. - // TODO(adg): figure out what to do here, if anything. - // "/ref/": "/doc/#references", - - // Be nice to people who are looking in the wrong place. - "/doc/mem": "/ref/mem", - "/doc/spec": "/ref/spec", - - "/talks": "http://talks.golang.org", - "/tour": "http://tour.golang.org", - "/wiki": "https://github.com/golang/go/wiki", - - "/doc/articles/c_go_cgo.html": "/blog/c-go-cgo", - "/doc/articles/concurrency_patterns.html": "/blog/go-concurrency-patterns-timing-out-and", - "/doc/articles/defer_panic_recover.html": "/blog/defer-panic-and-recover", - "/doc/articles/error_handling.html": "/blog/error-handling-and-go", - "/doc/articles/gobs_of_data.html": "/blog/gobs-of-data", - "/doc/articles/godoc_documenting_go_code.html": "/blog/godoc-documenting-go-code", - "/doc/articles/gos_declaration_syntax.html": "/blog/gos-declaration-syntax", - "/doc/articles/image_draw.html": "/blog/go-imagedraw-package", - "/doc/articles/image_package.html": "/blog/go-image-package", - "/doc/articles/json_and_go.html": "/blog/json-and-go", - "/doc/articles/json_rpc_tale_of_interfaces.html": "/blog/json-rpc-tale-of-interfaces", - "/doc/articles/laws_of_reflection.html": "/blog/laws-of-reflection", - "/doc/articles/slices_usage_and_internals.html": "/blog/go-slices-usage-and-internals", - "/doc/go_for_cpp_programmers.html": "/wiki/GoForCPPProgrammers", - "/doc/go_tutorial.html": "http://tour.golang.org/", -} - -var prefixHelpers = map[string]string{ - "issue": "https://github.com/golang/go/issues/", - "issues": "https://github.com/golang/go/issues/", - "play": "http://play.golang.org/", - "talks": "http://talks.golang.org/", - "wiki": "https://github.com/golang/go/wiki/", } func Handler(target string) http.Handler { @@ -181,144 +56,3 @@ func srcPkgHandler(w http.ResponseWriter, r *http.Request) { r.URL.Path = "/src/" + r.URL.Path[len("/src/pkg/"):] http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) } - -func clHandler(w http.ResponseWriter, r *http.Request) { - const prefix = "/cl/" - if p := r.URL.Path; p == prefix { - // redirect /prefix/ to /prefix - http.Redirect(w, r, p[:len(p)-1], http.StatusFound) - return - } - id := r.URL.Path[len(prefix):] - // support /cl/152700045/, which is used in commit 0edafefc36. - id = strings.TrimSuffix(id, "/") - if !validID.MatchString(id) { - http.Error(w, "Not found", http.StatusNotFound) - return - } - target := "" - - if n, err := strconv.Atoi(id); err == nil && isRietveldCL(n) { - // Issue 28836: if this Rietveld CL happens to - // also be a Gerrit CL, render a disambiguation HTML - // page with two links instead. We need to make a - // Gerrit API call to figure that out, but we cache - // known Gerrit CLs so it's done at most once per CL. - if ok, err := isGerritCL(r.Context(), n); err == nil && ok { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - clDisambiguationHTML.Execute(w, n) - return - } - - target = "https://codereview.appspot.com/" + id - } else { - target = "https://go-review.googlesource.com/" + id - } - http.Redirect(w, r, target, http.StatusFound) -} - -var clDisambiguationHTML = template.Must(template.New("").Parse(` - - - Go CL {{.}} Disambiguation - - - - CL number {{.}} exists in both Gerrit (the current code review system) - and Rietveld (the previous code review system). Please make a choice: - - - -`)) - -// isGerritCL reports whether a Gerrit CL with the specified numeric change ID (e.g., "4247") -// is known to exist by querying the Gerrit API at https://go-review.googlesource.com. -// isGerritCL uses gerritCLCache as a cache of Gerrit CL IDs that exist. -func isGerritCL(ctx context.Context, id int) (bool, error) { - // Check cache first. - gerritCLCache.Lock() - ok := gerritCLCache.exist[id] - gerritCLCache.Unlock() - if ok { - return true, nil - } - - // Query the Gerrit API Get Change endpoint, as documented at - // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change. - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - resp, err := ctxhttp.Get(ctx, nil, fmt.Sprintf("https://go-review.googlesource.com/changes/%d", id)) - if err != nil { - return false, err - } - resp.Body.Close() - switch resp.StatusCode { - case http.StatusOK: - // A Gerrit CL with this ID exists. Add it to cache. - gerritCLCache.Lock() - gerritCLCache.exist[id] = true - gerritCLCache.Unlock() - return true, nil - case http.StatusNotFound: - // A Gerrit CL with this ID doesn't exist. It may get created in the future. - return false, nil - default: - return false, fmt.Errorf("unexpected status code: %v", resp.Status) - } -} - -var gerritCLCache = struct { - sync.Mutex - exist map[int]bool // exist is a set of Gerrit CL IDs that are known to exist. -}{exist: make(map[int]bool)} - -var changeMap *hashMap - -// LoadChangeMap loads the specified map of Mercurial to Git revisions, -// which is used by the /change/ handler to intelligently map old hg -// revisions to their new git equivalents. -// It should be called before calling Register. -// The file should remain open as long as the process is running. -// See the implementation of this package for details. -func LoadChangeMap(filename string) error { - f, err := os.Open(filename) - if err != nil { - return err - } - m, err := newHashMap(f) - if err != nil { - return err - } - changeMap = m - return nil -} - -func changeHandler(w http.ResponseWriter, r *http.Request) { - const prefix = "/change/" - if p := r.URL.Path; p == prefix { - // redirect /prefix/ to /prefix - http.Redirect(w, r, p[:len(p)-1], http.StatusFound) - return - } - hash := r.URL.Path[len(prefix):] - target := "https://go.googlesource.com/go/+/" + hash - if git := changeMap.Lookup(hash); git > 0 { - target = fmt.Sprintf("https://go.googlesource.com/%v/+/%v", git.Repo(), git.Hash()) - } - http.Redirect(w, r, target, http.StatusFound) -} - -func designHandler(w http.ResponseWriter, r *http.Request) { - const prefix = "/design/" - if p := r.URL.Path; p == prefix { - // redirect /prefix/ to /prefix - http.Redirect(w, r, p[:len(p)-1], http.StatusFound) - return - } - name := r.URL.Path[len(prefix):] - target := "https://go.googlesource.com/proposal/+/master/design/" + name + ".md" - http.Redirect(w, r, target, http.StatusFound) -} diff --git a/godoc/redirect/redirect_test.go b/godoc/redirect/redirect_test.go index 1de3c6ca779..59677c435cc 100644 --- a/godoc/redirect/redirect_test.go +++ b/godoc/redirect/redirect_test.go @@ -21,56 +21,7 @@ func errorResult(status int) redirectResult { func TestRedirects(t *testing.T) { var tests = map[string]redirectResult{ - "/build": {301, "http://build.golang.org"}, - "/ref": {301, "/doc/#references"}, - "/doc/mem": {301, "/ref/mem"}, - "/doc/spec": {301, "/ref/spec"}, - "/tour": {301, "http://tour.golang.org"}, - "/foo": errorResult(404), - - "/pkg/asn1": {301, "/pkg/encoding/asn1/"}, - "/pkg/template/parse": {301, "/pkg/text/template/parse/"}, - - "/src/pkg/foo": {301, "/src/foo"}, - - "/cmd/gofix": {301, "/cmd/fix/"}, - - // git commits (/change) - // TODO: mercurial tags and LoadChangeMap. - "/change": {301, "https://go.googlesource.com/go"}, - "/change/a": {302, "https://go.googlesource.com/go/+/a"}, - - "/issue": {301, "https://github.com/golang/go/issues"}, - "/issue?": {301, "https://github.com/golang/go/issues"}, - "/issue/1": {302, "https://github.com/golang/go/issues/1"}, - "/issue/new": {301, "https://github.com/golang/go/issues/new"}, - "/issue/new?a=b&c=d%20&e=f": {301, "https://github.com/golang/go/issues/new?a=b&c=d%20&e=f"}, - "/issues": {301, "https://github.com/golang/go/issues"}, - "/issues/1": {302, "https://github.com/golang/go/issues/1"}, - "/issues/new": {301, "https://github.com/golang/go/issues/new"}, - "/issues/1/2/3": errorResult(404), - - "/wiki/foo": {302, "https://github.com/golang/go/wiki/foo"}, - "/wiki/foo/": {302, "https://github.com/golang/go/wiki/foo/"}, - - "/design": {301, "https://go.googlesource.com/proposal/+/master/design"}, - "/design/": {302, "/design"}, - "/design/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/123-foo.md"}, - "/design/text/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/text/123-foo.md"}, - - "/cl/1": {302, "https://go-review.googlesource.com/1"}, - "/cl/1/": {302, "https://go-review.googlesource.com/1"}, - "/cl/267120043": {302, "https://codereview.appspot.com/267120043"}, - "/cl/267120043/": {302, "https://codereview.appspot.com/267120043"}, - - // Verify that we're using the Rietveld CL table: - "/cl/152046": {302, "https://codereview.appspot.com/152046"}, - "/cl/152047": {302, "https://go-review.googlesource.com/152047"}, - "/cl/152048": {302, "https://codereview.appspot.com/152048"}, - - // And verify we're using the "bigEnoughAssumeRietveld" value: - "/cl/299999": {302, "https://go-review.googlesource.com/299999"}, - "/cl/300000": {302, "https://codereview.appspot.com/300000"}, + "/foo": errorResult(404), } mux := http.NewServeMux() diff --git a/godoc/redirect/rietveld.go b/godoc/redirect/rietveld.go deleted file mode 100644 index 81b1094db17..00000000000 --- a/godoc/redirect/rietveld.go +++ /dev/null @@ -1,1093 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package redirect - -// bigEnoughAssumeRietveld is the value where CLs equal or great are -// assumed to be on Rietveld. By including this threshold we shrink -// the size of the table below. When Go amasses 150,000 more CLs, we'll -// need to bump this number and regenerate the list below. -const bigEnoughAssumeRietveld = 300000 - -// isRietveldCL reports whether cl was a Rietveld CL number. -func isRietveldCL(cl int) bool { - return cl >= bigEnoughAssumeRietveld || lowRietveldCL[cl] -} - -// lowRietveldCLs are the old CL numbers assigned by Rietveld code -// review system as used by Go prior to Gerrit which are less than -// bigEnoughAssumeRietveld. -// -// This list of numbers is registered with the /cl/NNNN redirect -// handler to disambiguate which code review system a particular -// number corresponds to. In some rare cases there may be duplicates, -// in which case we might render an HTML choice for the user. -// -// To re-generate this list, run: -// -// $ cd $GOROOT -// $ git log 7d7c6a9..94151eb | grep "^ https://golang.org/cl/" | perl -ne 's,^\s+https://golang.org/cl/(\d+).*$,$1,; chomp; print "$_: true,\n" if $_ < 300000' | sort -n | uniq -// -// Note that we ignore the x/* repos because we didn't start using -// "subrepos" until the Rietveld CLs numbers were already 4,000,000+, -// well above bigEnoughAssumeRietveld. -var lowRietveldCL = map[int]bool{ - 152046: true, - 152048: true, - 152049: true, - 152050: true, - 152051: true, - 152052: true, - 152055: true, - 152056: true, - 152057: true, - 152072: true, - 152073: true, - 152075: true, - 152076: true, - 152077: true, - 152078: true, - 152079: true, - 152080: true, - 152082: true, - 152084: true, - 152085: true, - 152086: true, - 152088: true, - 152089: true, - 152091: true, - 152098: true, - 152101: true, - 152102: true, - 152105: true, - 152106: true, - 152107: true, - 152108: true, - 152109: true, - 152110: true, - 152114: true, - 152117: true, - 152118: true, - 152120: true, - 152123: true, - 152124: true, - 152128: true, - 152130: true, - 152131: true, - 152138: true, - 152141: true, - 152142: true, - 153048: true, - 153049: true, - 153050: true, - 153051: true, - 153055: true, - 153056: true, - 153057: true, - 154043: true, - 154044: true, - 154045: true, - 154049: true, - 154055: true, - 154057: true, - 154058: true, - 154059: true, - 154061: true, - 154064: true, - 154065: true, - 154067: true, - 154068: true, - 154069: true, - 154071: true, - 154072: true, - 154073: true, - 154076: true, - 154079: true, - 154096: true, - 154097: true, - 154099: true, - 154100: true, - 154101: true, - 154102: true, - 154108: true, - 154118: true, - 154121: true, - 154122: true, - 154123: true, - 154125: true, - 154126: true, - 154128: true, - 154136: true, - 154138: true, - 154139: true, - 154140: true, - 154141: true, - 154142: true, - 154143: true, - 154144: true, - 154145: true, - 154146: true, - 154152: true, - 154153: true, - 154156: true, - 154159: true, - 154161: true, - 154166: true, - 154167: true, - 154169: true, - 154171: true, - 154172: true, - 154173: true, - 154174: true, - 154175: true, - 154176: true, - 154177: true, - 154178: true, - 154179: true, - 154180: true, - 155041: true, - 155042: true, - 155045: true, - 155047: true, - 155048: true, - 155049: true, - 155050: true, - 155054: true, - 155055: true, - 155056: true, - 155057: true, - 155058: true, - 155059: true, - 155061: true, - 155062: true, - 155063: true, - 155065: true, - 155067: true, - 155069: true, - 155072: true, - 155074: true, - 155075: true, - 155077: true, - 155078: true, - 155079: true, - 156041: true, - 156044: true, - 156045: true, - 156046: true, - 156047: true, - 156051: true, - 156052: true, - 156054: true, - 156055: true, - 156056: true, - 156058: true, - 156059: true, - 156060: true, - 156061: true, - 156062: true, - 156063: true, - 156066: true, - 156067: true, - 156070: true, - 156071: true, - 156073: true, - 156075: true, - 156077: true, - 156079: true, - 156080: true, - 156081: true, - 156083: true, - 156084: true, - 156085: true, - 156086: true, - 156089: true, - 156091: true, - 156092: true, - 156093: true, - 156094: true, - 156097: true, - 156099: true, - 156100: true, - 156102: true, - 156103: true, - 156104: true, - 156106: true, - 156107: true, - 156108: true, - 156109: true, - 156110: true, - 156113: true, - 156115: true, - 156116: true, - 157041: true, - 157042: true, - 157043: true, - 157044: true, - 157046: true, - 157053: true, - 157055: true, - 157056: true, - 157058: true, - 157060: true, - 157061: true, - 157062: true, - 157065: true, - 157066: true, - 157067: true, - 157068: true, - 157069: true, - 157071: true, - 157072: true, - 157073: true, - 157074: true, - 157075: true, - 157076: true, - 157077: true, - 157082: true, - 157084: true, - 157085: true, - 157087: true, - 157088: true, - 157091: true, - 157095: true, - 157096: true, - 157099: true, - 157100: true, - 157101: true, - 157102: true, - 157103: true, - 157104: true, - 157106: true, - 157110: true, - 157111: true, - 157112: true, - 157114: true, - 157116: true, - 157119: true, - 157140: true, - 157142: true, - 157143: true, - 157144: true, - 157146: true, - 157147: true, - 157149: true, - 157151: true, - 157152: true, - 157153: true, - 157154: true, - 157156: true, - 157157: true, - 157158: true, - 157159: true, - 157160: true, - 157162: true, - 157166: true, - 157167: true, - 157168: true, - 157170: true, - 158041: true, - 159044: true, - 159049: true, - 159050: true, - 159051: true, - 160043: true, - 160044: true, - 160045: true, - 160046: true, - 160047: true, - 160054: true, - 160056: true, - 160057: true, - 160059: true, - 160060: true, - 160061: true, - 160064: true, - 160065: true, - 160069: true, - 160070: true, - 161049: true, - 161050: true, - 161056: true, - 161058: true, - 161060: true, - 161061: true, - 161069: true, - 161070: true, - 161073: true, - 161075: true, - 162041: true, - 162044: true, - 162046: true, - 162053: true, - 162054: true, - 162055: true, - 162056: true, - 162057: true, - 162058: true, - 162059: true, - 162061: true, - 162062: true, - 163042: true, - 163044: true, - 163049: true, - 163050: true, - 163051: true, - 163052: true, - 163053: true, - 163055: true, - 163058: true, - 163061: true, - 163062: true, - 163064: true, - 163067: true, - 163068: true, - 163069: true, - 163070: true, - 163071: true, - 163072: true, - 163082: true, - 163083: true, - 163085: true, - 163088: true, - 163091: true, - 163092: true, - 163097: true, - 163098: true, - 164043: true, - 164047: true, - 164049: true, - 164052: true, - 164053: true, - 164056: true, - 164059: true, - 164060: true, - 164062: true, - 164068: true, - 164069: true, - 164071: true, - 164073: true, - 164074: true, - 164075: true, - 164078: true, - 164079: true, - 164081: true, - 164082: true, - 164083: true, - 164085: true, - 164086: true, - 164088: true, - 164090: true, - 164091: true, - 164092: true, - 164093: true, - 164094: true, - 164095: true, - 165042: true, - 165044: true, - 165045: true, - 165048: true, - 165049: true, - 165050: true, - 165051: true, - 165055: true, - 165057: true, - 165058: true, - 165059: true, - 165061: true, - 165062: true, - 165063: true, - 165064: true, - 165065: true, - 165068: true, - 165070: true, - 165076: true, - 165078: true, - 165080: true, - 165083: true, - 165086: true, - 165097: true, - 165100: true, - 165101: true, - 166041: true, - 166043: true, - 166044: true, - 166047: true, - 166049: true, - 166052: true, - 166053: true, - 166055: true, - 166058: true, - 166059: true, - 166060: true, - 166064: true, - 166066: true, - 166067: true, - 166068: true, - 166070: true, - 166071: true, - 166072: true, - 166073: true, - 166074: true, - 166076: true, - 166077: true, - 166078: true, - 166080: true, - 167043: true, - 167044: true, - 167047: true, - 167050: true, - 167055: true, - 167057: true, - 167058: true, - 168041: true, - 168045: true, - 170042: true, - 170043: true, - 170044: true, - 170046: true, - 170047: true, - 170048: true, - 170049: true, - 171044: true, - 171046: true, - 171047: true, - 171048: true, - 171051: true, - 172041: true, - 172042: true, - 172043: true, - 172045: true, - 172049: true, - 173041: true, - 173044: true, - 173045: true, - 174042: true, - 174047: true, - 174048: true, - 174050: true, - 174051: true, - 174052: true, - 174053: true, - 174063: true, - 174064: true, - 174072: true, - 174076: true, - 174077: true, - 174078: true, - 174082: true, - 174083: true, - 174087: true, - 175045: true, - 175046: true, - 175047: true, - 175048: true, - 176056: true, - 176057: true, - 176058: true, - 176061: true, - 176062: true, - 176063: true, - 176064: true, - 176066: true, - 176067: true, - 176070: true, - 176071: true, - 176076: true, - 178043: true, - 178044: true, - 178046: true, - 178048: true, - 179047: true, - 179055: true, - 179061: true, - 179062: true, - 179063: true, - 179067: true, - 179069: true, - 179070: true, - 179072: true, - 179079: true, - 179088: true, - 179095: true, - 179096: true, - 179097: true, - 179099: true, - 179105: true, - 179106: true, - 179108: true, - 179118: true, - 179120: true, - 179125: true, - 179126: true, - 179128: true, - 179129: true, - 179130: true, - 180044: true, - 180045: true, - 180046: true, - 180047: true, - 180048: true, - 180049: true, - 180050: true, - 180052: true, - 180053: true, - 180054: true, - 180055: true, - 180056: true, - 180057: true, - 180059: true, - 180061: true, - 180064: true, - 180065: true, - 180068: true, - 180069: true, - 180070: true, - 180074: true, - 180075: true, - 180081: true, - 180082: true, - 180085: true, - 180092: true, - 180099: true, - 180105: true, - 180108: true, - 180112: true, - 180118: true, - 181041: true, - 181043: true, - 181044: true, - 181045: true, - 181049: true, - 181050: true, - 181055: true, - 181057: true, - 181058: true, - 181059: true, - 181063: true, - 181071: true, - 181073: true, - 181075: true, - 181077: true, - 181080: true, - 181083: true, - 181084: true, - 181085: true, - 181086: true, - 181087: true, - 181089: true, - 181097: true, - 181099: true, - 181102: true, - 181111: true, - 181130: true, - 181135: true, - 181137: true, - 181138: true, - 181139: true, - 181151: true, - 181152: true, - 181153: true, - 181155: true, - 181156: true, - 181157: true, - 181158: true, - 181160: true, - 181161: true, - 181163: true, - 181164: true, - 181171: true, - 181179: true, - 181183: true, - 181184: true, - 181186: true, - 182041: true, - 182043: true, - 182044: true, - 183042: true, - 183043: true, - 183044: true, - 183047: true, - 183049: true, - 183065: true, - 183066: true, - 183073: true, - 183074: true, - 183075: true, - 183083: true, - 183084: true, - 183087: true, - 183088: true, - 183090: true, - 183095: true, - 183104: true, - 183107: true, - 183109: true, - 183111: true, - 183112: true, - 183113: true, - 183116: true, - 183123: true, - 183124: true, - 183125: true, - 183126: true, - 183132: true, - 183133: true, - 183135: true, - 183136: true, - 183137: true, - 183138: true, - 183139: true, - 183140: true, - 183141: true, - 183142: true, - 183153: true, - 183155: true, - 183156: true, - 183157: true, - 183160: true, - 184043: true, - 184055: true, - 184058: true, - 184059: true, - 184068: true, - 184069: true, - 184079: true, - 184080: true, - 184081: true, - 185043: true, - 185045: true, - 186042: true, - 186043: true, - 186073: true, - 186076: true, - 186077: true, - 186078: true, - 186079: true, - 186081: true, - 186095: true, - 186108: true, - 186113: true, - 186115: true, - 186116: true, - 186118: true, - 186119: true, - 186132: true, - 186137: true, - 186138: true, - 186139: true, - 186143: true, - 186144: true, - 186145: true, - 186146: true, - 186147: true, - 186148: true, - 186159: true, - 186160: true, - 186161: true, - 186165: true, - 186169: true, - 186173: true, - 186180: true, - 186210: true, - 186211: true, - 186212: true, - 186213: true, - 186214: true, - 186215: true, - 186216: true, - 186228: true, - 186229: true, - 186230: true, - 186232: true, - 186234: true, - 186255: true, - 186263: true, - 186276: true, - 186279: true, - 186282: true, - 186283: true, - 188043: true, - 189042: true, - 189057: true, - 189059: true, - 189062: true, - 189078: true, - 189080: true, - 189083: true, - 189088: true, - 189093: true, - 189095: true, - 189096: true, - 189098: true, - 189100: true, - 190041: true, - 190042: true, - 190043: true, - 190044: true, - 190059: true, - 190062: true, - 190068: true, - 190074: true, - 190076: true, - 190077: true, - 190079: true, - 190085: true, - 190088: true, - 190103: true, - 190104: true, - 193055: true, - 193066: true, - 193067: true, - 193070: true, - 193075: true, - 193079: true, - 193080: true, - 193081: true, - 193091: true, - 193092: true, - 193095: true, - 193101: true, - 193104: true, - 194043: true, - 194045: true, - 194046: true, - 194050: true, - 194051: true, - 194052: true, - 194053: true, - 194064: true, - 194066: true, - 194069: true, - 194071: true, - 194072: true, - 194073: true, - 194074: true, - 194076: true, - 194077: true, - 194078: true, - 194082: true, - 194084: true, - 194085: true, - 194090: true, - 194091: true, - 194092: true, - 194094: true, - 194097: true, - 194098: true, - 194099: true, - 194100: true, - 194114: true, - 194116: true, - 194118: true, - 194119: true, - 194120: true, - 194121: true, - 194122: true, - 194126: true, - 194129: true, - 194131: true, - 194132: true, - 194133: true, - 194134: true, - 194146: true, - 194151: true, - 194156: true, - 194157: true, - 194159: true, - 194161: true, - 194165: true, - 195041: true, - 195044: true, - 195050: true, - 195051: true, - 195052: true, - 195068: true, - 195075: true, - 195076: true, - 195079: true, - 195080: true, - 195081: true, - 196042: true, - 196044: true, - 196050: true, - 196051: true, - 196055: true, - 196056: true, - 196061: true, - 196063: true, - 196065: true, - 196070: true, - 196071: true, - 196075: true, - 196077: true, - 196079: true, - 196087: true, - 196088: true, - 196090: true, - 196091: true, - 197041: true, - 197042: true, - 197043: true, - 197044: true, - 198044: true, - 198045: true, - 198046: true, - 198048: true, - 198049: true, - 198050: true, - 198053: true, - 198057: true, - 198058: true, - 198066: true, - 198071: true, - 198074: true, - 198081: true, - 198084: true, - 198085: true, - 198102: true, - 199042: true, - 199044: true, - 199045: true, - 199046: true, - 199047: true, - 199052: true, - 199054: true, - 199057: true, - 199066: true, - 199070: true, - 199082: true, - 199091: true, - 199094: true, - 199096: true, - 201041: true, - 201042: true, - 201043: true, - 201047: true, - 201048: true, - 201049: true, - 201058: true, - 201061: true, - 201064: true, - 201065: true, - 201068: true, - 202042: true, - 202043: true, - 202044: true, - 202051: true, - 202054: true, - 202055: true, - 203043: true, - 203050: true, - 203051: true, - 203053: true, - 203060: true, - 203062: true, - 204042: true, - 204044: true, - 204048: true, - 204052: true, - 204053: true, - 204061: true, - 204062: true, - 204064: true, - 204065: true, - 204067: true, - 204068: true, - 204069: true, - 205042: true, - 205044: true, - 206043: true, - 206044: true, - 206047: true, - 206050: true, - 206051: true, - 206052: true, - 206053: true, - 206054: true, - 206055: true, - 206058: true, - 206059: true, - 206060: true, - 206067: true, - 206069: true, - 206077: true, - 206078: true, - 206079: true, - 206084: true, - 206089: true, - 206101: true, - 206107: true, - 206109: true, - 207043: true, - 207044: true, - 207049: true, - 207050: true, - 207051: true, - 207052: true, - 207053: true, - 207054: true, - 207055: true, - 207061: true, - 207062: true, - 207069: true, - 207071: true, - 207085: true, - 207086: true, - 207087: true, - 207088: true, - 207095: true, - 207096: true, - 207102: true, - 207103: true, - 207106: true, - 207108: true, - 207110: true, - 207111: true, - 207112: true, - 209041: true, - 209042: true, - 209043: true, - 209044: true, - 210042: true, - 210043: true, - 210044: true, - 210047: true, - 211041: true, - 212041: true, - 212045: true, - 212046: true, - 212047: true, - 213041: true, - 213042: true, - 214042: true, - 214046: true, - 214049: true, - 214050: true, - 215042: true, - 215048: true, - 215050: true, - 216043: true, - 216046: true, - 216047: true, - 216052: true, - 216053: true, - 216054: true, - 216059: true, - 216068: true, - 217041: true, - 217044: true, - 217047: true, - 217048: true, - 217049: true, - 217056: true, - 217058: true, - 217059: true, - 217060: true, - 217061: true, - 217064: true, - 217066: true, - 217069: true, - 217071: true, - 217085: true, - 217086: true, - 217088: true, - 217093: true, - 217094: true, - 217108: true, - 217109: true, - 217111: true, - 217115: true, - 217116: true, - 218042: true, - 218044: true, - 218046: true, - 218050: true, - 218060: true, - 218061: true, - 218063: true, - 218064: true, - 218065: true, - 218070: true, - 218071: true, - 218072: true, - 218074: true, - 218076: true, - 222041: true, - 223041: true, - 223043: true, - 223044: true, - 223050: true, - 223052: true, - 223054: true, - 223058: true, - 223059: true, - 223061: true, - 223068: true, - 223069: true, - 223070: true, - 223071: true, - 223073: true, - 223075: true, - 223076: true, - 223083: true, - 223087: true, - 223094: true, - 223096: true, - 223101: true, - 223106: true, - 223108: true, - 224041: true, - 224042: true, - 224043: true, - 224045: true, - 224051: true, - 224053: true, - 224057: true, - 224060: true, - 224061: true, - 224062: true, - 224063: true, - 224068: true, - 224069: true, - 224081: true, - 224084: true, - 224087: true, - 224090: true, - 224096: true, - 224105: true, - 225042: true, - 227041: true, - 229045: true, - 229046: true, - 229048: true, - 229049: true, - 229050: true, - 231042: true, - 236041: true, - 237041: true, - 238041: true, - 238042: true, - 240041: true, - 240042: true, - 240043: true, - 241041: true, - 243041: true, - 244041: true, - 245041: true, - 247041: true, - 250041: true, - 252041: true, - 253041: true, - 253045: true, - 254043: true, - 255042: true, - 255043: true, - 257041: true, - 257042: true, - 258041: true, - 261041: true, - 264041: true, - 294042: true, - 296042: true, -} From e55fb40e67ba08fdaebb2e477544eb560f198210 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 15 Aug 2022 18:41:45 -0400 Subject: [PATCH 195/723] internal/lsp/cache: clear shouldLoad IDs on load CL 417576 externalized shouldLoad tracking into a map, which was used to trigger a reload and cleared once reload completes. Unfortunately, it overlooked the fact that we may also reload the entire workspace (via reinitialization). In this case, we should clear newly loaded IDs from the shouldLoad map, so that they are not subsequently loaded again. Fixes golang/go#54473 Change-Id: I26f49552cae502644142dc4a4e946294db37f6f7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/424074 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- .../regtest/workspace/workspace_test.go | 25 +++++++++++++++++++ internal/lsp/cache/load.go | 4 +++ internal/lsp/regtest/expectation.go | 6 ++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 3e980de95d8..5196aa90966 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -156,6 +156,31 @@ func TestClearAnalysisDiagnostics(t *testing.T) { }) } +// TestReloadOnlyOnce checks that changes to the go.mod file do not result in +// redundant package loads (golang/go#54473). +// +// Note that this test may be fragile, as it depends on specific structure to +// log messages around reinitialization. Nevertheless, it is important for +// guarding against accidentally duplicate reloading. +func TestReloadOnlyOnce(t *testing.T) { + WithOptions( + ProxyFiles(workspaceProxy), + WorkspaceFolders("pkg"), + ).Run(t, workspaceModule, func(t *testing.T, env *Env) { + dir := env.Sandbox.Workdir.URI("goodbye").SpanURI().Filename() + goModWithReplace := fmt.Sprintf(`%s +replace random.org => %s +`, env.ReadWorkspaceFile("pkg/go.mod"), dir) + env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace) + env.Await( + OnceMet( + env.DoneWithChangeWatchedFiles(), + LogMatching(protocol.Info, `packages\.Load #\d+\n`, 2, false), + ), + ) + }) +} + // This test checks that gopls updates the set of files it watches when a // replace target is added to the go.mod. func TestWatchReplaceTargets(t *testing.T) { diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 6d9c598606b..2bc9277a026 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -126,11 +126,14 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf if ctx.Err() != nil { return ctx.Err() } + + // This log message is sought for by TestReloadOnlyOnce. if err != nil { event.Error(ctx, eventName, err, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) } else { event.Log(ctx, eventName, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) } + if len(pkgs) == 0 { if err == nil { err = fmt.Errorf("no packages returned") @@ -210,6 +213,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf if existing := s.meta.metadata[m.ID]; existing == nil || !existing.Valid { updates[m.ID] = m updatedIDs = append(updatedIDs, m.ID) + delete(s.shouldLoad, m.ID) } } diff --git a/internal/lsp/regtest/expectation.go b/internal/lsp/regtest/expectation.go index eb88a4c8584..e85f40955d4 100644 --- a/internal/lsp/regtest/expectation.go +++ b/internal/lsp/regtest/expectation.go @@ -337,7 +337,11 @@ func NoErrorLogs() LogExpectation { } // LogMatching asserts that the client has received a log message -// of type typ matching the regexp re. +// of type typ matching the regexp re a certain number of times. +// +// The count argument specifies the expected number of matching logs. If +// atLeast is set, this is a lower bound, otherwise there must be exactly cound +// matching logs. func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) LogExpectation { rec, err := regexp.Compile(re) if err != nil { From 587a15310bddfc939d37cfaa8be8ea4c3808c3f1 Mon Sep 17 00:00:00 2001 From: pjw Date: Mon, 8 Aug 2022 07:57:28 -0400 Subject: [PATCH 196/723] internal/lsp: hover to render go 1.19 doc comments Go 1.19 introduced new formatting for doc comments, and a new package for processing them. This change uses the new package when gopls is compiled with go 1.19 or later. The difficulty is with the hover tests, which have to work both when gopls is compiled with earlier versions of go, and with go 1.19. Fortunately the changes in formatting the test cases are easily checked. Fixes golang/go#54260 Change-Id: I9e8e7f0cf3392afa0865b5d3f4e5fcdd88dfe75f Reviewed-on: https://go-review.googlesource.com/c/tools/+/421502 Run-TryBot: Peter Weinberger gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/cache/debug.go | 2 +- internal/lsp/cmd/test/definition.go | 8 +- internal/lsp/lsp_test.go | 2 +- internal/lsp/source/comment.go | 5 +- internal/lsp/source/comment_go118.go | 37 ++++++ ...{comment_test.go => comment_go118_test.go} | 3 + internal/lsp/source/hover.go | 6 +- internal/lsp/source/source_test.go | 3 +- internal/lsp/testdata/godef/a/a.go.golden | 39 ++++-- internal/lsp/testdata/godef/a/d.go.golden | 19 +-- internal/lsp/testdata/godef/a/g.go.golden | 3 +- internal/lsp/testdata/godef/a/h.go.golden | 22 ++++ .../lsp/testdata/godef/a/random.go.golden | 7 +- internal/lsp/testdata/godef/b/b.go.golden | 70 +++++++---- internal/lsp/testdata/godef/b/c.go.golden | 8 +- internal/lsp/testdata/godef/b/e.go.golden | 16 ++- internal/lsp/testdata/godef/b/h.go.golden | 3 +- .../godef/broken/unclosedIf.go.golden | 7 +- .../godef/hover_generics/hover.go.golden | 6 +- internal/lsp/tests/metadata_go118.go | 116 ++++++++++++++++++ internal/lsp/tests/metadata_go119.go | 23 ++++ 21 files changed, 325 insertions(+), 80 deletions(-) create mode 100644 internal/lsp/source/comment_go118.go rename internal/lsp/source/{comment_test.go => comment_go118_test.go} (99%) create mode 100644 internal/lsp/tests/metadata_go118.go create mode 100644 internal/lsp/tests/metadata_go119.go diff --git a/internal/lsp/cache/debug.go b/internal/lsp/cache/debug.go index ca8b7c866e4..d665b011daf 100644 --- a/internal/lsp/cache/debug.go +++ b/internal/lsp/cache/debug.go @@ -23,7 +23,7 @@ func debugf(format string, args ...interface{}) { return } if false { - fmt.Sprintf(format, args...) // encourage vet to validate format strings + _ = fmt.Sprintf(format, args...) // encourage vet to validate format strings } fmt.Fprintf(os.Stderr, ">>> "+format+"\n", args...) } diff --git a/internal/lsp/cmd/test/definition.go b/internal/lsp/cmd/test/definition.go index c82d9a6c1ae..9c49eaa7d58 100644 --- a/internal/lsp/cmd/test/definition.go +++ b/internal/lsp/cmd/test/definition.go @@ -10,8 +10,6 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" "golang.org/x/tools/internal/lsp/tests" "golang.org/x/tools/internal/span" ) @@ -51,11 +49,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { return []byte(got), nil }))) if expect != "" && !strings.HasPrefix(got, expect) { - d, err := myers.ComputeEdits("", expect, got) - if err != nil { - t.Fatal(err) - } - t.Errorf("definition %v failed with %#v\n%s", tag, args, diff.ToUnified("expect", "got", expect, d)) + tests.CheckSameMarkdown(t, got, expect) } } } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 53890dc616b..5d37c56a8ea 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -739,7 +739,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { got := tests.StripSubscripts(hover.Contents.Value) expectHover = tests.StripSubscripts(expectHover) if got != expectHover { - t.Errorf("%s:\n%s", d.Src, tests.Diff(t, expectHover, got)) + tests.CheckSameMarkdown(t, got, expectHover) } } if !d.OnlyHover { diff --git a/internal/lsp/source/comment.go b/internal/lsp/source/comment.go index 000d6136c80..ff6d11f4ff7 100644 --- a/internal/lsp/source/comment.go +++ b/internal/lsp/source/comment.go @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !go1.19 +// +build !go1.19 + package source import ( @@ -226,7 +229,7 @@ func unindent(block []string) { prefix := block[0][0:indentLen(block[0])] for _, line := range block { if !isBlank(line) { - prefix = commonPrefix(prefix, line[0:indentLen(line)]) + prefix = commonPrefix(prefix, line) } } n := len(prefix) diff --git a/internal/lsp/source/comment_go118.go b/internal/lsp/source/comment_go118.go new file mode 100644 index 00000000000..0503670d3bf --- /dev/null +++ b/internal/lsp/source/comment_go118.go @@ -0,0 +1,37 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package source + +// Starting with go1.19, the formatting of comments has changed, and there +// is a new package (go/doc/comment) for processing them. +// As long as gopls has to compile under earlier versions, tests +// have to pass with both the old and new code, which produce +// slightly different results. (cmd/test/definition.go, source/comment_test.go, +// and source/source_test.go) Each of the test files checks the results +// with a function, tests.CheckSameMarkdown, that accepts both the old and the new +// results. (The old code escapes many characters the new code does not, +// and the new code sometimes adds a blank line.) + +// When gopls no longer needs to compile with go1.18, the old comment.go should +// be replaced by this file, the golden test files should be updated. +// (and checkSameMarkdown() could be replaced by a simple comparison.) + +import "go/doc/comment" + +// CommentToMarkdown converts comment text to formatted markdown. +// The comment was prepared by DocReader, +// so it is known not to have leading, trailing blank lines +// nor to have trailing spaces at the end of lines. +// The comment markers have already been removed. +func CommentToMarkdown(text string) string { + var p comment.Parser + doc := p.Parse(text) + var pr comment.Printer + easy := pr.Markdown(doc) + return string(easy) +} diff --git a/internal/lsp/source/comment_test.go b/internal/lsp/source/comment_go118_test.go similarity index 99% rename from internal/lsp/source/comment_test.go rename to internal/lsp/source/comment_go118_test.go index 9efde16ef3c..b48b2e753ce 100644 --- a/internal/lsp/source/comment_test.go +++ b/internal/lsp/source/comment_go118_test.go @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !go1.19 +// +build !go1.19 + package source import ( diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go index d2f80c793be..6ad458ba4cb 100644 --- a/internal/lsp/source/hover.go +++ b/internal/lsp/source/hover.go @@ -797,12 +797,8 @@ func FormatHover(h *HoverJSON, options *Options) (string, error) { if el != "" { b.WriteString(el) - // Don't write out final newline. - if i == len(parts) { - continue - } // If any elements of the remainder of the list are non-empty, - // write a newline. + // write an extra newline. if anyNonEmpty(parts[i+1:]) { if options.PreferredContentFormat == protocol.Markdown { b.WriteString("\n\n") diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index 5fdcc0f86ea..177b85d13af 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -584,7 +584,8 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { hover = tests.StripSubscripts(hover) expectHover = tests.StripSubscripts(expectHover) if hover != expectHover { - t.Errorf("hoverdef for %s failed:\n%s", d.Src, tests.Diff(t, expectHover, hover)) + tests.CheckSameMarkdown(t, hover, expectHover) + } } if !d.OnlyHover { diff --git a/internal/lsp/testdata/godef/a/a.go.golden b/internal/lsp/testdata/godef/a/a.go.golden index 819262672a0..7f114e54b06 100644 --- a/internal/lsp/testdata/godef/a/a.go.golden +++ b/internal/lsp/testdata/godef/a/a.go.golden @@ -3,7 +3,8 @@ func (*sync.Mutex).Lock() ``` -Lock locks m\. +Lock locks m. + [`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock) -- Name-hoverdef -- @@ -11,7 +12,8 @@ Lock locks m\. func (*types.object).Name() string ``` -Name returns the object\'s \(package\-local, unqualified\) name\. +Name returns the object's (package-local, unqualified) name. + [`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name) -- Random-definition -- @@ -75,7 +77,8 @@ func Random2(y int) int [`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2) -- aPackage-hoverdef -- -Package a is a package for testing go to definition\. +Package a is a package for testing go to definition. + -- declBlockA-hoverdef -- ```go type a struct { @@ -84,12 +87,14 @@ type a struct { ``` 1st type declaration block + -- declBlockB-hoverdef -- ```go type b struct{} ``` b has a comment + -- declBlockC-hoverdef -- ```go type c struct { @@ -98,12 +103,14 @@ type c struct { ``` c is a struct + -- declBlockD-hoverdef -- ```go type d string ``` 3rd type declaration block + -- declBlockE-hoverdef -- ```go type e struct { @@ -112,12 +119,13 @@ type e struct { ``` e has a comment + -- err-definition -- godef/a/a.go:33:6-9: defined here as ```go var err error ``` -\@err +@err -- err-definition-json -- { "span": { @@ -133,7 +141,7 @@ var err error "offset": 615 } }, - "description": "```go\nvar err error\n```\n\n\\@err" + "description": "```go\nvar err error\n```\n\n@err" } -- err-hoverdef -- @@ -141,25 +149,29 @@ var err error var err error ``` -\@err +@err + -- g-hoverdef -- ```go const g untyped int = 1 ``` -When I hover on g, I should see this comment\. +When I hover on g, I should see this comment. + -- h-hoverdef -- ```go const h untyped int = 2 ``` -Constant block\. +Constant block. + -- make-hoverdef -- ```go func make(t Type, size ...int) Type ``` -The make built\-in function allocates and initializes an object of type slice, map, or chan \(only\)\. +The make built-in function allocates and initializes an object of type slice, map, or chan (only). + [`make` on pkg.go.dev](https://pkg.go.dev/builtin#make) -- string-hoverdef -- @@ -167,7 +179,8 @@ The make built\-in function allocates and initializes an object of type slice, m type string string ``` -string is the set of all strings of 8\-bit bytes, conventionally but not necessarily representing UTF\-8\-encoded text\. +string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. + [`string` on pkg.go.dev](https://pkg.go.dev/builtin#string) -- typesImport-hoverdef -- @@ -181,10 +194,12 @@ package types ("go/types") var x string ``` -x is a variable\. +x is a variable. + -- z-hoverdef -- ```go var z string ``` -z is a variable too\. +z is a variable too. + diff --git a/internal/lsp/testdata/godef/a/d.go.golden b/internal/lsp/testdata/godef/a/d.go.golden index 7577b556b8d..9f04317c00c 100644 --- a/internal/lsp/testdata/godef/a/d.go.golden +++ b/internal/lsp/testdata/godef/a/d.go.golden @@ -3,7 +3,8 @@ godef/a/d.go:6:2-8: defined here as ```go field Member string ``` -\@Member +@Member + [`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) -- Member-definition-json -- @@ -21,7 +22,7 @@ field Member string "offset": 96 } }, - "description": "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)" + "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)" } -- Member-hoverdef -- @@ -29,7 +30,8 @@ field Member string field Member string ``` -\@Member +@Member + [`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) -- Method-definition -- @@ -67,7 +69,8 @@ godef/a/d.go:9:5-10: defined here as ```go var Other Thing ``` -\@Other +@Other + [`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) -- Other-definition-json -- @@ -85,7 +88,7 @@ var Other Thing "offset": 126 } }, - "description": "```go\nvar Other Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)" + "description": "```go\nvar Other Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)" } -- Other-hoverdef -- @@ -93,7 +96,8 @@ var Other Thing var Other Thing ``` -\@Other +@Other + [`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) -- Thing-definition -- @@ -161,4 +165,5 @@ func Things(val []string) []Thing [`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things) -- a-hoverdef -- -Package a is a package for testing go to definition\. +Package a is a package for testing go to definition. + diff --git a/internal/lsp/testdata/godef/a/g.go.golden b/internal/lsp/testdata/godef/a/g.go.golden index b7ed7392806..f7a2e1b0775 100644 --- a/internal/lsp/testdata/godef/a/g.go.golden +++ b/internal/lsp/testdata/godef/a/g.go.golden @@ -3,4 +3,5 @@ const dur time.Duration = 910350000000 // 15m10.35s ``` -dur is a constant of type time\.Duration\. +dur is a constant of type time.Duration. + diff --git a/internal/lsp/testdata/godef/a/h.go.golden b/internal/lsp/testdata/godef/a/h.go.golden index 4b27211e9aa..295876647ab 100644 --- a/internal/lsp/testdata/godef/a/h.go.golden +++ b/internal/lsp/testdata/godef/a/h.go.golden @@ -4,42 +4,49 @@ field d int ``` d field + -- arrE-hoverdef -- ```go field e struct{f int} ``` e nested struct + -- arrF-hoverdef -- ```go field f int ``` f field of nested struct + -- complexH-hoverdef -- ```go field h int ``` h field + -- complexI-hoverdef -- ```go field i struct{j int} ``` i nested struct + -- complexJ-hoverdef -- ```go field j int ``` j field of nested struct + -- mapStructKeyX-hoverdef -- ```go field x []string ``` X key field + -- mapStructKeyY-hoverdef -- ```go field y string @@ -50,87 +57,102 @@ field x string ``` X value field + -- nestedMap-hoverdef -- ```go field m map[string]float64 ``` nested map + -- nestedNumber-hoverdef -- ```go field number int64 ``` nested number + -- nestedString-hoverdef -- ```go field str string ``` nested string + -- openMethod-hoverdef -- ```go func (interface).open() error ``` open method comment + -- returnX-hoverdef -- ```go field x int ``` X coord + -- returnY-hoverdef -- ```go field y int ``` Y coord + -- structA-hoverdef -- ```go field a int ``` a field + -- structB-hoverdef -- ```go field b struct{c int} ``` b nested struct + -- structC-hoverdef -- ```go field c int ``` c field of nested struct + -- testDescription-hoverdef -- ```go field desc string ``` test description + -- testInput-hoverdef -- ```go field in map[string][]struct{key string; value interface{}} ``` test input + -- testInputKey-hoverdef -- ```go field key string ``` test key + -- testInputValue-hoverdef -- ```go field value interface{} ``` test value + -- testResultValue-hoverdef -- ```go field value int ``` expected test value + diff --git a/internal/lsp/testdata/godef/a/random.go.golden b/internal/lsp/testdata/godef/a/random.go.golden index e8b180a4b43..eb70a515f11 100644 --- a/internal/lsp/testdata/godef/a/random.go.golden +++ b/internal/lsp/testdata/godef/a/random.go.golden @@ -33,7 +33,7 @@ godef/a/random.go:13:2-3: defined here as ```go field x int ``` -\@mark\(PosX, \"x\"\),mark\(PosY, \"y\"\) +@mark(PosX, "x"),mark(PosY, "y") -- PosX-definition-json -- { "span": { @@ -49,7 +49,7 @@ field x int "offset": 188 } }, - "description": "```go\nfield x int\n```\n\n\\@mark\\(PosX, \\\"x\\\"\\),mark\\(PosY, \\\"y\\\"\\)" + "description": "```go\nfield x int\n```\n\n@mark(PosX, \"x\"),mark(PosY, \"y\")" } -- PosX-hoverdef -- @@ -57,7 +57,8 @@ field x int field x int ``` -\@mark\(PosX, \"x\"\),mark\(PosY, \"y\"\) +@mark(PosX, "x"),mark(PosY, "y") + -- RandomParamY-definition -- godef/a/random.go:8:14-15: defined here as ```go var y int diff --git a/internal/lsp/testdata/godef/b/b.go.golden b/internal/lsp/testdata/godef/b/b.go.golden index a30b3f70b55..073d633ce3e 100644 --- a/internal/lsp/testdata/godef/b/b.go.golden +++ b/internal/lsp/testdata/godef/b/b.go.golden @@ -3,7 +3,8 @@ func (a.I).B() ``` -\@mark\(AB, \"B\"\) +@mark(AB, "B") + [`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#I.B) -- AField-hoverdef -- @@ -11,7 +12,8 @@ func (a.I).B() field Field int ``` -\@mark\(AField, \"Field\"\) +@mark(AField, "Field") + [`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#S.Field) -- AField2-hoverdef -- @@ -19,7 +21,8 @@ field Field int field Field2 int ``` -\@mark\(AField2, \"Field2\"\) +@mark(AField2, "Field2") + [`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#R.Field2) -- AGoodbye-hoverdef -- @@ -27,7 +30,8 @@ field Field2 int func (a.H).Goodbye() ``` -\@mark\(AGoodbye, \"Goodbye\"\) +@mark(AGoodbye, "Goodbye") + [`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#H.Goodbye) -- AHello-hoverdef -- @@ -35,7 +39,8 @@ func (a.H).Goodbye() func (a.J).Hello() ``` -\@mark\(AHello, \"Hello\"\) +@mark(AHello, "Hello") + [`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#J.Hello) -- AHey-hoverdef -- @@ -85,7 +90,8 @@ godef/a/a.go:26:6-7: defined here as ```go type A string ``` -\@mark\(AString, \"A\"\) +@mark(AString, "A") + [`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A) -- AString-definition-json -- @@ -103,7 +109,7 @@ type A string "offset": 468 } }, - "description": "```go\ntype A string\n```\n\n\\@mark\\(AString, \\\"A\\\"\\)\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)" + "description": "```go\ntype A string\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)" } -- AString-hoverdef -- @@ -111,7 +117,8 @@ type A string type A string ``` -\@mark\(AString, \"A\"\) +@mark(AString, "A") + [`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A) -- AStuff-definition -- @@ -189,7 +196,8 @@ godef/b/b.go:28:2-4: defined here as ```go field F1 int ``` -\@mark\(S1F1, \"F1\"\) +@mark(S1F1, "F1") + [`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) -- S1F1-definition-json -- @@ -207,7 +215,7 @@ field F1 int "offset": 608 } }, - "description": "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)" + "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)" } -- S1F1-hoverdef -- @@ -215,7 +223,8 @@ field F1 int field F1 int ``` -\@mark\(S1F1, \"F1\"\) +@mark(S1F1, "F1") + [`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) -- S1S2-definition -- @@ -223,7 +232,8 @@ godef/b/b.go:29:2-4: defined here as ```go field S2 S2 ``` -\@godef\(\"S2\", S2\),mark\(S1S2, \"S2\"\) +@godef("S2", S2),mark(S1S2, "S2") + [`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2) -- S1S2-definition-json -- @@ -241,7 +251,7 @@ field S2 S2 "offset": 640 } }, - "description": "```go\nfield S2 S2\n```\n\n\\@godef\\(\\\"S2\\\", S2\\),mark\\(S1S2, \\\"S2\\\"\\)\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)" + "description": "```go\nfield S2 S2\n```\n\n@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)" } -- S1S2-hoverdef -- @@ -249,7 +259,8 @@ field S2 S2 field S2 S2 ``` -\@godef\(\"S2\", S2\),mark\(S1S2, \"S2\"\) +@godef("S2", S2),mark(S1S2, "S2") + [`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2) -- S2-definition -- @@ -295,7 +306,8 @@ godef/b/b.go:35:2-4: defined here as ```go field F1 string ``` -\@mark\(S2F1, \"F1\"\) +@mark(S2F1, "F1") + [`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1) -- S2F1-definition-json -- @@ -313,7 +325,7 @@ field F1 string "offset": 783 } }, - "description": "```go\nfield F1 string\n```\n\n\\@mark\\(S2F1, \\\"F1\\\"\\)\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)" + "description": "```go\nfield F1 string\n```\n\n@mark(S2F1, \"F1\")\n\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)" } -- S2F1-hoverdef -- @@ -321,7 +333,8 @@ field F1 string field F1 string ``` -\@mark\(S2F1, \"F1\"\) +@mark(S2F1, "F1") + [`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1) -- S2F2-definition -- @@ -329,7 +342,8 @@ godef/b/b.go:36:2-4: defined here as ```go field F2 int ``` -\@mark\(S2F2, \"F2\"\) +@mark(S2F2, "F2") + [`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2) -- S2F2-definition-json -- @@ -347,7 +361,7 @@ field F2 int "offset": 816 } }, - "description": "```go\nfield F2 int\n```\n\n\\@mark\\(S2F2, \\\"F2\\\"\\)\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)" + "description": "```go\nfield F2 int\n```\n\n@mark(S2F2, \"F2\")\n\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)" } -- S2F2-hoverdef -- @@ -355,7 +369,8 @@ field F2 int field F2 int ``` -\@mark\(S2F2, \"F2\"\) +@mark(S2F2, "F2") + [`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2) -- aAlias-definition -- @@ -363,7 +378,7 @@ godef/b/b.go:25:6-12: defined here as ```go type aAlias = a.A ``` -\@mark\(aAlias, \"aAlias\"\) +@mark(aAlias, "aAlias") -- aAlias-definition-json -- { "span": { @@ -379,7 +394,7 @@ type aAlias = a.A "offset": 548 } }, - "description": "```go\ntype aAlias = a.A\n```\n\n\\@mark\\(aAlias, \\\"aAlias\\\"\\)" + "description": "```go\ntype aAlias = a.A\n```\n\n@mark(aAlias, \"aAlias\")" } -- aAlias-hoverdef -- @@ -387,13 +402,15 @@ type aAlias = a.A type aAlias = a.A ``` -\@mark\(aAlias, \"aAlias\"\) +@mark(aAlias, "aAlias") + -- bX-definition -- godef/b/b.go:57:7-8: defined here as ```go const X untyped int = 0 ``` -\@mark\(bX, \"X\"\),godef\(\"X\", bX\) +@mark(bX, "X"),godef("X", bX) + [`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X) -- bX-definition-json -- @@ -411,7 +428,7 @@ const X untyped int = 0 "offset": 1250 } }, - "description": "```go\nconst X untyped int = 0\n```\n\n\\@mark\\(bX, \\\"X\\\"\\),godef\\(\\\"X\\\", bX\\)\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)" + "description": "```go\nconst X untyped int = 0\n```\n\n@mark(bX, \"X\"),godef(\"X\", bX)\n\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)" } -- bX-hoverdef -- @@ -419,7 +436,8 @@ const X untyped int = 0 const X untyped int = 0 ``` -\@mark\(bX, \"X\"\),godef\(\"X\", bX\) +@mark(bX, "X"),godef("X", bX) + [`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X) -- myFoo-definition -- diff --git a/internal/lsp/testdata/godef/b/c.go.golden b/internal/lsp/testdata/godef/b/c.go.golden index fddf3d273ce..a48dbd2b881 100644 --- a/internal/lsp/testdata/godef/b/c.go.golden +++ b/internal/lsp/testdata/godef/b/c.go.golden @@ -43,7 +43,8 @@ godef/b/b.go:28:2-4: defined here as ```go field F1 int ``` -\@mark\(S1F1, \"F1\"\) +@mark(S1F1, "F1") + [`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) -- S1F1-definition-json -- @@ -61,7 +62,7 @@ field F1 int "offset": 608 } }, - "description": "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)" + "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)" } -- S1F1-hoverdef -- @@ -69,6 +70,7 @@ field F1 int field F1 int ``` -\@mark\(S1F1, \"F1\"\) +@mark(S1F1, "F1") + [`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) diff --git a/internal/lsp/testdata/godef/b/e.go.golden b/internal/lsp/testdata/godef/b/e.go.golden index 5a26caa4fe6..d03b6728a4f 100644 --- a/internal/lsp/testdata/godef/b/e.go.golden +++ b/internal/lsp/testdata/godef/b/e.go.golden @@ -3,7 +3,8 @@ godef/a/d.go:6:2-8: defined here as ```go field Member string ``` -\@Member +@Member + [`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) -- Member-definition-json -- @@ -21,7 +22,7 @@ field Member string "offset": 96 } }, - "description": "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)" + "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)" } -- Member-hoverdef -- @@ -29,7 +30,8 @@ field Member string field Member string ``` -\@Member +@Member + [`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) -- Other-definition -- @@ -37,7 +39,8 @@ godef/a/d.go:9:5-10: defined here as ```go var a.Other a.Thing ``` -\@Other +@Other + [`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) -- Other-definition-json -- @@ -55,7 +58,7 @@ var a.Other a.Thing "offset": 126 } }, - "description": "```go\nvar a.Other a.Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)" + "description": "```go\nvar a.Other a.Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)" } -- Other-hoverdef -- @@ -63,7 +66,8 @@ var a.Other a.Thing var a.Other a.Thing ``` -\@Other +@Other + [`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) -- Thing-definition -- diff --git a/internal/lsp/testdata/godef/b/h.go.golden b/internal/lsp/testdata/godef/b/h.go.golden index 73593c63da2..08a15395e92 100644 --- a/internal/lsp/testdata/godef/b/h.go.golden +++ b/internal/lsp/testdata/godef/b/h.go.golden @@ -9,4 +9,5 @@ func AStuff() var _ A ``` -variable of type a\.A +variable of type a.A + diff --git a/internal/lsp/testdata/godef/broken/unclosedIf.go.golden b/internal/lsp/testdata/godef/broken/unclosedIf.go.golden index 5c3329d8b67..9ce869848cb 100644 --- a/internal/lsp/testdata/godef/broken/unclosedIf.go.golden +++ b/internal/lsp/testdata/godef/broken/unclosedIf.go.golden @@ -3,7 +3,7 @@ godef/broken/unclosedIf.go:7:7-19: defined here as ```go var myUnclosedIf string ``` -\@myUnclosedIf +@myUnclosedIf -- myUnclosedIf-definition-json -- { "span": { @@ -19,7 +19,7 @@ var myUnclosedIf string "offset": 80 } }, - "description": "```go\nvar myUnclosedIf string\n```\n\n\\@myUnclosedIf" + "description": "```go\nvar myUnclosedIf string\n```\n\n@myUnclosedIf" } -- myUnclosedIf-hoverdef -- @@ -27,4 +27,5 @@ var myUnclosedIf string var myUnclosedIf string ``` -\@myUnclosedIf +@myUnclosedIf + diff --git a/internal/lsp/testdata/godef/hover_generics/hover.go.golden b/internal/lsp/testdata/godef/hover_generics/hover.go.golden index 91981a11779..dad9d343bdf 100644 --- a/internal/lsp/testdata/godef/hover_generics/hover.go.golden +++ b/internal/lsp/testdata/godef/hover_generics/hover.go.golden @@ -11,7 +11,8 @@ type parameter P interface{~int|string} field Q int ``` -\@mark\(ValueQfield, \"Q\"\),hoverdef\(\"Q\", ValueQfield\) +@mark(ValueQfield, "Q"),hoverdef("Q", ValueQfield) + [`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/hover_generics#Value.Q) -- ValueTdecl-hoverdef -- @@ -34,7 +35,8 @@ type value[T any] struct { field Q int ``` -\@mark\(valueQfield, \"Q\"\),hoverdef\(\"Q\", valueQfield\) +@mark(valueQfield, "Q"),hoverdef("Q", valueQfield) + -- valueTdecl-hoverdef -- ```go type parameter T any diff --git a/internal/lsp/tests/metadata_go118.go b/internal/lsp/tests/metadata_go118.go new file mode 100644 index 00000000000..3d9748b4e31 --- /dev/null +++ b/internal/lsp/tests/metadata_go118.go @@ -0,0 +1,116 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.19 +// +build !go1.19 + +package tests + +import ( + "testing" +) + +// The markdown in the golden files matches the converter in comment.go, +// but for go1.19 and later the conversion is done using go/doc/comment. +// Compared to the newer version, the older version +// has extra escapes, and treats code blocks slightly differently. +func CheckSameMarkdown(t *testing.T, got, want string) { + t.Helper() + for _, dd := range markDiffs { + if got == dd.v18 && want == dd.v19 { + return + } + } + t.Errorf("got %q want %q", got, want) +} + +type markDiff struct { + v18, v19 string +} + +var markDiffs = []markDiff{{v19: "Package a is a package for testing go to definition.\n", v18: "Package a is a package for testing go to definition\\."}, + {v19: "```go\nconst X untyped int = 0\n```\n\n@mark(bX, \"X\"),godef(\"X\", bX)\n\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)", v18: "```go\nconst X untyped int = 0\n```\n\n\\@mark\\(bX, \\\"X\\\"\\),godef\\(\\\"X\\\", bX\\)\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)"}, + {v19: "```go\nconst dur time.Duration = 910350000000 // 15m10.35s\n```\n\ndur is a constant of type time.Duration.\n", v18: "```go\nconst dur time.Duration = 910350000000 // 15m10.35s\n```\n\ndur is a constant of type time\\.Duration\\."}, + {v19: "```go\nconst g untyped int = 1\n```\n\nWhen I hover on g, I should see this comment.\n", v18: "```go\nconst g untyped int = 1\n```\n\nWhen I hover on g, I should see this comment\\."}, + {v19: "```go\nconst h untyped int = 2\n```\n\nConstant block.\n", v18: "```go\nconst h untyped int = 2\n```\n\nConstant block\\."}, + {v19: "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)", v18: "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)"}, + {v19: "```go\nfield F1 string\n```\n\n@mark(S2F1, \"F1\")\n\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)", v18: "```go\nfield F1 string\n```\n\n\\@mark\\(S2F1, \\\"F1\\\"\\)\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)"}, + {v19: "```go\nfield F2 int\n```\n\n@mark(S2F2, \"F2\")\n\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)", v18: "```go\nfield F2 int\n```\n\n\\@mark\\(S2F2, \\\"F2\\\"\\)\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)"}, + {v19: "```go\nfield Field int\n```\n\n@mark(AField, \"Field\")\n\n\n[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#S.Field)", v18: "```go\nfield Field int\n```\n\n\\@mark\\(AField, \\\"Field\\\"\\)\n\n[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#S.Field)"}, + {v19: "```go\nfield Field2 int\n```\n\n@mark(AField2, \"Field2\")\n\n\n[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#R.Field2)", v18: "```go\nfield Field2 int\n```\n\n\\@mark\\(AField2, \\\"Field2\\\"\\)\n\n[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#R.Field2)"}, + {v19: "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)", v18: "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)"}, + {v19: "```go\nfield Q int\n```\n\n@mark(ValueQfield, \"Q\"),hoverdef(\"Q\", ValueQfield)\n\n\n[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/hover_generics#Value.Q)", v18: "```go\nfield Q int\n```\n\n\\@mark\\(ValueQfield, \\\"Q\\\"\\),hoverdef\\(\\\"Q\\\", ValueQfield\\)\n\n[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/hover_generics#Value.Q)"}, + {v19: "```go\nfield Q int\n```\n\n@mark(valueQfield, \"Q\"),hoverdef(\"Q\", valueQfield)\n", v18: "```go\nfield Q int\n```\n\n\\@mark\\(valueQfield, \\\"Q\\\"\\),hoverdef\\(\\\"Q\\\", valueQfield\\)"}, + {v19: "```go\nfield S2 S2\n```\n\n@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)", v18: "```go\nfield S2 S2\n```\n\n\\@godef\\(\\\"S2\\\", S2\\),mark\\(S1S2, \\\"S2\\\"\\)\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)"}, + {v19: "```go\nfield a int\n```\n\na field\n", v18: "```go\nfield a int\n```\n\na field"}, + {v19: "```go\nfield b struct{c int}\n```\n\nb nested struct\n", v18: "```go\nfield b struct{c int}\n```\n\nb nested struct"}, + {v19: "```go\nfield c int\n```\n\nc field of nested struct\n", v18: "```go\nfield c int\n```\n\nc field of nested struct"}, + {v19: "```go\nfield d int\n```\n\nd field\n", v18: "```go\nfield d int\n```\n\nd field"}, + {v19: "```go\nfield desc string\n```\n\ntest description\n", v18: "```go\nfield desc string\n```\n\ntest description"}, + {v19: "```go\nfield e struct{f int}\n```\n\ne nested struct\n", v18: "```go\nfield e struct{f int}\n```\n\ne nested struct"}, + {v19: "```go\nfield f int\n```\n\nf field of nested struct\n", v18: "```go\nfield f int\n```\n\nf field of nested struct"}, + {v19: "```go\nfield h int\n```\n\nh field\n", v18: "```go\nfield h int\n```\n\nh field"}, + {v19: "```go\nfield i struct{j int}\n```\n\ni nested struct\n", v18: "```go\nfield i struct{j int}\n```\n\ni nested struct"}, + {v19: "```go\nfield in map[string][]struct{key string; value interface{}}\n```\n\ntest input\n", v18: "```go\nfield in map[string][]struct{key string; value interface{}}\n```\n\ntest input"}, + {v19: "```go\nfield j int\n```\n\nj field of nested struct\n", v18: "```go\nfield j int\n```\n\nj field of nested struct"}, + {v19: "```go\nfield key string\n```\n\ntest key\n", v18: "```go\nfield key string\n```\n\ntest key"}, + {v19: "```go\nfield m map[string]float64\n```\n\nnested map\n", v18: "```go\nfield m map[string]float64\n```\n\nnested map"}, + {v19: "```go\nfield number int64\n```\n\nnested number\n", v18: "```go\nfield number int64\n```\n\nnested number"}, + {v19: "```go\nfield str string\n```\n\nnested string\n", v18: "```go\nfield str string\n```\n\nnested string"}, + {v19: "```go\nfield value int\n```\n\nexpected test value\n", v18: "```go\nfield value int\n```\n\nexpected test value"}, + {v19: "```go\nfield value interface{}\n```\n\ntest value\n", v18: "```go\nfield value interface{}\n```\n\ntest value"}, + {v19: "```go\nfield x []string\n```\n\nX key field\n", v18: "```go\nfield x []string\n```\n\nX key field"}, + {v19: "```go\nfield x int\n```\n\n@mark(PosX, \"x\"),mark(PosY, \"y\")\n", v18: "```go\nfield x int\n```\n\n\\@mark\\(PosX, \\\"x\\\"\\),mark\\(PosY, \\\"y\\\"\\)"}, + {v19: "```go\nfield x int\n```\n\nX coord\n", v18: "```go\nfield x int\n```\n\nX coord"}, + {v19: "```go\nfield x string\n```\n\nX value field\n", v18: "```go\nfield x string\n```\n\nX value field"}, + {v19: "```go\nfield y int\n```\n\nY coord\n", v18: "```go\nfield y int\n```\n\nY coord"}, + {v19: "```go\nfunc (*sync.Mutex).Lock()\n```\n\nLock locks m.\n\n\n[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock)", v18: "```go\nfunc (*sync.Mutex).Lock()\n```\n\nLock locks m\\.\n\n[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock)"}, + {v19: "```go\nfunc (*types.object).Name() string\n```\n\nName returns the object's (package-local, unqualified) name.\n\n\n[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name)", v18: "```go\nfunc (*types.object).Name() string\n```\n\nName returns the object\\'s \\(package\\-local, unqualified\\) name\\.\n\n[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name)"}, + {v19: "```go\nfunc (a.H).Goodbye()\n```\n\n@mark(AGoodbye, \"Goodbye\")\n\n\n[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#H.Goodbye)", v18: "```go\nfunc (a.H).Goodbye()\n```\n\n\\@mark\\(AGoodbye, \\\"Goodbye\\\"\\)\n\n[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#H.Goodbye)"}, + {v19: "```go\nfunc (a.I).B()\n```\n\n@mark(AB, \"B\")\n\n\n[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#I.B)", v18: "```go\nfunc (a.I).B()\n```\n\n\\@mark\\(AB, \\\"B\\\"\\)\n\n[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#I.B)"}, + {v19: "```go\nfunc (a.J).Hello()\n```\n\n@mark(AHello, \"Hello\")\n\n\n[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#J.Hello)", v18: "```go\nfunc (a.J).Hello()\n```\n\n\\@mark\\(AHello, \\\"Hello\\\"\\)\n\n[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#J.Hello)"}, + {v19: "```go\nfunc (interface).open() error\n```\n\nopen method comment\n", v18: "```go\nfunc (interface).open() error\n```\n\nopen method comment"}, + {v19: "```go\nfunc make(t Type, size ...int) Type\n```\n\nThe make built-in function allocates and initializes an object of type slice, map, or chan (only).\n\n\n[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make)", v18: "```go\nfunc make(t Type, size ...int) Type\n```\n\nThe make built\\-in function allocates and initializes an object of type slice, map, or chan \\(only\\)\\.\n\n[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make)"}, + {v19: "```go\ntype A string\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)", v18: "```go\ntype A string\n```\n\n\\@mark\\(AString, \\\"A\\\"\\)\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)"}, + {v19: "```go\ntype a struct {\n\tx string\n}\n```\n\n1st type declaration block\n", v18: "```go\ntype a struct {\n\tx string\n}\n```\n\n1st type declaration block"}, + {v19: "```go\ntype aAlias = a.A\n```\n\n@mark(aAlias, \"aAlias\")\n", v18: "```go\ntype aAlias = a.A\n```\n\n\\@mark\\(aAlias, \\\"aAlias\\\"\\)"}, + {v19: "```go\ntype b struct{}\n```\n\nb has a comment\n", v18: "```go\ntype b struct{}\n```\n\nb has a comment"}, + {v19: "```go\ntype c struct {\n\tf string\n}\n```\n\nc is a struct\n", v18: "```go\ntype c struct {\n\tf string\n}\n```\n\nc is a struct"}, + {v19: "```go\ntype d string\n```\n\n3rd type declaration block\n", v18: "```go\ntype d string\n```\n\n3rd type declaration block"}, + {v19: "```go\ntype e struct {\n\tf float64\n}\n```\n\ne has a comment\n", v18: "```go\ntype e struct {\n\tf float64\n}\n```\n\ne has a comment"}, + {v19: "```go\ntype string string\n```\n\nstring is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text.\n\n\n[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string)", v18: "```go\ntype string string\n```\n\nstring is the set of all strings of 8\\-bit bytes, conventionally but not necessarily representing UTF\\-8\\-encoded text\\.\n\n[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string)"}, + {v19: "```go\nvar Other Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)", v18: "```go\nvar Other Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"}, + {v19: "```go\nvar _ A\n```\n\nvariable of type a.A\n", v18: "```go\nvar _ A\n```\n\nvariable of type a\\.A"}, + {v19: "```go\nvar a.Other a.Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)", v18: "```go\nvar a.Other a.Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"}, + {v19: "```go\nvar err error\n```\n\n@err\n", v18: "```go\nvar err error\n```\n\n\\@err"}, + {v19: "```go\nvar myUnclosedIf string\n```\n\n@myUnclosedIf\n", v18: "```go\nvar myUnclosedIf string\n```\n\n\\@myUnclosedIf"}, + {v19: "```go\nvar x string\n```\n\nx is a variable.\n", v18: "```go\nvar x string\n```\n\nx is a variable\\."}, + {v19: "```go\nvar z string\n```\n\nz is a variable too.\n", v18: "```go\nvar z string\n```\n\nz is a variable too\\."}, + {v19: "godef/a/a.go:26:6-7: defined here as ```go\ntype A string\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)", v18: "godef/a/a.go:26:6-7: defined here as ```go\ntype A string\n```\n\n\\@mark\\(AString, \\\"A\\\"\\)\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)"}, + {v19: "godef/a/a.go:33:6-9: defined here as ```go\nvar err error\n```\n\n@err", v18: "godef/a/a.go:33:6-9: defined here as ```go\nvar err error\n```\n\n\\@err"}, + {v19: "godef/a/d.go:6:2-8: defined here as ```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)", v18: "godef/a/d.go:6:2-8: defined here as ```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)"}, + {v19: "godef/a/d.go:9:5-10: defined here as ```go\nvar Other Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)", v18: "godef/a/d.go:9:5-10: defined here as ```go\nvar Other Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"}, + {v19: "godef/a/d.go:9:5-10: defined here as ```go\nvar a.Other a.Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)", v18: "godef/a/d.go:9:5-10: defined here as ```go\nvar a.Other a.Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"}, + {v19: "godef/a/random.go:13:2-3: defined here as ```go\nfield x int\n```\n\n@mark(PosX, \"x\"),mark(PosY, \"y\")", v18: "godef/a/random.go:13:2-3: defined here as ```go\nfield x int\n```\n\n\\@mark\\(PosX, \\\"x\\\"\\),mark\\(PosY, \\\"y\\\"\\)"}, + {v19: "godef/b/b.go:25:6-12: defined here as ```go\ntype aAlias = a.A\n```\n\n@mark(aAlias, \"aAlias\")", v18: "godef/b/b.go:25:6-12: defined here as ```go\ntype aAlias = a.A\n```\n\n\\@mark\\(aAlias, \\\"aAlias\\\"\\)"}, + {v19: "godef/b/b.go:28:2-4: defined here as ```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)", v18: "godef/b/b.go:28:2-4: defined here as ```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)"}, + {v19: "godef/b/b.go:29:2-4: defined here as ```go\nfield S2 S2\n```\n\n@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)", v18: "godef/b/b.go:29:2-4: defined here as ```go\nfield S2 S2\n```\n\n\\@godef\\(\\\"S2\\\", S2\\),mark\\(S1S2, \\\"S2\\\"\\)\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)"}, + {v19: "godef/b/b.go:35:2-4: defined here as ```go\nfield F1 string\n```\n\n@mark(S2F1, \"F1\")\n\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)", v18: "godef/b/b.go:35:2-4: defined here as ```go\nfield F1 string\n```\n\n\\@mark\\(S2F1, \\\"F1\\\"\\)\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)"}, + {v19: "godef/b/b.go:36:2-4: defined here as ```go\nfield F2 int\n```\n\n@mark(S2F2, \"F2\")\n\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)", v18: "godef/b/b.go:36:2-4: defined here as ```go\nfield F2 int\n```\n\n\\@mark\\(S2F2, \\\"F2\\\"\\)\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)"}, + {v19: "godef/b/b.go:57:7-8: defined here as ```go\nconst X untyped int = 0\n```\n\n@mark(bX, \"X\"),godef(\"X\", bX)\n\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)", v18: "godef/b/b.go:57:7-8: defined here as ```go\nconst X untyped int = 0\n```\n\n\\@mark\\(bX, \\\"X\\\"\\),godef\\(\\\"X\\\", bX\\)\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)"}, + {v19: "godef/broken/unclosedIf.go:7:7-19: defined here as ```go\nvar myUnclosedIf string\n```\n\n@myUnclosedIf", v18: "godef/broken/unclosedIf.go:7:7-19: defined here as ```go\nvar myUnclosedIf string\n```\n\n\\@myUnclosedIf"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/a.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 26,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 467\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 26,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 468\n\t\t}\n\t},\n\t\"description\": \"```go\\ntype A string\\n```\\n\\n@mark(AString, \\\"A\\\")\\n\\n\\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/a.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 26,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 467\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 26,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 468\n\t\t}\n\t},\n\t\"description\": \"```go\\ntype A string\\n```\\n\\n\\\\@mark\\\\(AString, \\\\\\\"A\\\\\\\"\\\\)\\n\\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/a.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 33,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 612\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 33,\n\t\t\t\"column\": 9,\n\t\t\t\"offset\": 615\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar err error\\n```\\n\\n@err\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/a.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 33,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 612\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 33,\n\t\t\t\"column\": 9,\n\t\t\t\"offset\": 615\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar err error\\n```\\n\\n\\\\@err\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 6,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 90\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 6,\n\t\t\t\"column\": 8,\n\t\t\t\"offset\": 96\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield Member string\\n```\\n\\n@Member\\n\\n\\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 6,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 90\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 6,\n\t\t\t\"column\": 8,\n\t\t\t\"offset\": 96\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield Member string\\n```\\n\\n\\\\@Member\\n\\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 5,\n\t\t\t\"offset\": 121\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 10,\n\t\t\t\"offset\": 126\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar Other Thing\\n```\\n\\n@Other\\n\\n\\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 5,\n\t\t\t\"offset\": 121\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 10,\n\t\t\t\"offset\": 126\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar Other Thing\\n```\\n\\n\\\\@Other\\n\\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 5,\n\t\t\t\"offset\": 121\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 10,\n\t\t\t\"offset\": 126\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar a.Other a.Thing\\n```\\n\\n@Other\\n\\n\\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 5,\n\t\t\t\"offset\": 121\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 10,\n\t\t\t\"offset\": 126\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar a.Other a.Thing\\n```\\n\\n\\\\@Other\\n\\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/random.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 13,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 187\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 13,\n\t\t\t\"column\": 3,\n\t\t\t\"offset\": 188\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield x int\\n```\\n\\n@mark(PosX, \\\"x\\\"),mark(PosY, \\\"y\\\")\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/random.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 13,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 187\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 13,\n\t\t\t\"column\": 3,\n\t\t\t\"offset\": 188\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield x int\\n```\\n\\n\\\\@mark\\\\(PosX, \\\\\\\"x\\\\\\\"\\\\),mark\\\\(PosY, \\\\\\\"y\\\\\\\"\\\\)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 25,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 542\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 25,\n\t\t\t\"column\": 12,\n\t\t\t\"offset\": 548\n\t\t}\n\t},\n\t\"description\": \"```go\\ntype aAlias = a.A\\n```\\n\\n@mark(aAlias, \\\"aAlias\\\")\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 25,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 542\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 25,\n\t\t\t\"column\": 12,\n\t\t\t\"offset\": 548\n\t\t}\n\t},\n\t\"description\": \"```go\\ntype aAlias = a.A\\n```\\n\\n\\\\@mark\\\\(aAlias, \\\\\\\"aAlias\\\\\\\"\\\\)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 28,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 606\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 28,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 608\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F1 int\\n```\\n\\n@mark(S1F1, \\\"F1\\\")\\n\\n\\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 28,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 606\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 28,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 608\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F1 int\\n```\\n\\n\\\\@mark\\\\(S1F1, \\\\\\\"F1\\\\\\\"\\\\)\\n\\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 29,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 638\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 29,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 640\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield S2 S2\\n```\\n\\n@godef(\\\"S2\\\", S2),mark(S1S2, \\\"S2\\\")\\n\\n\\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 29,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 638\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 29,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 640\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield S2 S2\\n```\\n\\n\\\\@godef\\\\(\\\\\\\"S2\\\\\\\", S2\\\\),mark\\\\(S1S2, \\\\\\\"S2\\\\\\\"\\\\)\\n\\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 35,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 781\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 35,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 783\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F1 string\\n```\\n\\n@mark(S2F1, \\\"F1\\\")\\n\\n\\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 35,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 781\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 35,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 783\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F1 string\\n```\\n\\n\\\\@mark\\\\(S2F1, \\\\\\\"F1\\\\\\\"\\\\)\\n\\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 36,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 814\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 36,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 816\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F2 int\\n```\\n\\n@mark(S2F2, \\\"F2\\\")\\n\\n\\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 36,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 814\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 36,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 816\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F2 int\\n```\\n\\n\\\\@mark\\\\(S2F2, \\\\\\\"F2\\\\\\\"\\\\)\\n\\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 57,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 1249\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 57,\n\t\t\t\"column\": 8,\n\t\t\t\"offset\": 1250\n\t\t}\n\t},\n\t\"description\": \"```go\\nconst X untyped int = 0\\n```\\n\\n@mark(bX, \\\"X\\\"),godef(\\\"X\\\", bX)\\n\\n\\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 57,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 1249\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 57,\n\t\t\t\"column\": 8,\n\t\t\t\"offset\": 1250\n\t\t}\n\t},\n\t\"description\": \"```go\\nconst X untyped int = 0\\n```\\n\\n\\\\@mark\\\\(bX, \\\\\\\"X\\\\\\\"\\\\),godef\\\\(\\\\\\\"X\\\\\\\", bX\\\\)\\n\\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)\"\n}\n"}, + {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/broken/unclosedIf.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 7,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 68\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 7,\n\t\t\t\"column\": 19,\n\t\t\t\"offset\": 80\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar myUnclosedIf string\\n```\\n\\n@myUnclosedIf\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/broken/unclosedIf.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 7,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 68\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 7,\n\t\t\t\"column\": 19,\n\t\t\t\"offset\": 80\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar myUnclosedIf string\\n```\\n\\n\\\\@myUnclosedIf\"\n}\n"}, +} diff --git a/internal/lsp/tests/metadata_go119.go b/internal/lsp/tests/metadata_go119.go new file mode 100644 index 00000000000..462f130662f --- /dev/null +++ b/internal/lsp/tests/metadata_go119.go @@ -0,0 +1,23 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package tests + +import ( + "testing" +) + +// The markdown in the golden files matches the converter in comment.go, +// but for go1.19 and later the conversion is done using go/doc/comment. +// Compared to the newer version, the older version +// has extra escapes, and treats code blocks slightly differently. +func CheckSameMarkdown(t *testing.T, got, want string) { + t.Helper() + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} From db6a62ca5610f628f8323d4fc2e9ed6e2c11e4f7 Mon Sep 17 00:00:00 2001 From: Matthew Dempsky Date: Wed, 24 Aug 2022 12:05:57 -0700 Subject: [PATCH 197/723] go/internal/gcimporter: call Interface.Complete in unified importer Port of CL 425360 and CL 425365 from stdlib importer to x/tools. Fixes golang/go#54653. Change-Id: Ib475f715ae70400e3ebfb91d6b7755d8e1ddee37 Reviewed-on: https://go-review.googlesource.com/c/tools/+/425362 Run-TryBot: Matthew Dempsky Reviewed-by: Robert Griesemer gopls-CI: kokoro --- go/internal/gcimporter/ureader_yes.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/go/internal/gcimporter/ureader_yes.go b/go/internal/gcimporter/ureader_yes.go index 1a0ce647264..2d421c9619d 100644 --- a/go/internal/gcimporter/ureader_yes.go +++ b/go/internal/gcimporter/ureader_yes.go @@ -38,6 +38,10 @@ type pkgReader struct { laterFns []func() // laterFors is used in case of 'type A B' to ensure that B is processed before A. laterFors map[types.Type]int + + // ifaces holds a list of constructed Interfaces, which need to have + // Complete called after importing is done. + ifaces []*types.Interface } // later adds a function to be invoked at the end of import reading. @@ -113,6 +117,10 @@ func readUnifiedPackage(fset *token.FileSet, ctxt *types.Context, imports map[st fn() } + for _, iface := range pr.ifaces { + iface.Complete() + } + pkg.MarkComplete() return pkg } @@ -407,6 +415,16 @@ func (r *reader) interfaceType() *types.Interface { if implicit { iface.MarkImplicit() } + + // We need to call iface.Complete(), but if there are any embedded + // defined types, then we may not have set their underlying + // interface type yet. So we need to defer calling Complete until + // after we've called SetUnderlying everywhere. + // + // TODO(mdempsky): After CL 424876 lands, it should be safe to call + // iface.Complete() immediately. + r.p.ifaces = append(r.p.ifaces, iface) + return iface } @@ -542,7 +560,9 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { embeds[i] = iface.EmbeddedType(i) } - underlying = types.NewInterfaceType(methods, embeds) + newIface := types.NewInterfaceType(methods, embeds) + r.p.ifaces = append(r.p.ifaces, newIface) + underlying = newIface } named.SetUnderlying(underlying) From 7111c2e56dc12af9b09619e553adf4a73d8b151f Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Thu, 25 Aug 2022 11:18:37 -0700 Subject: [PATCH 198/723] x/tools/internal/lsp: disable a test so we can change the parser error This will allow us to submit CL 425007 after which we can re-enable this code and adjust the error accordingly. For golang/go#54511. Change-Id: I2861a8f372bce214824d7cbdffad6abf7ca4a58e Reviewed-on: https://go-review.googlesource.com/c/tools/+/425497 Reviewed-by: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Griesemer Auto-Submit: Robert Griesemer Run-TryBot: Robert Griesemer --- internal/lsp/testdata/badstmt/badstmt.go.in | 11 +++++++++-- internal/lsp/testdata/summary.txt.golden | 4 ++-- internal/lsp/testdata/summary_go1.18.txt.golden | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/lsp/testdata/badstmt/badstmt.go.in b/internal/lsp/testdata/badstmt/badstmt.go.in index 5a560791086..b6cb6140696 100644 --- a/internal/lsp/testdata/badstmt/badstmt.go.in +++ b/internal/lsp/testdata/badstmt/badstmt.go.in @@ -5,9 +5,16 @@ import ( ) func _() { - defer foo.F //@complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement", "error") + /* + Temporarily disabled so we can change the parser error message + TODO(gri) uncomment once CL 425007 is submitted, and remove spaces + between // and @. + + defer foo.F // @complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement", "error") y := 1 - defer foo.F //@complete(" //", Foo) + + defer foo.F // @complete(" //", Foo) + */ } func _() { diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index 9324833570f..56c7f48aa3d 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -1,14 +1,14 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 265 +CompletionsCount = 263 CompletionSnippetCount = 106 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 164 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 38 +DiagnosticsCount = 37 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 9ec3c79d3c3..f195be907a6 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -1,14 +1,14 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 266 +CompletionsCount = 264 CompletionSnippetCount = 116 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 174 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 38 +DiagnosticsCount = 37 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 From d35bb1970811516f850b9097d31dab0dddb55f1b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 25 Aug 2022 14:43:13 -0400 Subject: [PATCH 199/723] internal/lsp/tests: improve assertion error message Change-Id: I487faada9f1041434dde981d5aded195f6b40054 Reviewed-on: https://go-review.googlesource.com/c/tools/+/425574 Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Griesemer --- internal/lsp/tests/tests.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index 4c3201ab8bc..e5bc829974a 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -1004,7 +1004,10 @@ func checkData(t *testing.T, data *Data) { })) got := buf.String() if want != got { - t.Errorf("test summary does not match:\n%s", Diff(t, want, got)) + // These counters change when assertions are added or removed. + // They act as an independent safety net to ensure that the + // tests didn't spuriously pass because they did no work. + t.Errorf("test summary does not match:\n%s\n(Run with -golden to update golden file; also, there may be one per Go version.)", Diff(t, want, got)) } } From 2f38e1deaaaf5e1457457a5e5bf2b26f3539a462 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 25 Aug 2022 15:26:40 -0400 Subject: [PATCH 200/723] internal/lsp/tests: disable failing test on ARM This is a stopgap until I can diagnost the problem, but in the meantime we need to fix the builders. Updates https://github.com/golang/go/issues/54655 Change-Id: I6260828e8c07e3121c45f99166a26d51aa9805a4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/425575 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Tim King --- internal/lsp/tests/tests.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index e5bc829974a..da7af231bf0 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -17,6 +17,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "sort" "strconv" "strings" @@ -710,6 +711,13 @@ func Run(t *testing.T, tests Tests, data *Data) { if shouldSkip(data, spn.URI()) { continue } + // Temporarily suppress test failing on some ARM builders. + // https://github.com/golang/go/issues/54655. + // TODO(adonovan): reenable along with fix. + if name := SpanName(spn); runtime.GOARCH == "arm64" && (name == "a4_28_18" || name == "a4_35_20") { + t.Logf("skipping test %s on GOARCH=arm64", name) + continue + } t.Run(SpanName(spn), func(t *testing.T) { t.Helper() tests.SuggestedFix(t, spn, actionKinds, 1) From 7c5e03569b8c754ab4db8043f6654b4804a30bba Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 24 Aug 2022 17:33:37 -0400 Subject: [PATCH 201/723] internal/lsp: fix suppressed panic in analyzer This change ensures that the End position provided to span.NewRange in suggestedAnalysisFixes is valid even when the diagnostic has only a valid start position. This seems to be the cause of some panics observed in the ARM builders in the attached issue. Also, reduce the scope of the recover operation to just the analyzer's run method: we don't want to hide further bugs (or discard stack traces) in the setup or postprocessing logic. Also: - split a single assertion in span.NewRange into two. - Add information to various error messages to help identify causes. - Add TODO comments about inconsistent treatment of token.File in span.FileSpan, and temporarily remove bug.Errorf that is obviously reachable from valid inputs. - Add TODO to fix another panic in an analyzer that is covered by our tests but was hitherto suppressed. - Add TODO to use bug.Errorf after recover to prevent recurrences. We can't do that until the previous panic is fixed. Updates https://github.com/golang/go/issues/54655 Change-Id: I0576d03fcfffe0c8df157cf6c6520c9d402f8803 Reviewed-on: https://go-review.googlesource.com/c/tools/+/425356 Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller --- .../lsp/analysis/fillreturns/fillreturns.go | 3 +++ internal/lsp/cache/analysis.go | 20 +++++++++----- internal/lsp/cache/errors.go | 19 ++++++++++++- internal/span/token.go | 27 ++++++++++++++++--- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/internal/lsp/analysis/fillreturns/fillreturns.go b/internal/lsp/analysis/fillreturns/fillreturns.go index 705ae124d57..ab2fa869115 100644 --- a/internal/lsp/analysis/fillreturns/fillreturns.go +++ b/internal/lsp/analysis/fillreturns/fillreturns.go @@ -168,6 +168,9 @@ outer: var match ast.Expr var idx int for j, val := range remaining { + // TODO(adonovan): if TypeOf returns nil (as it may, since we + // RunDespiteErrors), then matchingTypes will panic in + // types.AssignableTo. Fix it, and audit for other instances. if !matchingTypes(info.TypeOf(val), retTyp) { continue } diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index ca0e04d64b0..3ddc3a0117a 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -242,11 +242,6 @@ func runAnalysis(ctx context.Context, snapshot *snapshot, analyzer *analysis.Ana objectFacts: make(map[objectFactKey]analysis.Fact), packageFacts: make(map[packageFactKey]analysis.Fact), } - defer func() { - if r := recover(); r != nil { - data.err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) - } - }() // Plumb the output values of the dependencies // into the inputs of this action. Also facts. @@ -362,7 +357,20 @@ func runAnalysis(ctx context.Context, snapshot *snapshot, analyzer *analysis.Ana data.err = fmt.Errorf("analysis skipped due to errors in package") return data } - data.result, data.err = pass.Analyzer.Run(pass) + + // Recover from panics (only) within the analyzer logic. + // (Use an anonymous function to limit the recover scope.) + func() { + defer func() { + if r := recover(); r != nil { + // TODO(adonovan): use bug.Errorf here so that we + // detect crashes covered by our test suite. + // e.g. + data.err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) + } + }() + data.result, data.err = pass.Analyzer.Run(pass) + }() if data.err != nil { return data } diff --git a/internal/lsp/cache/errors.go b/internal/lsp/cache/errors.go index 80ae5451c8d..94378298916 100644 --- a/internal/lsp/cache/errors.go +++ b/internal/lsp/cache/errors.go @@ -298,7 +298,11 @@ func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnos if tokFile == nil { return nil, bug.Errorf("no file for edit position") } - spn, err := span.NewRange(tokFile, e.Pos, e.End).Span() + end := e.End + if !end.IsValid() { + end = e.Pos + } + spn, err := span.NewRange(tokFile, e.Pos, end).Span() if err != nil { return nil, err } @@ -353,7 +357,20 @@ func typeErrorData(fset *token.FileSet, pkg *pkg, terr types.Error) (typesintern start, end = terr.Pos, terr.Pos ecode = 0 } + // go/types may return invalid positions in some cases, such as + // in errors on tokens missing from the syntax tree. + if !start.IsValid() { + return 0, span.Span{}, fmt.Errorf("type error (%q, code %d, go116=%t) without position", terr.Msg, ecode, ok) + } + // go/types errors retain their FileSet. + // Sanity-check that we're using the right one. + if fset != terr.Fset { + return 0, span.Span{}, bug.Errorf("wrong FileSet for type error") + } posn := fset.Position(start) + if !posn.IsValid() { + return 0, span.Span{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr) + } pgf, err := pkg.File(span.URIFromPath(posn.Filename)) if err != nil { return 0, span.Span{}, err diff --git a/internal/span/token.go b/internal/span/token.go index c35a512c158..61b5995e058 100644 --- a/internal/span/token.go +++ b/internal/span/token.go @@ -29,8 +29,11 @@ func NewRange(file *token.File, start, end token.Pos) Range { if file == nil { panic("nil *token.File") } - if !start.IsValid() || !end.IsValid() { - panic("invalid start/end token.Pos") + if !start.IsValid() { + panic("invalid start token.Pos") + } + if !end.IsValid() { + panic("invalid end token.Pos") } // TODO(adonovan): ideally we would make this stronger assertion: @@ -76,6 +79,9 @@ func (r Range) Span() (Span, error) { // line directives they may reference positions in another file. If srcFile is // provided, it is used to map the line:column positions referenced by start // and end to offsets in the corresponding file. +// +// TODO(adonovan): clarify whether it is valid to pass posFile==srcFile when +// //line directives are in use. If so, fix this function; if not, fix Range.Span. func FileSpan(posFile, srcFile *token.File, start, end token.Pos) (Span, error) { if !start.IsValid() { return Span{}, fmt.Errorf("start pos is not valid") @@ -111,7 +117,16 @@ func FileSpan(posFile, srcFile *token.File, start, end token.Pos) (Span, error) tf = srcFile } if startFilename != tf.Name() { - return Span{}, bug.Errorf("must supply Converter for file %q", startFilename) + // 'start' identifies a position specified by a //line directive + // in a file other than the one containing the directive. + // (Debugging support for https://github.com/golang/go/issues/54655.) + // + // This used to be a bug.Errorf, but that was unsound because + // Range.Span passes this function the same TokFile argument twice, + // which is never going to pass this test for a file containing + // a //line directive. + // TODO(adonovan): decide where the bug.Errorf really belongs. + return Span{}, fmt.Errorf("must supply Converter for file %q (tf.Name() = %q)", startFilename, tf.Name()) } return s.WithOffset(tf) } @@ -202,5 +217,11 @@ func ToOffset(tf *token.File, line, col int) (int, error) { // we assume that column is in bytes here, and that the first byte of a // line is at column 1 pos += token.Pos(col - 1) + + // Debugging support for https://github.com/golang/go/issues/54655. + if pos > token.Pos(tf.Base()+tf.Size()) { + return 0, fmt.Errorf("ToOffset: column %d is beyond end of file", col) + } + return offset(tf, pos) } From 7f2330708b5c3ba1924e67af8dcd4d01f05de59a Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 23 Aug 2022 14:20:00 -0400 Subject: [PATCH 202/723] internal/lsp: limit diagnostics for upgrades to codelens go.mod file The Check for upgrades codelens was only looking for upgrades for the current module, but was applying diagnostics to all go.mod files in the workspace. This change makes sure to only apply the diagnostics in the same selected go.mod. Fixes golang/go#54556 Change-Id: I1eacbc8af2e9dcfe1e1a67516f047bcb94099872 Reviewed-on: https://go-review.googlesource.com/c/tools/+/425195 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Suzy Mueller Reviewed-by: Alan Donovan --- .../regtest/codelens/codelens_test.go | 84 +++++++++++++++---- internal/lsp/cache/session.go | 2 +- internal/lsp/cache/view.go | 23 +++-- internal/lsp/command.go | 2 +- internal/lsp/mod/diagnostics.go | 2 +- internal/lsp/source/view.go | 10 ++- 6 files changed, 92 insertions(+), 31 deletions(-) diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 4d2be8e74d7..73752f3adf9 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -75,7 +75,8 @@ const ( // This test confirms the full functionality of the code lenses for updating // dependencies in a go.mod file. It checks for the code lens that suggests // an update and then executes the command associated with that code lens. A -// regression test for golang/go#39446. +// regression test for golang/go#39446. It also checks that these code lenses +// only affect the diagnostics and contents of the containing go.mod file. func TestUpgradeCodelens(t *testing.T) { const proxyWithLatest = ` -- golang.org/x/hello@v1.3.3/go.mod -- @@ -97,30 +98,64 @@ var Goodbye error ` const shouldUpdateDep = ` --- go.mod -- -module mod.com +-- go.work -- +go 1.18 + +use ( + ./a + ./b +) +-- a/go.mod -- +module mod.com/a go 1.14 require golang.org/x/hello v1.2.3 --- go.sum -- +-- a/go.sum -- golang.org/x/hello v1.2.3 h1:7Wesfkx/uBd+eFgPrq0irYj/1XfmbvLV8jZ/W7C2Dwg= golang.org/x/hello v1.2.3/go.mod h1:OgtlzsxVMUUdsdQCIDYgaauCTH47B8T8vofouNJfzgY= --- main.go -- +-- a/main.go -- package main import "golang.org/x/hello/hi" +func main() { + _ = hi.Goodbye +} +-- b/go.mod -- +module mod.com/b + +go 1.14 + +require golang.org/x/hello v1.2.3 +-- b/go.sum -- +golang.org/x/hello v1.2.3 h1:7Wesfkx/uBd+eFgPrq0irYj/1XfmbvLV8jZ/W7C2Dwg= +golang.org/x/hello v1.2.3/go.mod h1:OgtlzsxVMUUdsdQCIDYgaauCTH47B8T8vofouNJfzgY= +-- b/main.go -- +package main + +import ( + "golang.org/x/hello/hi" +) + func main() { _ = hi.Goodbye } ` - const wantGoMod = `module mod.com + const wantGoModA = `module mod.com/a go 1.14 require golang.org/x/hello v1.3.3 +` + // Applying the diagnostics or running the codelenses for a/go.mod + // should not change the contents of b/go.mod + const wantGoModB = `module mod.com/b + +go 1.14 + +require golang.org/x/hello v1.2.3 ` for _, commandTitle := range []string{ @@ -131,10 +166,11 @@ require golang.org/x/hello v1.3.3 WithOptions( ProxyFiles(proxyWithLatest), ).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { - env.OpenFile("go.mod") + env.OpenFile("a/go.mod") + env.OpenFile("b/go.mod") var lens protocol.CodeLens var found bool - for _, l := range env.CodeLens("go.mod") { + for _, l := range env.CodeLens("a/go.mod") { if l.Command.Title == commandTitle { lens = l found = true @@ -150,8 +186,11 @@ require golang.org/x/hello v1.3.3 t.Fatal(err) } env.Await(env.DoneWithChangeWatchedFiles()) - if got := env.Editor.BufferText("go.mod"); got != wantGoMod { - t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(t, wantGoMod, got)) + if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA { + t.Fatalf("a/go.mod upgrade failed:\n%s", tests.Diff(t, wantGoModA, got)) + } + if got := env.Editor.BufferText("b/go.mod"); got != wantGoModB { + t.Fatalf("b/go.mod changed unexpectedly:\n%s", tests.Diff(t, wantGoModB, got)) } }) }) @@ -160,22 +199,31 @@ require golang.org/x/hello v1.3.3 t.Run(fmt.Sprintf("Upgrade individual dependency vendoring=%v", vendoring), func(t *testing.T) { WithOptions(ProxyFiles(proxyWithLatest)).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { if vendoring { - env.RunGoCommand("mod", "vendor") + env.RunGoCommandInDir("a", "mod", "vendor") } env.Await(env.DoneWithChangeWatchedFiles()) - env.OpenFile("go.mod") - env.ExecuteCodeLensCommand("go.mod", command.CheckUpgrades) + env.OpenFile("a/go.mod") + env.OpenFile("b/go.mod") + env.ExecuteCodeLensCommand("a/go.mod", command.CheckUpgrades) d := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("go.mod", `require`, "can be upgraded"), - ReadDiagnostics("go.mod", d), + env.DiagnosticAtRegexpWithMessage("a/go.mod", `require`, "can be upgraded"), + ReadDiagnostics("a/go.mod", d), + // We do not want there to be a diagnostic for b/go.mod, + // but there may be some subtlety in timing here, where this + // should always succeed, but may not actually test the correct + // behavior. + env.NoDiagnosticAtRegexp("b/go.mod", `require`), ), ) - env.ApplyQuickFixes("go.mod", d.Diagnostics) + env.ApplyQuickFixes("a/go.mod", d.Diagnostics) env.Await(env.DoneWithChangeWatchedFiles()) - if got := env.Editor.BufferText("go.mod"); got != wantGoMod { - t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(t, wantGoMod, got)) + if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA { + t.Fatalf("a/go.mod upgrade failed:\n%s", tests.Diff(t, wantGoModA, got)) + } + if got := env.Editor.BufferText("b/go.mod"); got != wantGoModB { + t.Fatalf("b/go.mod changed unexpectedly:\n%s", tests.Diff(t, wantGoModB, got)) } }) }) diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go index d11c06d1e7e..32fd4484432 100644 --- a/internal/lsp/cache/session.go +++ b/internal/lsp/cache/session.go @@ -226,7 +226,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, baseCtx: baseCtx, name: name, folder: folder, - moduleUpgrades: map[string]string{}, + moduleUpgrades: map[span.URI]map[string]string{}, filesByURI: map[span.URI]*fileBase{}, filesByBase: map[string][]*fileBase{}, rootURI: root, diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 9d1ee558103..6eec3536d8f 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -56,8 +56,9 @@ type View struct { importsState *importsState - // moduleUpgrades tracks known upgrades for module paths. - moduleUpgrades map[string]string + // moduleUpgrades tracks known upgrades for module paths in each modfile. + // Each modfile has a map of module name to upgrade version. + moduleUpgrades map[span.URI]map[string]string // keep track of files by uri and by basename, a single file may be mapped // to multiple uris, and the same basename may map to multiple files @@ -1000,23 +1001,33 @@ func (v *View) IsGoPrivatePath(target string) bool { return globsMatchPath(v.goprivate, target) } -func (v *View) ModuleUpgrades() map[string]string { +func (v *View) ModuleUpgrades(uri span.URI) map[string]string { v.mu.Lock() defer v.mu.Unlock() upgrades := map[string]string{} - for mod, ver := range v.moduleUpgrades { + for mod, ver := range v.moduleUpgrades[uri] { upgrades[mod] = ver } return upgrades } -func (v *View) RegisterModuleUpgrades(upgrades map[string]string) { +func (v *View) RegisterModuleUpgrades(uri span.URI, upgrades map[string]string) { + // Return early if there are no upgrades. + if len(upgrades) == 0 { + return + } + v.mu.Lock() defer v.mu.Unlock() + m := v.moduleUpgrades[uri] + if m == nil { + m = make(map[string]string) + v.moduleUpgrades[uri] = m + } for mod, ver := range upgrades { - v.moduleUpgrades[mod] = ver + m[mod] = ver } } diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 35bc0e43287..f9f8caede87 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -180,7 +180,7 @@ func (c *commandHandler) CheckUpgrades(ctx context.Context, args command.CheckUp if err != nil { return err } - deps.snapshot.View().RegisterModuleUpgrades(upgrades) + deps.snapshot.View().RegisterModuleUpgrades(args.URI.SpanURI(), upgrades) // Re-diagnose the snapshot to publish the new module diagnostics. c.s.diagnoseSnapshot(deps.snapshot, nil, false) return nil diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go index 9c49d8b36b1..d866cfd8148 100644 --- a/internal/lsp/mod/diagnostics.go +++ b/internal/lsp/mod/diagnostics.go @@ -55,7 +55,7 @@ func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source. var diagnostics []*source.Diagnostic // Add upgrade quick fixes for individual modules if we know about them. - upgrades := snapshot.View().ModuleUpgrades() + upgrades := snapshot.View().ModuleUpgrades(fh.URI()) for _, req := range pm.File.Require { ver, ok := upgrades[req.Mod.Path] if !ok || req.Mod.Version == ver { diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index f0d22c7c414..e9587d1aa4d 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -259,11 +259,13 @@ type View interface { // by the GOPRIVATE environment variable. IsGoPrivatePath(path string) bool - // ModuleUpgrades returns known module upgrades. - ModuleUpgrades() map[string]string + // ModuleUpgrades returns known module upgrades for the dependencies of + // modfile. + ModuleUpgrades(modfile span.URI) map[string]string - // RegisterModuleUpgrades registers that upgrades exist for the given modules. - RegisterModuleUpgrades(upgrades map[string]string) + // RegisterModuleUpgrades registers that upgrades exist for the given modules + // required by modfile. + RegisterModuleUpgrades(modfile span.URI, upgrades map[string]string) // FileKind returns the type of a file FileKind(FileHandle) FileKind From 717a671622f7b89965123f259e4db4c2fccbeb4e Mon Sep 17 00:00:00 2001 From: Abirdcfly Date: Fri, 26 Aug 2022 06:46:27 +0000 Subject: [PATCH 203/723] go/analysis/passes/printf: remove unused hasBasicType Change-Id: Ic1be5931a620e3fd15b58a7acc34d0013f011a20 GitHub-Last-Rev: 32b11c95b8c0715aa13a741913759198ec208942 GitHub-Pull-Request: golang/tools#391 Reviewed-on: https://go-review.googlesource.com/c/tools/+/425834 Run-TryBot: Tim King Reviewed-by: David Chase gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Tim King --- go/analysis/passes/printf/types.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/go/analysis/passes/printf/types.go b/go/analysis/passes/printf/types.go index 270e917c809..7cbb0bdbf5f 100644 --- a/go/analysis/passes/printf/types.go +++ b/go/analysis/passes/printf/types.go @@ -299,13 +299,3 @@ func isConvertibleToString(typ types.Type) bool { return false } - -// hasBasicType reports whether x's type is a types.Basic with the given kind. -func hasBasicType(pass *analysis.Pass, x ast.Expr, kind types.BasicKind) bool { - t := pass.TypesInfo.Types[x].Type - if t != nil { - t = t.Underlying() - } - b, ok := t.(*types.Basic) - return ok && b.Kind() == kind -} From 431f4eff4fe2fd294ab046173647b8c508a94071 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 29 Aug 2022 13:45:39 -0400 Subject: [PATCH 204/723] internal/lsp/tests: re-enable ARM tests This is a simple revert of CL 425497, which disabled the tests. The problem was due to a misencoded FMOVD instruction causing some 0.0 constants to compile to nonzero; fixed by CL 425188. Updates golang/go#54655 Updates golang/go#425188 Change-Id: I2231b57b7b78ac1ae2e8b60cda62898ea7122cda Reviewed-on: https://go-review.googlesource.com/c/tools/+/426017 gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan --- internal/lsp/tests/tests.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index da7af231bf0..e5bc829974a 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -17,7 +17,6 @@ import ( "os" "path/filepath" "regexp" - "runtime" "sort" "strconv" "strings" @@ -711,13 +710,6 @@ func Run(t *testing.T, tests Tests, data *Data) { if shouldSkip(data, spn.URI()) { continue } - // Temporarily suppress test failing on some ARM builders. - // https://github.com/golang/go/issues/54655. - // TODO(adonovan): reenable along with fix. - if name := SpanName(spn); runtime.GOARCH == "arm64" && (name == "a4_28_18" || name == "a4_35_20") { - t.Logf("skipping test %s on GOARCH=arm64", name) - continue - } t.Run(SpanName(spn), func(t *testing.T) { t.Helper() tests.SuggestedFix(t, spn, actionKinds, 1) From 248c34b88a4148128f89e41923498bd86f805b7d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 26 Aug 2022 17:47:56 -0400 Subject: [PATCH 205/723] internal/lsp: support regular expressions in Diagnostics tests The diagnostics emitted by the Go toolchain vary from one release to the next, and the gopls tests run against multiple versions. Therefore our tests need a way to express multiple possibilities in their expectations. This change interprets the patterns in @diag test annotations as regular expressions, allowing some flexibility in recognizing both old and new messages. It's not a panacea but it is one step to reducing the considerable friction of making changes to the compiler or go/types in the main tree. Details: - Factor the three implementations of the Tests.Diagnostics abstract method so that they all use DiffDiagnostics to report differences, substantially rewriting one of them. - Move the "no_diagnostics" hack, of which there were three copies, not all consistent, into DiffDiagnostics. - Eliminate the named type for each tests.Data field; a type alias is all that's needed. - Add Diagnostics.String method. - Add various TODOs for further improvements. - Add various apparently missing Fatal statements within the tests. Change-Id: Id38ad72a851b551dd4eb1d8c021bcb8adbb2213f Reviewed-on: https://go-review.googlesource.com/c/tools/+/425956 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Griesemer Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- internal/lsp/cmd/test/check.go | 78 ++++++++++++----------- internal/lsp/lsp_test.go | 11 +--- internal/lsp/source/source_test.go | 7 --- internal/lsp/source/view.go | 4 ++ internal/lsp/testdata/bad/bad0.go | 5 +- internal/lsp/testdata/good/good1.go | 1 + internal/lsp/tests/tests.go | 97 +++++++++++++++-------------- internal/lsp/tests/util.go | 26 ++++++-- 8 files changed, 120 insertions(+), 109 deletions(-) diff --git a/internal/lsp/cmd/test/check.go b/internal/lsp/cmd/test/check.go index 6a53925051f..ac1eac401ae 100644 --- a/internal/lsp/cmd/test/check.go +++ b/internal/lsp/cmd/test/check.go @@ -5,59 +5,61 @@ package cmdtest import ( - "fmt" "io/ioutil" "strings" "testing" + "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/lsp/tests" "golang.org/x/tools/internal/span" ) +// Diagnostics runs the gopls command on a single file, parses its +// diagnostics, and compares against the expectations defined by +// markers in the source file. func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) { - if len(want) == 1 && want[0].Message == "" { - return + out, _ := r.runGoplsCmd(t, "check", uri.Filename()) + + content, err := ioutil.ReadFile(uri.Filename()) + if err != nil { + t.Fatal(err) } - fname := uri.Filename() - out, _ := r.runGoplsCmd(t, "check", fname) - // parse got into a collection of reports - got := map[string]struct{}{} - for _, l := range strings.Split(out, "\n") { - if len(l) == 0 { - continue + mapper := protocol.NewColumnMapper(uri, content) + + // Parse command output into a set of diagnostics. + var got []*source.Diagnostic + for _, line := range strings.Split(out, "\n") { + if line == "" { + continue // skip blank + } + parts := strings.SplitN(line, ": ", 2) // "span: message" + if len(parts) != 2 { + t.Fatalf("output line not of form 'span: message': %q", line) } - // parse and reprint to normalize the span - bits := strings.SplitN(l, ": ", 2) - if len(bits) == 2 { - spn := span.Parse(strings.TrimSpace(bits[0])) - spn = span.New(spn.URI(), spn.Start(), span.Point{}) - data, err := ioutil.ReadFile(fname) - if err != nil { - t.Fatal(err) - } - converter := span.NewTokenFile(fname, data) - s, err := spn.WithPosition(converter) - if err != nil { - t.Fatal(err) - } - l = fmt.Sprintf("%s: %s", s, strings.TrimSpace(bits[1])) + spn, message := span.Parse(parts[0]), parts[1] + rng, err := mapper.Range(spn) + if err != nil { + t.Fatal(err) } - got[r.NormalizePrefix(l)] = struct{}{} + // Set only the fields needed by DiffDiagnostics. + got = append(got, &source.Diagnostic{ + URI: uri, + Range: rng, + Message: message, + }) } + + // Don't expect fields that we can't populate from the command output. for _, diag := range want { - expect := fmt.Sprintf("%v:%v:%v: %v", uri.Filename(), diag.Range.Start.Line+1, diag.Range.Start.Character+1, diag.Message) - if diag.Range.Start.Character == 0 { - expect = fmt.Sprintf("%v:%v: %v", uri.Filename(), diag.Range.Start.Line+1, diag.Message) - } - expect = r.NormalizePrefix(expect) - _, found := got[expect] - if !found { - t.Errorf("missing diagnostic %q, %v", expect, got) - } else { - delete(got, expect) + if diag.Source == "no_diagnostics" { + continue // see DiffDiagnostics } + diag.Source = "" + diag.Severity = 0 } - for extra := range got { - t.Errorf("extra diagnostic %q", extra) + + if diff := tests.DiffDiagnostics(uri, want, got); diff != "" { + t.Error(diff) } } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 5d37c56a8ea..def47b9fd1d 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -211,16 +211,7 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost // Get the diagnostics for this view if we have not done it before. v := r.server.session.View(r.data.Config.Dir) r.collectDiagnostics(v) - d := r.diagnostics[uri] - got := make([]*source.Diagnostic, len(d)) - copy(got, d) - // A special case to test that there are no diagnostics for a file. - if len(want) == 1 && want[0].Source == "no_diagnostics" { - if len(got) != 0 { - t.Errorf("expected no diagnostics for %s, got %v", uri, got) - } - return - } + got := append([]*source.Diagnostic(nil), r.diagnostics[uri]...) // copy if diff := tests.DiffDiagnostics(uri, want, got); diff != "" { t.Error(diff) } diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index 177b85d13af..d5c9728bc88 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -156,13 +156,6 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost if err != nil { t.Fatal(err) } - // A special case to test that there are no diagnostics for a file. - if len(want) == 1 && want[0].Source == "no_diagnostics" { - if len(got) != 0 { - t.Errorf("expected no diagnostics for %s, got %v", uri, got) - } - return - } if diff := tests.DiffDiagnostics(fileID.URI, want, got); diff != "" { t.Error(diff) } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index e9587d1aa4d..a188e3666b1 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -685,6 +685,10 @@ type Diagnostic struct { Analyzer *Analyzer } +func (d *Diagnostic) String() string { + return fmt.Sprintf("%v: %s", d.Range, d.Message) +} + type DiagnosticSource string const ( diff --git a/internal/lsp/testdata/bad/bad0.go b/internal/lsp/testdata/bad/bad0.go index 36a4e6b95f7..4120cf3a0d7 100644 --- a/internal/lsp/testdata/bad/bad0.go +++ b/internal/lsp/testdata/bad/bad0.go @@ -1,12 +1,13 @@ +//go:build go1.11 // +build go1.11 package bad -import _ "golang.org/x/tools/internal/lsp/assign/internal/secret" //@diag("\"golang.org/x/tools/internal/lsp/assign/internal/secret\"", "compiler", "could not import golang.org/x/tools/internal/lsp/assign/internal/secret (invalid use of internal package golang.org/x/tools/internal/lsp/assign/internal/secret)", "error") +import _ "golang.org/x/tools/internal/lsp/assign/internal/secret" //@diag("\"golang.org/x/tools/internal/lsp/assign/internal/secret\"", "compiler", "could not import golang.org/x/tools/internal/lsp/assign/internal/secret \\(invalid use of internal package golang.org/x/tools/internal/lsp/assign/internal/secret\\)", "error") func stuff() { //@item(stuff, "stuff", "func()", "func") x := "heeeeyyyy" - random2(x) //@diag("x", "compiler", "cannot use x (variable of type string) as int value in argument to random2", "error") + random2(x) //@diag("x", "compiler", "cannot use x \\(variable of type string\\) as int value in argument to random2", "error") random2(1) //@complete("dom", random, random2, random3) y := 3 //@diag("y", "compiler", "y declared but not used", "error") } diff --git a/internal/lsp/testdata/good/good1.go b/internal/lsp/testdata/good/good1.go index c4664a7e5d4..3014e2a3d55 100644 --- a/internal/lsp/testdata/good/good1.go +++ b/internal/lsp/testdata/good/good1.go @@ -14,6 +14,7 @@ func random2(y int) int { //@item(good_random2, "random2", "func(y int) int", "f //@complete("", good_y_param, types_import, good_random, good_random2, good_stuff) var b types.Bob = &types.X{} //@prepare("ypes","types", "types") if _, ok := b.(*types.X); ok { //@complete("X", X_struct, Y_struct, Bob_interface, CoolAlias) + _ = 0 // suppress "empty branch" diagnostic } return y diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index e5bc829974a..1b02c785976 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -54,39 +54,41 @@ func init() { var UpdateGolden = flag.Bool("golden", false, "Update golden files") -type CallHierarchy map[span.Span]*CallHierarchyResult -type CodeLens map[span.URI][]protocol.CodeLens -type Diagnostics map[span.URI][]*source.Diagnostic -type CompletionItems map[token.Pos]*completion.CompletionItem -type Completions map[span.Span][]Completion -type CompletionSnippets map[span.Span][]CompletionSnippet -type UnimportedCompletions map[span.Span][]Completion -type DeepCompletions map[span.Span][]Completion -type FuzzyCompletions map[span.Span][]Completion -type CaseSensitiveCompletions map[span.Span][]Completion -type RankCompletions map[span.Span][]Completion -type FoldingRanges []span.Span -type Formats []span.Span -type Imports []span.Span -type SemanticTokens []span.Span -type SuggestedFixes map[span.Span][]SuggestedFix -type FunctionExtractions map[span.Span]span.Span -type MethodExtractions map[span.Span]span.Span -type Definitions map[span.Span]Definition -type Implementations map[span.Span][]span.Span -type Highlights map[span.Span][]span.Span -type References map[span.Span][]span.Span -type Renames map[span.Span]string -type PrepareRenames map[span.Span]*source.PrepareItem -type Symbols map[span.URI][]protocol.DocumentSymbol -type SymbolsChildren map[string][]protocol.DocumentSymbol -type SymbolInformation map[span.Span]protocol.SymbolInformation -type InlayHints []span.Span -type WorkspaceSymbols map[WorkspaceSymbolsTestType]map[span.URI][]string -type Signatures map[span.Span]*protocol.SignatureHelp -type Links map[span.URI][]Link -type AddImport map[span.URI]string -type Hovers map[span.Span]string +// These type names apparently avoid the need to repeat the +// type in the field name and the make() expression. +type CallHierarchy = map[span.Span]*CallHierarchyResult +type CodeLens = map[span.URI][]protocol.CodeLens +type Diagnostics = map[span.URI][]*source.Diagnostic +type CompletionItems = map[token.Pos]*completion.CompletionItem +type Completions = map[span.Span][]Completion +type CompletionSnippets = map[span.Span][]CompletionSnippet +type UnimportedCompletions = map[span.Span][]Completion +type DeepCompletions = map[span.Span][]Completion +type FuzzyCompletions = map[span.Span][]Completion +type CaseSensitiveCompletions = map[span.Span][]Completion +type RankCompletions = map[span.Span][]Completion +type FoldingRanges = []span.Span +type Formats = []span.Span +type Imports = []span.Span +type SemanticTokens = []span.Span +type SuggestedFixes = map[span.Span][]SuggestedFix +type FunctionExtractions = map[span.Span]span.Span +type MethodExtractions = map[span.Span]span.Span +type Definitions = map[span.Span]Definition +type Implementations = map[span.Span][]span.Span +type Highlights = map[span.Span][]span.Span +type References = map[span.Span][]span.Span +type Renames = map[span.Span]string +type PrepareRenames = map[span.Span]*source.PrepareItem +type Symbols = map[span.URI][]protocol.DocumentSymbol +type SymbolsChildren = map[string][]protocol.DocumentSymbol +type SymbolInformation = map[span.Span]protocol.SymbolInformation +type InlayHints = []span.Span +type WorkspaceSymbols = map[WorkspaceSymbolsTestType]map[span.URI][]string +type Signatures = map[span.Span]*protocol.SignatureHelp +type Links = map[span.URI][]Link +type AddImport = map[span.URI]string +type Hovers = map[span.Span]string type Data struct { Config packages.Config @@ -137,6 +139,12 @@ type Data struct { mappers map[span.URI]*protocol.ColumnMapper } +// TODO(adonovan): there are multiple implementations of this (undocumented) +// interface, each of which must implement similar semantics. For example: +// - *runner in ../cmd/test/check.go +// - *runner in ../source/source_test.go +// - *runner in ../lsp_test.go +// Can we avoid this duplication? type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) CodeLens(*testing.T, span.URI, []protocol.CodeLens) @@ -1084,11 +1092,11 @@ func (data *Data) collectCodeLens(spn span.Span, title, cmd string) { } m, err := data.Mapper(spn.URI()) if err != nil { - return + data.t.Fatalf("Mapper: %v", err) } rng, err := m.Range(spn) if err != nil { - return + data.t.Fatalf("Range: %v", err) } data.CodeLens[spn.URI()] = append(data.CodeLens[spn.URI()], protocol.CodeLens{ Range: rng, @@ -1099,18 +1107,16 @@ func (data *Data) collectCodeLens(spn span.Span, title, cmd string) { }) } -func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg, msgSeverity string) { - if _, ok := data.Diagnostics[spn.URI()]; !ok { - data.Diagnostics[spn.URI()] = []*source.Diagnostic{} - } +func (data *Data) collectDiagnostics(spn span.Span, msgSource, msgPattern, msgSeverity string) { m, err := data.Mapper(spn.URI()) if err != nil { - return + data.t.Fatalf("Mapper: %v", err) } rng, err := m.Range(spn) if err != nil { - return + data.t.Fatalf("Range: %v", err) } + severity := protocol.SeverityError switch msgSeverity { case "error": @@ -1122,14 +1128,13 @@ func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg, msgSeverity case "information": severity = protocol.SeverityInformation } - // This is not the correct way to do this, but it seems excessive to do the full conversion here. - want := &source.Diagnostic{ + + data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], &source.Diagnostic{ Range: rng, Severity: severity, Source: source.DiagnosticSource(msgSource), - Message: msg, - } - data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], want) + Message: msgPattern, + }) } func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []token.Pos) { diff --git a/internal/lsp/tests/util.go b/internal/lsp/tests/util.go index 98562d63657..5a049134ae1 100644 --- a/internal/lsp/tests/util.go +++ b/internal/lsp/tests/util.go @@ -10,6 +10,7 @@ import ( "fmt" "go/token" "path/filepath" + "regexp" "sort" "strconv" "strings" @@ -110,27 +111,40 @@ func summarizeSymbols(i int, want, got []protocol.DocumentSymbol, reason string, } // DiffDiagnostics prints the diff between expected and actual diagnostics test -// results. +// results. If the sole expectation is "no_diagnostics", the check is suppressed. +// The Message field of each want element must be a regular expression. func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string { + // A special case to test that there are no diagnostics for a file. + if len(want) == 1 && want[0].Source == "no_diagnostics" { + if len(got) != 0 { + return fmt.Sprintf("expected no diagnostics for %s, got %v", uri, got) + } + return "" + } + source.SortDiagnostics(want) source.SortDiagnostics(got) if len(got) != len(want) { + // TODO(adonovan): print the actual difference, not the difference in length! return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want)) } for i, w := range want { g := got[i] - if w.Message != g.Message { - return summarizeDiagnostics(i, uri, want, got, "incorrect Message got %v want %v", g.Message, w.Message) + if match, err := regexp.MatchString(w.Message, g.Message); err != nil { + return summarizeDiagnostics(i, uri, want, got, "invalid regular expression %q: %v", w.Message, err) + + } else if !match { + return summarizeDiagnostics(i, uri, want, got, "got Message %q, want match for pattern %q", g.Message, w.Message) } if w.Severity != g.Severity { - return summarizeDiagnostics(i, uri, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity) + return summarizeDiagnostics(i, uri, want, got, "got Severity %v, want %v", g.Severity, w.Severity) } if w.Source != g.Source { - return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source) + return summarizeDiagnostics(i, uri, want, got, "got Source %v, want %v", g.Source, w.Source) } if !rangeOverlaps(g.Range, w.Range) { - return summarizeDiagnostics(i, uri, want, got, "range %v does not overlap %v", g.Range, w.Range) + return summarizeDiagnostics(i, uri, want, got, "got Range %v, want overlap with %v", g.Range, w.Range) } } return "" From ddbeb754297c663e090e1b417390adebe8741cf6 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 30 Aug 2022 15:16:04 -0400 Subject: [PATCH 206/723] internal/lsp: run internal/lsp/reset_golden.sh Run reset_golden.sh, so that our golden files are stable. This will be useful later, when we migrate internal/lsp to gopls/internal/lsp, and golden files must be updated to account for changing offsets. For golang/go#54509 Change-Id: I2e9a8d3493d64d632b9f0f0e0360d633803f9d92 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426797 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- internal/lsp/reset_golden.sh | 10 +- .../extract_method/extract_basic.go.golden | 364 ------------------ .../extract_func_call.go.golden | 12 - .../testdata/fillstruct/typeparams.go.golden | 202 ---------- .../godef/infer_generics/inferred.go.golden | 4 - .../missingfunction/literals.go.golden | 16 - .../missingfunction/operation.go.golden | 16 - .../missingfunction/unique_params.go.golden | 17 - .../testdata/signature/signature.go.golden | 12 - .../signature/signature_test.go.golden | 21 - .../lsp/testdata/snippets/literal.go.golden | 3 - 11 files changed, 9 insertions(+), 668 deletions(-) diff --git a/internal/lsp/reset_golden.sh b/internal/lsp/reset_golden.sh index 2689407ca15..ef9dacf2dc1 100755 --- a/internal/lsp/reset_golden.sh +++ b/internal/lsp/reset_golden.sh @@ -1,6 +1,14 @@ #!/bin/bash +# +# Copyright 2022 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +# +# Regenerates the *.golden files in the ./internal/lsp/testdata directory. -find ./internal/lsp/ -name *.golden -delete +set -eu + +find ./internal/lsp/testdata -name *.golden ! -name summary*.txt.golden -delete go test ./internal/lsp/source -golden go test ./internal/lsp/ -golden go test ./internal/lsp/cmd -golden diff --git a/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden b/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden index eab22a673c1..3310d973e01 100644 --- a/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden +++ b/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden @@ -180,188 +180,6 @@ func (a A) Add() int { return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") } --- functionextraction_extract_method_13_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(a *A) int { - sum := a.x + a.y - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_method_14_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(sum int) int { - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_method_18_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func newFunction(a A) bool { - return a.x < a.y -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_method_22_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(a A) int { - sum := a.x + a.y - return sum -} - --- functionextraction_extract_method_23_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(sum int) int { - return sum -} - --- functionextraction_extract_method_9_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func newFunction(a *A) bool { - return a.x < a.y -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -- methodextraction_extract_basic_13_2 -- package extract @@ -544,185 +362,3 @@ func (a A) Add() int { return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") } --- methodextraction_extract_method_13_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a *A) newMethod() int { - sum := a.x + a.y - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_method_14_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (*A) newMethod(sum int) int { - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_method_18_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) newMethod() bool { - return a.x < a.y -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_method_22_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) newMethod() int { - sum := a.x + a.y - return sum -} - --- methodextraction_extract_method_23_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (A) newMethod(sum int) int { - return sum -} - --- methodextraction_extract_method_9_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) newMethod() bool { - return a.x < a.y -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - diff --git a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden b/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden index 4423fc92770..d59c0ee99f2 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden +++ b/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden @@ -1,15 +1,3 @@ --- suggestedfix_extract_func_call_6_7 -- -package extract - -import "strconv" - -func _() { - x0 := append([]int{}, 1) - a := x0 //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") - str := "1" - b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") -} - -- suggestedfix_extract_func_call_6_8 -- package extract diff --git a/internal/lsp/testdata/fillstruct/typeparams.go.golden b/internal/lsp/testdata/fillstruct/typeparams.go.golden index 9b2b90c12ee..12e2f844444 100644 --- a/internal/lsp/testdata/fillstruct/typeparams.go.golden +++ b/internal/lsp/testdata/fillstruct/typeparams.go.golden @@ -1,43 +1,3 @@ --- suggestedfix_typeparams_11_40 -- -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{ - foo: 0, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} - -func _[T any]() { - type S struct{ t T } - x := S{} - var _ = basicStructWithTypeParams[T]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - _ = x -} - -- suggestedfix_typeparams_14_40 -- //go:build go1.18 // +build go1.18 @@ -80,89 +40,6 @@ func _[T any]() { _ = x } --- suggestedfix_typeparams_18_49 -- -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{ - foo: "", - bar: 0, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} - -func _[T any]() { - type S struct{ t T } - x := S{} - var _ = basicStructWithTypeParams[T]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - _ = x -} - --- suggestedfix_typeparams_20_49 -- -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = basicStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{ - foo: "", - bar: 0, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} - -func _[T any]() { - type S struct{ t T } - x := S{} - _ = x -} - -- suggestedfix_typeparams_21_49 -- //go:build go1.18 // +build go1.18 @@ -206,85 +83,6 @@ func _[T any]() { _ = x } --- suggestedfix_typeparams_22_1 -- -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - foo: 0, - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} - -func _[T any]() { - type S struct{ t T } - x := S{} - var _ = basicStructWithTypeParams[T]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - _ = x -} - --- suggestedfix_typeparams_24_1 -- -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = basicStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - foo: 0, - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} - -func _[T any]() { - type S struct{ t T } - x := S{} - _ = x -} - -- suggestedfix_typeparams_25_1 -- //go:build go1.18 // +build go1.18 diff --git a/internal/lsp/testdata/godef/infer_generics/inferred.go.golden b/internal/lsp/testdata/godef/infer_generics/inferred.go.golden index 4a36ff460b6..3fcc5f43539 100644 --- a/internal/lsp/testdata/godef/infer_generics/inferred.go.golden +++ b/internal/lsp/testdata/godef/infer_generics/inferred.go.golden @@ -2,10 +2,6 @@ ```go func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S ``` --- constrInf-hoverdef -- -```go -func app(s []int, e int) []int // func[S₁ interface{~[]E₂}, E₂ interface{}](s S₁, e E₂) S₁ -``` -- constrInfer-hoverdef -- ```go func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S diff --git a/internal/lsp/testdata/missingfunction/literals.go.golden b/internal/lsp/testdata/missingfunction/literals.go.golden index 599f020a75b..cb85de4eb11 100644 --- a/internal/lsp/testdata/missingfunction/literals.go.golden +++ b/internal/lsp/testdata/missingfunction/literals.go.golden @@ -1,19 +1,3 @@ --- suggestedfix_literals_10_2 -- -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package missingfunction - -type T struct{} - -func literals() { - undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix", "") -} - -func undefinedLiterals(s string, t1 T, t2 *T) { - panic("implement me!") -} -- suggestedfix_literals_6_2 -- package missingfunction diff --git a/internal/lsp/testdata/missingfunction/operation.go.golden b/internal/lsp/testdata/missingfunction/operation.go.golden index fce69b11d85..6f9e6ffab6d 100644 --- a/internal/lsp/testdata/missingfunction/operation.go.golden +++ b/internal/lsp/testdata/missingfunction/operation.go.golden @@ -1,19 +1,3 @@ --- suggestedfix_operation_10_2 -- -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package missingfunction - -import "time" - -func operation() { - undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix", "") -} - -func undefinedOperation(duration time.Duration) { - panic("implement me!") -} -- suggestedfix_operation_6_2 -- package missingfunction diff --git a/internal/lsp/testdata/missingfunction/unique_params.go.golden b/internal/lsp/testdata/missingfunction/unique_params.go.golden index 4797b3ba784..8d6352cded4 100644 --- a/internal/lsp/testdata/missingfunction/unique_params.go.golden +++ b/internal/lsp/testdata/missingfunction/unique_params.go.golden @@ -1,20 +1,3 @@ --- suggestedfix_unique_params_10_2 -- -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package missingfunction - -func uniqueArguments() { - var s string - var i int - undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix", "") -} - -func undefinedUniqueArguments(s1 string, i int, s2 string) { - panic("implement me!") -} - -- suggestedfix_unique_params_6_2 -- package missingfunction diff --git a/internal/lsp/testdata/signature/signature.go.golden b/internal/lsp/testdata/signature/signature.go.golden index d7a65b3b873..90a4facf9a7 100644 --- a/internal/lsp/testdata/signature/signature.go.golden +++ b/internal/lsp/testdata/signature/signature.go.golden @@ -10,12 +10,6 @@ Bar(float64, ...byte) -- Foo(a string, b int) (c bool)-signature -- Foo(a string, b int) (c bool) --- GetAlias() Alias-signature -- -GetAlias() Alias - --- GetAliasPtr() *Alias-signature -- -GetAliasPtr() *Alias - -- Next(n int) []byte-signature -- Next(n int) []byte @@ -24,12 +18,6 @@ Next returns a slice containing the next n bytes from the buffer, advancing the -- OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias-signature -- OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias --- SetAliasSlice(a []*Alias)-signature -- -SetAliasSlice(a []*Alias) - --- SetOtherAliasMap(a map[*Alias]OtherAlias)-signature -- -SetOtherAliasMap(a map[*Alias]OtherAlias) - -- fn(hi string, there string) func(i int) rune-signature -- fn(hi string, there string) func(i int) rune diff --git a/internal/lsp/testdata/signature/signature_test.go.golden b/internal/lsp/testdata/signature/signature_test.go.golden index 3853dffc905..9e6561ac529 100644 --- a/internal/lsp/testdata/signature/signature_test.go.golden +++ b/internal/lsp/testdata/signature/signature_test.go.golden @@ -1,30 +1,9 @@ -- AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias)-signature -- AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias) --- AliasMap(a map[*signature.Alias]signature.StringAlias) (b map[*signature.Alias]signature.StringAlias, c map[*signature.Alias]signature.StringAlias)-signature -- -AliasMap(a map[*signature.Alias]signature.StringAlias) (b map[*signature.Alias]signature.StringAlias, c map[*signature.Alias]signature.StringAlias) - -- AliasSlice(a []*sig.Alias) (b sig.Alias)-signature -- AliasSlice(a []*sig.Alias) (b sig.Alias) --- AliasSlice(a []*signature.Alias) (b signature.Alias)-signature -- -AliasSlice(a []*signature.Alias) (b signature.Alias) - --- GetAlias() signature.Alias-signature -- -GetAlias() signature.Alias - --- GetAliasPtr() *signature.Alias-signature -- -GetAliasPtr() *signature.Alias - -- OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias-signature -- OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias --- OtherAliasMap(a map[signature.Alias]signature.OtherAlias, b map[signature.Alias]signature.OtherAlias) map[signature.Alias]signature.OtherAlias-signature -- -OtherAliasMap(a map[signature.Alias]signature.OtherAlias, b map[signature.Alias]signature.OtherAlias) map[signature.Alias]signature.OtherAlias - --- SetAliasSlice(a []*signature.Alias)-signature -- -SetAliasSlice(a []*signature.Alias) - --- SetOtherAliasMap(a map[*signature.Alias]signature.OtherAlias)-signature -- -SetOtherAliasMap(a map[*signature.Alias]signature.OtherAlias) - diff --git a/internal/lsp/testdata/snippets/literal.go.golden b/internal/lsp/testdata/snippets/literal.go.golden index f9725f73305..c91e5e9e086 100644 --- a/internal/lsp/testdata/snippets/literal.go.golden +++ b/internal/lsp/testdata/snippets/literal.go.golden @@ -1,6 +1,3 @@ -- X(_ map[signature.Alias]t.CoolAlias) map[signature.Alias]t.CoolAlias-signature -- X(_ map[signature.Alias]t.CoolAlias) map[signature.Alias]t.CoolAlias --- X(_ map[signatures.Alias]types.CoolAlias) map[signatures.Alias]types.CoolAlias-signature -- -X(_ map[signatures.Alias]types.CoolAlias) map[signatures.Alias]types.CoolAlias - From fe1a27b55bb12199b0a4f5d12f757763a8ded30e Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 30 Aug 2022 16:26:24 -0400 Subject: [PATCH 207/723] gopls/doc: make doc generation work regardless of the current directory As a nice side effect, make it easier to migrate internal/lsp/ to gopls/internal/lsp. For golang/go#54509 Change-Id: Ib541c08426f1f1d1e2a42b2d1cab47eab96dc092 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426775 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/doc/generate.go | 35 ++++++++++++++++++++++++++++------- gopls/doc/generate_test.go | 2 +- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go index c7e0e0ffcc0..28e0444a467 100644 --- a/gopls/doc/generate.go +++ b/gopls/doc/generate.go @@ -20,6 +20,7 @@ import ( "io" "io/ioutil" "os" + "os/exec" "path/filepath" "reflect" "regexp" @@ -39,37 +40,57 @@ import ( ) func main() { - if _, err := doMain("..", true); err != nil { + if _, err := doMain(true); err != nil { fmt.Fprintf(os.Stderr, "Generation failed: %v\n", err) os.Exit(1) } } -func doMain(baseDir string, write bool) (bool, error) { +func doMain(write bool) (bool, error) { api, err := loadAPI() if err != nil { return false, err } - if ok, err := rewriteFile(filepath.Join(baseDir, "internal/lsp/source/api_json.go"), api, write, rewriteAPI); !ok || err != nil { + sourceDir, err := pkgDir("golang.org/x/tools/internal/lsp/source") + if err != nil { + return false, err + } + + if ok, err := rewriteFile(filepath.Join(sourceDir, "api_json.go"), api, write, rewriteAPI); !ok || err != nil { return ok, err } - if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/settings.md"), api, write, rewriteSettings); !ok || err != nil { + + goplsDir, err := pkgDir("golang.org/x/tools/gopls") + if err != nil { + return false, err + } + + if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "settings.md"), api, write, rewriteSettings); !ok || err != nil { return ok, err } - if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/commands.md"), api, write, rewriteCommands); !ok || err != nil { + if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "commands.md"), api, write, rewriteCommands); !ok || err != nil { return ok, err } - if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/analyzers.md"), api, write, rewriteAnalyzers); !ok || err != nil { + if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "analyzers.md"), api, write, rewriteAnalyzers); !ok || err != nil { return ok, err } - if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/inlayHints.md"), api, write, rewriteInlayHints); !ok || err != nil { + if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "inlayHints.md"), api, write, rewriteInlayHints); !ok || err != nil { return ok, err } return true, nil } +// pkgDir returns the directory corresponding to the import path pkgPath. +func pkgDir(pkgPath string) (string, error) { + out, err := exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath).Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil +} + func loadAPI() (*source.APIJSON, error) { pkgs, err := packages.Load( &packages.Config{ diff --git a/gopls/doc/generate_test.go b/gopls/doc/generate_test.go index 137a646cd8d..d33594d6159 100644 --- a/gopls/doc/generate_test.go +++ b/gopls/doc/generate_test.go @@ -16,7 +16,7 @@ import ( func TestGenerated(t *testing.T) { testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. - ok, err := doMain("../..", false) + ok, err := doMain(false) if err != nil { t.Fatal(err) } From 41c3a9b12b01312abe6d8167323025d2210c0ad1 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 29 Aug 2022 16:11:04 -0400 Subject: [PATCH 208/723] internal/lsp/analysis/fillreturns: be defensive w.r.t. type errors In the presence of type errors, TypeOf may return nil, and this appears to have contributed to a crash in the fillreturns analysis. I haven't been able to find or deduce a test case, but this change makes the logic more defensive. Also remove a stale pre-go1.17 test that used to trigger a panic (the one fixed here? unclear) to assert that panics were recoverable. Updates golang/go#54655 Change-Id: Ic9ca9a307eede50a2d4db6424822a155dd43e635 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426019 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan --- .../regtest/diagnostics/diagnostics_test.go | 31 ------------------- .../lsp/analysis/fillreturns/fillreturns.go | 16 ++++++---- 2 files changed, 10 insertions(+), 37 deletions(-) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 209e015e0ef..4d34b0aa202 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1868,37 +1868,6 @@ package main }) } -// Tests golang/go#45075: A panic in fillreturns broke diagnostics. -// Expect an error log indicating that fillreturns panicked, as well type -// errors for the broken code. -func TestFillReturnsPanic(t *testing.T) { - // At tip, the panic no longer reproduces. - testenv.SkipAfterGo1Point(t, 16) - - const files = ` --- go.mod -- -module mod.com - -go 1.15 --- main.go -- -package main - -func foo() int { - return x, nil -} -` - Run(t, files, func(t *testing.T, env *Env) { - env.OpenFile("main.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - LogMatching(protocol.Error, `.*analysis fillreturns.*panicked.*`, 1, true), - env.DiagnosticAtRegexpWithMessage("main.go", `return x`, "wrong number of return values"), - ), - ) - }) -} - // This test confirms that the view does not reinitialize when a go.mod file is // opened. func TestNoReinitialize(t *testing.T) { diff --git a/internal/lsp/analysis/fillreturns/fillreturns.go b/internal/lsp/analysis/fillreturns/fillreturns.go index ab2fa869115..8be83adb140 100644 --- a/internal/lsp/analysis/fillreturns/fillreturns.go +++ b/internal/lsp/analysis/fillreturns/fillreturns.go @@ -71,6 +71,8 @@ outer: } // Get the end position of the error. + // (This heuristic assumes that the buffer is formatted, + // at least up to the end position of the error.) var buf bytes.Buffer if err := format.Node(&buf, pass.Fset, file); err != nil { continue @@ -156,11 +158,16 @@ outer: fixed := make([]ast.Expr, len(enclosingFunc.Results.List)) // For each value in the return function declaration, find the leftmost element - // in the return statement that has the desired type. If no such element exits, + // in the return statement that has the desired type. If no such element exists, // fill in the missing value with the appropriate "zero" value. + // Beware that type information may be incomplete. var retTyps []types.Type for _, ret := range enclosingFunc.Results.List { - retTyps = append(retTyps, info.TypeOf(ret.Type)) + retTyp := info.TypeOf(ret.Type) + if retTyp == nil { + return nil, nil + } + retTyps = append(retTyps, retTyp) } matches := analysisinternal.FindMatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg) @@ -168,10 +175,7 @@ outer: var match ast.Expr var idx int for j, val := range remaining { - // TODO(adonovan): if TypeOf returns nil (as it may, since we - // RunDespiteErrors), then matchingTypes will panic in - // types.AssignableTo. Fix it, and audit for other instances. - if !matchingTypes(info.TypeOf(val), retTyp) { + if t := info.TypeOf(val); t == nil || !matchingTypes(t, retTyp) { continue } if !analysisinternal.IsZeroValue(val) { From cb91d6c88ffc59a49bff79b144ca36777007bb17 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 31 Aug 2022 13:47:17 -0400 Subject: [PATCH 209/723] internal/lsp/cache: clarify error control flow of analysis This change consolidates the logic of the memoized function for analysis (previously split across execAll and runAnalysis) to make the error control flow clearer. The new function, actionImpl, follows the same conventions as in other memoized functions. Errors from await (e.g. cancellation) are now separate from errors from analysis itself. Only the latter are now ignored in the diagnostics loop over the roots. Also, where previously the recursion would ignore failed dependencies (proceeding to call runAnalysis even after seeing an &actionData{err!=nil}), now the recursion aborts if one of the prerequisites fails, as it should. The only edges currently enabled are "horizontal" (inputs, not facts). This would explain the crash in golang/go#54798, in which the result of the nspector pass was not available to the structtag pass. Also: - merge the loops over deps in execAll and runAnalysis. - remove unnecessary checks for context cancellation in loops of pure computation. - don't suppress errors from awaitPromise. - add commentary. - turn two "can't happen" errors into panics. - remove unnecessary logging of analysis failures. Fixes golang/go#54798 Change-Id: Iefb42d2d074a31f2f717dc94c38aed7f1dab1c80 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426800 Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/lsp/cache/analysis.go | 223 +++++++++++++++------------------ 1 file changed, 101 insertions(+), 122 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index 3ddc3a0117a..fa10cc3d548 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -40,20 +40,21 @@ func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.A roots = append(roots, ah) } - // Check if the context has been canceled before running the analyses. - if ctx.Err() != nil { - return nil, ctx.Err() - } - + // Run and wait for all analyzers, and report diagnostics + // only from those that succeed. Ignore the others. var results []*source.Diagnostic for _, ah := range roots { - diagnostics, _, err := ah.analyze(ctx, s) + v, err := s.awaitPromise(ctx, ah.promise) if err != nil { - // Keep going if a single analyzer failed. - event.Error(ctx, fmt.Sprintf("analyzer %q failed", ah.analyzer.Name), err) - continue + return nil, err // wait was cancelled + } + + res := v.(actionResult) + if res.err != nil { + continue // analysis failed; ignore it. } - results = append(results, diagnostics...) + + results = append(results, res.data.diagnostics...) } return results, nil } @@ -70,18 +71,24 @@ type actionHandleKey source.Hash // package (as different analyzers are applied, either in sequence or // parallel), and across packages (as dependencies are analyzed). type actionHandle struct { - promise *memoize.Promise + promise *memoize.Promise // [actionResult] analyzer *analysis.Analyzer pkg *pkg } +// actionData is the successful result of analyzing a package. type actionData struct { diagnostics []*source.Diagnostic result interface{} objectFacts map[objectFactKey]analysis.Fact packageFacts map[packageFactKey]analysis.Fact - err error +} + +// actionResult holds the result of a call to actionImpl. +type actionResult struct { + data *actionData + err error } type objectFactKey struct { @@ -156,15 +163,8 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } promise, release := s.store.Promise(buildActionKey(a, ph), func(ctx context.Context, arg interface{}) interface{} { - snapshot := arg.(*snapshot) - // Analyze dependencies first. - results, err := execAll(ctx, snapshot, deps) - if err != nil { - return &actionData{ - err: err, - } - } - return runAnalysis(ctx, snapshot, a, pkg, results) + res, err := actionImpl(ctx, arg.(*snapshot), deps, a, pkg) + return actionResult{res, err} }) ah := &actionHandle{ @@ -187,21 +187,6 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A return ah, nil } -func (act *actionHandle) analyze(ctx context.Context, snapshot *snapshot) ([]*source.Diagnostic, interface{}, error) { - d, err := snapshot.awaitPromise(ctx, act.promise) - if err != nil { - return nil, nil, err - } - data, ok := d.(*actionData) - if !ok { - return nil, nil, fmt.Errorf("unexpected type for %s:%s", act.pkg.ID(), act.analyzer.Name) - } - if data == nil { - return nil, nil, fmt.Errorf("unexpected nil analysis for %s:%s", act.pkg.ID(), act.analyzer.Name) - } - return data.diagnostics, data.result, data.err -} - func buildActionKey(a *analysis.Analyzer, ph *packageHandle) actionHandleKey { return actionHandleKey(source.Hashf("%p%s", a, ph.key[:])) } @@ -210,80 +195,71 @@ func (act *actionHandle) String() string { return fmt.Sprintf("%s@%s", act.analyzer, act.pkg.PkgPath()) } -func execAll(ctx context.Context, snapshot *snapshot, actions []*actionHandle) (map[*actionHandle]*actionData, error) { - var mu sync.Mutex - results := make(map[*actionHandle]*actionData) - +// actionImpl runs the analysis for action node (analyzer, pkg), +// whose direct dependencies are deps. +func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, analyzer *analysis.Analyzer, pkg *pkg) (*actionData, error) { + // Run action dependencies first, and plumb the results and + // facts of each dependency into the inputs of this action. + var ( + mu sync.Mutex + inputs = make(map[*analysis.Analyzer]interface{}) + objectFacts = make(map[objectFactKey]analysis.Fact) + packageFacts = make(map[packageFactKey]analysis.Fact) + ) g, ctx := errgroup.WithContext(ctx) - for _, act := range actions { - act := act + for _, dep := range deps { + dep := dep g.Go(func() error { - v, err := snapshot.awaitPromise(ctx, act.promise) + v, err := snapshot.awaitPromise(ctx, dep.promise) if err != nil { - return err + return err // e.g. cancelled } - data, ok := v.(*actionData) - if !ok { - return fmt.Errorf("unexpected type for %s: %T", act, v) + res := v.(actionResult) + if res.err != nil { + return res.err // analysis of dependency failed } + data := res.data mu.Lock() defer mu.Unlock() - results[act] = data - + if dep.pkg == pkg { + // Same package, different analysis (horizontal edge): + // in-memory outputs of prerequisite analyzers + // become inputs to this analysis pass. + inputs[dep.analyzer] = data.result + } else if dep.analyzer == analyzer { // (always true) + // Same analysis, different package (vertical edge): + // serialized facts produced by prerequisite analysis + // become available to this analysis pass. + for key, fact := range data.objectFacts { + // Filter out facts related to objects + // that are irrelevant downstream + // (equivalently: not in the compiler export data). + if !exportedFrom(key.obj, dep.pkg.types) { + continue + } + objectFacts[key] = fact + } + for key, fact := range data.packageFacts { + // TODO: filter out facts that belong to + // packages not mentioned in the export data + // to prevent side channels. + packageFacts[key] = fact + } + } return nil }) } - return results, g.Wait() -} - -func runAnalysis(ctx context.Context, snapshot *snapshot, analyzer *analysis.Analyzer, pkg *pkg, deps map[*actionHandle]*actionData) (data *actionData) { - data = &actionData{ - objectFacts: make(map[objectFactKey]analysis.Fact), - packageFacts: make(map[packageFactKey]analysis.Fact), - } - - // Plumb the output values of the dependencies - // into the inputs of this action. Also facts. - inputs := make(map[*analysis.Analyzer]interface{}) - - for depHandle, depData := range deps { - if depHandle.pkg == pkg { - // Same package, different analysis (horizontal edge): - // in-memory outputs of prerequisite analyzers - // become inputs to this analysis pass. - inputs[depHandle.analyzer] = depData.result - } else if depHandle.analyzer == analyzer { // (always true) - // Same analysis, different package (vertical edge): - // serialized facts produced by prerequisite analysis - // become available to this analysis pass. - for key, fact := range depData.objectFacts { - // Filter out facts related to objects - // that are irrelevant downstream - // (equivalently: not in the compiler export data). - if !exportedFrom(key.obj, depHandle.pkg.types) { - continue - } - data.objectFacts[key] = fact - } - for key, fact := range depData.packageFacts { - // TODO: filter out facts that belong to - // packages not mentioned in the export data - // to prevent side channels. - - data.packageFacts[key] = fact - } - } + if err := g.Wait(); err != nil { + return nil, err // e.g. cancelled } + // Now run the (pkg, analyzer) analysis. var syntax []*ast.File for _, cgf := range pkg.compiledGoFiles { syntax = append(syntax, cgf.File) } - - var diagnostics []*analysis.Diagnostic - - // Run the analysis. + var rawDiagnostics []analysis.Diagnostic pass := &analysis.Pass{ Analyzer: analyzer, Fset: snapshot.FileSet(), @@ -299,7 +275,7 @@ func runAnalysis(ctx context.Context, snapshot *snapshot, analyzer *analysis.Ana } else { d.Category = analyzer.Name + "." + d.Category } - diagnostics = append(diagnostics, &d) + rawDiagnostics = append(rawDiagnostics, d) }, ImportObjectFact: func(obj types.Object, ptr analysis.Fact) bool { if obj == nil { @@ -307,7 +283,7 @@ func runAnalysis(ctx context.Context, snapshot *snapshot, analyzer *analysis.Ana } key := objectFactKey{obj, factType(ptr)} - if v, ok := data.objectFacts[key]; ok { + if v, ok := objectFacts[key]; ok { reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem()) return true } @@ -319,14 +295,14 @@ func runAnalysis(ctx context.Context, snapshot *snapshot, analyzer *analysis.Ana analyzer, pkg.ID(), obj, fact)) } key := objectFactKey{obj, factType(fact)} - data.objectFacts[key] = fact // clobber any existing entry + objectFacts[key] = fact // clobber any existing entry }, ImportPackageFact: func(pkg *types.Package, ptr analysis.Fact) bool { if pkg == nil { panic("nil package") } key := packageFactKey{pkg, factType(ptr)} - if v, ok := data.packageFacts[key]; ok { + if v, ok := packageFacts[key]; ok { reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem()) return true } @@ -334,52 +310,53 @@ func runAnalysis(ctx context.Context, snapshot *snapshot, analyzer *analysis.Ana }, ExportPackageFact: func(fact analysis.Fact) { key := packageFactKey{pkg.types, factType(fact)} - data.packageFacts[key] = fact // clobber any existing entry + packageFacts[key] = fact // clobber any existing entry }, AllObjectFacts: func() []analysis.ObjectFact { - facts := make([]analysis.ObjectFact, 0, len(data.objectFacts)) - for k := range data.objectFacts { - facts = append(facts, analysis.ObjectFact{Object: k.obj, Fact: data.objectFacts[k]}) + facts := make([]analysis.ObjectFact, 0, len(objectFacts)) + for k := range objectFacts { + facts = append(facts, analysis.ObjectFact{Object: k.obj, Fact: objectFacts[k]}) } return facts }, AllPackageFacts: func() []analysis.PackageFact { - facts := make([]analysis.PackageFact, 0, len(data.packageFacts)) - for k := range data.packageFacts { - facts = append(facts, analysis.PackageFact{Package: k.pkg, Fact: data.packageFacts[k]}) + facts := make([]analysis.PackageFact, 0, len(packageFacts)) + for k := range packageFacts { + facts = append(facts, analysis.PackageFact{Package: k.pkg, Fact: packageFacts[k]}) } return facts }, } analysisinternal.SetTypeErrors(pass, pkg.typeErrors) + // We never run analyzers on ill-typed code, + // even those marked RunDespiteErrors=true. if pkg.IsIllTyped() { - data.err = fmt.Errorf("analysis skipped due to errors in package") - return data + return nil, fmt.Errorf("analysis skipped due to errors in package") } // Recover from panics (only) within the analyzer logic. // (Use an anonymous function to limit the recover scope.) + var result interface{} + var err error func() { defer func() { if r := recover(); r != nil { // TODO(adonovan): use bug.Errorf here so that we // detect crashes covered by our test suite. - // e.g. - data.err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) + err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) } }() - data.result, data.err = pass.Analyzer.Run(pass) + result, err = pass.Analyzer.Run(pass) }() - if data.err != nil { - return data + if err != nil { + return nil, err } - if got, want := reflect.TypeOf(data.result), pass.Analyzer.ResultType; got != want { - data.err = fmt.Errorf( + if got, want := reflect.TypeOf(result), pass.Analyzer.ResultType; got != want { + return nil, fmt.Errorf( "internal error: on package %s, analyzer %s returned a result of type %v, but declared ResultType %v", pass.Pkg.Path(), pass.Analyzer, got, want) - return data } // disallow calls after Run @@ -390,19 +367,21 @@ func runAnalysis(ctx context.Context, snapshot *snapshot, analyzer *analysis.Ana panic(fmt.Sprintf("%s:%s: Pass.ExportPackageFact(%T) called after Run", analyzer.Name, pkg.PkgPath(), fact)) } - for _, diag := range diagnostics { - srcDiags, err := analysisDiagnosticDiagnostics(snapshot, pkg, analyzer, diag) + var diagnostics []*source.Diagnostic + for _, diag := range rawDiagnostics { + srcDiags, err := analysisDiagnosticDiagnostics(snapshot, pkg, analyzer, &diag) if err != nil { event.Error(ctx, "unable to compute analysis error position", err, tag.Category.Of(diag.Category), tag.Package.Of(pkg.ID())) continue } - if ctx.Err() != nil { - data.err = ctx.Err() - return data - } - data.diagnostics = append(data.diagnostics, srcDiags...) + diagnostics = append(diagnostics, srcDiags...) } - return data + return &actionData{ + diagnostics: diagnostics, + result: result, + objectFacts: objectFacts, + packageFacts: packageFacts, + }, nil } // exportedFrom reports whether obj may be visible to a package that imports pkg. From 3eb8a8f04e850749214aad381b203dfc1f3a5916 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 31 Aug 2022 15:29:37 -0400 Subject: [PATCH 210/723] internal/lsp/cache: delete misleading Package.IllTyped method This method does not in fact report whether the package is ill-typed. It actually reports whether any of three fields of pkg are nil. They never are. This does rather explain why type-error analyzers are executed on (genuinely) ill-typed packages, despite the !IllTyped() condition. And why analyzers that aren't prepared to handle ill-typed code (!RunDespiteErrors) sometimes crash in production. Updates golang/go#54762 Change-Id: I95421584bec68fb849b5ed52cc4e6d9b6bb679be Reviewed-on: https://go-review.googlesource.com/c/tools/+/426802 Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Alan Donovan --- internal/lsp/cache/analysis.go | 8 +++----- internal/lsp/cache/cache.go | 9 ++------- internal/lsp/cache/pkg.go | 4 ---- internal/lsp/source/rename.go | 5 +---- internal/lsp/source/view.go | 1 - 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index fa10cc3d548..5a29feffa7f 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -329,11 +329,9 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a } analysisinternal.SetTypeErrors(pass, pkg.typeErrors) - // We never run analyzers on ill-typed code, - // even those marked RunDespiteErrors=true. - if pkg.IsIllTyped() { - return nil, fmt.Errorf("analysis skipped due to errors in package") - } + // TODO(adonovan): fix: don't run analyzers on packages + // containing type errors unless the analyzers have + // RunDespiteErrors=true. // Recover from panics (only) within the analyzer logic. // (Use an anonymous function to limit the recover scope.) diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index c002850653b..eea302187d5 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -226,13 +226,8 @@ func (c *Cache) PackageStats(withNames bool) template.HTML { if v.pkg == nil { break } - var typsCost, typInfoCost int64 - if v.pkg.types != nil { - typsCost = typesCost(v.pkg.types.Scope()) - } - if v.pkg.typesInfo != nil { - typInfoCost = typesInfoCost(v.pkg.typesInfo) - } + typsCost := typesCost(v.pkg.types.Scope()) + typInfoCost := typesInfoCost(v.pkg.typesInfo) stat := packageStat{ id: v.pkg.m.ID, mode: v.pkg.mode, diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go index 1217ec29fd4..3478c58ad1f 100644 --- a/internal/lsp/cache/pkg.go +++ b/internal/lsp/cache/pkg.go @@ -93,10 +93,6 @@ func (p *pkg) GetTypesSizes() types.Sizes { return p.typesSizes } -func (p *pkg) IsIllTyped() bool { - return p.types == nil || p.typesInfo == nil || p.typesSizes == nil -} - func (p *pkg) ForTest() string { return string(p.m.ForTest) } diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go index 6bbe91afae6..85d7c12f945 100644 --- a/internal/lsp/source/rename.go +++ b/internal/lsp/source/rename.go @@ -158,7 +158,7 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, return nil, err } - obj, pkg := qos[0].obj, qos[0].pkg + obj := qos[0].obj if err := checkRenamable(obj); err != nil { return nil, err @@ -169,9 +169,6 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, if !isValidIdentifier(newName) { return nil, fmt.Errorf("invalid identifier to rename: %q", newName) } - if pkg == nil || pkg.IsIllTyped() { - return nil, fmt.Errorf("package for %s is ill typed", f.URI()) - } refs, err := references(ctx, s, qos, true, false, true) if err != nil { return nil, err diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index a188e3666b1..498299e1f7f 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -639,7 +639,6 @@ type Package interface { GetTypes() *types.Package GetTypesInfo() *types.Info GetTypesSizes() types.Sizes - IsIllTyped() bool ForTest() string GetImport(pkgPath string) (Package, error) MissingDependencies() []string From 4ccc73cbb5c77d2b77a1bcf94c9493551a27be15 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 30 Aug 2022 18:35:02 -0400 Subject: [PATCH 211/723] internal/lsp/tests: simplify comparison of markdown at go1.18 In several places throughout the x/tools codebase, the internal/lsp/diff package is used to provide clearer test output when comparing large, multi-line strings. In some places, this is implemented via the tests.Diff helper function, but in others it is not (presumably due to import cycles). Factor out this pattern into a diff.Pretty function, and add commentary. Also remove the *testing.T argument, as diffs should never fail; opt to panic instead. Also add a test. Using this function, simplify the logic to comparing our 1.18 markdown output with 1.19 golden content, by normalizing differences between the two. This is necessary as markdown content will change as a result of moving internal/lsp to gopls/internal/lsp. For golang/go#54509 Change-Id: Ie1fea1091bbbeb49e00c4efa7e02707cafa415cc Reviewed-on: https://go-review.googlesource.com/c/tools/+/426776 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan --- .../regtest/codelens/codelens_test.go | 12 +- .../internal/regtest/misc/definition_test.go | 4 +- gopls/internal/regtest/misc/fix_test.go | 4 +- .../internal/regtest/misc/formatting_test.go | 19 ++- gopls/internal/regtest/misc/import_test.go | 4 +- .../internal/regtest/modfile/modfile_test.go | 30 ++--- internal/lsp/cmd/test/suggested_fix.go | 3 +- internal/lsp/cmd/test/workspace_symbol.go | 3 +- internal/lsp/diff/unified.go | 48 +++++--- internal/lsp/lsp_test.go | 23 ++-- internal/lsp/source/format_test.go | 26 +--- internal/lsp/source/source_test.go | 18 ++- internal/lsp/tests/compare/text.go | 48 ++++++++ internal/lsp/tests/compare/text_test.go | 28 +++++ internal/lsp/tests/markdown_go118.go | 63 ++++++++++ .../{metadata_go119.go => markdown_go119.go} | 7 +- internal/lsp/tests/metadata_go118.go | 116 ------------------ internal/lsp/tests/tests.go | 3 +- internal/lsp/tests/util.go | 14 --- 19 files changed, 238 insertions(+), 235 deletions(-) create mode 100644 internal/lsp/tests/compare/text.go create mode 100644 internal/lsp/tests/compare/text_test.go create mode 100644 internal/lsp/tests/markdown_go118.go rename internal/lsp/tests/{metadata_go119.go => markdown_go119.go} (78%) delete mode 100644 internal/lsp/tests/metadata_go118.go diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 73752f3adf9..f67e2dae66a 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -11,10 +11,10 @@ import ( "golang.org/x/tools/gopls/internal/hooks" "golang.org/x/tools/internal/lsp/bug" . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/internal/lsp/tests/compare" "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" "golang.org/x/tools/internal/testenv" ) @@ -187,10 +187,10 @@ require golang.org/x/hello v1.2.3 } env.Await(env.DoneWithChangeWatchedFiles()) if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA { - t.Fatalf("a/go.mod upgrade failed:\n%s", tests.Diff(t, wantGoModA, got)) + t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) } if got := env.Editor.BufferText("b/go.mod"); got != wantGoModB { - t.Fatalf("b/go.mod changed unexpectedly:\n%s", tests.Diff(t, wantGoModB, got)) + t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) } }) }) @@ -220,10 +220,10 @@ require golang.org/x/hello v1.2.3 env.ApplyQuickFixes("a/go.mod", d.Diagnostics) env.Await(env.DoneWithChangeWatchedFiles()) if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA { - t.Fatalf("a/go.mod upgrade failed:\n%s", tests.Diff(t, wantGoModA, got)) + t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) } if got := env.Editor.BufferText("b/go.mod"); got != wantGoModB { - t.Fatalf("b/go.mod changed unexpectedly:\n%s", tests.Diff(t, wantGoModB, got)) + t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) } }) }) @@ -285,7 +285,7 @@ go 1.14 require golang.org/x/hello v1.0.0 ` if got != wantGoMod { - t.Fatalf("go.mod tidy failed:\n%s", tests.Diff(t, wantGoMod, got)) + t.Fatalf("go.mod tidy failed:\n%s", compare.Text(wantGoMod, got)) } }) } diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index b71cf231079..1842cb5bb3c 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -11,10 +11,10 @@ import ( "golang.org/x/tools/internal/lsp/protocol" . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/internal/lsp/tests/compare" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/tests" ) const internalDefinition = ` @@ -133,7 +133,7 @@ func main() { } want := "```go\nfunc (error).Error() string\n```" if content.Value != want { - t.Fatalf("hover failed:\n%s", tests.Diff(t, want, content.Value)) + t.Fatalf("hover failed:\n%s", compare.Text(want, content.Value)) } }) } diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index 8318ae557da..6c3ea7c0bbb 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -8,9 +8,9 @@ import ( "testing" . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/internal/lsp/tests/compare" "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" ) // A basic test for fillstruct, now that it uses a command. @@ -56,7 +56,7 @@ func Foo() { } ` if got := env.Editor.BufferText("main.go"); got != want { - t.Fatalf("TestFillStruct failed:\n%s", tests.Diff(t, want, got)) + t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) } }) } diff --git a/gopls/internal/regtest/misc/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go index 71b8cadab40..a697b7a1b5a 100644 --- a/gopls/internal/regtest/misc/formatting_test.go +++ b/gopls/internal/regtest/misc/formatting_test.go @@ -9,8 +9,7 @@ import ( "testing" . "golang.org/x/tools/internal/lsp/regtest" - - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/internal/lsp/tests/compare" ) const unformattedProgram = ` @@ -37,7 +36,7 @@ func TestFormatting(t *testing.T) { got := env.Editor.BufferText("main.go") want := env.ReadWorkspaceFile("main.go.golden") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -59,7 +58,7 @@ func f() {} got := env.Editor.BufferText("a.go") want := env.ReadWorkspaceFile("a.go.formatted") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -83,7 +82,7 @@ func f() { fmt.Println() } got := env.Editor.BufferText("a.go") want := env.ReadWorkspaceFile("a.go.imported") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -104,7 +103,7 @@ func f() {} got := env.Editor.BufferText("a.go") want := env.ReadWorkspaceFile("a.go.imported") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -150,7 +149,7 @@ func TestOrganizeImports(t *testing.T) { got := env.Editor.BufferText("main.go") want := env.ReadWorkspaceFile("main.go.organized") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -162,7 +161,7 @@ func TestFormattingOnSave(t *testing.T) { got := env.Editor.BufferText("main.go") want := env.ReadWorkspaceFile("main.go.formatted") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -262,7 +261,7 @@ func main() { got := env.Editor.BufferText("main.go") got = strings.ReplaceAll(got, "\r\n", "\n") // convert everything to LF for simplicity if tt.want != got { - t.Errorf("unexpected content after save:\n%s", tests.Diff(t, tt.want, got)) + t.Errorf("unexpected content after save:\n%s", compare.Text(tt.want, got)) } }) }) @@ -361,7 +360,7 @@ const Bar = 42 got := env.Editor.BufferText("foo.go") want := env.ReadWorkspaceFile("foo.go.formatted") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } diff --git a/gopls/internal/regtest/misc/import_test.go b/gopls/internal/regtest/misc/import_test.go index d5b6bcf43f1..ef4fbde362b 100644 --- a/gopls/internal/regtest/misc/import_test.go +++ b/gopls/internal/regtest/misc/import_test.go @@ -11,7 +11,7 @@ import ( "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/protocol" . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/internal/lsp/tests/compare" ) func TestAddImport(t *testing.T) { @@ -51,7 +51,7 @@ func main() { }, nil) got := env.Editor.BufferText("main.go") if got != want { - t.Fatalf("gopls.add_import failed\n%s", tests.Diff(t, want, got)) + t.Fatalf("gopls.add_import failed\n%s", compare.Text(want, got)) } }) } diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 8dabc43f6a4..dee7cb34933 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -13,9 +13,9 @@ import ( "golang.org/x/tools/gopls/internal/hooks" "golang.org/x/tools/internal/lsp/bug" . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/internal/lsp/tests/compare" "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" "golang.org/x/tools/internal/testenv" ) @@ -101,7 +101,7 @@ func main() { ), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { - t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got)) + t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) } // Save the buffer, which will format and organize imports. // Confirm that the go.mod file still does not change. @@ -110,7 +110,7 @@ func main() { env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { - t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got)) + t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) } }) }) @@ -154,7 +154,7 @@ func main() { ), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { - t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got)) + t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) } }) }) @@ -206,7 +206,7 @@ require example.com v1.2.3 } env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{goGetDiag}) if got := env.ReadWorkspaceFile("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -256,7 +256,7 @@ require random.org v1.2.3 } env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{randomDiag}) if got := env.ReadWorkspaceFile("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -312,7 +312,7 @@ require random.org v1.2.3 } env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{randomDiag}) if got := env.ReadWorkspaceFile("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -359,7 +359,7 @@ require example.com v1.2.3 ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) if got := env.Editor.BufferText("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -404,7 +404,7 @@ go 1.14 ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) if got := env.Editor.BufferText("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -476,7 +476,7 @@ require ( ) ` if got := env.ReadWorkspaceFile("a/go.mod"); got != want { - t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", tests.Diff(t, want, got)) + t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", compare.Text(want, got)) } }) } @@ -585,7 +585,7 @@ require ( env.SaveBuffer("a/go.mod") env.Await(EmptyDiagnostics("a/main.go")) if got := env.Editor.BufferText("a/go.mod"); got != want { - t.Fatalf("suggested fixes failed:\n%s", tests.Diff(t, want, got)) + t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got)) } }) } @@ -773,7 +773,7 @@ func main() { ) got := env.ReadWorkspaceFile("go.mod") if got != original { - t.Fatalf("go.mod file modified:\n%s", tests.Diff(t, original, got)) + t.Fatalf("go.mod file modified:\n%s", compare.Text(original, got)) } env.RunGoCommand("get", "example.com/blah@v1.2.3") env.RunGoCommand("mod", "tidy") @@ -1026,7 +1026,7 @@ go 1.12 ` env.ApplyQuickFixes("go.mod", d.Diagnostics) if got := env.Editor.BufferText("go.mod"); got != want { - t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) } }) }) @@ -1079,7 +1079,7 @@ require random.com v1.2.3 } env.ApplyQuickFixes("go.mod", diagnostics) if got := env.Editor.BufferText("go.mod"); got != want { - t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) } }) }) @@ -1125,7 +1125,7 @@ func main() { example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ` if got := env.ReadWorkspaceFile("go.sum"); got != want { - t.Fatalf("unexpected go.sum contents:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.sum contents:\n%s", compare.Text(want, got)) } }) } diff --git a/internal/lsp/cmd/test/suggested_fix.go b/internal/lsp/cmd/test/suggested_fix.go index db401350fb1..1481d8bad48 100644 --- a/internal/lsp/cmd/test/suggested_fix.go +++ b/internal/lsp/cmd/test/suggested_fix.go @@ -9,6 +9,7 @@ import ( "testing" "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" ) @@ -32,6 +33,6 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, suggestedFixes []test return []byte(got), nil })) if want != got { - t.Errorf("suggested fixes failed for %s:\n%s", filename, tests.Diff(t, want, got)) + t.Errorf("suggested fixes failed for %s:\n%s", filename, compare.Text(want, got)) } } diff --git a/internal/lsp/cmd/test/workspace_symbol.go b/internal/lsp/cmd/test/workspace_symbol.go index ce965f03a31..244ce04905e 100644 --- a/internal/lsp/cmd/test/workspace_symbol.go +++ b/internal/lsp/cmd/test/workspace_symbol.go @@ -13,6 +13,7 @@ import ( "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" ) @@ -48,6 +49,6 @@ func (r *runner) runWorkspaceSymbols(t *testing.T, uri span.URI, matcher, query })) if expect != got { - t.Errorf("workspace_symbol failed for %s:\n%s", query, tests.Diff(t, expect, got)) + t.Errorf("workspace_symbol failed for %s:\n%s", query, compare.Text(expect, got)) } } diff --git a/internal/lsp/diff/unified.go b/internal/lsp/diff/unified.go index 323471d2046..f9eaf4cdd4b 100644 --- a/internal/lsp/diff/unified.go +++ b/internal/lsp/diff/unified.go @@ -75,10 +75,10 @@ const ( // ToUnified takes a file contents and a sequence of edits, and calculates // a unified diff that represents those edits. -func ToUnified(from, to string, content string, edits []TextEdit) Unified { +func ToUnified(fromName, toName string, content string, edits []TextEdit) Unified { u := Unified{ - From: from, - To: to, + From: fromName, + To: toName, } if len(edits) == 0 { return u @@ -160,14 +160,25 @@ func addEqualLines(h *Hunk, lines []string, start, end int) int { return delta } -// Format converts a unified diff to the standard textual form for that diff. -// The output of this function can be passed to tools like patch. +// Format write a textual representation of u to f (see the String method). +// +// TODO(rfindley): investigate (and possibly remove) this method. It's not +// clear why Unified implements fmt.Formatter, since the formatting rune is not +// used. Probably it is sufficient to only implement Stringer, but this method +// was left here defensively. func (u Unified) Format(f fmt.State, r rune) { + fmt.Fprintf(f, "%s", u.String()) +} + +// String converts a unified diff to the standard textual form for that diff. +// The output of this function can be passed to tools like patch. +func (u Unified) String() string { if len(u.Hunks) == 0 { - return + return "" } - fmt.Fprintf(f, "--- %s\n", u.From) - fmt.Fprintf(f, "+++ %s\n", u.To) + b := new(strings.Builder) + fmt.Fprintf(b, "--- %s\n", u.From) + fmt.Fprintf(b, "+++ %s\n", u.To) for _, hunk := range u.Hunks { fromCount, toCount := 0, 0 for _, l := range hunk.Lines { @@ -181,30 +192,31 @@ func (u Unified) Format(f fmt.State, r rune) { toCount++ } } - fmt.Fprint(f, "@@") + fmt.Fprint(b, "@@") if fromCount > 1 { - fmt.Fprintf(f, " -%d,%d", hunk.FromLine, fromCount) + fmt.Fprintf(b, " -%d,%d", hunk.FromLine, fromCount) } else { - fmt.Fprintf(f, " -%d", hunk.FromLine) + fmt.Fprintf(b, " -%d", hunk.FromLine) } if toCount > 1 { - fmt.Fprintf(f, " +%d,%d", hunk.ToLine, toCount) + fmt.Fprintf(b, " +%d,%d", hunk.ToLine, toCount) } else { - fmt.Fprintf(f, " +%d", hunk.ToLine) + fmt.Fprintf(b, " +%d", hunk.ToLine) } - fmt.Fprint(f, " @@\n") + fmt.Fprint(b, " @@\n") for _, l := range hunk.Lines { switch l.Kind { case Delete: - fmt.Fprintf(f, "-%s", l.Content) + fmt.Fprintf(b, "-%s", l.Content) case Insert: - fmt.Fprintf(f, "+%s", l.Content) + fmt.Fprintf(b, "+%s", l.Content) default: - fmt.Fprintf(f, " %s", l.Content) + fmt.Fprintf(b, " %s", l.Content) } if !strings.HasSuffix(l.Content, "\n") { - fmt.Fprintf(f, "\n\\ No newline at end of file\n") + fmt.Fprintf(b, "\n\\ No newline at end of file\n") } } } + return b.String() } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index def47b9fd1d..6591251a1c1 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -19,10 +19,10 @@ import ( "golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/command" "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -469,12 +469,9 @@ func (r *runner) Import(t *testing.T, spn span.Span) { want := string(r.data.Golden("goimports", filename, func() ([]byte, error) { return []byte(got), nil })) - if want != got { - d, err := myers.ComputeEdits(uri, want, got) - if err != nil { - t.Fatal(err) - } - t.Errorf("import failed for %s: %s", filename, diff.ToUnified("want", "got", want, d)) + + if d := compare.Text(want, got); d != "" { + t.Errorf("import failed for %s:\n%s", filename, d) } } @@ -572,7 +569,7 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.S return []byte(got), nil })) if want != got { - t.Errorf("suggested fixes failed for %s:\n%s", u.Filename(), tests.Diff(t, want, got)) + t.Errorf("suggested fixes failed for %s:\n%s", u.Filename(), compare.Text(want, got)) } } } @@ -624,7 +621,7 @@ func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span return []byte(got), nil })) if want != got { - t.Errorf("function extraction failed for %s:\n%s", u.Filename(), tests.Diff(t, want, got)) + t.Errorf("function extraction failed for %s:\n%s", u.Filename(), compare.Text(want, got)) } } } @@ -676,7 +673,7 @@ func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) return []byte(got), nil })) if want != got { - t.Errorf("method extraction failed for %s:\n%s", u.Filename(), tests.Diff(t, want, got)) + t.Errorf("method extraction failed for %s:\n%s", u.Filename(), compare.Text(want, got)) } } } @@ -1041,7 +1038,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { return []byte(got), nil })) if want != got { - t.Errorf("rename failed for %s:\n%s", newText, tests.Diff(t, want, got)) + t.Errorf("rename failed for %s:\n%s", newText, compare.Text(want, got)) } } @@ -1186,7 +1183,7 @@ func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) - if diff := tests.Diff(t, want, got); diff != "" { + if diff := compare.Text(want, got); diff != "" { t.Error(diff) } } @@ -1299,7 +1296,7 @@ func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) { if want == nil { t.Fatalf("golden file %q not found", uri.Filename()) } - if diff := tests.Diff(t, got, string(want)); diff != "" { + if diff := compare.Text(got, string(want)); diff != "" { t.Errorf("%s mismatch\n%s", command.AddImport, diff) } } diff --git a/internal/lsp/source/format_test.go b/internal/lsp/source/format_test.go index eac78d97989..ab120124e5a 100644 --- a/internal/lsp/source/format_test.go +++ b/internal/lsp/source/format_test.go @@ -5,12 +5,10 @@ package source import ( - "fmt" "strings" "testing" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" + "golang.org/x/tools/internal/lsp/tests/compare" ) func TestImportPrefix(t *testing.T) { @@ -39,8 +37,8 @@ func TestImportPrefix(t *testing.T) { if err != nil { t.Fatal(err) } - if got != tt.want { - t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(t, tt.want, got)) + if d := compare.Text(tt.want, got); d != "" { + t.Errorf("%d: failed for %q:\n%s", i, tt.input, d) } } } @@ -70,22 +68,8 @@ Hi description t.Fatal(err) } want := strings.ReplaceAll(tt.want, "\n", "\r\n") - if got != want { - t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(t, want, got)) + if d := compare.Text(want, got); d != "" { + t.Errorf("%d: failed for %q:\n%s", i, tt.input, d) } } } - -func diffStr(t *testing.T, want, got string) string { - if want == got { - return "" - } - // Add newlines to avoid newline messages in diff. - want += "\n" - got += "\n" - d, err := myers.ComputeEdits("", want, got) - if err != nil { - t.Fatal(err) - } - return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d)) -} diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index d5c9728bc88..761b9109240 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -18,12 +18,12 @@ import ( "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" "golang.org/x/tools/internal/lsp/fuzzy" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source/completion" "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -370,7 +370,7 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, data s return []byte(got), nil })) - if diff := tests.Diff(t, want, got); diff != "" { + if diff := compare.Text(want, got); diff != "" { t.Errorf("%s: foldingRanges failed for %s, diff:\n%v", tag, uri.Filename(), diff) } } @@ -397,7 +397,7 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, data s return []byte(got), nil })) - if diff := tests.Diff(t, want, got); diff != "" { + if diff := compare.Text(want, got); diff != "" { t.Errorf("%s: failed for %s, diff:\n%v", tag, uri.Filename(), diff) } } @@ -526,12 +526,8 @@ func (r *runner) Import(t *testing.T, spn span.Span) { want := string(r.data.Golden("goimports", spn.URI().Filename(), func() ([]byte, error) { return []byte(got), nil })) - if want != got { - d, err := myers.ComputeEdits(spn.URI(), want, got) - if err != nil { - t.Fatal(err) - } - t.Errorf("import failed for %s: %s", spn.URI().Filename(), diff.ToUnified("want", "got", want, d)) + if d := compare.Text(got, want); d != "" { + t.Errorf("import failed for %s:\n%s", spn.URI().Filename(), d) } } @@ -920,8 +916,8 @@ func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) - if diff := tests.Diff(t, want, got); diff != "" { - t.Error(diff) + if d := compare.Text(want, got); d != "" { + t.Error(d) } } diff --git a/internal/lsp/tests/compare/text.go b/internal/lsp/tests/compare/text.go new file mode 100644 index 00000000000..efc2e8cae67 --- /dev/null +++ b/internal/lsp/tests/compare/text.go @@ -0,0 +1,48 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package compare + +import ( + "fmt" + + "golang.org/x/tools/internal/lsp/diff" +) + +// Text returns a formatted unified diff of the edits to go from want to +// got, returning "" if and only if want == got. +// +// This function is intended for use in testing, and panics if any error occurs +// while computing the diff. It is not sufficiently tested for production use. +func Text(want, got string) string { + if want == got { + return "" + } + + // Add newlines to avoid verbose newline messages ("No newline at end of file"). + want += "\n" + got += "\n" + + d, err := diff.NComputeEdits("", want, got) + + // Panic on errors. + // + // TODO(rfindley): refactor so that this function doesn't need to panic. + // Computing diffs should never fail. + if err != nil { + panic(fmt.Sprintf("computing edits failed: %v", err)) + } + + diff := diff.ToUnified("want", "got", want, d).String() + + // Defensively assert that we get an actual diff, so that we guarantee the + // invariant that we return "" if and only if want == got. + // + // This is probably unnecessary, but convenient. + if diff == "" { + panic("empty diff for non-identical input") + } + + return diff +} diff --git a/internal/lsp/tests/compare/text_test.go b/internal/lsp/tests/compare/text_test.go new file mode 100644 index 00000000000..6b3aaea8c3f --- /dev/null +++ b/internal/lsp/tests/compare/text_test.go @@ -0,0 +1,28 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package compare_test + +import ( + "testing" + + "golang.org/x/tools/internal/lsp/tests/compare" +) + +func TestText(t *testing.T) { + tests := []struct { + got, want, wantDiff string + }{ + {"", "", ""}, + {"equal", "equal", ""}, + {"a", "b", "--- want\n+++ got\n@@ -1 +1 @@\n-b\n+a\n"}, + {"a\nd\nc\n", "a\nb\nc\n", "--- want\n+++ got\n@@ -1,4 +1,4 @@\n a\n-b\n+d\n c\n \n"}, + } + + for _, test := range tests { + if gotDiff := compare.Text(test.want, test.got); gotDiff != test.wantDiff { + t.Errorf("compare.Text(%q, %q) =\n%q, want\n%q", test.want, test.got, gotDiff, test.wantDiff) + } + } +} diff --git a/internal/lsp/tests/markdown_go118.go b/internal/lsp/tests/markdown_go118.go new file mode 100644 index 00000000000..37ad62d476a --- /dev/null +++ b/internal/lsp/tests/markdown_go118.go @@ -0,0 +1,63 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.19 +// +build !go1.19 + +package tests + +import ( + "regexp" + "strings" + "testing" + + "golang.org/x/tools/internal/lsp/tests/compare" +) + +// The markdown in the golden files matches the converter in comment.go, +// but for go1.19 and later the conversion is done using go/doc/comment. +// Compared to the newer version, the older version +// has extra escapes, and treats code blocks slightly differently. +func CheckSameMarkdown(t *testing.T, got, want string) { + t.Helper() + + got = normalizeMarkdown(got) + want = normalizeMarkdown(want) + + if diff := compare.Text(want, got); diff != "" { + t.Errorf("normalized markdown differs:\n%s", diff) + } +} + +// normalizeMarkdown normalizes whitespace and escaping of the input string, to +// eliminate differences between the Go 1.18 and Go 1.19 generated markdown for +// doc comments. Note that it does not normalize to either the 1.18 or 1.19 +// formatting: it simplifies both so that they may be compared. +// +// This function may need to be adjusted as we encounter more differences in +// the generated text. +func normalizeMarkdown(input string) string { + input = strings.TrimSpace(input) + + // For simplicity, eliminate blank lines. + input = regexp.MustCompile("\n+").ReplaceAllString(input, "\n") + + // Replace common escaped characters with their unescaped version. + // + // This list may not be exhaustive: it was just sufficient to make tests + // pass. + input = strings.NewReplacer( + `\\`, ``, + `\@`, `@`, + `\(`, `(`, + `\)`, `)`, + `\"`, `"`, + `\.`, `.`, + `\-`, `-`, + `\'`, `'`, + `\n\n\n`, `\n\n`, // Note that these are *escaped* newlines. + ).Replace(input) + + return input +} diff --git a/internal/lsp/tests/metadata_go119.go b/internal/lsp/tests/markdown_go119.go similarity index 78% rename from internal/lsp/tests/metadata_go119.go rename to internal/lsp/tests/markdown_go119.go index 462f130662f..51aea4ddc18 100644 --- a/internal/lsp/tests/metadata_go119.go +++ b/internal/lsp/tests/markdown_go119.go @@ -9,6 +9,8 @@ package tests import ( "testing" + + "golang.org/x/tools/internal/lsp/tests/compare" ) // The markdown in the golden files matches the converter in comment.go, @@ -17,7 +19,8 @@ import ( // has extra escapes, and treats code blocks slightly differently. func CheckSameMarkdown(t *testing.T, got, want string) { t.Helper() - if got != want { - t.Errorf("got %q, want %q", got, want) + + if diff := compare.Text(want, got); diff != "" { + t.Errorf("normalized markdown differs:\n%s", diff) } } diff --git a/internal/lsp/tests/metadata_go118.go b/internal/lsp/tests/metadata_go118.go deleted file mode 100644 index 3d9748b4e31..00000000000 --- a/internal/lsp/tests/metadata_go118.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.19 -// +build !go1.19 - -package tests - -import ( - "testing" -) - -// The markdown in the golden files matches the converter in comment.go, -// but for go1.19 and later the conversion is done using go/doc/comment. -// Compared to the newer version, the older version -// has extra escapes, and treats code blocks slightly differently. -func CheckSameMarkdown(t *testing.T, got, want string) { - t.Helper() - for _, dd := range markDiffs { - if got == dd.v18 && want == dd.v19 { - return - } - } - t.Errorf("got %q want %q", got, want) -} - -type markDiff struct { - v18, v19 string -} - -var markDiffs = []markDiff{{v19: "Package a is a package for testing go to definition.\n", v18: "Package a is a package for testing go to definition\\."}, - {v19: "```go\nconst X untyped int = 0\n```\n\n@mark(bX, \"X\"),godef(\"X\", bX)\n\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)", v18: "```go\nconst X untyped int = 0\n```\n\n\\@mark\\(bX, \\\"X\\\"\\),godef\\(\\\"X\\\", bX\\)\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)"}, - {v19: "```go\nconst dur time.Duration = 910350000000 // 15m10.35s\n```\n\ndur is a constant of type time.Duration.\n", v18: "```go\nconst dur time.Duration = 910350000000 // 15m10.35s\n```\n\ndur is a constant of type time\\.Duration\\."}, - {v19: "```go\nconst g untyped int = 1\n```\n\nWhen I hover on g, I should see this comment.\n", v18: "```go\nconst g untyped int = 1\n```\n\nWhen I hover on g, I should see this comment\\."}, - {v19: "```go\nconst h untyped int = 2\n```\n\nConstant block.\n", v18: "```go\nconst h untyped int = 2\n```\n\nConstant block\\."}, - {v19: "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)", v18: "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)"}, - {v19: "```go\nfield F1 string\n```\n\n@mark(S2F1, \"F1\")\n\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)", v18: "```go\nfield F1 string\n```\n\n\\@mark\\(S2F1, \\\"F1\\\"\\)\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)"}, - {v19: "```go\nfield F2 int\n```\n\n@mark(S2F2, \"F2\")\n\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)", v18: "```go\nfield F2 int\n```\n\n\\@mark\\(S2F2, \\\"F2\\\"\\)\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)"}, - {v19: "```go\nfield Field int\n```\n\n@mark(AField, \"Field\")\n\n\n[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#S.Field)", v18: "```go\nfield Field int\n```\n\n\\@mark\\(AField, \\\"Field\\\"\\)\n\n[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#S.Field)"}, - {v19: "```go\nfield Field2 int\n```\n\n@mark(AField2, \"Field2\")\n\n\n[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#R.Field2)", v18: "```go\nfield Field2 int\n```\n\n\\@mark\\(AField2, \\\"Field2\\\"\\)\n\n[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#R.Field2)"}, - {v19: "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)", v18: "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)"}, - {v19: "```go\nfield Q int\n```\n\n@mark(ValueQfield, \"Q\"),hoverdef(\"Q\", ValueQfield)\n\n\n[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/hover_generics#Value.Q)", v18: "```go\nfield Q int\n```\n\n\\@mark\\(ValueQfield, \\\"Q\\\"\\),hoverdef\\(\\\"Q\\\", ValueQfield\\)\n\n[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/hover_generics#Value.Q)"}, - {v19: "```go\nfield Q int\n```\n\n@mark(valueQfield, \"Q\"),hoverdef(\"Q\", valueQfield)\n", v18: "```go\nfield Q int\n```\n\n\\@mark\\(valueQfield, \\\"Q\\\"\\),hoverdef\\(\\\"Q\\\", valueQfield\\)"}, - {v19: "```go\nfield S2 S2\n```\n\n@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)", v18: "```go\nfield S2 S2\n```\n\n\\@godef\\(\\\"S2\\\", S2\\),mark\\(S1S2, \\\"S2\\\"\\)\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)"}, - {v19: "```go\nfield a int\n```\n\na field\n", v18: "```go\nfield a int\n```\n\na field"}, - {v19: "```go\nfield b struct{c int}\n```\n\nb nested struct\n", v18: "```go\nfield b struct{c int}\n```\n\nb nested struct"}, - {v19: "```go\nfield c int\n```\n\nc field of nested struct\n", v18: "```go\nfield c int\n```\n\nc field of nested struct"}, - {v19: "```go\nfield d int\n```\n\nd field\n", v18: "```go\nfield d int\n```\n\nd field"}, - {v19: "```go\nfield desc string\n```\n\ntest description\n", v18: "```go\nfield desc string\n```\n\ntest description"}, - {v19: "```go\nfield e struct{f int}\n```\n\ne nested struct\n", v18: "```go\nfield e struct{f int}\n```\n\ne nested struct"}, - {v19: "```go\nfield f int\n```\n\nf field of nested struct\n", v18: "```go\nfield f int\n```\n\nf field of nested struct"}, - {v19: "```go\nfield h int\n```\n\nh field\n", v18: "```go\nfield h int\n```\n\nh field"}, - {v19: "```go\nfield i struct{j int}\n```\n\ni nested struct\n", v18: "```go\nfield i struct{j int}\n```\n\ni nested struct"}, - {v19: "```go\nfield in map[string][]struct{key string; value interface{}}\n```\n\ntest input\n", v18: "```go\nfield in map[string][]struct{key string; value interface{}}\n```\n\ntest input"}, - {v19: "```go\nfield j int\n```\n\nj field of nested struct\n", v18: "```go\nfield j int\n```\n\nj field of nested struct"}, - {v19: "```go\nfield key string\n```\n\ntest key\n", v18: "```go\nfield key string\n```\n\ntest key"}, - {v19: "```go\nfield m map[string]float64\n```\n\nnested map\n", v18: "```go\nfield m map[string]float64\n```\n\nnested map"}, - {v19: "```go\nfield number int64\n```\n\nnested number\n", v18: "```go\nfield number int64\n```\n\nnested number"}, - {v19: "```go\nfield str string\n```\n\nnested string\n", v18: "```go\nfield str string\n```\n\nnested string"}, - {v19: "```go\nfield value int\n```\n\nexpected test value\n", v18: "```go\nfield value int\n```\n\nexpected test value"}, - {v19: "```go\nfield value interface{}\n```\n\ntest value\n", v18: "```go\nfield value interface{}\n```\n\ntest value"}, - {v19: "```go\nfield x []string\n```\n\nX key field\n", v18: "```go\nfield x []string\n```\n\nX key field"}, - {v19: "```go\nfield x int\n```\n\n@mark(PosX, \"x\"),mark(PosY, \"y\")\n", v18: "```go\nfield x int\n```\n\n\\@mark\\(PosX, \\\"x\\\"\\),mark\\(PosY, \\\"y\\\"\\)"}, - {v19: "```go\nfield x int\n```\n\nX coord\n", v18: "```go\nfield x int\n```\n\nX coord"}, - {v19: "```go\nfield x string\n```\n\nX value field\n", v18: "```go\nfield x string\n```\n\nX value field"}, - {v19: "```go\nfield y int\n```\n\nY coord\n", v18: "```go\nfield y int\n```\n\nY coord"}, - {v19: "```go\nfunc (*sync.Mutex).Lock()\n```\n\nLock locks m.\n\n\n[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock)", v18: "```go\nfunc (*sync.Mutex).Lock()\n```\n\nLock locks m\\.\n\n[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock)"}, - {v19: "```go\nfunc (*types.object).Name() string\n```\n\nName returns the object's (package-local, unqualified) name.\n\n\n[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name)", v18: "```go\nfunc (*types.object).Name() string\n```\n\nName returns the object\\'s \\(package\\-local, unqualified\\) name\\.\n\n[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name)"}, - {v19: "```go\nfunc (a.H).Goodbye()\n```\n\n@mark(AGoodbye, \"Goodbye\")\n\n\n[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#H.Goodbye)", v18: "```go\nfunc (a.H).Goodbye()\n```\n\n\\@mark\\(AGoodbye, \\\"Goodbye\\\"\\)\n\n[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#H.Goodbye)"}, - {v19: "```go\nfunc (a.I).B()\n```\n\n@mark(AB, \"B\")\n\n\n[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#I.B)", v18: "```go\nfunc (a.I).B()\n```\n\n\\@mark\\(AB, \\\"B\\\"\\)\n\n[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#I.B)"}, - {v19: "```go\nfunc (a.J).Hello()\n```\n\n@mark(AHello, \"Hello\")\n\n\n[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#J.Hello)", v18: "```go\nfunc (a.J).Hello()\n```\n\n\\@mark\\(AHello, \\\"Hello\\\"\\)\n\n[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#J.Hello)"}, - {v19: "```go\nfunc (interface).open() error\n```\n\nopen method comment\n", v18: "```go\nfunc (interface).open() error\n```\n\nopen method comment"}, - {v19: "```go\nfunc make(t Type, size ...int) Type\n```\n\nThe make built-in function allocates and initializes an object of type slice, map, or chan (only).\n\n\n[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make)", v18: "```go\nfunc make(t Type, size ...int) Type\n```\n\nThe make built\\-in function allocates and initializes an object of type slice, map, or chan \\(only\\)\\.\n\n[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make)"}, - {v19: "```go\ntype A string\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)", v18: "```go\ntype A string\n```\n\n\\@mark\\(AString, \\\"A\\\"\\)\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)"}, - {v19: "```go\ntype a struct {\n\tx string\n}\n```\n\n1st type declaration block\n", v18: "```go\ntype a struct {\n\tx string\n}\n```\n\n1st type declaration block"}, - {v19: "```go\ntype aAlias = a.A\n```\n\n@mark(aAlias, \"aAlias\")\n", v18: "```go\ntype aAlias = a.A\n```\n\n\\@mark\\(aAlias, \\\"aAlias\\\"\\)"}, - {v19: "```go\ntype b struct{}\n```\n\nb has a comment\n", v18: "```go\ntype b struct{}\n```\n\nb has a comment"}, - {v19: "```go\ntype c struct {\n\tf string\n}\n```\n\nc is a struct\n", v18: "```go\ntype c struct {\n\tf string\n}\n```\n\nc is a struct"}, - {v19: "```go\ntype d string\n```\n\n3rd type declaration block\n", v18: "```go\ntype d string\n```\n\n3rd type declaration block"}, - {v19: "```go\ntype e struct {\n\tf float64\n}\n```\n\ne has a comment\n", v18: "```go\ntype e struct {\n\tf float64\n}\n```\n\ne has a comment"}, - {v19: "```go\ntype string string\n```\n\nstring is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text.\n\n\n[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string)", v18: "```go\ntype string string\n```\n\nstring is the set of all strings of 8\\-bit bytes, conventionally but not necessarily representing UTF\\-8\\-encoded text\\.\n\n[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string)"}, - {v19: "```go\nvar Other Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)", v18: "```go\nvar Other Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"}, - {v19: "```go\nvar _ A\n```\n\nvariable of type a.A\n", v18: "```go\nvar _ A\n```\n\nvariable of type a\\.A"}, - {v19: "```go\nvar a.Other a.Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)", v18: "```go\nvar a.Other a.Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"}, - {v19: "```go\nvar err error\n```\n\n@err\n", v18: "```go\nvar err error\n```\n\n\\@err"}, - {v19: "```go\nvar myUnclosedIf string\n```\n\n@myUnclosedIf\n", v18: "```go\nvar myUnclosedIf string\n```\n\n\\@myUnclosedIf"}, - {v19: "```go\nvar x string\n```\n\nx is a variable.\n", v18: "```go\nvar x string\n```\n\nx is a variable\\."}, - {v19: "```go\nvar z string\n```\n\nz is a variable too.\n", v18: "```go\nvar z string\n```\n\nz is a variable too\\."}, - {v19: "godef/a/a.go:26:6-7: defined here as ```go\ntype A string\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)", v18: "godef/a/a.go:26:6-7: defined here as ```go\ntype A string\n```\n\n\\@mark\\(AString, \\\"A\\\"\\)\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)"}, - {v19: "godef/a/a.go:33:6-9: defined here as ```go\nvar err error\n```\n\n@err", v18: "godef/a/a.go:33:6-9: defined here as ```go\nvar err error\n```\n\n\\@err"}, - {v19: "godef/a/d.go:6:2-8: defined here as ```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)", v18: "godef/a/d.go:6:2-8: defined here as ```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)"}, - {v19: "godef/a/d.go:9:5-10: defined here as ```go\nvar Other Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)", v18: "godef/a/d.go:9:5-10: defined here as ```go\nvar Other Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"}, - {v19: "godef/a/d.go:9:5-10: defined here as ```go\nvar a.Other a.Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)", v18: "godef/a/d.go:9:5-10: defined here as ```go\nvar a.Other a.Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)"}, - {v19: "godef/a/random.go:13:2-3: defined here as ```go\nfield x int\n```\n\n@mark(PosX, \"x\"),mark(PosY, \"y\")", v18: "godef/a/random.go:13:2-3: defined here as ```go\nfield x int\n```\n\n\\@mark\\(PosX, \\\"x\\\"\\),mark\\(PosY, \\\"y\\\"\\)"}, - {v19: "godef/b/b.go:25:6-12: defined here as ```go\ntype aAlias = a.A\n```\n\n@mark(aAlias, \"aAlias\")", v18: "godef/b/b.go:25:6-12: defined here as ```go\ntype aAlias = a.A\n```\n\n\\@mark\\(aAlias, \\\"aAlias\\\"\\)"}, - {v19: "godef/b/b.go:28:2-4: defined here as ```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)", v18: "godef/b/b.go:28:2-4: defined here as ```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)"}, - {v19: "godef/b/b.go:29:2-4: defined here as ```go\nfield S2 S2\n```\n\n@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)", v18: "godef/b/b.go:29:2-4: defined here as ```go\nfield S2 S2\n```\n\n\\@godef\\(\\\"S2\\\", S2\\),mark\\(S1S2, \\\"S2\\\"\\)\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)"}, - {v19: "godef/b/b.go:35:2-4: defined here as ```go\nfield F1 string\n```\n\n@mark(S2F1, \"F1\")\n\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)", v18: "godef/b/b.go:35:2-4: defined here as ```go\nfield F1 string\n```\n\n\\@mark\\(S2F1, \\\"F1\\\"\\)\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)"}, - {v19: "godef/b/b.go:36:2-4: defined here as ```go\nfield F2 int\n```\n\n@mark(S2F2, \"F2\")\n\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)", v18: "godef/b/b.go:36:2-4: defined here as ```go\nfield F2 int\n```\n\n\\@mark\\(S2F2, \\\"F2\\\"\\)\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)"}, - {v19: "godef/b/b.go:57:7-8: defined here as ```go\nconst X untyped int = 0\n```\n\n@mark(bX, \"X\"),godef(\"X\", bX)\n\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)", v18: "godef/b/b.go:57:7-8: defined here as ```go\nconst X untyped int = 0\n```\n\n\\@mark\\(bX, \\\"X\\\"\\),godef\\(\\\"X\\\", bX\\)\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)"}, - {v19: "godef/broken/unclosedIf.go:7:7-19: defined here as ```go\nvar myUnclosedIf string\n```\n\n@myUnclosedIf", v18: "godef/broken/unclosedIf.go:7:7-19: defined here as ```go\nvar myUnclosedIf string\n```\n\n\\@myUnclosedIf"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/a.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 26,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 467\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 26,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 468\n\t\t}\n\t},\n\t\"description\": \"```go\\ntype A string\\n```\\n\\n@mark(AString, \\\"A\\\")\\n\\n\\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/a.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 26,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 467\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 26,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 468\n\t\t}\n\t},\n\t\"description\": \"```go\\ntype A string\\n```\\n\\n\\\\@mark\\\\(AString, \\\\\\\"A\\\\\\\"\\\\)\\n\\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/a.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 33,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 612\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 33,\n\t\t\t\"column\": 9,\n\t\t\t\"offset\": 615\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar err error\\n```\\n\\n@err\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/a.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 33,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 612\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 33,\n\t\t\t\"column\": 9,\n\t\t\t\"offset\": 615\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar err error\\n```\\n\\n\\\\@err\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 6,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 90\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 6,\n\t\t\t\"column\": 8,\n\t\t\t\"offset\": 96\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield Member string\\n```\\n\\n@Member\\n\\n\\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 6,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 90\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 6,\n\t\t\t\"column\": 8,\n\t\t\t\"offset\": 96\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield Member string\\n```\\n\\n\\\\@Member\\n\\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 5,\n\t\t\t\"offset\": 121\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 10,\n\t\t\t\"offset\": 126\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar Other Thing\\n```\\n\\n@Other\\n\\n\\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 5,\n\t\t\t\"offset\": 121\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 10,\n\t\t\t\"offset\": 126\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar Other Thing\\n```\\n\\n\\\\@Other\\n\\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 5,\n\t\t\t\"offset\": 121\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 10,\n\t\t\t\"offset\": 126\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar a.Other a.Thing\\n```\\n\\n@Other\\n\\n\\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/d.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 5,\n\t\t\t\"offset\": 121\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 9,\n\t\t\t\"column\": 10,\n\t\t\t\"offset\": 126\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar a.Other a.Thing\\n```\\n\\n\\\\@Other\\n\\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/random.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 13,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 187\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 13,\n\t\t\t\"column\": 3,\n\t\t\t\"offset\": 188\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield x int\\n```\\n\\n@mark(PosX, \\\"x\\\"),mark(PosY, \\\"y\\\")\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/a/random.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 13,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 187\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 13,\n\t\t\t\"column\": 3,\n\t\t\t\"offset\": 188\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield x int\\n```\\n\\n\\\\@mark\\\\(PosX, \\\\\\\"x\\\\\\\"\\\\),mark\\\\(PosY, \\\\\\\"y\\\\\\\"\\\\)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 25,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 542\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 25,\n\t\t\t\"column\": 12,\n\t\t\t\"offset\": 548\n\t\t}\n\t},\n\t\"description\": \"```go\\ntype aAlias = a.A\\n```\\n\\n@mark(aAlias, \\\"aAlias\\\")\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 25,\n\t\t\t\"column\": 6,\n\t\t\t\"offset\": 542\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 25,\n\t\t\t\"column\": 12,\n\t\t\t\"offset\": 548\n\t\t}\n\t},\n\t\"description\": \"```go\\ntype aAlias = a.A\\n```\\n\\n\\\\@mark\\\\(aAlias, \\\\\\\"aAlias\\\\\\\"\\\\)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 28,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 606\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 28,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 608\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F1 int\\n```\\n\\n@mark(S1F1, \\\"F1\\\")\\n\\n\\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 28,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 606\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 28,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 608\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F1 int\\n```\\n\\n\\\\@mark\\\\(S1F1, \\\\\\\"F1\\\\\\\"\\\\)\\n\\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 29,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 638\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 29,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 640\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield S2 S2\\n```\\n\\n@godef(\\\"S2\\\", S2),mark(S1S2, \\\"S2\\\")\\n\\n\\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 29,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 638\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 29,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 640\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield S2 S2\\n```\\n\\n\\\\@godef\\\\(\\\\\\\"S2\\\\\\\", S2\\\\),mark\\\\(S1S2, \\\\\\\"S2\\\\\\\"\\\\)\\n\\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 35,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 781\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 35,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 783\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F1 string\\n```\\n\\n@mark(S2F1, \\\"F1\\\")\\n\\n\\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 35,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 781\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 35,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 783\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F1 string\\n```\\n\\n\\\\@mark\\\\(S2F1, \\\\\\\"F1\\\\\\\"\\\\)\\n\\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 36,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 814\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 36,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 816\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F2 int\\n```\\n\\n@mark(S2F2, \\\"F2\\\")\\n\\n\\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 36,\n\t\t\t\"column\": 2,\n\t\t\t\"offset\": 814\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 36,\n\t\t\t\"column\": 4,\n\t\t\t\"offset\": 816\n\t\t}\n\t},\n\t\"description\": \"```go\\nfield F2 int\\n```\\n\\n\\\\@mark\\\\(S2F2, \\\\\\\"F2\\\\\\\"\\\\)\\n\\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 57,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 1249\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 57,\n\t\t\t\"column\": 8,\n\t\t\t\"offset\": 1250\n\t\t}\n\t},\n\t\"description\": \"```go\\nconst X untyped int = 0\\n```\\n\\n@mark(bX, \\\"X\\\"),godef(\\\"X\\\", bX)\\n\\n\\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/b/b.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 57,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 1249\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 57,\n\t\t\t\"column\": 8,\n\t\t\t\"offset\": 1250\n\t\t}\n\t},\n\t\"description\": \"```go\\nconst X untyped int = 0\\n```\\n\\n\\\\@mark\\\\(bX, \\\\\\\"X\\\\\\\"\\\\),godef\\\\(\\\\\\\"X\\\\\\\", bX\\\\)\\n\\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)\"\n}\n"}, - {v19: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/broken/unclosedIf.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 7,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 68\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 7,\n\t\t\t\"column\": 19,\n\t\t\t\"offset\": 80\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar myUnclosedIf string\\n```\\n\\n@myUnclosedIf\"\n}", v18: "{\n\t\"span\": {\n\t\t\"uri\": \"file://godef/broken/unclosedIf.go\",\n\t\t\"start\": {\n\t\t\t\"line\": 7,\n\t\t\t\"column\": 7,\n\t\t\t\"offset\": 68\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": 7,\n\t\t\t\"column\": 19,\n\t\t\t\"offset\": 80\n\t\t}\n\t},\n\t\"description\": \"```go\\nvar myUnclosedIf string\\n```\\n\\n\\\\@myUnclosedIf\"\n}\n"}, -} diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index 1b02c785976..eb7c26e6ba8 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -31,6 +31,7 @@ import ( "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source/completion" + "golang.org/x/tools/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" @@ -1015,7 +1016,7 @@ func checkData(t *testing.T, data *Data) { // These counters change when assertions are added or removed. // They act as an independent safety net to ensure that the // tests didn't spuriously pass because they did no work. - t.Errorf("test summary does not match:\n%s\n(Run with -golden to update golden file; also, there may be one per Go version.)", Diff(t, want, got)) + t.Errorf("test summary does not match:\n%s\n(Run with -golden to update golden file; also, there may be one per Go version.)", compare.Text(want, got)) } } diff --git a/internal/lsp/tests/util.go b/internal/lsp/tests/util.go index 5a049134ae1..66ff217c78b 100644 --- a/internal/lsp/tests/util.go +++ b/internal/lsp/tests/util.go @@ -569,20 +569,6 @@ func WorkspaceSymbolsTestTypeToMatcher(typ WorkspaceSymbolsTestType) source.Symb } } -func Diff(t *testing.T, want, got string) string { - if want == got { - return "" - } - // Add newlines to avoid newline messages in diff. - want += "\n" - got += "\n" - d, err := myers.ComputeEdits("", want, got) - if err != nil { - t.Fatal(err) - } - return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d)) -} - // StripSubscripts removes type parameter id subscripts. // // TODO(rfindley): remove this function once subscripts are removed from the From 550e1f5a555b8f395b0f6059b3e70a3ef4bd17ef Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 31 Aug 2022 14:01:44 -0400 Subject: [PATCH 212/723] internal/lsp/tests: use a shorter module path for test data While moving internal/lsp to gopls/internal/lsp, we discovered that we're bumping up against a command line length limit on windows. Use an arbitrary shorter module path to avoid this, for now. Updates golang/go#54800 Updates golang/go#54509 Change-Id: I7be07da29a769c1ce7c31cbbd374ca47b0944132 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426801 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- internal/lsp/testdata/analyzer/bad_test.go | 2 +- .../lsp/testdata/arraytype/array_type.go.in | 2 +- internal/lsp/testdata/assign/assign.go.in | 2 +- internal/lsp/testdata/bad/bad0.go | 2 +- internal/lsp/testdata/badstmt/badstmt.go.in | 2 +- internal/lsp/testdata/badstmt/badstmt_2.go.in | 2 +- internal/lsp/testdata/badstmt/badstmt_3.go.in | 2 +- internal/lsp/testdata/badstmt/badstmt_4.go.in | 2 +- internal/lsp/testdata/bar/bar.go.in | 2 +- internal/lsp/testdata/baz/baz.go.in | 4 +- .../testdata/callhierarchy/callhierarchy.go | 2 +- .../callhierarchy/incoming/incoming.go | 2 +- .../lsp/testdata/cgo/declarecgo.go.golden | 6 +- .../lsp/testdata/cgoimport/usecgo.go.golden | 6 +- internal/lsp/testdata/cgoimport/usecgo.go.in | 2 +- .../danglingstmt/dangling_selector_2.go | 2 +- internal/lsp/testdata/errors/errors.go | 2 +- internal/lsp/testdata/fillstruct/a.go | 2 +- internal/lsp/testdata/fillstruct/a.go.golden | 8 +- .../fillstruct/fill_struct_package.go | 2 +- .../fillstruct/fill_struct_package.go.golden | 4 +- internal/lsp/testdata/foo/foo.go | 2 +- internal/lsp/testdata/godef/a/a.go.golden | 12 +- internal/lsp/testdata/godef/a/d.go.golden | 30 ++-- .../lsp/testdata/godef/a/random.go.golden | 6 +- internal/lsp/testdata/godef/b/b.go | 4 +- internal/lsp/testdata/godef/b/b.go.golden | 128 +++++++++--------- internal/lsp/testdata/godef/b/c.go.golden | 20 +-- internal/lsp/testdata/godef/b/e.go | 2 +- internal/lsp/testdata/godef/b/e.go.golden | 24 ++-- internal/lsp/testdata/godef/b/h.go | 2 +- internal/lsp/testdata/godef/b/h.go.golden | 2 +- .../godef/hover_generics/hover.go.golden | 2 +- internal/lsp/testdata/good/good1.go | 2 +- .../testdata/implementation/implementation.go | 2 +- .../importedcomplit/imported_complit.go.in | 14 +- internal/lsp/testdata/links/links.go | 2 +- .../lsp/testdata/nodisk/nodisk.overlay.go | 2 +- .../testdata/references/another/another.go | 2 +- .../lsp/testdata/references/other/other.go | 2 +- internal/lsp/testdata/rename/b/b.go.golden | 2 +- .../lsp/testdata/rename/bad/bad.go.golden | 2 +- internal/lsp/testdata/rename/c/c.go | 2 +- internal/lsp/testdata/rename/c/c.go.golden | 2 +- .../rename/crosspkg/crosspkg.go.golden | 4 +- .../testdata/rename/crosspkg/other/other.go | 2 +- .../rename/crosspkg/other/other.go.golden | 2 +- internal/lsp/testdata/selector/selector.go.in | 2 +- .../lsp/testdata/signature/signature_test.go | 2 +- internal/lsp/testdata/snippets/literal.go | 4 +- .../testdata/snippets/literal_snippets.go.in | 2 +- .../stub/stub_renamed_import_iface.go | 2 +- .../stub/stub_renamed_import_iface.go.golden | 2 +- internal/lsp/testdata/testy/testy_test.go | 4 +- .../lsp/testdata/unimported/export_test.go | 2 +- .../lsp/testdata/unimported/unimported.go.in | 2 +- .../unimported/unimported_cand_type.go | 4 +- internal/lsp/tests/tests.go | 7 +- 58 files changed, 187 insertions(+), 182 deletions(-) diff --git a/internal/lsp/testdata/analyzer/bad_test.go b/internal/lsp/testdata/analyzer/bad_test.go index 3d89101fc02..b1724c66693 100644 --- a/internal/lsp/testdata/analyzer/bad_test.go +++ b/internal/lsp/testdata/analyzer/bad_test.go @@ -11,7 +11,7 @@ func Testbad(t *testing.T) { //@diag("", "tests", "Testbad has malformed name: f var x sync.Mutex _ = x //@diag("x", "copylocks", "assignment copies lock value to _: sync.Mutex", "warning") - printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, "printf", "golang.org/x/tools/internal/lsp/analyzer.printfWrapper format %s reads arg #1, but call has 0 args", "warning") + printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, "printf", "golang.org/lsptests/analyzer.printfWrapper format %s reads arg #1, but call has 0 args", "warning") } func printfWrapper(format string, args ...interface{}) { diff --git a/internal/lsp/testdata/arraytype/array_type.go.in b/internal/lsp/testdata/arraytype/array_type.go.in index 7e9a96f7b0d..84264699ccd 100644 --- a/internal/lsp/testdata/arraytype/array_type.go.in +++ b/internal/lsp/testdata/arraytype/array_type.go.in @@ -1,7 +1,7 @@ package arraytype import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/assign/assign.go.in b/internal/lsp/testdata/assign/assign.go.in index 8c00ae9e0e5..93a622c8326 100644 --- a/internal/lsp/testdata/assign/assign.go.in +++ b/internal/lsp/testdata/assign/assign.go.in @@ -1,6 +1,6 @@ package assign -import "golang.org/x/tools/internal/lsp/assign/internal/secret" +import "golang.org/lsptests/assign/internal/secret" func _() { secret.Hello() diff --git a/internal/lsp/testdata/bad/bad0.go b/internal/lsp/testdata/bad/bad0.go index 4120cf3a0d7..e8f3c2f2978 100644 --- a/internal/lsp/testdata/bad/bad0.go +++ b/internal/lsp/testdata/bad/bad0.go @@ -3,7 +3,7 @@ package bad -import _ "golang.org/x/tools/internal/lsp/assign/internal/secret" //@diag("\"golang.org/x/tools/internal/lsp/assign/internal/secret\"", "compiler", "could not import golang.org/x/tools/internal/lsp/assign/internal/secret \\(invalid use of internal package golang.org/x/tools/internal/lsp/assign/internal/secret\\)", "error") +import _ "golang.org/lsptests/assign/internal/secret" //@diag("\"golang.org/lsptests/assign/internal/secret\"", "compiler", "could not import golang.org/lsptests/assign/internal/secret \\(invalid use of internal package golang.org/lsptests/assign/internal/secret\\)", "error") func stuff() { //@item(stuff, "stuff", "func()", "func") x := "heeeeyyyy" diff --git a/internal/lsp/testdata/badstmt/badstmt.go.in b/internal/lsp/testdata/badstmt/badstmt.go.in index b6cb6140696..1ac9f0a8078 100644 --- a/internal/lsp/testdata/badstmt/badstmt.go.in +++ b/internal/lsp/testdata/badstmt/badstmt.go.in @@ -1,7 +1,7 @@ package badstmt import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/badstmt/badstmt_2.go.in b/internal/lsp/testdata/badstmt/badstmt_2.go.in index f754b46aaac..6af9c35e3cf 100644 --- a/internal/lsp/testdata/badstmt/badstmt_2.go.in +++ b/internal/lsp/testdata/badstmt/badstmt_2.go.in @@ -1,7 +1,7 @@ package badstmt import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/badstmt/badstmt_3.go.in b/internal/lsp/testdata/badstmt/badstmt_3.go.in index be774e84b05..d135e201505 100644 --- a/internal/lsp/testdata/badstmt/badstmt_3.go.in +++ b/internal/lsp/testdata/badstmt/badstmt_3.go.in @@ -1,7 +1,7 @@ package badstmt import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/badstmt/badstmt_4.go.in b/internal/lsp/testdata/badstmt/badstmt_4.go.in index a9b46fb021b..6afd635ec2d 100644 --- a/internal/lsp/testdata/badstmt/badstmt_4.go.in +++ b/internal/lsp/testdata/badstmt/badstmt_4.go.in @@ -1,7 +1,7 @@ package badstmt import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/bar/bar.go.in b/internal/lsp/testdata/bar/bar.go.in index c0f4b4c45c2..502bdf74060 100644 --- a/internal/lsp/testdata/bar/bar.go.in +++ b/internal/lsp/testdata/bar/bar.go.in @@ -3,7 +3,7 @@ package bar import ( - "golang.org/x/tools/internal/lsp/foo" //@item(foo, "foo", "\"golang.org/x/tools/internal/lsp/foo\"", "package") + "golang.org/lsptests/foo" //@item(foo, "foo", "\"golang.org/lsptests/foo\"", "package") ) func helper(i foo.IntFoo) {} //@item(helper, "helper", "func(i foo.IntFoo)", "func") diff --git a/internal/lsp/testdata/baz/baz.go.in b/internal/lsp/testdata/baz/baz.go.in index 3b74ee580c3..94952e1267b 100644 --- a/internal/lsp/testdata/baz/baz.go.in +++ b/internal/lsp/testdata/baz/baz.go.in @@ -3,9 +3,9 @@ package baz import ( - "golang.org/x/tools/internal/lsp/bar" + "golang.org/lsptests/bar" - f "golang.org/x/tools/internal/lsp/foo" + f "golang.org/lsptests/foo" ) var FooStruct f.StructFoo diff --git a/internal/lsp/testdata/callhierarchy/callhierarchy.go b/internal/lsp/testdata/callhierarchy/callhierarchy.go index 58c23bdd634..252e8054f40 100644 --- a/internal/lsp/testdata/callhierarchy/callhierarchy.go +++ b/internal/lsp/testdata/callhierarchy/callhierarchy.go @@ -4,7 +4,7 @@ package callhierarchy -import "golang.org/x/tools/internal/lsp/callhierarchy/outgoing" +import "golang.org/lsptests/callhierarchy/outgoing" func a() { //@mark(hierarchyA, "a") D() diff --git a/internal/lsp/testdata/callhierarchy/incoming/incoming.go b/internal/lsp/testdata/callhierarchy/incoming/incoming.go index 3bfb4ad998d..c629aa87929 100644 --- a/internal/lsp/testdata/callhierarchy/incoming/incoming.go +++ b/internal/lsp/testdata/callhierarchy/incoming/incoming.go @@ -4,7 +4,7 @@ package incoming -import "golang.org/x/tools/internal/lsp/callhierarchy" +import "golang.org/lsptests/callhierarchy" // A is exported to test incoming calls across packages func A() { //@mark(incomingA, "A") diff --git a/internal/lsp/testdata/cgo/declarecgo.go.golden b/internal/lsp/testdata/cgo/declarecgo.go.golden index a032761ffd0..0d6fbb0fff6 100644 --- a/internal/lsp/testdata/cgo/declarecgo.go.golden +++ b/internal/lsp/testdata/cgo/declarecgo.go.golden @@ -3,7 +3,7 @@ cgo/declarecgo.go:18:6-13: defined here as ```go func Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example) -- funccgoexample-definition-json -- { "span": { @@ -19,7 +19,7 @@ func Example() "offset": 158 } }, - "description": "```go\nfunc Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)" + "description": "```go\nfunc Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)" } -- funccgoexample-hoverdef -- @@ -27,4 +27,4 @@ func Example() func Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example) diff --git a/internal/lsp/testdata/cgoimport/usecgo.go.golden b/internal/lsp/testdata/cgoimport/usecgo.go.golden index 6f638360a92..03fc22468ca 100644 --- a/internal/lsp/testdata/cgoimport/usecgo.go.golden +++ b/internal/lsp/testdata/cgoimport/usecgo.go.golden @@ -3,7 +3,7 @@ cgo/declarecgo.go:18:6-13: defined here as ```go func cgo.Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example) -- funccgoexample-definition-json -- { "span": { @@ -19,7 +19,7 @@ func cgo.Example() "offset": 158 } }, - "description": "```go\nfunc cgo.Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example)" + "description": "```go\nfunc cgo.Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)" } -- funccgoexample-hoverdef -- @@ -27,4 +27,4 @@ func cgo.Example() func cgo.Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example) diff --git a/internal/lsp/testdata/cgoimport/usecgo.go.in b/internal/lsp/testdata/cgoimport/usecgo.go.in index f258682ea13..414a739da99 100644 --- a/internal/lsp/testdata/cgoimport/usecgo.go.in +++ b/internal/lsp/testdata/cgoimport/usecgo.go.in @@ -1,7 +1,7 @@ package cgoimport import ( - "golang.org/x/tools/internal/lsp/cgo" + "golang.org/lsptests/cgo" ) func _() { diff --git a/internal/lsp/testdata/danglingstmt/dangling_selector_2.go b/internal/lsp/testdata/danglingstmt/dangling_selector_2.go index a9e75e82a57..8d4b15bff6a 100644 --- a/internal/lsp/testdata/danglingstmt/dangling_selector_2.go +++ b/internal/lsp/testdata/danglingstmt/dangling_selector_2.go @@ -1,6 +1,6 @@ package danglingstmt -import "golang.org/x/tools/internal/lsp/foo" +import "golang.org/lsptests/foo" func _() { foo. //@rank(" //", Foo) diff --git a/internal/lsp/testdata/errors/errors.go b/internal/lsp/testdata/errors/errors.go index 42105629eaa..e14cde69e9e 100644 --- a/internal/lsp/testdata/errors/errors.go +++ b/internal/lsp/testdata/errors/errors.go @@ -1,7 +1,7 @@ package errors import ( - "golang.org/x/tools/internal/lsp/types" + "golang.org/lsptests/types" ) func _() { diff --git a/internal/lsp/testdata/fillstruct/a.go b/internal/lsp/testdata/fillstruct/a.go index 4fb855d06b5..e1add2d4713 100644 --- a/internal/lsp/testdata/fillstruct/a.go +++ b/internal/lsp/testdata/fillstruct/a.go @@ -1,7 +1,7 @@ package fillstruct import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) type basicStruct struct { diff --git a/internal/lsp/testdata/fillstruct/a.go.golden b/internal/lsp/testdata/fillstruct/a.go.golden index 76789f0fc26..ca1db04ead8 100644 --- a/internal/lsp/testdata/fillstruct/a.go.golden +++ b/internal/lsp/testdata/fillstruct/a.go.golden @@ -2,7 +2,7 @@ package fillstruct import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) type basicStruct struct { @@ -33,7 +33,7 @@ var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") package fillstruct import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) type basicStruct struct { @@ -65,7 +65,7 @@ var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") package fillstruct import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) type basicStruct struct { @@ -97,7 +97,7 @@ var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") package fillstruct import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) type basicStruct struct { diff --git a/internal/lsp/testdata/fillstruct/fill_struct_package.go b/internal/lsp/testdata/fillstruct/fill_struct_package.go index edb88c48675..ef35627c8ea 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_package.go +++ b/internal/lsp/testdata/fillstruct/fill_struct_package.go @@ -3,7 +3,7 @@ package fillstruct import ( h2 "net/http" - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) func unexported() { diff --git a/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden b/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden index 57b261329dc..0cdbfc820ba 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden +++ b/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden @@ -4,7 +4,7 @@ package fillstruct import ( h2 "net/http" - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) func unexported() { @@ -20,7 +20,7 @@ package fillstruct import ( h2 "net/http" - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) func unexported() { diff --git a/internal/lsp/testdata/foo/foo.go b/internal/lsp/testdata/foo/foo.go index 20ea183e5d9..66631c58ca9 100644 --- a/internal/lsp/testdata/foo/foo.go +++ b/internal/lsp/testdata/foo/foo.go @@ -1,4 +1,4 @@ -package foo //@mark(PackageFoo, "foo"),item(PackageFoo, "foo", "\"golang.org/x/tools/internal/lsp/foo\"", "package") +package foo //@mark(PackageFoo, "foo"),item(PackageFoo, "foo", "\"golang.org/lsptests/foo\"", "package") type StructFoo struct { //@item(StructFoo, "StructFoo", "struct{...}", "struct") Value int //@item(Value, "Value", "int", "field") diff --git a/internal/lsp/testdata/godef/a/a.go.golden b/internal/lsp/testdata/godef/a/a.go.golden index 7f114e54b06..0a037a9c6a5 100644 --- a/internal/lsp/testdata/godef/a/a.go.golden +++ b/internal/lsp/testdata/godef/a/a.go.golden @@ -21,7 +21,7 @@ godef/a/random.go:3:6-12: defined here as ```go func Random() int ``` -[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random) +[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random) -- Random-definition-json -- { "span": { @@ -37,7 +37,7 @@ func Random() int "offset": 22 } }, - "description": "```go\nfunc Random() int\n```\n\n[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random)" + "description": "```go\nfunc Random() int\n```\n\n[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random)" } -- Random-hoverdef -- @@ -45,13 +45,13 @@ func Random() int func Random() int ``` -[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random) +[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random) -- Random2-definition -- godef/a/random.go:8:6-13: defined here as ```go func Random2(y int) int ``` -[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2) +[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random2) -- Random2-definition-json -- { "span": { @@ -67,7 +67,7 @@ func Random2(y int) int "offset": 78 } }, - "description": "```go\nfunc Random2(y int) int\n```\n\n[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2)" + "description": "```go\nfunc Random2(y int) int\n```\n\n[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random2)" } -- Random2-hoverdef -- @@ -75,7 +75,7 @@ func Random2(y int) int func Random2(y int) int ``` -[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Random2) +[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random2) -- aPackage-hoverdef -- Package a is a package for testing go to definition. diff --git a/internal/lsp/testdata/godef/a/d.go.golden b/internal/lsp/testdata/godef/a/d.go.golden index 9f04317c00c..54908ce5281 100644 --- a/internal/lsp/testdata/godef/a/d.go.golden +++ b/internal/lsp/testdata/godef/a/d.go.golden @@ -6,7 +6,7 @@ field Member string @Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member) -- Member-definition-json -- { "span": { @@ -22,7 +22,7 @@ field Member string "offset": 96 } }, - "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)" + "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)" } -- Member-hoverdef -- @@ -33,13 +33,13 @@ field Member string @Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member) -- Method-definition -- godef/a/d.go:15:16-22: defined here as ```go func (Thing).Method(i int) string ``` -[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method) +[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method) -- Method-definition-json -- { "span": { @@ -55,7 +55,7 @@ func (Thing).Method(i int) string "offset": 225 } }, - "description": "```go\nfunc (Thing).Method(i int) string\n```\n\n[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method)" + "description": "```go\nfunc (Thing).Method(i int) string\n```\n\n[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method)" } -- Method-hoverdef -- @@ -63,7 +63,7 @@ func (Thing).Method(i int) string func (Thing).Method(i int) string ``` -[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Method) +[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method) -- Other-definition -- godef/a/d.go:9:5-10: defined here as ```go var Other Thing @@ -72,7 +72,7 @@ var Other Thing @Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other) -- Other-definition-json -- { "span": { @@ -88,7 +88,7 @@ var Other Thing "offset": 126 } }, - "description": "```go\nvar Other Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)" + "description": "```go\nvar Other Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)" } -- Other-hoverdef -- @@ -99,7 +99,7 @@ var Other Thing @Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other) -- Thing-definition -- godef/a/d.go:5:6-11: defined here as ```go type Thing struct { @@ -107,7 +107,7 @@ type Thing struct { } ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) -- Thing-definition-json -- { "span": { @@ -123,7 +123,7 @@ type Thing struct { "offset": 70 } }, - "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)" + "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)" } -- Thing-hoverdef -- @@ -133,13 +133,13 @@ type Thing struct { } ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) -- Things-definition -- godef/a/d.go:11:6-12: defined here as ```go func Things(val []string) []Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things) -- Things-definition-json -- { "span": { @@ -155,7 +155,7 @@ func Things(val []string) []Thing "offset": 154 } }, - "description": "```go\nfunc Things(val []string) []Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)" + "description": "```go\nfunc Things(val []string) []Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)" } -- Things-hoverdef -- @@ -163,7 +163,7 @@ func Things(val []string) []Thing func Things(val []string) []Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things) -- a-hoverdef -- Package a is a package for testing go to definition. diff --git a/internal/lsp/testdata/godef/a/random.go.golden b/internal/lsp/testdata/godef/a/random.go.golden index eb70a515f11..d7ba51d1e82 100644 --- a/internal/lsp/testdata/godef/a/random.go.golden +++ b/internal/lsp/testdata/godef/a/random.go.golden @@ -3,7 +3,7 @@ godef/a/random.go:24:15-18: defined here as ```go func (*Pos).Sum() int ``` -[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum) +[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Pos.Sum) -- PosSum-definition-json -- { "span": { @@ -19,7 +19,7 @@ func (*Pos).Sum() int "offset": 416 } }, - "description": "```go\nfunc (*Pos).Sum() int\n```\n\n[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum)" + "description": "```go\nfunc (*Pos).Sum() int\n```\n\n[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Pos.Sum)" } -- PosSum-hoverdef -- @@ -27,7 +27,7 @@ func (*Pos).Sum() int func (*Pos).Sum() int ``` -[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Pos.Sum) +[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Pos.Sum) -- PosX-definition -- godef/a/random.go:13:2-3: defined here as ```go field x int diff --git a/internal/lsp/testdata/godef/b/b.go b/internal/lsp/testdata/godef/b/b.go index f9c1d64024b..ee536ecfdc3 100644 --- a/internal/lsp/testdata/godef/b/b.go +++ b/internal/lsp/testdata/godef/b/b.go @@ -1,8 +1,8 @@ package b import ( - myFoo "golang.org/x/tools/internal/lsp/foo" //@mark(myFoo, "myFoo"),godef("myFoo", myFoo) - "golang.org/x/tools/internal/lsp/godef/a" //@mark(AImport, re"\".*\"") + myFoo "golang.org/lsptests/foo" //@mark(myFoo, "myFoo"),godef("myFoo", myFoo) + "golang.org/lsptests/godef/a" //@mark(AImport, re"\".*\"") ) type Embed struct { diff --git a/internal/lsp/testdata/godef/b/b.go.golden b/internal/lsp/testdata/godef/b/b.go.golden index 073d633ce3e..4e06a3a8987 100644 --- a/internal/lsp/testdata/godef/b/b.go.golden +++ b/internal/lsp/testdata/godef/b/b.go.golden @@ -6,7 +6,7 @@ func (a.I).B() @mark(AB, "B") -[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#I.B) +[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#I.B) -- AField-hoverdef -- ```go field Field int @@ -15,7 +15,7 @@ field Field int @mark(AField, "Field") -[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#S.Field) +[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#S.Field) -- AField2-hoverdef -- ```go field Field2 int @@ -24,7 +24,7 @@ field Field2 int @mark(AField2, "Field2") -[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#R.Field2) +[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#R.Field2) -- AGoodbye-hoverdef -- ```go func (a.H).Goodbye() @@ -33,7 +33,7 @@ func (a.H).Goodbye() @mark(AGoodbye, "Goodbye") -[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#H.Goodbye) +[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#H.Goodbye) -- AHello-hoverdef -- ```go func (a.J).Hello() @@ -42,25 +42,25 @@ func (a.J).Hello() @mark(AHello, "Hello") -[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#J.Hello) +[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#J.Hello) -- AHey-hoverdef -- ```go func (a.R).Hey() ``` -[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#R.Hey) +[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#R.Hey) -- AHi-hoverdef -- ```go func (a.A).Hi() ``` -[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A.Hi) +[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A.Hi) -- AImport-definition -- -godef/b/b.go:5:2-43: defined here as ```go -package a ("golang.org/x/tools/internal/lsp/godef/a") +godef/b/b.go:5:2-31: defined here as ```go +package a ("golang.org/lsptests/godef/a") ``` -[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a) +[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a) -- AImport-definition-json -- { "span": { @@ -68,23 +68,23 @@ package a ("golang.org/x/tools/internal/lsp/godef/a") "start": { "line": 5, "column": 2, - "offset": 112 + "offset": 100 }, "end": { "line": 5, - "column": 43, - "offset": 153 + "column": 31, + "offset": 129 } }, - "description": "```go\npackage a (\"golang.org/x/tools/internal/lsp/godef/a\")\n```\n\n[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a)" + "description": "```go\npackage a (\"golang.org/lsptests/godef/a\")\n```\n\n[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a)" } -- AImport-hoverdef -- ```go -package a ("golang.org/x/tools/internal/lsp/godef/a") +package a ("golang.org/lsptests/godef/a") ``` -[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a) +[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a) -- AString-definition -- godef/a/a.go:26:6-7: defined here as ```go type A string @@ -93,7 +93,7 @@ type A string @mark(AString, "A") -[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A) +[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A) -- AString-definition-json -- { "span": { @@ -109,7 +109,7 @@ type A string "offset": 468 } }, - "description": "```go\ntype A string\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A)" + "description": "```go\ntype A string\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A)" } -- AString-hoverdef -- @@ -120,13 +120,13 @@ type A string @mark(AString, "A") -[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#A) +[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A) -- AStuff-definition -- godef/a/a.go:28:6-12: defined here as ```go func a.AStuff() ``` -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff) +[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff) -- AStuff-definition-json -- { "span": { @@ -142,7 +142,7 @@ func a.AStuff() "offset": 510 } }, - "description": "```go\nfunc a.AStuff()\n```\n\n[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff)" + "description": "```go\nfunc a.AStuff()\n```\n\n[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff)" } -- AStuff-hoverdef -- @@ -150,7 +150,7 @@ func a.AStuff() func a.AStuff() ``` -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff) +[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff) -- S1-definition -- godef/b/b.go:27:6-8: defined here as ```go type S1 struct { @@ -161,7 +161,7 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) -- S1-definition-json -- { "span": { @@ -169,15 +169,15 @@ type S1 struct { "start": { "line": 27, "column": 6, - "offset": 587 + "offset": 563 }, "end": { "line": 27, "column": 8, - "offset": 589 + "offset": 565 } }, - "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)" + "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1)" } -- S1-hoverdef -- @@ -190,7 +190,7 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) -- S1F1-definition -- godef/b/b.go:28:2-4: defined here as ```go field F1 int @@ -199,7 +199,7 @@ field F1 int @mark(S1F1, "F1") -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) -- S1F1-definition-json -- { "span": { @@ -207,15 +207,15 @@ field F1 int "start": { "line": 28, "column": 2, - "offset": 606 + "offset": 582 }, "end": { "line": 28, "column": 4, - "offset": 608 + "offset": 584 } }, - "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)" + "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1)" } -- S1F1-hoverdef -- @@ -226,7 +226,7 @@ field F1 int @mark(S1F1, "F1") -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) -- S1S2-definition -- godef/b/b.go:29:2-4: defined here as ```go field S2 S2 @@ -235,7 +235,7 @@ field S2 S2 @godef("S2", S2),mark(S1S2, "S2") -[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2) +[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.S2) -- S1S2-definition-json -- { "span": { @@ -243,15 +243,15 @@ field S2 S2 "start": { "line": 29, "column": 2, - "offset": 638 + "offset": 614 }, "end": { "line": 29, "column": 4, - "offset": 640 + "offset": 616 } }, - "description": "```go\nfield S2 S2\n```\n\n@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2)" + "description": "```go\nfield S2 S2\n```\n\n@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.S2)" } -- S1S2-hoverdef -- @@ -262,7 +262,7 @@ field S2 S2 @godef("S2", S2),mark(S1S2, "S2") -[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.S2) +[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.S2) -- S2-definition -- godef/b/b.go:34:6-8: defined here as ```go type S2 struct { @@ -272,7 +272,7 @@ type S2 struct { } ``` -[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2) +[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2) -- S2-definition-json -- { "span": { @@ -280,15 +280,15 @@ type S2 struct { "start": { "line": 34, "column": 6, - "offset": 762 + "offset": 738 }, "end": { "line": 34, "column": 8, - "offset": 764 + "offset": 740 } }, - "description": "```go\ntype S2 struct {\n\tF1 string //@mark(S2F1, \"F1\")\n\tF2 int //@mark(S2F2, \"F2\")\n\t*a.A //@godef(\"A\", AString),godef(\"a\",AImport)\n}\n```\n\n[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2)" + "description": "```go\ntype S2 struct {\n\tF1 string //@mark(S2F1, \"F1\")\n\tF2 int //@mark(S2F2, \"F2\")\n\t*a.A //@godef(\"A\", AString),godef(\"a\",AImport)\n}\n```\n\n[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2)" } -- S2-hoverdef -- @@ -300,7 +300,7 @@ type S2 struct { } ``` -[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2) +[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2) -- S2F1-definition -- godef/b/b.go:35:2-4: defined here as ```go field F1 string @@ -309,7 +309,7 @@ field F1 string @mark(S2F1, "F1") -[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1) +[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F1) -- S2F1-definition-json -- { "span": { @@ -317,15 +317,15 @@ field F1 string "start": { "line": 35, "column": 2, - "offset": 781 + "offset": 757 }, "end": { "line": 35, "column": 4, - "offset": 783 + "offset": 759 } }, - "description": "```go\nfield F1 string\n```\n\n@mark(S2F1, \"F1\")\n\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1)" + "description": "```go\nfield F1 string\n```\n\n@mark(S2F1, \"F1\")\n\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F1)" } -- S2F1-hoverdef -- @@ -336,7 +336,7 @@ field F1 string @mark(S2F1, "F1") -[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F1) +[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F1) -- S2F2-definition -- godef/b/b.go:36:2-4: defined here as ```go field F2 int @@ -345,7 +345,7 @@ field F2 int @mark(S2F2, "F2") -[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2) +[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F2) -- S2F2-definition-json -- { "span": { @@ -353,15 +353,15 @@ field F2 int "start": { "line": 36, "column": 2, - "offset": 814 + "offset": 790 }, "end": { "line": 36, "column": 4, - "offset": 816 + "offset": 792 } }, - "description": "```go\nfield F2 int\n```\n\n@mark(S2F2, \"F2\")\n\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2)" + "description": "```go\nfield F2 int\n```\n\n@mark(S2F2, \"F2\")\n\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F2)" } -- S2F2-hoverdef -- @@ -372,7 +372,7 @@ field F2 int @mark(S2F2, "F2") -[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S2.F2) +[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F2) -- aAlias-definition -- godef/b/b.go:25:6-12: defined here as ```go type aAlias = a.A @@ -386,12 +386,12 @@ type aAlias = a.A "start": { "line": 25, "column": 6, - "offset": 542 + "offset": 518 }, "end": { "line": 25, "column": 12, - "offset": 548 + "offset": 524 } }, "description": "```go\ntype aAlias = a.A\n```\n\n@mark(aAlias, \"aAlias\")" @@ -412,7 +412,7 @@ const X untyped int = 0 @mark(bX, "X"),godef("X", bX) -[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X) +[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#X) -- bX-definition-json -- { "span": { @@ -420,15 +420,15 @@ const X untyped int = 0 "start": { "line": 57, "column": 7, - "offset": 1249 + "offset": 1225 }, "end": { "line": 57, "column": 8, - "offset": 1250 + "offset": 1226 } }, - "description": "```go\nconst X untyped int = 0\n```\n\n@mark(bX, \"X\"),godef(\"X\", bX)\n\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X)" + "description": "```go\nconst X untyped int = 0\n```\n\n@mark(bX, \"X\"),godef(\"X\", bX)\n\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#X)" } -- bX-hoverdef -- @@ -439,13 +439,13 @@ const X untyped int = 0 @mark(bX, "X"),godef("X", bX) -[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#X) +[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#X) -- myFoo-definition -- godef/b/b.go:4:2-7: defined here as ```go -package myFoo ("golang.org/x/tools/internal/lsp/foo") +package myFoo ("golang.org/lsptests/foo") ``` -[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo) +[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/foo) -- myFoo-definition-json -- { "span": { @@ -461,12 +461,12 @@ package myFoo ("golang.org/x/tools/internal/lsp/foo") "offset": 26 } }, - "description": "```go\npackage myFoo (\"golang.org/x/tools/internal/lsp/foo\")\n```\n\n[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo)" + "description": "```go\npackage myFoo (\"golang.org/lsptests/foo\")\n```\n\n[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/foo)" } -- myFoo-hoverdef -- ```go -package myFoo ("golang.org/x/tools/internal/lsp/foo") +package myFoo ("golang.org/lsptests/foo") ``` -[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo) +[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/foo) diff --git a/internal/lsp/testdata/godef/b/c.go.golden b/internal/lsp/testdata/godef/b/c.go.golden index a48dbd2b881..575bd1e7b51 100644 --- a/internal/lsp/testdata/godef/b/c.go.golden +++ b/internal/lsp/testdata/godef/b/c.go.golden @@ -8,7 +8,7 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) -- S1-definition-json -- { "span": { @@ -16,15 +16,15 @@ type S1 struct { "start": { "line": 27, "column": 6, - "offset": 587 + "offset": 563 }, "end": { "line": 27, "column": 8, - "offset": 589 + "offset": 565 } }, - "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1)" + "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1)" } -- S1-hoverdef -- @@ -37,7 +37,7 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) -- S1F1-definition -- godef/b/b.go:28:2-4: defined here as ```go field F1 int @@ -46,7 +46,7 @@ field F1 int @mark(S1F1, "F1") -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) -- S1F1-definition-json -- { "span": { @@ -54,15 +54,15 @@ field F1 int "start": { "line": 28, "column": 2, - "offset": 606 + "offset": 582 }, "end": { "line": 28, "column": 4, - "offset": 608 + "offset": 584 } }, - "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1)" + "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1)" } -- S1F1-hoverdef -- @@ -73,4 +73,4 @@ field F1 int @mark(S1F1, "F1") -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b#S1.F1) +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) diff --git a/internal/lsp/testdata/godef/b/e.go b/internal/lsp/testdata/godef/b/e.go index 7b96cd7e8ae..9c81cad3171 100644 --- a/internal/lsp/testdata/godef/b/e.go +++ b/internal/lsp/testdata/godef/b/e.go @@ -3,7 +3,7 @@ package b import ( "fmt" - "golang.org/x/tools/internal/lsp/godef/a" + "golang.org/lsptests/godef/a" ) func useThings() { diff --git a/internal/lsp/testdata/godef/b/e.go.golden b/internal/lsp/testdata/godef/b/e.go.golden index d03b6728a4f..75e493bdbee 100644 --- a/internal/lsp/testdata/godef/b/e.go.golden +++ b/internal/lsp/testdata/godef/b/e.go.golden @@ -6,7 +6,7 @@ field Member string @Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member) -- Member-definition-json -- { "span": { @@ -22,7 +22,7 @@ field Member string "offset": 96 } }, - "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member)" + "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)" } -- Member-hoverdef -- @@ -33,7 +33,7 @@ field Member string @Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member) -- Other-definition -- godef/a/d.go:9:5-10: defined here as ```go var a.Other a.Thing @@ -42,7 +42,7 @@ var a.Other a.Thing @Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other) -- Other-definition-json -- { "span": { @@ -58,7 +58,7 @@ var a.Other a.Thing "offset": 126 } }, - "description": "```go\nvar a.Other a.Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other)" + "description": "```go\nvar a.Other a.Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)" } -- Other-hoverdef -- @@ -69,7 +69,7 @@ var a.Other a.Thing @Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other) -- Thing-definition -- godef/a/d.go:5:6-11: defined here as ```go type Thing struct { @@ -77,7 +77,7 @@ type Thing struct { } ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) -- Thing-definition-json -- { "span": { @@ -93,7 +93,7 @@ type Thing struct { "offset": 70 } }, - "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing)" + "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)" } -- Thing-hoverdef -- @@ -103,13 +103,13 @@ type Thing struct { } ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) -- Things-definition -- godef/a/d.go:11:6-12: defined here as ```go func a.Things(val []string) []a.Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things) -- Things-definition-json -- { "span": { @@ -125,7 +125,7 @@ func a.Things(val []string) []a.Thing "offset": 154 } }, - "description": "```go\nfunc a.Things(val []string) []a.Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things)" + "description": "```go\nfunc a.Things(val []string) []a.Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)" } -- Things-hoverdef -- @@ -133,7 +133,7 @@ func a.Things(val []string) []a.Thing func a.Things(val []string) []a.Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things) -- eInt-hoverdef -- ```go var x int diff --git a/internal/lsp/testdata/godef/b/h.go b/internal/lsp/testdata/godef/b/h.go index c8cbe850f9c..88017643336 100644 --- a/internal/lsp/testdata/godef/b/h.go +++ b/internal/lsp/testdata/godef/b/h.go @@ -1,6 +1,6 @@ package b -import . "golang.org/x/tools/internal/lsp/godef/a" +import . "golang.org/lsptests/godef/a" func _() { // variable of type a.A diff --git a/internal/lsp/testdata/godef/b/h.go.golden b/internal/lsp/testdata/godef/b/h.go.golden index 08a15395e92..04c7a291338 100644 --- a/internal/lsp/testdata/godef/b/h.go.golden +++ b/internal/lsp/testdata/godef/b/h.go.golden @@ -3,7 +3,7 @@ func AStuff() ``` -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a#AStuff) +[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff) -- AVariable-hoverdef -- ```go var _ A diff --git a/internal/lsp/testdata/godef/hover_generics/hover.go.golden b/internal/lsp/testdata/godef/hover_generics/hover.go.golden index dad9d343bdf..7f302ab6865 100644 --- a/internal/lsp/testdata/godef/hover_generics/hover.go.golden +++ b/internal/lsp/testdata/godef/hover_generics/hover.go.golden @@ -14,7 +14,7 @@ field Q int @mark(ValueQfield, "Q"),hoverdef("Q", ValueQfield) -[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/hover_generics#Value.Q) +[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/hover_generics#Value.Q) -- ValueTdecl-hoverdef -- ```go type parameter T any diff --git a/internal/lsp/testdata/good/good1.go b/internal/lsp/testdata/good/good1.go index 3014e2a3d55..624d8147af2 100644 --- a/internal/lsp/testdata/good/good1.go +++ b/internal/lsp/testdata/good/good1.go @@ -1,7 +1,7 @@ package good //@diag("package", "no_diagnostics", "", "error") import ( - "golang.org/x/tools/internal/lsp/types" //@item(types_import, "types", "\"golang.org/x/tools/internal/lsp/types\"", "package") + "golang.org/lsptests/types" //@item(types_import, "types", "\"golang.org/lsptests/types\"", "package") ) func random() int { //@item(good_random, "random", "func() int", "func") diff --git a/internal/lsp/testdata/implementation/implementation.go b/internal/lsp/testdata/implementation/implementation.go index c3229121a3d..b817319d5ef 100644 --- a/internal/lsp/testdata/implementation/implementation.go +++ b/internal/lsp/testdata/implementation/implementation.go @@ -1,6 +1,6 @@ package implementation -import "golang.org/x/tools/internal/lsp/implementation/other" +import "golang.org/lsptests/implementation/other" type ImpP struct{} //@ImpP,implementations("ImpP", Laugher, OtherLaugher) diff --git a/internal/lsp/testdata/importedcomplit/imported_complit.go.in b/internal/lsp/testdata/importedcomplit/imported_complit.go.in index 80d85245cb4..2f4cbada141 100644 --- a/internal/lsp/testdata/importedcomplit/imported_complit.go.in +++ b/internal/lsp/testdata/importedcomplit/imported_complit.go.in @@ -1,16 +1,16 @@ package importedcomplit import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" // import completions "fm" //@complete("\" //", fmtImport) "go/pars" //@complete("\" //", parserImport) - "golang.org/x/tools/internal/lsp/signa" //@complete("na\" //", signatureImport) - "golang.org/x/too" //@complete("\" //", toolsImport) + "golang.org/lsptests/signa" //@complete("na\" //", signatureImport) + "golang.org/lspte" //@complete("\" //", lsptestsImport) "crypto/elli" //@complete("\" //", cryptoImport) - "golang.org/x/tools/internal/lsp/sign" //@complete("\" //", signatureImport) - "golang.org/x/tools/internal/lsp/sign" //@complete("ols", toolsImport) + "golang.org/lsptests/sign" //@complete("\" //", signatureImport) + "golang.org/lsptests/sign" //@complete("ests", lsptestsImport) namedParser "go/pars" //@complete("\" //", parserImport) ) @@ -37,6 +37,6 @@ func _() { /* "fmt" */ //@item(fmtImport, "fmt", "\"fmt\"", "package") /* "go/parser" */ //@item(parserImport, "parser", "\"go/parser\"", "package") -/* "golang.org/x/tools/internal/lsp/signature" */ //@item(signatureImport, "signature", "\"golang.org/x/tools/internal/lsp/signature\"", "package") -/* "golang.org/x/tools/" */ //@item(toolsImport, "tools/", "\"golang.org/x/tools/\"", "package") +/* "golang.org/lsptests/signature" */ //@item(signatureImport, "signature", "\"golang.org/lsptests/signature\"", "package") +/* "golang.org/lsptests/" */ //@item(lsptestsImport, "lsptests/", "\"golang.org/lsptests/\"", "package") /* "crypto/elliptic" */ //@item(cryptoImport, "elliptic", "\"crypto/elliptic\"", "package") diff --git a/internal/lsp/testdata/links/links.go b/internal/lsp/testdata/links/links.go index 06ab290e84a..378134341b4 100644 --- a/internal/lsp/testdata/links/links.go +++ b/internal/lsp/testdata/links/links.go @@ -3,7 +3,7 @@ package links import ( "fmt" //@link(`fmt`,"https://pkg.go.dev/fmt") - "golang.org/x/tools/internal/lsp/foo" //@link(`golang.org/x/tools/internal/lsp/foo`,`https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo`) + "golang.org/lsptests/foo" //@link(`golang.org/lsptests/foo`,`https://pkg.go.dev/golang.org/lsptests/foo`) _ "database/sql" //@link(`database/sql`, `https://pkg.go.dev/database/sql`) ) diff --git a/internal/lsp/testdata/nodisk/nodisk.overlay.go b/internal/lsp/testdata/nodisk/nodisk.overlay.go index f9194be569c..08aebd12f7b 100644 --- a/internal/lsp/testdata/nodisk/nodisk.overlay.go +++ b/internal/lsp/testdata/nodisk/nodisk.overlay.go @@ -1,7 +1,7 @@ package nodisk import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/references/another/another.go b/internal/lsp/testdata/references/another/another.go index 47bda1e4acf..20e3ebca1cb 100644 --- a/internal/lsp/testdata/references/another/another.go +++ b/internal/lsp/testdata/references/another/another.go @@ -2,7 +2,7 @@ package another import ( - other "golang.org/x/tools/internal/lsp/references/other" + other "golang.org/lsptests/references/other" ) func _() { diff --git a/internal/lsp/testdata/references/other/other.go b/internal/lsp/testdata/references/other/other.go index de35cc81a9e..daac1a0282b 100644 --- a/internal/lsp/testdata/references/other/other.go +++ b/internal/lsp/testdata/references/other/other.go @@ -1,7 +1,7 @@ package other import ( - references "golang.org/x/tools/internal/lsp/references" + references "golang.org/lsptests/references" ) func GetXes() []references.X { diff --git a/internal/lsp/testdata/rename/b/b.go.golden b/internal/lsp/testdata/rename/b/b.go.golden index 9cdc5677fd4..36c6d39d0e8 100644 --- a/internal/lsp/testdata/rename/b/b.go.golden +++ b/internal/lsp/testdata/rename/b/b.go.golden @@ -46,7 +46,7 @@ func Goodbye() {} //@rename("Hello", "Goodbye") c.go: package c -import "golang.org/x/tools/internal/lsp/rename/b" +import "golang.org/lsptests/rename/b" func _() { b.Goodbye() //@rename("Hello", "Goodbye") diff --git a/internal/lsp/testdata/rename/bad/bad.go.golden b/internal/lsp/testdata/rename/bad/bad.go.golden index 7f45813926a..1b27e1782f3 100644 --- a/internal/lsp/testdata/rename/bad/bad.go.golden +++ b/internal/lsp/testdata/rename/bad/bad.go.golden @@ -1,2 +1,2 @@ -- rFunc-rename -- -renaming "sFunc" to "rFunc" not possible because "golang.org/x/tools/internal/lsp/rename/bad" has errors +renaming "sFunc" to "rFunc" not possible because "golang.org/lsptests/rename/bad" has errors diff --git a/internal/lsp/testdata/rename/c/c.go b/internal/lsp/testdata/rename/c/c.go index 519d2f6fcdf..6332c78f3f9 100644 --- a/internal/lsp/testdata/rename/c/c.go +++ b/internal/lsp/testdata/rename/c/c.go @@ -1,6 +1,6 @@ package c -import "golang.org/x/tools/internal/lsp/rename/b" +import "golang.org/lsptests/rename/b" func _() { b.Hello() //@rename("Hello", "Goodbye") diff --git a/internal/lsp/testdata/rename/c/c.go.golden b/internal/lsp/testdata/rename/c/c.go.golden index 56937420c59..d56250693a9 100644 --- a/internal/lsp/testdata/rename/c/c.go.golden +++ b/internal/lsp/testdata/rename/c/c.go.golden @@ -24,7 +24,7 @@ func Goodbye() {} //@rename("Hello", "Goodbye") c.go: package c -import "golang.org/x/tools/internal/lsp/rename/b" +import "golang.org/lsptests/rename/b" func _() { b.Goodbye() //@rename("Hello", "Goodbye") diff --git a/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden b/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden index 810926de627..49ff7f841cf 100644 --- a/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden +++ b/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden @@ -11,7 +11,7 @@ var Bar int //@rename("Bar", "Tomato") other.go: package other -import "golang.org/x/tools/internal/lsp/rename/crosspkg" +import "golang.org/lsptests/rename/crosspkg" func Other() { crosspkg.Bar @@ -31,7 +31,7 @@ var Tomato int //@rename("Bar", "Tomato") other.go: package other -import "golang.org/x/tools/internal/lsp/rename/crosspkg" +import "golang.org/lsptests/rename/crosspkg" func Other() { crosspkg.Tomato diff --git a/internal/lsp/testdata/rename/crosspkg/other/other.go b/internal/lsp/testdata/rename/crosspkg/other/other.go index 10d17cd34b5..5fd147da62e 100644 --- a/internal/lsp/testdata/rename/crosspkg/other/other.go +++ b/internal/lsp/testdata/rename/crosspkg/other/other.go @@ -1,6 +1,6 @@ package other -import "golang.org/x/tools/internal/lsp/rename/crosspkg" +import "golang.org/lsptests/rename/crosspkg" func Other() { crosspkg.Bar diff --git a/internal/lsp/testdata/rename/crosspkg/other/other.go.golden b/internal/lsp/testdata/rename/crosspkg/other/other.go.golden index 2722ad96e61..f7b4aaad42f 100644 --- a/internal/lsp/testdata/rename/crosspkg/other/other.go.golden +++ b/internal/lsp/testdata/rename/crosspkg/other/other.go.golden @@ -11,7 +11,7 @@ var Bar int //@rename("Bar", "Tomato") other.go: package other -import "golang.org/x/tools/internal/lsp/rename/crosspkg" +import "golang.org/lsptests/rename/crosspkg" func Other() { crosspkg.Bar diff --git a/internal/lsp/testdata/selector/selector.go.in b/internal/lsp/testdata/selector/selector.go.in index 277f98bde7c..b1498a08c77 100644 --- a/internal/lsp/testdata/selector/selector.go.in +++ b/internal/lsp/testdata/selector/selector.go.in @@ -3,7 +3,7 @@ package selector import ( - "golang.org/x/tools/internal/lsp/bar" + "golang.org/lsptests/bar" ) type S struct { diff --git a/internal/lsp/testdata/signature/signature_test.go b/internal/lsp/testdata/signature/signature_test.go index 62e54a23834..500247dbdec 100644 --- a/internal/lsp/testdata/signature/signature_test.go +++ b/internal/lsp/testdata/signature/signature_test.go @@ -3,7 +3,7 @@ package signature_test import ( "testing" - sig "golang.org/x/tools/internal/lsp/signature" + sig "golang.org/lsptests/signature" ) func TestSignature(t *testing.T) { diff --git a/internal/lsp/testdata/snippets/literal.go b/internal/lsp/testdata/snippets/literal.go index 43931d18ef7..fbb642f08a5 100644 --- a/internal/lsp/testdata/snippets/literal.go +++ b/internal/lsp/testdata/snippets/literal.go @@ -1,8 +1,8 @@ package snippets import ( - "golang.org/x/tools/internal/lsp/signature" - t "golang.org/x/tools/internal/lsp/types" + "golang.org/lsptests/signature" + t "golang.org/lsptests/types" ) type structy struct { diff --git a/internal/lsp/testdata/snippets/literal_snippets.go.in b/internal/lsp/testdata/snippets/literal_snippets.go.in index 4a2a01dfa1f..c6e6c0fbd60 100644 --- a/internal/lsp/testdata/snippets/literal_snippets.go.in +++ b/internal/lsp/testdata/snippets/literal_snippets.go.in @@ -7,7 +7,7 @@ import ( "net/http" "sort" - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/stub/stub_renamed_import_iface.go b/internal/lsp/testdata/stub/stub_renamed_import_iface.go index 26142d0a893..0f175868504 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import_iface.go +++ b/internal/lsp/testdata/stub/stub_renamed_import_iface.go @@ -1,7 +1,7 @@ package stub import ( - "golang.org/x/tools/internal/lsp/stub/other" + "golang.org/lsptests/stub/other" ) // This file tests that if an interface diff --git a/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden b/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden index 134c24bf88a..3d6ac0a551c 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden +++ b/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden @@ -4,7 +4,7 @@ package stub import ( "bytes" renamed_context "context" - "golang.org/x/tools/internal/lsp/stub/other" + "golang.org/lsptests/stub/other" ) // This file tests that if an interface diff --git a/internal/lsp/testdata/testy/testy_test.go b/internal/lsp/testdata/testy/testy_test.go index 4939f86b50b..487b7301761 100644 --- a/internal/lsp/testdata/testy/testy_test.go +++ b/internal/lsp/testdata/testy/testy_test.go @@ -3,8 +3,8 @@ package testy import ( "testing" - sig "golang.org/x/tools/internal/lsp/signature" - "golang.org/x/tools/internal/lsp/snippets" + sig "golang.org/lsptests/signature" + "golang.org/lsptests/snippets" ) func TestSomething(t *testing.T) { //@item(TestSomething, "TestSomething(t *testing.T)", "", "func") diff --git a/internal/lsp/testdata/unimported/export_test.go b/internal/lsp/testdata/unimported/export_test.go index 4f85700fa79..964d27d3b94 100644 --- a/internal/lsp/testdata/unimported/export_test.go +++ b/internal/lsp/testdata/unimported/export_test.go @@ -1,3 +1,3 @@ package unimported -var TestExport int //@item(testexport, "TestExport", "(from \"golang.org/x/tools/internal/lsp/unimported\")", "var") +var TestExport int //@item(testexport, "TestExport", "(from \"golang.org/lsptests/unimported\")", "var") diff --git a/internal/lsp/testdata/unimported/unimported.go.in b/internal/lsp/testdata/unimported/unimported.go.in index c3c0243d901..4d1438d1bd8 100644 --- a/internal/lsp/testdata/unimported/unimported.go.in +++ b/internal/lsp/testdata/unimported/unimported.go.in @@ -14,7 +14,7 @@ func _() { /* ring.Ring */ //@item(ringring, "Ring", "(from \"container/ring\")", "var") -/* signature.Foo */ //@item(signaturefoo, "Foo", "func(a string, b int) (c bool) (from \"golang.org/x/tools/internal/lsp/signature\")", "func") +/* signature.Foo */ //@item(signaturefoo, "Foo", "func(a string, b int) (c bool) (from \"golang.org/lsptests/signature\")", "func") /* context.Background */ //@item(contextBackground, "Background", "func() context.Context (from \"context\")", "func") /* context.Background().Err */ //@item(contextBackgroundErr, "Background().Err", "func() error (from \"context\")", "method") diff --git a/internal/lsp/testdata/unimported/unimported_cand_type.go b/internal/lsp/testdata/unimported/unimported_cand_type.go index 531aa2d180a..554c426a998 100644 --- a/internal/lsp/testdata/unimported/unimported_cand_type.go +++ b/internal/lsp/testdata/unimported/unimported_cand_type.go @@ -3,8 +3,8 @@ package unimported import ( _ "context" - "golang.org/x/tools/internal/lsp/baz" - _ "golang.org/x/tools/internal/lsp/signature" // provide type information for unimported completions in the other file + "golang.org/lsptests/baz" + _ "golang.org/lsptests/signature" // provide type information for unimported completions in the other file ) func _() { diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index eb7c26e6ba8..53c04f1220e 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -42,7 +42,12 @@ const ( overlayFileSuffix = ".overlay" goldenFileSuffix = ".golden" inFileSuffix = ".in" - testModule = "golang.org/x/tools/internal/lsp" + + // The module path containing the testdata packages. + // + // Warning: the length of this module path matters, as we have bumped up + // against command-line limitations on windows (golang/go#54800). + testModule = "golang.org/lsptests" ) var summaryFile = "summary.txt" From 49ab44de906c9aaa48918fed8208d4d22efd0402 Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Thu, 25 Aug 2022 11:52:14 -0700 Subject: [PATCH 213/723] x/tools/internal/lsp: re-enable a test with adjusted error message This is a follow-up on CL 425497. Must be submitted after CL 425007. For golang/go#54511. Change-Id: Ice9c1c62b911efa8efa20ea295bf313aabe8c5bb Reviewed-on: https://go-review.googlesource.com/c/tools/+/425594 Reviewed-by: Robert Griesemer TryBot-Result: Gopher Robot Run-TryBot: Robert Griesemer Reviewed-by: Alan Donovan --- internal/lsp/testdata/badstmt/badstmt.go.in | 11 ++--------- internal/lsp/testdata/summary.txt.golden | 4 ++-- internal/lsp/testdata/summary_go1.18.txt.golden | 4 ++-- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/internal/lsp/testdata/badstmt/badstmt.go.in b/internal/lsp/testdata/badstmt/badstmt.go.in index 1ac9f0a8078..8f0654a8a52 100644 --- a/internal/lsp/testdata/badstmt/badstmt.go.in +++ b/internal/lsp/testdata/badstmt/badstmt.go.in @@ -5,16 +5,9 @@ import ( ) func _() { - /* - Temporarily disabled so we can change the parser error message - TODO(gri) uncomment once CL 425007 is submitted, and remove spaces - between // and @. - - defer foo.F // @complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement", "error") + defer foo.F //@complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement|expression in defer must be function call", "error") y := 1 - - defer foo.F // @complete(" //", Foo) - */ + defer foo.F //@complete(" //", Foo) } func _() { diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index 56c7f48aa3d..9324833570f 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -1,14 +1,14 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 263 +CompletionsCount = 265 CompletionSnippetCount = 106 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 164 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 37 +DiagnosticsCount = 38 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index f195be907a6..9ec3c79d3c3 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -1,14 +1,14 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 264 +CompletionsCount = 266 CompletionSnippetCount = 116 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 174 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 37 +DiagnosticsCount = 38 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 From 6c10975b72dcad6a00720f002f908e146ac6737c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 31 Aug 2022 15:41:08 -0400 Subject: [PATCH 214/723] internal/lsp/cache: honor RunDespiteErrors=false This change prevents analysis from running on a package containing any kind of error unless the analyzer has indicated that it is robust. This is presumed to be the cause of several panics in production. And a test. Updates golang/go#54762 Change-Id: I9327e18ef8d7773c943ea45fc786991188358131 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426803 Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- internal/lsp/cache/analysis.go | 6 +++--- .../testdata/rundespiteerrors/rundespiteerrors.go | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go index 5a29feffa7f..144ef3ce4ea 100644 --- a/internal/lsp/cache/analysis.go +++ b/internal/lsp/cache/analysis.go @@ -329,9 +329,9 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a } analysisinternal.SetTypeErrors(pass, pkg.typeErrors) - // TODO(adonovan): fix: don't run analyzers on packages - // containing type errors unless the analyzers have - // RunDespiteErrors=true. + if (pkg.HasListOrParseErrors() || pkg.HasTypeErrors()) && !analyzer.RunDespiteErrors { + return nil, fmt.Errorf("skipping analysis %s because package %s contains errors", analyzer.Name, pkg.ID()) + } // Recover from panics (only) within the analyzer logic. // (Use an anonymous function to limit the recover scope.) diff --git a/internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go b/internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go new file mode 100644 index 00000000000..783e9a55f17 --- /dev/null +++ b/internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go @@ -0,0 +1,14 @@ +package rundespiteerrors + +// This test verifies that analyzers without RunDespiteErrors are not +// executed on a package containing type errors (see issue #54762). +func _() { + // A type error. + _ = 1 + "" //@diag("1", "compiler", "mismatched types|cannot convert", "error") + + // A violation of an analyzer for which RunDespiteErrors=false: + // no diagnostic is produced; the diag comment is merely illustrative. + for _ = range "" { //diag("for _", "simplifyrange", "simplify range expression", "warning") + + } +} From dfc8d4933946dc107ae28a1e7d3b014940ef3d85 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 1 Sep 2022 13:16:21 -0400 Subject: [PATCH 215/723] internal/lsp/testdata: fix diagnostics checksum Fix an incorrect diagnostics checksum causing builder failures. Change-Id: Ief4f9edd6acbf8d42eaf1109ec6ddc0085f20b05 Reviewed-on: https://go-review.googlesource.com/c/tools/+/427536 Auto-Submit: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Dylan Le Reviewed-by: Heschi Kreinick --- internal/lsp/testdata/summary.txt.golden | 2 +- internal/lsp/testdata/summary_go1.18.txt.golden | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index 9324833570f..c296ad22c70 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -8,7 +8,7 @@ DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 164 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 38 +DiagnosticsCount = 39 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 9ec3c79d3c3..6df2fa37485 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -8,7 +8,7 @@ DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 174 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 38 +DiagnosticsCount = 39 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 From f16be35d921e4b940c7707ee89d21b391dace03f Mon Sep 17 00:00:00 2001 From: Brian Pursley Date: Thu, 11 Aug 2022 14:06:41 -0400 Subject: [PATCH 216/723] internal/lsp/source: add functions to type hover output Add functions to the output when hovering over a type. Fixes golang/go#54008 Change-Id: Ia0a7b5a878c3d63c4bbc549f003c45592db1c135 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420714 Auto-Submit: Robert Findley Reviewed-by: Alan Donovan Reviewed-by: Robert Findley Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/source/hover.go | 19 ++++++++++++++ internal/lsp/testdata/godef/a/a.go | 12 ++++++--- internal/lsp/testdata/godef/a/a.go.golden | 25 ++++++++++++++++++ internal/lsp/testdata/godef/a/d.go | 26 +++++++++++++++++++ internal/lsp/testdata/godef/a/d.go.golden | 24 ++++++++++++++++- internal/lsp/testdata/godef/b/b.go.golden | 12 +++++++-- internal/lsp/testdata/godef/b/e.go.golden | 10 ++++++- internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- 9 files changed, 123 insertions(+), 9 deletions(-) diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go index 6ad458ba4cb..9ab1023d68c 100644 --- a/internal/lsp/source/hover.go +++ b/internal/lsp/source/hover.go @@ -21,6 +21,7 @@ import ( "unicode/utf8" "golang.org/x/text/unicode/runenames" + "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/bug" "golang.org/x/tools/internal/lsp/protocol" @@ -273,6 +274,24 @@ func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) if err := format.Node(&b, fset, &x2); err != nil { return nil, err } + + // Display the declared methods accessible from the identifier. + // + // (The format.Node call above displays any struct fields, public + // or private, in syntactic form. We choose not to recursively + // enumerate any fields and methods promoted from them.) + obj := i.Type.Object + if obj != nil && !types.IsInterface(obj.Type()) { + sep := "\n\n" + for _, m := range typeutil.IntuitiveMethodSet(obj.Type(), nil) { + if (m.Obj().Exported() || m.Obj().Pkg() == i.pkg.GetTypes()) && len(m.Index()) == 1 { + b.WriteString(sep) + sep = "\n" + b.WriteString(objectString(m.Obj(), i.qf, nil)) + } + } + } + h.Signature = b.String() case ast.Node: diff --git a/internal/lsp/testdata/godef/a/a.go b/internal/lsp/testdata/godef/a/a.go index 5cc85527aeb..53ca6ddc412 100644 --- a/internal/lsp/testdata/godef/a/a.go +++ b/internal/lsp/testdata/godef/a/a.go @@ -60,16 +60,16 @@ type R struct { func (_ R) Hey() {} //@mark(AHey, "Hey") -type H interface { +type H interface { //@H Goodbye() //@mark(AGoodbye, "Goodbye") } -type I interface { +type I interface { //@I B() //@mark(AB, "B") J } -type J interface { +type J interface { //@J Hello() //@mark(AHello, "Hello") } @@ -103,3 +103,9 @@ func _() { } // e has a comment ) } + +var ( + hh H //@hoverdef("H", H) + ii I //@hoverdef("I", I) + jj J //@hoverdef("J", J) +) diff --git a/internal/lsp/testdata/godef/a/a.go.golden b/internal/lsp/testdata/godef/a/a.go.golden index 0a037a9c6a5..3363100ce0d 100644 --- a/internal/lsp/testdata/godef/a/a.go.golden +++ b/internal/lsp/testdata/godef/a/a.go.golden @@ -165,6 +165,31 @@ const h untyped int = 2 Constant block. +-- H-hoverdef -- +```go +type H interface { + Goodbye() //@mark(AGoodbye, "Goodbye") +} +``` + +[`a.H` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#H) +-- I-hoverdef -- +```go +type I interface { + B() //@mark(AB, "B") + J +} +``` + +[`a.I` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#I) +-- J-hoverdef -- +```go +type J interface { + Hello() //@mark(AHello, "Hello") +} +``` + +[`a.J` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#J) -- make-hoverdef -- ```go func make(t Type, size ...int) Type diff --git a/internal/lsp/testdata/godef/a/d.go b/internal/lsp/testdata/godef/a/d.go index 2da8d058edf..a1d17ad0da3 100644 --- a/internal/lsp/testdata/godef/a/d.go +++ b/internal/lsp/testdata/godef/a/d.go @@ -16,6 +16,16 @@ func (t Thing) Method(i int) string { //@Method return t.Member } +func (t Thing) Method3() { +} + +func (t *Thing) Method2(i int, j int) (error, string) { + return nil, t.Member +} + +func (t *Thing) private() { +} + func useThings() { t := Thing{ //@mark(aStructType, "ing") Member: "string", //@mark(fMember, "ember") @@ -26,6 +36,22 @@ func useThings() { t.Method() //@mark(aMethod, "eth") } +type NextThing struct { //@NextThing + Thing + Value int +} + +func (n NextThing) another() string { + return n.Member +} + +// Shadows Thing.Method3 +func (n *NextThing) Method3() int { + return n.Value +} + +var nextThing NextThing //@hoverdef("NextThing", NextThing) + /*@ godef(aStructType, Thing) godef(aMember, Member) diff --git a/internal/lsp/testdata/godef/a/d.go.golden b/internal/lsp/testdata/godef/a/d.go.golden index 54908ce5281..840dbaf60ac 100644 --- a/internal/lsp/testdata/godef/a/d.go.golden +++ b/internal/lsp/testdata/godef/a/d.go.golden @@ -105,6 +105,11 @@ godef/a/d.go:5:6-11: defined here as ```go type Thing struct { Member string //@Member } + +func (Thing).Method(i int) string +func (*Thing).Method2(i int, j int) (error, string) +func (Thing).Method3() +func (*Thing).private() ``` [`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) @@ -123,7 +128,7 @@ type Thing struct { "offset": 70 } }, - "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)" + "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n\nfunc (Thing).Method(i int) string\nfunc (*Thing).Method2(i int, j int) (error, string)\nfunc (Thing).Method3()\nfunc (*Thing).private()\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)" } -- Thing-hoverdef -- @@ -131,6 +136,11 @@ type Thing struct { type Thing struct { Member string //@Member } + +func (Thing).Method(i int) string +func (*Thing).Method2(i int, j int) (error, string) +func (Thing).Method3() +func (*Thing).private() ``` [`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) @@ -167,3 +177,15 @@ func Things(val []string) []Thing -- a-hoverdef -- Package a is a package for testing go to definition. +-- NextThing-hoverdef -- +```go +type NextThing struct { + Thing + Value int +} + +func (*NextThing).Method3() int +func (NextThing).another() string +``` + +[`a.NextThing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#NextThing) diff --git a/internal/lsp/testdata/godef/b/b.go.golden b/internal/lsp/testdata/godef/b/b.go.golden index 4e06a3a8987..cfe3917ba88 100644 --- a/internal/lsp/testdata/godef/b/b.go.golden +++ b/internal/lsp/testdata/godef/b/b.go.golden @@ -88,6 +88,8 @@ package a ("golang.org/lsptests/godef/a") -- AString-definition -- godef/a/a.go:26:6-7: defined here as ```go type A string + +func (a.A).Hi() ``` @mark(AString, "A") @@ -109,12 +111,14 @@ type A string "offset": 468 } }, - "description": "```go\ntype A string\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A)" + "description": "```go\ntype A string\n\nfunc (a.A).Hi()\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A)" } -- AString-hoverdef -- ```go type A string + +func (a.A).Hi() ``` @mark(AString, "A") @@ -376,6 +380,8 @@ field F2 int -- aAlias-definition -- godef/b/b.go:25:6-12: defined here as ```go type aAlias = a.A + +func (a.A).Hi() ``` @mark(aAlias, "aAlias") @@ -394,12 +400,14 @@ type aAlias = a.A "offset": 524 } }, - "description": "```go\ntype aAlias = a.A\n```\n\n@mark(aAlias, \"aAlias\")" + "description": "```go\ntype aAlias = a.A\n\nfunc (a.A).Hi()\n```\n\n@mark(aAlias, \"aAlias\")" } -- aAlias-hoverdef -- ```go type aAlias = a.A + +func (a.A).Hi() ``` @mark(aAlias, "aAlias") diff --git a/internal/lsp/testdata/godef/b/e.go.golden b/internal/lsp/testdata/godef/b/e.go.golden index 75e493bdbee..3d7d8979771 100644 --- a/internal/lsp/testdata/godef/b/e.go.golden +++ b/internal/lsp/testdata/godef/b/e.go.golden @@ -75,6 +75,10 @@ godef/a/d.go:5:6-11: defined here as ```go type Thing struct { Member string //@Member } + +func (a.Thing).Method(i int) string +func (*a.Thing).Method2(i int, j int) (error, string) +func (a.Thing).Method3() ``` [`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) @@ -93,7 +97,7 @@ type Thing struct { "offset": 70 } }, - "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)" + "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n\nfunc (a.Thing).Method(i int) string\nfunc (*a.Thing).Method2(i int, j int) (error, string)\nfunc (a.Thing).Method3()\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)" } -- Thing-hoverdef -- @@ -101,6 +105,10 @@ type Thing struct { type Thing struct { Member string //@Member } + +func (a.Thing).Method(i int) string +func (*a.Thing).Method2(i int, j int) (error, string) +func (a.Thing).Method3() ``` [`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index c296ad22c70..7420b9466fb 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -16,7 +16,7 @@ SemanticTokenCount = 3 SuggestedFixCount = 63 FunctionExtractionCount = 25 MethodExtractionCount = 6 -DefinitionsCount = 95 +DefinitionsCount = 99 TypeDefinitionsCount = 18 HighlightsCount = 69 InlayHintsCount = 4 diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 6df2fa37485..94f1e6706a0 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -16,7 +16,7 @@ SemanticTokenCount = 3 SuggestedFixCount = 67 FunctionExtractionCount = 25 MethodExtractionCount = 6 -DefinitionsCount = 108 +DefinitionsCount = 112 TypeDefinitionsCount = 18 HighlightsCount = 69 InlayHintsCount = 5 From 40cfafff0289ca242f0025af99e4eea46c44f87f Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Thu, 1 Sep 2022 13:35:53 -0700 Subject: [PATCH 217/723] x/tools/internal/lsp/source: disable some tests for std lib changes CL 425716 changes parser behavior for incorrect code (it is more lenient) which breaks a few lsp tests. Disable them for now so we can submit CL 425716. For golang/go#54511. For golang/go#54822. Change-Id: I00fa67e29628137f3e4e44c38e92094ea581a61b Reviewed-on: https://go-review.googlesource.com/c/tools/+/427654 Reviewed-by: Robert Griesemer TryBot-Result: Gopher Robot Run-TryBot: Robert Griesemer gopls-CI: kokoro Reviewed-by: Robert Findley --- internal/lsp/testdata/arraytype/array_type.go.in | 6 ++++-- internal/lsp/testdata/nested_complit/nested_complit.go.in | 3 ++- internal/lsp/testdata/summary.txt.golden | 2 +- internal/lsp/testdata/summary_go1.18.txt.golden | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/lsp/testdata/arraytype/array_type.go.in b/internal/lsp/testdata/arraytype/array_type.go.in index 84264699ccd..ac1a3e78297 100644 --- a/internal/lsp/testdata/arraytype/array_type.go.in +++ b/internal/lsp/testdata/arraytype/array_type.go.in @@ -9,7 +9,8 @@ func _() { val string //@item(atVal, "val", "string", "var") ) - [] //@complete(" //", PackageFoo) + // disabled - see issue #54822 + [] // complete(" //", PackageFoo) []val //@complete(" //") @@ -33,7 +34,8 @@ func _() { var s []myInt //@item(atS, "s", "[]myInt", "var") s = []m //@complete(" //", atMyInt) - s = [] //@complete(" //", atMyInt, PackageFoo) + // disabled - see issue #54822 + s = [] // complete(" //", atMyInt, PackageFoo) var a [1]myInt a = [1]m //@complete(" //", atMyInt) diff --git a/internal/lsp/testdata/nested_complit/nested_complit.go.in b/internal/lsp/testdata/nested_complit/nested_complit.go.in index 1dddd5b1b53..3ad2d213e98 100644 --- a/internal/lsp/testdata/nested_complit/nested_complit.go.in +++ b/internal/lsp/testdata/nested_complit/nested_complit.go.in @@ -9,6 +9,7 @@ type ncBar struct { //@item(structNCBar, "ncBar", "struct{...}", "struct") func _() { []ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var") _ := ncBar{ - baz: [] //@complete(" //", structNCFoo, structNCBar) + // disabled - see issue #54822 + baz: [] // complete(" //", structNCFoo, structNCBar) } } diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index 7420b9466fb..d83085db26c 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -1,7 +1,7 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 265 +CompletionsCount = 262 CompletionSnippetCount = 106 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 94f1e6706a0..6bfb1ef3e1d 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -1,7 +1,7 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 266 +CompletionsCount = 263 CompletionSnippetCount = 116 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 From 33c1ddd5a887633d5bd0ca5c8f0f012875e5d5d3 Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Thu, 1 Sep 2022 16:46:23 -0700 Subject: [PATCH 218/723] tools/gopls/internal/regtest/diagnostics: handle new error message Pending CL 426477 changes go/types error messages containing the phrase: type parameters require to: type parameter requires Adjust diagnostics test accordingly (note that regular expressions are currently not supported). For golang/go#54511. Change-Id: I561cb940a41cb6cc949c44e0d4b8f009336a46cd Reviewed-on: https://go-review.googlesource.com/c/tools/+/427736 TryBot-Result: Gopher Robot Run-TryBot: Robert Griesemer gopls-CI: kokoro Auto-Submit: Robert Griesemer Reviewed-by: Robert Findley Reviewed-by: Robert Griesemer --- gopls/internal/regtest/diagnostics/diagnostics_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 4d34b0aa202..4b38bf0e57c 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -2109,7 +2109,7 @@ func F[T any](_ T) { var d protocol.PublishDiagnosticsParams env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameters require"), + env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), ReadDiagnostics("main.go", &d), ), ) @@ -2143,7 +2143,7 @@ func F[T any](_ T) { var d protocol.PublishDiagnosticsParams env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameters require"), + env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), ReadDiagnostics("main.go", &d), ), ) From 655abda1c0230800004a343281dfcf5615b432ce Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 1 Sep 2022 21:02:09 -0400 Subject: [PATCH 219/723] gopls/internal/regtest: TestEditGoDirectiveWorkspace should fail eagerly TestEditGoDirectiveWorkspace did an unconditional wait on expected diagnostics. Turn this into OnceMet conditions that fail as soon as the relevant diagnostic pass is complete. Fixes golang/go#54825 Change-Id: I2654682108561dd60546a589ebd1c6aa2ebaff1f Reviewed-on: https://go-review.googlesource.com/c/tools/+/427543 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro --- .../internal/regtest/diagnostics/diagnostics_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 4b38bf0e57c..432b9b1e46f 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -2141,17 +2141,27 @@ func F[T any](_ T) { ` Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. var d protocol.PublishDiagnosticsParams + + // Once the initial workspace load is complete, we should have a diagnostic + // because generics are not supported at 1.16. env.Await( OnceMet( + InitialWorkspaceLoad, env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), ReadDiagnostics("main.go", &d), ), ) + // This diagnostic should have a quick fix to edit the go version. env.ApplyQuickFixes("main.go", d.Diagnostics) + // Once the edit is applied, the problematic diagnostics should be + // resolved. env.Await( - EmptyDiagnostics("main.go"), + OnceMet( + env.DoneWithChangeWatchedFiles(), // go.mod should have been quick-fixed + EmptyDiagnostics("main.go"), + ), ) }) } From eb8352e30701c81c710e5d1693fa7641079d7e5d Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 1 Sep 2022 17:17:06 -0400 Subject: [PATCH 220/723] gopls/internal/govulncheck: sync x/vuln@62b0186 VulnDB OSV schema was changed recently https://go-review.googlesource.com/c/vulndb/+/424895 to fix the misinterpretation of 'affected.package.name', and the database entries were repopulated with the new schema. We need to update the client library to pick up the change. We also need to update the fake vulndb entries used in tests. gopls/regtest/misc/testdata/vulndb was copied from golang.org/x/vuln/cmd/govulncheck/testdata/vulndb @ 62b0186 (the version updated in cl/424895) Also reverse golang.org/cl/425183 which includes the position information in the SummarizeCallStack result. Like in govulncheck -v, the position info is already available in the callstack, thus this is unnecessary for us. Since x/vuln is currently frozen until the preview release, revert it from gopls/internal/vulncheck. Ran go mod tidy -compat=1.16; otherwise, the transitive dependency on github.com/client9/misspell from golang.org/x/vuln breaks go1.16 build. Updated copy.sh script to copy x/vuln/internal/semver package (golang/go#54401) and add the build tags back to all go files. Gopls's builder builds&tests packages with old go versions, so we still need go1.18 build tag. Fixes golang/go#54818 Change-Id: I37770d698082378656a7988d3412a4ca2196ca7b Reviewed-on: https://go-review.googlesource.com/c/tools/+/427542 gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Reviewed-by: Jonathan Amsterdam --- gopls/go.mod | 4 +- gopls/go.sum | 7 +- gopls/internal/govulncheck/cache.go | 19 +++-- gopls/internal/govulncheck/cache_test.go | 4 +- gopls/internal/govulncheck/copy.sh | 19 +++++ gopls/internal/govulncheck/filepath.go | 38 +++++++++ gopls/internal/govulncheck/filepath_test.go | 42 ++++++++++ gopls/internal/govulncheck/semver/semver.go | 84 +++++++++++++++++++ .../govulncheck/semver/semver_test.go | 43 ++++++++++ gopls/internal/govulncheck/source.go | 2 +- gopls/internal/govulncheck/util.go | 22 ++++- .../testdata/vulndb/golang.org/x/crypto.json | 2 +- .../testdata/vulndb/golang.org/x/text.json | 2 +- .../regtest/misc/testdata/vulndb/stdlib.json | 2 +- gopls/internal/regtest/misc/vuln_test.go | 4 +- gopls/internal/vulncheck/command.go | 13 ++- gopls/internal/vulncheck/command_test.go | 38 +++++++-- 17 files changed, 313 insertions(+), 32 deletions(-) create mode 100644 gopls/internal/govulncheck/filepath.go create mode 100644 gopls/internal/govulncheck/filepath_test.go create mode 100644 gopls/internal/govulncheck/semver/semver.go create mode 100644 gopls/internal/govulncheck/semver/semver_test.go diff --git a/gopls/go.mod b/gopls/go.mod index 49f5804442a..d994f48ef60 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -10,12 +10,14 @@ require ( golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 golang.org/x/tools v0.1.13-0.20220810174125-0ad49fdeb955 - golang.org/x/vuln v0.0.0-20220809164104-12ff722659c1 + golang.org/x/vuln v0.0.0-20220901221904-62b0186a1058 honnef.co/go/tools v0.3.3 mvdan.cc/gofumpt v0.3.1 mvdan.cc/xurls/v2 v2.4.0 ) +require golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + require ( github.com/BurntSushi/toml v1.2.0 // indirect github.com/google/safehtml v0.0.2 // indirect diff --git a/gopls/go.sum b/gopls/go.sum index 2b9d61584da..f647316716b 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -12,7 +12,6 @@ github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnX github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/google/go-cmdtest v0.4.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -43,6 +42,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e h1:7Xs2YCOpMlNqSQSmrrnhlzBXIE/bpMecZplbLePTJvE= golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= @@ -68,8 +69,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/vuln v0.0.0-20220809164104-12ff722659c1 h1:wxIK8Hnmd3ervTxk4aON+gAbfWbb2hToeKSTQd0eXgo= -golang.org/x/vuln v0.0.0-20220809164104-12ff722659c1/go.mod h1:t0tyWMAuNGUOL2N4il/aj/M5LLt4ktPpOGuTBNUqmiM= +golang.org/x/vuln v0.0.0-20220901221904-62b0186a1058 h1:YnB27EXBD8XxB0JcaOeluuvhF2kS4DrH0k+lpopG2xc= +golang.org/x/vuln v0.0.0-20220901221904-62b0186a1058/go.mod h1:7tDfEDtOLlzHQRi4Yzfg5seVBSvouUIjyPzBx4q5CxQ= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gopls/internal/govulncheck/cache.go b/gopls/internal/govulncheck/cache.go index 404c3567320..2fa6a05d727 100644 --- a/gopls/internal/govulncheck/cache.go +++ b/gopls/internal/govulncheck/cache.go @@ -11,7 +11,6 @@ package govulncheck import ( "encoding/json" "go/build" - "io/ioutil" "os" "path/filepath" "sync" @@ -66,7 +65,7 @@ func (c *FSCache) ReadIndex(dbName string) (client.DBIndex, time.Time, error) { c.mu.Lock() defer c.mu.Unlock() - b, err := ioutil.ReadFile(filepath.Join(c.rootDir, dbName, "index.json")) + b, err := os.ReadFile(filepath.Join(c.rootDir, dbName, "index.json")) if err != nil { if os.IsNotExist(err) { return nil, time.Time{}, nil @@ -95,7 +94,7 @@ func (c *FSCache) WriteIndex(dbName string, index client.DBIndex, retrieved time if err != nil { return err } - if err := ioutil.WriteFile(filepath.Join(path, "index.json"), j, 0666); err != nil { + if err := os.WriteFile(filepath.Join(path, "index.json"), j, 0666); err != nil { return err } return nil @@ -105,7 +104,11 @@ func (c *FSCache) ReadEntries(dbName string, p string) ([]*osv.Entry, error) { c.mu.Lock() defer c.mu.Unlock() - b, err := ioutil.ReadFile(filepath.Join(c.rootDir, dbName, p, "vulns.json")) + ep, err := client.EscapeModulePath(p) + if err != nil { + return nil, err + } + b, err := os.ReadFile(filepath.Join(c.rootDir, dbName, ep, "vulns.json")) if err != nil { if os.IsNotExist(err) { return nil, nil @@ -123,7 +126,11 @@ func (c *FSCache) WriteEntries(dbName string, p string, entries []*osv.Entry) er c.mu.Lock() defer c.mu.Unlock() - path := filepath.Join(c.rootDir, dbName, p) + ep, err := client.EscapeModulePath(p) + if err != nil { + return err + } + path := filepath.Join(c.rootDir, dbName, ep) if err := os.MkdirAll(path, 0777); err != nil { return err } @@ -131,7 +138,7 @@ func (c *FSCache) WriteEntries(dbName string, p string, entries []*osv.Entry) er if err != nil { return err } - if err := ioutil.WriteFile(filepath.Join(path, "vulns.json"), j, 0666); err != nil { + if err := os.WriteFile(filepath.Join(path, "vulns.json"), j, 0666); err != nil { return err } return nil diff --git a/gopls/internal/govulncheck/cache_test.go b/gopls/internal/govulncheck/cache_test.go index 5a25c781020..57e87659046 100644 --- a/gopls/internal/govulncheck/cache_test.go +++ b/gopls/internal/govulncheck/cache_test.go @@ -93,7 +93,7 @@ func TestConcurrency(t *testing.T) { i := i g.Go(func() error { id := i % 5 - p := fmt.Sprintf("package%d", id) + p := fmt.Sprintf("example.com/package%d", id) entries, err := cache.ReadEntries(dbName, p) if err != nil { @@ -115,7 +115,7 @@ func TestConcurrency(t *testing.T) { // sanity checking for i := 0; i < 5; i++ { id := fmt.Sprint(i) - p := fmt.Sprintf("package%s", id) + p := fmt.Sprintf("example.com/package%s", id) es, err := cache.ReadEntries(dbName, p) if err != nil { diff --git a/gopls/internal/govulncheck/copy.sh b/gopls/internal/govulncheck/copy.sh index 24ed45bfe5a..398cde2f466 100755 --- a/gopls/internal/govulncheck/copy.sh +++ b/gopls/internal/govulncheck/copy.sh @@ -11,3 +11,22 @@ set -o pipefail rm -f *.go cp ../../../../vuln/cmd/govulncheck/internal/govulncheck/*.go . + +sed -i '' 's/\"golang.org\/x\/vuln\/internal\/semver\"/\"golang.org\/x\/tools\/gopls\/internal\/govulncheck\/semver\"/g' *.go +sed -i '' -e '4 i\ +' -e '4 i\ +//go:build go1.18' -e '4 i\ +// +build go1.18' *.go + +# Copy golang.org/x/vuln/internal/semver that +# golang.org/x/vuln/cmd/govulncheck/internal/govulncheck +# depends on. + +mkdir -p semver +cd semver +rm -f *.go +cp ../../../../../vuln/internal/semver/*.go . +sed -i '' -e '4 i\ +' -e '4 i\ +//go:build go1.18' -e '4 i\ +// +build go1.18' *.go diff --git a/gopls/internal/govulncheck/filepath.go b/gopls/internal/govulncheck/filepath.go new file mode 100644 index 00000000000..cef78f5a47c --- /dev/null +++ b/gopls/internal/govulncheck/filepath.go @@ -0,0 +1,38 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package govulncheck + +import ( + "path/filepath" + "strings" +) + +// AbsRelShorter takes path and returns its path relative +// to the current directory, if shorter. Returns path +// when path is an empty string or upon any error. +func AbsRelShorter(path string) string { + if path == "" { + return "" + } + + c, err := filepath.Abs(".") + if err != nil { + return path + } + r, err := filepath.Rel(c, path) + if err != nil { + return path + } + + rSegments := strings.Split(r, string(filepath.Separator)) + pathSegments := strings.Split(path, string(filepath.Separator)) + if len(rSegments) < len(pathSegments) { + return r + } + return path +} diff --git a/gopls/internal/govulncheck/filepath_test.go b/gopls/internal/govulncheck/filepath_test.go new file mode 100644 index 00000000000..06ef40a1239 --- /dev/null +++ b/gopls/internal/govulncheck/filepath_test.go @@ -0,0 +1,42 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package govulncheck + +import ( + "os" + "path/filepath" + "testing" +) + +func TestAbsRelShorter(t *testing.T) { + thisFile := "filepath_test.go" + thisFileAbs, _ := filepath.Abs(thisFile) + + tf, err := os.CreateTemp("", "filepath_test.gp") + if err != nil { + t.Errorf("could not create temporary filepath_test.go file: %v", err) + } + tempFile := tf.Name() + tempFileAbs, _ := filepath.Abs(tempFile) + + for _, test := range []struct { + l string + want string + }{ + {thisFile, "filepath_test.go"}, + {thisFileAbs, "filepath_test.go"}, + // Relative path to temp file from "." is longer as + // it needs to go back the length of the absolute + // path and then in addition go to os.TempDir. + {tempFile, tempFileAbs}, + } { + if got := AbsRelShorter(test.l); got != test.want { + t.Errorf("want %s; got %s", test.want, got) + } + } +} diff --git a/gopls/internal/govulncheck/semver/semver.go b/gopls/internal/govulncheck/semver/semver.go new file mode 100644 index 00000000000..8b1cfe55ea2 --- /dev/null +++ b/gopls/internal/govulncheck/semver/semver.go @@ -0,0 +1,84 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +// Package semver provides shared utilities for manipulating +// Go semantic versions. +package semver + +import ( + "regexp" + "strings" +) + +// addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed +// with 'v' or 'go'. This allows us to easily test go-style SEMVER +// strings against normal SEMVER strings. +func addSemverPrefix(s string) string { + if !strings.HasPrefix(s, "v") && !strings.HasPrefix(s, "go") { + return "v" + s + } + return s +} + +// removeSemverPrefix removes the 'v' or 'go' prefixes from go-style +// SEMVER strings, for usage in the public vulnerability format. +func removeSemverPrefix(s string) string { + s = strings.TrimPrefix(s, "v") + s = strings.TrimPrefix(s, "go") + return s +} + +// CanonicalizeSemverPrefix turns a SEMVER string into the canonical +// representation using the 'v' prefix, as used by the OSV format. +// Input may be a bare SEMVER ("1.2.3"), Go prefixed SEMVER ("go1.2.3"), +// or already canonical SEMVER ("v1.2.3"). +func CanonicalizeSemverPrefix(s string) string { + return addSemverPrefix(removeSemverPrefix(s)) +} + +var ( + // Regexp for matching go tags. The groups are: + // 1 the major.minor version + // 2 the patch version, or empty if none + // 3 the entire prerelease, if present + // 4 the prerelease type ("beta" or "rc") + // 5 the prerelease number + tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc|-pre)(\d+))?$`) +) + +// This is a modified copy of pkgsite/internal/stdlib:VersionForTag. +func GoTagToSemver(tag string) string { + if tag == "" { + return "" + } + + tag = strings.Fields(tag)[0] + // Special cases for go1. + if tag == "go1" { + return "v1.0.0" + } + if tag == "go1.0" { + return "" + } + m := tagRegexp.FindStringSubmatch(tag) + if m == nil { + return "" + } + version := "v" + m[1] + if m[2] != "" { + version += m[2] + } else { + version += ".0" + } + if m[3] != "" { + if !strings.HasPrefix(m[4], "-") { + version += "-" + } + version += m[4] + "." + m[5] + } + return version +} diff --git a/gopls/internal/govulncheck/semver/semver_test.go b/gopls/internal/govulncheck/semver/semver_test.go new file mode 100644 index 00000000000..56b6ea89999 --- /dev/null +++ b/gopls/internal/govulncheck/semver/semver_test.go @@ -0,0 +1,43 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package semver + +import ( + "testing" +) + +func TestCanonicalize(t *testing.T) { + for _, test := range []struct { + v string + want string + }{ + {"v1.2.3", "v1.2.3"}, + {"1.2.3", "v1.2.3"}, + {"go1.2.3", "v1.2.3"}, + } { + got := CanonicalizeSemverPrefix(test.v) + if got != test.want { + t.Errorf("want %s; got %s", test.want, got) + } + } +} + +func TestGoTagToSemver(t *testing.T) { + for _, test := range []struct { + v string + want string + }{ + {"go1.19", "v1.19.0"}, + {"go1.20-pre4", "v1.20.0-pre.4"}, + } { + got := GoTagToSemver(test.v) + if got != test.want { + t.Errorf("want %s; got %s", test.want, got) + } + } +} diff --git a/gopls/internal/govulncheck/source.go b/gopls/internal/govulncheck/source.go index d51fe8c0c2d..d3f519d86ed 100644 --- a/gopls/internal/govulncheck/source.go +++ b/gopls/internal/govulncheck/source.go @@ -25,7 +25,7 @@ func (e *PackageError) Error() string { var b strings.Builder fmt.Fprintln(&b, "Packages contain errors:") for _, e := range e.Errors { - fmt.Println(&b, e) + fmt.Fprintln(&b, e) } return b.String() } diff --git a/gopls/internal/govulncheck/util.go b/gopls/internal/govulncheck/util.go index baa2d961329..fc63d5678ad 100644 --- a/gopls/internal/govulncheck/util.go +++ b/gopls/internal/govulncheck/util.go @@ -12,6 +12,7 @@ import ( "strings" "golang.org/x/mod/semver" + isem "golang.org/x/tools/gopls/internal/govulncheck/semver" "golang.org/x/vuln/osv" "golang.org/x/vuln/vulncheck" ) @@ -24,7 +25,8 @@ func LatestFixed(as []osv.Affected) string { for _, r := range a.Ranges { if r.Type == osv.TypeSemver { for _, e := range r.Events { - if e.Fixed != "" && (v == "" || semver.Compare(e.Fixed, v) > 0) { + if e.Fixed != "" && (v == "" || + semver.Compare(isem.CanonicalizeSemverPrefix(e.Fixed), isem.CanonicalizeSemverPrefix(v)) > 0) { v = e.Fixed } } @@ -60,12 +62,16 @@ func SummarizeCallStack(cs vulncheck.CallStack, topPkgs map[string]bool, vulnPkg } iVuln += iTop + 1 // adjust for slice in call to highest. topName := FuncName(cs[iTop].Function) + topPos := AbsRelShorter(FuncPos(cs[iTop].Call)) + if topPos != "" { + topPos += ": " + } vulnName := FuncName(cs[iVuln].Function) if iVuln == iTop+1 { - return fmt.Sprintf("%s calls %s", topName, vulnName) + return fmt.Sprintf("%s%s calls %s", topPos, topName, vulnName) } - return fmt.Sprintf("%s calls %s, which eventually calls %s", - topName, FuncName(cs[iTop+1].Function), vulnName) + return fmt.Sprintf("%s%s calls %s, which eventually calls %s", + topPos, topName, FuncName(cs[iTop+1].Function), vulnName) } // highest returns the highest (one with the smallest index) entry in the call @@ -107,3 +113,11 @@ func PkgPath(fn *vulncheck.FuncNode) string { func FuncName(fn *vulncheck.FuncNode) string { return strings.TrimPrefix(fn.String(), "*") } + +// FuncPos returns the function position from call. +func FuncPos(call *vulncheck.CallSite) string { + if call != nil && call.Pos != nil { + return call.Pos.String() + } + return "" +} diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json index e0a279129dd..7af60bb6fb5 100644 --- a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json +++ b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json @@ -1 +1 @@ -[{"id":"GO-2020-0012","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2020-9283"],"details":"An attacker can craft an ssh-ed25519 or sk-ssh-ed25519@openssh.com public\nkey, such that the library will panic when trying to verify a signature\nwith it. If verifying signatures using user supplied public keys, this\nmay be used as a denial of service vector.\n","affected":[{"package":{"name":"golang.org/x/crypto/ssh","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20200220183623-bac4c82f6975"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0012"},"ecosystem_specific":{"symbols":["parseED25519","ed25519PublicKey.Verify","parseSKEd25519","skEd25519PublicKey.Verify","NewPublicKey"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/crypto/+/220357"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/bac4c82f69751a6dd76e702d54b3ceb88adab236"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/3L45YRc91SY"}]},{"id":"GO-2020-0013","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2017-3204"],"details":"By default host key verification is disabled which allows for\nman-in-the-middle attacks against SSH clients if\nClientConfig.HostKeyCallback is not set.\n","affected":[{"package":{"name":"golang.org/x/crypto/ssh","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20170330155735-e4e2799dd7aa"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0013"},"ecosystem_specific":{"symbols":["NewClientConn"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/38701"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/e4e2799dd7aab89f583e1d898300d96367750991"},{"type":"WEB","url":"https://go.dev/issue/19767"},{"type":"WEB","url":"https://bridge.grumpy-troll.org/2017/04/golang-ssh-security/"}]},{"id":"GO-2021-0227","published":"2022-02-17T17:35:32Z","modified":"2022-02-17T17:35:32Z","aliases":["CVE-2020-29652"],"details":"Clients can cause a panic in SSH servers. An attacker can craft\nan authentication request message for the “gssapi-with-mic” method\nwhich will cause NewServerConn to panic via a nil pointer dereference\nif ServerConfig.GSSAPIWithMICConfig is nil.\n","affected":[{"package":{"name":"golang.org/x/crypto/ssh","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20201216223049-8b5274cf687f"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0227"},"ecosystem_specific":{"symbols":["connection.serverAuthenticate"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/crypto/+/278852"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/8b5274cf687fd9316b4108863654cc57385531e8"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/ouZIlBimOsE?pli=1"}]}] \ No newline at end of file +[{"id":"GO-2020-0012","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2020-9283","GHSA-ffhg-7mh4-33c4"],"details":"An attacker can craft an ssh-ed25519 or sk-ssh-ed25519@openssh.com public\nkey, such that the library will panic when trying to verify a signature\nwith it. If verifying signatures using user supplied public keys, this\nmay be used as a denial of service vector.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20200220183623-bac4c82f6975"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0012"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/ssh","symbols":["NewPublicKey","ed25519PublicKey.Verify","parseED25519","parseSKEd25519","skEd25519PublicKey.Verify"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/220357"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/bac4c82f69751a6dd76e702d54b3ceb88adab236"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/3L45YRc91SY"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2020-9283"},{"type":"WEB","url":"https://github.com/advisories/GHSA-ffhg-7mh4-33c4"}]},{"id":"GO-2020-0013","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2017-3204"],"details":"By default host key verification is disabled which allows for\nman-in-the-middle attacks against SSH clients if\nClientConfig.HostKeyCallback is not set.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20170330155735-e4e2799dd7aa"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0013"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/ssh","symbols":["NewClientConn"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/340830"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/e4e2799dd7aab89f583e1d898300d96367750991"},{"type":"WEB","url":"https://go.dev/issue/19767"},{"type":"WEB","url":"https://bridge.grumpy-troll.org/2017/04/golang-ssh-security/"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2017-3204"}]},{"id":"GO-2021-0227","published":"2022-02-17T17:35:32Z","modified":"2022-02-17T17:35:32Z","aliases":["CVE-2020-29652"],"details":"Clients can cause a panic in SSH servers. An attacker can craft\nan authentication request message for the “gssapi-with-mic” method\nwhich will cause NewServerConn to panic via a nil pointer dereference\nif ServerConfig.GSSAPIWithMICConfig is nil.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20201216223049-8b5274cf687f"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0227"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/ssh","symbols":["connection.serverAuthenticate"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/278852"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/8b5274cf687fd9316b4108863654cc57385531e8"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/ouZIlBimOsE?pli=1"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2020-29652"}]},{"id":"GO-2021-0356","published":"2022-04-25T20:38:40Z","modified":"2022-08-18T20:22:13Z","aliases":["CVE-2022-27191","GHSA-8c26-wmh5-6g9v"],"details":"Attackers can cause a crash in SSH servers when the server has been\nconfigured by passing a Signer to ServerConfig.AddHostKey such that\n 1) the Signer passed to AddHostKey does not implement AlgorithmSigner, and\n 2) the Signer passed to AddHostKey returns a key of type “ssh-rsa” from its\n PublicKey method.\n\nServers that only use Signer implementations provided by the ssh package are\nunaffected.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20220314234659-1baeb1ce4c0b"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0356"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/ssh","symbols":["ServerConfig.AddHostKey","ServerConfig.AddHostKey"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/392355"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/1baeb1ce4c0b006eff0f294c47cb7617598dfb3d"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/-cp44ypCT5s"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2022-27191"},{"type":"WEB","url":"https://github.com/advisories/GHSA-8c26-wmh5-6g9v"}]},{"id":"GO-2022-0209","published":"2022-07-01T20:15:25Z","modified":"2022-08-18T20:22:13Z","aliases":["CVE-2019-11840"],"details":"XORKeyStream generates incorrect and insecure output for very\nlarge inputs.\n\nIf more than 256 GiB of keystream is generated, or if the counter\notherwise grows greater than 32 bits, the amd64 implementation will\nfirst generate incorrect output, and then cycle back to previously\ngenerated keystream. Repeated keystream bytes can lead to loss of\nconfidentiality in encryption applications, or to predictability\nin CSPRNG applications.\n\nThe issue might affect uses of golang.org/x/crypto/nacl with extremely\nlarge messages.\n\nArchitectures other than amd64 and uses that generate less than 256 GiB\nof keystream for a single salsa20.XORKeyStream invocation are unaffected.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20190320223903-b7391e95e576"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2022-0209"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/salsa20/salsa","goarch":["amd64"],"symbols":["XORKeyStream"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/168406"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/b7391e95e576cacdcdd422573063bc057239113d"},{"type":"WEB","url":"https://go.dev/issue/30965"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/tjyNcJxb2vQ/m/n0NRBziSCAAJ"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2019-11840"}]},{"id":"GO-2022-0229","published":"2022-07-06T18:23:48Z","modified":"2022-08-18T20:22:13Z","aliases":["CVE-2020-7919","GHSA-cjjc-xp8v-855w"],"details":"On 32-bit architectures, a malformed input to crypto/x509 or\nthe ASN.1 parsing functions of golang.org/x/crypto/cryptobyte\ncan lead to a panic.\n\nThe malformed certificate can be delivered via a crypto/tls\nconnection to a client, or to a server that accepts client\ncertificates. net/http clients can be made to crash by an HTTPS\nserver, while net/http servers that accept client certificates\nwill recover the panic and are unaffected.\n","affected":[{"package":{"name":"stdlib","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.12.16"},{"introduced":"1.13.0"},{"fixed":"1.13.7"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2022-0229"},"ecosystem_specific":{"imports":[{"path":"crypto/x509"}]}},{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20200124225646-8b5121be2f68"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2022-0229"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/cryptobyte"}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/216680"},{"type":"FIX","url":"https://go.googlesource.com/go/+/b13ce14c4a6aa59b7b041ad2b6eed2d23e15b574"},{"type":"WEB","url":"https://go.dev/cl/216677"},{"type":"WEB","url":"https://go.dev/issue/36837"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/Hsw4mHYc470"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2020-7919"},{"type":"WEB","url":"https://github.com/advisories/GHSA-cjjc-xp8v-855w"}]}] \ No newline at end of file diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json index eee052ffcd3..e9d55a39c9f 100644 --- a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json +++ b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json @@ -1 +1 @@ -[{"id":"GO-2020-0015","published":"2021-04-14T20:04:52Z","modified":"2021-06-07T12:00:00Z","aliases":["CVE-2020-14040"],"details":"An attacker could provide a single byte to a UTF16 decoder instantiated with\nUseBOM or ExpectBOM to trigger an infinite loop if the String function on\nthe Decoder is called, or the Decoder is passed to transform.String.\nIf used to parse user supplied input, this may be used as a denial of service\nvector.\n","affected":[{"package":{"name":"golang.org/x/text/encoding/unicode","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.3"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0015"},"ecosystem_specific":{"symbols":["utf16Decoder.Transform","bomOverride.Transform"]}},{"package":{"name":"golang.org/x/text/transform","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.3"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0015"},"ecosystem_specific":{"symbols":["Transform"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/text/+/238238"},{"type":"FIX","url":"https://go.googlesource.com/text/+/23ae387dee1f90d29a23c0e87ee0b46038fbed0e"},{"type":"WEB","url":"https://go.dev/issue/39491"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/bXVeAmGOqz0"}]},{"id":"GO-2021-0113","published":"2021-10-06T17:51:21Z","modified":"2021-10-06T17:51:21Z","aliases":["CVE-2021-38561"],"details":"Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n","affected":[{"package":{"name":"golang.org/x/text/language","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.7"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0113"},"ecosystem_specific":{"symbols":["Parse","MatchStrings","MustParse","ParseAcceptLanguage"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/text/+/340830"},{"type":"FIX","url":"https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f"}]}] \ No newline at end of file +[{"id":"GO-2020-0015","published":"2021-04-14T20:04:52Z","modified":"2021-06-07T12:00:00Z","aliases":["CVE-2020-14040","GHSA-5rcv-m4m3-hfh7"],"details":"An attacker could provide a single byte to a UTF16 decoder instantiated with\nUseBOM or ExpectBOM to trigger an infinite loop if the String function on\nthe Decoder is called, or the Decoder is passed to transform.String.\nIf used to parse user supplied input, this may be used as a denial of service\nvector.\n","affected":[{"package":{"name":"golang.org/x/text","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.3"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0015"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/text/encoding/unicode","symbols":["bomOverride.Transform","utf16Decoder.Transform"]},{"path":"golang.org/x/text/transform","symbols":["Transform"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/238238"},{"type":"FIX","url":"https://go.googlesource.com/text/+/23ae387dee1f90d29a23c0e87ee0b46038fbed0e"},{"type":"WEB","url":"https://go.dev/issue/39491"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/bXVeAmGOqz0"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2020-14040"},{"type":"WEB","url":"https://github.com/advisories/GHSA-5rcv-m4m3-hfh7"}]},{"id":"GO-2021-0113","published":"2021-10-06T17:51:21Z","modified":"2021-10-06T17:51:21Z","aliases":["CVE-2021-38561"],"details":"Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n","affected":[{"package":{"name":"golang.org/x/text","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.7"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0113"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/text/language","symbols":["MatchStrings","MustParse","Parse","ParseAcceptLanguage"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/340830"},{"type":"FIX","url":"https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2021-38561"}]}] \ No newline at end of file diff --git a/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json b/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json index 7cbfafcd103..756727b1a7d 100644 --- a/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json +++ b/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json @@ -1 +1 @@ -[{"id":"GO-0000-001","affected":[{"package":{"name":"archive/zip"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.18.0"}]}],"ecosystem_specific":{"symbols":["OpenReader"]}}]}] +[{"id":"STD","affected":[{"package":{"name":"stdlib"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.18.0"}]}],"ecosystem_specific":{"imports":[{"path":"archive/zip","symbols":["OpenReader"]}]}}]}] diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 91fef3f88bc..41d0375fd25 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -61,7 +61,7 @@ import ( ) func main() { - _, err := zip.OpenReader("file.zip") // vulnerability GO-0000-001 + _, err := zip.OpenReader("file.zip") // vulnerability id: STD fmt.Println(err) } ` @@ -111,7 +111,7 @@ func main() { env.Await( CompletedWork("govulncheck", 1, true), // TODO(hyangah): once the diagnostics are published, wait for diagnostics. - ShownMessage("Found GO-0000-001"), + ShownMessage("Found STD"), ) }) } diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 60d582ca318..b9aba157068 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -175,7 +175,10 @@ func toVulns(ci *gvc.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, for _, v := range vg { if css := ci.CallStacks[v]; len(css) > 0 { vuln.CallStacks = append(vuln.CallStacks, toCallStack(css[0])) - vuln.CallStackSummaries = append(vuln.CallStackSummaries, gvc.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath)) + // TODO(hyangah): https://go-review.googlesource.com/c/vuln/+/425183 added position info + // in the summary but we don't need the info. Allow SummarizeCallStack to skip it optionally. + sum := trimPosPrefix(gvc.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath)) + vuln.CallStackSummaries = append(vuln.CallStackSummaries, sum) } } vulns = append(vulns, vuln) @@ -196,3 +199,11 @@ func toVulns(ci *gvc.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, } return vulns, nil } + +func trimPosPrefix(summary string) string { + _, after, found := strings.Cut(summary, ": ") + if !found { + return summary + } + return after +} diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index 71eaf4a580f..1ce1fae7d20 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -14,6 +14,7 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -42,6 +43,13 @@ func TestCmd_Run(t *testing.T) { for _, v := range result { got = append(got, toReport(v)) } + // drop the workspace root directory path included in the summary. + cwd := cfg.Dir + for _, g := range got { + for i, summary := range g.CallStackSummaries { + g.CallStackSummaries[i] = strings.ReplaceAll(summary, cwd, ".") + } + } var want = []report{ { @@ -232,9 +240,13 @@ var testClient1 = &mockClient{ }, }, Affected: []osv.Affected{{ - Package: osv.Package{Name: "golang.org/amod/avuln"}, - Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.0.4"}, {Introduced: "1.1.2"}}}}, - EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"VulnData.Vuln1", "VulnData.Vuln2"}}, + Package: osv.Package{Name: "golang.org/amod"}, + Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.0.4"}, {Introduced: "1.1.2"}}}}, + EcosystemSpecific: osv.EcosystemSpecific{ + Imports: []osv.EcosystemSpecificImport{{ + Path: "golang.org/amod/avuln", + Symbols: []string{"VulnData.Vuln1", "VulnData.Vuln2"}}}, + }, }}, }, { @@ -247,9 +259,13 @@ var testClient1 = &mockClient{ }, }, Affected: []osv.Affected{{ - Package: osv.Package{Name: "golang.org/amod/avuln"}, - Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.0.4"}, {Introduced: "1.1.2"}}}}, - EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"nonExisting"}}, + Package: osv.Package{Name: "golang.org/amod"}, + Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.0.4"}, {Introduced: "1.1.2"}}}}, + EcosystemSpecific: osv.EcosystemSpecific{ + Imports: []osv.EcosystemSpecificImport{{ + Path: "golang.org/amod/avuln", + Symbols: []string{"nonExisting"}}}, + }, }}, }, }, @@ -257,9 +273,13 @@ var testClient1 = &mockClient{ { ID: "GO-2022-02", Affected: []osv.Affected{{ - Package: osv.Package{Name: "golang.org/bmod/bvuln"}, - Ranges: osv.Affects{{Type: osv.TypeSemver}}, - EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"Vuln"}}, + Package: osv.Package{Name: "golang.org/bmod"}, + Ranges: osv.Affects{{Type: osv.TypeSemver}}, + EcosystemSpecific: osv.EcosystemSpecific{ + Imports: []osv.EcosystemSpecificImport{{ + Path: "golang.org/bmod/bvuln", + Symbols: []string{"Vuln"}}}, + }, }}, }, }, From 5ba85415fa1b4a88c216d791d246867e565e1edd Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Fri, 2 Sep 2022 10:49:32 -0700 Subject: [PATCH 221/723] x/tools/internal/lsp/source: disable some tests for std lib changes CL 410955 changes printing of unions which breaks a few lsp tests. Disable them for now so we can submit CL 410955. For golang/go#53279. For golang/go#54822. Change-Id: I54ff99a4f5530181a39557b6b62e776af082c28d Reviewed-on: https://go-review.googlesource.com/c/tools/+/428054 Auto-Submit: Robert Griesemer Reviewed-by: Robert Griesemer Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Griesemer --- internal/lsp/testdata/godef/hover_generics/hover.go | 6 ++++-- internal/lsp/testdata/summary_go1.18.txt.golden | 4 ++-- internal/lsp/testdata/typeparams/type_params.go | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/lsp/testdata/godef/hover_generics/hover.go b/internal/lsp/testdata/godef/hover_generics/hover.go index 7400e1acdd8..a26980a5e15 100644 --- a/internal/lsp/testdata/godef/hover_generics/hover.go +++ b/internal/lsp/testdata/godef/hover_generics/hover.go @@ -10,6 +10,8 @@ type Value[T any] struct { //@mark(ValueTdecl, "T"),hoverdef("T",ValueTdecl) Q int //@mark(ValueQfield, "Q"),hoverdef("Q", ValueQfield) } -func F[P interface{ ~int | string }]() { //@mark(Pparam, "P"),hoverdef("P",Pparam) - var _ P //@mark(Pvar, "P"),hoverdef("P",Pvar) +// disabled - see issue #54822 +func F[P interface{ ~int | string }]() { // mark(Pparam, "P"),hoverdef("P",Pparam) + // disabled - see issue #54822 + var _ P // mark(Pvar, "P"),hoverdef("P",Pvar) } diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/internal/lsp/testdata/summary_go1.18.txt.golden index 6bfb1ef3e1d..fd2fb666ff9 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/internal/lsp/testdata/summary_go1.18.txt.golden @@ -2,7 +2,7 @@ CallHierarchyCount = 2 CodeLensCount = 5 CompletionsCount = 263 -CompletionSnippetCount = 116 +CompletionSnippetCount = 115 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 @@ -16,7 +16,7 @@ SemanticTokenCount = 3 SuggestedFixCount = 67 FunctionExtractionCount = 25 MethodExtractionCount = 6 -DefinitionsCount = 112 +DefinitionsCount = 110 TypeDefinitionsCount = 18 HighlightsCount = 69 InlayHintsCount = 5 diff --git a/internal/lsp/testdata/typeparams/type_params.go b/internal/lsp/testdata/typeparams/type_params.go index 715726b1a41..21fc7049f5b 100644 --- a/internal/lsp/testdata/typeparams/type_params.go +++ b/internal/lsp/testdata/typeparams/type_params.go @@ -42,7 +42,8 @@ func returnTP[A int | float64](a A) A { //@item(returnTP, "returnTP", "something } func _() { - var _ int = returnTP //@snippet(" //", returnTP, "returnTP[${1:}](${2:})", "returnTP[${1:A int|float64}](${2:a A})") + // disabled - see issue #54822 + var _ int = returnTP // snippet(" //", returnTP, "returnTP[${1:}](${2:})", "returnTP[${1:A int|float64}](${2:a A})") var aa int //@item(tpInt, "aa", "int", "var") var ab float64 //@item(tpFloat, "ab", "float64", "var") From be9eab19b45daaaedbaf091e593c07898bc8a4d2 Mon Sep 17 00:00:00 2001 From: Abirdcfly Date: Thu, 1 Sep 2022 01:49:25 +0000 Subject: [PATCH 222/723] go/analysis/passes/inspect: fix example return Change-Id: I9e248c45078d36ded9c6008e2b7e2c8bf8586df1 GitHub-Last-Rev: 33900677c6118060af36c6ec16db5b937912ae61 GitHub-Pull-Request: golang/tools#390 Reviewed-on: https://go-review.googlesource.com/c/tools/+/425204 TryBot-Result: Gopher Robot Run-TryBot: Tim King gopls-CI: kokoro Reviewed-by: Tim King Reviewed-by: Heschi Kreinick --- go/analysis/passes/inspect/inspect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/analysis/passes/inspect/inspect.go b/go/analysis/passes/inspect/inspect.go index c1c1127d089..165c70cbd36 100644 --- a/go/analysis/passes/inspect/inspect.go +++ b/go/analysis/passes/inspect/inspect.go @@ -24,7 +24,7 @@ // inspect.Preorder(nil, func(n ast.Node) { // ... // }) -// return nil +// return nil, nil // } package inspect From 6a585a2bf955b81402c7fc0691e88f9c8bfdf898 Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Fri, 2 Sep 2022 11:41:53 -0700 Subject: [PATCH 223/723] x/tools/internal/typeparams: use regexp to match wanted result CL 410955 changes printing of unions which breaks two typeparams tests. Use regexp matching and adjust the wanted results accordingly. For golang/go#53279. For golang/go#54822. Change-Id: I7060df47d36ce3069570237dafff024aaad637a5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/428055 Auto-Submit: Robert Griesemer Reviewed-by: Robert Griesemer TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Robert Griesemer --- internal/typeparams/normalize_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/typeparams/normalize_test.go b/internal/typeparams/normalize_test.go index 5969eee3de0..769433d701d 100644 --- a/internal/typeparams/normalize_test.go +++ b/internal/typeparams/normalize_test.go @@ -9,6 +9,7 @@ import ( "go/parser" "go/token" "go/types" + "regexp" "strings" "testing" @@ -38,7 +39,7 @@ func TestStructuralTerms(t *testing.T) { {"package emptyintersection; type T[P interface{ ~int; string }] int", "", "empty type set"}, {"package embedded0; type T[P interface{ I }] int; type I interface { int }", "int", ""}, - {"package embedded1; type T[P interface{ I | string }] int; type I interface{ int | ~string }", "int|~string", ""}, + {"package embedded1; type T[P interface{ I | string }] int; type I interface{ int | ~string }", "int ?\\| ?~string", ""}, {"package embedded2; type T[P interface{ I; string }] int; type I interface{ int | ~string }", "string", ""}, {"package named; type T[P C] int; type C interface{ ~int|int }", "~int", ""}, @@ -52,7 +53,7 @@ type B interface{ int|string } type C interface { ~string|~int } type T[P interface{ A|B; C }] int -`, "~string|int", ""}, +`, "~string ?\\| ?int", ""}, } for _, test := range tests { @@ -96,7 +97,8 @@ type T[P interface{ A|B; C }] int qf := types.RelativeTo(pkg) got = types.TypeString(NewUnion(terms), qf) } - if got != test.want { + want := regexp.MustCompile(test.want) + if !want.MatchString(got) { t.Errorf("StructuralTerms(%s) = %q, want %q", T, got, test.want) } }) From d815cba58228ee3879cfe21cc099ac35ac8b1682 Mon Sep 17 00:00:00 2001 From: Dylan Le Date: Thu, 1 Sep 2022 14:29:52 -0400 Subject: [PATCH 224/723] internal/lsp: update LSP protocol for WorkspaceEdit Change DocumentChanges field type from []TextDocumentEdit to []DocumentChanges. DocumentChanges is a union type of TextDocumentEdit and RenameFile used for renaming package feature. It distinguishes a renaming directory ops from a text document edit ops. For golang/go#41567 Change-Id: I25d106b34821effc53b712800f7175248e59ee25 Reviewed-on: https://go-review.googlesource.com/c/tools/+/427538 gopls-CI: kokoro Run-TryBot: Dylan Le Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- internal/lsp/cmd/imports.go | 6 ++- internal/lsp/cmd/rename.go | 9 +++-- internal/lsp/cmd/suggested_fix.go | 18 ++++++--- internal/lsp/code_action.go | 32 +++++++++------- internal/lsp/command.go | 34 ++++++++++++----- internal/lsp/fake/client.go | 10 ++++- internal/lsp/fake/editor.go | 24 +++++++----- internal/lsp/lsp_test.go | 34 +++++++++-------- internal/lsp/protocol/tsdocument_changes.go | 41 +++++++++++++++++++++ internal/lsp/protocol/tsprotocol.go | 2 +- internal/lsp/rename.go | 2 +- 11 files changed, 148 insertions(+), 64 deletions(-) create mode 100644 internal/lsp/protocol/tsdocument_changes.go diff --git a/internal/lsp/cmd/imports.go b/internal/lsp/cmd/imports.go index 49778603d23..8f12a94ce47 100644 --- a/internal/lsp/cmd/imports.go +++ b/internal/lsp/cmd/imports.go @@ -74,8 +74,10 @@ func (t *imports) Run(ctx context.Context, args ...string) error { continue } for _, c := range a.Edit.DocumentChanges { - if fileURI(c.TextDocument.URI) == uri { - edits = append(edits, c.Edits...) + if c.TextDocumentEdit != nil { + if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { + edits = append(edits, c.TextDocumentEdit.Edits...) + } } } } diff --git a/internal/lsp/cmd/rename.go b/internal/lsp/cmd/rename.go index 9411275949f..be7fd09a302 100644 --- a/internal/lsp/cmd/rename.go +++ b/internal/lsp/cmd/rename.go @@ -81,9 +81,12 @@ func (r *rename) Run(ctx context.Context, args ...string) error { var orderedURIs []string edits := map[span.URI][]protocol.TextEdit{} for _, c := range edit.DocumentChanges { - uri := fileURI(c.TextDocument.URI) - edits[uri] = append(edits[uri], c.Edits...) - orderedURIs = append(orderedURIs, string(uri)) + // Todo: Add handler for RenameFile edits + if c.TextDocumentEdit != nil { + uri := fileURI(c.TextDocumentEdit.TextDocument.URI) + edits[uri] = append(edits[uri], c.TextDocumentEdit.Edits...) + orderedURIs = append(orderedURIs, string(uri)) + } } sort.Strings(orderedURIs) changeCount := len(orderedURIs) diff --git a/internal/lsp/cmd/suggested_fix.go b/internal/lsp/cmd/suggested_fix.go index c6f26e2d685..a3e51325d70 100644 --- a/internal/lsp/cmd/suggested_fix.go +++ b/internal/lsp/cmd/suggested_fix.go @@ -103,8 +103,10 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { } if !from.HasPosition() { for _, c := range a.Edit.DocumentChanges { - if fileURI(c.TextDocument.URI) == uri { - edits = append(edits, c.Edits...) + if c.TextDocumentEdit != nil { + if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { + edits = append(edits, c.TextDocumentEdit.Edits...) + } } } continue @@ -118,8 +120,10 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { } if span.ComparePoint(from.Start(), spn.Start()) == 0 { for _, c := range a.Edit.DocumentChanges { - if fileURI(c.TextDocument.URI) == uri { - edits = append(edits, c.Edits...) + if c.TextDocumentEdit != nil { + if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { + edits = append(edits, c.TextDocumentEdit.Edits...) + } } } break @@ -129,8 +133,10 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { // If suggested fix is not a diagnostic, still must collect edits. if len(a.Diagnostics) == 0 { for _, c := range a.Edit.DocumentChanges { - if fileURI(c.TextDocument.URI) == uri { - edits = append(edits, c.Edits...) + if c.TextDocumentEdit != nil { + if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { + edits = append(edits, c.TextDocumentEdit.Edits...) + } } } } diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go index 4147e17ce6d..450d678f658 100644 --- a/internal/lsp/code_action.go +++ b/internal/lsp/code_action.go @@ -338,16 +338,18 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.P return actions, nil } -func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.TextDocumentEdit { - return []protocol.TextDocumentEdit{ +func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.DocumentChanges { + return []protocol.DocumentChanges{ { - TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ - Version: fh.Version(), - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(fh.URI()), + TextDocumentEdit: &protocol.TextDocumentEdit{ + TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ + Version: fh.Version(), + TextDocumentIdentifier: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(fh.URI()), + }, }, + Edits: edits, }, - Edits: edits, }, } } @@ -378,20 +380,22 @@ func codeActionsMatchingDiagnostics(ctx context.Context, snapshot source.Snapsho func codeActionsForDiagnostic(ctx context.Context, snapshot source.Snapshot, sd *source.Diagnostic, pd *protocol.Diagnostic) ([]protocol.CodeAction, error) { var actions []protocol.CodeAction for _, fix := range sd.SuggestedFixes { - var changes []protocol.TextDocumentEdit + var changes []protocol.DocumentChanges for uri, edits := range fix.Edits { fh, err := snapshot.GetVersionedFile(ctx, uri) if err != nil { return nil, err } - changes = append(changes, protocol.TextDocumentEdit{ - TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ - Version: fh.Version(), - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(uri), + changes = append(changes, protocol.DocumentChanges{ + TextDocumentEdit: &protocol.TextDocumentEdit{ + TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ + Version: fh.Version(), + TextDocumentIdentifier: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(fh.URI()), + }, }, + Edits: edits, }, - Edits: edits, }) } action := protocol.CodeAction{ diff --git a/internal/lsp/command.go b/internal/lsp/command.go index f9f8caede87..06dc2a4cf41 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -144,9 +144,15 @@ func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs if err != nil { return err } + var changes []protocol.DocumentChanges + for _, edit := range edits { + changes = append(changes, protocol.DocumentChanges{ + TextDocumentEdit: &edit, + }) + } r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ Edit: protocol.WorkspaceEdit{ - DocumentChanges: edits, + DocumentChanges: changes, }, }) if err != nil { @@ -322,15 +328,19 @@ func (c *commandHandler) RemoveDependency(ctx context.Context, args command.Remo } response, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ Edit: protocol.WorkspaceEdit{ - DocumentChanges: []protocol.TextDocumentEdit{{ - TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ - Version: deps.fh.Version(), - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(deps.fh.URI()), + DocumentChanges: []protocol.DocumentChanges{ + { + TextDocumentEdit: &protocol.TextDocumentEdit{ + TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ + Version: deps.fh.Version(), + TextDocumentIdentifier: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(deps.fh.URI()), + }, + }, + Edits: edits, }, }, - Edits: edits, - }}, + }, }, }) if err != nil { @@ -544,9 +554,15 @@ func (s *Server) runGoModUpdateCommands(ctx context.Context, snapshot source.Sna if len(changes) == 0 { return nil } + var documentChanges []protocol.DocumentChanges + for _, change := range changes { + documentChanges = append(documentChanges, protocol.DocumentChanges{ + TextDocumentEdit: &change, + }) + } response, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ Edit: protocol.WorkspaceEdit{ - DocumentChanges: changes, + DocumentChanges: documentChanges, }, }) if err != nil { diff --git a/internal/lsp/fake/client.go b/internal/lsp/fake/client.go index bb258f29f50..dd44ed0d5dd 100644 --- a/internal/lsp/fake/client.go +++ b/internal/lsp/fake/client.go @@ -121,8 +121,14 @@ func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceE return &protocol.ApplyWorkspaceEditResult{FailureReason: "Edit.Changes is unsupported"}, nil } for _, change := range params.Edit.DocumentChanges { - if err := c.editor.applyProtocolEdit(ctx, change); err != nil { - return nil, err + // Todo: Add a handler for RenameFile edits + if change.RenameFile != nil { + panic("Fake editor does not support the RenameFile edits.") + } + if change.TextDocumentEdit != nil { + if err := c.editor.applyProtocolEdit(ctx, *change.TextDocumentEdit); err != nil { + return nil, err + } } } return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index db4326011d9..eb721f12216 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -843,14 +843,16 @@ func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol // ApplyCodeAction applies the given code action. func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction) error { for _, change := range action.Edit.DocumentChanges { - path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) - if int32(e.buffers[path].version) != change.TextDocument.Version { - // Skip edits for old versions. - continue - } - edits := convertEdits(change.Edits) - if err := e.EditBuffer(ctx, path, edits); err != nil { - return fmt.Errorf("editing buffer %q: %w", path, err) + if change.TextDocumentEdit != nil { + path := e.sandbox.Workdir.URIToPath(change.TextDocumentEdit.TextDocument.URI) + if int32(e.buffers[path].version) != change.TextDocumentEdit.TextDocument.Version { + // Skip edits for old versions. + continue + } + edits := convertEdits(change.TextDocumentEdit.Edits) + if err := e.EditBuffer(ctx, path, edits); err != nil { + return fmt.Errorf("editing buffer %q: %w", path, err) + } } } // Execute any commands. The specification says that commands are @@ -1155,8 +1157,10 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin return err } for _, change := range wsEdits.DocumentChanges { - if err := e.applyProtocolEdit(ctx, change); err != nil { - return err + if change.TextDocumentEdit != nil { + if err := e.applyProtocolEdit(ctx, *change.TextDocumentEdit); err != nil { + return err + } } } return nil diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 6591251a1c1..e8188fe896a 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -1086,27 +1086,29 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare } } -func applyTextDocumentEdits(r *runner, edits []protocol.TextDocumentEdit) (map[span.URI]string, error) { +func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[span.URI]string, error) { res := map[span.URI]string{} for _, docEdits := range edits { - uri := docEdits.TextDocument.URI.SpanURI() - var m *protocol.ColumnMapper - // If we have already edited this file, we use the edited version (rather than the - // file in its original state) so that we preserve our initial changes. - if content, ok := res[uri]; ok { - m = protocol.NewColumnMapper(uri, []byte(content)) - } else { - var err error - if m, err = r.data.Mapper(uri); err != nil { + if docEdits.TextDocumentEdit != nil { + uri := docEdits.TextDocumentEdit.TextDocument.URI.SpanURI() + var m *protocol.ColumnMapper + // If we have already edited this file, we use the edited version (rather than the + // file in its original state) so that we preserve our initial changes. + if content, ok := res[uri]; ok { + m = protocol.NewColumnMapper(uri, []byte(content)) + } else { + var err error + if m, err = r.data.Mapper(uri); err != nil { + return nil, err + } + } + res[uri] = string(m.Content) + sedits, err := source.FromProtocolEdits(m, docEdits.TextDocumentEdit.Edits) + if err != nil { return nil, err } + res[uri] = applyEdits(res[uri], sedits) } - res[uri] = string(m.Content) - sedits, err := source.FromProtocolEdits(m, docEdits.Edits) - if err != nil { - return nil, err - } - res[uri] = applyEdits(res[uri], sedits) } return res, nil } diff --git a/internal/lsp/protocol/tsdocument_changes.go b/internal/lsp/protocol/tsdocument_changes.go new file mode 100644 index 00000000000..7296a151ac2 --- /dev/null +++ b/internal/lsp/protocol/tsdocument_changes.go @@ -0,0 +1,41 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package protocol + +import ( + "encoding/json" + "fmt" +) + +// DocumentChanges is a union of a file edit and directory rename operations +// for package renaming feature. At most one field of this struct is non-nil. +type DocumentChanges struct { + TextDocumentEdit *TextDocumentEdit + RenameFile *RenameFile +} + +func (d *DocumentChanges) UnmarshalJSON(data []byte) error { + var m map[string]interface{} + + if err := json.Unmarshal(data, &m); err != nil { + return err + } + + if _, ok := m["textDocument"]; ok { + d.TextDocumentEdit = new(TextDocumentEdit) + return json.Unmarshal(data, d.TextDocumentEdit) + } + + d.RenameFile = new(RenameFile) + return json.Unmarshal(data, d.RenameFile) +} + +func (d *DocumentChanges) MarshalJSON() ([]byte, error) { + if d.TextDocumentEdit != nil { + return json.Marshal(d.TextDocumentEdit) + } else if d.RenameFile != nil { + return json.Marshal(d.RenameFile) + } + return nil, fmt.Errorf("Empty DocumentChanges union value") +} diff --git a/internal/lsp/protocol/tsprotocol.go b/internal/lsp/protocol/tsprotocol.go index 3a284bf45a6..72c0372e8a3 100644 --- a/internal/lsp/protocol/tsprotocol.go +++ b/internal/lsp/protocol/tsprotocol.go @@ -5707,7 +5707,7 @@ type WorkspaceEdit struct { * If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then * only plain `TextEdit`s using the `changes` property are supported. */ - DocumentChanges []TextDocumentEdit/*TextDocumentEdit | CreateFile | RenameFile | DeleteFile*/ `json:"documentChanges,omitempty"` + DocumentChanges []DocumentChanges/*TextDocumentEdit | CreateFile | RenameFile | DeleteFile*/ `json:"documentChanges,omitempty"` /** * A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and * delete file / folder operations. diff --git a/internal/lsp/rename.go b/internal/lsp/rename.go index 739ae906b37..e78e2164473 100644 --- a/internal/lsp/rename.go +++ b/internal/lsp/rename.go @@ -22,7 +22,7 @@ func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*pr return nil, err } - var docChanges []protocol.TextDocumentEdit + var docChanges []protocol.DocumentChanges for uri, e := range edits { fh, err := snapshot.GetVersionedFile(ctx, uri) if err != nil { From 83d76192b23f16fb924f90e0c223f8a63295d28c Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Tue, 6 Sep 2022 10:27:50 -0400 Subject: [PATCH 225/723] gopls : add a mention of staticcheck to settings documentation Fixes golang/go#52874 Change-Id: I04664154d68e31f48234c13aefe8470b09f0413e Reviewed-on: https://go-review.googlesource.com/c/tools/+/428595 TryBot-Result: Gopher Robot Run-TryBot: Peter Weinberger Reviewed-by: Robert Findley --- gopls/doc/settings.md | 1 + internal/lsp/source/api_json.go | 2 +- internal/lsp/source/options.go | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index f71bbebdce9..43dd54b074a 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -307,6 +307,7 @@ Default: `{}`. **This setting is experimental and may be deleted.** staticcheck enables additional analyses from staticcheck.io. +These analyses are documented at [here](https://staticcheck.io/docs/checks/). Default: `false`. diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index f6833fe3df8..94e6b1d2a89 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -461,7 +461,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "staticcheck", Type: "bool", - Doc: "staticcheck enables additional analyses from staticcheck.io.\n", + Doc: "staticcheck enables additional analyses from staticcheck.io.\nThese analyses are documented at [here](https://staticcheck.io/docs/checks/).\n", Default: "false", Status: "experimental", Hierarchy: "ui.diagnostic", diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 096f1acf9a4..984f228163b 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -402,6 +402,7 @@ type DiagnosticOptions struct { Analyses map[string]bool // Staticcheck enables additional analyses from staticcheck.io. + // These analyses are documented at [here](https://staticcheck.io/docs/checks/). Staticcheck bool `status:"experimental"` // Annotations specifies the various kinds of optimization diagnostics From c1dd25e80b559a5b0e8e2dd7d5bd1e946aa996a0 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 30 Aug 2022 14:09:52 -0400 Subject: [PATCH 226/723] gopls/internal/migrate.sh: a script to migrate internal/lsp to gopls/ Add a script that does the migration of the internal/lsp directory to gopls/internal/lsp. This is done in a separate CL so that in-progress CLs can rebase on top of *this CL*, run this script, and then rebase to tip. For golang/go#54509 Change-Id: I6f529c1e4ba29b4d88dc26278d54a055f1ef212e Reviewed-on: https://go-review.googlesource.com/c/tools/+/426795 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley --- gopls/internal/migrate.sh | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100755 gopls/internal/migrate.sh diff --git a/gopls/internal/migrate.sh b/gopls/internal/migrate.sh new file mode 100755 index 00000000000..6f2bebc6ad6 --- /dev/null +++ b/gopls/internal/migrate.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# +# Copyright 2022 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +# +# Migrates the internal/lsp directory to gopls/internal/lsp. Run this script +# from the root of x/tools to migrate in-progress CLs. +# +# See golang/go#54509 for more details. This script may be deleted once a +# reasonable amount of time has passed such that all active in-progress CLs +# have been rebased. + +set -eu + +# A portable -i flag. Darwin requires two parameters. +# See https://stackoverflow.com/questions/5694228/sed-in-place-flag-that-works-both-on-mac-bsd-and-linux +# for more details. +sedi=(-i) +case "$(uname)" in + Darwin*) sedi=(-i "") +esac + +# mvpath moves the directory at the relative path $1 to the relative path $2, +# moving files and rewriting import paths. +# +# It uses heuristics to identify import path literals, and therefore may be +# imprecise. +function mvpath() { + # If the source also doesn't exist, it may have already been moved. + # Skip so that this script is idempotent. + if [[ ! -d $1 ]]; then + echo "WARNING: skipping nonexistent source directory $1" + return 0 + fi + + # git can sometimes leave behind empty directories, which can change the + # behavior of the mv command below. + if [[ -d $2 ]] || [[ -f $2 ]]; then + echo "ERROR: destination $2 already exists" + exit 1 + fi + + mv $1 $2 + + local old="golang.org/x/tools/$1" + local new="golang.org/x/tools/$2" + + # Replace instances of the old import path with the new. This is imprecise, + # but we are a bit careful to avoid replacing golang.org/x/tools/foox with + # golang.org/x/tools/barx when moving foo->bar: the occurrence of the import + # path must be followed by whitespace, /, or a closing " or `. + local replace="s:${old}\([[:space:]/\"\`]\):${new}\1:g" + find . -type f \( \ + -name ".git" -prune -o \ + -name "*.go" -o \ + -name "*.in" -o \ + -name "*.golden" -o \ + -name "*.hlp" -o \ + -name "*.md" \) \ + -exec sed "${sedi[@]}" -e $replace {} \; +} + +mvpath internal/lsp/diff internal/diff +mvpath internal/lsp/fuzzy internal/fuzzy +mvpath internal/lsp/debug/tag internal/event/tag +mvpath internal/lsp/bug internal/bug +mvpath internal/lsp gopls/internal/lsp From bac5507b3ee936f1f05531e0de4ded2da57b2d62 Mon Sep 17 00:00:00 2001 From: Abirdcfly Date: Wed, 7 Sep 2022 06:31:42 +0000 Subject: [PATCH 227/723] go/analysis/internal/checker: make applyFixes work from the first character Make sure modifying the first character of the file takes effect. Fixes golang/go#54774 Change-Id: Ib77231b9bd15f35fe50b2c2d6c7ea260c9c3cba5 GitHub-Last-Rev: b58bbdf4c247b22947223b157a1d36ecc856c652 GitHub-Pull-Request: golang/tools#393 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426654 Reviewed-by: Benny Siegert Reviewed-by: Tim King --- go/analysis/internal/checker/checker.go | 2 + go/analysis/internal/checker/start_test.go | 85 ++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 go/analysis/internal/checker/start_test.go diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index b5148ff26db..ca77a764ce8 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -408,6 +408,8 @@ func applyFixes(roots []*action) { if edit.start > cur { out.Write(contents[cur:edit.start]) out.Write(edit.newText) + } else if cur == 0 && edit.start == 0 { // edit starts at first character? + out.Write(edit.newText) } cur = edit.end diff --git a/go/analysis/internal/checker/start_test.go b/go/analysis/internal/checker/start_test.go new file mode 100644 index 00000000000..ede21159bc8 --- /dev/null +++ b/go/analysis/internal/checker/start_test.go @@ -0,0 +1,85 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package checker_test + +import ( + "go/ast" + "io/ioutil" + "path/filepath" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/internal/checker" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/testenv" +) + +// TestStartFixes make sure modifying the first character +// of the file takes effect. +func TestStartFixes(t *testing.T) { + testenv.NeedsGoPackages(t) + + files := map[string]string{ + "comment/doc.go": `/* Package comment */ +package comment +`} + + want := `// Package comment +package comment +` + + testdata, cleanup, err := analysistest.WriteFiles(files) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(testdata, "src/comment/doc.go") + checker.Fix = true + checker.Run([]string{"file=" + path}, []*analysis.Analyzer{commentAnalyzer}) + + contents, err := ioutil.ReadFile(path) + if err != nil { + t.Fatal(err) + } + + got := string(contents) + if got != want { + t.Errorf("contents of rewritten file\ngot: %s\nwant: %s", got, want) + } + + defer cleanup() +} + +var commentAnalyzer = &analysis.Analyzer{ + Name: "comment", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: commentRun, +} + +func commentRun(pass *analysis.Pass) (interface{}, error) { + const ( + from = "/* Package comment */" + to = "// Package comment" + ) + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + inspect.Preorder(nil, func(n ast.Node) { + if n, ok := n.(*ast.Comment); ok && n.Text == from { + pass.Report(analysis.Diagnostic{ + Pos: n.Pos(), + End: n.End(), + SuggestedFixes: []analysis.SuggestedFix{{ + TextEdits: []analysis.TextEdit{{ + Pos: n.Pos(), + End: n.End(), + NewText: []byte(to), + }}, + }}, + }) + } + }) + + return nil, nil +} From dd1bab2d98d3435471d3d9590b3ce4f3c4960c69 Mon Sep 17 00:00:00 2001 From: Timon Wong Date: Wed, 7 Sep 2022 07:00:12 +0000 Subject: [PATCH 228/723] go/analysis/passes/printf: fix panic parsing argument index Fixes golang/go#54828 Change-Id: I516dc83230f6bc96b0ff21f3bbae702f1511e5b0 GitHub-Last-Rev: b26f46a2df820d4862b7c81200c86551b84719fa GitHub-Pull-Request: golang/tools#395 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426875 Auto-Submit: Tim King TryBot-Result: Gopher Robot Reviewed-by: Michael Knyszek gopls-CI: kokoro Run-TryBot: Tim King Reviewed-by: Tim King --- go/analysis/passes/printf/printf.go | 5 +++-- go/analysis/passes/printf/testdata/src/a/a.go | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index f56b9b7beb7..87f171e16c8 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -672,12 +672,13 @@ func (s *formatState) parseIndex() bool { s.scanNum() ok := true if s.nbytes == len(s.format) || s.nbytes == start || s.format[s.nbytes] != ']' { - ok = false - s.nbytes = strings.Index(s.format, "]") + ok = false // syntax error is either missing "]" or invalid index. + s.nbytes = strings.Index(s.format[start:], "]") if s.nbytes < 0 { s.pass.ReportRangef(s.call, "%s format %s is missing closing ]", s.name, s.format) return false } + s.nbytes = s.nbytes + start } arg32, err := strconv.ParseInt(s.format[start:s.nbytes], 10, 32) if err != nil || !ok || arg32 <= 0 || arg32 > int64(len(s.call.Args)-s.firstArg) { diff --git a/go/analysis/passes/printf/testdata/src/a/a.go b/go/analysis/passes/printf/testdata/src/a/a.go index 5eca3172dec..1721ecc1406 100644 --- a/go/analysis/passes/printf/testdata/src/a/a.go +++ b/go/analysis/passes/printf/testdata/src/a/a.go @@ -217,6 +217,7 @@ func PrintfTests() { Printf("%[2]*.[1]*[3]d x", 2, "hi", 4) // want `a.Printf format %\[2]\*\.\[1\]\*\[3\]d uses non-int \x22hi\x22 as argument of \*` Printf("%[0]s x", "arg1") // want `a.Printf format has invalid argument index \[0\]` Printf("%[0]d x", 1) // want `a.Printf format has invalid argument index \[0\]` + Printf("%[3]*.[2*[1]f", 1, 2, 3) // want `a.Printf format has invalid argument index \[2\*\[1\]` // Something that satisfies the error interface. var e error fmt.Println(e.Error()) // ok From b15dac2b8849754d58ebde3b57bbb75776ba869b Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 30 Aug 2022 14:40:12 -0400 Subject: [PATCH 229/723] gopls: migrate internal/lsp to gopls/internal/lsp This CL was created using the following commands: ./gopls/internal/migrate.sh git add . git codereview gofmt For golang/go#54509 Change-Id: Iceeec602748a5e6f609c3ceda8d19157e5c94009 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426796 gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Peter Weinberger TryBot-Result: Gopher Robot --- copyright/copyright.go | 2 +- go/analysis/analysistest/analysistest.go | 4 +-- gopls/api-diff/api_diff.go | 6 ++-- gopls/doc/contributing.md | 2 +- gopls/doc/design/integrating.md | 4 +-- gopls/doc/generate.go | 14 ++++---- gopls/doc/settings.md | 2 +- gopls/internal/coverage/coverage.go | 10 ++++-- gopls/internal/hooks/analysis.go | 4 +-- gopls/internal/hooks/analysis_117.go | 2 +- gopls/internal/hooks/diff.go | 2 +- gopls/internal/hooks/diff_test.go | 2 +- gopls/internal/hooks/hooks.go | 4 +-- {internal => gopls/internal}/lsp/README.md | 0 .../analysis/embeddirective/embeddirective.go | 0 .../embeddirective/embeddirective_test.go | 0 .../embeddirective/testdata/src/a/a.go | 0 .../embeddirective/testdata/src/a/b.go | 0 .../embeddirective/testdata/src/a/embedText | 0 .../lsp/analysis/fillreturns/fillreturns.go | 2 +- .../analysis/fillreturns/fillreturns_test.go | 2 +- .../analysis/fillreturns/testdata/src/a/a.go | 0 .../fillreturns/testdata/src/a/a.go.golden | 0 .../testdata/src/a/typeparams/a.go | 0 .../testdata/src/a/typeparams/a.go.golden | 0 .../lsp/analysis/fillstruct/fillstruct.go | 2 +- .../analysis/fillstruct/fillstruct_test.go | 2 +- .../analysis/fillstruct/testdata/src/a/a.go | 0 .../analysis/fillstruct/testdata/src/b/b.go | 0 .../testdata/src/typeparams/typeparams.go | 0 .../analysis/infertypeargs/infertypeargs.go | 0 .../infertypeargs/infertypeargs_test.go | 2 +- .../lsp/analysis/infertypeargs/run_go117.go | 0 .../lsp/analysis/infertypeargs/run_go118.go | 0 .../infertypeargs/testdata/src/a/basic.go | 0 .../testdata/src/a/basic.go.golden | 0 .../infertypeargs/testdata/src/a/imported.go | 0 .../testdata/src/a/imported.go.golden | 0 .../testdata/src/a/imported/imported.go | 0 .../testdata/src/a/notypechange.go | 0 .../testdata/src/a/notypechange.go.golden | 0 .../lsp/analysis/nonewvars/nonewvars.go | 0 .../lsp/analysis/nonewvars/nonewvars_test.go | 2 +- .../analysis/nonewvars/testdata/src/a/a.go | 0 .../nonewvars/testdata/src/a/a.go.golden | 0 .../nonewvars/testdata/src/typeparams/a.go | 0 .../testdata/src/typeparams/a.go.golden | 0 .../analysis/noresultvalues/noresultvalues.go | 0 .../noresultvalues/noresultvalues_test.go | 2 +- .../noresultvalues/testdata/src/a/a.go | 0 .../noresultvalues/testdata/src/a/a.go.golden | 0 .../testdata/src/typeparams/a.go | 0 .../testdata/src/typeparams/a.go.golden | 0 .../simplifycompositelit.go | 0 .../simplifycompositelit_test.go | 2 +- .../simplifycompositelit/testdata/src/a/a.go | 0 .../testdata/src/a/a.go.golden | 0 .../analysis/simplifyrange/simplifyrange.go | 0 .../simplifyrange/simplifyrange_test.go | 2 +- .../simplifyrange/testdata/src/a/a.go | 0 .../simplifyrange/testdata/src/a/a.go.golden | 0 .../analysis/simplifyslice/simplifyslice.go | 0 .../simplifyslice/simplifyslice_test.go | 2 +- .../simplifyslice/testdata/src/a/a.go | 0 .../simplifyslice/testdata/src/a/a.go.golden | 0 .../testdata/src/typeparams/typeparams.go | 0 .../src/typeparams/typeparams.go.golden | 0 .../lsp/analysis/stubmethods/stubmethods.go | 0 .../undeclaredname/testdata/src/a/a.go | 0 .../undeclaredname/testdata/src/a/channels.go | 0 .../testdata/src/a/consecutive_params.go | 0 .../testdata/src/a/error_param.go | 0 .../undeclaredname/testdata/src/a/literals.go | 0 .../testdata/src/a/operation.go | 0 .../undeclaredname/testdata/src/a/selector.go | 0 .../undeclaredname/testdata/src/a/slice.go | 0 .../undeclaredname/testdata/src/a/tuple.go | 0 .../testdata/src/a/unique_params.go | 0 .../lsp/analysis/undeclaredname/undeclared.go | 0 .../undeclaredname/undeclared_test.go | 2 +- .../analysis/unusedparams/testdata/src/a/a.go | 0 .../unusedparams/testdata/src/a/a.go.golden | 0 .../testdata/src/typeparams/typeparams.go | 0 .../src/typeparams/typeparams.go.golden | 0 .../lsp/analysis/unusedparams/unusedparams.go | 0 .../unusedparams/unusedparams_test.go | 2 +- .../unusedvariable/testdata/src/assign/a.go | 0 .../testdata/src/assign/a.go.golden | 0 .../unusedvariable/testdata/src/decl/a.go | 0 .../testdata/src/decl/a.go.golden | 0 .../analysis/unusedvariable/unusedvariable.go | 0 .../unusedvariable/unusedvariable_test.go | 2 +- .../lsp/analysis/useany/testdata/src/a/a.go | 0 .../useany/testdata/src/a/a.go.golden | 0 .../internal}/lsp/analysis/useany/useany.go | 0 .../lsp/analysis/useany/useany_test.go | 2 +- .../internal}/lsp/browser/README.md | 0 .../internal}/lsp/browser/browser.go | 0 .../internal}/lsp/cache/analysis.go | 4 +-- .../internal}/lsp/cache/cache.go | 4 +-- .../internal}/lsp/cache/check.go | 8 ++--- .../internal}/lsp/cache/debug.go | 0 .../internal}/lsp/cache/error_test.go | 0 .../internal}/lsp/cache/errors.go | 8 ++--- .../internal}/lsp/cache/graph.go | 2 +- .../internal}/lsp/cache/imports.go | 2 +- .../internal}/lsp/cache/keys.go | 0 .../internal}/lsp/cache/load.go | 6 ++-- .../internal}/lsp/cache/maps.go | 2 +- .../internal}/lsp/cache/metadata.go | 14 ++++---- {internal => gopls/internal}/lsp/cache/mod.go | 8 ++--- .../internal}/lsp/cache/mod_tidy.go | 10 +++--- .../internal}/lsp/cache/os_darwin.go | 0 .../internal}/lsp/cache/os_windows.go | 0 .../internal}/lsp/cache/parse.go | 12 +++---- .../internal}/lsp/cache/parse_test.go | 0 .../internal}/lsp/cache/parsemode_go116.go | 0 .../internal}/lsp/cache/parsemode_go117.go | 0 {internal => gopls/internal}/lsp/cache/pkg.go | 2 +- .../internal}/lsp/cache/session.go | 4 +-- .../internal}/lsp/cache/snapshot.go | 6 ++-- .../internal}/lsp/cache/symbols.go | 6 ++-- .../internal}/lsp/cache/view.go | 6 ++-- .../internal}/lsp/cache/view_test.go | 4 +-- .../internal}/lsp/cache/workspace.go | 2 +- .../internal}/lsp/cache/workspace_test.go | 4 +-- .../internal}/lsp/call_hierarchy.go | 4 +-- .../internal}/lsp/cmd/call_hierarchy.go | 2 +- .../internal}/lsp/cmd/capabilities_test.go | 6 ++-- {internal => gopls/internal}/lsp/cmd/check.go | 0 {internal => gopls/internal}/lsp/cmd/cmd.go | 12 +++---- .../internal}/lsp/cmd/cmd_test.go | 6 ++-- .../internal}/lsp/cmd/definition.go | 4 +-- .../internal}/lsp/cmd/export_test.go | 0 .../internal}/lsp/cmd/folding_range.go | 2 +- .../internal}/lsp/cmd/format.go | 6 ++-- .../internal}/lsp/cmd/help_test.go | 2 +- .../internal}/lsp/cmd/highlight.go | 2 +- .../internal}/lsp/cmd/implementation.go | 2 +- .../internal}/lsp/cmd/imports.go | 6 ++-- {internal => gopls/internal}/lsp/cmd/info.go | 6 ++-- {internal => gopls/internal}/lsp/cmd/links.go | 2 +- .../internal}/lsp/cmd/prepare_rename.go | 2 +- .../internal}/lsp/cmd/references.go | 2 +- .../internal}/lsp/cmd/remote.go | 4 +-- .../internal}/lsp/cmd/rename.go | 6 ++-- .../internal}/lsp/cmd/semantictokens.go | 6 ++-- {internal => gopls/internal}/lsp/cmd/serve.go | 8 ++--- .../internal}/lsp/cmd/signature.go | 2 +- .../internal}/lsp/cmd/subcommands.go | 0 .../internal}/lsp/cmd/suggested_fix.go | 6 ++-- .../internal}/lsp/cmd/symbols.go | 2 +- .../internal}/lsp/cmd/test/call_hierarchy.go | 4 +-- .../internal}/lsp/cmd/test/check.go | 6 ++-- .../internal}/lsp/cmd/test/cmdtest.go | 14 ++++---- .../internal}/lsp/cmd/test/definition.go | 2 +- .../internal}/lsp/cmd/test/folding_range.go | 0 .../internal}/lsp/cmd/test/format.go | 0 .../internal}/lsp/cmd/test/highlight.go | 0 .../internal}/lsp/cmd/test/implementation.go | 0 .../internal}/lsp/cmd/test/imports.go | 4 +-- .../internal}/lsp/cmd/test/links.go | 4 +-- .../internal}/lsp/cmd/test/prepare_rename.go | 6 ++-- .../internal}/lsp/cmd/test/references.go | 0 .../internal}/lsp/cmd/test/rename.go | 0 .../internal}/lsp/cmd/test/semanticdriver.go | 0 .../internal}/lsp/cmd/test/signature.go | 4 +-- .../internal}/lsp/cmd/test/suggested_fix.go | 4 +-- .../internal}/lsp/cmd/test/symbols.go | 2 +- .../lsp/cmd/test/workspace_symbol.go | 6 ++-- .../internal}/lsp/cmd/usage/api-json.hlp | 0 .../internal}/lsp/cmd/usage/bug.hlp | 0 .../lsp/cmd/usage/call_hierarchy.hlp | 0 .../internal}/lsp/cmd/usage/check.hlp | 0 .../internal}/lsp/cmd/usage/definition.hlp | 0 .../internal}/lsp/cmd/usage/fix.hlp | 0 .../lsp/cmd/usage/folding_ranges.hlp | 0 .../internal}/lsp/cmd/usage/format.hlp | 0 .../internal}/lsp/cmd/usage/help.hlp | 0 .../internal}/lsp/cmd/usage/highlight.hlp | 0 .../lsp/cmd/usage/implementation.hlp | 0 .../internal}/lsp/cmd/usage/imports.hlp | 0 .../internal}/lsp/cmd/usage/inspect.hlp | 0 .../internal}/lsp/cmd/usage/licenses.hlp | 0 .../internal}/lsp/cmd/usage/links.hlp | 0 .../lsp/cmd/usage/prepare_rename.hlp | 0 .../internal}/lsp/cmd/usage/references.hlp | 0 .../internal}/lsp/cmd/usage/remote.hlp | 0 .../internal}/lsp/cmd/usage/rename.hlp | 0 .../internal}/lsp/cmd/usage/semtok.hlp | 0 .../internal}/lsp/cmd/usage/serve.hlp | 0 .../internal}/lsp/cmd/usage/signature.hlp | 0 .../internal}/lsp/cmd/usage/symbols.hlp | 0 .../internal}/lsp/cmd/usage/usage.hlp | 0 .../internal}/lsp/cmd/usage/version.hlp | 0 .../internal}/lsp/cmd/usage/vulncheck.hlp | 2 +- .../internal}/lsp/cmd/usage/workspace.hlp | 0 .../lsp/cmd/usage/workspace_symbol.hlp | 0 .../internal}/lsp/cmd/vulncheck.go | 4 +-- .../internal}/lsp/cmd/workspace.go | 6 ++-- .../internal}/lsp/cmd/workspace_symbol.go | 4 +-- .../internal}/lsp/code_action.go | 10 +++--- {internal => gopls/internal}/lsp/code_lens.go | 8 ++--- {internal => gopls/internal}/lsp/command.go | 10 +++--- .../internal}/lsp/command/command_gen.go | 2 +- .../internal}/lsp/command/commandmeta/meta.go | 4 +-- .../internal}/lsp/command/gen/gen.go | 6 ++-- .../internal}/lsp/command/generate.go | 2 +- .../internal}/lsp/command/interface.go | 2 +- .../internal}/lsp/command/interface_test.go | 2 +- .../internal}/lsp/command/util.go | 0 .../internal}/lsp/completion.go | 14 ++++---- .../internal}/lsp/completion_test.go | 6 ++-- {internal => gopls/internal}/lsp/debounce.go | 0 .../internal}/lsp/debounce_test.go | 0 .../internal}/lsp/debug/buildinfo_go1.12.go | 0 .../internal}/lsp/debug/buildinfo_go1.18.go | 0 .../internal}/lsp/debug/info.go | 2 +- .../internal}/lsp/debug/info_test.go | 0 .../internal}/lsp/debug/log/log.go | 2 +- .../internal}/lsp/debug/metrics.go | 2 +- {internal => gopls/internal}/lsp/debug/rpc.go | 2 +- .../internal}/lsp/debug/serve.go | 12 +++---- .../internal}/lsp/debug/trace.go | 0 .../internal}/lsp/definition.go | 6 ++-- .../internal}/lsp/diagnostics.go | 14 ++++---- .../internal}/lsp/fake/client.go | 2 +- {internal => gopls/internal}/lsp/fake/doc.go | 0 {internal => gopls/internal}/lsp/fake/edit.go | 2 +- .../internal}/lsp/fake/edit_test.go | 0 .../internal}/lsp/fake/editor.go | 4 +-- .../internal}/lsp/fake/editor_test.go | 0 .../internal}/lsp/fake/proxy.go | 0 .../internal}/lsp/fake/sandbox.go | 0 .../internal}/lsp/fake/workdir.go | 2 +- .../internal}/lsp/fake/workdir_test.go | 2 +- .../internal}/lsp/fake/workdir_windows.go | 0 .../internal}/lsp/folding_range.go | 4 +-- {internal => gopls/internal}/lsp/format.go | 8 ++--- {internal => gopls/internal}/lsp/general.go | 8 ++--- .../internal}/lsp/helper/README.md | 0 .../internal}/lsp/helper/helper.go | 2 +- {internal => gopls/internal}/lsp/highlight.go | 8 ++--- {internal => gopls/internal}/lsp/hover.go | 10 +++--- .../internal}/lsp/implementation.go | 4 +-- .../internal}/lsp/inlay_hint.go | 4 +-- {internal => gopls/internal}/lsp/link.go | 6 ++-- {internal => gopls/internal}/lsp/lsp_test.go | 16 ++++----- .../internal}/lsp/lsppos/lsppos.go | 2 +- .../internal}/lsp/lsppos/lsppos_test.go | 4 +-- .../internal}/lsp/lsppos/token.go | 4 +-- .../internal}/lsp/lsppos/token_test.go | 4 +-- .../internal}/lsp/lsprpc/autostart_default.go | 0 .../internal}/lsp/lsprpc/autostart_posix.go | 0 .../internal}/lsp/lsprpc/binder.go | 2 +- .../internal}/lsp/lsprpc/binder_test.go | 4 +-- .../lsp/lsprpc/commandinterceptor.go | 2 +- .../lsp/lsprpc/commandinterceptor_test.go | 4 +-- .../internal}/lsp/lsprpc/dialer.go | 0 .../internal}/lsp/lsprpc/goenv.go | 2 +- .../internal}/lsp/lsprpc/goenv_test.go | 4 +-- .../internal}/lsp/lsprpc/lsprpc.go | 12 +++---- .../internal}/lsp/lsprpc/lsprpc_test.go | 8 ++--- .../internal}/lsp/lsprpc/middleware.go | 0 .../internal}/lsp/lsprpc/middleware_test.go | 2 +- .../internal}/lsp/mod/code_lens.go | 6 ++-- .../internal}/lsp/mod/diagnostics.go | 8 ++--- .../internal}/lsp/mod/format.go | 4 +-- {internal => gopls/internal}/lsp/mod/hover.go | 4 +-- .../internal}/lsp/mod/mod_test.go | 6 ++-- .../lsp/mod/testdata/unchanged/go.mod | 0 .../lsp/mod/testdata/unchanged/main.go | 0 .../internal}/lsp/progress/progress.go | 4 +-- .../internal}/lsp/progress/progress_test.go | 2 +- .../internal}/lsp/protocol/codeactionkind.go | 0 .../internal}/lsp/protocol/context.go | 0 .../internal}/lsp/protocol/doc.go | 0 .../internal}/lsp/protocol/enums.go | 0 .../internal}/lsp/protocol/log.go | 0 .../internal}/lsp/protocol/protocol.go | 0 .../internal}/lsp/protocol/span.go | 0 .../internal}/lsp/protocol/tsclient.go | 0 .../lsp/protocol/tsdocument_changes.go | 0 .../internal}/lsp/protocol/tsprotocol.go | 0 .../internal}/lsp/protocol/tsserver.go | 0 .../lsp/protocol/typescript/README.md | 0 .../internal}/lsp/protocol/typescript/code.ts | 0 .../lsp/protocol/typescript/tsconfig.json | 0 .../internal}/lsp/protocol/typescript/util.ts | 0 .../internal}/lsp/references.go | 6 ++-- .../internal}/lsp/regtest/doc.go | 2 +- .../internal}/lsp/regtest/env.go | 4 +-- .../internal}/lsp/regtest/env_test.go | 2 +- .../internal}/lsp/regtest/expectation.go | 6 ++-- .../internal}/lsp/regtest/regtest.go | 4 +-- .../internal}/lsp/regtest/runner.go | 12 +++---- .../internal}/lsp/regtest/wrappers.go | 6 ++-- {internal => gopls/internal}/lsp/rename.go | 4 +-- .../internal}/lsp/reset_golden.sh | 0 .../internal}/lsp/safetoken/safetoken.go | 0 .../internal}/lsp/safetoken/safetoken_test.go | 8 ++--- {internal => gopls/internal}/lsp/semantic.go | 8 ++--- {internal => gopls/internal}/lsp/server.go | 6 ++-- .../internal}/lsp/server_gen.go | 2 +- .../internal}/lsp/signature_help.go | 6 ++-- .../internal}/lsp/snippet/snippet_builder.go | 0 .../lsp/snippet/snippet_builder_test.go | 0 .../internal}/lsp/source/add_import.go | 2 +- .../internal}/lsp/source/api_json.go | 0 .../internal}/lsp/source/call_hierarchy.go | 4 +-- .../internal}/lsp/source/code_lens.go | 4 +-- .../internal}/lsp/source/comment.go | 0 .../internal}/lsp/source/comment_go118.go | 0 .../lsp/source/comment_go118_test.go | 0 .../lsp/source/completion/builtin.go | 0 .../lsp/source/completion/completion.go | 8 ++--- .../lsp/source/completion/deep_completion.go | 0 .../source/completion/deep_completion_test.go | 0 .../lsp/source/completion/definition.go | 6 ++-- .../internal}/lsp/source/completion/format.go | 8 ++--- .../internal}/lsp/source/completion/fuzz.go | 2 +- .../lsp/source/completion/keywords.go | 4 +-- .../internal}/lsp/source/completion/labels.go | 0 .../lsp/source/completion/literal.go | 6 ++-- .../lsp/source/completion/package.go | 10 +++--- .../lsp/source/completion/package_test.go | 0 .../lsp/source/completion/postfix_snippets.go | 6 ++-- .../internal}/lsp/source/completion/printf.go | 0 .../lsp/source/completion/printf_test.go | 0 .../lsp/source/completion/snippet.go | 2 +- .../lsp/source/completion/statements.go | 6 ++-- .../internal}/lsp/source/completion/util.go | 6 ++-- .../lsp/source/completion/util_test.go | 0 .../internal}/lsp/source/diagnostics.go | 2 +- .../internal}/lsp/source/extract.go | 4 +-- .../internal}/lsp/source/fix.go | 8 ++--- .../internal}/lsp/source/folding_range.go | 2 +- .../internal}/lsp/source/format.go | 8 ++--- .../internal}/lsp/source/format_test.go | 2 +- .../internal}/lsp/source/gc_annotations.go | 2 +- .../internal}/lsp/source/highlight.go | 2 +- .../internal}/lsp/source/hover.go | 6 ++-- .../internal}/lsp/source/identifier.go | 6 ++-- .../internal}/lsp/source/identifier_test.go | 0 .../internal}/lsp/source/implementation.go | 4 +-- .../internal}/lsp/source/inlay_hint.go | 4 +-- .../internal}/lsp/source/known_packages.go | 0 .../internal}/lsp/source/options.go | 36 +++++++++---------- .../internal}/lsp/source/options_test.go | 0 .../internal}/lsp/source/references.go | 4 +-- .../internal}/lsp/source/rename.go | 4 +-- .../internal}/lsp/source/rename_check.go | 0 .../internal}/lsp/source/signature_help.go | 2 +- .../internal}/lsp/source/source_test.go | 18 +++++----- .../internal}/lsp/source/stub.go | 6 ++-- .../internal}/lsp/source/symbols.go | 2 +- .../internal}/lsp/source/types_format.go | 4 +-- .../internal}/lsp/source/util.go | 4 +-- .../internal}/lsp/source/util_test.go | 2 +- .../internal}/lsp/source/view.go | 4 +-- .../internal}/lsp/source/workspace_symbol.go | 4 +-- .../lsp/source/workspace_symbol_test.go | 0 {internal => gopls/internal}/lsp/symbols.go | 8 ++--- .../internal}/lsp/template/completion.go | 4 +-- .../internal}/lsp/template/completion_test.go | 2 +- .../internal}/lsp/template/highlight.go | 4 +-- .../internal}/lsp/template/implementations.go | 4 +-- .../internal}/lsp/template/parse.go | 4 +-- .../internal}/lsp/template/parse_test.go | 0 .../internal}/lsp/template/symbols.go | 4 +-- .../lsp/testdata/%percent/perc%ent.go | 0 .../testdata/addimport/addimport.go.golden | 0 .../lsp/testdata/addimport/addimport.go.in | 0 .../internal}/lsp/testdata/address/address.go | 0 .../lsp/testdata/analyzer/bad_test.go | 0 .../internal}/lsp/testdata/anon/anon.go.in | 0 .../internal}/lsp/testdata/append/append.go | 0 .../lsp/testdata/append/append2.go.in | 0 .../lsp/testdata/arraytype/array_type.go.in | 0 .../lsp/testdata/assign/assign.go.in | 0 .../testdata/assign/internal/secret/secret.go | 0 .../internal}/lsp/testdata/bad/bad0.go | 0 .../internal}/lsp/testdata/bad/bad1.go | 0 .../lsp/testdata/badstmt/badstmt.go.in | 0 .../lsp/testdata/badstmt/badstmt_2.go.in | 0 .../lsp/testdata/badstmt/badstmt_3.go.in | 0 .../lsp/testdata/badstmt/badstmt_4.go.in | 0 .../internal}/lsp/testdata/bar/bar.go.in | 0 .../lsp/testdata/basiclit/basiclit.go | 0 .../internal}/lsp/testdata/baz/baz.go.in | 0 .../lsp/testdata/builtins/builtin_args.go | 0 .../lsp/testdata/builtins/builtin_types.go | 0 .../lsp/testdata/builtins/builtins.go | 0 .../lsp/testdata/builtins/constants.go | 0 .../testdata/callhierarchy/callhierarchy.go | 0 .../callhierarchy/incoming/incoming.go | 0 .../callhierarchy/outgoing/outgoing.go | 0 .../testdata/casesensitive/casesensitive.go | 0 .../internal}/lsp/testdata/cast/cast.go.in | 0 .../internal}/lsp/testdata/cgo/declarecgo.go | 0 .../lsp/testdata/cgo/declarecgo.go.golden | 0 .../lsp/testdata/cgo/declarecgo_nocgo.go | 0 .../lsp/testdata/cgoimport/usecgo.go.golden | 0 .../lsp/testdata/cgoimport/usecgo.go.in | 0 .../internal}/lsp/testdata/channel/channel.go | 0 .../lsp/testdata/codelens/codelens_test.go | 0 .../comment_completion.go.in | 0 .../lsp/testdata/complit/complit.go.in | 0 .../lsp/testdata/constant/constant.go | 0 .../lsp/testdata/danglingstmt/dangling_for.go | 0 .../danglingstmt/dangling_for_init.go | 0 .../danglingstmt/dangling_for_init_cond.go | 0 .../dangling_for_init_cond_post.go | 0 .../lsp/testdata/danglingstmt/dangling_if.go | 0 .../testdata/danglingstmt/dangling_if_eof.go | 0 .../testdata/danglingstmt/dangling_if_init.go | 0 .../danglingstmt/dangling_if_init_cond.go | 0 .../danglingstmt/dangling_multiline_if.go | 0 .../danglingstmt/dangling_selector_1.go | 0 .../danglingstmt/dangling_selector_2.go | 0 .../danglingstmt/dangling_switch_init.go | 0 .../danglingstmt/dangling_switch_init_tag.go | 0 .../internal}/lsp/testdata/deep/deep.go | 0 .../internal}/lsp/testdata/errors/errors.go | 0 .../extract_function/extract_args_returns.go | 0 .../extract_args_returns.go.golden | 0 .../extract/extract_function/extract_basic.go | 0 .../extract_function/extract_basic.go.golden | 0 .../extract_function/extract_basic_comment.go | 0 .../extract_basic_comment.go.golden | 0 .../extract_function/extract_issue_44813.go | 0 .../extract_issue_44813.go.golden | 0 .../extract_function/extract_redefine.go | 0 .../extract_redefine.go.golden | 0 .../extract_function/extract_return_basic.go | 0 .../extract_return_basic.go.golden | 0 .../extract_return_basic_nonnested.go | 0 .../extract_return_basic_nonnested.go.golden | 0 .../extract_return_complex.go | 0 .../extract_return_complex.go.golden | 0 .../extract_return_complex_nonnested.go | 0 ...extract_return_complex_nonnested.go.golden | 0 .../extract_return_func_lit.go | 0 .../extract_return_func_lit.go.golden | 0 .../extract_return_func_lit_nonnested.go | 0 ...xtract_return_func_lit_nonnested.go.golden | 0 .../extract_function/extract_return_init.go | 0 .../extract_return_init.go.golden | 0 .../extract_return_init_nonnested.go | 0 .../extract_return_init_nonnested.go.golden | 0 .../extract/extract_function/extract_scope.go | 0 .../extract_function/extract_scope.go.golden | 0 .../extract_smart_initialization.go | 0 .../extract_smart_initialization.go.golden | 0 .../extract_function/extract_smart_return.go | 0 .../extract_smart_return.go.golden | 0 .../extract_unnecessary_param.go | 0 .../extract_unnecessary_param.go.golden | 0 .../extract/extract_method/extract_basic.go | 0 .../extract_method/extract_basic.go.golden | 0 .../extract_variable/extract_basic_lit.go | 0 .../extract_basic_lit.go.golden | 0 .../extract_variable/extract_func_call.go | 0 .../extract_func_call.go.golden | 0 .../extract/extract_variable/extract_scope.go | 0 .../extract_variable/extract_scope.go.golden | 0 .../lsp/testdata/fieldlist/field_list.go | 0 .../internal}/lsp/testdata/fillstruct/a.go | 0 .../lsp/testdata/fillstruct/a.go.golden | 0 .../internal}/lsp/testdata/fillstruct/a2.go | 0 .../lsp/testdata/fillstruct/a2.go.golden | 0 .../internal}/lsp/testdata/fillstruct/a3.go | 0 .../lsp/testdata/fillstruct/a3.go.golden | 0 .../internal}/lsp/testdata/fillstruct/a4.go | 0 .../lsp/testdata/fillstruct/a4.go.golden | 0 .../lsp/testdata/fillstruct/data/a.go | 0 .../lsp/testdata/fillstruct/fill_struct.go | 0 .../testdata/fillstruct/fill_struct.go.golden | 0 .../testdata/fillstruct/fill_struct_anon.go | 0 .../fillstruct/fill_struct_anon.go.golden | 0 .../testdata/fillstruct/fill_struct_nested.go | 0 .../fillstruct/fill_struct_nested.go.golden | 0 .../fillstruct/fill_struct_package.go | 0 .../fillstruct/fill_struct_package.go.golden | 0 .../fillstruct/fill_struct_partial.go | 0 .../fillstruct/fill_struct_partial.go.golden | 0 .../testdata/fillstruct/fill_struct_spaces.go | 0 .../fillstruct/fill_struct_spaces.go.golden | 0 .../testdata/fillstruct/fill_struct_unsafe.go | 0 .../fillstruct/fill_struct_unsafe.go.golden | 0 .../lsp/testdata/fillstruct/typeparams.go | 0 .../testdata/fillstruct/typeparams.go.golden | 0 .../internal}/lsp/testdata/folding/a.go | 0 .../lsp/testdata/folding/a.go.golden | 0 .../lsp/testdata/folding/bad.go.golden | 0 .../internal}/lsp/testdata/folding/bad.go.in | 0 .../internal}/lsp/testdata/foo/foo.go | 0 .../lsp/testdata/format/bad_format.go.golden | 0 .../lsp/testdata/format/bad_format.go.in | 0 .../lsp/testdata/format/good_format.go | 0 .../lsp/testdata/format/good_format.go.golden | 0 .../testdata/format/newline_format.go.golden | 0 .../lsp/testdata/format/newline_format.go.in | 0 .../lsp/testdata/format/one_line.go.golden | 0 .../lsp/testdata/format/one_line.go.in | 0 .../lsp/testdata/func_rank/func_rank.go.in | 0 .../lsp/testdata/funcsig/func_sig.go | 0 .../lsp/testdata/funcvalue/func_value.go | 0 .../lsp/testdata/fuzzymatch/fuzzymatch.go | 0 .../lsp/testdata/generate/generate.go | 0 .../lsp/testdata/generated/generated.go | 0 .../lsp/testdata/generated/generator.go | 0 .../internal}/lsp/testdata/godef/a/a.go | 0 .../lsp/testdata/godef/a/a.go.golden | 0 .../internal}/lsp/testdata/godef/a/a_test.go | 0 .../lsp/testdata/godef/a/a_test.go.golden | 0 .../lsp/testdata/godef/a/a_x_test.go | 0 .../lsp/testdata/godef/a/a_x_test.go.golden | 0 .../internal}/lsp/testdata/godef/a/d.go | 0 .../lsp/testdata/godef/a/d.go.golden | 0 .../internal}/lsp/testdata/godef/a/f.go | 0 .../lsp/testdata/godef/a/f.go.golden | 0 .../internal}/lsp/testdata/godef/a/g.go | 0 .../lsp/testdata/godef/a/g.go.golden | 0 .../internal}/lsp/testdata/godef/a/h.go | 0 .../lsp/testdata/godef/a/h.go.golden | 0 .../internal}/lsp/testdata/godef/a/random.go | 0 .../lsp/testdata/godef/a/random.go.golden | 0 .../internal}/lsp/testdata/godef/b/b.go | 0 .../lsp/testdata/godef/b/b.go.golden | 0 .../internal}/lsp/testdata/godef/b/c.go | 0 .../lsp/testdata/godef/b/c.go.golden | 0 .../internal}/lsp/testdata/godef/b/c.go.saved | 0 .../internal}/lsp/testdata/godef/b/e.go | 0 .../lsp/testdata/godef/b/e.go.golden | 0 .../internal}/lsp/testdata/godef/b/h.go | 0 .../lsp/testdata/godef/b/h.go.golden | 0 .../godef/broken/unclosedIf.go.golden | 0 .../testdata/godef/broken/unclosedIf.go.in | 0 .../testdata/godef/hover_generics/hover.go | 0 .../godef/hover_generics/hover.go.golden | 0 .../testdata/godef/infer_generics/inferred.go | 0 .../godef/infer_generics/inferred.go.golden | 0 .../internal}/lsp/testdata/good/good0.go | 0 .../internal}/lsp/testdata/good/good1.go | 0 .../lsp/testdata/highlights/highlights.go | 0 .../testdata/implementation/implementation.go | 0 .../testdata/implementation/other/other.go | 0 .../implementation/other/other_test.go | 0 .../importedcomplit/imported_complit.go.in | 0 .../lsp/testdata/imports/add_import.go.golden | 0 .../lsp/testdata/imports/add_import.go.in | 0 .../testdata/imports/good_imports.go.golden | 0 .../lsp/testdata/imports/good_imports.go.in | 0 .../lsp/testdata/imports/issue35458.go.golden | 0 .../lsp/testdata/imports/issue35458.go.in | 0 .../imports/multiple_blocks.go.golden | 0 .../testdata/imports/multiple_blocks.go.in | 0 .../testdata/imports/needs_imports.go.golden | 0 .../lsp/testdata/imports/needs_imports.go.in | 0 .../testdata/imports/remove_import.go.golden | 0 .../lsp/testdata/imports/remove_import.go.in | 0 .../testdata/imports/remove_imports.go.golden | 0 .../lsp/testdata/imports/remove_imports.go.in | 0 .../lsp/testdata/imports/two_lines.go.golden | 0 .../lsp/testdata/imports/two_lines.go.in | 0 .../internal}/lsp/testdata/index/index.go | 0 .../testdata/inlay_hint/composite_literals.go | 0 .../inlay_hint/composite_literals.go.golden | 0 .../testdata/inlay_hint/constant_values.go | 0 .../inlay_hint/constant_values.go.golden | 0 .../testdata/inlay_hint/parameter_names.go | 0 .../inlay_hint/parameter_names.go.golden | 0 .../lsp/testdata/inlay_hint/type_params.go | 0 .../testdata/inlay_hint/type_params.go.golden | 0 .../lsp/testdata/inlay_hint/variable_types.go | 0 .../inlay_hint/variable_types.go.golden | 0 .../testdata/interfacerank/interface_rank.go | 0 .../keywords/accidental_keywords.go.in | 0 .../lsp/testdata/keywords/empty_select.go | 0 .../lsp/testdata/keywords/empty_switch.go | 0 .../lsp/testdata/keywords/keywords.go | 0 .../internal}/lsp/testdata/labels/labels.go | 0 .../internal}/lsp/testdata/links/links.go | 0 .../internal}/lsp/testdata/maps/maps.go.in | 0 .../lsp/testdata/missingfunction/channels.go | 0 .../missingfunction/channels.go.golden | 0 .../missingfunction/consecutive_params.go | 0 .../consecutive_params.go.golden | 0 .../testdata/missingfunction/error_param.go | 0 .../missingfunction/error_param.go.golden | 0 .../lsp/testdata/missingfunction/literals.go | 0 .../missingfunction/literals.go.golden | 0 .../lsp/testdata/missingfunction/operation.go | 0 .../missingfunction/operation.go.golden | 0 .../lsp/testdata/missingfunction/selector.go | 0 .../missingfunction/selector.go.golden | 0 .../lsp/testdata/missingfunction/slice.go | 0 .../testdata/missingfunction/slice.go.golden | 0 .../lsp/testdata/missingfunction/tuple.go | 0 .../testdata/missingfunction/tuple.go.golden | 0 .../testdata/missingfunction/unique_params.go | 0 .../missingfunction/unique_params.go.golden | 0 .../testdata/multireturn/multi_return.go.in | 0 .../nested_complit/nested_complit.go.in | 0 .../internal}/lsp/testdata/nodisk/empty | 0 .../lsp/testdata/nodisk/nodisk.overlay.go | 0 .../lsp/testdata/noparse/noparse.go.in | 0 .../noparse_format/noparse_format.go.golden | 0 .../noparse_format/noparse_format.go.in | 0 .../noparse_format/parse_format.go.golden | 0 .../noparse_format/parse_format.go.in | 0 .../internal}/lsp/testdata/printf/printf.go | 0 .../lsp/testdata/rank/assign_rank.go.in | 0 .../lsp/testdata/rank/binexpr_rank.go.in | 0 .../lsp/testdata/rank/boolexpr_rank.go | 0 .../lsp/testdata/rank/convert_rank.go.in | 0 .../lsp/testdata/rank/struct/struct_rank.go | 0 .../lsp/testdata/rank/switch_rank.go.in | 0 .../lsp/testdata/rank/type_assert_rank.go.in | 0 .../lsp/testdata/rank/type_switch_rank.go.in | 0 .../testdata/references/another/another.go | 0 .../references/interfaces/interfaces.go | 0 .../lsp/testdata/references/other/other.go | 0 .../internal}/lsp/testdata/references/refs.go | 0 .../lsp/testdata/references/refs_test.go | 0 .../lsp/testdata/rename/a/random.go.golden | 0 .../lsp/testdata/rename/a/random.go.in | 0 .../internal}/lsp/testdata/rename/b/b.go | 0 .../lsp/testdata/rename/b/b.go.golden | 0 .../lsp/testdata/rename/bad/bad.go.golden | 0 .../lsp/testdata/rename/bad/bad.go.in | 0 .../lsp/testdata/rename/bad/bad_test.go.in | 0 .../internal}/lsp/testdata/rename/c/c.go | 0 .../lsp/testdata/rename/c/c.go.golden | 0 .../internal}/lsp/testdata/rename/c/c2.go | 0 .../lsp/testdata/rename/c/c2.go.golden | 0 .../rename/crosspkg/another/another.go | 0 .../rename/crosspkg/another/another.go.golden | 0 .../lsp/testdata/rename/crosspkg/crosspkg.go | 0 .../rename/crosspkg/crosspkg.go.golden | 0 .../testdata/rename/crosspkg/other/other.go | 0 .../rename/crosspkg/other/other.go.golden | 0 .../lsp/testdata/rename/generics/embedded.go | 0 .../rename/generics/embedded.go.golden | 0 .../lsp/testdata/rename/generics/generics.go | 0 .../rename/generics/generics.go.golden | 0 .../lsp/testdata/rename/generics/unions.go | 0 .../testdata/rename/generics/unions.go.golden | 0 .../rename/issue39614/issue39614.go.golden | 0 .../rename/issue39614/issue39614.go.in | 0 .../lsp/testdata/rename/issue42134/1.go | 0 .../testdata/rename/issue42134/1.go.golden | 0 .../lsp/testdata/rename/issue42134/2.go | 0 .../testdata/rename/issue42134/2.go.golden | 0 .../lsp/testdata/rename/issue42134/3.go | 0 .../testdata/rename/issue42134/3.go.golden | 0 .../lsp/testdata/rename/issue42134/4.go | 0 .../testdata/rename/issue42134/4.go.golden | 0 .../rename/issue43616/issue43616.go.golden | 0 .../rename/issue43616/issue43616.go.in | 0 .../lsp/testdata/rename/shadow/shadow.go | 0 .../testdata/rename/shadow/shadow.go.golden | 0 .../lsp/testdata/rename/testy/testy.go | 0 .../lsp/testdata/rename/testy/testy.go.golden | 0 .../lsp/testdata/rename/testy/testy_test.go | 0 .../rename/testy/testy_test.go.golden | 0 .../rundespiteerrors/rundespiteerrors.go | 0 .../lsp/testdata/selector/selector.go.in | 0 .../internal}/lsp/testdata/semantic/README.md | 0 .../internal}/lsp/testdata/semantic/a.go | 0 .../lsp/testdata/semantic/a.go.golden | 0 .../internal}/lsp/testdata/semantic/b.go | 0 .../lsp/testdata/semantic/b.go.golden | 0 .../lsp/testdata/semantic/semantic_test.go | 0 .../lsp/testdata/signature/signature.go | 0 .../testdata/signature/signature.go.golden | 0 .../testdata/signature/signature2.go.golden | 0 .../lsp/testdata/signature/signature2.go.in | 0 .../testdata/signature/signature3.go.golden | 0 .../lsp/testdata/signature/signature3.go.in | 0 .../lsp/testdata/signature/signature_test.go | 0 .../signature/signature_test.go.golden | 0 .../testdata/snippets/func_snippets118.go.in | 0 .../lsp/testdata/snippets/literal.go | 0 .../lsp/testdata/snippets/literal.go.golden | 0 .../testdata/snippets/literal_snippets.go.in | 0 .../snippets/literal_snippets118.go.in | 0 .../lsp/testdata/snippets/postfix.go | 0 .../lsp/testdata/snippets/snippets.go.golden | 0 .../lsp/testdata/snippets/snippets.go.in | 0 .../lsp/testdata/statements/append.go | 0 .../statements/if_err_check_return.go | 0 .../statements/if_err_check_return_2.go | 0 .../testdata/statements/if_err_check_test.go | 0 .../lsp/testdata/stub/other/other.go | 0 .../lsp/testdata/stub/stub_add_selector.go | 0 .../testdata/stub/stub_add_selector.go.golden | 0 .../lsp/testdata/stub/stub_assign.go | 0 .../lsp/testdata/stub/stub_assign.go.golden | 0 .../testdata/stub/stub_assign_multivars.go | 0 .../stub/stub_assign_multivars.go.golden | 0 .../lsp/testdata/stub/stub_call_expr.go | 0 .../testdata/stub/stub_call_expr.go.golden | 0 .../lsp/testdata/stub/stub_embedded.go | 0 .../lsp/testdata/stub/stub_embedded.go.golden | 0 .../internal}/lsp/testdata/stub/stub_err.go | 0 .../lsp/testdata/stub/stub_err.go.golden | 0 .../lsp/testdata/stub/stub_function_return.go | 0 .../stub/stub_function_return.go.golden | 0 .../testdata/stub/stub_generic_receiver.go | 0 .../stub/stub_generic_receiver.go.golden | 0 .../lsp/testdata/stub/stub_ignored_imports.go | 0 .../stub/stub_ignored_imports.go.golden | 0 .../lsp/testdata/stub/stub_multi_var.go | 0 .../testdata/stub/stub_multi_var.go.golden | 0 .../lsp/testdata/stub/stub_pointer.go | 0 .../lsp/testdata/stub/stub_pointer.go.golden | 0 .../lsp/testdata/stub/stub_renamed_import.go | 0 .../stub/stub_renamed_import.go.golden | 0 .../stub/stub_renamed_import_iface.go | 0 .../stub/stub_renamed_import_iface.go.golden | 0 .../lsp/testdata/stub/stub_stdlib.go | 0 .../lsp/testdata/stub/stub_stdlib.go.golden | 0 .../suggestedfix/has_suggested_fix.go | 0 .../suggestedfix/has_suggested_fix.go.golden | 0 .../internal}/lsp/testdata/summary.txt.golden | 0 .../lsp/testdata/summary_go1.18.txt.golden | 0 .../internal}/lsp/testdata/symbols/main.go | 0 .../lsp/testdata/symbols/main.go.golden | 0 .../internal}/lsp/testdata/testy/testy.go | 0 .../lsp/testdata/testy/testy_test.go | 0 .../lsp/testdata/testy/testy_test.go.golden | 0 .../internal}/lsp/testdata/typdef/typdef.go | 0 .../lsp/testdata/typeassert/type_assert.go | 0 .../lsp/testdata/typeerrors/noresultvalues.go | 0 .../typeerrors/noresultvalues.go.golden | 0 .../lsp/testdata/typemods/type_mods.go | 0 .../lsp/testdata/typeparams/type_params.go | 0 .../internal}/lsp/testdata/types/types.go | 0 .../internal}/lsp/testdata/undeclared/var.go | 0 .../lsp/testdata/undeclared/var.go.golden | 0 .../lsp/testdata/unimported/export_test.go | 0 .../lsp/testdata/unimported/unimported.go.in | 0 .../unimported/unimported_cand_type.go | 0 .../lsp/testdata/unimported/x_test.go | 0 .../lsp/testdata/unresolved/unresolved.go.in | 0 .../internal}/lsp/testdata/unsafe/unsafe.go | 0 .../lsp/testdata/variadic/variadic.go.in | 0 .../lsp/testdata/variadic/variadic_intf.go | 0 .../lsp/testdata/workspacesymbol/a/a.go | 0 .../testdata/workspacesymbol/a/a.go.golden | 0 .../lsp/testdata/workspacesymbol/a/a_test.go | 0 .../workspacesymbol/a/a_test.go.golden | 0 .../testdata/workspacesymbol/a/a_x_test.go | 0 .../workspacesymbol/a/a_x_test.go.golden | 0 .../lsp/testdata/workspacesymbol/b/b.go | 0 .../testdata/workspacesymbol/b/b.go.golden | 0 .../testdata/workspacesymbol/issue44806.go | 0 .../lsp/testdata/workspacesymbol/main.go | 0 .../lsp/testdata/workspacesymbol/p/p.go | 0 .../lsp/testdata/workspacesymbol/query.go | 0 .../testdata/workspacesymbol/query.go.golden | 0 .../internal}/lsp/tests/README.md | 2 +- .../internal}/lsp/tests/compare/text.go | 2 +- .../internal}/lsp/tests/compare/text_test.go | 2 +- .../internal}/lsp/tests/markdown_go118.go | 2 +- .../internal}/lsp/tests/markdown_go119.go | 2 +- .../internal}/lsp/tests/normalizer.go | 0 .../internal}/lsp/tests/tests.go | 10 +++--- .../internal}/lsp/tests/util.go | 10 +++--- .../internal}/lsp/text_synchronization.go | 4 +-- .../internal}/lsp/work/completion.go | 4 +-- .../internal}/lsp/work/diagnostics.go | 6 ++-- .../internal}/lsp/work/format.go | 4 +-- .../internal}/lsp/work/hover.go | 4 +-- {internal => gopls/internal}/lsp/workspace.go | 4 +-- .../internal}/lsp/workspace_symbol.go | 4 +-- gopls/internal/regtest/bench/bench_test.go | 10 +++--- .../internal/regtest/bench/completion_test.go | 4 +-- .../internal/regtest/bench/didchange_test.go | 2 +- gopls/internal/regtest/bench/iwl_test.go | 4 +-- gopls/internal/regtest/bench/stress_test.go | 6 ++-- .../regtest/codelens/codelens_test.go | 10 +++--- .../regtest/codelens/gcdetails_test.go | 10 +++--- .../regtest/completion/completion18_test.go | 2 +- .../regtest/completion/completion_test.go | 8 ++--- .../completion/postfix_snippet_test.go | 2 +- gopls/internal/regtest/debug/debug_test.go | 4 +-- .../regtest/diagnostics/analysis_test.go | 4 +-- .../regtest/diagnostics/builtin_test.go | 2 +- .../regtest/diagnostics/diagnostics_test.go | 10 +++--- .../regtest/diagnostics/invalidation_test.go | 4 +-- .../regtest/diagnostics/undeclared_test.go | 4 +-- .../regtest/inlayhints/inlayhints_test.go | 6 ++-- .../regtest/misc/call_hierarchy_test.go | 4 +-- .../regtest/misc/configuration_test.go | 2 +- .../internal/regtest/misc/debugserver_test.go | 6 ++-- .../internal/regtest/misc/definition_test.go | 8 ++--- gopls/internal/regtest/misc/embed_test.go | 2 +- gopls/internal/regtest/misc/failures_test.go | 2 +- gopls/internal/regtest/misc/fix_test.go | 6 ++-- .../internal/regtest/misc/formatting_test.go | 4 +-- gopls/internal/regtest/misc/generate_test.go | 2 +- gopls/internal/regtest/misc/highlight_test.go | 6 ++-- gopls/internal/regtest/misc/hover_test.go | 4 +-- gopls/internal/regtest/misc/import_test.go | 8 ++--- gopls/internal/regtest/misc/imports_test.go | 4 +-- gopls/internal/regtest/misc/link_test.go | 2 +- gopls/internal/regtest/misc/misc_test.go | 4 +-- .../regtest/misc/multiple_adhoc_test.go | 2 +- .../internal/regtest/misc/references_test.go | 2 +- gopls/internal/regtest/misc/rename_test.go | 4 +-- .../regtest/misc/semantictokens_test.go | 4 +-- gopls/internal/regtest/misc/settings_test.go | 2 +- gopls/internal/regtest/misc/shared_test.go | 4 +-- .../internal/regtest/misc/staticcheck_test.go | 2 +- gopls/internal/regtest/misc/vendor_test.go | 4 +-- gopls/internal/regtest/misc/vuln_test.go | 6 ++-- .../regtest/misc/workspace_symbol_test.go | 6 ++-- .../internal/regtest/modfile/modfile_test.go | 8 ++--- .../regtest/template/template_test.go | 6 ++-- gopls/internal/regtest/watch/watch_test.go | 8 ++--- .../internal/regtest/workspace/broken_test.go | 4 +-- .../workspace/directoryfilters_test.go | 2 +- .../regtest/workspace/fromenv_test.go | 2 +- .../regtest/workspace/metadata_test.go | 2 +- .../regtest/workspace/workspace_test.go | 8 ++--- gopls/internal/vulncheck/command.go | 2 +- gopls/internal/vulncheck/command_test.go | 8 ++--- gopls/internal/vulncheck/util.go | 2 +- gopls/internal/vulncheck/vulncheck.go | 2 +- gopls/main.go | 2 +- gopls/release/release.go | 2 +- gopls/test/debug/debug_test.go | 8 ++--- gopls/test/gopls_test.go | 8 ++--- gopls/test/json_test.go | 2 +- internal/{lsp => }/bug/bug.go | 0 internal/{lsp => }/bug/bug_test.go | 0 internal/{lsp => }/diff/diff.go | 0 internal/{lsp => }/diff/diff_test.go | 8 ++--- internal/{lsp => }/diff/difftest/difftest.go | 4 +-- .../{lsp => }/diff/difftest/difftest_test.go | 4 +-- internal/{lsp => }/diff/lcs/common.go | 0 internal/{lsp => }/diff/lcs/common_test.go | 0 internal/{lsp => }/diff/lcs/doc.go | 0 internal/{lsp => }/diff/lcs/git.sh | 0 internal/{lsp => }/diff/lcs/labels.go | 0 internal/{lsp => }/diff/lcs/old.go | 0 internal/{lsp => }/diff/lcs/old_test.go | 4 +-- internal/{lsp => }/diff/myers/diff.go | 2 +- internal/{lsp => }/diff/myers/diff_test.go | 4 +-- internal/{lsp => }/diff/ndiff.go | 2 +- internal/{lsp => }/diff/unified.go | 0 internal/{lsp/debug => event}/tag/tag.go | 0 internal/{lsp => }/fuzzy/input.go | 0 internal/{lsp => }/fuzzy/input_test.go | 2 +- internal/{lsp => }/fuzzy/matcher.go | 0 internal/{lsp => }/fuzzy/matcher_test.go | 2 +- internal/{lsp => }/fuzzy/symbol.go | 0 internal/{lsp => }/fuzzy/symbol_test.go | 2 +- internal/jsonrpc2/conn.go | 2 +- internal/jsonrpc2_v2/conn.go | 2 +- internal/span/token.go | 2 +- 864 files changed, 670 insertions(+), 666 deletions(-) rename {internal => gopls/internal}/lsp/README.md (100%) rename {internal => gopls/internal}/lsp/analysis/embeddirective/embeddirective.go (100%) rename {internal => gopls/internal}/lsp/analysis/embeddirective/embeddirective_test.go (100%) rename {internal => gopls/internal}/lsp/analysis/embeddirective/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/embeddirective/testdata/src/a/b.go (100%) rename {internal => gopls/internal}/lsp/analysis/embeddirective/testdata/src/a/embedText (100%) rename {internal => gopls/internal}/lsp/analysis/fillreturns/fillreturns.go (99%) rename {internal => gopls/internal}/lsp/analysis/fillreturns/fillreturns_test.go (89%) rename {internal => gopls/internal}/lsp/analysis/fillreturns/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/fillreturns/testdata/src/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/fillstruct/fillstruct.go (99%) rename {internal => gopls/internal}/lsp/analysis/fillstruct/fillstruct_test.go (89%) rename {internal => gopls/internal}/lsp/analysis/fillstruct/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/fillstruct/testdata/src/b/b.go (100%) rename {internal => gopls/internal}/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/infertypeargs.go (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/infertypeargs_test.go (90%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/run_go117.go (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/run_go118.go (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/testdata/src/a/basic.go (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/testdata/src/a/imported.go (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go (100%) rename {internal => gopls/internal}/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/nonewvars/nonewvars.go (100%) rename {internal => gopls/internal}/lsp/analysis/nonewvars/nonewvars_test.go (89%) rename {internal => gopls/internal}/lsp/analysis/nonewvars/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/nonewvars/testdata/src/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/nonewvars/testdata/src/typeparams/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/noresultvalues/noresultvalues.go (100%) rename {internal => gopls/internal}/lsp/analysis/noresultvalues/noresultvalues_test.go (89%) rename {internal => gopls/internal}/lsp/analysis/noresultvalues/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/simplifycompositelit/simplifycompositelit.go (100%) rename {internal => gopls/internal}/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go (85%) rename {internal => gopls/internal}/lsp/analysis/simplifycompositelit/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/simplifyrange/simplifyrange.go (100%) rename {internal => gopls/internal}/lsp/analysis/simplifyrange/simplifyrange_test.go (86%) rename {internal => gopls/internal}/lsp/analysis/simplifyrange/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/simplifyslice/simplifyslice.go (100%) rename {internal => gopls/internal}/lsp/analysis/simplifyslice/simplifyslice_test.go (89%) rename {internal => gopls/internal}/lsp/analysis/simplifyslice/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go (100%) rename {internal => gopls/internal}/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/stubmethods/stubmethods.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/channels.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/error_param.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/literals.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/operation.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/selector.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/slice.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/tuple.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/undeclared.go (100%) rename {internal => gopls/internal}/lsp/analysis/undeclaredname/undeclared_test.go (85%) rename {internal => gopls/internal}/lsp/analysis/unusedparams/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/unusedparams/testdata/src/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go (100%) rename {internal => gopls/internal}/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/unusedparams/unusedparams.go (100%) rename {internal => gopls/internal}/lsp/analysis/unusedparams/unusedparams_test.go (89%) rename {internal => gopls/internal}/lsp/analysis/unusedvariable/testdata/src/assign/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/unusedvariable/testdata/src/decl/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/unusedvariable/unusedvariable.go (100%) rename {internal => gopls/internal}/lsp/analysis/unusedvariable/unusedvariable_test.go (89%) rename {internal => gopls/internal}/lsp/analysis/useany/testdata/src/a/a.go (100%) rename {internal => gopls/internal}/lsp/analysis/useany/testdata/src/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/analysis/useany/useany.go (100%) rename {internal => gopls/internal}/lsp/analysis/useany/useany_test.go (89%) rename {internal => gopls/internal}/lsp/browser/README.md (100%) rename {internal => gopls/internal}/lsp/browser/browser.go (100%) rename {internal => gopls/internal}/lsp/cache/analysis.go (99%) rename {internal => gopls/internal}/lsp/cache/cache.go (98%) rename {internal => gopls/internal}/lsp/cache/check.go (99%) rename {internal => gopls/internal}/lsp/cache/debug.go (100%) rename {internal => gopls/internal}/lsp/cache/error_test.go (100%) rename {internal => gopls/internal}/lsp/cache/errors.go (98%) rename {internal => gopls/internal}/lsp/cache/graph.go (98%) rename {internal => gopls/internal}/lsp/cache/imports.go (99%) rename {internal => gopls/internal}/lsp/cache/keys.go (100%) rename {internal => gopls/internal}/lsp/cache/load.go (99%) rename {internal => gopls/internal}/lsp/cache/maps.go (98%) rename {internal => gopls/internal}/lsp/cache/metadata.go (76%) rename {internal => gopls/internal}/lsp/cache/mod.go (98%) rename {internal => gopls/internal}/lsp/cache/mod_tidy.go (98%) rename {internal => gopls/internal}/lsp/cache/os_darwin.go (100%) rename {internal => gopls/internal}/lsp/cache/os_windows.go (100%) rename {internal => gopls/internal}/lsp/cache/parse.go (99%) rename {internal => gopls/internal}/lsp/cache/parse_test.go (100%) rename {internal => gopls/internal}/lsp/cache/parsemode_go116.go (100%) rename {internal => gopls/internal}/lsp/cache/parsemode_go117.go (100%) rename {internal => gopls/internal}/lsp/cache/pkg.go (98%) rename {internal => gopls/internal}/lsp/cache/session.go (99%) rename {internal => gopls/internal}/lsp/cache/snapshot.go (99%) rename {internal => gopls/internal}/lsp/cache/symbols.go (97%) rename {internal => gopls/internal}/lsp/cache/view.go (99%) rename {internal => gopls/internal}/lsp/cache/view_test.go (98%) rename {internal => gopls/internal}/lsp/cache/workspace.go (99%) rename {internal => gopls/internal}/lsp/cache/workspace_test.go (99%) rename {internal => gopls/internal}/lsp/call_hierarchy.go (92%) rename {internal => gopls/internal}/lsp/cmd/call_hierarchy.go (98%) rename {internal => gopls/internal}/lsp/cmd/capabilities_test.go (97%) rename {internal => gopls/internal}/lsp/cmd/check.go (100%) rename {internal => gopls/internal}/lsp/cmd/cmd.go (98%) rename {internal => gopls/internal}/lsp/cmd/cmd_test.go (76%) rename {internal => gopls/internal}/lsp/cmd/definition.go (97%) rename {internal => gopls/internal}/lsp/cmd/export_test.go (100%) rename {internal => gopls/internal}/lsp/cmd/folding_range.go (96%) rename {internal => gopls/internal}/lsp/cmd/format.go (95%) rename {internal => gopls/internal}/lsp/cmd/help_test.go (97%) rename {internal => gopls/internal}/lsp/cmd/highlight.go (97%) rename {internal => gopls/internal}/lsp/cmd/implementation.go (97%) rename {internal => gopls/internal}/lsp/cmd/imports.go (95%) rename {internal => gopls/internal}/lsp/cmd/info.go (98%) rename {internal => gopls/internal}/lsp/cmd/links.go (97%) rename {internal => gopls/internal}/lsp/cmd/prepare_rename.go (97%) rename {internal => gopls/internal}/lsp/cmd/references.go (97%) rename {internal => gopls/internal}/lsp/cmd/remote.go (97%) rename {internal => gopls/internal}/lsp/cmd/rename.go (96%) rename {internal => gopls/internal}/lsp/cmd/semantictokens.go (97%) rename {internal => gopls/internal}/lsp/cmd/serve.go (95%) rename {internal => gopls/internal}/lsp/cmd/signature.go (97%) rename {internal => gopls/internal}/lsp/cmd/subcommands.go (100%) rename {internal => gopls/internal}/lsp/cmd/suggested_fix.go (97%) rename {internal => gopls/internal}/lsp/cmd/symbols.go (98%) rename {internal => gopls/internal}/lsp/cmd/test/call_hierarchy.go (96%) rename {internal => gopls/internal}/lsp/cmd/test/check.go (91%) rename {internal => gopls/internal}/lsp/cmd/test/cmdtest.go (94%) rename {internal => gopls/internal}/lsp/cmd/test/definition.go (96%) rename {internal => gopls/internal}/lsp/cmd/test/folding_range.go (100%) rename {internal => gopls/internal}/lsp/cmd/test/format.go (100%) rename {internal => gopls/internal}/lsp/cmd/test/highlight.go (100%) rename {internal => gopls/internal}/lsp/cmd/test/implementation.go (100%) rename {internal => gopls/internal}/lsp/cmd/test/imports.go (89%) rename {internal => gopls/internal}/lsp/cmd/test/links.go (87%) rename {internal => gopls/internal}/lsp/cmd/test/prepare_rename.go (88%) rename {internal => gopls/internal}/lsp/cmd/test/references.go (100%) rename {internal => gopls/internal}/lsp/cmd/test/rename.go (100%) rename {internal => gopls/internal}/lsp/cmd/test/semanticdriver.go (100%) rename {internal => gopls/internal}/lsp/cmd/test/signature.go (90%) rename {internal => gopls/internal}/lsp/cmd/test/suggested_fix.go (91%) rename {internal => gopls/internal}/lsp/cmd/test/symbols.go (92%) rename {internal => gopls/internal}/lsp/cmd/test/workspace_symbol.go (90%) rename {internal => gopls/internal}/lsp/cmd/usage/api-json.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/bug.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/call_hierarchy.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/check.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/definition.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/fix.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/folding_ranges.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/format.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/help.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/highlight.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/implementation.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/imports.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/inspect.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/licenses.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/links.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/prepare_rename.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/references.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/remote.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/rename.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/semtok.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/serve.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/signature.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/symbols.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/usage.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/version.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/vulncheck.hlp (84%) rename {internal => gopls/internal}/lsp/cmd/usage/workspace.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/usage/workspace_symbol.hlp (100%) rename {internal => gopls/internal}/lsp/cmd/vulncheck.go (95%) rename {internal => gopls/internal}/lsp/cmd/workspace.go (93%) rename {internal => gopls/internal}/lsp/cmd/workspace_symbol.go (95%) rename {internal => gopls/internal}/lsp/code_action.go (98%) rename {internal => gopls/internal}/lsp/code_lens.go (88%) rename {internal => gopls/internal}/lsp/command.go (99%) rename {internal => gopls/internal}/lsp/command/command_gen.go (99%) rename {internal => gopls/internal}/lsp/command/commandmeta/meta.go (98%) rename {internal => gopls/internal}/lsp/command/gen/gen.go (94%) rename {internal => gopls/internal}/lsp/command/generate.go (88%) rename {internal => gopls/internal}/lsp/command/interface.go (99%) rename {internal => gopls/internal}/lsp/command/interface_test.go (92%) rename {internal => gopls/internal}/lsp/command/util.go (100%) rename {internal => gopls/internal}/lsp/completion.go (92%) rename {internal => gopls/internal}/lsp/completion_test.go (97%) rename {internal => gopls/internal}/lsp/debounce.go (100%) rename {internal => gopls/internal}/lsp/debounce_test.go (100%) rename {internal => gopls/internal}/lsp/debug/buildinfo_go1.12.go (100%) rename {internal => gopls/internal}/lsp/debug/buildinfo_go1.18.go (100%) rename {internal => gopls/internal}/lsp/debug/info.go (99%) rename {internal => gopls/internal}/lsp/debug/info_test.go (100%) rename {internal => gopls/internal}/lsp/debug/log/log.go (95%) rename {internal => gopls/internal}/lsp/debug/metrics.go (97%) rename {internal => gopls/internal}/lsp/debug/rpc.go (99%) rename {internal => gopls/internal}/lsp/debug/serve.go (98%) rename {internal => gopls/internal}/lsp/debug/trace.go (100%) rename {internal => gopls/internal}/lsp/definition.go (92%) rename {internal => gopls/internal}/lsp/diagnostics.go (98%) rename {internal => gopls/internal}/lsp/fake/client.go (98%) rename {internal => gopls/internal}/lsp/fake/doc.go (100%) rename {internal => gopls/internal}/lsp/fake/edit.go (98%) rename {internal => gopls/internal}/lsp/fake/edit_test.go (100%) rename {internal => gopls/internal}/lsp/fake/editor.go (99%) rename {internal => gopls/internal}/lsp/fake/editor_test.go (100%) rename {internal => gopls/internal}/lsp/fake/proxy.go (100%) rename {internal => gopls/internal}/lsp/fake/sandbox.go (100%) rename {internal => gopls/internal}/lsp/fake/workdir.go (99%) rename {internal => gopls/internal}/lsp/fake/workdir_test.go (98%) rename {internal => gopls/internal}/lsp/fake/workdir_windows.go (100%) rename {internal => gopls/internal}/lsp/folding_range.go (92%) rename {internal => gopls/internal}/lsp/format.go (78%) rename {internal => gopls/internal}/lsp/general.go (98%) rename {internal => gopls/internal}/lsp/helper/README.md (100%) rename {internal => gopls/internal}/lsp/helper/helper.go (99%) rename {internal => gopls/internal}/lsp/highlight.go (85%) rename {internal => gopls/internal}/lsp/hover.go (77%) rename {internal => gopls/internal}/lsp/implementation.go (84%) rename {internal => gopls/internal}/lsp/inlay_hint.go (83%) rename {internal => gopls/internal}/lsp/link.go (98%) rename {internal => gopls/internal}/lsp/lsp_test.go (99%) rename {internal => gopls/internal}/lsp/lsppos/lsppos.go (98%) rename {internal => gopls/internal}/lsp/lsppos/lsppos_test.go (96%) rename {internal => gopls/internal}/lsp/lsppos/token.go (94%) rename {internal => gopls/internal}/lsp/lsppos/token_test.go (93%) rename {internal => gopls/internal}/lsp/lsprpc/autostart_default.go (100%) rename {internal => gopls/internal}/lsp/lsprpc/autostart_posix.go (100%) rename {internal => gopls/internal}/lsp/lsprpc/binder.go (98%) rename {internal => gopls/internal}/lsp/lsprpc/binder_test.go (97%) rename {internal => gopls/internal}/lsp/lsprpc/commandinterceptor.go (96%) rename {internal => gopls/internal}/lsp/lsprpc/commandinterceptor_test.go (90%) rename {internal => gopls/internal}/lsp/lsprpc/dialer.go (100%) rename {internal => gopls/internal}/lsp/lsprpc/goenv.go (98%) rename {internal => gopls/internal}/lsp/lsprpc/goenv_test.go (94%) rename {internal => gopls/internal}/lsp/lsprpc/lsprpc.go (98%) rename {internal => gopls/internal}/lsp/lsprpc/lsprpc_test.go (98%) rename {internal => gopls/internal}/lsp/lsprpc/middleware.go (100%) rename {internal => gopls/internal}/lsp/lsprpc/middleware_test.go (97%) rename {internal => gopls/internal}/lsp/mod/code_lens.go (97%) rename {internal => gopls/internal}/lsp/mod/diagnostics.go (94%) rename {internal => gopls/internal}/lsp/mod/format.go (89%) rename {internal => gopls/internal}/lsp/mod/hover.go (97%) rename {internal => gopls/internal}/lsp/mod/mod_test.go (91%) rename {internal => gopls/internal}/lsp/mod/testdata/unchanged/go.mod (100%) rename {internal => gopls/internal}/lsp/mod/testdata/unchanged/main.go (100%) rename {internal => gopls/internal}/lsp/progress/progress.go (98%) rename {internal => gopls/internal}/lsp/progress/progress_test.go (98%) rename {internal => gopls/internal}/lsp/protocol/codeactionkind.go (100%) rename {internal => gopls/internal}/lsp/protocol/context.go (100%) rename {internal => gopls/internal}/lsp/protocol/doc.go (100%) rename {internal => gopls/internal}/lsp/protocol/enums.go (100%) rename {internal => gopls/internal}/lsp/protocol/log.go (100%) rename {internal => gopls/internal}/lsp/protocol/protocol.go (100%) rename {internal => gopls/internal}/lsp/protocol/span.go (100%) rename {internal => gopls/internal}/lsp/protocol/tsclient.go (100%) rename {internal => gopls/internal}/lsp/protocol/tsdocument_changes.go (100%) rename {internal => gopls/internal}/lsp/protocol/tsprotocol.go (100%) rename {internal => gopls/internal}/lsp/protocol/tsserver.go (100%) rename {internal => gopls/internal}/lsp/protocol/typescript/README.md (100%) rename {internal => gopls/internal}/lsp/protocol/typescript/code.ts (100%) rename {internal => gopls/internal}/lsp/protocol/typescript/tsconfig.json (100%) rename {internal => gopls/internal}/lsp/protocol/typescript/util.ts (100%) rename {internal => gopls/internal}/lsp/references.go (87%) rename {internal => gopls/internal}/lsp/regtest/doc.go (95%) rename {internal => gopls/internal}/lsp/regtest/env.go (99%) rename {internal => gopls/internal}/lsp/regtest/env_test.go (97%) rename {internal => gopls/internal}/lsp/regtest/expectation.go (99%) rename {internal => gopls/internal}/lsp/regtest/regtest.go (97%) rename {internal => gopls/internal}/lsp/regtest/runner.go (98%) rename {internal => gopls/internal}/lsp/regtest/wrappers.go (98%) rename {internal => gopls/internal}/lsp/rename.go (94%) rename {internal => gopls/internal}/lsp/reset_golden.sh (100%) rename {internal => gopls/internal}/lsp/safetoken/safetoken.go (100%) rename {internal => gopls/internal}/lsp/safetoken/safetoken_test.go (84%) rename {internal => gopls/internal}/lsp/semantic.go (99%) rename {internal => gopls/internal}/lsp/server.go (97%) rename {internal => gopls/internal}/lsp/server_gen.go (99%) rename {internal => gopls/internal}/lsp/signature_help.go (85%) rename {internal => gopls/internal}/lsp/snippet/snippet_builder.go (100%) rename {internal => gopls/internal}/lsp/snippet/snippet_builder_test.go (100%) rename {internal => gopls/internal}/lsp/source/add_import.go (93%) rename {internal => gopls/internal}/lsp/source/api_json.go (100%) rename {internal => gopls/internal}/lsp/source/call_hierarchy.go (99%) rename {internal => gopls/internal}/lsp/source/code_lens.go (98%) rename {internal => gopls/internal}/lsp/source/comment.go (100%) rename {internal => gopls/internal}/lsp/source/comment_go118.go (100%) rename {internal => gopls/internal}/lsp/source/comment_go118_test.go (100%) rename {internal => gopls/internal}/lsp/source/completion/builtin.go (100%) rename {internal => gopls/internal}/lsp/source/completion/completion.go (99%) rename {internal => gopls/internal}/lsp/source/completion/deep_completion.go (100%) rename {internal => gopls/internal}/lsp/source/completion/deep_completion_test.go (100%) rename {internal => gopls/internal}/lsp/source/completion/definition.go (95%) rename {internal => gopls/internal}/lsp/source/completion/format.go (97%) rename {internal => gopls/internal}/lsp/source/completion/fuzz.go (98%) rename {internal => gopls/internal}/lsp/source/completion/keywords.go (97%) rename {internal => gopls/internal}/lsp/source/completion/labels.go (100%) rename {internal => gopls/internal}/lsp/source/completion/literal.go (99%) rename {internal => gopls/internal}/lsp/source/completion/package.go (98%) rename {internal => gopls/internal}/lsp/source/completion/package_test.go (100%) rename {internal => gopls/internal}/lsp/source/completion/postfix_snippets.go (98%) rename {internal => gopls/internal}/lsp/source/completion/printf.go (100%) rename {internal => gopls/internal}/lsp/source/completion/printf_test.go (100%) rename {internal => gopls/internal}/lsp/source/completion/snippet.go (98%) rename {internal => gopls/internal}/lsp/source/completion/statements.go (98%) rename {internal => gopls/internal}/lsp/source/completion/util.go (98%) rename {internal => gopls/internal}/lsp/source/completion/util_test.go (100%) rename {internal => gopls/internal}/lsp/source/diagnostics.go (97%) rename {internal => gopls/internal}/lsp/source/extract.go (99%) rename {internal => gopls/internal}/lsp/source/fix.go (95%) rename {internal => gopls/internal}/lsp/source/folding_range.go (99%) rename {internal => gopls/internal}/lsp/source/format.go (98%) rename {internal => gopls/internal}/lsp/source/format_test.go (97%) rename {internal => gopls/internal}/lsp/source/gc_annotations.go (99%) rename {internal => gopls/internal}/lsp/source/highlight.go (99%) rename {internal => gopls/internal}/lsp/source/hover.go (99%) rename {internal => gopls/internal}/lsp/source/identifier.go (99%) rename {internal => gopls/internal}/lsp/source/identifier_test.go (100%) rename {internal => gopls/internal}/lsp/source/implementation.go (99%) rename {internal => gopls/internal}/lsp/source/inlay_hint.go (99%) rename {internal => gopls/internal}/lsp/source/known_packages.go (100%) rename {internal => gopls/internal}/lsp/source/options.go (97%) rename {internal => gopls/internal}/lsp/source/options_test.go (100%) rename {internal => gopls/internal}/lsp/source/references.go (98%) rename {internal => gopls/internal}/lsp/source/rename.go (99%) rename {internal => gopls/internal}/lsp/source/rename_check.go (100%) rename {internal => gopls/internal}/lsp/source/signature_help.go (99%) rename {internal => gopls/internal}/lsp/source/source_test.go (98%) rename {internal => gopls/internal}/lsp/source/stub.go (98%) rename {internal => gopls/internal}/lsp/source/symbols.go (99%) rename {internal => gopls/internal}/lsp/source/types_format.go (99%) rename {internal => gopls/internal}/lsp/source/util.go (99%) rename {internal => gopls/internal}/lsp/source/util_test.go (97%) rename {internal => gopls/internal}/lsp/source/view.go (99%) rename {internal => gopls/internal}/lsp/source/workspace_symbol.go (99%) rename {internal => gopls/internal}/lsp/source/workspace_symbol_test.go (100%) rename {internal => gopls/internal}/lsp/symbols.go (89%) rename {internal => gopls/internal}/lsp/template/completion.go (98%) rename {internal => gopls/internal}/lsp/template/completion_test.go (98%) rename {internal => gopls/internal}/lsp/template/highlight.go (96%) rename {internal => gopls/internal}/lsp/template/implementations.go (98%) rename {internal => gopls/internal}/lsp/template/parse.go (99%) rename {internal => gopls/internal}/lsp/template/parse_test.go (100%) rename {internal => gopls/internal}/lsp/template/symbols.go (98%) rename {internal => gopls/internal}/lsp/testdata/%percent/perc%ent.go (100%) rename {internal => gopls/internal}/lsp/testdata/addimport/addimport.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/addimport/addimport.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/address/address.go (100%) rename {internal => gopls/internal}/lsp/testdata/analyzer/bad_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/anon/anon.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/append/append.go (100%) rename {internal => gopls/internal}/lsp/testdata/append/append2.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/arraytype/array_type.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/assign/assign.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/assign/internal/secret/secret.go (100%) rename {internal => gopls/internal}/lsp/testdata/bad/bad0.go (100%) rename {internal => gopls/internal}/lsp/testdata/bad/bad1.go (100%) rename {internal => gopls/internal}/lsp/testdata/badstmt/badstmt.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/badstmt/badstmt_2.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/badstmt/badstmt_3.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/badstmt/badstmt_4.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/bar/bar.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/basiclit/basiclit.go (100%) rename {internal => gopls/internal}/lsp/testdata/baz/baz.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/builtins/builtin_args.go (100%) rename {internal => gopls/internal}/lsp/testdata/builtins/builtin_types.go (100%) rename {internal => gopls/internal}/lsp/testdata/builtins/builtins.go (100%) rename {internal => gopls/internal}/lsp/testdata/builtins/constants.go (100%) rename {internal => gopls/internal}/lsp/testdata/callhierarchy/callhierarchy.go (100%) rename {internal => gopls/internal}/lsp/testdata/callhierarchy/incoming/incoming.go (100%) rename {internal => gopls/internal}/lsp/testdata/callhierarchy/outgoing/outgoing.go (100%) rename {internal => gopls/internal}/lsp/testdata/casesensitive/casesensitive.go (100%) rename {internal => gopls/internal}/lsp/testdata/cast/cast.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/cgo/declarecgo.go (100%) rename {internal => gopls/internal}/lsp/testdata/cgo/declarecgo.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/cgo/declarecgo_nocgo.go (100%) rename {internal => gopls/internal}/lsp/testdata/cgoimport/usecgo.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/cgoimport/usecgo.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/channel/channel.go (100%) rename {internal => gopls/internal}/lsp/testdata/codelens/codelens_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/comment_completion/comment_completion.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/complit/complit.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/constant/constant.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_for.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_for_init.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_for_init_cond.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_if.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_if_eof.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_if_init.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_if_init_cond.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_multiline_if.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_selector_1.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_selector_2.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_switch_init.go (100%) rename {internal => gopls/internal}/lsp/testdata/danglingstmt/dangling_switch_init_tag.go (100%) rename {internal => gopls/internal}/lsp/testdata/deep/deep.go (100%) rename {internal => gopls/internal}/lsp/testdata/errors/errors.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_args_returns.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_args_returns.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_basic.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_basic.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_basic_comment.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_issue_44813.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_issue_44813.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_redefine.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_redefine.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_basic.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_basic.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_complex.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_complex.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_func_lit.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_func_lit.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_init.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_init.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_scope.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_scope.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_smart_initialization.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_smart_initialization.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_smart_return.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_smart_return.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_unnecessary_param.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_function/extract_unnecessary_param.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_method/extract_basic.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_method/extract_basic.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_variable/extract_basic_lit.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_variable/extract_func_call.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_variable/extract_func_call.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_variable/extract_scope.go (100%) rename {internal => gopls/internal}/lsp/testdata/extract/extract_variable/extract_scope.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fieldlist/field_list.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/a.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/a.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/a2.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/a2.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/a3.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/a3.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/a4.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/a4.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/data/a.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_anon.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_anon.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_nested.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_nested.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_package.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_package.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_partial.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_partial.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_spaces.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_spaces.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_unsafe.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/typeparams.go (100%) rename {internal => gopls/internal}/lsp/testdata/fillstruct/typeparams.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/folding/a.go (100%) rename {internal => gopls/internal}/lsp/testdata/folding/a.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/folding/bad.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/folding/bad.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/foo/foo.go (100%) rename {internal => gopls/internal}/lsp/testdata/format/bad_format.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/format/bad_format.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/format/good_format.go (100%) rename {internal => gopls/internal}/lsp/testdata/format/good_format.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/format/newline_format.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/format/newline_format.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/format/one_line.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/format/one_line.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/func_rank/func_rank.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/funcsig/func_sig.go (100%) rename {internal => gopls/internal}/lsp/testdata/funcvalue/func_value.go (100%) rename {internal => gopls/internal}/lsp/testdata/fuzzymatch/fuzzymatch.go (100%) rename {internal => gopls/internal}/lsp/testdata/generate/generate.go (100%) rename {internal => gopls/internal}/lsp/testdata/generated/generated.go (100%) rename {internal => gopls/internal}/lsp/testdata/generated/generator.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/a.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/a_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/a_test.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/a_x_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/a_x_test.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/d.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/d.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/f.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/f.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/g.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/g.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/h.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/h.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/random.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/a/random.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/b/b.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/b/b.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/b/c.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/b/c.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/b/c.go.saved (100%) rename {internal => gopls/internal}/lsp/testdata/godef/b/e.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/b/e.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/b/h.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/b/h.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/broken/unclosedIf.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/broken/unclosedIf.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/godef/hover_generics/hover.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/hover_generics/hover.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/godef/infer_generics/inferred.go (100%) rename {internal => gopls/internal}/lsp/testdata/godef/infer_generics/inferred.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/good/good0.go (100%) rename {internal => gopls/internal}/lsp/testdata/good/good1.go (100%) rename {internal => gopls/internal}/lsp/testdata/highlights/highlights.go (100%) rename {internal => gopls/internal}/lsp/testdata/implementation/implementation.go (100%) rename {internal => gopls/internal}/lsp/testdata/implementation/other/other.go (100%) rename {internal => gopls/internal}/lsp/testdata/implementation/other/other_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/importedcomplit/imported_complit.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/imports/add_import.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/imports/add_import.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/imports/good_imports.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/imports/good_imports.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/imports/issue35458.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/imports/issue35458.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/imports/multiple_blocks.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/imports/multiple_blocks.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/imports/needs_imports.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/imports/needs_imports.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/imports/remove_import.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/imports/remove_import.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/imports/remove_imports.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/imports/remove_imports.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/imports/two_lines.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/imports/two_lines.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/index/index.go (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/composite_literals.go (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/composite_literals.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/constant_values.go (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/constant_values.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/parameter_names.go (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/parameter_names.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/type_params.go (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/type_params.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/variable_types.go (100%) rename {internal => gopls/internal}/lsp/testdata/inlay_hint/variable_types.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/interfacerank/interface_rank.go (100%) rename {internal => gopls/internal}/lsp/testdata/keywords/accidental_keywords.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/keywords/empty_select.go (100%) rename {internal => gopls/internal}/lsp/testdata/keywords/empty_switch.go (100%) rename {internal => gopls/internal}/lsp/testdata/keywords/keywords.go (100%) rename {internal => gopls/internal}/lsp/testdata/labels/labels.go (100%) rename {internal => gopls/internal}/lsp/testdata/links/links.go (100%) rename {internal => gopls/internal}/lsp/testdata/maps/maps.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/channels.go (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/channels.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/consecutive_params.go (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/consecutive_params.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/error_param.go (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/error_param.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/literals.go (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/literals.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/operation.go (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/operation.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/selector.go (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/selector.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/slice.go (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/slice.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/tuple.go (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/tuple.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/unique_params.go (100%) rename {internal => gopls/internal}/lsp/testdata/missingfunction/unique_params.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/multireturn/multi_return.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/nested_complit/nested_complit.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/nodisk/empty (100%) rename {internal => gopls/internal}/lsp/testdata/nodisk/nodisk.overlay.go (100%) rename {internal => gopls/internal}/lsp/testdata/noparse/noparse.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/noparse_format/noparse_format.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/noparse_format/noparse_format.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/noparse_format/parse_format.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/noparse_format/parse_format.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/printf/printf.go (100%) rename {internal => gopls/internal}/lsp/testdata/rank/assign_rank.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rank/binexpr_rank.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rank/boolexpr_rank.go (100%) rename {internal => gopls/internal}/lsp/testdata/rank/convert_rank.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rank/struct/struct_rank.go (100%) rename {internal => gopls/internal}/lsp/testdata/rank/switch_rank.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rank/type_assert_rank.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rank/type_switch_rank.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/references/another/another.go (100%) rename {internal => gopls/internal}/lsp/testdata/references/interfaces/interfaces.go (100%) rename {internal => gopls/internal}/lsp/testdata/references/other/other.go (100%) rename {internal => gopls/internal}/lsp/testdata/references/refs.go (100%) rename {internal => gopls/internal}/lsp/testdata/references/refs_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/a/random.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/a/random.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rename/b/b.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/b/b.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/bad/bad.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/bad/bad.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rename/bad/bad_test.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rename/c/c.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/c/c.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/c/c2.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/c/c2.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/crosspkg/another/another.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/crosspkg/another/another.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/crosspkg/crosspkg.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/crosspkg/crosspkg.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/crosspkg/other/other.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/crosspkg/other/other.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/generics/embedded.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/generics/embedded.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/generics/generics.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/generics/generics.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/generics/unions.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/generics/unions.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue39614/issue39614.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue39614/issue39614.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue42134/1.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue42134/1.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue42134/2.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue42134/2.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue42134/3.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue42134/3.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue42134/4.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue42134/4.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue43616/issue43616.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/issue43616/issue43616.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/rename/shadow/shadow.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/shadow/shadow.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/testy/testy.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/testy/testy.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rename/testy/testy_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/rename/testy/testy_test.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/rundespiteerrors/rundespiteerrors.go (100%) rename {internal => gopls/internal}/lsp/testdata/selector/selector.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/semantic/README.md (100%) rename {internal => gopls/internal}/lsp/testdata/semantic/a.go (100%) rename {internal => gopls/internal}/lsp/testdata/semantic/a.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/semantic/b.go (100%) rename {internal => gopls/internal}/lsp/testdata/semantic/b.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/semantic/semantic_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/signature/signature.go (100%) rename {internal => gopls/internal}/lsp/testdata/signature/signature.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/signature/signature2.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/signature/signature2.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/signature/signature3.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/signature/signature3.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/signature/signature_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/signature/signature_test.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/snippets/func_snippets118.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/snippets/literal.go (100%) rename {internal => gopls/internal}/lsp/testdata/snippets/literal.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/snippets/literal_snippets.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/snippets/literal_snippets118.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/snippets/postfix.go (100%) rename {internal => gopls/internal}/lsp/testdata/snippets/snippets.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/snippets/snippets.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/statements/append.go (100%) rename {internal => gopls/internal}/lsp/testdata/statements/if_err_check_return.go (100%) rename {internal => gopls/internal}/lsp/testdata/statements/if_err_check_return_2.go (100%) rename {internal => gopls/internal}/lsp/testdata/statements/if_err_check_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/other/other.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_add_selector.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_add_selector.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_assign.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_assign.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_assign_multivars.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_assign_multivars.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_call_expr.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_call_expr.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_embedded.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_embedded.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_err.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_err.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_function_return.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_function_return.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_generic_receiver.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_generic_receiver.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_ignored_imports.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_ignored_imports.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_multi_var.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_multi_var.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_pointer.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_pointer.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_renamed_import.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_renamed_import.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_renamed_import_iface.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_renamed_import_iface.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_stdlib.go (100%) rename {internal => gopls/internal}/lsp/testdata/stub/stub_stdlib.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/suggestedfix/has_suggested_fix.go (100%) rename {internal => gopls/internal}/lsp/testdata/suggestedfix/has_suggested_fix.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/summary.txt.golden (100%) rename {internal => gopls/internal}/lsp/testdata/summary_go1.18.txt.golden (100%) rename {internal => gopls/internal}/lsp/testdata/symbols/main.go (100%) rename {internal => gopls/internal}/lsp/testdata/symbols/main.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/testy/testy.go (100%) rename {internal => gopls/internal}/lsp/testdata/testy/testy_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/testy/testy_test.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/typdef/typdef.go (100%) rename {internal => gopls/internal}/lsp/testdata/typeassert/type_assert.go (100%) rename {internal => gopls/internal}/lsp/testdata/typeerrors/noresultvalues.go (100%) rename {internal => gopls/internal}/lsp/testdata/typeerrors/noresultvalues.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/typemods/type_mods.go (100%) rename {internal => gopls/internal}/lsp/testdata/typeparams/type_params.go (100%) rename {internal => gopls/internal}/lsp/testdata/types/types.go (100%) rename {internal => gopls/internal}/lsp/testdata/undeclared/var.go (100%) rename {internal => gopls/internal}/lsp/testdata/undeclared/var.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/unimported/export_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/unimported/unimported.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/unimported/unimported_cand_type.go (100%) rename {internal => gopls/internal}/lsp/testdata/unimported/x_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/unresolved/unresolved.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/unsafe/unsafe.go (100%) rename {internal => gopls/internal}/lsp/testdata/variadic/variadic.go.in (100%) rename {internal => gopls/internal}/lsp/testdata/variadic/variadic_intf.go (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/a/a.go (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/a/a.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/a/a_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/a/a_test.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/a/a_x_test.go (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/a/a_x_test.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/b/b.go (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/b/b.go.golden (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/issue44806.go (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/main.go (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/p/p.go (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/query.go (100%) rename {internal => gopls/internal}/lsp/testdata/workspacesymbol/query.go.golden (100%) rename {internal => gopls/internal}/lsp/tests/README.md (95%) rename {internal => gopls/internal}/lsp/tests/compare/text.go (96%) rename {internal => gopls/internal}/lsp/tests/compare/text_test.go (92%) rename {internal => gopls/internal}/lsp/tests/markdown_go118.go (96%) rename {internal => gopls/internal}/lsp/tests/markdown_go119.go (92%) rename {internal => gopls/internal}/lsp/tests/normalizer.go (100%) rename {internal => gopls/internal}/lsp/tests/tests.go (99%) rename {internal => gopls/internal}/lsp/tests/util.go (98%) rename {internal => gopls/internal}/lsp/text_synchronization.go (99%) rename {internal => gopls/internal}/lsp/work/completion.go (97%) rename {internal => gopls/internal}/lsp/work/diagnostics.go (94%) rename {internal => gopls/internal}/lsp/work/format.go (89%) rename {internal => gopls/internal}/lsp/work/hover.go (96%) rename {internal => gopls/internal}/lsp/workspace.go (97%) rename {internal => gopls/internal}/lsp/workspace_symbol.go (86%) rename internal/{lsp => }/bug/bug.go (100%) rename internal/{lsp => }/bug/bug_test.go (100%) rename internal/{lsp => }/diff/diff.go (100%) rename internal/{lsp => }/diff/diff_test.go (93%) rename internal/{lsp => }/diff/difftest/difftest.go (98%) rename internal/{lsp => }/diff/difftest/difftest_test.go (95%) rename internal/{lsp => }/diff/lcs/common.go (100%) rename internal/{lsp => }/diff/lcs/common_test.go (100%) rename internal/{lsp => }/diff/lcs/doc.go (100%) rename internal/{lsp => }/diff/lcs/git.sh (100%) rename internal/{lsp => }/diff/lcs/labels.go (100%) rename internal/{lsp => }/diff/lcs/old.go (100%) rename internal/{lsp => }/diff/lcs/old_test.go (95%) rename internal/{lsp => }/diff/myers/diff.go (99%) rename internal/{lsp => }/diff/myers/diff_test.go (74%) rename internal/{lsp => }/diff/ndiff.go (98%) rename internal/{lsp => }/diff/unified.go (100%) rename internal/{lsp/debug => event}/tag/tag.go (100%) rename internal/{lsp => }/fuzzy/input.go (100%) rename internal/{lsp => }/fuzzy/input_test.go (98%) rename internal/{lsp => }/fuzzy/matcher.go (100%) rename internal/{lsp => }/fuzzy/matcher_test.go (99%) rename internal/{lsp => }/fuzzy/symbol.go (100%) rename internal/{lsp => }/fuzzy/symbol_test.go (97%) diff --git a/copyright/copyright.go b/copyright/copyright.go index eb56ef28b22..db63c59922e 100644 --- a/copyright/copyright.go +++ b/copyright/copyright.go @@ -94,7 +94,7 @@ func checkFile(toolsDir, filename string) (bool, error) { return shouldAddCopyright, nil } -// Copied from golang.org/x/tools/internal/lsp/source/util.go. +// Copied from golang.org/x/tools/gopls/internal/lsp/source/util.go. // Matches cgo generated comment as well as the proposed standard: // // https://golang.org/s/generatedcode diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index ea67807f78c..1fd0a84603f 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -24,8 +24,8 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/internal/checker" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" diff --git a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go index 167bdbd1b9f..8e6c09e275f 100644 --- a/gopls/api-diff/api_diff.go +++ b/gopls/api-diff/api_diff.go @@ -21,10 +21,10 @@ import ( "path/filepath" "strings" + "golang.org/x/tools/gopls/internal/lsp/source" + difflib "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/gocommand" - difflib "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/source" ) var ( diff --git a/gopls/doc/contributing.md b/gopls/doc/contributing.md index 99e45292296..110aa1bdc84 100644 --- a/gopls/doc/contributing.md +++ b/gopls/doc/contributing.md @@ -18,7 +18,7 @@ claiming it. ## Getting started -Most of the `gopls` logic is actually in the `golang.org/x/tools/internal/lsp` +Most of the `gopls` logic is actually in the `golang.org/x/tools/gopls/internal/lsp` directory, so you are most likely to develop in the golang.org/x/tools module. ## Build diff --git a/gopls/doc/design/integrating.md b/gopls/doc/design/integrating.md index 845f9eb007f..7937ba844ca 100644 --- a/gopls/doc/design/integrating.md +++ b/gopls/doc/design/integrating.md @@ -61,8 +61,8 @@ For instance, files that are needed to do correct type checking are modified by Monitoring files inside gopls directly has a lot of awkward problems, but the [LSP specification] has methods that allow gopls to request that the client notify it of file system changes, specifically [`workspace/didChangeWatchedFiles`]. This is currently being added to gopls by a community member, and tracked in [#31553] -[InitializeResult]: https://pkg.go.dev/golang.org/x/tools/internal/lsp/protocol#InitializeResult -[ServerCapabilities]: https://pkg.go.dev/golang.org/x/tools/internal/lsp/protocol#ServerCapabilities +[InitializeResult]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#InitializeResult +[ServerCapabilities]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#ServerCapabilities [`golang.org/x/tools/internal/span`]: https://pkg.go.dev/golang.org/x/tools/internal/span#NewPoint [LSP specification]: https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/ diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go index 28e0444a467..449751e8e02 100644 --- a/gopls/doc/generate.go +++ b/gopls/doc/generate.go @@ -33,10 +33,10 @@ import ( "github.com/jba/printsrc" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/command/commandmeta" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/command/commandmeta" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/source" ) func main() { @@ -52,7 +52,7 @@ func doMain(write bool) (bool, error) { return false, err } - sourceDir, err := pkgDir("golang.org/x/tools/internal/lsp/source") + sourceDir, err := pkgDir("golang.org/x/tools/gopls/internal/lsp/source") if err != nil { return false, err } @@ -96,7 +96,7 @@ func loadAPI() (*source.APIJSON, error) { &packages.Config{ Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps, }, - "golang.org/x/tools/internal/lsp/source", + "golang.org/x/tools/gopls/internal/lsp/source", ) if err != nil { return nil, err @@ -587,7 +587,7 @@ func rewriteFile(file string, api *source.APIJSON, write bool, rewrite func([]by func rewriteAPI(_ []byte, api *source.APIJSON) ([]byte, error) { var buf bytes.Buffer fmt.Fprintf(&buf, "// Code generated by \"golang.org/x/tools/gopls/doc/generate\"; DO NOT EDIT.\n\npackage source\n\nvar GeneratedAPIJSON = ") - if err := printsrc.NewPrinter("golang.org/x/tools/internal/lsp/source").Fprint(&buf, api); err != nil { + if err := printsrc.NewPrinter("golang.org/x/tools/gopls/internal/lsp/source").Fprint(&buf, api); err != nil { return nil, err } return format.Source(buf.Bytes()) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 43dd54b074a..04124687e9d 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -1,6 +1,6 @@ # Settings - + This document describes the global settings for `gopls` inside the editor. The settings block will be called `"gopls"` and contains a collection of diff --git a/gopls/internal/coverage/coverage.go b/gopls/internal/coverage/coverage.go index 7bb3640bdbd..1ceabefab58 100644 --- a/gopls/internal/coverage/coverage.go +++ b/gopls/internal/coverage/coverage.go @@ -12,9 +12,13 @@ // -o controls where the coverage file is written, defaulting to /tmp/cover.out // -i coverage-file will generate the report from an existing coverage file // -v controls verbosity (0: only report coverage, 1: report as each directory is finished, -// 2: report on each test, 3: more details, 4: too much) +// +// 2: report on each test, 3: more details, 4: too much) +// // -t tests only tests packages in the given comma-separated list of directories in gopls. -// The names should start with ., as in ./internal/regtest/bench +// +// The names should start with ., as in ./internal/regtest/bench +// // -run tests. If set, -run tests is passed on to the go test command. // // Despite gopls' use of goroutines, the counts are almost deterministic. @@ -60,7 +64,7 @@ func main() { tests = realTestName(tests) // report coverage for packages under internal/lsp - parg := "golang.org/x/tools/internal/lsp/..." + parg := "golang.org/x/tools/gopls/internal/lsp/..." accum := []string{} seen := make(map[string]bool) diff --git a/gopls/internal/hooks/analysis.go b/gopls/internal/hooks/analysis.go index 51048991d5a..27ab9a699f9 100644 --- a/gopls/internal/hooks/analysis.go +++ b/gopls/internal/hooks/analysis.go @@ -8,8 +8,8 @@ package hooks import ( - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "honnef.co/go/tools/analysis/lint" "honnef.co/go/tools/quickfix" "honnef.co/go/tools/simple" diff --git a/gopls/internal/hooks/analysis_117.go b/gopls/internal/hooks/analysis_117.go index 02f9170ab63..dd429dea898 100644 --- a/gopls/internal/hooks/analysis_117.go +++ b/gopls/internal/hooks/analysis_117.go @@ -7,7 +7,7 @@ package hooks -import "golang.org/x/tools/internal/lsp/source" +import "golang.org/x/tools/gopls/internal/lsp/source" func updateAnalyzers(options *source.Options) { options.StaticcheckSupported = false diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go index e0461a152bb..01a37aed4b8 100644 --- a/gopls/internal/hooks/diff.go +++ b/gopls/internal/hooks/diff.go @@ -20,7 +20,7 @@ import ( "unicode" "github.com/sergi/go-diff/diffmatchpatch" - "golang.org/x/tools/internal/lsp/diff" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/span" ) diff --git a/gopls/internal/hooks/diff_test.go b/gopls/internal/hooks/diff_test.go index a9e536728b0..acc5d29991f 100644 --- a/gopls/internal/hooks/diff_test.go +++ b/gopls/internal/hooks/diff_test.go @@ -11,7 +11,7 @@ import ( "testing" "unicode/utf8" - "golang.org/x/tools/internal/lsp/diff/difftest" + "golang.org/x/tools/internal/diff/difftest" ) func TestDiff(t *testing.T) { diff --git a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go index b55917e0737..9d497e9ed15 100644 --- a/gopls/internal/hooks/hooks.go +++ b/gopls/internal/hooks/hooks.go @@ -10,9 +10,9 @@ package hooks // import "golang.org/x/tools/gopls/internal/hooks" import ( "context" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/vulncheck" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/diff" "mvdan.cc/gofumpt/format" "mvdan.cc/xurls/v2" ) diff --git a/internal/lsp/README.md b/gopls/internal/lsp/README.md similarity index 100% rename from internal/lsp/README.md rename to gopls/internal/lsp/README.md diff --git a/internal/lsp/analysis/embeddirective/embeddirective.go b/gopls/internal/lsp/analysis/embeddirective/embeddirective.go similarity index 100% rename from internal/lsp/analysis/embeddirective/embeddirective.go rename to gopls/internal/lsp/analysis/embeddirective/embeddirective.go diff --git a/internal/lsp/analysis/embeddirective/embeddirective_test.go b/gopls/internal/lsp/analysis/embeddirective/embeddirective_test.go similarity index 100% rename from internal/lsp/analysis/embeddirective/embeddirective_test.go rename to gopls/internal/lsp/analysis/embeddirective/embeddirective_test.go diff --git a/internal/lsp/analysis/embeddirective/testdata/src/a/a.go b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/embeddirective/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/embeddirective/testdata/src/a/a.go diff --git a/internal/lsp/analysis/embeddirective/testdata/src/a/b.go b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/b.go similarity index 100% rename from internal/lsp/analysis/embeddirective/testdata/src/a/b.go rename to gopls/internal/lsp/analysis/embeddirective/testdata/src/a/b.go diff --git a/internal/lsp/analysis/embeddirective/testdata/src/a/embedText b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/embedText similarity index 100% rename from internal/lsp/analysis/embeddirective/testdata/src/a/embedText rename to gopls/internal/lsp/analysis/embeddirective/testdata/src/a/embedText diff --git a/internal/lsp/analysis/fillreturns/fillreturns.go b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go similarity index 99% rename from internal/lsp/analysis/fillreturns/fillreturns.go rename to gopls/internal/lsp/analysis/fillreturns/fillreturns.go index 8be83adb140..d71defb3b83 100644 --- a/internal/lsp/analysis/fillreturns/fillreturns.go +++ b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go @@ -19,7 +19,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/lsp/fuzzy" + "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/fillreturns/fillreturns_test.go b/gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go similarity index 89% rename from internal/lsp/analysis/fillreturns/fillreturns_test.go rename to gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go index 7ef0d46792e..1f7627551a0 100644 --- a/internal/lsp/analysis/fillreturns/fillreturns_test.go +++ b/gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/fillreturns" + "golang.org/x/tools/gopls/internal/lsp/analysis/fillreturns" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/fillreturns/testdata/src/a/a.go b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/fillreturns/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go diff --git a/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go similarity index 100% rename from internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go rename to gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go diff --git a/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden similarity index 100% rename from internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden rename to gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden diff --git a/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go similarity index 99% rename from internal/lsp/analysis/fillstruct/fillstruct.go rename to gopls/internal/lsp/analysis/fillstruct/fillstruct.go index 2c0084ff6f5..aaa3075f6c2 100644 --- a/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -21,7 +21,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/lsp/fuzzy" + "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/fillstruct/fillstruct_test.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go similarity index 89% rename from internal/lsp/analysis/fillstruct/fillstruct_test.go rename to gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go index 51a516cdfdb..66642b7ab59 100644 --- a/internal/lsp/analysis/fillstruct/fillstruct_test.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/fillstruct" + "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/fillstruct/testdata/src/a/a.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/fillstruct/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go diff --git a/internal/lsp/analysis/fillstruct/testdata/src/b/b.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/b/b.go similarity index 100% rename from internal/lsp/analysis/fillstruct/testdata/src/b/b.go rename to gopls/internal/lsp/analysis/fillstruct/testdata/src/b/b.go diff --git a/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go similarity index 100% rename from internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go rename to gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go diff --git a/internal/lsp/analysis/infertypeargs/infertypeargs.go b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/infertypeargs.go rename to gopls/internal/lsp/analysis/infertypeargs/infertypeargs.go diff --git a/internal/lsp/analysis/infertypeargs/infertypeargs_test.go b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go similarity index 90% rename from internal/lsp/analysis/infertypeargs/infertypeargs_test.go rename to gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go index 2957f46e367..2d687f0e72a 100644 --- a/internal/lsp/analysis/infertypeargs/infertypeargs_test.go +++ b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/infertypeargs" + "golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/infertypeargs/run_go117.go b/gopls/internal/lsp/analysis/infertypeargs/run_go117.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/run_go117.go rename to gopls/internal/lsp/analysis/infertypeargs/run_go117.go diff --git a/internal/lsp/analysis/infertypeargs/run_go118.go b/gopls/internal/lsp/analysis/infertypeargs/run_go118.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/run_go118.go rename to gopls/internal/lsp/analysis/infertypeargs/run_go118.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden diff --git a/internal/lsp/analysis/nonewvars/nonewvars.go b/gopls/internal/lsp/analysis/nonewvars/nonewvars.go similarity index 100% rename from internal/lsp/analysis/nonewvars/nonewvars.go rename to gopls/internal/lsp/analysis/nonewvars/nonewvars.go diff --git a/internal/lsp/analysis/nonewvars/nonewvars_test.go b/gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go similarity index 89% rename from internal/lsp/analysis/nonewvars/nonewvars_test.go rename to gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go index dc58ab0ff5e..8f6f0a51fb4 100644 --- a/internal/lsp/analysis/nonewvars/nonewvars_test.go +++ b/gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/nonewvars" + "golang.org/x/tools/gopls/internal/lsp/analysis/nonewvars" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/nonewvars/testdata/src/a/a.go b/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/nonewvars/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go diff --git a/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go b/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go similarity index 100% rename from internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go rename to gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go diff --git a/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden b/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden similarity index 100% rename from internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden rename to gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden diff --git a/internal/lsp/analysis/noresultvalues/noresultvalues.go b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go similarity index 100% rename from internal/lsp/analysis/noresultvalues/noresultvalues.go rename to gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go diff --git a/internal/lsp/analysis/noresultvalues/noresultvalues_test.go b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go similarity index 89% rename from internal/lsp/analysis/noresultvalues/noresultvalues_test.go rename to gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go index 12198a1c130..24ce39207ee 100644 --- a/internal/lsp/analysis/noresultvalues/noresultvalues_test.go +++ b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/noresultvalues" + "golang.org/x/tools/gopls/internal/lsp/analysis/noresultvalues" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/noresultvalues/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go diff --git a/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go similarity index 100% rename from internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go rename to gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go diff --git a/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden similarity index 100% rename from internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden rename to gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden diff --git a/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go similarity index 100% rename from internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go rename to gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go diff --git a/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go similarity index 85% rename from internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go rename to gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go index e60f7d6b055..b0365a6b3da 100644 --- a/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go +++ b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/simplifycompositelit" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifycompositelit" ) func Test(t *testing.T) { diff --git a/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go b/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go diff --git a/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/simplifyrange/simplifyrange.go b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange.go similarity index 100% rename from internal/lsp/analysis/simplifyrange/simplifyrange.go rename to gopls/internal/lsp/analysis/simplifyrange/simplifyrange.go diff --git a/internal/lsp/analysis/simplifyrange/simplifyrange_test.go b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go similarity index 86% rename from internal/lsp/analysis/simplifyrange/simplifyrange_test.go rename to gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go index ecc7a969257..fbd57ec2d65 100644 --- a/internal/lsp/analysis/simplifyrange/simplifyrange_test.go +++ b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/simplifyrange" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyrange" ) func Test(t *testing.T) { diff --git a/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go b/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/simplifyrange/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go diff --git a/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/simplifyslice/simplifyslice.go b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice.go similarity index 100% rename from internal/lsp/analysis/simplifyslice/simplifyslice.go rename to gopls/internal/lsp/analysis/simplifyslice/simplifyslice.go diff --git a/internal/lsp/analysis/simplifyslice/simplifyslice_test.go b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go similarity index 89% rename from internal/lsp/analysis/simplifyslice/simplifyslice_test.go rename to gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go index cff6267c679..41914ba3170 100644 --- a/internal/lsp/analysis/simplifyslice/simplifyslice_test.go +++ b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/simplifyslice" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyslice" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/simplifyslice/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go diff --git a/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go similarity index 100% rename from internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go rename to gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go diff --git a/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden similarity index 100% rename from internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden rename to gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden diff --git a/internal/lsp/analysis/stubmethods/stubmethods.go b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go similarity index 100% rename from internal/lsp/analysis/stubmethods/stubmethods.go rename to gopls/internal/lsp/analysis/stubmethods/stubmethods.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go diff --git a/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go similarity index 100% rename from internal/lsp/analysis/undeclaredname/undeclared.go rename to gopls/internal/lsp/analysis/undeclaredname/undeclared.go diff --git a/internal/lsp/analysis/undeclaredname/undeclared_test.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go similarity index 85% rename from internal/lsp/analysis/undeclaredname/undeclared_test.go rename to gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go index b7154393742..306c3f03941 100644 --- a/internal/lsp/analysis/undeclaredname/undeclared_test.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/undeclaredname" + "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" ) func Test(t *testing.T) { diff --git a/internal/lsp/analysis/unusedparams/testdata/src/a/a.go b/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/unusedparams/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go diff --git a/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go similarity index 100% rename from internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go rename to gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go diff --git a/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden similarity index 100% rename from internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden rename to gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden diff --git a/internal/lsp/analysis/unusedparams/unusedparams.go b/gopls/internal/lsp/analysis/unusedparams/unusedparams.go similarity index 100% rename from internal/lsp/analysis/unusedparams/unusedparams.go rename to gopls/internal/lsp/analysis/unusedparams/unusedparams.go diff --git a/internal/lsp/analysis/unusedparams/unusedparams_test.go b/gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go similarity index 89% rename from internal/lsp/analysis/unusedparams/unusedparams_test.go rename to gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go index dff17c95e5d..fdd43b821fe 100644 --- a/internal/lsp/analysis/unusedparams/unusedparams_test.go +++ b/gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/unusedparams" + "golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go similarity index 100% rename from internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go rename to gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go diff --git a/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden similarity index 100% rename from internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden rename to gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden diff --git a/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go similarity index 100% rename from internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go rename to gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go diff --git a/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden similarity index 100% rename from internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden rename to gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden diff --git a/internal/lsp/analysis/unusedvariable/unusedvariable.go b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go similarity index 100% rename from internal/lsp/analysis/unusedvariable/unusedvariable.go rename to gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go diff --git a/internal/lsp/analysis/unusedvariable/unusedvariable_test.go b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go similarity index 89% rename from internal/lsp/analysis/unusedvariable/unusedvariable_test.go rename to gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go index e6d7c020f04..08223155f6e 100644 --- a/internal/lsp/analysis/unusedvariable/unusedvariable_test.go +++ b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/unusedvariable" + "golang.org/x/tools/gopls/internal/lsp/analysis/unusedvariable" ) func Test(t *testing.T) { diff --git a/internal/lsp/analysis/useany/testdata/src/a/a.go b/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/useany/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/useany/testdata/src/a/a.go diff --git a/internal/lsp/analysis/useany/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/useany/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/useany/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/useany/useany.go b/gopls/internal/lsp/analysis/useany/useany.go similarity index 100% rename from internal/lsp/analysis/useany/useany.go rename to gopls/internal/lsp/analysis/useany/useany.go diff --git a/internal/lsp/analysis/useany/useany_test.go b/gopls/internal/lsp/analysis/useany/useany_test.go similarity index 89% rename from internal/lsp/analysis/useany/useany_test.go rename to gopls/internal/lsp/analysis/useany/useany_test.go index 535d9152665..083c3d54fd4 100644 --- a/internal/lsp/analysis/useany/useany_test.go +++ b/gopls/internal/lsp/analysis/useany/useany_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/useany" + "golang.org/x/tools/gopls/internal/lsp/analysis/useany" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/browser/README.md b/gopls/internal/lsp/browser/README.md similarity index 100% rename from internal/lsp/browser/README.md rename to gopls/internal/lsp/browser/README.md diff --git a/internal/lsp/browser/browser.go b/gopls/internal/lsp/browser/browser.go similarity index 100% rename from internal/lsp/browser/browser.go rename to gopls/internal/lsp/browser/browser.go diff --git a/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go similarity index 99% rename from internal/lsp/cache/analysis.go rename to gopls/internal/lsp/cache/analysis.go index 144ef3ce4ea..0b5ce8995ec 100644 --- a/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -16,8 +16,8 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go similarity index 98% rename from internal/lsp/cache/cache.go rename to gopls/internal/lsp/cache/cache.go index eea302187d5..5056111ff75 100644 --- a/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -22,8 +22,8 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go similarity index 99% rename from internal/lsp/cache/check.go rename to gopls/internal/lsp/cache/check.go index 6beee1f7bed..1fbd9c002db 100644 --- a/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -22,10 +22,10 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/span" diff --git a/internal/lsp/cache/debug.go b/gopls/internal/lsp/cache/debug.go similarity index 100% rename from internal/lsp/cache/debug.go rename to gopls/internal/lsp/cache/debug.go diff --git a/internal/lsp/cache/error_test.go b/gopls/internal/lsp/cache/error_test.go similarity index 100% rename from internal/lsp/cache/error_test.go rename to gopls/internal/lsp/cache/error_test.go diff --git a/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go similarity index 98% rename from internal/lsp/cache/errors.go rename to gopls/internal/lsp/cache/errors.go index 94378298916..d01bf7f92e0 100644 --- a/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -16,10 +16,10 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typesinternal" ) diff --git a/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go similarity index 98% rename from internal/lsp/cache/graph.go rename to gopls/internal/lsp/cache/graph.go index d0f80cc4014..f90ee960881 100644 --- a/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -7,7 +7,7 @@ package cache import ( "sort" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go similarity index 99% rename from internal/lsp/cache/imports.go rename to gopls/internal/lsp/cache/imports.go index 6510bbd5734..a73f9beff3e 100644 --- a/internal/lsp/cache/imports.go +++ b/gopls/internal/lsp/cache/imports.go @@ -17,7 +17,7 @@ import ( "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source" ) type importsState struct { diff --git a/internal/lsp/cache/keys.go b/gopls/internal/lsp/cache/keys.go similarity index 100% rename from internal/lsp/cache/keys.go rename to gopls/internal/lsp/cache/keys.go diff --git a/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go similarity index 99% rename from internal/lsp/cache/load.go rename to gopls/internal/lsp/cache/load.go index 2bc9277a026..2bf4a337dfe 100644 --- a/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -19,9 +19,9 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go similarity index 98% rename from internal/lsp/cache/maps.go rename to gopls/internal/lsp/cache/maps.go index eef918843e8..0493398798d 100644 --- a/internal/lsp/cache/maps.go +++ b/gopls/internal/lsp/cache/maps.go @@ -5,7 +5,7 @@ package cache import ( - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/persistent" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go similarity index 76% rename from internal/lsp/cache/metadata.go rename to gopls/internal/lsp/cache/metadata.go index 668d0829a13..711508c9a11 100644 --- a/internal/lsp/cache/metadata.go +++ b/gopls/internal/lsp/cache/metadata.go @@ -42,23 +42,23 @@ type Metadata struct { // IsIntermediateTestVariant reports whether the given package is an // intermediate test variant, e.g. - // "golang.org/x/tools/internal/lsp/cache [golang.org/x/tools/internal/lsp/source.test]". + // "golang.org/x/tools/gopls/internal/lsp/cache [golang.org/x/tools/gopls/internal/lsp/source.test]". // // Such test variants arise when an x_test package (in this case source_test) // imports a package (in this case cache) that itself imports the the // non-x_test package (in this case source). // // This is done so that the forward transitive closure of source_test has - // only one package for the "golang.org/x/tools/internal/lsp/source" import. + // only one package for the "golang.org/x/tools/gopls/internal/lsp/source" import. // The intermediate test variant exists to hold the test variant import: // - // golang.org/x/tools/internal/lsp/source_test [golang.org/x/tools/internal/lsp/source.test] - // | "golang.org/x/tools/internal/lsp/cache" -> golang.org/x/tools/internal/lsp/cache [golang.org/x/tools/internal/lsp/source.test] - // | "golang.org/x/tools/internal/lsp/source" -> golang.org/x/tools/internal/lsp/source [golang.org/x/tools/internal/lsp/source.test] + // golang.org/x/tools/gopls/internal/lsp/source_test [golang.org/x/tools/gopls/internal/lsp/source.test] + // | "golang.org/x/tools/gopls/internal/lsp/cache" -> golang.org/x/tools/gopls/internal/lsp/cache [golang.org/x/tools/gopls/internal/lsp/source.test] + // | "golang.org/x/tools/gopls/internal/lsp/source" -> golang.org/x/tools/gopls/internal/lsp/source [golang.org/x/tools/gopls/internal/lsp/source.test] // | ... // - // golang.org/x/tools/internal/lsp/cache [golang.org/x/tools/internal/lsp/source.test] - // | "golang.org/x/tools/internal/lsp/source" -> golang.org/x/tools/internal/lsp/source [golang.org/x/tools/internal/lsp/source.test] + // golang.org/x/tools/gopls/internal/lsp/cache [golang.org/x/tools/gopls/internal/lsp/source.test] + // | "golang.org/x/tools/gopls/internal/lsp/source" -> golang.org/x/tools/gopls/internal/lsp/source [golang.org/x/tools/gopls/internal/lsp/source.test] // | ... // // We filter these variants out in certain places. For example, there is diff --git a/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go similarity index 98% rename from internal/lsp/cache/mod.go rename to gopls/internal/lsp/cache/mod.go index 57fa1e2d0aa..50fa0a4dd1c 100644 --- a/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -16,10 +16,10 @@ import ( "golang.org/x/mod/module" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go similarity index 98% rename from internal/lsp/cache/mod_tidy.go rename to gopls/internal/lsp/cache/mod_tidy.go index 704e1a63a46..1bbc6238056 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -18,11 +18,11 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cache/os_darwin.go b/gopls/internal/lsp/cache/os_darwin.go similarity index 100% rename from internal/lsp/cache/os_darwin.go rename to gopls/internal/lsp/cache/os_darwin.go diff --git a/internal/lsp/cache/os_windows.go b/gopls/internal/lsp/cache/os_windows.go similarity index 100% rename from internal/lsp/cache/os_windows.go rename to gopls/internal/lsp/cache/os_windows.go diff --git a/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go similarity index 99% rename from internal/lsp/cache/parse.go rename to gopls/internal/lsp/cache/parse.go index 77e893a6681..5cb5ef7203e 100644 --- a/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -19,12 +19,12 @@ import ( "strings" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/myers" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" ) diff --git a/internal/lsp/cache/parse_test.go b/gopls/internal/lsp/cache/parse_test.go similarity index 100% rename from internal/lsp/cache/parse_test.go rename to gopls/internal/lsp/cache/parse_test.go diff --git a/internal/lsp/cache/parsemode_go116.go b/gopls/internal/lsp/cache/parsemode_go116.go similarity index 100% rename from internal/lsp/cache/parsemode_go116.go rename to gopls/internal/lsp/cache/parsemode_go116.go diff --git a/internal/lsp/cache/parsemode_go117.go b/gopls/internal/lsp/cache/parsemode_go117.go similarity index 100% rename from internal/lsp/cache/parsemode_go117.go rename to gopls/internal/lsp/cache/parsemode_go117.go diff --git a/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go similarity index 98% rename from internal/lsp/cache/pkg.go rename to gopls/internal/lsp/cache/pkg.go index 3478c58ad1f..b1ee50e2faa 100644 --- a/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -11,7 +11,7 @@ import ( "go/types" "golang.org/x/mod/module" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go similarity index 99% rename from internal/lsp/cache/session.go rename to gopls/internal/lsp/cache/session.go index 32fd4484432..59318131516 100644 --- a/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -16,8 +16,8 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/progress" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/progress" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/persistent" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" diff --git a/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go similarity index 99% rename from internal/lsp/cache/snapshot.go rename to gopls/internal/lsp/cache/snapshot.go index 87ef595c334..aa088bb4081 100644 --- a/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -33,9 +33,9 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/persistent" diff --git a/internal/lsp/cache/symbols.go b/gopls/internal/lsp/cache/symbols.go similarity index 97% rename from internal/lsp/cache/symbols.go rename to gopls/internal/lsp/cache/symbols.go index e98f554969c..69b2b044273 100644 --- a/internal/lsp/cache/symbols.go +++ b/gopls/internal/lsp/cache/symbols.go @@ -12,9 +12,9 @@ import ( "go/types" "strings" - "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" ) diff --git a/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go similarity index 99% rename from internal/lsp/cache/view.go rename to gopls/internal/lsp/cache/view.go index 6eec3536d8f..cc88c3de5a2 100644 --- a/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -27,9 +27,9 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go similarity index 98% rename from internal/lsp/cache/view_test.go rename to gopls/internal/lsp/cache/view_test.go index 59684ea3614..f3851e9131d 100644 --- a/internal/lsp/cache/view_test.go +++ b/gopls/internal/lsp/cache/view_test.go @@ -10,8 +10,8 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go similarity index 99% rename from internal/lsp/cache/workspace.go rename to gopls/internal/lsp/cache/workspace.go index f04fbe8eec0..e066c7e56ec 100644 --- a/internal/lsp/cache/workspace.go +++ b/gopls/internal/lsp/cache/workspace.go @@ -16,7 +16,7 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/internal/lsp/cache/workspace_test.go b/gopls/internal/lsp/cache/workspace_test.go similarity index 99% rename from internal/lsp/cache/workspace_test.go rename to gopls/internal/lsp/cache/workspace_test.go index f1cd00b7119..e6047a92629 100644 --- a/internal/lsp/cache/workspace_test.go +++ b/gopls/internal/lsp/cache/workspace_test.go @@ -12,8 +12,8 @@ import ( "testing" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/call_hierarchy.go b/gopls/internal/lsp/call_hierarchy.go similarity index 92% rename from internal/lsp/call_hierarchy.go rename to gopls/internal/lsp/call_hierarchy.go index 43c4ea8d5b7..79eeb25cc15 100644 --- a/internal/lsp/call_hierarchy.go +++ b/gopls/internal/lsp/call_hierarchy.go @@ -7,8 +7,8 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) prepareCallHierarchy(ctx context.Context, params *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) { diff --git a/internal/lsp/cmd/call_hierarchy.go b/gopls/internal/lsp/cmd/call_hierarchy.go similarity index 98% rename from internal/lsp/cmd/call_hierarchy.go rename to gopls/internal/lsp/cmd/call_hierarchy.go index c9f9e73e0e2..643b2f7ff86 100644 --- a/internal/lsp/cmd/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/call_hierarchy.go @@ -10,7 +10,7 @@ import ( "fmt" "strings" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/capabilities_test.go b/gopls/internal/lsp/cmd/capabilities_test.go similarity index 97% rename from internal/lsp/cmd/capabilities_test.go rename to gopls/internal/lsp/cmd/capabilities_test.go index 930621bf307..bd209639420 100644 --- a/internal/lsp/cmd/capabilities_test.go +++ b/gopls/internal/lsp/cmd/capabilities_test.go @@ -12,9 +12,9 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // TestCapabilities does some minimal validation of the server's adherence to the LSP. diff --git a/internal/lsp/cmd/check.go b/gopls/internal/lsp/cmd/check.go similarity index 100% rename from internal/lsp/cmd/check.go rename to gopls/internal/lsp/cmd/check.go diff --git a/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go similarity index 98% rename from internal/lsp/cmd/cmd.go rename to gopls/internal/lsp/cmd/cmd.go index 5911f97d1c1..254313a5af4 100644 --- a/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -23,12 +23,12 @@ import ( "time" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/lsprpc" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" "golang.org/x/tools/internal/xcontext" diff --git a/internal/lsp/cmd/cmd_test.go b/gopls/internal/lsp/cmd/cmd_test.go similarity index 76% rename from internal/lsp/cmd/cmd_test.go rename to gopls/internal/lsp/cmd/cmd_test.go index c44bd5722cb..877ceb66eb9 100644 --- a/internal/lsp/cmd/cmd_test.go +++ b/gopls/internal/lsp/cmd/cmd_test.go @@ -8,9 +8,9 @@ import ( "os" "testing" - "golang.org/x/tools/internal/lsp/bug" - cmdtest "golang.org/x/tools/internal/lsp/cmd/test" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/internal/bug" + cmdtest "golang.org/x/tools/gopls/internal/lsp/cmd/test" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/testenv" ) diff --git a/internal/lsp/cmd/definition.go b/gopls/internal/lsp/cmd/definition.go similarity index 97% rename from internal/lsp/cmd/definition.go rename to gopls/internal/lsp/cmd/definition.go index 44e6fc8c717..3c4474a75ae 100644 --- a/internal/lsp/cmd/definition.go +++ b/gopls/internal/lsp/cmd/definition.go @@ -12,8 +12,8 @@ import ( "os" "strings" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/export_test.go b/gopls/internal/lsp/cmd/export_test.go similarity index 100% rename from internal/lsp/cmd/export_test.go rename to gopls/internal/lsp/cmd/export_test.go diff --git a/internal/lsp/cmd/folding_range.go b/gopls/internal/lsp/cmd/folding_range.go similarity index 96% rename from internal/lsp/cmd/folding_range.go rename to gopls/internal/lsp/cmd/folding_range.go index 513c9bdd227..ee407150dd4 100644 --- a/internal/lsp/cmd/folding_range.go +++ b/gopls/internal/lsp/cmd/folding_range.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go similarity index 95% rename from internal/lsp/cmd/format.go rename to gopls/internal/lsp/cmd/format.go index 5e17ed4a570..409850642fe 100644 --- a/internal/lsp/cmd/format.go +++ b/gopls/internal/lsp/cmd/format.go @@ -10,9 +10,9 @@ import ( "fmt" "io/ioutil" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/help_test.go b/gopls/internal/lsp/cmd/help_test.go similarity index 97% rename from internal/lsp/cmd/help_test.go rename to gopls/internal/lsp/cmd/help_test.go index 536d19dc219..f8d9b0b75ca 100644 --- a/internal/lsp/cmd/help_test.go +++ b/gopls/internal/lsp/cmd/help_test.go @@ -12,7 +12,7 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/cmd" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/highlight.go b/gopls/internal/lsp/cmd/highlight.go similarity index 97% rename from internal/lsp/cmd/highlight.go rename to gopls/internal/lsp/cmd/highlight.go index a325a2d53d9..ace8125fd81 100644 --- a/internal/lsp/cmd/highlight.go +++ b/gopls/internal/lsp/cmd/highlight.go @@ -10,7 +10,7 @@ import ( "fmt" "sort" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/implementation.go b/gopls/internal/lsp/cmd/implementation.go similarity index 97% rename from internal/lsp/cmd/implementation.go rename to gopls/internal/lsp/cmd/implementation.go index 7b42d994303..073a61a75b3 100644 --- a/internal/lsp/cmd/implementation.go +++ b/gopls/internal/lsp/cmd/implementation.go @@ -10,7 +10,7 @@ import ( "fmt" "sort" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go similarity index 95% rename from internal/lsp/cmd/imports.go rename to gopls/internal/lsp/cmd/imports.go index 8f12a94ce47..d6a968d5e03 100644 --- a/internal/lsp/cmd/imports.go +++ b/gopls/internal/lsp/cmd/imports.go @@ -10,9 +10,9 @@ import ( "fmt" "io/ioutil" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/info.go b/gopls/internal/lsp/cmd/info.go similarity index 98% rename from internal/lsp/cmd/info.go rename to gopls/internal/lsp/cmd/info.go index 8e581a37cb1..68ef40ffb29 100644 --- a/internal/lsp/cmd/info.go +++ b/gopls/internal/lsp/cmd/info.go @@ -14,9 +14,9 @@ import ( "os" "strings" - "golang.org/x/tools/internal/lsp/browser" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/browser" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/links.go b/gopls/internal/lsp/cmd/links.go similarity index 97% rename from internal/lsp/cmd/links.go rename to gopls/internal/lsp/cmd/links.go index 1c48c8c50b9..d63ac218ecb 100644 --- a/internal/lsp/cmd/links.go +++ b/gopls/internal/lsp/cmd/links.go @@ -11,7 +11,7 @@ import ( "fmt" "os" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/prepare_rename.go b/gopls/internal/lsp/cmd/prepare_rename.go similarity index 97% rename from internal/lsp/cmd/prepare_rename.go rename to gopls/internal/lsp/cmd/prepare_rename.go index 44a192b5be3..7dfac9b2545 100644 --- a/internal/lsp/cmd/prepare_rename.go +++ b/gopls/internal/lsp/cmd/prepare_rename.go @@ -10,7 +10,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/references.go b/gopls/internal/lsp/cmd/references.go similarity index 97% rename from internal/lsp/cmd/references.go rename to gopls/internal/lsp/cmd/references.go index 0697d2e11b7..13c30bdf13f 100644 --- a/internal/lsp/cmd/references.go +++ b/gopls/internal/lsp/cmd/references.go @@ -10,7 +10,7 @@ import ( "fmt" "sort" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/remote.go b/gopls/internal/lsp/cmd/remote.go similarity index 97% rename from internal/lsp/cmd/remote.go rename to gopls/internal/lsp/cmd/remote.go index 0f4c7216444..684981cfff8 100644 --- a/internal/lsp/cmd/remote.go +++ b/gopls/internal/lsp/cmd/remote.go @@ -13,8 +13,8 @@ import ( "log" "os" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) type remote struct { diff --git a/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go similarity index 96% rename from internal/lsp/cmd/rename.go rename to gopls/internal/lsp/cmd/rename.go index be7fd09a302..eb7e10a3ef5 100644 --- a/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -13,9 +13,9 @@ import ( "path/filepath" "sort" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/semantictokens.go b/gopls/internal/lsp/cmd/semantictokens.go similarity index 97% rename from internal/lsp/cmd/semantictokens.go rename to gopls/internal/lsp/cmd/semantictokens.go index 7dbb7f93c61..547b0194f21 100644 --- a/internal/lsp/cmd/semantictokens.go +++ b/gopls/internal/lsp/cmd/semantictokens.go @@ -16,9 +16,9 @@ import ( "os" "unicode/utf8" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/serve.go b/gopls/internal/lsp/cmd/serve.go similarity index 95% rename from internal/lsp/cmd/serve.go rename to gopls/internal/lsp/cmd/serve.go index 10730fd89cd..8a4de5eac6c 100644 --- a/internal/lsp/cmd/serve.go +++ b/gopls/internal/lsp/cmd/serve.go @@ -16,10 +16,10 @@ import ( "golang.org/x/tools/internal/fakenet" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/lsprpc" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/signature.go b/gopls/internal/lsp/cmd/signature.go similarity index 97% rename from internal/lsp/cmd/signature.go rename to gopls/internal/lsp/cmd/signature.go index db948430183..81ec4feabaf 100644 --- a/internal/lsp/cmd/signature.go +++ b/gopls/internal/lsp/cmd/signature.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/subcommands.go b/gopls/internal/lsp/cmd/subcommands.go similarity index 100% rename from internal/lsp/cmd/subcommands.go rename to gopls/internal/lsp/cmd/subcommands.go diff --git a/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go similarity index 97% rename from internal/lsp/cmd/suggested_fix.go rename to gopls/internal/lsp/cmd/suggested_fix.go index a3e51325d70..048dcd62e2a 100644 --- a/internal/lsp/cmd/suggested_fix.go +++ b/gopls/internal/lsp/cmd/suggested_fix.go @@ -10,9 +10,9 @@ import ( "fmt" "io/ioutil" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/symbols.go b/gopls/internal/lsp/cmd/symbols.go similarity index 98% rename from internal/lsp/cmd/symbols.go rename to gopls/internal/lsp/cmd/symbols.go index b43a6dcd1f7..0d6e4dbe08d 100644 --- a/internal/lsp/cmd/symbols.go +++ b/gopls/internal/lsp/cmd/symbols.go @@ -11,7 +11,7 @@ import ( "fmt" "sort" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/test/call_hierarchy.go b/gopls/internal/lsp/cmd/test/call_hierarchy.go similarity index 96% rename from internal/lsp/cmd/test/call_hierarchy.go rename to gopls/internal/lsp/cmd/test/call_hierarchy.go index 38f8ed707a4..be4ebe8e339 100644 --- a/internal/lsp/cmd/test/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/test/call_hierarchy.go @@ -10,8 +10,8 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/test/check.go b/gopls/internal/lsp/cmd/test/check.go similarity index 91% rename from internal/lsp/cmd/test/check.go rename to gopls/internal/lsp/cmd/test/check.go index ac1eac401ae..4f5e471dd31 100644 --- a/internal/lsp/cmd/test/check.go +++ b/gopls/internal/lsp/cmd/test/check.go @@ -9,9 +9,9 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/test/cmdtest.go b/gopls/internal/lsp/cmd/test/cmdtest.go similarity index 94% rename from internal/lsp/cmd/test/cmdtest.go rename to gopls/internal/lsp/cmd/test/cmdtest.go index 5342e9b7faf..86608c7c953 100644 --- a/internal/lsp/cmd/test/cmdtest.go +++ b/gopls/internal/lsp/cmd/test/cmdtest.go @@ -16,13 +16,13 @@ import ( "testing" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/cmd" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/lsprpc" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/test/definition.go b/gopls/internal/lsp/cmd/test/definition.go similarity index 96% rename from internal/lsp/cmd/test/definition.go rename to gopls/internal/lsp/cmd/test/definition.go index 9c49eaa7d58..2f8c5ab24d4 100644 --- a/internal/lsp/cmd/test/definition.go +++ b/gopls/internal/lsp/cmd/test/definition.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/test/folding_range.go b/gopls/internal/lsp/cmd/test/folding_range.go similarity index 100% rename from internal/lsp/cmd/test/folding_range.go rename to gopls/internal/lsp/cmd/test/folding_range.go diff --git a/internal/lsp/cmd/test/format.go b/gopls/internal/lsp/cmd/test/format.go similarity index 100% rename from internal/lsp/cmd/test/format.go rename to gopls/internal/lsp/cmd/test/format.go diff --git a/internal/lsp/cmd/test/highlight.go b/gopls/internal/lsp/cmd/test/highlight.go similarity index 100% rename from internal/lsp/cmd/test/highlight.go rename to gopls/internal/lsp/cmd/test/highlight.go diff --git a/internal/lsp/cmd/test/implementation.go b/gopls/internal/lsp/cmd/test/implementation.go similarity index 100% rename from internal/lsp/cmd/test/implementation.go rename to gopls/internal/lsp/cmd/test/implementation.go diff --git a/internal/lsp/cmd/test/imports.go b/gopls/internal/lsp/cmd/test/imports.go similarity index 89% rename from internal/lsp/cmd/test/imports.go rename to gopls/internal/lsp/cmd/test/imports.go index ce8aee55dfa..6bb8af9a852 100644 --- a/internal/lsp/cmd/test/imports.go +++ b/gopls/internal/lsp/cmd/test/imports.go @@ -7,8 +7,8 @@ package cmdtest import ( "testing" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/test/links.go b/gopls/internal/lsp/cmd/test/links.go similarity index 87% rename from internal/lsp/cmd/test/links.go rename to gopls/internal/lsp/cmd/test/links.go index 88df768323a..52d2a3185e2 100644 --- a/internal/lsp/cmd/test/links.go +++ b/gopls/internal/lsp/cmd/test/links.go @@ -8,8 +8,8 @@ import ( "encoding/json" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/test/prepare_rename.go b/gopls/internal/lsp/cmd/test/prepare_rename.go similarity index 88% rename from internal/lsp/cmd/test/prepare_rename.go rename to gopls/internal/lsp/cmd/test/prepare_rename.go index b5359e57b42..4ae6d1ad857 100644 --- a/internal/lsp/cmd/test/prepare_rename.go +++ b/gopls/internal/lsp/cmd/test/prepare_rename.go @@ -8,9 +8,9 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/lsp/cmd" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/test/references.go b/gopls/internal/lsp/cmd/test/references.go similarity index 100% rename from internal/lsp/cmd/test/references.go rename to gopls/internal/lsp/cmd/test/references.go diff --git a/internal/lsp/cmd/test/rename.go b/gopls/internal/lsp/cmd/test/rename.go similarity index 100% rename from internal/lsp/cmd/test/rename.go rename to gopls/internal/lsp/cmd/test/rename.go diff --git a/internal/lsp/cmd/test/semanticdriver.go b/gopls/internal/lsp/cmd/test/semanticdriver.go similarity index 100% rename from internal/lsp/cmd/test/semanticdriver.go rename to gopls/internal/lsp/cmd/test/semanticdriver.go diff --git a/internal/lsp/cmd/test/signature.go b/gopls/internal/lsp/cmd/test/signature.go similarity index 90% rename from internal/lsp/cmd/test/signature.go rename to gopls/internal/lsp/cmd/test/signature.go index f6bdaebf312..9419d45218a 100644 --- a/internal/lsp/cmd/test/signature.go +++ b/gopls/internal/lsp/cmd/test/signature.go @@ -8,8 +8,8 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/test/suggested_fix.go b/gopls/internal/lsp/cmd/test/suggested_fix.go similarity index 91% rename from internal/lsp/cmd/test/suggested_fix.go rename to gopls/internal/lsp/cmd/test/suggested_fix.go index 1481d8bad48..0981e267b5b 100644 --- a/internal/lsp/cmd/test/suggested_fix.go +++ b/gopls/internal/lsp/cmd/test/suggested_fix.go @@ -8,8 +8,8 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/test/symbols.go b/gopls/internal/lsp/cmd/test/symbols.go similarity index 92% rename from internal/lsp/cmd/test/symbols.go rename to gopls/internal/lsp/cmd/test/symbols.go index 055be030829..f75b3a4426a 100644 --- a/internal/lsp/cmd/test/symbols.go +++ b/gopls/internal/lsp/cmd/test/symbols.go @@ -7,7 +7,7 @@ package cmdtest import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/test/workspace_symbol.go b/gopls/internal/lsp/cmd/test/workspace_symbol.go similarity index 90% rename from internal/lsp/cmd/test/workspace_symbol.go rename to gopls/internal/lsp/cmd/test/workspace_symbol.go index 244ce04905e..876887b20ab 100644 --- a/internal/lsp/cmd/test/workspace_symbol.go +++ b/gopls/internal/lsp/cmd/test/workspace_symbol.go @@ -11,9 +11,9 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/cmd/usage/api-json.hlp b/gopls/internal/lsp/cmd/usage/api-json.hlp similarity index 100% rename from internal/lsp/cmd/usage/api-json.hlp rename to gopls/internal/lsp/cmd/usage/api-json.hlp diff --git a/internal/lsp/cmd/usage/bug.hlp b/gopls/internal/lsp/cmd/usage/bug.hlp similarity index 100% rename from internal/lsp/cmd/usage/bug.hlp rename to gopls/internal/lsp/cmd/usage/bug.hlp diff --git a/internal/lsp/cmd/usage/call_hierarchy.hlp b/gopls/internal/lsp/cmd/usage/call_hierarchy.hlp similarity index 100% rename from internal/lsp/cmd/usage/call_hierarchy.hlp rename to gopls/internal/lsp/cmd/usage/call_hierarchy.hlp diff --git a/internal/lsp/cmd/usage/check.hlp b/gopls/internal/lsp/cmd/usage/check.hlp similarity index 100% rename from internal/lsp/cmd/usage/check.hlp rename to gopls/internal/lsp/cmd/usage/check.hlp diff --git a/internal/lsp/cmd/usage/definition.hlp b/gopls/internal/lsp/cmd/usage/definition.hlp similarity index 100% rename from internal/lsp/cmd/usage/definition.hlp rename to gopls/internal/lsp/cmd/usage/definition.hlp diff --git a/internal/lsp/cmd/usage/fix.hlp b/gopls/internal/lsp/cmd/usage/fix.hlp similarity index 100% rename from internal/lsp/cmd/usage/fix.hlp rename to gopls/internal/lsp/cmd/usage/fix.hlp diff --git a/internal/lsp/cmd/usage/folding_ranges.hlp b/gopls/internal/lsp/cmd/usage/folding_ranges.hlp similarity index 100% rename from internal/lsp/cmd/usage/folding_ranges.hlp rename to gopls/internal/lsp/cmd/usage/folding_ranges.hlp diff --git a/internal/lsp/cmd/usage/format.hlp b/gopls/internal/lsp/cmd/usage/format.hlp similarity index 100% rename from internal/lsp/cmd/usage/format.hlp rename to gopls/internal/lsp/cmd/usage/format.hlp diff --git a/internal/lsp/cmd/usage/help.hlp b/gopls/internal/lsp/cmd/usage/help.hlp similarity index 100% rename from internal/lsp/cmd/usage/help.hlp rename to gopls/internal/lsp/cmd/usage/help.hlp diff --git a/internal/lsp/cmd/usage/highlight.hlp b/gopls/internal/lsp/cmd/usage/highlight.hlp similarity index 100% rename from internal/lsp/cmd/usage/highlight.hlp rename to gopls/internal/lsp/cmd/usage/highlight.hlp diff --git a/internal/lsp/cmd/usage/implementation.hlp b/gopls/internal/lsp/cmd/usage/implementation.hlp similarity index 100% rename from internal/lsp/cmd/usage/implementation.hlp rename to gopls/internal/lsp/cmd/usage/implementation.hlp diff --git a/internal/lsp/cmd/usage/imports.hlp b/gopls/internal/lsp/cmd/usage/imports.hlp similarity index 100% rename from internal/lsp/cmd/usage/imports.hlp rename to gopls/internal/lsp/cmd/usage/imports.hlp diff --git a/internal/lsp/cmd/usage/inspect.hlp b/gopls/internal/lsp/cmd/usage/inspect.hlp similarity index 100% rename from internal/lsp/cmd/usage/inspect.hlp rename to gopls/internal/lsp/cmd/usage/inspect.hlp diff --git a/internal/lsp/cmd/usage/licenses.hlp b/gopls/internal/lsp/cmd/usage/licenses.hlp similarity index 100% rename from internal/lsp/cmd/usage/licenses.hlp rename to gopls/internal/lsp/cmd/usage/licenses.hlp diff --git a/internal/lsp/cmd/usage/links.hlp b/gopls/internal/lsp/cmd/usage/links.hlp similarity index 100% rename from internal/lsp/cmd/usage/links.hlp rename to gopls/internal/lsp/cmd/usage/links.hlp diff --git a/internal/lsp/cmd/usage/prepare_rename.hlp b/gopls/internal/lsp/cmd/usage/prepare_rename.hlp similarity index 100% rename from internal/lsp/cmd/usage/prepare_rename.hlp rename to gopls/internal/lsp/cmd/usage/prepare_rename.hlp diff --git a/internal/lsp/cmd/usage/references.hlp b/gopls/internal/lsp/cmd/usage/references.hlp similarity index 100% rename from internal/lsp/cmd/usage/references.hlp rename to gopls/internal/lsp/cmd/usage/references.hlp diff --git a/internal/lsp/cmd/usage/remote.hlp b/gopls/internal/lsp/cmd/usage/remote.hlp similarity index 100% rename from internal/lsp/cmd/usage/remote.hlp rename to gopls/internal/lsp/cmd/usage/remote.hlp diff --git a/internal/lsp/cmd/usage/rename.hlp b/gopls/internal/lsp/cmd/usage/rename.hlp similarity index 100% rename from internal/lsp/cmd/usage/rename.hlp rename to gopls/internal/lsp/cmd/usage/rename.hlp diff --git a/internal/lsp/cmd/usage/semtok.hlp b/gopls/internal/lsp/cmd/usage/semtok.hlp similarity index 100% rename from internal/lsp/cmd/usage/semtok.hlp rename to gopls/internal/lsp/cmd/usage/semtok.hlp diff --git a/internal/lsp/cmd/usage/serve.hlp b/gopls/internal/lsp/cmd/usage/serve.hlp similarity index 100% rename from internal/lsp/cmd/usage/serve.hlp rename to gopls/internal/lsp/cmd/usage/serve.hlp diff --git a/internal/lsp/cmd/usage/signature.hlp b/gopls/internal/lsp/cmd/usage/signature.hlp similarity index 100% rename from internal/lsp/cmd/usage/signature.hlp rename to gopls/internal/lsp/cmd/usage/signature.hlp diff --git a/internal/lsp/cmd/usage/symbols.hlp b/gopls/internal/lsp/cmd/usage/symbols.hlp similarity index 100% rename from internal/lsp/cmd/usage/symbols.hlp rename to gopls/internal/lsp/cmd/usage/symbols.hlp diff --git a/internal/lsp/cmd/usage/usage.hlp b/gopls/internal/lsp/cmd/usage/usage.hlp similarity index 100% rename from internal/lsp/cmd/usage/usage.hlp rename to gopls/internal/lsp/cmd/usage/usage.hlp diff --git a/internal/lsp/cmd/usage/version.hlp b/gopls/internal/lsp/cmd/usage/version.hlp similarity index 100% rename from internal/lsp/cmd/usage/version.hlp rename to gopls/internal/lsp/cmd/usage/version.hlp diff --git a/internal/lsp/cmd/usage/vulncheck.hlp b/gopls/internal/lsp/cmd/usage/vulncheck.hlp similarity index 84% rename from internal/lsp/cmd/usage/vulncheck.hlp rename to gopls/internal/lsp/cmd/usage/vulncheck.hlp index 19a674b2ea7..4cdb8f3b640 100644 --- a/internal/lsp/cmd/usage/vulncheck.hlp +++ b/gopls/internal/lsp/cmd/usage/vulncheck.hlp @@ -6,7 +6,7 @@ Usage: WARNING: this command is experimental. By default, the command outputs a JSON-encoded - golang.org/x/tools/internal/lsp/command.VulncheckResult + golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult message. Example: $ gopls vulncheck diff --git a/internal/lsp/cmd/usage/workspace.hlp b/gopls/internal/lsp/cmd/usage/workspace.hlp similarity index 100% rename from internal/lsp/cmd/usage/workspace.hlp rename to gopls/internal/lsp/cmd/usage/workspace.hlp diff --git a/internal/lsp/cmd/usage/workspace_symbol.hlp b/gopls/internal/lsp/cmd/usage/workspace_symbol.hlp similarity index 100% rename from internal/lsp/cmd/usage/workspace_symbol.hlp rename to gopls/internal/lsp/cmd/usage/workspace_symbol.hlp diff --git a/internal/lsp/cmd/vulncheck.go b/gopls/internal/lsp/cmd/vulncheck.go similarity index 95% rename from internal/lsp/cmd/vulncheck.go rename to gopls/internal/lsp/cmd/vulncheck.go index d5b05a92427..5ee9b0e3721 100644 --- a/internal/lsp/cmd/vulncheck.go +++ b/gopls/internal/lsp/cmd/vulncheck.go @@ -12,7 +12,7 @@ import ( "os" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/tool" ) @@ -44,7 +44,7 @@ func (v *vulncheck) DetailedHelp(f *flag.FlagSet) { WARNING: this command is experimental. By default, the command outputs a JSON-encoded - golang.org/x/tools/internal/lsp/command.VulncheckResult + golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult message. Example: $ gopls vulncheck diff --git a/internal/lsp/cmd/workspace.go b/gopls/internal/lsp/cmd/workspace.go similarity index 93% rename from internal/lsp/cmd/workspace.go rename to gopls/internal/lsp/cmd/workspace.go index c0ddd9eb46e..2038d276348 100644 --- a/internal/lsp/cmd/workspace.go +++ b/gopls/internal/lsp/cmd/workspace.go @@ -9,9 +9,9 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) // workspace is a top-level command for working with the gopls workspace. This diff --git a/internal/lsp/cmd/workspace_symbol.go b/gopls/internal/lsp/cmd/workspace_symbol.go similarity index 95% rename from internal/lsp/cmd/workspace_symbol.go rename to gopls/internal/lsp/cmd/workspace_symbol.go index 38fe5decf7f..71a121e3362 100644 --- a/internal/lsp/cmd/workspace_symbol.go +++ b/gopls/internal/lsp/cmd/workspace_symbol.go @@ -9,8 +9,8 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go similarity index 98% rename from internal/lsp/code_action.go rename to gopls/internal/lsp/code_action.go index 450d678f658..b09f42214ce 100644 --- a/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -12,11 +12,11 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/code_lens.go b/gopls/internal/lsp/code_lens.go similarity index 88% rename from internal/lsp/code_lens.go rename to gopls/internal/lsp/code_lens.go index e1944583883..4bbe2bb34c6 100644 --- a/internal/lsp/code_lens.go +++ b/gopls/internal/lsp/code_lens.go @@ -10,10 +10,10 @@ import ( "sort" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) { diff --git a/internal/lsp/command.go b/gopls/internal/lsp/command.go similarity index 99% rename from internal/lsp/command.go rename to gopls/internal/lsp/command.go index 06dc2a4cf41..18682557428 100644 --- a/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -22,11 +22,11 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/progress" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/progress" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go similarity index 99% rename from internal/lsp/command/command_gen.go rename to gopls/internal/lsp/command/command_gen.go index 207def42e64..301cf6f604d 100644 --- a/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -15,7 +15,7 @@ import ( "context" "fmt" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) const ( diff --git a/internal/lsp/command/commandmeta/meta.go b/gopls/internal/lsp/command/commandmeta/meta.go similarity index 98% rename from internal/lsp/command/commandmeta/meta.go rename to gopls/internal/lsp/command/commandmeta/meta.go index a3a357df4b0..11d9940f6d4 100644 --- a/internal/lsp/command/commandmeta/meta.go +++ b/gopls/internal/lsp/command/commandmeta/meta.go @@ -17,7 +17,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/command" ) type Command struct { @@ -52,7 +52,7 @@ func Load() (*packages.Package, []*Command, error) { Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, BuildFlags: []string{"-tags=generate"}, }, - "golang.org/x/tools/internal/lsp/command", + "golang.org/x/tools/gopls/internal/lsp/command", ) if err != nil { return nil, nil, fmt.Errorf("packages.Load: %v", err) diff --git a/internal/lsp/command/gen/gen.go b/gopls/internal/lsp/command/gen/gen.go similarity index 94% rename from internal/lsp/command/gen/gen.go rename to gopls/internal/lsp/command/gen/gen.go index 8f7a2d50313..29428699ee6 100644 --- a/internal/lsp/command/gen/gen.go +++ b/gopls/internal/lsp/command/gen/gen.go @@ -13,7 +13,7 @@ import ( "text/template" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/command/commandmeta" + "golang.org/x/tools/gopls/internal/lsp/command/commandmeta" ) const src = `// Copyright 2021 The Go Authors. All rights reserved. @@ -109,10 +109,10 @@ func Generate() ([]byte, error) { Imports: map[string]bool{ "context": true, "fmt": true, - "golang.org/x/tools/internal/lsp/protocol": true, + "golang.org/x/tools/gopls/internal/lsp/protocol": true, }, } - const thispkg = "golang.org/x/tools/internal/lsp/command" + const thispkg = "golang.org/x/tools/gopls/internal/lsp/command" for _, c := range d.Commands { for _, arg := range c.Args { pth := pkgPath(arg.Type) diff --git a/internal/lsp/command/generate.go b/gopls/internal/lsp/command/generate.go similarity index 88% rename from internal/lsp/command/generate.go rename to gopls/internal/lsp/command/generate.go index 14628c733b5..79ff49b0e33 100644 --- a/internal/lsp/command/generate.go +++ b/gopls/internal/lsp/command/generate.go @@ -12,7 +12,7 @@ import ( "io/ioutil" "os" - "golang.org/x/tools/internal/lsp/command/gen" + "golang.org/x/tools/gopls/internal/lsp/command/gen" ) func main() { diff --git a/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go similarity index 99% rename from internal/lsp/command/interface.go rename to gopls/internal/lsp/command/interface.go index 4a4498a77ea..1fff896bd16 100644 --- a/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -17,7 +17,7 @@ package command import ( "context" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // Interface defines the interface gopls exposes for the diff --git a/internal/lsp/command/interface_test.go b/gopls/internal/lsp/command/interface_test.go similarity index 92% rename from internal/lsp/command/interface_test.go rename to gopls/internal/lsp/command/interface_test.go index 9ea30b4463e..de3ce62737f 100644 --- a/internal/lsp/command/interface_test.go +++ b/gopls/internal/lsp/command/interface_test.go @@ -9,7 +9,7 @@ import ( "io/ioutil" "testing" - "golang.org/x/tools/internal/lsp/command/gen" + "golang.org/x/tools/gopls/internal/lsp/command/gen" "golang.org/x/tools/internal/testenv" ) diff --git a/internal/lsp/command/util.go b/gopls/internal/lsp/command/util.go similarity index 100% rename from internal/lsp/command/util.go rename to gopls/internal/lsp/command/util.go diff --git a/internal/lsp/completion.go b/gopls/internal/lsp/completion.go similarity index 92% rename from internal/lsp/completion.go rename to gopls/internal/lsp/completion.go index 06af1bdaec0..465526668a6 100644 --- a/internal/lsp/completion.go +++ b/gopls/internal/lsp/completion.go @@ -10,13 +10,13 @@ import ( "strings" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/source/completion" - "golang.org/x/tools/internal/lsp/template" - "golang.org/x/tools/internal/lsp/work" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/completion" + "golang.org/x/tools/gopls/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/work" ) func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { diff --git a/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go similarity index 97% rename from internal/lsp/completion_test.go rename to gopls/internal/lsp/completion_test.go index d496a40a5cc..98d92b982cd 100644 --- a/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -8,9 +8,9 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/debounce.go b/gopls/internal/lsp/debounce.go similarity index 100% rename from internal/lsp/debounce.go rename to gopls/internal/lsp/debounce.go diff --git a/internal/lsp/debounce_test.go b/gopls/internal/lsp/debounce_test.go similarity index 100% rename from internal/lsp/debounce_test.go rename to gopls/internal/lsp/debounce_test.go diff --git a/internal/lsp/debug/buildinfo_go1.12.go b/gopls/internal/lsp/debug/buildinfo_go1.12.go similarity index 100% rename from internal/lsp/debug/buildinfo_go1.12.go rename to gopls/internal/lsp/debug/buildinfo_go1.12.go diff --git a/internal/lsp/debug/buildinfo_go1.18.go b/gopls/internal/lsp/debug/buildinfo_go1.18.go similarity index 100% rename from internal/lsp/debug/buildinfo_go1.18.go rename to gopls/internal/lsp/debug/buildinfo_go1.18.go diff --git a/internal/lsp/debug/info.go b/gopls/internal/lsp/debug/info.go similarity index 99% rename from internal/lsp/debug/info.go rename to gopls/internal/lsp/debug/info.go index bcc2f4f0605..8784e7455bb 100644 --- a/internal/lsp/debug/info.go +++ b/gopls/internal/lsp/debug/info.go @@ -16,7 +16,7 @@ import ( "sort" "strings" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source" ) type PrintMode int diff --git a/internal/lsp/debug/info_test.go b/gopls/internal/lsp/debug/info_test.go similarity index 100% rename from internal/lsp/debug/info_test.go rename to gopls/internal/lsp/debug/info_test.go diff --git a/internal/lsp/debug/log/log.go b/gopls/internal/lsp/debug/log/log.go similarity index 95% rename from internal/lsp/debug/log/log.go rename to gopls/internal/lsp/debug/log/log.go index 44638f8a582..e3eaa106f7e 100644 --- a/internal/lsp/debug/log/log.go +++ b/gopls/internal/lsp/debug/log/log.go @@ -12,7 +12,7 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) // Level parameterizes log severity. diff --git a/internal/lsp/debug/metrics.go b/gopls/internal/lsp/debug/metrics.go similarity index 97% rename from internal/lsp/debug/metrics.go rename to gopls/internal/lsp/debug/metrics.go index 8efc1d495e0..c8da803d6b1 100644 --- a/internal/lsp/debug/metrics.go +++ b/gopls/internal/lsp/debug/metrics.go @@ -7,7 +7,7 @@ package debug import ( "golang.org/x/tools/internal/event/export/metric" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) var ( diff --git a/internal/lsp/debug/rpc.go b/gopls/internal/lsp/debug/rpc.go similarity index 99% rename from internal/lsp/debug/rpc.go rename to gopls/internal/lsp/debug/rpc.go index 033ee3797fb..5610021479c 100644 --- a/internal/lsp/debug/rpc.go +++ b/gopls/internal/lsp/debug/rpc.go @@ -17,7 +17,7 @@ import ( "golang.org/x/tools/internal/event/core" "golang.org/x/tools/internal/event/export" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) var RPCTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` diff --git a/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go similarity index 98% rename from internal/lsp/debug/serve.go rename to gopls/internal/lsp/debug/serve.go index d343a6d65a2..da0d68d41d8 100644 --- a/internal/lsp/debug/serve.go +++ b/gopls/internal/lsp/debug/serve.go @@ -34,12 +34,12 @@ import ( "golang.org/x/tools/internal/event/export/prometheus" "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug/log" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug/log" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) type contextKeyType int diff --git a/internal/lsp/debug/trace.go b/gopls/internal/lsp/debug/trace.go similarity index 100% rename from internal/lsp/debug/trace.go rename to gopls/internal/lsp/debug/trace.go diff --git a/internal/lsp/definition.go b/gopls/internal/lsp/definition.go similarity index 92% rename from internal/lsp/definition.go rename to gopls/internal/lsp/definition.go index 9487c684327..d2ad4742b97 100644 --- a/internal/lsp/definition.go +++ b/gopls/internal/lsp/definition.go @@ -8,9 +8,9 @@ import ( "context" "fmt" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" ) func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) { diff --git a/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go similarity index 98% rename from internal/lsp/diagnostics.go rename to gopls/internal/lsp/diagnostics.go index 19776140659..d7587621a69 100644 --- a/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -16,13 +16,13 @@ import ( "time" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/log" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" - "golang.org/x/tools/internal/lsp/work" + "golang.org/x/tools/gopls/internal/lsp/debug/log" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/work" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go similarity index 98% rename from internal/lsp/fake/client.go rename to gopls/internal/lsp/fake/client.go index dd44ed0d5dd..037de8e3d19 100644 --- a/internal/lsp/fake/client.go +++ b/gopls/internal/lsp/fake/client.go @@ -8,7 +8,7 @@ import ( "context" "fmt" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // ClientHooks are called to handle the corresponding client LSP method. diff --git a/internal/lsp/fake/doc.go b/gopls/internal/lsp/fake/doc.go similarity index 100% rename from internal/lsp/fake/doc.go rename to gopls/internal/lsp/fake/doc.go diff --git a/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go similarity index 98% rename from internal/lsp/fake/edit.go rename to gopls/internal/lsp/fake/edit.go index 579c3a18de9..b45f23f9e81 100644 --- a/internal/lsp/fake/edit.go +++ b/gopls/internal/lsp/fake/edit.go @@ -9,7 +9,7 @@ import ( "sort" "strings" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // Pos represents a position in a text buffer. Both Line and Column are diff --git a/internal/lsp/fake/edit_test.go b/gopls/internal/lsp/fake/edit_test.go similarity index 100% rename from internal/lsp/fake/edit_test.go rename to gopls/internal/lsp/fake/edit_test.go diff --git a/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go similarity index 99% rename from internal/lsp/fake/editor.go rename to gopls/internal/lsp/fake/editor.go index eb721f12216..ebdf1a891d0 100644 --- a/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -18,8 +18,8 @@ import ( "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/internal/lsp/fake/editor_test.go b/gopls/internal/lsp/fake/editor_test.go similarity index 100% rename from internal/lsp/fake/editor_test.go rename to gopls/internal/lsp/fake/editor_test.go diff --git a/internal/lsp/fake/proxy.go b/gopls/internal/lsp/fake/proxy.go similarity index 100% rename from internal/lsp/fake/proxy.go rename to gopls/internal/lsp/fake/proxy.go diff --git a/internal/lsp/fake/sandbox.go b/gopls/internal/lsp/fake/sandbox.go similarity index 100% rename from internal/lsp/fake/sandbox.go rename to gopls/internal/lsp/fake/sandbox.go diff --git a/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go similarity index 99% rename from internal/lsp/fake/workdir.go rename to gopls/internal/lsp/fake/workdir.go index 0a72083bf2f..4b8c2f30fd0 100644 --- a/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -17,7 +17,7 @@ import ( "sync" "time" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/fake/workdir_test.go b/gopls/internal/lsp/fake/workdir_test.go similarity index 98% rename from internal/lsp/fake/workdir_test.go rename to gopls/internal/lsp/fake/workdir_test.go index 33fbb9fa1d5..77c6684556c 100644 --- a/internal/lsp/fake/workdir_test.go +++ b/gopls/internal/lsp/fake/workdir_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) const data = ` diff --git a/internal/lsp/fake/workdir_windows.go b/gopls/internal/lsp/fake/workdir_windows.go similarity index 100% rename from internal/lsp/fake/workdir_windows.go rename to gopls/internal/lsp/fake/workdir_windows.go diff --git a/internal/lsp/folding_range.go b/gopls/internal/lsp/folding_range.go similarity index 92% rename from internal/lsp/folding_range.go rename to gopls/internal/lsp/folding_range.go index 75f48a4498f..4a2d828e995 100644 --- a/internal/lsp/folding_range.go +++ b/gopls/internal/lsp/folding_range.go @@ -7,8 +7,8 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) { diff --git a/internal/lsp/format.go b/gopls/internal/lsp/format.go similarity index 78% rename from internal/lsp/format.go rename to gopls/internal/lsp/format.go index 19736af38bc..773a4690e92 100644 --- a/internal/lsp/format.go +++ b/gopls/internal/lsp/format.go @@ -7,10 +7,10 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/work" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/work" ) func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { diff --git a/internal/lsp/general.go b/gopls/internal/lsp/general.go similarity index 98% rename from internal/lsp/general.go rename to gopls/internal/lsp/general.go index 8ea4d7f5fa3..ee0739f2b97 100644 --- a/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -17,10 +17,10 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/helper/README.md b/gopls/internal/lsp/helper/README.md similarity index 100% rename from internal/lsp/helper/README.md rename to gopls/internal/lsp/helper/README.md diff --git a/internal/lsp/helper/helper.go b/gopls/internal/lsp/helper/helper.go similarity index 99% rename from internal/lsp/helper/helper.go rename to gopls/internal/lsp/helper/helper.go index cadda0246be..391d75adef0 100644 --- a/internal/lsp/helper/helper.go +++ b/gopls/internal/lsp/helper/helper.go @@ -56,7 +56,7 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) {{range $key, $v := .Stuff}} diff --git a/internal/lsp/highlight.go b/gopls/internal/lsp/highlight.go similarity index 85% rename from internal/lsp/highlight.go rename to gopls/internal/lsp/highlight.go index 5dc636eb58a..290444ec962 100644 --- a/internal/lsp/highlight.go +++ b/gopls/internal/lsp/highlight.go @@ -8,10 +8,10 @@ import ( "context" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" ) func (s *Server) documentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) { diff --git a/internal/lsp/hover.go b/gopls/internal/lsp/hover.go similarity index 77% rename from internal/lsp/hover.go rename to gopls/internal/lsp/hover.go index d59f5dbdb3b..2d1aae7d5b5 100644 --- a/internal/lsp/hover.go +++ b/gopls/internal/lsp/hover.go @@ -7,11 +7,11 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" - "golang.org/x/tools/internal/lsp/work" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/work" ) func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { diff --git a/internal/lsp/implementation.go b/gopls/internal/lsp/implementation.go similarity index 84% rename from internal/lsp/implementation.go rename to gopls/internal/lsp/implementation.go index 49992b9113a..0eb82652e9e 100644 --- a/internal/lsp/implementation.go +++ b/gopls/internal/lsp/implementation.go @@ -7,8 +7,8 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) implementation(ctx context.Context, params *protocol.ImplementationParams) ([]protocol.Location, error) { diff --git a/internal/lsp/inlay_hint.go b/gopls/internal/lsp/inlay_hint.go similarity index 83% rename from internal/lsp/inlay_hint.go rename to gopls/internal/lsp/inlay_hint.go index 8d8a419c235..6aceecb0d33 100644 --- a/internal/lsp/inlay_hint.go +++ b/gopls/internal/lsp/inlay_hint.go @@ -7,8 +7,8 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) inlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) { diff --git a/internal/lsp/link.go b/gopls/internal/lsp/link.go similarity index 98% rename from internal/lsp/link.go rename to gopls/internal/lsp/link.go index 65da8a54c31..456f4173048 100644 --- a/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -18,9 +18,9 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go similarity index 99% rename from internal/lsp/lsp_test.go rename to gopls/internal/lsp/lsp_test.go index e8188fe896a..6656549cd75 100644 --- a/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -15,14 +15,14 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) diff --git a/internal/lsp/lsppos/lsppos.go b/gopls/internal/lsp/lsppos/lsppos.go similarity index 98% rename from internal/lsp/lsppos/lsppos.go rename to gopls/internal/lsp/lsppos/lsppos.go index 6afad47b7d2..7db25fea9e7 100644 --- a/internal/lsp/lsppos/lsppos.go +++ b/gopls/internal/lsp/lsppos/lsppos.go @@ -22,7 +22,7 @@ import ( "sort" "unicode/utf8" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // Mapper maps utf-8 byte offsets to LSP positions for a single file. diff --git a/internal/lsp/lsppos/lsppos_test.go b/gopls/internal/lsp/lsppos/lsppos_test.go similarity index 96% rename from internal/lsp/lsppos/lsppos_test.go rename to gopls/internal/lsp/lsppos/lsppos_test.go index 8353f927681..f65b64ff804 100644 --- a/internal/lsp/lsppos/lsppos_test.go +++ b/gopls/internal/lsp/lsppos/lsppos_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) type testCase struct { diff --git a/internal/lsp/lsppos/token.go b/gopls/internal/lsp/lsppos/token.go similarity index 94% rename from internal/lsp/lsppos/token.go rename to gopls/internal/lsp/lsppos/token.go index 0f1f2b24c7b..2a16ba283ed 100644 --- a/internal/lsp/lsppos/token.go +++ b/gopls/internal/lsp/lsppos/token.go @@ -8,8 +8,8 @@ import ( "errors" "go/token" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" ) // TokenMapper maps token.Pos to LSP positions for a single file. diff --git a/internal/lsp/lsppos/token_test.go b/gopls/internal/lsp/lsppos/token_test.go similarity index 93% rename from internal/lsp/lsppos/token_test.go rename to gopls/internal/lsp/lsppos/token_test.go index c12d15026c7..a8fa6f667c7 100644 --- a/internal/lsp/lsppos/token_test.go +++ b/gopls/internal/lsp/lsppos/token_test.go @@ -8,8 +8,8 @@ import ( "go/token" "testing" - . "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func makeTokenMapper(content []byte) (*TokenMapper, *token.File) { diff --git a/internal/lsp/lsprpc/autostart_default.go b/gopls/internal/lsp/lsprpc/autostart_default.go similarity index 100% rename from internal/lsp/lsprpc/autostart_default.go rename to gopls/internal/lsp/lsprpc/autostart_default.go diff --git a/internal/lsp/lsprpc/autostart_posix.go b/gopls/internal/lsp/lsprpc/autostart_posix.go similarity index 100% rename from internal/lsp/lsprpc/autostart_posix.go rename to gopls/internal/lsp/lsprpc/autostart_posix.go diff --git a/internal/lsp/lsprpc/binder.go b/gopls/internal/lsp/lsprpc/binder.go similarity index 98% rename from internal/lsp/lsprpc/binder.go rename to gopls/internal/lsp/lsprpc/binder.go index aa2edb3309d..b12cc491ffb 100644 --- a/internal/lsp/lsprpc/binder.go +++ b/gopls/internal/lsp/lsprpc/binder.go @@ -11,7 +11,7 @@ import ( "golang.org/x/tools/internal/event" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/xcontext" ) diff --git a/internal/lsp/lsprpc/binder_test.go b/gopls/internal/lsp/lsprpc/binder_test.go similarity index 97% rename from internal/lsp/lsprpc/binder_test.go rename to gopls/internal/lsp/lsprpc/binder_test.go index f7dd830331c..8b048ab34e7 100644 --- a/internal/lsp/lsprpc/binder_test.go +++ b/gopls/internal/lsp/lsprpc/binder_test.go @@ -12,9 +12,9 @@ import ( "time" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/lsprpc" + . "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) type TestEnv struct { diff --git a/internal/lsp/lsprpc/commandinterceptor.go b/gopls/internal/lsp/lsprpc/commandinterceptor.go similarity index 96% rename from internal/lsp/lsprpc/commandinterceptor.go rename to gopls/internal/lsp/lsprpc/commandinterceptor.go index 5c36af759e1..be68efe78aa 100644 --- a/internal/lsp/lsprpc/commandinterceptor.go +++ b/gopls/internal/lsp/lsprpc/commandinterceptor.go @@ -9,7 +9,7 @@ import ( "encoding/json" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // HandlerMiddleware is a middleware that only modifies the jsonrpc2 handler. diff --git a/internal/lsp/lsprpc/commandinterceptor_test.go b/gopls/internal/lsp/lsprpc/commandinterceptor_test.go similarity index 90% rename from internal/lsp/lsprpc/commandinterceptor_test.go rename to gopls/internal/lsp/lsprpc/commandinterceptor_test.go index 06550e8fa7d..555f15130cc 100644 --- a/internal/lsp/lsprpc/commandinterceptor_test.go +++ b/gopls/internal/lsp/lsprpc/commandinterceptor_test.go @@ -8,9 +8,9 @@ import ( "context" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/lsprpc" + . "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) func TestCommandInterceptor(t *testing.T) { diff --git a/internal/lsp/lsprpc/dialer.go b/gopls/internal/lsp/lsprpc/dialer.go similarity index 100% rename from internal/lsp/lsprpc/dialer.go rename to gopls/internal/lsp/lsprpc/dialer.go diff --git a/internal/lsp/lsprpc/goenv.go b/gopls/internal/lsp/lsprpc/goenv.go similarity index 98% rename from internal/lsp/lsprpc/goenv.go rename to gopls/internal/lsp/lsprpc/goenv.go index f313724c875..c316ea07c70 100644 --- a/internal/lsp/lsprpc/goenv.go +++ b/gopls/internal/lsp/lsprpc/goenv.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func GoEnvMiddleware() (Middleware, error) { diff --git a/internal/lsp/lsprpc/goenv_test.go b/gopls/internal/lsp/lsprpc/goenv_test.go similarity index 94% rename from internal/lsp/lsprpc/goenv_test.go rename to gopls/internal/lsp/lsprpc/goenv_test.go index cdfe23c9089..b4a1b0ddaf5 100644 --- a/internal/lsp/lsprpc/goenv_test.go +++ b/gopls/internal/lsp/lsprpc/goenv_test.go @@ -8,10 +8,10 @@ import ( "context" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" - . "golang.org/x/tools/internal/lsp/lsprpc" + . "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) type initServer struct { diff --git a/internal/lsp/lsprpc/lsprpc.go b/gopls/internal/lsp/lsprpc/lsprpc.go similarity index 98% rename from internal/lsp/lsprpc/lsprpc.go rename to gopls/internal/lsp/lsprpc/lsprpc.go index 7e37229b1fd..f0fe53dcf59 100644 --- a/internal/lsp/lsprpc/lsprpc.go +++ b/gopls/internal/lsp/lsprpc/lsprpc.go @@ -21,12 +21,12 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // Unique identifiers for client/server. diff --git a/internal/lsp/lsprpc/lsprpc_test.go b/gopls/internal/lsp/lsprpc/lsprpc_test.go similarity index 98% rename from internal/lsp/lsprpc/lsprpc_test.go rename to gopls/internal/lsp/lsprpc/lsprpc_test.go index b43629b19e5..5718dfec1fa 100644 --- a/internal/lsp/lsprpc/lsprpc_test.go +++ b/gopls/internal/lsp/lsprpc/lsprpc_test.go @@ -15,10 +15,10 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) diff --git a/internal/lsp/lsprpc/middleware.go b/gopls/internal/lsp/lsprpc/middleware.go similarity index 100% rename from internal/lsp/lsprpc/middleware.go rename to gopls/internal/lsp/lsprpc/middleware.go diff --git a/internal/lsp/lsprpc/middleware_test.go b/gopls/internal/lsp/lsprpc/middleware_test.go similarity index 97% rename from internal/lsp/lsprpc/middleware_test.go rename to gopls/internal/lsp/lsprpc/middleware_test.go index a385f10037a..a37294a31a1 100644 --- a/internal/lsp/lsprpc/middleware_test.go +++ b/gopls/internal/lsp/lsprpc/middleware_test.go @@ -12,7 +12,7 @@ import ( "time" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - . "golang.org/x/tools/internal/lsp/lsprpc" + . "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) var noopBinder = BinderFunc(func(context.Context, *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { diff --git a/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go similarity index 97% rename from internal/lsp/mod/code_lens.go rename to gopls/internal/lsp/mod/code_lens.go index 1de25c2f8f4..2068e60df4a 100644 --- a/internal/lsp/mod/code_lens.go +++ b/gopls/internal/lsp/mod/code_lens.go @@ -11,9 +11,9 @@ import ( "path/filepath" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) // LensFuncs returns the supported lensFuncs for go.mod files. diff --git a/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go similarity index 94% rename from internal/lsp/mod/diagnostics.go rename to gopls/internal/lsp/mod/diagnostics.go index d866cfd8148..96b95c059d4 100644 --- a/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -11,10 +11,10 @@ import ( "fmt" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { diff --git a/internal/lsp/mod/format.go b/gopls/internal/lsp/mod/format.go similarity index 89% rename from internal/lsp/mod/format.go rename to gopls/internal/lsp/mod/format.go index c3557663272..d2bce4c8a75 100644 --- a/internal/lsp/mod/format.go +++ b/gopls/internal/lsp/mod/format.go @@ -8,8 +8,8 @@ import ( "context" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) { diff --git a/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go similarity index 97% rename from internal/lsp/mod/hover.go rename to gopls/internal/lsp/mod/hover.go index 1461d52edbd..0f3dfc478de 100644 --- a/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -12,8 +12,8 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { diff --git a/internal/lsp/mod/mod_test.go b/gopls/internal/lsp/mod/mod_test.go similarity index 91% rename from internal/lsp/mod/mod_test.go rename to gopls/internal/lsp/mod/mod_test.go index 56af9860b9d..75c16660e5a 100644 --- a/internal/lsp/mod/mod_test.go +++ b/gopls/internal/lsp/mod/mod_test.go @@ -10,9 +10,9 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) diff --git a/internal/lsp/mod/testdata/unchanged/go.mod b/gopls/internal/lsp/mod/testdata/unchanged/go.mod similarity index 100% rename from internal/lsp/mod/testdata/unchanged/go.mod rename to gopls/internal/lsp/mod/testdata/unchanged/go.mod diff --git a/internal/lsp/mod/testdata/unchanged/main.go b/gopls/internal/lsp/mod/testdata/unchanged/main.go similarity index 100% rename from internal/lsp/mod/testdata/unchanged/main.go rename to gopls/internal/lsp/mod/testdata/unchanged/main.go diff --git a/internal/lsp/progress/progress.go b/gopls/internal/lsp/progress/progress.go similarity index 98% rename from internal/lsp/progress/progress.go rename to gopls/internal/lsp/progress/progress.go index ee63bb76cde..31a8cb67d1d 100644 --- a/internal/lsp/progress/progress.go +++ b/gopls/internal/lsp/progress/progress.go @@ -13,8 +13,8 @@ import ( "sync" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/xcontext" ) diff --git a/internal/lsp/progress/progress_test.go b/gopls/internal/lsp/progress/progress_test.go similarity index 98% rename from internal/lsp/progress/progress_test.go rename to gopls/internal/lsp/progress/progress_test.go index 6e901d17e97..ef87eba121a 100644 --- a/internal/lsp/progress/progress_test.go +++ b/gopls/internal/lsp/progress/progress_test.go @@ -10,7 +10,7 @@ import ( "sync" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) type fakeClient struct { diff --git a/internal/lsp/protocol/codeactionkind.go b/gopls/internal/lsp/protocol/codeactionkind.go similarity index 100% rename from internal/lsp/protocol/codeactionkind.go rename to gopls/internal/lsp/protocol/codeactionkind.go diff --git a/internal/lsp/protocol/context.go b/gopls/internal/lsp/protocol/context.go similarity index 100% rename from internal/lsp/protocol/context.go rename to gopls/internal/lsp/protocol/context.go diff --git a/internal/lsp/protocol/doc.go b/gopls/internal/lsp/protocol/doc.go similarity index 100% rename from internal/lsp/protocol/doc.go rename to gopls/internal/lsp/protocol/doc.go diff --git a/internal/lsp/protocol/enums.go b/gopls/internal/lsp/protocol/enums.go similarity index 100% rename from internal/lsp/protocol/enums.go rename to gopls/internal/lsp/protocol/enums.go diff --git a/internal/lsp/protocol/log.go b/gopls/internal/lsp/protocol/log.go similarity index 100% rename from internal/lsp/protocol/log.go rename to gopls/internal/lsp/protocol/log.go diff --git a/internal/lsp/protocol/protocol.go b/gopls/internal/lsp/protocol/protocol.go similarity index 100% rename from internal/lsp/protocol/protocol.go rename to gopls/internal/lsp/protocol/protocol.go diff --git a/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go similarity index 100% rename from internal/lsp/protocol/span.go rename to gopls/internal/lsp/protocol/span.go diff --git a/internal/lsp/protocol/tsclient.go b/gopls/internal/lsp/protocol/tsclient.go similarity index 100% rename from internal/lsp/protocol/tsclient.go rename to gopls/internal/lsp/protocol/tsclient.go diff --git a/internal/lsp/protocol/tsdocument_changes.go b/gopls/internal/lsp/protocol/tsdocument_changes.go similarity index 100% rename from internal/lsp/protocol/tsdocument_changes.go rename to gopls/internal/lsp/protocol/tsdocument_changes.go diff --git a/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go similarity index 100% rename from internal/lsp/protocol/tsprotocol.go rename to gopls/internal/lsp/protocol/tsprotocol.go diff --git a/internal/lsp/protocol/tsserver.go b/gopls/internal/lsp/protocol/tsserver.go similarity index 100% rename from internal/lsp/protocol/tsserver.go rename to gopls/internal/lsp/protocol/tsserver.go diff --git a/internal/lsp/protocol/typescript/README.md b/gopls/internal/lsp/protocol/typescript/README.md similarity index 100% rename from internal/lsp/protocol/typescript/README.md rename to gopls/internal/lsp/protocol/typescript/README.md diff --git a/internal/lsp/protocol/typescript/code.ts b/gopls/internal/lsp/protocol/typescript/code.ts similarity index 100% rename from internal/lsp/protocol/typescript/code.ts rename to gopls/internal/lsp/protocol/typescript/code.ts diff --git a/internal/lsp/protocol/typescript/tsconfig.json b/gopls/internal/lsp/protocol/typescript/tsconfig.json similarity index 100% rename from internal/lsp/protocol/typescript/tsconfig.json rename to gopls/internal/lsp/protocol/typescript/tsconfig.json diff --git a/internal/lsp/protocol/typescript/util.ts b/gopls/internal/lsp/protocol/typescript/util.ts similarity index 100% rename from internal/lsp/protocol/typescript/util.ts rename to gopls/internal/lsp/protocol/typescript/util.ts diff --git a/internal/lsp/references.go b/gopls/internal/lsp/references.go similarity index 87% rename from internal/lsp/references.go rename to gopls/internal/lsp/references.go index f96e5532cb5..6f4e3ee2060 100644 --- a/internal/lsp/references.go +++ b/gopls/internal/lsp/references.go @@ -7,9 +7,9 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" ) func (s *Server) references(ctx context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) { diff --git a/internal/lsp/regtest/doc.go b/gopls/internal/lsp/regtest/doc.go similarity index 95% rename from internal/lsp/regtest/doc.go rename to gopls/internal/lsp/regtest/doc.go index e97276965b1..39eddd8dcff 100644 --- a/internal/lsp/regtest/doc.go +++ b/gopls/internal/lsp/regtest/doc.go @@ -19,7 +19,7 @@ // - the Env type provides a collection of resources to use in writing tests // (for example a temporary working directory and fake text editor) // - user interactions with these resources are scripted using test wrappers -// around the API provided by the golang.org/x/tools/internal/lsp/fake +// around the API provided by the golang.org/x/tools/gopls/internal/lsp/fake // package. // // Regressions are expressed in terms of Expectations, which at a high level diff --git a/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go similarity index 99% rename from internal/lsp/regtest/env.go rename to gopls/internal/lsp/regtest/env.go index 77744ae614a..23be64e9293 100644 --- a/internal/lsp/regtest/env.go +++ b/gopls/internal/lsp/regtest/env.go @@ -12,8 +12,8 @@ import ( "testing" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // Env holds the building blocks of an editor testing environment, providing diff --git a/internal/lsp/regtest/env_test.go b/gopls/internal/lsp/regtest/env_test.go similarity index 97% rename from internal/lsp/regtest/env_test.go rename to gopls/internal/lsp/regtest/env_test.go index f54f7f29dc4..824c602df4d 100644 --- a/internal/lsp/regtest/env_test.go +++ b/gopls/internal/lsp/regtest/env_test.go @@ -9,7 +9,7 @@ import ( "encoding/json" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func TestProgressUpdating(t *testing.T) { diff --git a/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go similarity index 99% rename from internal/lsp/regtest/expectation.go rename to gopls/internal/lsp/regtest/expectation.go index e85f40955d4..5234f7e9b94 100644 --- a/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -9,9 +9,9 @@ import ( "regexp" "strings" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) diff --git a/internal/lsp/regtest/regtest.go b/gopls/internal/lsp/regtest/regtest.go similarity index 97% rename from internal/lsp/regtest/regtest.go rename to gopls/internal/lsp/regtest/regtest.go index d499bde79b4..2698aaba743 100644 --- a/internal/lsp/regtest/regtest.go +++ b/gopls/internal/lsp/regtest/regtest.go @@ -15,8 +15,8 @@ import ( "testing" "time" - "golang.org/x/tools/internal/lsp/cmd" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/tool" diff --git a/internal/lsp/regtest/runner.go b/gopls/internal/lsp/regtest/runner.go similarity index 98% rename from internal/lsp/regtest/runner.go rename to gopls/internal/lsp/regtest/runner.go index 93bb1397eb2..effc8aae4b2 100644 --- a/internal/lsp/regtest/runner.go +++ b/gopls/internal/lsp/regtest/runner.go @@ -25,12 +25,12 @@ import ( "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/lsprpc" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/xcontext" diff --git a/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go similarity index 98% rename from internal/lsp/regtest/wrappers.go rename to gopls/internal/lsp/regtest/wrappers.go index e8f49a68dfe..b5d1783e55a 100644 --- a/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -8,9 +8,9 @@ import ( "encoding/json" "path" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func (e *Env) ChangeFilesOnDisk(events []fake.FileEvent) { diff --git a/internal/lsp/rename.go b/gopls/internal/lsp/rename.go similarity index 94% rename from internal/lsp/rename.go rename to gopls/internal/lsp/rename.go index e78e2164473..5127b5df1f4 100644 --- a/internal/lsp/rename.go +++ b/gopls/internal/lsp/rename.go @@ -7,8 +7,8 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { diff --git a/internal/lsp/reset_golden.sh b/gopls/internal/lsp/reset_golden.sh similarity index 100% rename from internal/lsp/reset_golden.sh rename to gopls/internal/lsp/reset_golden.sh diff --git a/internal/lsp/safetoken/safetoken.go b/gopls/internal/lsp/safetoken/safetoken.go similarity index 100% rename from internal/lsp/safetoken/safetoken.go rename to gopls/internal/lsp/safetoken/safetoken.go diff --git a/internal/lsp/safetoken/safetoken_test.go b/gopls/internal/lsp/safetoken/safetoken_test.go similarity index 84% rename from internal/lsp/safetoken/safetoken_test.go rename to gopls/internal/lsp/safetoken/safetoken_test.go index 43d73a74d78..1486d68f327 100644 --- a/internal/lsp/safetoken/safetoken_test.go +++ b/gopls/internal/lsp/safetoken/safetoken_test.go @@ -21,7 +21,7 @@ func TestTokenOffset(t *testing.T) { pkgs, err := packages.Load(&packages.Config{ Fset: fset, Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, - }, "go/token", "golang.org/x/tools/internal/lsp/...", "golang.org/x/tools/gopls/...") + }, "go/token", "golang.org/x/tools/gopls/internal/lsp/...", "golang.org/x/tools/gopls/...") if err != nil { t.Fatal(err) } @@ -30,7 +30,7 @@ func TestTokenOffset(t *testing.T) { switch pkg.PkgPath { case "go/token": tokenPkg = pkg - case "golang.org/x/tools/internal/lsp/safetoken": + case "golang.org/x/tools/gopls/internal/lsp/safetoken": safePkg = pkg } } @@ -39,7 +39,7 @@ func TestTokenOffset(t *testing.T) { t.Fatal("missing package go/token") } if safePkg == nil { - t.Fatal("missing package golang.org/x/tools/internal/lsp/safetoken") + t.Fatal("missing package golang.org/x/tools/gopls/internal/lsp/safetoken") } fileObj := tokenPkg.Types.Scope().Lookup("File") @@ -58,7 +58,7 @@ func TestTokenOffset(t *testing.T) { if safeOffset.Pos() <= ident.Pos() && ident.Pos() <= safeOffset.Scope().End() { continue // accepted usage } - t.Errorf(`%s: Unexpected use of (*go/token.File).Offset. Please use golang.org/x/tools/internal/lsp/safetoken.Offset instead.`, fset.Position(ident.Pos())) + t.Errorf(`%s: Unexpected use of (*go/token.File).Offset. Please use golang.org/x/tools/gopls/internal/lsp/safetoken.Offset instead.`, fset.Position(ident.Pos())) } } } diff --git a/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go similarity index 99% rename from internal/lsp/semantic.go rename to gopls/internal/lsp/semantic.go index 648d5c446f1..641c2c477de 100644 --- a/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -19,10 +19,10 @@ import ( "time" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/server.go b/gopls/internal/lsp/server.go similarity index 97% rename from internal/lsp/server.go rename to gopls/internal/lsp/server.go index fb820cccfea..25ef5654d9c 100644 --- a/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -11,9 +11,9 @@ import ( "sync" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/progress" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/progress" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/server_gen.go b/gopls/internal/lsp/server_gen.go similarity index 99% rename from internal/lsp/server_gen.go rename to gopls/internal/lsp/server_gen.go index 4e9db0efa19..42635c69c29 100644 --- a/internal/lsp/server_gen.go +++ b/gopls/internal/lsp/server_gen.go @@ -9,7 +9,7 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { diff --git a/internal/lsp/signature_help.go b/gopls/internal/lsp/signature_help.go similarity index 85% rename from internal/lsp/signature_help.go rename to gopls/internal/lsp/signature_help.go index 24dee1b9a8d..8a343fbec81 100644 --- a/internal/lsp/signature_help.go +++ b/gopls/internal/lsp/signature_help.go @@ -8,9 +8,9 @@ import ( "context" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) signatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) { diff --git a/internal/lsp/snippet/snippet_builder.go b/gopls/internal/lsp/snippet/snippet_builder.go similarity index 100% rename from internal/lsp/snippet/snippet_builder.go rename to gopls/internal/lsp/snippet/snippet_builder.go diff --git a/internal/lsp/snippet/snippet_builder_test.go b/gopls/internal/lsp/snippet/snippet_builder_test.go similarity index 100% rename from internal/lsp/snippet/snippet_builder_test.go rename to gopls/internal/lsp/snippet/snippet_builder_test.go diff --git a/internal/lsp/source/add_import.go b/gopls/internal/lsp/source/add_import.go similarity index 93% rename from internal/lsp/source/add_import.go rename to gopls/internal/lsp/source/add_import.go index 816acc2c25b..2fc03e5d758 100644 --- a/internal/lsp/source/add_import.go +++ b/gopls/internal/lsp/source/add_import.go @@ -8,7 +8,7 @@ import ( "context" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // AddImport adds a single import statement to the given file diff --git a/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go similarity index 100% rename from internal/lsp/source/api_json.go rename to gopls/internal/lsp/source/api_json.go diff --git a/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go similarity index 99% rename from internal/lsp/source/call_hierarchy.go rename to gopls/internal/lsp/source/call_hierarchy.go index 4e7daf0f9bc..298c712d2ea 100644 --- a/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -15,8 +15,8 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go similarity index 98% rename from internal/lsp/source/code_lens.go rename to gopls/internal/lsp/source/code_lens.go index 85a0a2f522d..6f1d720b37f 100644 --- a/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -13,8 +13,8 @@ import ( "regexp" "strings" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/comment.go b/gopls/internal/lsp/source/comment.go similarity index 100% rename from internal/lsp/source/comment.go rename to gopls/internal/lsp/source/comment.go diff --git a/internal/lsp/source/comment_go118.go b/gopls/internal/lsp/source/comment_go118.go similarity index 100% rename from internal/lsp/source/comment_go118.go rename to gopls/internal/lsp/source/comment_go118.go diff --git a/internal/lsp/source/comment_go118_test.go b/gopls/internal/lsp/source/comment_go118_test.go similarity index 100% rename from internal/lsp/source/comment_go118_test.go rename to gopls/internal/lsp/source/comment_go118_test.go diff --git a/internal/lsp/source/completion/builtin.go b/gopls/internal/lsp/source/completion/builtin.go similarity index 100% rename from internal/lsp/source/completion/builtin.go rename to gopls/internal/lsp/source/completion/builtin.go diff --git a/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go similarity index 99% rename from internal/lsp/source/completion/completion.go rename to gopls/internal/lsp/source/completion/completion.go index e230fc58e58..2ffd9bf5d60 100644 --- a/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -25,10 +25,10 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/fuzzy" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/fuzzy" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/source/completion/deep_completion.go b/gopls/internal/lsp/source/completion/deep_completion.go similarity index 100% rename from internal/lsp/source/completion/deep_completion.go rename to gopls/internal/lsp/source/completion/deep_completion.go diff --git a/internal/lsp/source/completion/deep_completion_test.go b/gopls/internal/lsp/source/completion/deep_completion_test.go similarity index 100% rename from internal/lsp/source/completion/deep_completion_test.go rename to gopls/internal/lsp/source/completion/deep_completion_test.go diff --git a/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go similarity index 95% rename from internal/lsp/source/completion/definition.go rename to gopls/internal/lsp/source/completion/definition.go index 7644fc443d6..e61a9fa2620 100644 --- a/internal/lsp/source/completion/definition.go +++ b/gopls/internal/lsp/source/completion/definition.go @@ -12,9 +12,9 @@ import ( "unicode" "unicode/utf8" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go similarity index 97% rename from internal/lsp/source/completion/format.go rename to gopls/internal/lsp/source/completion/format.go index d34cee22ad2..bcf523fac3e 100644 --- a/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -15,10 +15,10 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/source/completion/fuzz.go b/gopls/internal/lsp/source/completion/fuzz.go similarity index 98% rename from internal/lsp/source/completion/fuzz.go rename to gopls/internal/lsp/source/completion/fuzz.go index 92349ab9343..d7912ceabc6 100644 --- a/internal/lsp/source/completion/fuzz.go +++ b/gopls/internal/lsp/source/completion/fuzz.go @@ -11,7 +11,7 @@ import ( "go/types" "strings" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // golang/go#51089 diff --git a/internal/lsp/source/completion/keywords.go b/gopls/internal/lsp/source/completion/keywords.go similarity index 97% rename from internal/lsp/source/completion/keywords.go rename to gopls/internal/lsp/source/completion/keywords.go index bbf59b0221f..a068ca2d57c 100644 --- a/internal/lsp/source/completion/keywords.go +++ b/gopls/internal/lsp/source/completion/keywords.go @@ -7,8 +7,8 @@ package completion import ( "go/ast" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) const ( diff --git a/internal/lsp/source/completion/labels.go b/gopls/internal/lsp/source/completion/labels.go similarity index 100% rename from internal/lsp/source/completion/labels.go rename to gopls/internal/lsp/source/completion/labels.go diff --git a/internal/lsp/source/completion/literal.go b/gopls/internal/lsp/source/completion/literal.go similarity index 99% rename from internal/lsp/source/completion/literal.go rename to gopls/internal/lsp/source/completion/literal.go index 139ec17dc05..8f0e43c1af6 100644 --- a/internal/lsp/source/completion/literal.go +++ b/gopls/internal/lsp/source/completion/literal.go @@ -12,9 +12,9 @@ import ( "unicode" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go similarity index 98% rename from internal/lsp/source/completion/package.go rename to gopls/internal/lsp/source/completion/package.go index 566d8ee2a05..1f3649b428b 100644 --- a/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -18,11 +18,11 @@ import ( "strings" "unicode" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/fuzzy" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/fuzzy" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/completion/package_test.go b/gopls/internal/lsp/source/completion/package_test.go similarity index 100% rename from internal/lsp/source/completion/package_test.go rename to gopls/internal/lsp/source/completion/package_test.go diff --git a/internal/lsp/source/completion/postfix_snippets.go b/gopls/internal/lsp/source/completion/postfix_snippets.go similarity index 98% rename from internal/lsp/source/completion/postfix_snippets.go rename to gopls/internal/lsp/source/completion/postfix_snippets.go index aa8454f8e98..414db42592e 100644 --- a/internal/lsp/source/completion/postfix_snippets.go +++ b/gopls/internal/lsp/source/completion/postfix_snippets.go @@ -18,9 +18,9 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" ) // Postfix snippets are artificial methods that allow the user to diff --git a/internal/lsp/source/completion/printf.go b/gopls/internal/lsp/source/completion/printf.go similarity index 100% rename from internal/lsp/source/completion/printf.go rename to gopls/internal/lsp/source/completion/printf.go diff --git a/internal/lsp/source/completion/printf_test.go b/gopls/internal/lsp/source/completion/printf_test.go similarity index 100% rename from internal/lsp/source/completion/printf_test.go rename to gopls/internal/lsp/source/completion/printf_test.go diff --git a/internal/lsp/source/completion/snippet.go b/gopls/internal/lsp/source/completion/snippet.go similarity index 98% rename from internal/lsp/source/completion/snippet.go rename to gopls/internal/lsp/source/completion/snippet.go index 72c351f946e..1a9ebb1d4be 100644 --- a/internal/lsp/source/completion/snippet.go +++ b/gopls/internal/lsp/source/completion/snippet.go @@ -7,7 +7,7 @@ package completion import ( "go/ast" - "golang.org/x/tools/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/snippet" ) // structFieldSnippets calculates the snippet for struct literal field names. diff --git a/internal/lsp/source/completion/statements.go b/gopls/internal/lsp/source/completion/statements.go similarity index 98% rename from internal/lsp/source/completion/statements.go rename to gopls/internal/lsp/source/completion/statements.go index d8e30a2d5b2..b43629c0394 100644 --- a/internal/lsp/source/completion/statements.go +++ b/gopls/internal/lsp/source/completion/statements.go @@ -10,9 +10,9 @@ import ( "go/token" "go/types" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" ) // addStatementCandidates adds full statement completion candidates diff --git a/internal/lsp/source/completion/util.go b/gopls/internal/lsp/source/completion/util.go similarity index 98% rename from internal/lsp/source/completion/util.go rename to gopls/internal/lsp/source/completion/util.go index e0a264bef9e..f40c0b31dd0 100644 --- a/internal/lsp/source/completion/util.go +++ b/gopls/internal/lsp/source/completion/util.go @@ -10,9 +10,9 @@ import ( "go/types" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/source/completion/util_test.go b/gopls/internal/lsp/source/completion/util_test.go similarity index 100% rename from internal/lsp/source/completion/util_test.go rename to gopls/internal/lsp/source/completion/util_test.go diff --git a/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go similarity index 97% rename from internal/lsp/source/diagnostics.go rename to gopls/internal/lsp/source/diagnostics.go index e393c2f9426..faf49191e3e 100644 --- a/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -7,7 +7,7 @@ package source import ( "context" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go similarity index 99% rename from internal/lsp/source/extract.go rename to gopls/internal/lsp/source/extract.go index a4e0a148adb..b09ed9ee6c3 100644 --- a/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -18,8 +18,8 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/safetoken" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go similarity index 95% rename from internal/lsp/source/fix.go rename to gopls/internal/lsp/source/fix.go index dce279e2016..bce2536976e 100644 --- a/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -12,10 +12,10 @@ import ( "go/types" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/internal/lsp/analysis/fillstruct" - "golang.org/x/tools/internal/lsp/analysis/undeclaredname" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" + "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/folding_range.go b/gopls/internal/lsp/source/folding_range.go similarity index 99% rename from internal/lsp/source/folding_range.go rename to gopls/internal/lsp/source/folding_range.go index b70cb4decd8..157dff2158d 100644 --- a/internal/lsp/source/folding_range.go +++ b/gopls/internal/lsp/source/folding_range.go @@ -11,7 +11,7 @@ import ( "sort" "strings" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // FoldingRangeInfo holds range and kind info of folding for an ast.Node diff --git a/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go similarity index 98% rename from internal/lsp/source/format.go rename to gopls/internal/lsp/source/format.go index 1dd914ec3ff..e2301171be6 100644 --- a/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -18,10 +18,10 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" ) // Format formats a file with a given range. diff --git a/internal/lsp/source/format_test.go b/gopls/internal/lsp/source/format_test.go similarity index 97% rename from internal/lsp/source/format_test.go rename to gopls/internal/lsp/source/format_test.go index ab120124e5a..fac80c3115b 100644 --- a/internal/lsp/source/format_test.go +++ b/gopls/internal/lsp/source/format_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) func TestImportPrefix(t *testing.T) { diff --git a/internal/lsp/source/gc_annotations.go b/gopls/internal/lsp/source/gc_annotations.go similarity index 99% rename from internal/lsp/source/gc_annotations.go rename to gopls/internal/lsp/source/gc_annotations.go index 3616bbfb1cf..111cd4fb8c0 100644 --- a/internal/lsp/source/gc_annotations.go +++ b/gopls/internal/lsp/source/gc_annotations.go @@ -15,7 +15,7 @@ import ( "strings" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go similarity index 99% rename from internal/lsp/source/highlight.go rename to gopls/internal/lsp/source/highlight.go index 4be078b7fce..0cde62825ae 100644 --- a/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -14,7 +14,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Range, error) { diff --git a/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go similarity index 99% rename from internal/lsp/source/hover.go rename to gopls/internal/lsp/source/hover.go index 9ab1023d68c..74b7e57b07c 100644 --- a/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -23,9 +23,9 @@ import ( "golang.org/x/text/unicode/runenames" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go similarity index 99% rename from internal/lsp/source/identifier.go rename to gopls/internal/lsp/source/identifier.go index 5378ae840ed..9ab3fe7ad97 100644 --- a/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -17,9 +17,9 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/source/identifier_test.go b/gopls/internal/lsp/source/identifier_test.go similarity index 100% rename from internal/lsp/source/identifier_test.go rename to gopls/internal/lsp/source/identifier_test.go diff --git a/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go similarity index 99% rename from internal/lsp/source/implementation.go rename to gopls/internal/lsp/source/implementation.go index 39a9289d1d6..f612e35d253 100644 --- a/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -14,8 +14,8 @@ import ( "sort" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go similarity index 99% rename from internal/lsp/source/inlay_hint.go rename to gopls/internal/lsp/source/inlay_hint.go index 4fb1cfb44f8..9d9152f87e8 100644 --- a/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -14,8 +14,8 @@ import ( "strings" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go similarity index 100% rename from internal/lsp/source/known_packages.go rename to gopls/internal/lsp/source/known_packages.go diff --git a/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go similarity index 97% rename from internal/lsp/source/options.go rename to gopls/internal/lsp/source/options.go index 984f228163b..c904340c970 100644 --- a/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -50,24 +50,24 @@ import ( "golang.org/x/tools/go/analysis/passes/unusedresult" "golang.org/x/tools/go/analysis/passes/unusedwrite" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/analysis/embeddirective" - "golang.org/x/tools/internal/lsp/analysis/fillreturns" - "golang.org/x/tools/internal/lsp/analysis/fillstruct" - "golang.org/x/tools/internal/lsp/analysis/infertypeargs" - "golang.org/x/tools/internal/lsp/analysis/nonewvars" - "golang.org/x/tools/internal/lsp/analysis/noresultvalues" - "golang.org/x/tools/internal/lsp/analysis/simplifycompositelit" - "golang.org/x/tools/internal/lsp/analysis/simplifyrange" - "golang.org/x/tools/internal/lsp/analysis/simplifyslice" - "golang.org/x/tools/internal/lsp/analysis/stubmethods" - "golang.org/x/tools/internal/lsp/analysis/undeclaredname" - "golang.org/x/tools/internal/lsp/analysis/unusedparams" - "golang.org/x/tools/internal/lsp/analysis/unusedvariable" - "golang.org/x/tools/internal/lsp/analysis/useany" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/analysis/embeddirective" + "golang.org/x/tools/gopls/internal/lsp/analysis/fillreturns" + "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" + "golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs" + "golang.org/x/tools/gopls/internal/lsp/analysis/nonewvars" + "golang.org/x/tools/gopls/internal/lsp/analysis/noresultvalues" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifycompositelit" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyrange" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyslice" + "golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods" + "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" + "golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams" + "golang.org/x/tools/gopls/internal/lsp/analysis/unusedvariable" + "golang.org/x/tools/gopls/internal/lsp/analysis/useany" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/myers" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) var ( diff --git a/internal/lsp/source/options_test.go b/gopls/internal/lsp/source/options_test.go similarity index 100% rename from internal/lsp/source/options_test.go rename to gopls/internal/lsp/source/options_test.go diff --git a/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go similarity index 98% rename from internal/lsp/source/references.go rename to gopls/internal/lsp/source/references.go index 2bbdc0741ca..eacadab8540 100644 --- a/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -16,8 +16,8 @@ import ( "strconv" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go similarity index 99% rename from internal/lsp/source/rename.go rename to gopls/internal/lsp/source/rename.go index 85d7c12f945..e31dfd5e095 100644 --- a/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -18,8 +18,8 @@ import ( "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/refactor/satisfy" ) diff --git a/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go similarity index 100% rename from internal/lsp/source/rename_check.go rename to gopls/internal/lsp/source/rename_check.go diff --git a/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go similarity index 99% rename from internal/lsp/source/signature_help.go rename to gopls/internal/lsp/source/signature_help.go index 5b087e83769..f1a4bacce01 100644 --- a/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.SignatureInformation, int, error) { diff --git a/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go similarity index 98% rename from internal/lsp/source/source_test.go rename to gopls/internal/lsp/source/source_test.go index 761b9109240..25a642a2cbf 100644 --- a/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -15,15 +15,15 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/fuzzy" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/source/completion" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/fuzzy" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/completion" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) diff --git a/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go similarity index 98% rename from internal/lsp/source/stub.go rename to gopls/internal/lsp/source/stub.go index 0d1981795f2..9a17802cca8 100644 --- a/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -17,9 +17,9 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/internal/lsp/analysis/stubmethods" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/source/symbols.go b/gopls/internal/lsp/source/symbols.go similarity index 99% rename from internal/lsp/source/symbols.go rename to gopls/internal/lsp/source/symbols.go index 074b24eba01..802de37c342 100644 --- a/internal/lsp/source/symbols.go +++ b/gopls/internal/lsp/source/symbols.go @@ -11,7 +11,7 @@ import ( "go/types" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.DocumentSymbol, error) { diff --git a/internal/lsp/source/types_format.go b/gopls/internal/lsp/source/types_format.go similarity index 99% rename from internal/lsp/source/types_format.go rename to gopls/internal/lsp/source/types_format.go index 756d02de22a..036c8c77206 100644 --- a/internal/lsp/source/types_format.go +++ b/gopls/internal/lsp/source/types_format.go @@ -16,8 +16,8 @@ import ( "strings" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go similarity index 99% rename from internal/lsp/source/util.go rename to gopls/internal/lsp/source/util.go index 78448af4176..da9c3b5d436 100644 --- a/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -18,8 +18,8 @@ import ( "strings" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/util_test.go b/gopls/internal/lsp/source/util_test.go similarity index 97% rename from internal/lsp/source/util_test.go rename to gopls/internal/lsp/source/util_test.go index fe505e4d06c..e0b2a29e3bb 100644 --- a/internal/lsp/source/util_test.go +++ b/gopls/internal/lsp/source/util_test.go @@ -10,7 +10,7 @@ import ( "go/token" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go similarity index 99% rename from internal/lsp/source/view.go rename to gopls/internal/lsp/source/view.go index 498299e1f7f..8bf0344e7b9 100644 --- a/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -23,8 +23,8 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/progress" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/progress" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go similarity index 99% rename from internal/lsp/source/workspace_symbol.go rename to gopls/internal/lsp/source/workspace_symbol.go index e9da569d02c..8fe23889c75 100644 --- a/internal/lsp/source/workspace_symbol.go +++ b/gopls/internal/lsp/source/workspace_symbol.go @@ -17,8 +17,8 @@ import ( "unicode" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/fuzzy" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/fuzzy" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/source/workspace_symbol_test.go b/gopls/internal/lsp/source/workspace_symbol_test.go similarity index 100% rename from internal/lsp/source/workspace_symbol_test.go rename to gopls/internal/lsp/source/workspace_symbol_test.go diff --git a/internal/lsp/symbols.go b/gopls/internal/lsp/symbols.go similarity index 89% rename from internal/lsp/symbols.go rename to gopls/internal/lsp/symbols.go index f04e4572dba..0cc0b33a5b5 100644 --- a/internal/lsp/symbols.go +++ b/gopls/internal/lsp/symbols.go @@ -8,10 +8,10 @@ import ( "context" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" ) func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]interface{}, error) { diff --git a/internal/lsp/template/completion.go b/gopls/internal/lsp/template/completion.go similarity index 98% rename from internal/lsp/template/completion.go rename to gopls/internal/lsp/template/completion.go index 13dbdf1e525..7040081e8aa 100644 --- a/internal/lsp/template/completion.go +++ b/gopls/internal/lsp/template/completion.go @@ -12,8 +12,8 @@ import ( "go/token" "strings" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) // information needed for completion diff --git a/internal/lsp/template/completion_test.go b/gopls/internal/lsp/template/completion_test.go similarity index 98% rename from internal/lsp/template/completion_test.go rename to gopls/internal/lsp/template/completion_test.go index bfcdb537202..0fc478842ee 100644 --- a/internal/lsp/template/completion_test.go +++ b/gopls/internal/lsp/template/completion_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func init() { diff --git a/internal/lsp/template/highlight.go b/gopls/internal/lsp/template/highlight.go similarity index 96% rename from internal/lsp/template/highlight.go rename to gopls/internal/lsp/template/highlight.go index a45abaf5020..1e06b92085e 100644 --- a/internal/lsp/template/highlight.go +++ b/gopls/internal/lsp/template/highlight.go @@ -9,8 +9,8 @@ import ( "fmt" "regexp" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func Highlight(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, loc protocol.Position) ([]protocol.DocumentHighlight, error) { diff --git a/internal/lsp/template/implementations.go b/gopls/internal/lsp/template/implementations.go similarity index 98% rename from internal/lsp/template/implementations.go rename to gopls/internal/lsp/template/implementations.go index cda3e7ef0a5..3d14f6b4a9a 100644 --- a/internal/lsp/template/implementations.go +++ b/gopls/internal/lsp/template/implementations.go @@ -11,8 +11,8 @@ import ( "strconv" "time" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go similarity index 99% rename from internal/lsp/template/parse.go rename to gopls/internal/lsp/template/parse.go index 181a5228fd2..f2e4f25bff5 100644 --- a/internal/lsp/template/parse.go +++ b/gopls/internal/lsp/template/parse.go @@ -25,8 +25,8 @@ import ( "unicode/utf8" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/template/parse_test.go b/gopls/internal/lsp/template/parse_test.go similarity index 100% rename from internal/lsp/template/parse_test.go rename to gopls/internal/lsp/template/parse_test.go diff --git a/internal/lsp/template/symbols.go b/gopls/internal/lsp/template/symbols.go similarity index 98% rename from internal/lsp/template/symbols.go rename to gopls/internal/lsp/template/symbols.go index ce5a1e799b7..24f9604c100 100644 --- a/internal/lsp/template/symbols.go +++ b/gopls/internal/lsp/template/symbols.go @@ -12,8 +12,8 @@ import ( "unicode/utf8" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) // in local coordinates, to be translated to protocol.DocumentSymbol diff --git a/internal/lsp/testdata/%percent/perc%ent.go b/gopls/internal/lsp/testdata/%percent/perc%ent.go similarity index 100% rename from internal/lsp/testdata/%percent/perc%ent.go rename to gopls/internal/lsp/testdata/%percent/perc%ent.go diff --git a/internal/lsp/testdata/addimport/addimport.go.golden b/gopls/internal/lsp/testdata/addimport/addimport.go.golden similarity index 100% rename from internal/lsp/testdata/addimport/addimport.go.golden rename to gopls/internal/lsp/testdata/addimport/addimport.go.golden diff --git a/internal/lsp/testdata/addimport/addimport.go.in b/gopls/internal/lsp/testdata/addimport/addimport.go.in similarity index 100% rename from internal/lsp/testdata/addimport/addimport.go.in rename to gopls/internal/lsp/testdata/addimport/addimport.go.in diff --git a/internal/lsp/testdata/address/address.go b/gopls/internal/lsp/testdata/address/address.go similarity index 100% rename from internal/lsp/testdata/address/address.go rename to gopls/internal/lsp/testdata/address/address.go diff --git a/internal/lsp/testdata/analyzer/bad_test.go b/gopls/internal/lsp/testdata/analyzer/bad_test.go similarity index 100% rename from internal/lsp/testdata/analyzer/bad_test.go rename to gopls/internal/lsp/testdata/analyzer/bad_test.go diff --git a/internal/lsp/testdata/anon/anon.go.in b/gopls/internal/lsp/testdata/anon/anon.go.in similarity index 100% rename from internal/lsp/testdata/anon/anon.go.in rename to gopls/internal/lsp/testdata/anon/anon.go.in diff --git a/internal/lsp/testdata/append/append.go b/gopls/internal/lsp/testdata/append/append.go similarity index 100% rename from internal/lsp/testdata/append/append.go rename to gopls/internal/lsp/testdata/append/append.go diff --git a/internal/lsp/testdata/append/append2.go.in b/gopls/internal/lsp/testdata/append/append2.go.in similarity index 100% rename from internal/lsp/testdata/append/append2.go.in rename to gopls/internal/lsp/testdata/append/append2.go.in diff --git a/internal/lsp/testdata/arraytype/array_type.go.in b/gopls/internal/lsp/testdata/arraytype/array_type.go.in similarity index 100% rename from internal/lsp/testdata/arraytype/array_type.go.in rename to gopls/internal/lsp/testdata/arraytype/array_type.go.in diff --git a/internal/lsp/testdata/assign/assign.go.in b/gopls/internal/lsp/testdata/assign/assign.go.in similarity index 100% rename from internal/lsp/testdata/assign/assign.go.in rename to gopls/internal/lsp/testdata/assign/assign.go.in diff --git a/internal/lsp/testdata/assign/internal/secret/secret.go b/gopls/internal/lsp/testdata/assign/internal/secret/secret.go similarity index 100% rename from internal/lsp/testdata/assign/internal/secret/secret.go rename to gopls/internal/lsp/testdata/assign/internal/secret/secret.go diff --git a/internal/lsp/testdata/bad/bad0.go b/gopls/internal/lsp/testdata/bad/bad0.go similarity index 100% rename from internal/lsp/testdata/bad/bad0.go rename to gopls/internal/lsp/testdata/bad/bad0.go diff --git a/internal/lsp/testdata/bad/bad1.go b/gopls/internal/lsp/testdata/bad/bad1.go similarity index 100% rename from internal/lsp/testdata/bad/bad1.go rename to gopls/internal/lsp/testdata/bad/bad1.go diff --git a/internal/lsp/testdata/badstmt/badstmt.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt.go.in similarity index 100% rename from internal/lsp/testdata/badstmt/badstmt.go.in rename to gopls/internal/lsp/testdata/badstmt/badstmt.go.in diff --git a/internal/lsp/testdata/badstmt/badstmt_2.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt_2.go.in similarity index 100% rename from internal/lsp/testdata/badstmt/badstmt_2.go.in rename to gopls/internal/lsp/testdata/badstmt/badstmt_2.go.in diff --git a/internal/lsp/testdata/badstmt/badstmt_3.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt_3.go.in similarity index 100% rename from internal/lsp/testdata/badstmt/badstmt_3.go.in rename to gopls/internal/lsp/testdata/badstmt/badstmt_3.go.in diff --git a/internal/lsp/testdata/badstmt/badstmt_4.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt_4.go.in similarity index 100% rename from internal/lsp/testdata/badstmt/badstmt_4.go.in rename to gopls/internal/lsp/testdata/badstmt/badstmt_4.go.in diff --git a/internal/lsp/testdata/bar/bar.go.in b/gopls/internal/lsp/testdata/bar/bar.go.in similarity index 100% rename from internal/lsp/testdata/bar/bar.go.in rename to gopls/internal/lsp/testdata/bar/bar.go.in diff --git a/internal/lsp/testdata/basiclit/basiclit.go b/gopls/internal/lsp/testdata/basiclit/basiclit.go similarity index 100% rename from internal/lsp/testdata/basiclit/basiclit.go rename to gopls/internal/lsp/testdata/basiclit/basiclit.go diff --git a/internal/lsp/testdata/baz/baz.go.in b/gopls/internal/lsp/testdata/baz/baz.go.in similarity index 100% rename from internal/lsp/testdata/baz/baz.go.in rename to gopls/internal/lsp/testdata/baz/baz.go.in diff --git a/internal/lsp/testdata/builtins/builtin_args.go b/gopls/internal/lsp/testdata/builtins/builtin_args.go similarity index 100% rename from internal/lsp/testdata/builtins/builtin_args.go rename to gopls/internal/lsp/testdata/builtins/builtin_args.go diff --git a/internal/lsp/testdata/builtins/builtin_types.go b/gopls/internal/lsp/testdata/builtins/builtin_types.go similarity index 100% rename from internal/lsp/testdata/builtins/builtin_types.go rename to gopls/internal/lsp/testdata/builtins/builtin_types.go diff --git a/internal/lsp/testdata/builtins/builtins.go b/gopls/internal/lsp/testdata/builtins/builtins.go similarity index 100% rename from internal/lsp/testdata/builtins/builtins.go rename to gopls/internal/lsp/testdata/builtins/builtins.go diff --git a/internal/lsp/testdata/builtins/constants.go b/gopls/internal/lsp/testdata/builtins/constants.go similarity index 100% rename from internal/lsp/testdata/builtins/constants.go rename to gopls/internal/lsp/testdata/builtins/constants.go diff --git a/internal/lsp/testdata/callhierarchy/callhierarchy.go b/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go similarity index 100% rename from internal/lsp/testdata/callhierarchy/callhierarchy.go rename to gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go diff --git a/internal/lsp/testdata/callhierarchy/incoming/incoming.go b/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go similarity index 100% rename from internal/lsp/testdata/callhierarchy/incoming/incoming.go rename to gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go diff --git a/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go b/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go similarity index 100% rename from internal/lsp/testdata/callhierarchy/outgoing/outgoing.go rename to gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go diff --git a/internal/lsp/testdata/casesensitive/casesensitive.go b/gopls/internal/lsp/testdata/casesensitive/casesensitive.go similarity index 100% rename from internal/lsp/testdata/casesensitive/casesensitive.go rename to gopls/internal/lsp/testdata/casesensitive/casesensitive.go diff --git a/internal/lsp/testdata/cast/cast.go.in b/gopls/internal/lsp/testdata/cast/cast.go.in similarity index 100% rename from internal/lsp/testdata/cast/cast.go.in rename to gopls/internal/lsp/testdata/cast/cast.go.in diff --git a/internal/lsp/testdata/cgo/declarecgo.go b/gopls/internal/lsp/testdata/cgo/declarecgo.go similarity index 100% rename from internal/lsp/testdata/cgo/declarecgo.go rename to gopls/internal/lsp/testdata/cgo/declarecgo.go diff --git a/internal/lsp/testdata/cgo/declarecgo.go.golden b/gopls/internal/lsp/testdata/cgo/declarecgo.go.golden similarity index 100% rename from internal/lsp/testdata/cgo/declarecgo.go.golden rename to gopls/internal/lsp/testdata/cgo/declarecgo.go.golden diff --git a/internal/lsp/testdata/cgo/declarecgo_nocgo.go b/gopls/internal/lsp/testdata/cgo/declarecgo_nocgo.go similarity index 100% rename from internal/lsp/testdata/cgo/declarecgo_nocgo.go rename to gopls/internal/lsp/testdata/cgo/declarecgo_nocgo.go diff --git a/internal/lsp/testdata/cgoimport/usecgo.go.golden b/gopls/internal/lsp/testdata/cgoimport/usecgo.go.golden similarity index 100% rename from internal/lsp/testdata/cgoimport/usecgo.go.golden rename to gopls/internal/lsp/testdata/cgoimport/usecgo.go.golden diff --git a/internal/lsp/testdata/cgoimport/usecgo.go.in b/gopls/internal/lsp/testdata/cgoimport/usecgo.go.in similarity index 100% rename from internal/lsp/testdata/cgoimport/usecgo.go.in rename to gopls/internal/lsp/testdata/cgoimport/usecgo.go.in diff --git a/internal/lsp/testdata/channel/channel.go b/gopls/internal/lsp/testdata/channel/channel.go similarity index 100% rename from internal/lsp/testdata/channel/channel.go rename to gopls/internal/lsp/testdata/channel/channel.go diff --git a/internal/lsp/testdata/codelens/codelens_test.go b/gopls/internal/lsp/testdata/codelens/codelens_test.go similarity index 100% rename from internal/lsp/testdata/codelens/codelens_test.go rename to gopls/internal/lsp/testdata/codelens/codelens_test.go diff --git a/internal/lsp/testdata/comment_completion/comment_completion.go.in b/gopls/internal/lsp/testdata/comment_completion/comment_completion.go.in similarity index 100% rename from internal/lsp/testdata/comment_completion/comment_completion.go.in rename to gopls/internal/lsp/testdata/comment_completion/comment_completion.go.in diff --git a/internal/lsp/testdata/complit/complit.go.in b/gopls/internal/lsp/testdata/complit/complit.go.in similarity index 100% rename from internal/lsp/testdata/complit/complit.go.in rename to gopls/internal/lsp/testdata/complit/complit.go.in diff --git a/internal/lsp/testdata/constant/constant.go b/gopls/internal/lsp/testdata/constant/constant.go similarity index 100% rename from internal/lsp/testdata/constant/constant.go rename to gopls/internal/lsp/testdata/constant/constant.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_for.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_for.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_for.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_for_init.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_for_init.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_for_init.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_if.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_if.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_if.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_if_eof.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if_eof.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_if_eof.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_if_eof.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_if_init.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_if_init.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_if_init.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_multiline_if.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_selector_1.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_1.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_selector_1.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_selector_1.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_selector_2.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_selector_2.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_switch_init.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_switch_init.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go diff --git a/internal/lsp/testdata/deep/deep.go b/gopls/internal/lsp/testdata/deep/deep.go similarity index 100% rename from internal/lsp/testdata/deep/deep.go rename to gopls/internal/lsp/testdata/deep/deep.go diff --git a/internal/lsp/testdata/errors/errors.go b/gopls/internal/lsp/testdata/errors/errors.go similarity index 100% rename from internal/lsp/testdata/errors/errors.go rename to gopls/internal/lsp/testdata/errors/errors.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_args_returns.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_args_returns.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_args_returns.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_args_returns.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_args_returns.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_args_returns.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_args_returns.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_args_returns.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_basic.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_basic.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_basic.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_basic.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_basic.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_basic.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_basic_comment.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_issue_44813.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_issue_44813.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_redefine.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_redefine.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_redefine.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_redefine.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_redefine.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_redefine.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_redefine.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_redefine.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_basic.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_basic.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_basic.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_basic.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_complex.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_complex.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_complex.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_complex.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_init.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_init.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_init.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_init.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_init.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_init.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_init.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_init.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_scope.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_scope.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_scope.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_scope.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_scope.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_scope.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_scope.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_scope.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_smart_return.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_smart_return.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_smart_return.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_smart_return.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_smart_return.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_smart_return.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_smart_return.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_smart_return.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go.golden diff --git a/internal/lsp/testdata/extract/extract_method/extract_basic.go b/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go similarity index 100% rename from internal/lsp/testdata/extract/extract_method/extract_basic.go rename to gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go diff --git a/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden b/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_method/extract_basic.go.golden rename to gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden diff --git a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go b/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go similarity index 100% rename from internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go diff --git a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden b/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden diff --git a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go b/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go similarity index 100% rename from internal/lsp/testdata/extract/extract_variable/extract_func_call.go rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go diff --git a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden b/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden diff --git a/internal/lsp/testdata/extract/extract_variable/extract_scope.go b/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go similarity index 100% rename from internal/lsp/testdata/extract/extract_variable/extract_scope.go rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go diff --git a/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden b/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden diff --git a/internal/lsp/testdata/fieldlist/field_list.go b/gopls/internal/lsp/testdata/fieldlist/field_list.go similarity index 100% rename from internal/lsp/testdata/fieldlist/field_list.go rename to gopls/internal/lsp/testdata/fieldlist/field_list.go diff --git a/internal/lsp/testdata/fillstruct/a.go b/gopls/internal/lsp/testdata/fillstruct/a.go similarity index 100% rename from internal/lsp/testdata/fillstruct/a.go rename to gopls/internal/lsp/testdata/fillstruct/a.go diff --git a/internal/lsp/testdata/fillstruct/a.go.golden b/gopls/internal/lsp/testdata/fillstruct/a.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/a.go.golden rename to gopls/internal/lsp/testdata/fillstruct/a.go.golden diff --git a/internal/lsp/testdata/fillstruct/a2.go b/gopls/internal/lsp/testdata/fillstruct/a2.go similarity index 100% rename from internal/lsp/testdata/fillstruct/a2.go rename to gopls/internal/lsp/testdata/fillstruct/a2.go diff --git a/internal/lsp/testdata/fillstruct/a2.go.golden b/gopls/internal/lsp/testdata/fillstruct/a2.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/a2.go.golden rename to gopls/internal/lsp/testdata/fillstruct/a2.go.golden diff --git a/internal/lsp/testdata/fillstruct/a3.go b/gopls/internal/lsp/testdata/fillstruct/a3.go similarity index 100% rename from internal/lsp/testdata/fillstruct/a3.go rename to gopls/internal/lsp/testdata/fillstruct/a3.go diff --git a/internal/lsp/testdata/fillstruct/a3.go.golden b/gopls/internal/lsp/testdata/fillstruct/a3.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/a3.go.golden rename to gopls/internal/lsp/testdata/fillstruct/a3.go.golden diff --git a/internal/lsp/testdata/fillstruct/a4.go b/gopls/internal/lsp/testdata/fillstruct/a4.go similarity index 100% rename from internal/lsp/testdata/fillstruct/a4.go rename to gopls/internal/lsp/testdata/fillstruct/a4.go diff --git a/internal/lsp/testdata/fillstruct/a4.go.golden b/gopls/internal/lsp/testdata/fillstruct/a4.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/a4.go.golden rename to gopls/internal/lsp/testdata/fillstruct/a4.go.golden diff --git a/internal/lsp/testdata/fillstruct/data/a.go b/gopls/internal/lsp/testdata/fillstruct/data/a.go similarity index 100% rename from internal/lsp/testdata/fillstruct/data/a.go rename to gopls/internal/lsp/testdata/fillstruct/data/a.go diff --git a/internal/lsp/testdata/fillstruct/fill_struct.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct.go similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct.go diff --git a/internal/lsp/testdata/fillstruct/fill_struct.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct.go.golden diff --git a/internal/lsp/testdata/fillstruct/fill_struct_anon.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_anon.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go diff --git a/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden diff --git a/internal/lsp/testdata/fillstruct/fill_struct_nested.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_nested.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go diff --git a/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden diff --git a/internal/lsp/testdata/fillstruct/fill_struct_package.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_package.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go diff --git a/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_package.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden diff --git a/internal/lsp/testdata/fillstruct/fill_struct_partial.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_partial.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go diff --git a/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden diff --git a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_spaces.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go diff --git a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden diff --git a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_unsafe.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go diff --git a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden diff --git a/internal/lsp/testdata/fillstruct/typeparams.go b/gopls/internal/lsp/testdata/fillstruct/typeparams.go similarity index 100% rename from internal/lsp/testdata/fillstruct/typeparams.go rename to gopls/internal/lsp/testdata/fillstruct/typeparams.go diff --git a/internal/lsp/testdata/fillstruct/typeparams.go.golden b/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden similarity index 100% rename from internal/lsp/testdata/fillstruct/typeparams.go.golden rename to gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden diff --git a/internal/lsp/testdata/folding/a.go b/gopls/internal/lsp/testdata/folding/a.go similarity index 100% rename from internal/lsp/testdata/folding/a.go rename to gopls/internal/lsp/testdata/folding/a.go diff --git a/internal/lsp/testdata/folding/a.go.golden b/gopls/internal/lsp/testdata/folding/a.go.golden similarity index 100% rename from internal/lsp/testdata/folding/a.go.golden rename to gopls/internal/lsp/testdata/folding/a.go.golden diff --git a/internal/lsp/testdata/folding/bad.go.golden b/gopls/internal/lsp/testdata/folding/bad.go.golden similarity index 100% rename from internal/lsp/testdata/folding/bad.go.golden rename to gopls/internal/lsp/testdata/folding/bad.go.golden diff --git a/internal/lsp/testdata/folding/bad.go.in b/gopls/internal/lsp/testdata/folding/bad.go.in similarity index 100% rename from internal/lsp/testdata/folding/bad.go.in rename to gopls/internal/lsp/testdata/folding/bad.go.in diff --git a/internal/lsp/testdata/foo/foo.go b/gopls/internal/lsp/testdata/foo/foo.go similarity index 100% rename from internal/lsp/testdata/foo/foo.go rename to gopls/internal/lsp/testdata/foo/foo.go diff --git a/internal/lsp/testdata/format/bad_format.go.golden b/gopls/internal/lsp/testdata/format/bad_format.go.golden similarity index 100% rename from internal/lsp/testdata/format/bad_format.go.golden rename to gopls/internal/lsp/testdata/format/bad_format.go.golden diff --git a/internal/lsp/testdata/format/bad_format.go.in b/gopls/internal/lsp/testdata/format/bad_format.go.in similarity index 100% rename from internal/lsp/testdata/format/bad_format.go.in rename to gopls/internal/lsp/testdata/format/bad_format.go.in diff --git a/internal/lsp/testdata/format/good_format.go b/gopls/internal/lsp/testdata/format/good_format.go similarity index 100% rename from internal/lsp/testdata/format/good_format.go rename to gopls/internal/lsp/testdata/format/good_format.go diff --git a/internal/lsp/testdata/format/good_format.go.golden b/gopls/internal/lsp/testdata/format/good_format.go.golden similarity index 100% rename from internal/lsp/testdata/format/good_format.go.golden rename to gopls/internal/lsp/testdata/format/good_format.go.golden diff --git a/internal/lsp/testdata/format/newline_format.go.golden b/gopls/internal/lsp/testdata/format/newline_format.go.golden similarity index 100% rename from internal/lsp/testdata/format/newline_format.go.golden rename to gopls/internal/lsp/testdata/format/newline_format.go.golden diff --git a/internal/lsp/testdata/format/newline_format.go.in b/gopls/internal/lsp/testdata/format/newline_format.go.in similarity index 100% rename from internal/lsp/testdata/format/newline_format.go.in rename to gopls/internal/lsp/testdata/format/newline_format.go.in diff --git a/internal/lsp/testdata/format/one_line.go.golden b/gopls/internal/lsp/testdata/format/one_line.go.golden similarity index 100% rename from internal/lsp/testdata/format/one_line.go.golden rename to gopls/internal/lsp/testdata/format/one_line.go.golden diff --git a/internal/lsp/testdata/format/one_line.go.in b/gopls/internal/lsp/testdata/format/one_line.go.in similarity index 100% rename from internal/lsp/testdata/format/one_line.go.in rename to gopls/internal/lsp/testdata/format/one_line.go.in diff --git a/internal/lsp/testdata/func_rank/func_rank.go.in b/gopls/internal/lsp/testdata/func_rank/func_rank.go.in similarity index 100% rename from internal/lsp/testdata/func_rank/func_rank.go.in rename to gopls/internal/lsp/testdata/func_rank/func_rank.go.in diff --git a/internal/lsp/testdata/funcsig/func_sig.go b/gopls/internal/lsp/testdata/funcsig/func_sig.go similarity index 100% rename from internal/lsp/testdata/funcsig/func_sig.go rename to gopls/internal/lsp/testdata/funcsig/func_sig.go diff --git a/internal/lsp/testdata/funcvalue/func_value.go b/gopls/internal/lsp/testdata/funcvalue/func_value.go similarity index 100% rename from internal/lsp/testdata/funcvalue/func_value.go rename to gopls/internal/lsp/testdata/funcvalue/func_value.go diff --git a/internal/lsp/testdata/fuzzymatch/fuzzymatch.go b/gopls/internal/lsp/testdata/fuzzymatch/fuzzymatch.go similarity index 100% rename from internal/lsp/testdata/fuzzymatch/fuzzymatch.go rename to gopls/internal/lsp/testdata/fuzzymatch/fuzzymatch.go diff --git a/internal/lsp/testdata/generate/generate.go b/gopls/internal/lsp/testdata/generate/generate.go similarity index 100% rename from internal/lsp/testdata/generate/generate.go rename to gopls/internal/lsp/testdata/generate/generate.go diff --git a/internal/lsp/testdata/generated/generated.go b/gopls/internal/lsp/testdata/generated/generated.go similarity index 100% rename from internal/lsp/testdata/generated/generated.go rename to gopls/internal/lsp/testdata/generated/generated.go diff --git a/internal/lsp/testdata/generated/generator.go b/gopls/internal/lsp/testdata/generated/generator.go similarity index 100% rename from internal/lsp/testdata/generated/generator.go rename to gopls/internal/lsp/testdata/generated/generator.go diff --git a/internal/lsp/testdata/godef/a/a.go b/gopls/internal/lsp/testdata/godef/a/a.go similarity index 100% rename from internal/lsp/testdata/godef/a/a.go rename to gopls/internal/lsp/testdata/godef/a/a.go diff --git a/internal/lsp/testdata/godef/a/a.go.golden b/gopls/internal/lsp/testdata/godef/a/a.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/a.go.golden rename to gopls/internal/lsp/testdata/godef/a/a.go.golden diff --git a/internal/lsp/testdata/godef/a/a_test.go b/gopls/internal/lsp/testdata/godef/a/a_test.go similarity index 100% rename from internal/lsp/testdata/godef/a/a_test.go rename to gopls/internal/lsp/testdata/godef/a/a_test.go diff --git a/internal/lsp/testdata/godef/a/a_test.go.golden b/gopls/internal/lsp/testdata/godef/a/a_test.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/a_test.go.golden rename to gopls/internal/lsp/testdata/godef/a/a_test.go.golden diff --git a/internal/lsp/testdata/godef/a/a_x_test.go b/gopls/internal/lsp/testdata/godef/a/a_x_test.go similarity index 100% rename from internal/lsp/testdata/godef/a/a_x_test.go rename to gopls/internal/lsp/testdata/godef/a/a_x_test.go diff --git a/internal/lsp/testdata/godef/a/a_x_test.go.golden b/gopls/internal/lsp/testdata/godef/a/a_x_test.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/a_x_test.go.golden rename to gopls/internal/lsp/testdata/godef/a/a_x_test.go.golden diff --git a/internal/lsp/testdata/godef/a/d.go b/gopls/internal/lsp/testdata/godef/a/d.go similarity index 100% rename from internal/lsp/testdata/godef/a/d.go rename to gopls/internal/lsp/testdata/godef/a/d.go diff --git a/internal/lsp/testdata/godef/a/d.go.golden b/gopls/internal/lsp/testdata/godef/a/d.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/d.go.golden rename to gopls/internal/lsp/testdata/godef/a/d.go.golden diff --git a/internal/lsp/testdata/godef/a/f.go b/gopls/internal/lsp/testdata/godef/a/f.go similarity index 100% rename from internal/lsp/testdata/godef/a/f.go rename to gopls/internal/lsp/testdata/godef/a/f.go diff --git a/internal/lsp/testdata/godef/a/f.go.golden b/gopls/internal/lsp/testdata/godef/a/f.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/f.go.golden rename to gopls/internal/lsp/testdata/godef/a/f.go.golden diff --git a/internal/lsp/testdata/godef/a/g.go b/gopls/internal/lsp/testdata/godef/a/g.go similarity index 100% rename from internal/lsp/testdata/godef/a/g.go rename to gopls/internal/lsp/testdata/godef/a/g.go diff --git a/internal/lsp/testdata/godef/a/g.go.golden b/gopls/internal/lsp/testdata/godef/a/g.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/g.go.golden rename to gopls/internal/lsp/testdata/godef/a/g.go.golden diff --git a/internal/lsp/testdata/godef/a/h.go b/gopls/internal/lsp/testdata/godef/a/h.go similarity index 100% rename from internal/lsp/testdata/godef/a/h.go rename to gopls/internal/lsp/testdata/godef/a/h.go diff --git a/internal/lsp/testdata/godef/a/h.go.golden b/gopls/internal/lsp/testdata/godef/a/h.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/h.go.golden rename to gopls/internal/lsp/testdata/godef/a/h.go.golden diff --git a/internal/lsp/testdata/godef/a/random.go b/gopls/internal/lsp/testdata/godef/a/random.go similarity index 100% rename from internal/lsp/testdata/godef/a/random.go rename to gopls/internal/lsp/testdata/godef/a/random.go diff --git a/internal/lsp/testdata/godef/a/random.go.golden b/gopls/internal/lsp/testdata/godef/a/random.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/random.go.golden rename to gopls/internal/lsp/testdata/godef/a/random.go.golden diff --git a/internal/lsp/testdata/godef/b/b.go b/gopls/internal/lsp/testdata/godef/b/b.go similarity index 100% rename from internal/lsp/testdata/godef/b/b.go rename to gopls/internal/lsp/testdata/godef/b/b.go diff --git a/internal/lsp/testdata/godef/b/b.go.golden b/gopls/internal/lsp/testdata/godef/b/b.go.golden similarity index 100% rename from internal/lsp/testdata/godef/b/b.go.golden rename to gopls/internal/lsp/testdata/godef/b/b.go.golden diff --git a/internal/lsp/testdata/godef/b/c.go b/gopls/internal/lsp/testdata/godef/b/c.go similarity index 100% rename from internal/lsp/testdata/godef/b/c.go rename to gopls/internal/lsp/testdata/godef/b/c.go diff --git a/internal/lsp/testdata/godef/b/c.go.golden b/gopls/internal/lsp/testdata/godef/b/c.go.golden similarity index 100% rename from internal/lsp/testdata/godef/b/c.go.golden rename to gopls/internal/lsp/testdata/godef/b/c.go.golden diff --git a/internal/lsp/testdata/godef/b/c.go.saved b/gopls/internal/lsp/testdata/godef/b/c.go.saved similarity index 100% rename from internal/lsp/testdata/godef/b/c.go.saved rename to gopls/internal/lsp/testdata/godef/b/c.go.saved diff --git a/internal/lsp/testdata/godef/b/e.go b/gopls/internal/lsp/testdata/godef/b/e.go similarity index 100% rename from internal/lsp/testdata/godef/b/e.go rename to gopls/internal/lsp/testdata/godef/b/e.go diff --git a/internal/lsp/testdata/godef/b/e.go.golden b/gopls/internal/lsp/testdata/godef/b/e.go.golden similarity index 100% rename from internal/lsp/testdata/godef/b/e.go.golden rename to gopls/internal/lsp/testdata/godef/b/e.go.golden diff --git a/internal/lsp/testdata/godef/b/h.go b/gopls/internal/lsp/testdata/godef/b/h.go similarity index 100% rename from internal/lsp/testdata/godef/b/h.go rename to gopls/internal/lsp/testdata/godef/b/h.go diff --git a/internal/lsp/testdata/godef/b/h.go.golden b/gopls/internal/lsp/testdata/godef/b/h.go.golden similarity index 100% rename from internal/lsp/testdata/godef/b/h.go.golden rename to gopls/internal/lsp/testdata/godef/b/h.go.golden diff --git a/internal/lsp/testdata/godef/broken/unclosedIf.go.golden b/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.golden similarity index 100% rename from internal/lsp/testdata/godef/broken/unclosedIf.go.golden rename to gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.golden diff --git a/internal/lsp/testdata/godef/broken/unclosedIf.go.in b/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.in similarity index 100% rename from internal/lsp/testdata/godef/broken/unclosedIf.go.in rename to gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.in diff --git a/internal/lsp/testdata/godef/hover_generics/hover.go b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go similarity index 100% rename from internal/lsp/testdata/godef/hover_generics/hover.go rename to gopls/internal/lsp/testdata/godef/hover_generics/hover.go diff --git a/internal/lsp/testdata/godef/hover_generics/hover.go.golden b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden similarity index 100% rename from internal/lsp/testdata/godef/hover_generics/hover.go.golden rename to gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden diff --git a/internal/lsp/testdata/godef/infer_generics/inferred.go b/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go similarity index 100% rename from internal/lsp/testdata/godef/infer_generics/inferred.go rename to gopls/internal/lsp/testdata/godef/infer_generics/inferred.go diff --git a/internal/lsp/testdata/godef/infer_generics/inferred.go.golden b/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go.golden similarity index 100% rename from internal/lsp/testdata/godef/infer_generics/inferred.go.golden rename to gopls/internal/lsp/testdata/godef/infer_generics/inferred.go.golden diff --git a/internal/lsp/testdata/good/good0.go b/gopls/internal/lsp/testdata/good/good0.go similarity index 100% rename from internal/lsp/testdata/good/good0.go rename to gopls/internal/lsp/testdata/good/good0.go diff --git a/internal/lsp/testdata/good/good1.go b/gopls/internal/lsp/testdata/good/good1.go similarity index 100% rename from internal/lsp/testdata/good/good1.go rename to gopls/internal/lsp/testdata/good/good1.go diff --git a/internal/lsp/testdata/highlights/highlights.go b/gopls/internal/lsp/testdata/highlights/highlights.go similarity index 100% rename from internal/lsp/testdata/highlights/highlights.go rename to gopls/internal/lsp/testdata/highlights/highlights.go diff --git a/internal/lsp/testdata/implementation/implementation.go b/gopls/internal/lsp/testdata/implementation/implementation.go similarity index 100% rename from internal/lsp/testdata/implementation/implementation.go rename to gopls/internal/lsp/testdata/implementation/implementation.go diff --git a/internal/lsp/testdata/implementation/other/other.go b/gopls/internal/lsp/testdata/implementation/other/other.go similarity index 100% rename from internal/lsp/testdata/implementation/other/other.go rename to gopls/internal/lsp/testdata/implementation/other/other.go diff --git a/internal/lsp/testdata/implementation/other/other_test.go b/gopls/internal/lsp/testdata/implementation/other/other_test.go similarity index 100% rename from internal/lsp/testdata/implementation/other/other_test.go rename to gopls/internal/lsp/testdata/implementation/other/other_test.go diff --git a/internal/lsp/testdata/importedcomplit/imported_complit.go.in b/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in similarity index 100% rename from internal/lsp/testdata/importedcomplit/imported_complit.go.in rename to gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in diff --git a/internal/lsp/testdata/imports/add_import.go.golden b/gopls/internal/lsp/testdata/imports/add_import.go.golden similarity index 100% rename from internal/lsp/testdata/imports/add_import.go.golden rename to gopls/internal/lsp/testdata/imports/add_import.go.golden diff --git a/internal/lsp/testdata/imports/add_import.go.in b/gopls/internal/lsp/testdata/imports/add_import.go.in similarity index 100% rename from internal/lsp/testdata/imports/add_import.go.in rename to gopls/internal/lsp/testdata/imports/add_import.go.in diff --git a/internal/lsp/testdata/imports/good_imports.go.golden b/gopls/internal/lsp/testdata/imports/good_imports.go.golden similarity index 100% rename from internal/lsp/testdata/imports/good_imports.go.golden rename to gopls/internal/lsp/testdata/imports/good_imports.go.golden diff --git a/internal/lsp/testdata/imports/good_imports.go.in b/gopls/internal/lsp/testdata/imports/good_imports.go.in similarity index 100% rename from internal/lsp/testdata/imports/good_imports.go.in rename to gopls/internal/lsp/testdata/imports/good_imports.go.in diff --git a/internal/lsp/testdata/imports/issue35458.go.golden b/gopls/internal/lsp/testdata/imports/issue35458.go.golden similarity index 100% rename from internal/lsp/testdata/imports/issue35458.go.golden rename to gopls/internal/lsp/testdata/imports/issue35458.go.golden diff --git a/internal/lsp/testdata/imports/issue35458.go.in b/gopls/internal/lsp/testdata/imports/issue35458.go.in similarity index 100% rename from internal/lsp/testdata/imports/issue35458.go.in rename to gopls/internal/lsp/testdata/imports/issue35458.go.in diff --git a/internal/lsp/testdata/imports/multiple_blocks.go.golden b/gopls/internal/lsp/testdata/imports/multiple_blocks.go.golden similarity index 100% rename from internal/lsp/testdata/imports/multiple_blocks.go.golden rename to gopls/internal/lsp/testdata/imports/multiple_blocks.go.golden diff --git a/internal/lsp/testdata/imports/multiple_blocks.go.in b/gopls/internal/lsp/testdata/imports/multiple_blocks.go.in similarity index 100% rename from internal/lsp/testdata/imports/multiple_blocks.go.in rename to gopls/internal/lsp/testdata/imports/multiple_blocks.go.in diff --git a/internal/lsp/testdata/imports/needs_imports.go.golden b/gopls/internal/lsp/testdata/imports/needs_imports.go.golden similarity index 100% rename from internal/lsp/testdata/imports/needs_imports.go.golden rename to gopls/internal/lsp/testdata/imports/needs_imports.go.golden diff --git a/internal/lsp/testdata/imports/needs_imports.go.in b/gopls/internal/lsp/testdata/imports/needs_imports.go.in similarity index 100% rename from internal/lsp/testdata/imports/needs_imports.go.in rename to gopls/internal/lsp/testdata/imports/needs_imports.go.in diff --git a/internal/lsp/testdata/imports/remove_import.go.golden b/gopls/internal/lsp/testdata/imports/remove_import.go.golden similarity index 100% rename from internal/lsp/testdata/imports/remove_import.go.golden rename to gopls/internal/lsp/testdata/imports/remove_import.go.golden diff --git a/internal/lsp/testdata/imports/remove_import.go.in b/gopls/internal/lsp/testdata/imports/remove_import.go.in similarity index 100% rename from internal/lsp/testdata/imports/remove_import.go.in rename to gopls/internal/lsp/testdata/imports/remove_import.go.in diff --git a/internal/lsp/testdata/imports/remove_imports.go.golden b/gopls/internal/lsp/testdata/imports/remove_imports.go.golden similarity index 100% rename from internal/lsp/testdata/imports/remove_imports.go.golden rename to gopls/internal/lsp/testdata/imports/remove_imports.go.golden diff --git a/internal/lsp/testdata/imports/remove_imports.go.in b/gopls/internal/lsp/testdata/imports/remove_imports.go.in similarity index 100% rename from internal/lsp/testdata/imports/remove_imports.go.in rename to gopls/internal/lsp/testdata/imports/remove_imports.go.in diff --git a/internal/lsp/testdata/imports/two_lines.go.golden b/gopls/internal/lsp/testdata/imports/two_lines.go.golden similarity index 100% rename from internal/lsp/testdata/imports/two_lines.go.golden rename to gopls/internal/lsp/testdata/imports/two_lines.go.golden diff --git a/internal/lsp/testdata/imports/two_lines.go.in b/gopls/internal/lsp/testdata/imports/two_lines.go.in similarity index 100% rename from internal/lsp/testdata/imports/two_lines.go.in rename to gopls/internal/lsp/testdata/imports/two_lines.go.in diff --git a/internal/lsp/testdata/index/index.go b/gopls/internal/lsp/testdata/index/index.go similarity index 100% rename from internal/lsp/testdata/index/index.go rename to gopls/internal/lsp/testdata/index/index.go diff --git a/internal/lsp/testdata/inlay_hint/composite_literals.go b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go similarity index 100% rename from internal/lsp/testdata/inlay_hint/composite_literals.go rename to gopls/internal/lsp/testdata/inlay_hint/composite_literals.go diff --git a/internal/lsp/testdata/inlay_hint/composite_literals.go.golden b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go.golden similarity index 100% rename from internal/lsp/testdata/inlay_hint/composite_literals.go.golden rename to gopls/internal/lsp/testdata/inlay_hint/composite_literals.go.golden diff --git a/internal/lsp/testdata/inlay_hint/constant_values.go b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go similarity index 100% rename from internal/lsp/testdata/inlay_hint/constant_values.go rename to gopls/internal/lsp/testdata/inlay_hint/constant_values.go diff --git a/internal/lsp/testdata/inlay_hint/constant_values.go.golden b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go.golden similarity index 100% rename from internal/lsp/testdata/inlay_hint/constant_values.go.golden rename to gopls/internal/lsp/testdata/inlay_hint/constant_values.go.golden diff --git a/internal/lsp/testdata/inlay_hint/parameter_names.go b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go similarity index 100% rename from internal/lsp/testdata/inlay_hint/parameter_names.go rename to gopls/internal/lsp/testdata/inlay_hint/parameter_names.go diff --git a/internal/lsp/testdata/inlay_hint/parameter_names.go.golden b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go.golden similarity index 100% rename from internal/lsp/testdata/inlay_hint/parameter_names.go.golden rename to gopls/internal/lsp/testdata/inlay_hint/parameter_names.go.golden diff --git a/internal/lsp/testdata/inlay_hint/type_params.go b/gopls/internal/lsp/testdata/inlay_hint/type_params.go similarity index 100% rename from internal/lsp/testdata/inlay_hint/type_params.go rename to gopls/internal/lsp/testdata/inlay_hint/type_params.go diff --git a/internal/lsp/testdata/inlay_hint/type_params.go.golden b/gopls/internal/lsp/testdata/inlay_hint/type_params.go.golden similarity index 100% rename from internal/lsp/testdata/inlay_hint/type_params.go.golden rename to gopls/internal/lsp/testdata/inlay_hint/type_params.go.golden diff --git a/internal/lsp/testdata/inlay_hint/variable_types.go b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go similarity index 100% rename from internal/lsp/testdata/inlay_hint/variable_types.go rename to gopls/internal/lsp/testdata/inlay_hint/variable_types.go diff --git a/internal/lsp/testdata/inlay_hint/variable_types.go.golden b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go.golden similarity index 100% rename from internal/lsp/testdata/inlay_hint/variable_types.go.golden rename to gopls/internal/lsp/testdata/inlay_hint/variable_types.go.golden diff --git a/internal/lsp/testdata/interfacerank/interface_rank.go b/gopls/internal/lsp/testdata/interfacerank/interface_rank.go similarity index 100% rename from internal/lsp/testdata/interfacerank/interface_rank.go rename to gopls/internal/lsp/testdata/interfacerank/interface_rank.go diff --git a/internal/lsp/testdata/keywords/accidental_keywords.go.in b/gopls/internal/lsp/testdata/keywords/accidental_keywords.go.in similarity index 100% rename from internal/lsp/testdata/keywords/accidental_keywords.go.in rename to gopls/internal/lsp/testdata/keywords/accidental_keywords.go.in diff --git a/internal/lsp/testdata/keywords/empty_select.go b/gopls/internal/lsp/testdata/keywords/empty_select.go similarity index 100% rename from internal/lsp/testdata/keywords/empty_select.go rename to gopls/internal/lsp/testdata/keywords/empty_select.go diff --git a/internal/lsp/testdata/keywords/empty_switch.go b/gopls/internal/lsp/testdata/keywords/empty_switch.go similarity index 100% rename from internal/lsp/testdata/keywords/empty_switch.go rename to gopls/internal/lsp/testdata/keywords/empty_switch.go diff --git a/internal/lsp/testdata/keywords/keywords.go b/gopls/internal/lsp/testdata/keywords/keywords.go similarity index 100% rename from internal/lsp/testdata/keywords/keywords.go rename to gopls/internal/lsp/testdata/keywords/keywords.go diff --git a/internal/lsp/testdata/labels/labels.go b/gopls/internal/lsp/testdata/labels/labels.go similarity index 100% rename from internal/lsp/testdata/labels/labels.go rename to gopls/internal/lsp/testdata/labels/labels.go diff --git a/internal/lsp/testdata/links/links.go b/gopls/internal/lsp/testdata/links/links.go similarity index 100% rename from internal/lsp/testdata/links/links.go rename to gopls/internal/lsp/testdata/links/links.go diff --git a/internal/lsp/testdata/maps/maps.go.in b/gopls/internal/lsp/testdata/maps/maps.go.in similarity index 100% rename from internal/lsp/testdata/maps/maps.go.in rename to gopls/internal/lsp/testdata/maps/maps.go.in diff --git a/internal/lsp/testdata/missingfunction/channels.go b/gopls/internal/lsp/testdata/missingfunction/channels.go similarity index 100% rename from internal/lsp/testdata/missingfunction/channels.go rename to gopls/internal/lsp/testdata/missingfunction/channels.go diff --git a/internal/lsp/testdata/missingfunction/channels.go.golden b/gopls/internal/lsp/testdata/missingfunction/channels.go.golden similarity index 100% rename from internal/lsp/testdata/missingfunction/channels.go.golden rename to gopls/internal/lsp/testdata/missingfunction/channels.go.golden diff --git a/internal/lsp/testdata/missingfunction/consecutive_params.go b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go similarity index 100% rename from internal/lsp/testdata/missingfunction/consecutive_params.go rename to gopls/internal/lsp/testdata/missingfunction/consecutive_params.go diff --git a/internal/lsp/testdata/missingfunction/consecutive_params.go.golden b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go.golden similarity index 100% rename from internal/lsp/testdata/missingfunction/consecutive_params.go.golden rename to gopls/internal/lsp/testdata/missingfunction/consecutive_params.go.golden diff --git a/internal/lsp/testdata/missingfunction/error_param.go b/gopls/internal/lsp/testdata/missingfunction/error_param.go similarity index 100% rename from internal/lsp/testdata/missingfunction/error_param.go rename to gopls/internal/lsp/testdata/missingfunction/error_param.go diff --git a/internal/lsp/testdata/missingfunction/error_param.go.golden b/gopls/internal/lsp/testdata/missingfunction/error_param.go.golden similarity index 100% rename from internal/lsp/testdata/missingfunction/error_param.go.golden rename to gopls/internal/lsp/testdata/missingfunction/error_param.go.golden diff --git a/internal/lsp/testdata/missingfunction/literals.go b/gopls/internal/lsp/testdata/missingfunction/literals.go similarity index 100% rename from internal/lsp/testdata/missingfunction/literals.go rename to gopls/internal/lsp/testdata/missingfunction/literals.go diff --git a/internal/lsp/testdata/missingfunction/literals.go.golden b/gopls/internal/lsp/testdata/missingfunction/literals.go.golden similarity index 100% rename from internal/lsp/testdata/missingfunction/literals.go.golden rename to gopls/internal/lsp/testdata/missingfunction/literals.go.golden diff --git a/internal/lsp/testdata/missingfunction/operation.go b/gopls/internal/lsp/testdata/missingfunction/operation.go similarity index 100% rename from internal/lsp/testdata/missingfunction/operation.go rename to gopls/internal/lsp/testdata/missingfunction/operation.go diff --git a/internal/lsp/testdata/missingfunction/operation.go.golden b/gopls/internal/lsp/testdata/missingfunction/operation.go.golden similarity index 100% rename from internal/lsp/testdata/missingfunction/operation.go.golden rename to gopls/internal/lsp/testdata/missingfunction/operation.go.golden diff --git a/internal/lsp/testdata/missingfunction/selector.go b/gopls/internal/lsp/testdata/missingfunction/selector.go similarity index 100% rename from internal/lsp/testdata/missingfunction/selector.go rename to gopls/internal/lsp/testdata/missingfunction/selector.go diff --git a/internal/lsp/testdata/missingfunction/selector.go.golden b/gopls/internal/lsp/testdata/missingfunction/selector.go.golden similarity index 100% rename from internal/lsp/testdata/missingfunction/selector.go.golden rename to gopls/internal/lsp/testdata/missingfunction/selector.go.golden diff --git a/internal/lsp/testdata/missingfunction/slice.go b/gopls/internal/lsp/testdata/missingfunction/slice.go similarity index 100% rename from internal/lsp/testdata/missingfunction/slice.go rename to gopls/internal/lsp/testdata/missingfunction/slice.go diff --git a/internal/lsp/testdata/missingfunction/slice.go.golden b/gopls/internal/lsp/testdata/missingfunction/slice.go.golden similarity index 100% rename from internal/lsp/testdata/missingfunction/slice.go.golden rename to gopls/internal/lsp/testdata/missingfunction/slice.go.golden diff --git a/internal/lsp/testdata/missingfunction/tuple.go b/gopls/internal/lsp/testdata/missingfunction/tuple.go similarity index 100% rename from internal/lsp/testdata/missingfunction/tuple.go rename to gopls/internal/lsp/testdata/missingfunction/tuple.go diff --git a/internal/lsp/testdata/missingfunction/tuple.go.golden b/gopls/internal/lsp/testdata/missingfunction/tuple.go.golden similarity index 100% rename from internal/lsp/testdata/missingfunction/tuple.go.golden rename to gopls/internal/lsp/testdata/missingfunction/tuple.go.golden diff --git a/internal/lsp/testdata/missingfunction/unique_params.go b/gopls/internal/lsp/testdata/missingfunction/unique_params.go similarity index 100% rename from internal/lsp/testdata/missingfunction/unique_params.go rename to gopls/internal/lsp/testdata/missingfunction/unique_params.go diff --git a/internal/lsp/testdata/missingfunction/unique_params.go.golden b/gopls/internal/lsp/testdata/missingfunction/unique_params.go.golden similarity index 100% rename from internal/lsp/testdata/missingfunction/unique_params.go.golden rename to gopls/internal/lsp/testdata/missingfunction/unique_params.go.golden diff --git a/internal/lsp/testdata/multireturn/multi_return.go.in b/gopls/internal/lsp/testdata/multireturn/multi_return.go.in similarity index 100% rename from internal/lsp/testdata/multireturn/multi_return.go.in rename to gopls/internal/lsp/testdata/multireturn/multi_return.go.in diff --git a/internal/lsp/testdata/nested_complit/nested_complit.go.in b/gopls/internal/lsp/testdata/nested_complit/nested_complit.go.in similarity index 100% rename from internal/lsp/testdata/nested_complit/nested_complit.go.in rename to gopls/internal/lsp/testdata/nested_complit/nested_complit.go.in diff --git a/internal/lsp/testdata/nodisk/empty b/gopls/internal/lsp/testdata/nodisk/empty similarity index 100% rename from internal/lsp/testdata/nodisk/empty rename to gopls/internal/lsp/testdata/nodisk/empty diff --git a/internal/lsp/testdata/nodisk/nodisk.overlay.go b/gopls/internal/lsp/testdata/nodisk/nodisk.overlay.go similarity index 100% rename from internal/lsp/testdata/nodisk/nodisk.overlay.go rename to gopls/internal/lsp/testdata/nodisk/nodisk.overlay.go diff --git a/internal/lsp/testdata/noparse/noparse.go.in b/gopls/internal/lsp/testdata/noparse/noparse.go.in similarity index 100% rename from internal/lsp/testdata/noparse/noparse.go.in rename to gopls/internal/lsp/testdata/noparse/noparse.go.in diff --git a/internal/lsp/testdata/noparse_format/noparse_format.go.golden b/gopls/internal/lsp/testdata/noparse_format/noparse_format.go.golden similarity index 100% rename from internal/lsp/testdata/noparse_format/noparse_format.go.golden rename to gopls/internal/lsp/testdata/noparse_format/noparse_format.go.golden diff --git a/internal/lsp/testdata/noparse_format/noparse_format.go.in b/gopls/internal/lsp/testdata/noparse_format/noparse_format.go.in similarity index 100% rename from internal/lsp/testdata/noparse_format/noparse_format.go.in rename to gopls/internal/lsp/testdata/noparse_format/noparse_format.go.in diff --git a/internal/lsp/testdata/noparse_format/parse_format.go.golden b/gopls/internal/lsp/testdata/noparse_format/parse_format.go.golden similarity index 100% rename from internal/lsp/testdata/noparse_format/parse_format.go.golden rename to gopls/internal/lsp/testdata/noparse_format/parse_format.go.golden diff --git a/internal/lsp/testdata/noparse_format/parse_format.go.in b/gopls/internal/lsp/testdata/noparse_format/parse_format.go.in similarity index 100% rename from internal/lsp/testdata/noparse_format/parse_format.go.in rename to gopls/internal/lsp/testdata/noparse_format/parse_format.go.in diff --git a/internal/lsp/testdata/printf/printf.go b/gopls/internal/lsp/testdata/printf/printf.go similarity index 100% rename from internal/lsp/testdata/printf/printf.go rename to gopls/internal/lsp/testdata/printf/printf.go diff --git a/internal/lsp/testdata/rank/assign_rank.go.in b/gopls/internal/lsp/testdata/rank/assign_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/assign_rank.go.in rename to gopls/internal/lsp/testdata/rank/assign_rank.go.in diff --git a/internal/lsp/testdata/rank/binexpr_rank.go.in b/gopls/internal/lsp/testdata/rank/binexpr_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/binexpr_rank.go.in rename to gopls/internal/lsp/testdata/rank/binexpr_rank.go.in diff --git a/internal/lsp/testdata/rank/boolexpr_rank.go b/gopls/internal/lsp/testdata/rank/boolexpr_rank.go similarity index 100% rename from internal/lsp/testdata/rank/boolexpr_rank.go rename to gopls/internal/lsp/testdata/rank/boolexpr_rank.go diff --git a/internal/lsp/testdata/rank/convert_rank.go.in b/gopls/internal/lsp/testdata/rank/convert_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/convert_rank.go.in rename to gopls/internal/lsp/testdata/rank/convert_rank.go.in diff --git a/internal/lsp/testdata/rank/struct/struct_rank.go b/gopls/internal/lsp/testdata/rank/struct/struct_rank.go similarity index 100% rename from internal/lsp/testdata/rank/struct/struct_rank.go rename to gopls/internal/lsp/testdata/rank/struct/struct_rank.go diff --git a/internal/lsp/testdata/rank/switch_rank.go.in b/gopls/internal/lsp/testdata/rank/switch_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/switch_rank.go.in rename to gopls/internal/lsp/testdata/rank/switch_rank.go.in diff --git a/internal/lsp/testdata/rank/type_assert_rank.go.in b/gopls/internal/lsp/testdata/rank/type_assert_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/type_assert_rank.go.in rename to gopls/internal/lsp/testdata/rank/type_assert_rank.go.in diff --git a/internal/lsp/testdata/rank/type_switch_rank.go.in b/gopls/internal/lsp/testdata/rank/type_switch_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/type_switch_rank.go.in rename to gopls/internal/lsp/testdata/rank/type_switch_rank.go.in diff --git a/internal/lsp/testdata/references/another/another.go b/gopls/internal/lsp/testdata/references/another/another.go similarity index 100% rename from internal/lsp/testdata/references/another/another.go rename to gopls/internal/lsp/testdata/references/another/another.go diff --git a/internal/lsp/testdata/references/interfaces/interfaces.go b/gopls/internal/lsp/testdata/references/interfaces/interfaces.go similarity index 100% rename from internal/lsp/testdata/references/interfaces/interfaces.go rename to gopls/internal/lsp/testdata/references/interfaces/interfaces.go diff --git a/internal/lsp/testdata/references/other/other.go b/gopls/internal/lsp/testdata/references/other/other.go similarity index 100% rename from internal/lsp/testdata/references/other/other.go rename to gopls/internal/lsp/testdata/references/other/other.go diff --git a/internal/lsp/testdata/references/refs.go b/gopls/internal/lsp/testdata/references/refs.go similarity index 100% rename from internal/lsp/testdata/references/refs.go rename to gopls/internal/lsp/testdata/references/refs.go diff --git a/internal/lsp/testdata/references/refs_test.go b/gopls/internal/lsp/testdata/references/refs_test.go similarity index 100% rename from internal/lsp/testdata/references/refs_test.go rename to gopls/internal/lsp/testdata/references/refs_test.go diff --git a/internal/lsp/testdata/rename/a/random.go.golden b/gopls/internal/lsp/testdata/rename/a/random.go.golden similarity index 100% rename from internal/lsp/testdata/rename/a/random.go.golden rename to gopls/internal/lsp/testdata/rename/a/random.go.golden diff --git a/internal/lsp/testdata/rename/a/random.go.in b/gopls/internal/lsp/testdata/rename/a/random.go.in similarity index 100% rename from internal/lsp/testdata/rename/a/random.go.in rename to gopls/internal/lsp/testdata/rename/a/random.go.in diff --git a/internal/lsp/testdata/rename/b/b.go b/gopls/internal/lsp/testdata/rename/b/b.go similarity index 100% rename from internal/lsp/testdata/rename/b/b.go rename to gopls/internal/lsp/testdata/rename/b/b.go diff --git a/internal/lsp/testdata/rename/b/b.go.golden b/gopls/internal/lsp/testdata/rename/b/b.go.golden similarity index 100% rename from internal/lsp/testdata/rename/b/b.go.golden rename to gopls/internal/lsp/testdata/rename/b/b.go.golden diff --git a/internal/lsp/testdata/rename/bad/bad.go.golden b/gopls/internal/lsp/testdata/rename/bad/bad.go.golden similarity index 100% rename from internal/lsp/testdata/rename/bad/bad.go.golden rename to gopls/internal/lsp/testdata/rename/bad/bad.go.golden diff --git a/internal/lsp/testdata/rename/bad/bad.go.in b/gopls/internal/lsp/testdata/rename/bad/bad.go.in similarity index 100% rename from internal/lsp/testdata/rename/bad/bad.go.in rename to gopls/internal/lsp/testdata/rename/bad/bad.go.in diff --git a/internal/lsp/testdata/rename/bad/bad_test.go.in b/gopls/internal/lsp/testdata/rename/bad/bad_test.go.in similarity index 100% rename from internal/lsp/testdata/rename/bad/bad_test.go.in rename to gopls/internal/lsp/testdata/rename/bad/bad_test.go.in diff --git a/internal/lsp/testdata/rename/c/c.go b/gopls/internal/lsp/testdata/rename/c/c.go similarity index 100% rename from internal/lsp/testdata/rename/c/c.go rename to gopls/internal/lsp/testdata/rename/c/c.go diff --git a/internal/lsp/testdata/rename/c/c.go.golden b/gopls/internal/lsp/testdata/rename/c/c.go.golden similarity index 100% rename from internal/lsp/testdata/rename/c/c.go.golden rename to gopls/internal/lsp/testdata/rename/c/c.go.golden diff --git a/internal/lsp/testdata/rename/c/c2.go b/gopls/internal/lsp/testdata/rename/c/c2.go similarity index 100% rename from internal/lsp/testdata/rename/c/c2.go rename to gopls/internal/lsp/testdata/rename/c/c2.go diff --git a/internal/lsp/testdata/rename/c/c2.go.golden b/gopls/internal/lsp/testdata/rename/c/c2.go.golden similarity index 100% rename from internal/lsp/testdata/rename/c/c2.go.golden rename to gopls/internal/lsp/testdata/rename/c/c2.go.golden diff --git a/internal/lsp/testdata/rename/crosspkg/another/another.go b/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go similarity index 100% rename from internal/lsp/testdata/rename/crosspkg/another/another.go rename to gopls/internal/lsp/testdata/rename/crosspkg/another/another.go diff --git a/internal/lsp/testdata/rename/crosspkg/another/another.go.golden b/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go.golden similarity index 100% rename from internal/lsp/testdata/rename/crosspkg/another/another.go.golden rename to gopls/internal/lsp/testdata/rename/crosspkg/another/another.go.golden diff --git a/internal/lsp/testdata/rename/crosspkg/crosspkg.go b/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go similarity index 100% rename from internal/lsp/testdata/rename/crosspkg/crosspkg.go rename to gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go diff --git a/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden b/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden similarity index 100% rename from internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden rename to gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden diff --git a/internal/lsp/testdata/rename/crosspkg/other/other.go b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go similarity index 100% rename from internal/lsp/testdata/rename/crosspkg/other/other.go rename to gopls/internal/lsp/testdata/rename/crosspkg/other/other.go diff --git a/internal/lsp/testdata/rename/crosspkg/other/other.go.golden b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go.golden similarity index 100% rename from internal/lsp/testdata/rename/crosspkg/other/other.go.golden rename to gopls/internal/lsp/testdata/rename/crosspkg/other/other.go.golden diff --git a/internal/lsp/testdata/rename/generics/embedded.go b/gopls/internal/lsp/testdata/rename/generics/embedded.go similarity index 100% rename from internal/lsp/testdata/rename/generics/embedded.go rename to gopls/internal/lsp/testdata/rename/generics/embedded.go diff --git a/internal/lsp/testdata/rename/generics/embedded.go.golden b/gopls/internal/lsp/testdata/rename/generics/embedded.go.golden similarity index 100% rename from internal/lsp/testdata/rename/generics/embedded.go.golden rename to gopls/internal/lsp/testdata/rename/generics/embedded.go.golden diff --git a/internal/lsp/testdata/rename/generics/generics.go b/gopls/internal/lsp/testdata/rename/generics/generics.go similarity index 100% rename from internal/lsp/testdata/rename/generics/generics.go rename to gopls/internal/lsp/testdata/rename/generics/generics.go diff --git a/internal/lsp/testdata/rename/generics/generics.go.golden b/gopls/internal/lsp/testdata/rename/generics/generics.go.golden similarity index 100% rename from internal/lsp/testdata/rename/generics/generics.go.golden rename to gopls/internal/lsp/testdata/rename/generics/generics.go.golden diff --git a/internal/lsp/testdata/rename/generics/unions.go b/gopls/internal/lsp/testdata/rename/generics/unions.go similarity index 100% rename from internal/lsp/testdata/rename/generics/unions.go rename to gopls/internal/lsp/testdata/rename/generics/unions.go diff --git a/internal/lsp/testdata/rename/generics/unions.go.golden b/gopls/internal/lsp/testdata/rename/generics/unions.go.golden similarity index 100% rename from internal/lsp/testdata/rename/generics/unions.go.golden rename to gopls/internal/lsp/testdata/rename/generics/unions.go.golden diff --git a/internal/lsp/testdata/rename/issue39614/issue39614.go.golden b/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue39614/issue39614.go.golden rename to gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.golden diff --git a/internal/lsp/testdata/rename/issue39614/issue39614.go.in b/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.in similarity index 100% rename from internal/lsp/testdata/rename/issue39614/issue39614.go.in rename to gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.in diff --git a/internal/lsp/testdata/rename/issue42134/1.go b/gopls/internal/lsp/testdata/rename/issue42134/1.go similarity index 100% rename from internal/lsp/testdata/rename/issue42134/1.go rename to gopls/internal/lsp/testdata/rename/issue42134/1.go diff --git a/internal/lsp/testdata/rename/issue42134/1.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/1.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue42134/1.go.golden rename to gopls/internal/lsp/testdata/rename/issue42134/1.go.golden diff --git a/internal/lsp/testdata/rename/issue42134/2.go b/gopls/internal/lsp/testdata/rename/issue42134/2.go similarity index 100% rename from internal/lsp/testdata/rename/issue42134/2.go rename to gopls/internal/lsp/testdata/rename/issue42134/2.go diff --git a/internal/lsp/testdata/rename/issue42134/2.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/2.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue42134/2.go.golden rename to gopls/internal/lsp/testdata/rename/issue42134/2.go.golden diff --git a/internal/lsp/testdata/rename/issue42134/3.go b/gopls/internal/lsp/testdata/rename/issue42134/3.go similarity index 100% rename from internal/lsp/testdata/rename/issue42134/3.go rename to gopls/internal/lsp/testdata/rename/issue42134/3.go diff --git a/internal/lsp/testdata/rename/issue42134/3.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/3.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue42134/3.go.golden rename to gopls/internal/lsp/testdata/rename/issue42134/3.go.golden diff --git a/internal/lsp/testdata/rename/issue42134/4.go b/gopls/internal/lsp/testdata/rename/issue42134/4.go similarity index 100% rename from internal/lsp/testdata/rename/issue42134/4.go rename to gopls/internal/lsp/testdata/rename/issue42134/4.go diff --git a/internal/lsp/testdata/rename/issue42134/4.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/4.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue42134/4.go.golden rename to gopls/internal/lsp/testdata/rename/issue42134/4.go.golden diff --git a/internal/lsp/testdata/rename/issue43616/issue43616.go.golden b/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue43616/issue43616.go.golden rename to gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.golden diff --git a/internal/lsp/testdata/rename/issue43616/issue43616.go.in b/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.in similarity index 100% rename from internal/lsp/testdata/rename/issue43616/issue43616.go.in rename to gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.in diff --git a/internal/lsp/testdata/rename/shadow/shadow.go b/gopls/internal/lsp/testdata/rename/shadow/shadow.go similarity index 100% rename from internal/lsp/testdata/rename/shadow/shadow.go rename to gopls/internal/lsp/testdata/rename/shadow/shadow.go diff --git a/internal/lsp/testdata/rename/shadow/shadow.go.golden b/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden similarity index 100% rename from internal/lsp/testdata/rename/shadow/shadow.go.golden rename to gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden diff --git a/internal/lsp/testdata/rename/testy/testy.go b/gopls/internal/lsp/testdata/rename/testy/testy.go similarity index 100% rename from internal/lsp/testdata/rename/testy/testy.go rename to gopls/internal/lsp/testdata/rename/testy/testy.go diff --git a/internal/lsp/testdata/rename/testy/testy.go.golden b/gopls/internal/lsp/testdata/rename/testy/testy.go.golden similarity index 100% rename from internal/lsp/testdata/rename/testy/testy.go.golden rename to gopls/internal/lsp/testdata/rename/testy/testy.go.golden diff --git a/internal/lsp/testdata/rename/testy/testy_test.go b/gopls/internal/lsp/testdata/rename/testy/testy_test.go similarity index 100% rename from internal/lsp/testdata/rename/testy/testy_test.go rename to gopls/internal/lsp/testdata/rename/testy/testy_test.go diff --git a/internal/lsp/testdata/rename/testy/testy_test.go.golden b/gopls/internal/lsp/testdata/rename/testy/testy_test.go.golden similarity index 100% rename from internal/lsp/testdata/rename/testy/testy_test.go.golden rename to gopls/internal/lsp/testdata/rename/testy/testy_test.go.golden diff --git a/internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go b/gopls/internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go similarity index 100% rename from internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go rename to gopls/internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go diff --git a/internal/lsp/testdata/selector/selector.go.in b/gopls/internal/lsp/testdata/selector/selector.go.in similarity index 100% rename from internal/lsp/testdata/selector/selector.go.in rename to gopls/internal/lsp/testdata/selector/selector.go.in diff --git a/internal/lsp/testdata/semantic/README.md b/gopls/internal/lsp/testdata/semantic/README.md similarity index 100% rename from internal/lsp/testdata/semantic/README.md rename to gopls/internal/lsp/testdata/semantic/README.md diff --git a/internal/lsp/testdata/semantic/a.go b/gopls/internal/lsp/testdata/semantic/a.go similarity index 100% rename from internal/lsp/testdata/semantic/a.go rename to gopls/internal/lsp/testdata/semantic/a.go diff --git a/internal/lsp/testdata/semantic/a.go.golden b/gopls/internal/lsp/testdata/semantic/a.go.golden similarity index 100% rename from internal/lsp/testdata/semantic/a.go.golden rename to gopls/internal/lsp/testdata/semantic/a.go.golden diff --git a/internal/lsp/testdata/semantic/b.go b/gopls/internal/lsp/testdata/semantic/b.go similarity index 100% rename from internal/lsp/testdata/semantic/b.go rename to gopls/internal/lsp/testdata/semantic/b.go diff --git a/internal/lsp/testdata/semantic/b.go.golden b/gopls/internal/lsp/testdata/semantic/b.go.golden similarity index 100% rename from internal/lsp/testdata/semantic/b.go.golden rename to gopls/internal/lsp/testdata/semantic/b.go.golden diff --git a/internal/lsp/testdata/semantic/semantic_test.go b/gopls/internal/lsp/testdata/semantic/semantic_test.go similarity index 100% rename from internal/lsp/testdata/semantic/semantic_test.go rename to gopls/internal/lsp/testdata/semantic/semantic_test.go diff --git a/internal/lsp/testdata/signature/signature.go b/gopls/internal/lsp/testdata/signature/signature.go similarity index 100% rename from internal/lsp/testdata/signature/signature.go rename to gopls/internal/lsp/testdata/signature/signature.go diff --git a/internal/lsp/testdata/signature/signature.go.golden b/gopls/internal/lsp/testdata/signature/signature.go.golden similarity index 100% rename from internal/lsp/testdata/signature/signature.go.golden rename to gopls/internal/lsp/testdata/signature/signature.go.golden diff --git a/internal/lsp/testdata/signature/signature2.go.golden b/gopls/internal/lsp/testdata/signature/signature2.go.golden similarity index 100% rename from internal/lsp/testdata/signature/signature2.go.golden rename to gopls/internal/lsp/testdata/signature/signature2.go.golden diff --git a/internal/lsp/testdata/signature/signature2.go.in b/gopls/internal/lsp/testdata/signature/signature2.go.in similarity index 100% rename from internal/lsp/testdata/signature/signature2.go.in rename to gopls/internal/lsp/testdata/signature/signature2.go.in diff --git a/internal/lsp/testdata/signature/signature3.go.golden b/gopls/internal/lsp/testdata/signature/signature3.go.golden similarity index 100% rename from internal/lsp/testdata/signature/signature3.go.golden rename to gopls/internal/lsp/testdata/signature/signature3.go.golden diff --git a/internal/lsp/testdata/signature/signature3.go.in b/gopls/internal/lsp/testdata/signature/signature3.go.in similarity index 100% rename from internal/lsp/testdata/signature/signature3.go.in rename to gopls/internal/lsp/testdata/signature/signature3.go.in diff --git a/internal/lsp/testdata/signature/signature_test.go b/gopls/internal/lsp/testdata/signature/signature_test.go similarity index 100% rename from internal/lsp/testdata/signature/signature_test.go rename to gopls/internal/lsp/testdata/signature/signature_test.go diff --git a/internal/lsp/testdata/signature/signature_test.go.golden b/gopls/internal/lsp/testdata/signature/signature_test.go.golden similarity index 100% rename from internal/lsp/testdata/signature/signature_test.go.golden rename to gopls/internal/lsp/testdata/signature/signature_test.go.golden diff --git a/internal/lsp/testdata/snippets/func_snippets118.go.in b/gopls/internal/lsp/testdata/snippets/func_snippets118.go.in similarity index 100% rename from internal/lsp/testdata/snippets/func_snippets118.go.in rename to gopls/internal/lsp/testdata/snippets/func_snippets118.go.in diff --git a/internal/lsp/testdata/snippets/literal.go b/gopls/internal/lsp/testdata/snippets/literal.go similarity index 100% rename from internal/lsp/testdata/snippets/literal.go rename to gopls/internal/lsp/testdata/snippets/literal.go diff --git a/internal/lsp/testdata/snippets/literal.go.golden b/gopls/internal/lsp/testdata/snippets/literal.go.golden similarity index 100% rename from internal/lsp/testdata/snippets/literal.go.golden rename to gopls/internal/lsp/testdata/snippets/literal.go.golden diff --git a/internal/lsp/testdata/snippets/literal_snippets.go.in b/gopls/internal/lsp/testdata/snippets/literal_snippets.go.in similarity index 100% rename from internal/lsp/testdata/snippets/literal_snippets.go.in rename to gopls/internal/lsp/testdata/snippets/literal_snippets.go.in diff --git a/internal/lsp/testdata/snippets/literal_snippets118.go.in b/gopls/internal/lsp/testdata/snippets/literal_snippets118.go.in similarity index 100% rename from internal/lsp/testdata/snippets/literal_snippets118.go.in rename to gopls/internal/lsp/testdata/snippets/literal_snippets118.go.in diff --git a/internal/lsp/testdata/snippets/postfix.go b/gopls/internal/lsp/testdata/snippets/postfix.go similarity index 100% rename from internal/lsp/testdata/snippets/postfix.go rename to gopls/internal/lsp/testdata/snippets/postfix.go diff --git a/internal/lsp/testdata/snippets/snippets.go.golden b/gopls/internal/lsp/testdata/snippets/snippets.go.golden similarity index 100% rename from internal/lsp/testdata/snippets/snippets.go.golden rename to gopls/internal/lsp/testdata/snippets/snippets.go.golden diff --git a/internal/lsp/testdata/snippets/snippets.go.in b/gopls/internal/lsp/testdata/snippets/snippets.go.in similarity index 100% rename from internal/lsp/testdata/snippets/snippets.go.in rename to gopls/internal/lsp/testdata/snippets/snippets.go.in diff --git a/internal/lsp/testdata/statements/append.go b/gopls/internal/lsp/testdata/statements/append.go similarity index 100% rename from internal/lsp/testdata/statements/append.go rename to gopls/internal/lsp/testdata/statements/append.go diff --git a/internal/lsp/testdata/statements/if_err_check_return.go b/gopls/internal/lsp/testdata/statements/if_err_check_return.go similarity index 100% rename from internal/lsp/testdata/statements/if_err_check_return.go rename to gopls/internal/lsp/testdata/statements/if_err_check_return.go diff --git a/internal/lsp/testdata/statements/if_err_check_return_2.go b/gopls/internal/lsp/testdata/statements/if_err_check_return_2.go similarity index 100% rename from internal/lsp/testdata/statements/if_err_check_return_2.go rename to gopls/internal/lsp/testdata/statements/if_err_check_return_2.go diff --git a/internal/lsp/testdata/statements/if_err_check_test.go b/gopls/internal/lsp/testdata/statements/if_err_check_test.go similarity index 100% rename from internal/lsp/testdata/statements/if_err_check_test.go rename to gopls/internal/lsp/testdata/statements/if_err_check_test.go diff --git a/internal/lsp/testdata/stub/other/other.go b/gopls/internal/lsp/testdata/stub/other/other.go similarity index 100% rename from internal/lsp/testdata/stub/other/other.go rename to gopls/internal/lsp/testdata/stub/other/other.go diff --git a/internal/lsp/testdata/stub/stub_add_selector.go b/gopls/internal/lsp/testdata/stub/stub_add_selector.go similarity index 100% rename from internal/lsp/testdata/stub/stub_add_selector.go rename to gopls/internal/lsp/testdata/stub/stub_add_selector.go diff --git a/internal/lsp/testdata/stub/stub_add_selector.go.golden b/gopls/internal/lsp/testdata/stub/stub_add_selector.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_add_selector.go.golden rename to gopls/internal/lsp/testdata/stub/stub_add_selector.go.golden diff --git a/internal/lsp/testdata/stub/stub_assign.go b/gopls/internal/lsp/testdata/stub/stub_assign.go similarity index 100% rename from internal/lsp/testdata/stub/stub_assign.go rename to gopls/internal/lsp/testdata/stub/stub_assign.go diff --git a/internal/lsp/testdata/stub/stub_assign.go.golden b/gopls/internal/lsp/testdata/stub/stub_assign.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_assign.go.golden rename to gopls/internal/lsp/testdata/stub/stub_assign.go.golden diff --git a/internal/lsp/testdata/stub/stub_assign_multivars.go b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go similarity index 100% rename from internal/lsp/testdata/stub/stub_assign_multivars.go rename to gopls/internal/lsp/testdata/stub/stub_assign_multivars.go diff --git a/internal/lsp/testdata/stub/stub_assign_multivars.go.golden b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_assign_multivars.go.golden rename to gopls/internal/lsp/testdata/stub/stub_assign_multivars.go.golden diff --git a/internal/lsp/testdata/stub/stub_call_expr.go b/gopls/internal/lsp/testdata/stub/stub_call_expr.go similarity index 100% rename from internal/lsp/testdata/stub/stub_call_expr.go rename to gopls/internal/lsp/testdata/stub/stub_call_expr.go diff --git a/internal/lsp/testdata/stub/stub_call_expr.go.golden b/gopls/internal/lsp/testdata/stub/stub_call_expr.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_call_expr.go.golden rename to gopls/internal/lsp/testdata/stub/stub_call_expr.go.golden diff --git a/internal/lsp/testdata/stub/stub_embedded.go b/gopls/internal/lsp/testdata/stub/stub_embedded.go similarity index 100% rename from internal/lsp/testdata/stub/stub_embedded.go rename to gopls/internal/lsp/testdata/stub/stub_embedded.go diff --git a/internal/lsp/testdata/stub/stub_embedded.go.golden b/gopls/internal/lsp/testdata/stub/stub_embedded.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_embedded.go.golden rename to gopls/internal/lsp/testdata/stub/stub_embedded.go.golden diff --git a/internal/lsp/testdata/stub/stub_err.go b/gopls/internal/lsp/testdata/stub/stub_err.go similarity index 100% rename from internal/lsp/testdata/stub/stub_err.go rename to gopls/internal/lsp/testdata/stub/stub_err.go diff --git a/internal/lsp/testdata/stub/stub_err.go.golden b/gopls/internal/lsp/testdata/stub/stub_err.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_err.go.golden rename to gopls/internal/lsp/testdata/stub/stub_err.go.golden diff --git a/internal/lsp/testdata/stub/stub_function_return.go b/gopls/internal/lsp/testdata/stub/stub_function_return.go similarity index 100% rename from internal/lsp/testdata/stub/stub_function_return.go rename to gopls/internal/lsp/testdata/stub/stub_function_return.go diff --git a/internal/lsp/testdata/stub/stub_function_return.go.golden b/gopls/internal/lsp/testdata/stub/stub_function_return.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_function_return.go.golden rename to gopls/internal/lsp/testdata/stub/stub_function_return.go.golden diff --git a/internal/lsp/testdata/stub/stub_generic_receiver.go b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go similarity index 100% rename from internal/lsp/testdata/stub/stub_generic_receiver.go rename to gopls/internal/lsp/testdata/stub/stub_generic_receiver.go diff --git a/internal/lsp/testdata/stub/stub_generic_receiver.go.golden b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_generic_receiver.go.golden rename to gopls/internal/lsp/testdata/stub/stub_generic_receiver.go.golden diff --git a/internal/lsp/testdata/stub/stub_ignored_imports.go b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go similarity index 100% rename from internal/lsp/testdata/stub/stub_ignored_imports.go rename to gopls/internal/lsp/testdata/stub/stub_ignored_imports.go diff --git a/internal/lsp/testdata/stub/stub_ignored_imports.go.golden b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_ignored_imports.go.golden rename to gopls/internal/lsp/testdata/stub/stub_ignored_imports.go.golden diff --git a/internal/lsp/testdata/stub/stub_multi_var.go b/gopls/internal/lsp/testdata/stub/stub_multi_var.go similarity index 100% rename from internal/lsp/testdata/stub/stub_multi_var.go rename to gopls/internal/lsp/testdata/stub/stub_multi_var.go diff --git a/internal/lsp/testdata/stub/stub_multi_var.go.golden b/gopls/internal/lsp/testdata/stub/stub_multi_var.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_multi_var.go.golden rename to gopls/internal/lsp/testdata/stub/stub_multi_var.go.golden diff --git a/internal/lsp/testdata/stub/stub_pointer.go b/gopls/internal/lsp/testdata/stub/stub_pointer.go similarity index 100% rename from internal/lsp/testdata/stub/stub_pointer.go rename to gopls/internal/lsp/testdata/stub/stub_pointer.go diff --git a/internal/lsp/testdata/stub/stub_pointer.go.golden b/gopls/internal/lsp/testdata/stub/stub_pointer.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_pointer.go.golden rename to gopls/internal/lsp/testdata/stub/stub_pointer.go.golden diff --git a/internal/lsp/testdata/stub/stub_renamed_import.go b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go similarity index 100% rename from internal/lsp/testdata/stub/stub_renamed_import.go rename to gopls/internal/lsp/testdata/stub/stub_renamed_import.go diff --git a/internal/lsp/testdata/stub/stub_renamed_import.go.golden b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_renamed_import.go.golden rename to gopls/internal/lsp/testdata/stub/stub_renamed_import.go.golden diff --git a/internal/lsp/testdata/stub/stub_renamed_import_iface.go b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go similarity index 100% rename from internal/lsp/testdata/stub/stub_renamed_import_iface.go rename to gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go diff --git a/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden rename to gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden diff --git a/internal/lsp/testdata/stub/stub_stdlib.go b/gopls/internal/lsp/testdata/stub/stub_stdlib.go similarity index 100% rename from internal/lsp/testdata/stub/stub_stdlib.go rename to gopls/internal/lsp/testdata/stub/stub_stdlib.go diff --git a/internal/lsp/testdata/stub/stub_stdlib.go.golden b/gopls/internal/lsp/testdata/stub/stub_stdlib.go.golden similarity index 100% rename from internal/lsp/testdata/stub/stub_stdlib.go.golden rename to gopls/internal/lsp/testdata/stub/stub_stdlib.go.golden diff --git a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go b/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go similarity index 100% rename from internal/lsp/testdata/suggestedfix/has_suggested_fix.go rename to gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go diff --git a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden b/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden similarity index 100% rename from internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden rename to gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden diff --git a/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden similarity index 100% rename from internal/lsp/testdata/summary.txt.golden rename to gopls/internal/lsp/testdata/summary.txt.golden diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden similarity index 100% rename from internal/lsp/testdata/summary_go1.18.txt.golden rename to gopls/internal/lsp/testdata/summary_go1.18.txt.golden diff --git a/internal/lsp/testdata/symbols/main.go b/gopls/internal/lsp/testdata/symbols/main.go similarity index 100% rename from internal/lsp/testdata/symbols/main.go rename to gopls/internal/lsp/testdata/symbols/main.go diff --git a/internal/lsp/testdata/symbols/main.go.golden b/gopls/internal/lsp/testdata/symbols/main.go.golden similarity index 100% rename from internal/lsp/testdata/symbols/main.go.golden rename to gopls/internal/lsp/testdata/symbols/main.go.golden diff --git a/internal/lsp/testdata/testy/testy.go b/gopls/internal/lsp/testdata/testy/testy.go similarity index 100% rename from internal/lsp/testdata/testy/testy.go rename to gopls/internal/lsp/testdata/testy/testy.go diff --git a/internal/lsp/testdata/testy/testy_test.go b/gopls/internal/lsp/testdata/testy/testy_test.go similarity index 100% rename from internal/lsp/testdata/testy/testy_test.go rename to gopls/internal/lsp/testdata/testy/testy_test.go diff --git a/internal/lsp/testdata/testy/testy_test.go.golden b/gopls/internal/lsp/testdata/testy/testy_test.go.golden similarity index 100% rename from internal/lsp/testdata/testy/testy_test.go.golden rename to gopls/internal/lsp/testdata/testy/testy_test.go.golden diff --git a/internal/lsp/testdata/typdef/typdef.go b/gopls/internal/lsp/testdata/typdef/typdef.go similarity index 100% rename from internal/lsp/testdata/typdef/typdef.go rename to gopls/internal/lsp/testdata/typdef/typdef.go diff --git a/internal/lsp/testdata/typeassert/type_assert.go b/gopls/internal/lsp/testdata/typeassert/type_assert.go similarity index 100% rename from internal/lsp/testdata/typeassert/type_assert.go rename to gopls/internal/lsp/testdata/typeassert/type_assert.go diff --git a/internal/lsp/testdata/typeerrors/noresultvalues.go b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go similarity index 100% rename from internal/lsp/testdata/typeerrors/noresultvalues.go rename to gopls/internal/lsp/testdata/typeerrors/noresultvalues.go diff --git a/internal/lsp/testdata/typeerrors/noresultvalues.go.golden b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go.golden similarity index 100% rename from internal/lsp/testdata/typeerrors/noresultvalues.go.golden rename to gopls/internal/lsp/testdata/typeerrors/noresultvalues.go.golden diff --git a/internal/lsp/testdata/typemods/type_mods.go b/gopls/internal/lsp/testdata/typemods/type_mods.go similarity index 100% rename from internal/lsp/testdata/typemods/type_mods.go rename to gopls/internal/lsp/testdata/typemods/type_mods.go diff --git a/internal/lsp/testdata/typeparams/type_params.go b/gopls/internal/lsp/testdata/typeparams/type_params.go similarity index 100% rename from internal/lsp/testdata/typeparams/type_params.go rename to gopls/internal/lsp/testdata/typeparams/type_params.go diff --git a/internal/lsp/testdata/types/types.go b/gopls/internal/lsp/testdata/types/types.go similarity index 100% rename from internal/lsp/testdata/types/types.go rename to gopls/internal/lsp/testdata/types/types.go diff --git a/internal/lsp/testdata/undeclared/var.go b/gopls/internal/lsp/testdata/undeclared/var.go similarity index 100% rename from internal/lsp/testdata/undeclared/var.go rename to gopls/internal/lsp/testdata/undeclared/var.go diff --git a/internal/lsp/testdata/undeclared/var.go.golden b/gopls/internal/lsp/testdata/undeclared/var.go.golden similarity index 100% rename from internal/lsp/testdata/undeclared/var.go.golden rename to gopls/internal/lsp/testdata/undeclared/var.go.golden diff --git a/internal/lsp/testdata/unimported/export_test.go b/gopls/internal/lsp/testdata/unimported/export_test.go similarity index 100% rename from internal/lsp/testdata/unimported/export_test.go rename to gopls/internal/lsp/testdata/unimported/export_test.go diff --git a/internal/lsp/testdata/unimported/unimported.go.in b/gopls/internal/lsp/testdata/unimported/unimported.go.in similarity index 100% rename from internal/lsp/testdata/unimported/unimported.go.in rename to gopls/internal/lsp/testdata/unimported/unimported.go.in diff --git a/internal/lsp/testdata/unimported/unimported_cand_type.go b/gopls/internal/lsp/testdata/unimported/unimported_cand_type.go similarity index 100% rename from internal/lsp/testdata/unimported/unimported_cand_type.go rename to gopls/internal/lsp/testdata/unimported/unimported_cand_type.go diff --git a/internal/lsp/testdata/unimported/x_test.go b/gopls/internal/lsp/testdata/unimported/x_test.go similarity index 100% rename from internal/lsp/testdata/unimported/x_test.go rename to gopls/internal/lsp/testdata/unimported/x_test.go diff --git a/internal/lsp/testdata/unresolved/unresolved.go.in b/gopls/internal/lsp/testdata/unresolved/unresolved.go.in similarity index 100% rename from internal/lsp/testdata/unresolved/unresolved.go.in rename to gopls/internal/lsp/testdata/unresolved/unresolved.go.in diff --git a/internal/lsp/testdata/unsafe/unsafe.go b/gopls/internal/lsp/testdata/unsafe/unsafe.go similarity index 100% rename from internal/lsp/testdata/unsafe/unsafe.go rename to gopls/internal/lsp/testdata/unsafe/unsafe.go diff --git a/internal/lsp/testdata/variadic/variadic.go.in b/gopls/internal/lsp/testdata/variadic/variadic.go.in similarity index 100% rename from internal/lsp/testdata/variadic/variadic.go.in rename to gopls/internal/lsp/testdata/variadic/variadic.go.in diff --git a/internal/lsp/testdata/variadic/variadic_intf.go b/gopls/internal/lsp/testdata/variadic/variadic_intf.go similarity index 100% rename from internal/lsp/testdata/variadic/variadic_intf.go rename to gopls/internal/lsp/testdata/variadic/variadic_intf.go diff --git a/internal/lsp/testdata/workspacesymbol/a/a.go b/gopls/internal/lsp/testdata/workspacesymbol/a/a.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/a/a.go rename to gopls/internal/lsp/testdata/workspacesymbol/a/a.go diff --git a/internal/lsp/testdata/workspacesymbol/a/a.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/a/a.go.golden similarity index 100% rename from internal/lsp/testdata/workspacesymbol/a/a.go.golden rename to gopls/internal/lsp/testdata/workspacesymbol/a/a.go.golden diff --git a/internal/lsp/testdata/workspacesymbol/a/a_test.go b/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/a/a_test.go rename to gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go diff --git a/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden similarity index 100% rename from internal/lsp/testdata/workspacesymbol/a/a_test.go.golden rename to gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden diff --git a/internal/lsp/testdata/workspacesymbol/a/a_x_test.go b/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/a/a_x_test.go rename to gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go diff --git a/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden similarity index 100% rename from internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden rename to gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden diff --git a/internal/lsp/testdata/workspacesymbol/b/b.go b/gopls/internal/lsp/testdata/workspacesymbol/b/b.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/b/b.go rename to gopls/internal/lsp/testdata/workspacesymbol/b/b.go diff --git a/internal/lsp/testdata/workspacesymbol/b/b.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/b/b.go.golden similarity index 100% rename from internal/lsp/testdata/workspacesymbol/b/b.go.golden rename to gopls/internal/lsp/testdata/workspacesymbol/b/b.go.golden diff --git a/internal/lsp/testdata/workspacesymbol/issue44806.go b/gopls/internal/lsp/testdata/workspacesymbol/issue44806.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/issue44806.go rename to gopls/internal/lsp/testdata/workspacesymbol/issue44806.go diff --git a/internal/lsp/testdata/workspacesymbol/main.go b/gopls/internal/lsp/testdata/workspacesymbol/main.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/main.go rename to gopls/internal/lsp/testdata/workspacesymbol/main.go diff --git a/internal/lsp/testdata/workspacesymbol/p/p.go b/gopls/internal/lsp/testdata/workspacesymbol/p/p.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/p/p.go rename to gopls/internal/lsp/testdata/workspacesymbol/p/p.go diff --git a/internal/lsp/testdata/workspacesymbol/query.go b/gopls/internal/lsp/testdata/workspacesymbol/query.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/query.go rename to gopls/internal/lsp/testdata/workspacesymbol/query.go diff --git a/internal/lsp/testdata/workspacesymbol/query.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/query.go.golden similarity index 100% rename from internal/lsp/testdata/workspacesymbol/query.go.golden rename to gopls/internal/lsp/testdata/workspacesymbol/query.go.golden diff --git a/internal/lsp/tests/README.md b/gopls/internal/lsp/tests/README.md similarity index 95% rename from internal/lsp/tests/README.md rename to gopls/internal/lsp/tests/README.md index 64ced79702e..07df28815c1 100644 --- a/internal/lsp/tests/README.md +++ b/gopls/internal/lsp/tests/README.md @@ -26,7 +26,7 @@ in the golden file with a heading like, ``` The format of these headings vary: they are defined by the -[`Golden`](https://pkg.go.dev/golang.org/x/tools/internal/lsp/tests#Data.Golden) +[`Golden`](https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/tests#Data.Golden) function for each annotation. In the case above, the format is: annotation name, file name, annotation line location, annotation character location. diff --git a/internal/lsp/tests/compare/text.go b/gopls/internal/lsp/tests/compare/text.go similarity index 96% rename from internal/lsp/tests/compare/text.go rename to gopls/internal/lsp/tests/compare/text.go index efc2e8cae67..a3023967385 100644 --- a/internal/lsp/tests/compare/text.go +++ b/gopls/internal/lsp/tests/compare/text.go @@ -7,7 +7,7 @@ package compare import ( "fmt" - "golang.org/x/tools/internal/lsp/diff" + "golang.org/x/tools/internal/diff" ) // Text returns a formatted unified diff of the edits to go from want to diff --git a/internal/lsp/tests/compare/text_test.go b/gopls/internal/lsp/tests/compare/text_test.go similarity index 92% rename from internal/lsp/tests/compare/text_test.go rename to gopls/internal/lsp/tests/compare/text_test.go index 6b3aaea8c3f..8f5af48bd11 100644 --- a/internal/lsp/tests/compare/text_test.go +++ b/gopls/internal/lsp/tests/compare/text_test.go @@ -7,7 +7,7 @@ package compare_test import ( "testing" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) func TestText(t *testing.T) { diff --git a/internal/lsp/tests/markdown_go118.go b/gopls/internal/lsp/tests/markdown_go118.go similarity index 96% rename from internal/lsp/tests/markdown_go118.go rename to gopls/internal/lsp/tests/markdown_go118.go index 37ad62d476a..c8c5eef172b 100644 --- a/internal/lsp/tests/markdown_go118.go +++ b/gopls/internal/lsp/tests/markdown_go118.go @@ -12,7 +12,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) // The markdown in the golden files matches the converter in comment.go, diff --git a/internal/lsp/tests/markdown_go119.go b/gopls/internal/lsp/tests/markdown_go119.go similarity index 92% rename from internal/lsp/tests/markdown_go119.go rename to gopls/internal/lsp/tests/markdown_go119.go index 51aea4ddc18..e09ed62c4aa 100644 --- a/internal/lsp/tests/markdown_go119.go +++ b/gopls/internal/lsp/tests/markdown_go119.go @@ -10,7 +10,7 @@ package tests import ( "testing" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) // The markdown in the golden files matches the converter in comment.go, diff --git a/internal/lsp/tests/normalizer.go b/gopls/internal/lsp/tests/normalizer.go similarity index 100% rename from internal/lsp/tests/normalizer.go rename to gopls/internal/lsp/tests/normalizer.go diff --git a/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go similarity index 99% rename from internal/lsp/tests/tests.go rename to gopls/internal/lsp/tests/tests.go index 53c04f1220e..cad0e9a5b33 100644 --- a/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -27,11 +27,11 @@ import ( "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages/packagestest" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/source/completion" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/completion" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" diff --git a/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go similarity index 98% rename from internal/lsp/tests/util.go rename to gopls/internal/lsp/tests/util.go index 66ff217c78b..d697f184237 100644 --- a/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -16,11 +16,11 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/source/completion" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/myers" + "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/internal/span" ) diff --git a/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go similarity index 99% rename from internal/lsp/text_synchronization.go rename to gopls/internal/lsp/text_synchronization.go index dd6714553c1..c360e1e1eeb 100644 --- a/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -14,8 +14,8 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go similarity index 97% rename from internal/lsp/work/completion.go rename to gopls/internal/lsp/work/completion.go index c7227bc268b..623d2ce8bda 100644 --- a/internal/lsp/work/completion.go +++ b/gopls/internal/lsp/work/completion.go @@ -15,8 +15,8 @@ import ( "strings" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, position protocol.Position) (*protocol.CompletionList, error) { diff --git a/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go similarity index 94% rename from internal/lsp/work/diagnostics.go rename to gopls/internal/lsp/work/diagnostics.go index e583e60fd75..0a382194a47 100644 --- a/internal/lsp/work/diagnostics.go +++ b/gopls/internal/lsp/work/diagnostics.go @@ -12,9 +12,9 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/work/format.go b/gopls/internal/lsp/work/format.go similarity index 89% rename from internal/lsp/work/format.go rename to gopls/internal/lsp/work/format.go index 35b804a73b5..ef5ac46dd8c 100644 --- a/internal/lsp/work/format.go +++ b/gopls/internal/lsp/work/format.go @@ -9,8 +9,8 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) { diff --git a/internal/lsp/work/hover.go b/gopls/internal/lsp/work/hover.go similarity index 96% rename from internal/lsp/work/hover.go rename to gopls/internal/lsp/work/hover.go index 8f7822d5b4b..ea4c1e58d9f 100644 --- a/internal/lsp/work/hover.go +++ b/gopls/internal/lsp/work/hover.go @@ -12,8 +12,8 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { diff --git a/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go similarity index 97% rename from internal/lsp/workspace.go rename to gopls/internal/lsp/workspace.go index b41406db5dc..79f144b8993 100644 --- a/internal/lsp/workspace.go +++ b/gopls/internal/lsp/workspace.go @@ -8,8 +8,8 @@ import ( "context" "fmt" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/workspace_symbol.go b/gopls/internal/lsp/workspace_symbol.go similarity index 86% rename from internal/lsp/workspace_symbol.go rename to gopls/internal/lsp/workspace_symbol.go index 20c5763ab73..9101a3e7d11 100644 --- a/internal/lsp/workspace_symbol.go +++ b/gopls/internal/lsp/workspace_symbol.go @@ -8,8 +8,8 @@ import ( "context" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) { diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index c7901806ed3..98d783c67c9 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -18,16 +18,16 @@ import ( "time" "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/fakenet" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/lsprpc" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // This package implements benchmarks that share a common editor session. diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go index a8725cee3d3..7b833b6e7c4 100644 --- a/gopls/internal/regtest/bench/completion_test.go +++ b/gopls/internal/regtest/bench/completion_test.go @@ -10,9 +10,9 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/fake" ) type completionBenchOptions struct { diff --git a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go index 00048f854e4..5fd5e9c577b 100644 --- a/gopls/internal/regtest/bench/didchange_test.go +++ b/gopls/internal/regtest/bench/didchange_test.go @@ -8,7 +8,7 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/fake" ) // BenchmarkDidChange benchmarks modifications of a single file by making diff --git a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go index b11173a8ff2..baa92fc4b4d 100644 --- a/gopls/internal/regtest/bench/iwl_test.go +++ b/gopls/internal/regtest/bench/iwl_test.go @@ -8,8 +8,8 @@ import ( "context" "testing" - "golang.org/x/tools/internal/lsp/fake" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/fake" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // BenchmarkInitialWorkspaceLoad benchmarks the initial workspace load time for diff --git a/gopls/internal/regtest/bench/stress_test.go b/gopls/internal/regtest/bench/stress_test.go index a410c3049c0..b1198b43338 100644 --- a/gopls/internal/regtest/bench/stress_test.go +++ b/gopls/internal/regtest/bench/stress_test.go @@ -12,11 +12,11 @@ import ( "time" "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/lsprpc" ) // github.com/pilosa/pilosa is a repository that has historically caused diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index f67e2dae66a..a0ddfe62485 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -9,12 +9,12 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/tests/compare" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go index 3764888d74e..762694ac718 100644 --- a/gopls/internal/regtest/codelens/gcdetails_test.go +++ b/gopls/internal/regtest/codelens/gcdetails_test.go @@ -11,11 +11,11 @@ import ( "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" ) func TestGCDetails_Toggle(t *testing.T) { diff --git a/gopls/internal/regtest/completion/completion18_test.go b/gopls/internal/regtest/completion/completion18_test.go index 9683e30c828..7c532529c7b 100644 --- a/gopls/internal/regtest/completion/completion18_test.go +++ b/gopls/internal/regtest/completion/completion18_test.go @@ -10,7 +10,7 @@ package completion import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // test generic receivers diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 51a54c49737..eaf4327a686 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -10,11 +10,11 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go index 54860474e73..e02f4c8ce10 100644 --- a/gopls/internal/regtest/completion/postfix_snippet_test.go +++ b/gopls/internal/regtest/completion/postfix_snippet_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestPostfixSnippetCompletion(t *testing.T) { diff --git a/gopls/internal/regtest/debug/debug_test.go b/gopls/internal/regtest/debug/debug_test.go index bae14802eef..f8efb8f5d30 100644 --- a/gopls/internal/regtest/debug/debug_test.go +++ b/gopls/internal/regtest/debug/debug_test.go @@ -8,8 +8,8 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" ) func TestMain(m *testing.M) { diff --git a/gopls/internal/regtest/diagnostics/analysis_test.go b/gopls/internal/regtest/diagnostics/analysis_test.go index fbebf602dc9..56ee23f3f85 100644 --- a/gopls/internal/regtest/diagnostics/analysis_test.go +++ b/gopls/internal/regtest/diagnostics/analysis_test.go @@ -7,8 +7,8 @@ package diagnostics import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // Test for the timeformat analyzer, following golang/vscode-go#2406. diff --git a/gopls/internal/regtest/diagnostics/builtin_test.go b/gopls/internal/regtest/diagnostics/builtin_test.go index 775e7ec0b14..8de47bcd946 100644 --- a/gopls/internal/regtest/diagnostics/builtin_test.go +++ b/gopls/internal/regtest/diagnostics/builtin_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestIssue44866(t *testing.T) { diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 432b9b1e46f..473d9b74174 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -11,12 +11,12 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/diagnostics/invalidation_test.go b/gopls/internal/regtest/diagnostics/invalidation_test.go index ea65037644c..2f0b173160c 100644 --- a/gopls/internal/regtest/diagnostics/invalidation_test.go +++ b/gopls/internal/regtest/diagnostics/invalidation_test.go @@ -8,8 +8,8 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // Test for golang/go#50267: diagnostics should be re-sent after a file is diff --git a/gopls/internal/regtest/diagnostics/undeclared_test.go b/gopls/internal/regtest/diagnostics/undeclared_test.go index ed2b1d0a630..c3456fa2d58 100644 --- a/gopls/internal/regtest/diagnostics/undeclared_test.go +++ b/gopls/internal/regtest/diagnostics/undeclared_test.go @@ -7,8 +7,8 @@ package diagnostics import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestUndeclaredDiagnostics(t *testing.T) { diff --git a/gopls/internal/regtest/inlayhints/inlayhints_test.go b/gopls/internal/regtest/inlayhints/inlayhints_test.go index 1ca1dfbc09e..4c8e75707e7 100644 --- a/gopls/internal/regtest/inlayhints/inlayhints_test.go +++ b/gopls/internal/regtest/inlayhints/inlayhints_test.go @@ -7,9 +7,9 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/source" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/call_hierarchy_test.go b/gopls/internal/regtest/misc/call_hierarchy_test.go index 9d98896ce2e..ece05b0e614 100644 --- a/gopls/internal/regtest/misc/call_hierarchy_test.go +++ b/gopls/internal/regtest/misc/call_hierarchy_test.go @@ -6,8 +6,8 @@ package misc import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // Test for golang/go#49125 diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index 433f96e8555..9cd0f0ff91e 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -7,7 +7,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/debugserver_test.go b/gopls/internal/regtest/misc/debugserver_test.go index c0df87070c0..519f7944790 100644 --- a/gopls/internal/regtest/misc/debugserver_test.go +++ b/gopls/internal/regtest/misc/debugserver_test.go @@ -8,10 +8,10 @@ import ( "net/http" "testing" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestStartDebugging(t *testing.T) { diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index 1842cb5bb3c..f2b7b1adbec 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -9,12 +9,12 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/fake" ) const internalDefinition = ` diff --git a/gopls/internal/regtest/misc/embed_test.go b/gopls/internal/regtest/misc/embed_test.go index 2e66d7866ca..3730e5ab004 100644 --- a/gopls/internal/regtest/misc/embed_test.go +++ b/gopls/internal/regtest/misc/embed_test.go @@ -6,7 +6,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index 185a68cfc8b..9ec5fb5916f 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -7,7 +7,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // This test passes (TestHoverOnError in definition_test.go) without diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index 6c3ea7c0bbb..f9283d74f72 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -7,10 +7,10 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/tests/compare" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // A basic test for fillstruct, now that it uses a command. diff --git a/gopls/internal/regtest/misc/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go index a697b7a1b5a..6b20afa98f0 100644 --- a/gopls/internal/regtest/misc/formatting_test.go +++ b/gopls/internal/regtest/misc/formatting_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/tests/compare" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) const unformattedProgram = ` diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go index 44789514f40..ca3461d68fe 100644 --- a/gopls/internal/regtest/misc/generate_test.go +++ b/gopls/internal/regtest/misc/generate_test.go @@ -12,7 +12,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestGenerateProgress(t *testing.T) { diff --git a/gopls/internal/regtest/misc/highlight_test.go b/gopls/internal/regtest/misc/highlight_test.go index affbffd66f4..8e7ce525e35 100644 --- a/gopls/internal/regtest/misc/highlight_test.go +++ b/gopls/internal/regtest/misc/highlight_test.go @@ -8,9 +8,9 @@ import ( "sort" "testing" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestWorkspacePackageHighlight(t *testing.T) { diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go index 4701b075acc..bea370a21ce 100644 --- a/gopls/internal/regtest/misc/hover_test.go +++ b/gopls/internal/regtest/misc/hover_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/fake" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/fake" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/import_test.go b/gopls/internal/regtest/misc/import_test.go index ef4fbde362b..2e95e83a562 100644 --- a/gopls/internal/regtest/misc/import_test.go +++ b/gopls/internal/regtest/misc/import_test.go @@ -8,10 +8,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) func TestAddImport(t *testing.T) { diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index c0e213e9aec..8a781bd424a 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -11,9 +11,9 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/link_test.go b/gopls/internal/regtest/misc/link_test.go index 3dc195a2d8f..5e937f73a75 100644 --- a/gopls/internal/regtest/misc/link_test.go +++ b/gopls/internal/regtest/misc/link_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/misc_test.go b/gopls/internal/regtest/misc/misc_test.go index c553bdb3780..12aea697c15 100644 --- a/gopls/internal/regtest/misc/misc_test.go +++ b/gopls/internal/regtest/misc/misc_test.go @@ -8,8 +8,8 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" ) func TestMain(m *testing.M) { diff --git a/gopls/internal/regtest/misc/multiple_adhoc_test.go b/gopls/internal/regtest/misc/multiple_adhoc_test.go index 5f803e4e385..400e7843483 100644 --- a/gopls/internal/regtest/misc/multiple_adhoc_test.go +++ b/gopls/internal/regtest/misc/multiple_adhoc_test.go @@ -7,7 +7,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestMultipleAdHocPackages(t *testing.T) { diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index de2e9b97fd8..058aad3e60b 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestStdlibReferences(t *testing.T) { diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 1d980d90aa9..70fc9c976da 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index 4437d402d46..b0296ee771f 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -7,8 +7,8 @@ package misc import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestBadURICrash_VSCodeIssue1498(t *testing.T) { diff --git a/gopls/internal/regtest/misc/settings_test.go b/gopls/internal/regtest/misc/settings_test.go index 62d3d903160..dd4042989a8 100644 --- a/gopls/internal/regtest/misc/settings_test.go +++ b/gopls/internal/regtest/misc/settings_test.go @@ -7,7 +7,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestEmptyDirectoryFilters_Issue51843(t *testing.T) { diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index 64e07208a59..e47ca2959d1 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -7,8 +7,8 @@ package misc import ( "testing" - "golang.org/x/tools/internal/lsp/fake" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/fake" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // Smoke test that simultaneous editing sessions in the same workspace works. diff --git a/gopls/internal/regtest/misc/staticcheck_test.go b/gopls/internal/regtest/misc/staticcheck_test.go index 6f1bda35068..86766900b71 100644 --- a/gopls/internal/regtest/misc/staticcheck_test.go +++ b/gopls/internal/regtest/misc/staticcheck_test.go @@ -9,7 +9,7 @@ import ( "golang.org/x/tools/internal/testenv" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestStaticcheckGenerics(t *testing.T) { diff --git a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go index b0f507aaf32..cec33cad173 100644 --- a/gopls/internal/regtest/misc/vendor_test.go +++ b/gopls/internal/regtest/misc/vendor_test.go @@ -7,9 +7,9 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 41d0375fd25..b17a279141d 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -9,9 +9,9 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go index 2dc3a1b10d0..4ba31354ed9 100644 --- a/gopls/internal/regtest/misc/workspace_symbol_test.go +++ b/gopls/internal/regtest/misc/workspace_symbol_test.go @@ -7,9 +7,9 @@ package misc import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index dee7cb34933..7da8830c910 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -11,11 +11,11 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/tests/compare" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index ade9ac93068..91c704de0d9 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -9,9 +9,9 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" ) func TestMain(m *testing.M) { diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 31655955e23..1be766b8689 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -8,11 +8,11 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index b06c15580c0..f839b0a3419 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go index bdc60a06ee3..3efa9f322a8 100644 --- a/gopls/internal/regtest/workspace/directoryfilters_test.go +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // This file contains regression tests for the directoryFilters setting. diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go index 8a77867cf44..83059bc75ce 100644 --- a/gopls/internal/regtest/workspace/fromenv_test.go +++ b/gopls/internal/regtest/workspace/fromenv_test.go @@ -7,7 +7,7 @@ package workspace import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // Test that setting go.work via environment variables or settings works. diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index 4c3f46b0a93..0356ebcb55d 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -7,7 +7,7 @@ package workspace import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 5196aa90966..cf24886cea2 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -11,12 +11,12 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/testenv" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestMain(m *testing.M) { diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index b9aba157068..9ed1e0ba2d1 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -16,7 +16,7 @@ import ( "golang.org/x/tools/go/packages" gvc "golang.org/x/tools/gopls/internal/govulncheck" - "golang.org/x/tools/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/vuln/client" "golang.org/x/vuln/osv" "golang.org/x/vuln/vulncheck" diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index 1ce1fae7d20..9b8678dc1e0 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -20,10 +20,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/vuln/client" "golang.org/x/vuln/osv" ) diff --git a/gopls/internal/vulncheck/util.go b/gopls/internal/vulncheck/util.go index 05332d375c3..d21d30517a4 100644 --- a/gopls/internal/vulncheck/util.go +++ b/gopls/internal/vulncheck/util.go @@ -15,7 +15,7 @@ import ( "os/exec" gvc "golang.org/x/tools/gopls/internal/govulncheck" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/vuln/osv" "golang.org/x/vuln/vulncheck" ) diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go index 7fc05ae423b..227b57d7f95 100644 --- a/gopls/internal/vulncheck/vulncheck.go +++ b/gopls/internal/vulncheck/vulncheck.go @@ -13,7 +13,7 @@ import ( "errors" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/command" ) // Govulncheck runs the in-process govulncheck implementation. diff --git a/gopls/main.go b/gopls/main.go index f73eabf5767..837a01d40b5 100644 --- a/gopls/main.go +++ b/gopls/main.go @@ -17,7 +17,7 @@ import ( "os" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/cmd" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/release/release.go b/gopls/release/release.go index 173909122b3..fb456b6aa0e 100644 --- a/gopls/release/release.go +++ b/gopls/release/release.go @@ -135,7 +135,7 @@ func validateHardcodedVersion(wd string, version string) error { Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes, - }, "golang.org/x/tools/internal/lsp/debug") + }, "golang.org/x/tools/gopls/internal/lsp/debug") if err != nil { return err } diff --git a/gopls/test/debug/debug_test.go b/gopls/test/debug/debug_test.go index 4d680eebbbe..9c6c0d6246e 100644 --- a/gopls/test/debug/debug_test.go +++ b/gopls/test/debug/debug_test.go @@ -21,9 +21,9 @@ import ( "github.com/jba/templatecheck" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" ) @@ -90,7 +90,7 @@ func TestTemplates(t *testing.T) { cfg := &packages.Config{ Mode: packages.NeedTypesInfo | packages.LoadAllSyntax, // figure out what's necessary PJW } - pkgs, err := packages.Load(cfg, "golang.org/x/tools/internal/lsp/debug") + pkgs, err := packages.Load(cfg, "golang.org/x/tools/gopls/internal/lsp/debug") if err != nil { t.Fatal(err) } diff --git a/gopls/test/gopls_test.go b/gopls/test/gopls_test.go index 6282224abb5..4f266baaa1e 100644 --- a/gopls/test/gopls_test.go +++ b/gopls/test/gopls_test.go @@ -9,10 +9,10 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - cmdtest "golang.org/x/tools/internal/lsp/cmd/test" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" + cmdtest "golang.org/x/tools/gopls/internal/lsp/cmd/test" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/test/json_test.go b/gopls/test/json_test.go index 5ea5b343450..69b7e485ddf 100644 --- a/gopls/test/json_test.go +++ b/gopls/test/json_test.go @@ -12,7 +12,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // verify that type errors in Initialize lsp messages don't cause diff --git a/internal/lsp/bug/bug.go b/internal/bug/bug.go similarity index 100% rename from internal/lsp/bug/bug.go rename to internal/bug/bug.go diff --git a/internal/lsp/bug/bug_test.go b/internal/bug/bug_test.go similarity index 100% rename from internal/lsp/bug/bug_test.go rename to internal/bug/bug_test.go diff --git a/internal/lsp/diff/diff.go b/internal/diff/diff.go similarity index 100% rename from internal/lsp/diff/diff.go rename to internal/diff/diff.go diff --git a/internal/lsp/diff/diff_test.go b/internal/diff/diff_test.go similarity index 93% rename from internal/lsp/diff/diff_test.go rename to internal/diff/diff_test.go index a3699498914..1bf03ea3dec 100644 --- a/internal/lsp/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -10,8 +10,8 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/difftest" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/difftest" "golang.org/x/tools/internal/span" ) @@ -131,9 +131,9 @@ func TestUnified(t *testing.T) { } func TestRegressionOld001(t *testing.T) { - a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/lsp/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" - b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/lsp/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" diffs, err := diff.NComputeEdits(span.URI("file://one"), a, b) if err != nil { t.Error(err) diff --git a/internal/lsp/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go similarity index 98% rename from internal/lsp/diff/difftest/difftest.go rename to internal/diff/difftest/difftest.go index a78e2674521..616125693a3 100644 --- a/internal/lsp/diff/difftest/difftest.go +++ b/internal/diff/difftest/difftest.go @@ -4,14 +4,14 @@ // Package difftest supplies a set of tests that will operate on any // implementation of a diff algorithm as exposed by -// "golang.org/x/tools/internal/lsp/diff" +// "golang.org/x/tools/internal/diff" package difftest import ( "fmt" "testing" - "golang.org/x/tools/internal/lsp/diff" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/diff/difftest/difftest_test.go b/internal/diff/difftest/difftest_test.go similarity index 95% rename from internal/lsp/diff/difftest/difftest_test.go rename to internal/diff/difftest/difftest_test.go index fd7ecf95997..d070d3276fc 100644 --- a/internal/lsp/diff/difftest/difftest_test.go +++ b/internal/diff/difftest/difftest_test.go @@ -4,7 +4,7 @@ // Package difftest supplies a set of tests that will operate on any // implementation of a diff algorithm as exposed by -// "golang.org/x/tools/internal/lsp/diff" +// "golang.org/x/tools/internal/diff" package difftest_test import ( @@ -15,7 +15,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/diff/difftest" + "golang.org/x/tools/internal/diff/difftest" "golang.org/x/tools/internal/testenv" ) diff --git a/internal/lsp/diff/lcs/common.go b/internal/diff/lcs/common.go similarity index 100% rename from internal/lsp/diff/lcs/common.go rename to internal/diff/lcs/common.go diff --git a/internal/lsp/diff/lcs/common_test.go b/internal/diff/lcs/common_test.go similarity index 100% rename from internal/lsp/diff/lcs/common_test.go rename to internal/diff/lcs/common_test.go diff --git a/internal/lsp/diff/lcs/doc.go b/internal/diff/lcs/doc.go similarity index 100% rename from internal/lsp/diff/lcs/doc.go rename to internal/diff/lcs/doc.go diff --git a/internal/lsp/diff/lcs/git.sh b/internal/diff/lcs/git.sh similarity index 100% rename from internal/lsp/diff/lcs/git.sh rename to internal/diff/lcs/git.sh diff --git a/internal/lsp/diff/lcs/labels.go b/internal/diff/lcs/labels.go similarity index 100% rename from internal/lsp/diff/lcs/labels.go rename to internal/diff/lcs/labels.go diff --git a/internal/lsp/diff/lcs/old.go b/internal/diff/lcs/old.go similarity index 100% rename from internal/lsp/diff/lcs/old.go rename to internal/diff/lcs/old.go diff --git a/internal/lsp/diff/lcs/old_test.go b/internal/diff/lcs/old_test.go similarity index 95% rename from internal/lsp/diff/lcs/old_test.go rename to internal/diff/lcs/old_test.go index ba22fe6f461..828a9d90e22 100644 --- a/internal/lsp/diff/lcs/old_test.go +++ b/internal/diff/lcs/old_test.go @@ -97,9 +97,9 @@ func TestSpecialOld(t *testing.T) { // needs lcs.fix } func TestRegressionOld001(t *testing.T) { - a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/lsp/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" - b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/lsp/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" for i := 1; i < len(b); i++ { diffs, lcs := Compute([]byte(a), []byte(b), int(i)) // 14 from gopls if !lcs.valid() { diff --git a/internal/lsp/diff/myers/diff.go b/internal/diff/myers/diff.go similarity index 99% rename from internal/lsp/diff/myers/diff.go rename to internal/diff/myers/diff.go index a59475058a5..07e40281107 100644 --- a/internal/lsp/diff/myers/diff.go +++ b/internal/diff/myers/diff.go @@ -8,7 +8,7 @@ package myers import ( "strings" - "golang.org/x/tools/internal/lsp/diff" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/diff/myers/diff_test.go b/internal/diff/myers/diff_test.go similarity index 74% rename from internal/lsp/diff/myers/diff_test.go rename to internal/diff/myers/diff_test.go index bce0399c58d..f244455586b 100644 --- a/internal/lsp/diff/myers/diff_test.go +++ b/internal/diff/myers/diff_test.go @@ -7,8 +7,8 @@ package myers_test import ( "testing" - "golang.org/x/tools/internal/lsp/diff/difftest" - "golang.org/x/tools/internal/lsp/diff/myers" + "golang.org/x/tools/internal/diff/difftest" + "golang.org/x/tools/internal/diff/myers" ) func TestDiff(t *testing.T) { diff --git a/internal/lsp/diff/ndiff.go b/internal/diff/ndiff.go similarity index 98% rename from internal/lsp/diff/ndiff.go rename to internal/diff/ndiff.go index 8f7732dd019..e537b72458d 100644 --- a/internal/lsp/diff/ndiff.go +++ b/internal/diff/ndiff.go @@ -8,7 +8,7 @@ import ( "strings" "unicode/utf8" - "golang.org/x/tools/internal/lsp/diff/lcs" + "golang.org/x/tools/internal/diff/lcs" "golang.org/x/tools/internal/span" ) diff --git a/internal/lsp/diff/unified.go b/internal/diff/unified.go similarity index 100% rename from internal/lsp/diff/unified.go rename to internal/diff/unified.go diff --git a/internal/lsp/debug/tag/tag.go b/internal/event/tag/tag.go similarity index 100% rename from internal/lsp/debug/tag/tag.go rename to internal/event/tag/tag.go diff --git a/internal/lsp/fuzzy/input.go b/internal/fuzzy/input.go similarity index 100% rename from internal/lsp/fuzzy/input.go rename to internal/fuzzy/input.go diff --git a/internal/lsp/fuzzy/input_test.go b/internal/fuzzy/input_test.go similarity index 98% rename from internal/lsp/fuzzy/input_test.go rename to internal/fuzzy/input_test.go index 0228347e4f0..64f66e363b8 100644 --- a/internal/lsp/fuzzy/input_test.go +++ b/internal/fuzzy/input_test.go @@ -9,7 +9,7 @@ import ( "sort" "testing" - "golang.org/x/tools/internal/lsp/fuzzy" + "golang.org/x/tools/internal/fuzzy" ) var rolesTests = []struct { diff --git a/internal/lsp/fuzzy/matcher.go b/internal/fuzzy/matcher.go similarity index 100% rename from internal/lsp/fuzzy/matcher.go rename to internal/fuzzy/matcher.go diff --git a/internal/lsp/fuzzy/matcher_test.go b/internal/fuzzy/matcher_test.go similarity index 99% rename from internal/lsp/fuzzy/matcher_test.go rename to internal/fuzzy/matcher_test.go index 132ab5c800a..528224bd98d 100644 --- a/internal/lsp/fuzzy/matcher_test.go +++ b/internal/fuzzy/matcher_test.go @@ -13,7 +13,7 @@ import ( "math" "testing" - "golang.org/x/tools/internal/lsp/fuzzy" + "golang.org/x/tools/internal/fuzzy" ) type comparator struct { diff --git a/internal/lsp/fuzzy/symbol.go b/internal/fuzzy/symbol.go similarity index 100% rename from internal/lsp/fuzzy/symbol.go rename to internal/fuzzy/symbol.go diff --git a/internal/lsp/fuzzy/symbol_test.go b/internal/fuzzy/symbol_test.go similarity index 97% rename from internal/lsp/fuzzy/symbol_test.go rename to internal/fuzzy/symbol_test.go index cb28160dedb..df74bbe0d37 100644 --- a/internal/lsp/fuzzy/symbol_test.go +++ b/internal/fuzzy/symbol_test.go @@ -7,7 +7,7 @@ package fuzzy_test import ( "testing" - . "golang.org/x/tools/internal/lsp/fuzzy" + . "golang.org/x/tools/internal/fuzzy" ) func TestSymbolMatchIndex(t *testing.T) { diff --git a/internal/jsonrpc2/conn.go b/internal/jsonrpc2/conn.go index ca7752d664a..529cfa5ded3 100644 --- a/internal/jsonrpc2/conn.go +++ b/internal/jsonrpc2/conn.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) // Conn is the common interface to jsonrpc clients and servers. diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index edcf0939f2a..47668e209be 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) // Binder builds a connection configuration. diff --git a/internal/span/token.go b/internal/span/token.go index 61b5995e058..d3827eb5fd0 100644 --- a/internal/span/token.go +++ b/internal/span/token.go @@ -8,7 +8,7 @@ import ( "fmt" "go/token" - "golang.org/x/tools/internal/lsp/bug" + "golang.org/x/tools/internal/bug" ) // Range represents a source code range in token.Pos form. From 5f27e05096aca9bfb880e6c7d2767e795a12d37b Mon Sep 17 00:00:00 2001 From: cui fliter Date: Wed, 7 Sep 2022 12:45:26 +0000 Subject: [PATCH 230/723] all: remove redundant type conversion Change-Id: Iffb04ebd6b4afbe1a13b96b7e7469ec3b3e3380f GitHub-Last-Rev: 6682724eb7cce8e3194650d28cb5eebd78e14700 GitHub-Pull-Request: golang/tools#396 Reviewed-on: https://go-review.googlesource.com/c/tools/+/428980 TryBot-Result: Gopher Robot Reviewed-by: Michael Knyszek Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Robert Findley --- cmd/splitdwarf/splitdwarf.go | 2 +- container/intsets/sparse.go | 2 +- internal/fastwalk/fastwalk_dirent_ino.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/splitdwarf/splitdwarf.go b/cmd/splitdwarf/splitdwarf.go index 13888aa512e..9729b0b7a6a 100644 --- a/cmd/splitdwarf/splitdwarf.go +++ b/cmd/splitdwarf/splitdwarf.go @@ -182,7 +182,7 @@ for input_exe need to allow writing. oldsym := symtab.Syms[ii] newsymtab.Syms = append(newsymtab.Syms, oldsym) - linkeditsyms = append(linkeditsyms, macho.Nlist64{Name: uint32(linkeditstringcur), + linkeditsyms = append(linkeditsyms, macho.Nlist64{Name: linkeditstringcur, Type: oldsym.Type, Sect: oldsym.Sect, Desc: oldsym.Desc, Value: oldsym.Value}) linkeditstringcur += uint32(len(oldsym.Name)) + 1 linkeditstrings = append(linkeditstrings, oldsym.Name) diff --git a/container/intsets/sparse.go b/container/intsets/sparse.go index c06aec80b0d..d5fe156ed36 100644 --- a/container/intsets/sparse.go +++ b/container/intsets/sparse.go @@ -190,7 +190,7 @@ func (b *block) min(take bool) int { if take { b.bits[i] = w &^ (1 << uint(tz)) } - return b.offset + int(i*bitsPerWord) + tz + return b.offset + i*bitsPerWord + tz } } panic("BUG: empty block") diff --git a/internal/fastwalk/fastwalk_dirent_ino.go b/internal/fastwalk/fastwalk_dirent_ino.go index ea02b9ebfe8..041775bf7c1 100644 --- a/internal/fastwalk/fastwalk_dirent_ino.go +++ b/internal/fastwalk/fastwalk_dirent_ino.go @@ -11,5 +11,5 @@ package fastwalk import "syscall" func direntInode(dirent *syscall.Dirent) uint64 { - return uint64(dirent.Ino) + return dirent.Ino } From 3ee171058fb26bc230e8f90c816f26cd005bcac2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 7 Sep 2022 13:43:00 -0400 Subject: [PATCH 231/723] gopls/doc: update stale documentation and improve link names Following up on comments from CL 428595 and CL 426796, improve links to 'here', and update a stale comment on gopls' code location. Updates golang/go#54509 Change-Id: Ie0e04b01b6e7193294fb9c39a809cee1a5b981c5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/429215 Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- gopls/doc/contributing.md | 4 ++-- gopls/doc/settings.md | 13 +++++++------ gopls/internal/lsp/source/api_json.go | 6 +++--- gopls/internal/lsp/source/options.go | 15 ++++++++------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/gopls/doc/contributing.md b/gopls/doc/contributing.md index 110aa1bdc84..367280f53e3 100644 --- a/gopls/doc/contributing.md +++ b/gopls/doc/contributing.md @@ -18,8 +18,8 @@ claiming it. ## Getting started -Most of the `gopls` logic is actually in the `golang.org/x/tools/gopls/internal/lsp` -directory, so you are most likely to develop in the golang.org/x/tools module. +Most of the `gopls` logic is in the `golang.org/x/tools/gopls/internal/lsp` +directory. ## Build diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 04124687e9d..58e97503f36 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -286,8 +286,8 @@ Default: `true`. analyses specify analyses that the user would like to enable or disable. A map of the names of analysis passes that should be enabled/disabled. -A full list of analyzers that gopls uses can be found -[here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). +A full list of analyzers that gopls uses can be found in +[analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). Example Usage: @@ -307,7 +307,8 @@ Default: `{}`. **This setting is experimental and may be deleted.** staticcheck enables additional analyses from staticcheck.io. -These analyses are documented at [here](https://staticcheck.io/docs/checks/). +These analyses are documented on +[Staticcheck's website](https://staticcheck.io/docs/checks/). Default: `false`. @@ -401,9 +402,9 @@ Default: `true`. **This setting is experimental and may be deleted.** -hints specify inlay hints that users want to see. -A full list of hints that gopls uses can be found -[here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). +hints specify inlay hints that users want to see. A full list of hints +that gopls uses can be found in +[inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). Default: `{}`. diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 94e6b1d2a89..56f7c0f5524 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/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\n[here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).\n\nExample Usage:\n\n```json5\n...\n\"analyses\": {\n \"unreachable\": false, // Disable the unreachable analyzer.\n \"unusedparams\": true // Enable the unusedparams analyzer.\n}\n...\n```\n", + Doc: "analyses specify analyses that the user would like to enable or disable.\nA map of the names of analysis passes that should be enabled/disabled.\nA full list of analyzers that gopls uses can be found in\n[analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).\n\nExample Usage:\n\n```json5\n...\n\"analyses\": {\n \"unreachable\": false, // Disable the unreachable analyzer.\n \"unusedparams\": true // Enable the unusedparams analyzer.\n}\n...\n```\n", EnumKeys: EnumKeys{ ValueType: "bool", Keys: []EnumKey{ @@ -461,7 +461,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "staticcheck", Type: "bool", - Doc: "staticcheck enables additional analyses from staticcheck.io.\nThese analyses are documented at [here](https://staticcheck.io/docs/checks/).\n", + Doc: "staticcheck enables additional analyses from staticcheck.io.\nThese analyses are documented on\n[Staticcheck's website](https://staticcheck.io/docs/checks/).\n", Default: "false", Status: "experimental", Hierarchy: "ui.diagnostic", @@ -518,7 +518,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "hints", Type: "map[string]bool", - Doc: "hints specify inlay hints that users want to see.\nA full list of hints that gopls uses can be found\n[here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n", + Doc: "hints specify inlay hints that users want to see. A full list of hints\nthat gopls uses can be found in\n[inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n", EnumKeys: EnumKeys{Keys: []EnumKey{ { Name: "\"assignVariableTypes\"", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index c904340c970..fdcb98c5225 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -65,9 +65,9 @@ import ( "golang.org/x/tools/gopls/internal/lsp/analysis/unusedvariable" "golang.org/x/tools/gopls/internal/lsp/analysis/useany" "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/diff/myers" - "golang.org/x/tools/gopls/internal/lsp/protocol" ) var ( @@ -386,8 +386,8 @@ type FormattingOptions struct { type DiagnosticOptions struct { // Analyses specify analyses that the user would like to enable or disable. // A map of the names of analysis passes that should be enabled/disabled. - // A full list of analyzers that gopls uses can be found - // [here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). + // A full list of analyzers that gopls uses can be found in + // [analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). // // Example Usage: // @@ -402,7 +402,8 @@ type DiagnosticOptions struct { Analyses map[string]bool // Staticcheck enables additional analyses from staticcheck.io. - // These analyses are documented at [here](https://staticcheck.io/docs/checks/). + // These analyses are documented on + // [Staticcheck's website](https://staticcheck.io/docs/checks/). Staticcheck bool `status:"experimental"` // Annotations specifies the various kinds of optimization diagnostics @@ -428,9 +429,9 @@ type DiagnosticOptions struct { } type InlayHintOptions struct { - // Hints specify inlay hints that users want to see. - // A full list of hints that gopls uses can be found - // [here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). + // Hints specify inlay hints that users want to see. A full list of hints + // that gopls uses can be found in + // [inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). Hints map[string]bool `status:"experimental"` } From 308e02cdee75cfa6522a1ad842cac5e996068075 Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Wed, 7 Sep 2022 13:45:32 -0700 Subject: [PATCH 232/723] x/tools/go/ssa: disable slice-to-array test CL 428938 implements slice to array conversions in the type checkers, per accepted proposal issue golang/go#46505. The disabled test was expected to fail, but it won't fail anymore for Go 1.20 and higher. Disable it for now (and track it in golang/go#54822) so we can make progress with CL 428938. For golang/go#46505. For golang/go#54822. Change-Id: I87fc78dccaa2bf1517cf875fec97855e360d0f25 Reviewed-on: https://go-review.googlesource.com/c/tools/+/429295 Auto-Submit: Robert Griesemer Reviewed-by: Robert Griesemer Run-TryBot: Robert Griesemer gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Tim King --- go/ssa/builder_go117_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go/ssa/builder_go117_test.go b/go/ssa/builder_go117_test.go index f6545e5e2cf..46a09526f59 100644 --- a/go/ssa/builder_go117_test.go +++ b/go/ssa/builder_go117_test.go @@ -57,7 +57,8 @@ func TestBuildPackageFailuresGo117(t *testing.T) { importer types.Importer }{ {"slice to array pointer - source is not a slice", "package p; var s [4]byte; var _ = (*[4]byte)(s)", nil}, - {"slice to array pointer - dest is not a pointer", "package p; var s []byte; var _ = ([4]byte)(s)", nil}, + // TODO(taking) re-enable test below for Go versions < Go 1.20 - see issue #54822 + // {"slice to array pointer - dest is not a pointer", "package p; var s []byte; var _ = ([4]byte)(s)", nil}, {"slice to array pointer - dest pointer elem is not an array", "package p; var s []byte; var _ = (*byte)(s)", nil}, } From a63075185d7ac26b2ac079a255fee138ce7ec2fe Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 7 Sep 2022 14:01:20 -0400 Subject: [PATCH 233/723] x/tools: update go.mod following moving LSP code to x/tools/gopls For golang/go#54509 Change-Id: I1c3cb834e7216bde11ebd86654ae4dbce02d655e Reviewed-on: https://go-review.googlesource.com/c/tools/+/429216 Reviewed-by: Hyang-Ah Hana Kim Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- go.mod | 2 -- go.sum | 3 --- 2 files changed, 5 deletions(-) diff --git a/go.mod b/go.mod index 272a6d2eaa7..492efe92e5f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,5 @@ require ( github.com/yuin/goldmark v1.4.13 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 golang.org/x/net v0.0.0-20220722155237-a158d28d115b - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f - golang.org/x/text v0.3.7 ) diff --git a/go.sum b/go.sum index f603000deb2..20a9d4158b0 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -21,7 +19,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= From 4754f75dadcf850602a0407141893c2da4d57716 Mon Sep 17 00:00:00 2001 From: cuiweixie Date: Wed, 7 Sep 2022 22:01:46 +0800 Subject: [PATCH 234/723] go/analysis/passes/copylock: modify match pattern to satisfy change sync.Once.done to atomic.Bool Change-Id: I387c946736d76613c83d2363cb85cbc038056e0c Reviewed-on: https://go-review.googlesource.com/c/tools/+/428986 Run-TryBot: Bryan Mills Reviewed-by: Michael Knyszek Run-TryBot: xie cui <523516579@qq.com> TryBot-Result: Gopher Robot Reviewed-by: Bryan Mills gopls-CI: kokoro --- .../copylock/testdata/src/a/copylock.go | 22 +++++++++---------- .../copylock/testdata/src/a/copylock_func.go | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go/analysis/passes/copylock/testdata/src/a/copylock.go b/go/analysis/passes/copylock/testdata/src/a/copylock.go index 7704b3a42b2..4ab66dca1f6 100644 --- a/go/analysis/passes/copylock/testdata/src/a/copylock.go +++ b/go/analysis/passes/copylock/testdata/src/a/copylock.go @@ -50,27 +50,27 @@ func BadFunc() { var t Tlock var tp *Tlock tp = &t - *tp = t // want `assignment copies lock value to \*tp: a.Tlock contains sync.Once contains sync.Mutex` - t = *tp // want "assignment copies lock value to t: a.Tlock contains sync.Once contains sync.Mutex" + *tp = t // want `assignment copies lock value to \*tp: a.Tlock contains sync.Once contains sync\b.*` + t = *tp // want `assignment copies lock value to t: a.Tlock contains sync.Once contains sync\b.*` y := *x // want "assignment copies lock value to y: sync.Mutex" - var z = t // want "variable declaration copies lock value to z: a.Tlock contains sync.Once contains sync.Mutex" + var z = t // want `variable declaration copies lock value to z: a.Tlock contains sync.Once contains sync\b.*` w := struct{ L sync.Mutex }{ L: *x, // want `literal copies lock value from \*x: sync.Mutex` } var q = map[int]Tlock{ - 1: t, // want "literal copies lock value from t: a.Tlock contains sync.Once contains sync.Mutex" - 2: *tp, // want `literal copies lock value from \*tp: a.Tlock contains sync.Once contains sync.Mutex` + 1: t, // want `literal copies lock value from t: a.Tlock contains sync.Once contains sync\b.*` + 2: *tp, // want `literal copies lock value from \*tp: a.Tlock contains sync.Once contains sync\b.*` } yy := []Tlock{ - t, // want "literal copies lock value from t: a.Tlock contains sync.Once contains sync.Mutex" - *tp, // want `literal copies lock value from \*tp: a.Tlock contains sync.Once contains sync.Mutex` + t, // want `literal copies lock value from t: a.Tlock contains sync.Once contains sync\b.*` + *tp, // want `literal copies lock value from \*tp: a.Tlock contains sync.Once contains sync\b.*` } // override 'new' keyword new := func(interface{}) {} - new(t) // want "call of new copies lock value: a.Tlock contains sync.Once contains sync.Mutex" + new(t) // want `call of new copies lock value: a.Tlock contains sync.Once contains sync\b.*` // copy of array of locks var muA [5]sync.Mutex @@ -193,9 +193,9 @@ func SyncTypesCheck() { var onceX sync.Once var onceXX = sync.Once{} onceX1 := new(sync.Once) - onceY := onceX // want "assignment copies lock value to onceY: sync.Once contains sync.Mutex" - onceY = onceX // want "assignment copies lock value to onceY: sync.Once contains sync.Mutex" - var onceYY = onceX // want "variable declaration copies lock value to onceYY: sync.Once contains sync.Mutex" + onceY := onceX // want `assignment copies lock value to onceY: sync.Once contains sync\b.*` + onceY = onceX // want `assignment copies lock value to onceY: sync.Once contains sync\b.*` + var onceYY = onceX // want `variable declaration copies lock value to onceYY: sync.Once contains sync\b.*` onceP := &onceX onceZ := &sync.Once{} } diff --git a/go/analysis/passes/copylock/testdata/src/a/copylock_func.go b/go/analysis/passes/copylock/testdata/src/a/copylock_func.go index 801bc6f24f1..0d3168f1ef1 100644 --- a/go/analysis/passes/copylock/testdata/src/a/copylock_func.go +++ b/go/analysis/passes/copylock/testdata/src/a/copylock_func.go @@ -126,7 +126,7 @@ func AcceptedCases() { // sync.Mutex gets called out, but without any reference to the sync.Once. type LocalOnce sync.Once -func (LocalOnce) Bad() {} // want "Bad passes lock by value: a.LocalOnce contains sync.Mutex" +func (LocalOnce) Bad() {} // want `Bad passes lock by value: a.LocalOnce contains sync.\b.*` // False negative: // LocalMutex doesn't have a Lock method. From eeaf4eb24c733638f3405f6ea6944361e4d99ad8 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 30 Aug 2022 11:56:37 -0400 Subject: [PATCH 235/723] internal/lsp/fake: add rename file support for testing This CL implements the fake.Editor.RenameFile method, which mimic's the behavior of VS Code when renaming files. Specifically: - open buffers affected by the renaming are closed, and then reopened with new URIs - files are moved on disk - didChangeWatchedFile notifications are sent Along the way, add missing comments and fix one place where the editor mutex was held while sending notifications (in Editor.CreateBuffer). Generally, the editor should not hold any mutex while making a remote call. For golang/go#41567 Change-Id: I2abfa846e923de566a21c096502a68f125e7e671 Reviewed-on: https://go-review.googlesource.com/c/tools/+/427903 Auto-Submit: Robert Findley Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/fake/client.go | 10 +- gopls/internal/lsp/fake/editor.go | 125 ++++++++++++++++++--- gopls/internal/lsp/fake/workdir.go | 28 ++++- gopls/internal/lsp/regtest/wrappers.go | 9 ++ gopls/internal/regtest/misc/rename_test.go | 68 +++++++++++ 5 files changed, 215 insertions(+), 25 deletions(-) diff --git a/gopls/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go index 037de8e3d19..5885c29c9f9 100644 --- a/gopls/internal/lsp/fake/client.go +++ b/gopls/internal/lsp/fake/client.go @@ -121,14 +121,8 @@ func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceE return &protocol.ApplyWorkspaceEditResult{FailureReason: "Edit.Changes is unsupported"}, nil } for _, change := range params.Edit.DocumentChanges { - // Todo: Add a handler for RenameFile edits - if change.RenameFile != nil { - panic("Fake editor does not support the RenameFile edits.") - } - if change.TextDocumentEdit != nil { - if err := c.editor.applyProtocolEdit(ctx, *change.TextDocumentEdit); err != nil { - return nil, err - } + if err := c.editor.applyDocumentChange(ctx, change); err != nil { + return nil, err } } return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index ebdf1a891d0..2c988ce6669 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -16,10 +16,11 @@ import ( "strings" "sync" - "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) @@ -39,7 +40,7 @@ type Editor struct { mu sync.Mutex // guards config, buffers, serverCapabilities config EditorConfig // editor configuration - buffers map[string]buffer // open buffers + buffers map[string]buffer // open buffers (relative path -> buffer content) serverCapabilities protocol.ServerCapabilities // capabilities / options // Call metrics for the purpose of expectations. This is done in an ad-hoc @@ -50,16 +51,18 @@ type Editor struct { calls CallCounts } +// CallCounts tracks the number of protocol notifications of different types. type CallCounts struct { DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose uint64 } +// buffer holds information about an open buffer in the editor. type buffer struct { - windowsLineEndings bool - version int - path string - lines []string - dirty bool + windowsLineEndings bool // use windows line endings when merging lines + version int // monotonic version; incremented on edits + path string // relative path in the workspace + lines []string // line content + dirty bool // if true, content is unsaved (TODO(rfindley): rename this field) } func (b buffer) text() string { @@ -374,7 +377,6 @@ func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error { e.mu.Lock() - defer e.mu.Unlock() buf := buffer{ windowsLineEndings: e.config.WindowsLineEndings, @@ -385,13 +387,25 @@ func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, cont } e.buffers[path] = buf - item := protocol.TextDocumentItem{ + item := e.textDocumentItem(buf) + e.mu.Unlock() + + return e.sendDidOpen(ctx, item) +} + +// textDocumentItem builds a protocol.TextDocumentItem for the given buffer. +// +// Precondition: e.mu must be held. +func (e *Editor) textDocumentItem(buf buffer) protocol.TextDocumentItem { + return protocol.TextDocumentItem{ URI: e.sandbox.Workdir.URI(buf.path), LanguageID: languageID(buf.path, e.config.FileAssociations), Version: int32(buf.version), Text: buf.text(), } +} +func (e *Editor) sendDidOpen(ctx context.Context, item protocol.TextDocumentItem) error { if e.Server != nil { if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ TextDocument: item, @@ -451,9 +465,13 @@ func (e *Editor) CloseBuffer(ctx context.Context, path string) error { delete(e.buffers, path) e.mu.Unlock() + return e.sendDidClose(ctx, e.TextDocumentIdentifier(path)) +} + +func (e *Editor) sendDidClose(ctx context.Context, doc protocol.TextDocumentIdentifier) error { if e.Server != nil { if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{ - TextDocument: e.TextDocumentIdentifier(path), + TextDocument: doc, }); err != nil { return fmt.Errorf("DidClose: %w", err) } @@ -1157,16 +1175,91 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin return err } for _, change := range wsEdits.DocumentChanges { - if change.TextDocumentEdit != nil { - if err := e.applyProtocolEdit(ctx, *change.TextDocumentEdit); err != nil { - return err - } + if err := e.applyDocumentChange(ctx, change); err != nil { + return err } } return nil } -func (e *Editor) applyProtocolEdit(ctx context.Context, change protocol.TextDocumentEdit) error { +func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error { + closed, opened, err := e.renameBuffers(ctx, oldPath, newPath) + if err != nil { + return err + } + + for _, c := range closed { + if err := e.sendDidClose(ctx, c); err != nil { + return err + } + } + for _, o := range opened { + if err := e.sendDidOpen(ctx, o); err != nil { + return err + } + } + + // Finally, perform the renaming on disk. + return e.sandbox.Workdir.RenameFile(ctx, oldPath, newPath) +} + +// renameBuffers renames in-memory buffers affected by the renaming of +// oldPath->newPath, returning the resulting text documents that must be closed +// and opened over the LSP. +func (e *Editor) renameBuffers(ctx context.Context, oldPath, newPath string) (closed []protocol.TextDocumentIdentifier, opened []protocol.TextDocumentItem, _ error) { + e.mu.Lock() + defer e.mu.Unlock() + + // In case either oldPath or newPath is absolute, convert to absolute paths + // before checking for containment. + oldAbs := e.sandbox.Workdir.AbsPath(oldPath) + newAbs := e.sandbox.Workdir.AbsPath(newPath) + + // Collect buffers that are affected by the given file or directory renaming. + buffersToRename := make(map[string]string) // old path -> new path + + for path := range e.buffers { + abs := e.sandbox.Workdir.AbsPath(path) + if oldAbs == abs || source.InDirLex(oldAbs, abs) { + rel, err := filepath.Rel(oldAbs, abs) + if err != nil { + return nil, nil, fmt.Errorf("filepath.Rel(%q, %q): %v", oldAbs, abs, err) + } + nabs := filepath.Join(newAbs, rel) + newPath := e.sandbox.Workdir.RelPath(nabs) + buffersToRename[path] = newPath + } + } + + // Update buffers, and build protocol changes. + for old, new := range buffersToRename { + buf := e.buffers[old] + delete(e.buffers, old) + buf.version = 1 + buf.path = new + e.buffers[new] = buf + + closed = append(closed, e.TextDocumentIdentifier(old)) + opened = append(opened, e.textDocumentItem(buf)) + } + + return closed, opened, nil +} + +func (e *Editor) applyDocumentChange(ctx context.Context, change protocol.DocumentChanges) error { + if change.RenameFile != nil { + oldPath := e.sandbox.Workdir.URIToPath(change.RenameFile.OldURI) + newPath := e.sandbox.Workdir.URIToPath(change.RenameFile.NewURI) + + return e.RenameFile(ctx, oldPath, newPath) + } + if change.TextDocumentEdit != nil { + return e.applyTextDocumentEdit(ctx, *change.TextDocumentEdit) + } + panic("Internal error: one of RenameFile or TextDocumentEdit must be set") +} + +func (e *Editor) applyTextDocumentEdit(ctx context.Context, change protocol.TextDocumentEdit) error { path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) if ver := int32(e.BufferVersion(path)); ver != change.TextDocument.Version { return fmt.Errorf("buffer versions for %q do not match: have %d, editing %d", path, ver, change.TextDocument.Version) diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index 4b8c2f30fd0..65302606092 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -315,13 +315,39 @@ func (w *Workdir) writeFile(ctx context.Context, path, content string) (FileEven if err := WriteFileData(path, []byte(content), w.RelativeTo); err != nil { return FileEvent{}, err } + return w.fileEvent(path, changeType), nil +} + +func (w *Workdir) fileEvent(path string, changeType protocol.FileChangeType) FileEvent { return FileEvent{ Path: path, ProtocolEvent: protocol.FileEvent{ URI: w.URI(path), Type: changeType, }, - }, nil + } +} + +// RenameFile performs an on disk-renaming of the workdir-relative oldPath to +// workdir-relative newPath. +func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error { + oldAbs := w.AbsPath(oldPath) + newAbs := w.AbsPath(newPath) + + if err := os.Rename(oldAbs, newAbs); err != nil { + return err + } + + // Send synthetic file events for the renaming. Renamed files are handled as + // Delete+Create events: + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#fileChangeType + events := []FileEvent{ + w.fileEvent(oldPath, protocol.Deleted), + w.fileEvent(newPath, protocol.Created), + } + w.sendEvents(ctx, events) + + return nil } // listFiles lists files in the given directory, returning a map of relative diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index b5d1783e55a..0f7cc9a1a66 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -400,6 +400,7 @@ func (e *Env) References(path string, pos fake.Pos) []protocol.Location { return locations } +// Rename wraps Editor.Rename, calling t.Fatal on any error. func (e *Env) Rename(path string, pos fake.Pos, newName string) { e.T.Helper() if err := e.Editor.Rename(e.Ctx, path, pos, newName); err != nil { @@ -407,6 +408,14 @@ func (e *Env) Rename(path string, pos fake.Pos, newName string) { } } +// RenameFile wraps Editor.RenameFile, calling t.Fatal on any error. +func (e *Env) RenameFile(oldPath, newPath string) { + e.T.Helper() + if err := e.Editor.RenameFile(e.Ctx, oldPath, newPath); err != nil { + e.T.Fatal(err) + } +} + // Completion executes a completion request on the server. func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList { e.T.Helper() diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 70fc9c976da..a0447c5026d 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -132,3 +132,71 @@ func main() { } }) } + +// This is a test that rename operation initiated by the editor function as expected. +func TestRenameFileFromEditor(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.16 +-- a/a.go -- +package a + +const X = 1 +-- a/x.go -- +package a + +const X = 2 +-- b/b.go -- +package b +` + + Run(t, files, func(t *testing.T, env *Env) { + // Rename files and verify that diagnostics are affected accordingly. + + // Initially, we should have diagnostics on both X's, for their duplicate declaration. + env.Await( + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "X"), + env.DiagnosticAtRegexp("a/x.go", "X"), + ), + ) + + // Moving x.go should make the diagnostic go away. + env.RenameFile("a/x.go", "b/x.go") + env.Await( + OnceMet( + env.DoneWithChangeWatchedFiles(), + EmptyDiagnostics("a/a.go"), // no more duplicate declarations + env.DiagnosticAtRegexp("b/b.go", "package"), // as package names mismatch + ), + ) + + // Renaming should also work on open buffers. + env.OpenFile("b/x.go") + + // Moving x.go back to a/ should cause the diagnostics to reappear. + env.RenameFile("b/x.go", "a/x.go") + // TODO(rfindley): enable using a OnceMet precondition here. We can't + // currently do this because DidClose, DidOpen and DidChangeWatchedFiles + // are sent, and it is not easy to use all as a precondition. + env.Await( + env.DiagnosticAtRegexp("a/a.go", "X"), + env.DiagnosticAtRegexp("a/x.go", "X"), + ) + + // Renaming the entire directory should move both the open and closed file. + env.RenameFile("a", "x") + env.Await( + env.DiagnosticAtRegexp("x/a.go", "X"), + env.DiagnosticAtRegexp("x/x.go", "X"), + ) + + // As a sanity check, verify that x/x.go is open. + if text := env.Editor.BufferText("x/x.go"); text == "" { + t.Fatal("got empty buffer for x/x.go") + } + }) +} From 0eebaabce71008160b146cfde709cd0ab971f928 Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 30 Aug 2022 10:28:12 -0700 Subject: [PATCH 236/723] go/analysis: allow for identical SuggestedFixes Stop treating identical SuggestedFixes as overlapping text edits. Packages can be loaded in multiple ways due to testing, e.g. "p" and "p [p.test]". Currently SuggestedFixes from both are considered overlapping and so are not applied. Updates applyFixes() to report errors in more situations. Fixes golang/go#54740 Change-Id: I73acb3b73d88535144cfae5161faabb0615a1774 Reviewed-on: https://go-review.googlesource.com/c/tools/+/426734 Reviewed-by: Abirdcfly Abirdcfly Run-TryBot: Tim King Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- go/analysis/internal/checker/checker.go | 34 ++++- go/analysis/internal/checker/checker_test.go | 9 +- go/analysis/internal/checker/fix_test.go | 143 +++++++++++++++++++ 3 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 go/analysis/internal/checker/fix_test.go diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index ca77a764ce8..c4eae378871 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -147,7 +147,11 @@ func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) { roots := analyze(initial, analyzers) if Fix { - applyFixes(roots) + if err := applyFixes(roots); err != nil { + // Fail when applying fixes failed. + log.Print(err) + return 1 + } } return printDiagnostics(roots) } @@ -305,7 +309,7 @@ func analyze(pkgs []*packages.Package, analyzers []*analysis.Analyzer) []*action return roots } -func applyFixes(roots []*action) { +func applyFixes(roots []*action) error { visited := make(map[*action]bool) var apply func(*action) error var visitAll func(actions []*action) error @@ -313,7 +317,9 @@ func applyFixes(roots []*action) { for _, act := range actions { if !visited[act] { visited[act] = true - visitAll(act.deps) + if err := visitAll(act.deps); err != nil { + return err + } if err := apply(act); err != nil { return err } @@ -332,6 +338,10 @@ func applyFixes(roots []*action) { edit offsetedit left, right *node } + // Edits x and y are equivalent. + equiv := func(x, y offsetedit) bool { + return x.start == y.start && x.end == y.end && bytes.Equal(x.newText, y.newText) + } var insert func(tree **node, edit offsetedit) error insert = func(treeptr **node, edit offsetedit) error { @@ -345,6 +355,13 @@ func applyFixes(roots []*action) { } else if edit.start >= tree.edit.end { return insert(&tree.right, edit) } + if equiv(edit, tree.edit) { // equivalent edits? + // We skip over equivalent edits without considering them + // an error. This handles identical edits coming from the + // multiple ways of loading a package into a + // *go/packages.Packages for testing, e.g. packages "p" and "p [p.test]". + return nil + } // Overlapping text edit. return fmt.Errorf("analyses applying overlapping text edits affecting pos range (%v, %v) and (%v, %v)", @@ -384,14 +401,16 @@ func applyFixes(roots []*action) { return nil } - visitAll(roots) + if err := visitAll(roots); err != nil { + return err + } fset := token.NewFileSet() // Shared by parse calls below // Now we've got a set of valid edits for each file. Get the new file contents. for f, tree := range editsForFile { contents, err := ioutil.ReadFile(f.Name()) if err != nil { - log.Fatal(err) + return err } cur := 0 // current position in the file @@ -432,8 +451,11 @@ func applyFixes(roots []*action) { } } - ioutil.WriteFile(f.Name(), out.Bytes(), 0644) + if err := ioutil.WriteFile(f.Name(), out.Bytes(), 0644); err != nil { + return err + } } + return nil } // printDiagnostics prints the diagnostics for the root packages in either diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go index eee211c21a4..383a8e1dd50 100644 --- a/go/analysis/internal/checker/checker_test.go +++ b/go/analysis/internal/checker/checker_test.go @@ -19,14 +19,9 @@ import ( "golang.org/x/tools/internal/testenv" ) -var from, to string - func TestApplyFixes(t *testing.T) { testenv.NeedsGoPackages(t) - from = "bar" - to = "baz" - files := map[string]string{ "rename/test.go": `package rename @@ -75,6 +70,10 @@ var analyzer = &analysis.Analyzer{ } func run(pass *analysis.Pass) (interface{}, error) { + const ( + from = "bar" + to = "baz" + ) inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{(*ast.Ident)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { diff --git a/go/analysis/internal/checker/fix_test.go b/go/analysis/internal/checker/fix_test.go new file mode 100644 index 00000000000..a114c01b645 --- /dev/null +++ b/go/analysis/internal/checker/fix_test.go @@ -0,0 +1,143 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package checker_test + +import ( + "flag" + "io/ioutil" + "os" + "os/exec" + "path" + "runtime" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/internal/checker" + "golang.org/x/tools/internal/testenv" +) + +func main() { + checker.Fix = true + patterns := flag.Args() + + code := checker.Run(patterns, []*analysis.Analyzer{analyzer}) + os.Exit(code) +} + +// TestFixes ensures that checker.Run applies fixes correctly. +// This test fork/execs the main function above. +func TestFixes(t *testing.T) { + oses := map[string]bool{"darwin": true, "linux": true} + if !oses[runtime.GOOS] { + t.Skipf("skipping fork/exec test on this platform") + } + + if os.Getenv("TESTFIXES_CHILD") == "1" { + // child process + + // replace [progname -test.run=TestFixes -- ...] + // by [progname ...] + os.Args = os.Args[2:] + os.Args[0] = "vet" + main() + panic("unreachable") + } + + testenv.NeedsTool(t, "go") + + files := map[string]string{ + "rename/foo.go": `package rename + +func Foo() { + bar := 12 + _ = bar +} + +// the end +`, + "rename/intestfile_test.go": `package rename + +func InTestFile() { + bar := 13 + _ = bar +} + +// the end +`, + "rename/foo_test.go": `package rename_test + +func Foo() { + bar := 14 + _ = bar +} + +// the end +`, + } + fixed := map[string]string{ + "rename/foo.go": `package rename + +func Foo() { + baz := 12 + _ = baz +} + +// the end +`, + "rename/intestfile_test.go": `package rename + +func InTestFile() { + baz := 13 + _ = baz +} + +// the end +`, + "rename/foo_test.go": `package rename_test + +func Foo() { + baz := 14 + _ = baz +} + +// the end +`, + } + dir, cleanup, err := analysistest.WriteFiles(files) + if err != nil { + t.Fatalf("Creating test files failed with %s", err) + } + defer cleanup() + + args := []string{"-test.run=TestFixes", "--", "rename"} + cmd := exec.Command(os.Args[0], args...) + cmd.Env = append(os.Environ(), "TESTFIXES_CHILD=1", "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off") + + out, err := cmd.CombinedOutput() + if len(out) > 0 { + t.Logf("%s: out=<<%s>>", args, out) + } + var exitcode int + if err, ok := err.(*exec.ExitError); ok { + exitcode = err.ExitCode() // requires go1.12 + } + + const diagnosticsExitCode = 3 + if exitcode != diagnosticsExitCode { + t.Errorf("%s: exited %d, want %d", args, exitcode, diagnosticsExitCode) + } + + for name, want := range fixed { + path := path.Join(dir, "src", name) + contents, err := ioutil.ReadFile(path) + if err != nil { + t.Errorf("error reading %s: %v", path, err) + } + if got := string(contents); got != want { + t.Errorf("contents of %s file did not match expectations. got=%s, want=%s", path, got, want) + } + } +} From e71c338beb115bd6674a15a7f0119d04a609323c Mon Sep 17 00:00:00 2001 From: Tim King Date: Wed, 10 Aug 2022 15:14:19 -0700 Subject: [PATCH 237/723] go/ssa/ssautil: initialize info when there is syntax Only initialize *types.Info when there are []*ast.Files available. Previously, when packages x and z, where x imports y and y imports z, are loaded with packages.LoadSyntax and built with ssautil.Packages, then package y will have *types.Info but no files. If y has any globals initialized from a func variable, the bodies of the function literals did not have type information and failed to build. Fixes golang/go#53604 Change-Id: I3cce608d6a127ac44b65947b68cf0d748fc2dfc6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/422639 Reviewed-by: Zvonimir Pavlinovic Reviewed-by: Alan Donovan --- go/ssa/builder.go | 3 ++ go/ssa/ssautil/load.go | 4 ++- go/ssa/ssautil/load_test.go | 56 +++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index b36775a4e34..98ed49dfead 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -2461,6 +2461,9 @@ func (p *Package) build() { } // Initialize package-level vars in correct order. + if len(p.info.InitOrder) > 0 && len(p.files) == 0 { + panic("no source files provided for package. cannot initialize globals") + } for _, varinit := range p.info.InitOrder { if init.Prog.mode&LogSource != 0 { fmt.Fprintf(os.Stderr, "build global initializer %v @ %s\n", diff --git a/go/ssa/ssautil/load.go b/go/ssa/ssautil/load.go index 58d185f6727..96d69a20a17 100644 --- a/go/ssa/ssautil/load.go +++ b/go/ssa/ssautil/load.go @@ -77,10 +77,12 @@ func doPackages(initial []*packages.Package, mode ssa.BuilderMode, deps bool) (* packages.Visit(initial, nil, func(p *packages.Package) { if p.Types != nil && !p.IllTyped { var files []*ast.File + var info *types.Info if deps || isInitial[p] { files = p.Syntax + info = p.TypesInfo } - ssamap[p] = prog.CreatePackage(p.Types, files, p.TypesInfo, true) + ssamap[p] = prog.CreatePackage(p.Types, files, info, true) } }) diff --git a/go/ssa/ssautil/load_test.go b/go/ssa/ssautil/load_test.go index f769be273bb..707a1d0b69d 100644 --- a/go/ssa/ssautil/load_test.go +++ b/go/ssa/ssautil/load_test.go @@ -12,10 +12,12 @@ import ( "go/token" "go/types" "os" + "path" "strings" "testing" "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" "golang.org/x/tools/internal/testenv" @@ -135,3 +137,57 @@ func TestIssue28106(t *testing.T) { prog, _ := ssautil.Packages(pkgs, ssa.BuilderMode(0)) prog.Build() // no crash } + +func TestIssue53604(t *testing.T) { + // Tests that variable initializers are not added to init() when syntax + // is not present but types.Info is available. + // + // Packages x, y, z are loaded with mode `packages.LoadSyntax`. + // Package x imports y, and y imports z. + // Packages are built using ssautil.Packages() with x and z as roots. + // This setup creates y using CreatePackage(pkg, files, info, ...) + // where len(files) == 0 but info != nil. + // + // Tests that globals from y are not initialized. + e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{ + { + Name: "golang.org/fake", + Files: map[string]interface{}{ + "x/x.go": `package x; import "golang.org/fake/y"; var V = y.F()`, + "y/y.go": `package y; import "golang.org/fake/z"; var F = func () *int { return &z.Z } `, + "z/z.go": `package z; var Z int`, + }, + }, + }) + defer e.Cleanup() + + // Load x and z as entry packages using packages.LoadSyntax + e.Config.Mode = packages.LoadSyntax + pkgs, err := packages.Load(e.Config, path.Join(e.Temp(), "fake/x"), path.Join(e.Temp(), "fake/z")) + if err != nil { + t.Fatal(err) + } + for _, p := range pkgs { + if len(p.Errors) > 0 { + t.Fatalf("%v", p.Errors) + } + } + + prog, _ := ssautil.Packages(pkgs, ssa.BuilderMode(0)) + prog.Build() + + // y does not initialize F. + y := prog.ImportedPackage("golang.org/fake/y") + if y == nil { + t.Fatal("Failed to load intermediate package y") + } + yinit := y.Members["init"].(*ssa.Function) + for _, bb := range yinit.Blocks { + for _, i := range bb.Instrs { + if store, ok := i.(*ssa.Store); ok && store.Addr == y.Var("F") { + t.Errorf("y.init() stores to F %v", store) + } + } + } + +} From 678efe0184ccc1ee853ed74216cfb7135ed5583d Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 8 Sep 2022 09:58:25 -0400 Subject: [PATCH 238/723] gopls/internal/lsp/cmd: fix vulncheck error handling Fix 1 - print usage info only when appropriate: tool.CommandLineError is a special error that makes a command exits with full usage documentation. When Govulncheck hook runs and fails, it's unlikely the failure was from command line misuse and the extra usage doc will distract users from the root cause. Let's stop that by returning a plain error. Fix 2 - stop printing package loading errors twice: Package loading error was printed by the logger in gopls/internal/vulncheck.cmd and once more by the gopls command line handler. Print it once. For testing, added back the replace statement to go.mod. We will update go.mod back after this commit is merged. Change-Id: Ifaf569a31bbbc84d7b84e1b6d90a8fa0b27ac758 Reviewed-on: https://go-review.googlesource.com/c/tools/+/429515 Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Reviewed-on: https://go-review.googlesource.com/c/tools/+/429937 --- gopls/internal/lsp/cmd/vulncheck.go | 6 +++--- gopls/internal/vulncheck/command.go | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gopls/internal/lsp/cmd/vulncheck.go b/gopls/internal/lsp/cmd/vulncheck.go index 5ee9b0e3721..770455cfd90 100644 --- a/gopls/internal/lsp/cmd/vulncheck.go +++ b/gopls/internal/lsp/cmd/vulncheck.go @@ -71,7 +71,7 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { opts := source.DefaultOptions().Clone() v.app.options(opts) // register hook if opts == nil || opts.Hooks.Govulncheck == nil { - return tool.CommandLineErrorf("vulncheck feature is not available") + return fmt.Errorf("vulncheck feature is not available") } loadCfg := &packages.Config{ @@ -83,11 +83,11 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { res, err := opts.Hooks.Govulncheck(ctx, loadCfg, pattern) if err != nil { - return tool.CommandLineErrorf("govulncheck failed: %v", err) + return fmt.Errorf("vulncheck failed: %v", err) } data, err := json.MarshalIndent(res, " ", " ") if err != nil { - return tool.CommandLineErrorf("failed to decode results: %v", err) + return fmt.Errorf("vulncheck failed to encode result: %v", err) } fmt.Printf("%s", data) return nil diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 9ed1e0ba2d1..641c9ddeb67 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -9,6 +9,7 @@ package vulncheck import ( "context" + "fmt" "log" "os" "sort" @@ -78,8 +79,8 @@ func (c *cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) logger.Println("loading packages...") loadedPkgs, err := gvc.LoadPackages(cfg, patterns...) if err != nil { - logger.Printf("package load failed: %v", err) - return nil, err + logger.Printf("%v", err) + return nil, fmt.Errorf("package load failed") } logger.Printf("analyzing %d packages...\n", len(loadedPkgs)) From a8b9ed3e93f79481c8d1076d3d2ef91551fc4554 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Sun, 11 Sep 2022 18:04:30 -0400 Subject: [PATCH 239/723] gopls/internal/lsp/source: remove Govulncheck from Hooks Now lsp packages are moved to the gopls module where the vulncheck implementation exists. We no longer need to inject govulncheck through the hook. Change-Id: Ia627f1abe4c626d254d3e72b778535d6cb1ab41e Reviewed-on: https://go-review.googlesource.com/c/tools/+/429938 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim --- gopls/internal/hooks/hooks.go | 3 --- gopls/internal/lsp/cmd/vulncheck.go | 12 +++++++----- gopls/internal/lsp/command.go | 10 +++++++--- gopls/internal/lsp/source/options.go | 5 ----- gopls/internal/vulncheck/vulncheck.go | 5 +---- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go index 9d497e9ed15..7c4ac9df8af 100644 --- a/gopls/internal/hooks/hooks.go +++ b/gopls/internal/hooks/hooks.go @@ -11,7 +11,6 @@ import ( "context" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/diff" "mvdan.cc/gofumpt/format" "mvdan.cc/xurls/v2" @@ -37,6 +36,4 @@ func Options(options *source.Options) { }) } updateAnalyzers(options) - - options.Govulncheck = vulncheck.Govulncheck } diff --git a/gopls/internal/lsp/cmd/vulncheck.go b/gopls/internal/lsp/cmd/vulncheck.go index 770455cfd90..3ae2175a962 100644 --- a/gopls/internal/lsp/cmd/vulncheck.go +++ b/gopls/internal/lsp/cmd/vulncheck.go @@ -12,7 +12,7 @@ import ( "os" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/source" + vulnchecklib "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/tool" ) @@ -54,6 +54,10 @@ func (v *vulncheck) DetailedHelp(f *flag.FlagSet) { } func (v *vulncheck) Run(ctx context.Context, args ...string) error { + if vulnchecklib.Govulncheck == nil { + return fmt.Errorf("vulncheck command is available only in gopls compiled with go1.18 or newer") + } + if len(args) > 1 { return tool.CommandLineErrorf("vulncheck accepts at most one package pattern") } @@ -68,9 +72,7 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { } } - opts := source.DefaultOptions().Clone() - v.app.options(opts) // register hook - if opts == nil || opts.Hooks.Govulncheck == nil { + if vulnchecklib.Govulncheck == nil { return fmt.Errorf("vulncheck feature is not available") } @@ -81,7 +83,7 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { // inherit the current process's cwd and env. } - res, err := opts.Hooks.Govulncheck(ctx, loadCfg, pattern) + res, err := vulnchecklib.Govulncheck(ctx, loadCfg, pattern) if err != nil { return fmt.Errorf("vulncheck failed: %v", err) } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 18682557428..f01bcb1a23a 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -20,13 +20,14 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/debug" "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/vulncheck" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) @@ -829,7 +830,10 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc }, func(ctx context.Context, deps commandDeps) error { view := deps.snapshot.View() opts := view.Options() - if opts == nil || opts.Hooks.Govulncheck == nil { + // quickly test if gopls is compiled to support govulncheck + // by checking vulncheck.Govulncheck. Alternatively, we can continue and + // let the `gopls vulncheck` command fail. This is lighter-weight. + if vulncheck.Govulncheck == nil { return errors.New("vulncheck feature is not available") } diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index fdcb98c5225..45aeca99dd1 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -49,7 +49,6 @@ import ( "golang.org/x/tools/go/analysis/passes/unsafeptr" "golang.org/x/tools/go/analysis/passes/unusedresult" "golang.org/x/tools/go/analysis/passes/unusedwrite" - "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/analysis/embeddirective" "golang.org/x/tools/gopls/internal/lsp/analysis/fillreturns" "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" @@ -519,9 +518,6 @@ type Hooks struct { TypeErrorAnalyzers map[string]*Analyzer ConvenienceAnalyzers map[string]*Analyzer StaticcheckAnalyzers map[string]*Analyzer - - // Govulncheck is the implementation of the Govulncheck gopls command. - Govulncheck func(context.Context, *packages.Config, string) (command.VulncheckResult, error) } // InternalOptions contains settings that are not intended for use by the @@ -773,7 +769,6 @@ func (o *Options) Clone() *Options { ComputeEdits: o.ComputeEdits, GofumptFormat: o.GofumptFormat, URLRegexp: o.URLRegexp, - Govulncheck: o.Govulncheck, }, ServerOptions: o.ServerOptions, UserOptions: o.UserOptions, diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go index 227b57d7f95..d452045a5ef 100644 --- a/gopls/internal/vulncheck/vulncheck.go +++ b/gopls/internal/vulncheck/vulncheck.go @@ -10,7 +10,6 @@ package vulncheck import ( "context" - "errors" "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/command" @@ -18,6 +17,4 @@ import ( // Govulncheck runs the in-process govulncheck implementation. // With go1.18+, this is swapped with the real implementation. -var Govulncheck = func(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) { - return res, errors.New("not implemented") -} +var Govulncheck func(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) = nil From a81fce34b3e3831110e4eb3d53e90a1787801741 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Sun, 11 Sep 2022 21:24:11 -0400 Subject: [PATCH 240/723] gopls: run go mod tidy -compat=1.16 golang.org/x/text and golang.org/x/sync are direct dependencies. Change-Id: I8fea3ddbbf5e2917e45a7a9a5b1d12310fe62c83 Reviewed-on: https://go-review.googlesource.com/c/tools/+/430275 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro --- gopls/go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/go.mod b/gopls/go.mod index d994f48ef60..d6bf740c4ce 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -8,7 +8,9 @@ require ( github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 + golang.org/x/text v0.3.7 golang.org/x/tools v0.1.13-0.20220810174125-0ad49fdeb955 golang.org/x/vuln v0.0.0-20220901221904-62b0186a1058 honnef.co/go/tools v0.3.3 @@ -22,8 +24,6 @@ require ( github.com/BurntSushi/toml v1.2.0 // indirect github.com/google/safehtml v0.0.2 // indirect golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 - golang.org/x/text v0.3.7 // indirect ) replace golang.org/x/tools => ../ From 7e129ca5efc81f8dbd25b0ab9a0ae9422d06cbe0 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 17 Aug 2022 17:26:25 -0400 Subject: [PATCH 241/723] gopls: add codelens to reset upgrade diagnostics This clears the diagnostics that were added by the `Check for upgrades` codelens for the given go.mod file. The diagnostic source for the diagnostics from `Check for upgrades` is now modCheckUpgradesSource instead of modSource in order to allow us to easily clear these diagnostics. Fixes golang/go#54065 Change-Id: I7c0824ce1fdfcf2a73ec83342501ceed82fc519f Reviewed-on: https://go-review.googlesource.com/c/tools/+/426016 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Suzy Mueller Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/doc/commands.md | 14 +++ gopls/internal/lsp/cache/view.go | 13 ++- gopls/internal/lsp/code_action.go | 14 ++- gopls/internal/lsp/command.go | 16 +++ gopls/internal/lsp/command/command_gen.go | 64 +++++++----- gopls/internal/lsp/command/interface.go | 5 + gopls/internal/lsp/diagnostics.go | 22 ++++- gopls/internal/lsp/mod/code_lens.go | 5 + gopls/internal/lsp/mod/diagnostics.go | 99 ++++++++++++------- gopls/internal/lsp/source/api_json.go | 6 ++ gopls/internal/lsp/source/view.go | 7 +- .../regtest/codelens/codelens_test.go | 7 ++ 12 files changed, 202 insertions(+), 70 deletions(-) diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index c202b51a2ca..c942be48fda 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -247,6 +247,20 @@ Args: } ``` +### **Reset go.mod diagnostics** +Identifier: `gopls.reset_go_mod_diagnostics` + +Reset diagnostics in the go.mod file of a module. + +Args: + +``` +{ + // The file URI. + "URI": string, +} +``` + ### **Run test(s)** Identifier: `gopls.run_tests` diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index cc88c3de5a2..025419d71df 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -24,12 +24,12 @@ import ( "golang.org/x/mod/semver" exec "golang.org/x/sys/execabs" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/bug" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) @@ -1031,6 +1031,13 @@ func (v *View) RegisterModuleUpgrades(uri span.URI, upgrades map[string]string) } } +func (v *View) ClearModuleUpgrades(modfile span.URI) { + v.mu.Lock() + defer v.mu.Unlock() + + v.moduleUpgrades[modfile] = nil +} + // Copied from // https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/str/path.go;l=58;drc=2910c5b4a01a573ebc97744890a07c1a3122c67a func globsMatchPath(globs, target string) bool { diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index b09f42214ce..1d062d717e8 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -10,13 +10,13 @@ import ( "sort" "strings" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/imports" "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/mod" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/span" ) @@ -70,14 +70,18 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara switch kind { case source.Mod: if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 { - diags, err := mod.DiagnosticsForMod(ctx, snapshot, fh) + diags, err := mod.ModDiagnostics(ctx, snapshot, fh) if source.IsNonFatalGoModError(err) { return nil, nil } if err != nil { return nil, err } - quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, diags) + udiags, err := mod.ModUpgradeDiagnostics(ctx, snapshot, fh) + if err != nil { + return nil, err + } + quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, append(diags, udiags...)) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index f01bcb1a23a..811307c1ff6 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -202,6 +202,22 @@ func (c *commandHandler) UpgradeDependency(ctx context.Context, args command.Dep return c.GoGetModule(ctx, args) } +func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, uri command.URIArg) error { + return c.run(ctx, commandConfig{ + forURI: uri.URI, + }, func(ctx context.Context, deps commandDeps) error { + deps.snapshot.View().ClearModuleUpgrades(uri.URI.SpanURI()) + // Clear all diagnostics coming from the upgrade check source. + // This will clear the diagnostics in all go.mod files, but they + // will be re-calculated when the snapshot is diagnosed again. + c.s.clearDiagnosticSource(modCheckUpgradesSource) + + // Re-diagnose the snapshot to remove the diagnostics. + c.s.diagnoseSnapshot(deps.snapshot, nil, false) + return nil + }) +} + func (c *commandHandler) GoGetModule(ctx context.Context, args command.DependencyArgs) error { return c.run(ctx, commandConfig{ progress: "Running go get", diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go index 301cf6f604d..615f9509c8c 100644 --- a/gopls/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -19,28 +19,29 @@ import ( ) const ( - AddDependency Command = "add_dependency" - AddImport Command = "add_import" - ApplyFix Command = "apply_fix" - CheckUpgrades Command = "check_upgrades" - EditGoDirective Command = "edit_go_directive" - GCDetails Command = "gc_details" - Generate Command = "generate" - GenerateGoplsMod Command = "generate_gopls_mod" - GoGetPackage Command = "go_get_package" - ListImports Command = "list_imports" - ListKnownPackages Command = "list_known_packages" - RegenerateCgo Command = "regenerate_cgo" - RemoveDependency Command = "remove_dependency" - RunTests Command = "run_tests" - RunVulncheckExp Command = "run_vulncheck_exp" - StartDebugging Command = "start_debugging" - Test Command = "test" - Tidy Command = "tidy" - ToggleGCDetails Command = "toggle_gc_details" - UpdateGoSum Command = "update_go_sum" - UpgradeDependency Command = "upgrade_dependency" - Vendor Command = "vendor" + AddDependency Command = "add_dependency" + AddImport Command = "add_import" + ApplyFix Command = "apply_fix" + CheckUpgrades Command = "check_upgrades" + EditGoDirective Command = "edit_go_directive" + GCDetails Command = "gc_details" + Generate Command = "generate" + GenerateGoplsMod Command = "generate_gopls_mod" + GoGetPackage Command = "go_get_package" + ListImports Command = "list_imports" + ListKnownPackages Command = "list_known_packages" + RegenerateCgo Command = "regenerate_cgo" + RemoveDependency Command = "remove_dependency" + ResetGoModDiagnostics Command = "reset_go_mod_diagnostics" + RunTests Command = "run_tests" + RunVulncheckExp Command = "run_vulncheck_exp" + StartDebugging Command = "start_debugging" + Test Command = "test" + Tidy Command = "tidy" + ToggleGCDetails Command = "toggle_gc_details" + UpdateGoSum Command = "update_go_sum" + UpgradeDependency Command = "upgrade_dependency" + Vendor Command = "vendor" ) var Commands = []Command{ @@ -57,6 +58,7 @@ var Commands = []Command{ ListKnownPackages, RegenerateCgo, RemoveDependency, + ResetGoModDiagnostics, RunTests, RunVulncheckExp, StartDebugging, @@ -148,6 +150,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.RemoveDependency(ctx, a0) + case "gopls.reset_go_mod_diagnostics": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return nil, s.ResetGoModDiagnostics(ctx, a0) case "gopls.run_tests": var a0 RunTestsArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -364,6 +372,18 @@ func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) (protocol }, nil } +func NewResetGoModDiagnosticsCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.reset_go_mod_diagnostics", + Arguments: args, + }, nil +} + func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index 1fff896bd16..d49a4330d89 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -98,6 +98,11 @@ type Interface interface { // Removes a dependency from the go.mod file of a module. RemoveDependency(context.Context, RemoveDependencyArgs) error + // ResetGoModDiagnostics: Reset go.mod diagnostics + // + // Reset diagnostics in the go.mod file of a module. + ResetGoModDiagnostics(context.Context, URIArg) error + // GoGetPackage: go get a package // // Runs `go get` to fetch a package. diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index d7587621a69..c00eb4206eb 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -15,14 +15,14 @@ import ( "sync" "time" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/debug/log" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/mod" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/template" "golang.org/x/tools/gopls/internal/lsp/work" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) @@ -37,6 +37,7 @@ const ( typeCheckSource orphanedSource workSource + modCheckUpgradesSource ) // A diagnosticReport holds results for a single diagnostic source. @@ -93,6 +94,8 @@ func (d diagnosticSource) String() string { return "FromOrphans" case workSource: return "FromGoWork" + case modCheckUpgradesSource: + return "FromCheckForUpgrades" default: return fmt.Sprintf("From?%d?", d) } @@ -238,6 +241,21 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn } s.storeDiagnostics(snapshot, id.URI, modSource, diags) } + upgradeModReports, upgradeErr := mod.UpgradeDiagnostics(ctx, snapshot) + if ctx.Err() != nil { + log.Trace.Log(ctx, "diagnose cancelled") + return + } + if upgradeErr != nil { + event.Error(ctx, "warning: diagnose go.mod upgrades", upgradeErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID())) + } + for id, diags := range upgradeModReports { + if id.URI == "" { + event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) + continue + } + s.storeDiagnostics(snapshot, id.URI, modCheckUpgradesSource, diags) + } // Diagnose the go.work file, if it exists. workReports, workErr := work.Diagnostics(ctx, snapshot) diff --git a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go index 2068e60df4a..daafee0cb52 100644 --- a/gopls/internal/lsp/mod/code_lens.go +++ b/gopls/internal/lsp/mod/code_lens.go @@ -63,6 +63,10 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File if err != nil { return nil, err } + reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.URIArg{URI: uri}) + if err != nil { + return nil, err + } // Put the upgrade code lenses above the first require block or statement. rng, err := firstRequireRange(fh, pm) if err != nil { @@ -73,6 +77,7 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File {Range: rng, Command: checkUpgrade}, {Range: rng, Command: upgradeTransitive}, {Range: rng, Command: upgradeDirect}, + {Range: rng, Command: reset}, }, nil } diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 96b95c059d4..1efbfdd7de2 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -10,25 +10,36 @@ import ( "context" "fmt" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" ) func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { ctx, done := event.Start(ctx, "mod.Diagnostics", tag.Snapshot.Of(snapshot.ID())) defer done() - reports := map[source.VersionedFileIdentity][]*source.Diagnostic{} + return collectDiagnostics(ctx, snapshot, ModDiagnostics) +} + +func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { + ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", tag.Snapshot.Of(snapshot.ID())) + defer done() + + return collectDiagnostics(ctx, snapshot, ModUpgradeDiagnostics) +} + +func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { + reports := make(map[source.VersionedFileIdentity][]*source.Diagnostic) for _, uri := range snapshot.ModFiles() { fh, err := snapshot.GetVersionedFile(ctx, uri) if err != nil { return nil, err } reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{} - diagnostics, err := DiagnosticsForMod(ctx, snapshot, fh) + diagnostics, err := diagFn(ctx, snapshot, fh) if err != nil { return nil, err } @@ -43,7 +54,9 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.Vers return reports, nil } -func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]*source.Diagnostic, error) { +// ModDiagnostics returns diagnostics from diagnosing the packages in the workspace and +// from tidying the go.mod file. +func ModDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (diagnostics []*source.Diagnostic, err error) { pm, err := snapshot.ParseMod(ctx, fh) if err != nil { if pm == nil || len(pm.ParseErrors) == 0 { @@ -52,9 +65,50 @@ func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source. return pm.ParseErrors, nil } - var diagnostics []*source.Diagnostic + // Packages in the workspace can contribute diagnostics to go.mod files. + // TODO(rfindley): Try to avoid calling DiagnosePackage on all packages in the workspace here, + // for every go.mod file. If gc_details is enabled, it looks like this could lead to extra + // go command invocations (as gc details is not memoized). + wspkgs, err := snapshot.ActivePackages(ctx) + if err != nil && !source.IsNonFatalGoModError(err) { + event.Error(ctx, fmt.Sprintf("workspace packages: diagnosing %s", pm.URI), err) + } + if err == nil { + for _, pkg := range wspkgs { + pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) + if err != nil { + return nil, err + } + diagnostics = append(diagnostics, pkgDiagnostics[fh.URI()]...) + } + } + + tidied, err := snapshot.ModTidy(ctx, pm) + if err != nil && !source.IsNonFatalGoModError(err) { + event.Error(ctx, fmt.Sprintf("tidy: diagnosing %s", pm.URI), err) + } + if err == nil { + for _, d := range tidied.Diagnostics { + if d.URI != fh.URI() { + continue + } + diagnostics = append(diagnostics, d) + } + } + return diagnostics, nil +} + +// ModUpgradeDiagnostics adds upgrade quick fixes for individual modules if the upgrades +// are recorded in the view. +func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (upgradeDiagnostics []*source.Diagnostic, err error) { + pm, err := snapshot.ParseMod(ctx, fh) + if err != nil { + if pm == nil || len(pm.ParseErrors) == 0 { + return nil, err + } + return nil, nil + } - // Add upgrade quick fixes for individual modules if we know about them. upgrades := snapshot.View().ModuleUpgrades(fh.URI()) for _, req := range pm.File.Require { ver, ok := upgrades[req.Mod.Path] @@ -75,7 +129,7 @@ func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source. if err != nil { return nil, err } - diagnostics = append(diagnostics, &source.Diagnostic{ + upgradeDiagnostics = append(upgradeDiagnostics, &source.Diagnostic{ URI: fh.URI(), Range: rng, Severity: protocol.SeverityInformation, @@ -85,32 +139,5 @@ func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source. }) } - // Packages in the workspace can contribute diagnostics to go.mod files. - wspkgs, err := snapshot.ActivePackages(ctx) - if err != nil && !source.IsNonFatalGoModError(err) { - event.Error(ctx, fmt.Sprintf("workspace packages: diagnosing %s", pm.URI), err) - } - if err == nil { - for _, pkg := range wspkgs { - pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) - if err != nil { - return nil, err - } - diagnostics = append(diagnostics, pkgDiagnostics[fh.URI()]...) - } - } - - tidied, err := snapshot.ModTidy(ctx, pm) - if err != nil && !source.IsNonFatalGoModError(err) { - event.Error(ctx, fmt.Sprintf("tidy: diagnosing %s", pm.URI), err) - } - if err == nil { - for _, d := range tidied.Diagnostics { - if d.URI != fh.URI() { - continue - } - diagnostics = append(diagnostics, d) - } - } - return diagnostics, nil + return upgradeDiagnostics, nil } diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 56f7c0f5524..62315079a06 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -740,6 +740,12 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Removes a dependency from the go.mod file of a module.", ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The module path to remove.\n\t\"ModulePath\": string,\n\t\"OnlyDiagnostic\": bool,\n}", }, + { + Command: "gopls.reset_go_mod_diagnostics", + Title: "Reset go.mod diagnostics", + Doc: "Reset diagnostics in the go.mod file of a module.", + ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", + }, { Command: "gopls.run_tests", Title: "Run test(s)", diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 8bf0344e7b9..47f463e13bb 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -21,10 +21,10 @@ import ( "golang.org/x/mod/module" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/imports" "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/gocommand" + "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/span" ) @@ -267,6 +267,9 @@ type View interface { // required by modfile. RegisterModuleUpgrades(modfile span.URI, upgrades map[string]string) + // ClearModuleUpgrades clears all upgrades for the modules in modfile. + ClearModuleUpgrades(modfile span.URI) + // FileKind returns the type of a file FileKind(FileHandle) FileKind } diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index a0ddfe62485..096d572f906 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -217,6 +217,13 @@ require golang.org/x/hello v1.2.3 env.NoDiagnosticAtRegexp("b/go.mod", `require`), ), ) + // Check for upgrades in b/go.mod and then clear them. + env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades) + env.Await(env.DiagnosticAtRegexpWithMessage("b/go.mod", `require`, "can be upgraded")) + env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics) + env.Await(EmptyDiagnostics("b/go.mod")) + + // Apply the diagnostics to a/go.mod. env.ApplyQuickFixes("a/go.mod", d.Diagnostics) env.Await(env.DoneWithChangeWatchedFiles()) if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA { From ec743893cd01c9423aaae4513b092c4c4c06f0f4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 9 Sep 2022 14:34:46 -0400 Subject: [PATCH 242/723] gopls/internal/lsp/source: make "chatty" diagnostics the default This CL changes the default behavior of gopls to always publish diagnostics for new file versions. In practice, this avoids stale diagnostics in multiple LSP clients (golang/go#54983 has more details). After this change, TestDownloadDeps was failing because it asserted on the non-existence of published diagnostics. Update the test to treat an empty diagnostic set the same as an unreceived diagnostic set. Fixes golang/go#54983 Change-Id: I41ed2f859b748e14585e7feb53702d3f38dcd599 Reviewed-on: https://go-review.googlesource.com/c/tools/+/429935 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- gopls/internal/lsp/source/options.go | 7 +------ gopls/internal/regtest/modfile/modfile_test.go | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 45aeca99dd1..b15d7144bf6 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -164,6 +164,7 @@ func DefaultOptions() *Options { CompleteUnimported: true, CompletionDocumentation: true, DeepCompletion: true, + ChattyDiagnostics: true, }, Hooks: Hooks{ ComputeEdits: myers.ComputeEdits, @@ -587,11 +588,6 @@ type InternalOptions struct { // ChattyDiagnostics controls whether to report file diagnostics for each // file change. If unset, gopls only reports diagnostics when they change, or // when a file is opened or closed. - // - // TODO(rfindley): is seems that for many clients this should be true by - // default. For example, coc.nvim seems to get confused if diagnostics are - // not re-published. Switch the default to true after some period of internal - // testing. ChattyDiagnostics bool } @@ -824,7 +820,6 @@ func (o *Options) EnableAllExperiments() { o.ExperimentalUseInvalidMetadata = true o.ExperimentalWatchedFileDelay = 50 * time.Millisecond o.NewDiff = "checked" - o.ChattyDiagnostics = true } func (o *Options) enableAllExperimentMaps() { diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 7da8830c910..eb3f9665696 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -1189,8 +1189,8 @@ func main() { ) env.ApplyQuickFixes("main.go", d.Diagnostics) env.Await( - EmptyDiagnostics("main.go"), - NoDiagnostics("go.mod"), + EmptyOrNoDiagnostics("main.go"), + EmptyOrNoDiagnostics("go.mod"), ) }) } From 9250e22aa32aa5029b848a580ba83deb48a3f2a8 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Tue, 16 Aug 2022 11:12:59 -0400 Subject: [PATCH 243/723] internal/lsp: latest version of LSP stubs This CL updates the LSP to 3.17.0. It is a DANGEROUS CL as the stubs are being generated by Go code reading vscode's language independent description of the protocol (in metaMode.json in the vscode-languageserver-node repository.) Some of the union types in the protocol have Go types with names containing 'Or'. These types have custom marshaling and unmarshaling code. Embedded structures in the protocol are broken out as their own types, with names constructed from the context in which they occur. The natural output has been modified to minimize the number of changes needed for gopls. (e.g., Workspace6Gn is preserved for compatibility.0 Thus, many types that are union types in the LSP description have been replaced by the types gopls already uses. Updates golang/go#52969 Change-Id: I16f6d877215155ac9e782b0f5bcbdab3f1aa2593 Reviewed-on: https://go-review.googlesource.com/c/tools/+/424214 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Peter Weinberger Reviewed-by: Robert Findley --- gopls/internal/lsp/cmd/cmd.go | 6 +- gopls/internal/lsp/fake/client.go | 4 + gopls/internal/lsp/general.go | 24 +- gopls/internal/lsp/protocol/enums.go | 15 - gopls/internal/lsp/protocol/tsclient.go | 255 +- gopls/internal/lsp/protocol/tsjson.go | 440 ++ gopls/internal/lsp/protocol/tsprotocol.go | 7101 ++++++++++----------- gopls/internal/lsp/protocol/tsserver.go | 1449 ++--- gopls/internal/lsp/server.go | 4 +- gopls/internal/lsp/server_gen.go | 22 +- gopls/test/json_test.go | 2 +- 11 files changed, 4520 insertions(+), 4802 deletions(-) create mode 100644 gopls/internal/lsp/protocol/tsjson.go diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index 254313a5af4..824382de96e 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -22,13 +22,13 @@ import ( "text/tabwriter" "time" - "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/gopls/internal/lsp" "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/debug" "golang.org/x/tools/gopls/internal/lsp/lsprpc" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" "golang.org/x/tools/internal/xcontext" @@ -422,6 +422,10 @@ func fileURI(uri protocol.DocumentURI) span.URI { return sURI } +func (c *cmdClient) CodeLensRefresh(context.Context) error { return nil } + +func (c *cmdClient) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } + func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil } func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { diff --git a/gopls/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go index 5885c29c9f9..f44bd73b215 100644 --- a/gopls/internal/lsp/fake/client.go +++ b/gopls/internal/lsp/fake/client.go @@ -30,6 +30,10 @@ type Client struct { hooks ClientHooks } +func (c *Client) CodeLensRefresh(context.Context) error { return nil } + +func (c *Client) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } + func (c *Client) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error { if c.hooks.OnShowMessage != nil { return c.hooks.OnShowMessage(ctx, params) diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index ee0739f2b97..1e7ad65613a 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -15,12 +15,12 @@ import ( "path/filepath" "sync" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/gopls/internal/lsp/debug" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/span" ) @@ -173,10 +173,7 @@ See https://github.com/golang/go/issues/45732 for more information.`, }, }, }, - ServerInfo: struct { - Name string `json:"name"` - Version string `json:"version,omitempty"` - }{ + ServerInfo: protocol.PServerInfoMsg_initialize{ Name: "gopls", Version: string(goplsVersion), }, @@ -404,13 +401,12 @@ func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, return nil } configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{ - ConfigurationParams: protocol.ConfigurationParams{ - Items: []protocol.ConfigurationItem{{ - ScopeURI: string(folder), - Section: "gopls", - }}, - }, - }) + Items: []protocol.ConfigurationItem{{ + ScopeURI: string(folder), + Section: "gopls", + }}, + }, + ) if err != nil { return fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err) } diff --git a/gopls/internal/lsp/protocol/enums.go b/gopls/internal/lsp/protocol/enums.go index 434808eeb18..82398e22189 100644 --- a/gopls/internal/lsp/protocol/enums.go +++ b/gopls/internal/lsp/protocol/enums.go @@ -10,7 +10,6 @@ import ( var ( namesTextDocumentSyncKind [int(Incremental) + 1]string - namesInitializeError [int(UnknownProtocolVersion) + 1]string namesMessageType [int(Log) + 1]string namesFileChangeType [int(Deleted) + 1]string namesWatchKind [int(WatchDelete) + 1]string @@ -29,8 +28,6 @@ func init() { namesTextDocumentSyncKind[int(Full)] = "Full" namesTextDocumentSyncKind[int(Incremental)] = "Incremental" - namesInitializeError[int(UnknownProtocolVersion)] = "UnknownProtocolVersion" - namesMessageType[int(Error)] = "Error" namesMessageType[int(Warning)] = "Warning" namesMessageType[int(Info)] = "Info" @@ -149,14 +146,6 @@ func ParseTextDocumentSyncKind(s string) TextDocumentSyncKind { return TextDocumentSyncKind(parseEnum(s, namesTextDocumentSyncKind[:])) } -func (e InitializeError) Format(f fmt.State, c rune) { - formatEnum(f, c, int(e), namesInitializeError[:], "InitializeError") -} - -func ParseInitializeError(s string) InitializeError { - return InitializeError(parseEnum(s, namesInitializeError[:])) -} - func (e MessageType) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesMessageType[:], "MessageType") } @@ -173,10 +162,6 @@ func ParseFileChangeType(s string) FileChangeType { return FileChangeType(parseEnum(s, namesFileChangeType[:])) } -func (e WatchKind) Format(f fmt.State, c rune) { - formatEnum(f, c, int(e), namesWatchKind[:], "WatchKind") -} - func ParseWatchKind(s string) WatchKind { return WatchKind(parseEnum(s, namesWatchKind[:])) } diff --git a/gopls/internal/lsp/protocol/tsclient.go b/gopls/internal/lsp/protocol/tsclient.go index 971a2df72b1..6dd81596bca 100644 --- a/gopls/internal/lsp/protocol/tsclient.go +++ b/gopls/internal/lsp/protocol/tsclient.go @@ -1,220 +1,219 @@ -// Copyright 2019 The Go Authors. All rights reserved. +// Copyright 2019-2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Code generated (see typescript/README.md) DO NOT EDIT. - package protocol -// Package protocol contains data types and code for LSP json rpcs -// generated automatically from vscode-languageserver-node -// commit: 696f9285bf849b73745682fdb1c1feac73eb8772 -// last fetched Fri Apr 01 2022 10:53:41 GMT-0400 (Eastern Daylight Time) +// Code generated from version 3.17.0 of protocol/metaModel.json. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Tue Sep 13 10:45:25 2022) +// Code generated; DO NOT EDIT. import ( "context" "encoding/json" - "fmt" "golang.org/x/tools/internal/jsonrpc2" ) type Client interface { - ShowMessage(context.Context, *ShowMessageParams) error - LogMessage(context.Context, *LogMessageParams) error - Event(context.Context, *interface{}) error - PublishDiagnostics(context.Context, *PublishDiagnosticsParams) error - Progress(context.Context, *ProgressParams) error - WorkspaceFolders(context.Context) ([]WorkspaceFolder /*WorkspaceFolder[] | null*/, error) - Configuration(context.Context, *ParamConfiguration) ([]LSPAny, error) - WorkDoneProgressCreate(context.Context, *WorkDoneProgressCreateParams) error - ShowDocument(context.Context, *ShowDocumentParams) (*ShowDocumentResult, error) - RegisterCapability(context.Context, *RegistrationParams) error - UnregisterCapability(context.Context, *UnregistrationParams) error - ShowMessageRequest(context.Context, *ShowMessageRequestParams) (*MessageActionItem /*MessageActionItem | null*/, error) - ApplyEdit(context.Context, *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) + LogTrace(context.Context, *LogTraceParams) error // $/logTrace + Progress(context.Context, *ProgressParams) error // $/progress + RegisterCapability(context.Context, *RegistrationParams) error // client/registerCapability + UnregisterCapability(context.Context, *UnregistrationParams) error // client/unregisterCapability + Event(context.Context, *interface{}) error // telemetry/event + PublishDiagnostics(context.Context, *PublishDiagnosticsParams) error // textDocument/publishDiagnostics + LogMessage(context.Context, *LogMessageParams) error // window/logMessage + ShowDocument(context.Context, *ShowDocumentParams) (*ShowDocumentResult, error) // window/showDocument + ShowMessage(context.Context, *ShowMessageParams) error // window/showMessage + ShowMessageRequest(context.Context, *ShowMessageRequestParams) (*MessageActionItem, error) // window/showMessageRequest + WorkDoneProgressCreate(context.Context, *WorkDoneProgressCreateParams) error // window/workDoneProgress/create + ApplyEdit(context.Context, *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) // workspace/applyEdit + CodeLensRefresh(context.Context) error // workspace/codeLens/refresh + Configuration(context.Context, *ParamConfiguration) ([]LSPAny, error) // workspace/configuration + WorkspaceFolders(context.Context) ([]WorkspaceFolder, error) // workspace/workspaceFolders } func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { switch r.Method() { - case "window/showMessage": // notif - var params ShowMessageParams + case "$/logTrace": + var params LogTraceParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.ShowMessage(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "window/logMessage": // notif - var params LogMessageParams + err := client.LogTrace(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "$/progress": + var params ProgressParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.LogMessage(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "telemetry/event": // notif + err := client.Progress(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "client/registerCapability": + var params RegistrationParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + err := client.RegisterCapability(ctx, ¶ms) + return true, reply(ctx, nil, err) // 155 + case "client/unregisterCapability": + var params UnregistrationParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + err := client.UnregisterCapability(ctx, ¶ms) + return true, reply(ctx, nil, err) // 155 + case "telemetry/event": var params interface{} if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.Event(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/publishDiagnostics": // notif + return true, reply(ctx, nil, err) // 231 + case "textDocument/publishDiagnostics": var params PublishDiagnosticsParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.PublishDiagnostics(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "$/progress": // notif - var params ProgressParams + return true, reply(ctx, nil, err) // 231 + case "window/logMessage": + var params LogMessageParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.Progress(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/workspaceFolders": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - resp, err := client.WorkspaceFolders(ctx) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "workspace/configuration": // req - var params ParamConfiguration + err := client.LogMessage(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "window/showDocument": + var params ShowDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := client.Configuration(ctx, ¶ms) + resp, err := client.ShowDocument(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "window/workDoneProgress/create": // req - var params WorkDoneProgressCreateParams + return true, reply(ctx, resp, nil) // 146 + case "window/showMessage": + var params ShowMessageParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.WorkDoneProgressCreate(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "window/showDocument": // req - var params ShowDocumentParams + err := client.ShowMessage(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "window/showMessageRequest": + var params ShowMessageRequestParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := client.ShowDocument(ctx, ¶ms) + resp, err := client.ShowMessageRequest(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "client/registerCapability": // req - var params RegistrationParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - } - err := client.RegisterCapability(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "client/unregisterCapability": // req - var params UnregistrationParams + return true, reply(ctx, resp, nil) // 146 + case "window/workDoneProgress/create": + var params WorkDoneProgressCreateParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.UnregisterCapability(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "window/showMessageRequest": // req - var params ShowMessageRequestParams + err := client.WorkDoneProgressCreate(ctx, ¶ms) + return true, reply(ctx, nil, err) // 155 + case "workspace/applyEdit": + var params ApplyWorkspaceEditParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := client.ShowMessageRequest(ctx, ¶ms) + resp, err := client.ApplyEdit(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/applyEdit": // req - var params ApplyWorkspaceEditParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/codeLens/refresh": + err := client.CodeLensRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/configuration": + var params ParamConfiguration if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := client.ApplyEdit(ctx, ¶ms) + resp, err := client.Configuration(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - + return true, reply(ctx, resp, nil) // 146 + case "workspace/workspaceFolders": + resp, err := client.WorkspaceFolders(ctx) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 165 default: return false, nil } } -func (s *clientDispatcher) ShowMessage(ctx context.Context, params *ShowMessageParams) error { - return s.sender.Notify(ctx, "window/showMessage", params) -} - -func (s *clientDispatcher) LogMessage(ctx context.Context, params *LogMessageParams) error { - return s.sender.Notify(ctx, "window/logMessage", params) -} - +func (s *clientDispatcher) LogTrace(ctx context.Context, params *LogTraceParams) error { + return s.sender.Notify(ctx, "$/logTrace", params) +} // 244 +func (s *clientDispatcher) Progress(ctx context.Context, params *ProgressParams) error { + return s.sender.Notify(ctx, "$/progress", params) +} // 244 +func (s *clientDispatcher) RegisterCapability(ctx context.Context, params *RegistrationParams) error { + return s.sender.Call(ctx, "client/registerCapability", params, nil) +} // 194 +func (s *clientDispatcher) UnregisterCapability(ctx context.Context, params *UnregistrationParams) error { + return s.sender.Call(ctx, "client/unregisterCapability", params, nil) +} // 194 func (s *clientDispatcher) Event(ctx context.Context, params *interface{}) error { return s.sender.Notify(ctx, "telemetry/event", params) -} - +} // 244 func (s *clientDispatcher) PublishDiagnostics(ctx context.Context, params *PublishDiagnosticsParams) error { return s.sender.Notify(ctx, "textDocument/publishDiagnostics", params) -} - -func (s *clientDispatcher) Progress(ctx context.Context, params *ProgressParams) error { - return s.sender.Notify(ctx, "$/progress", params) -} -func (s *clientDispatcher) WorkspaceFolders(ctx context.Context) ([]WorkspaceFolder /*WorkspaceFolder[] | null*/, error) { - var result []WorkspaceFolder /*WorkspaceFolder[] | null*/ - if err := s.sender.Call(ctx, "workspace/workspaceFolders", nil, &result); err != nil { - return nil, err - } - return result, nil -} - -func (s *clientDispatcher) Configuration(ctx context.Context, params *ParamConfiguration) ([]LSPAny, error) { - var result []LSPAny - if err := s.sender.Call(ctx, "workspace/configuration", params, &result); err != nil { - return nil, err - } - return result, nil -} - -func (s *clientDispatcher) WorkDoneProgressCreate(ctx context.Context, params *WorkDoneProgressCreateParams) error { - return s.sender.Call(ctx, "window/workDoneProgress/create", params, nil) // Call, not Notify -} - +} // 244 +func (s *clientDispatcher) LogMessage(ctx context.Context, params *LogMessageParams) error { + return s.sender.Notify(ctx, "window/logMessage", params) +} // 244 func (s *clientDispatcher) ShowDocument(ctx context.Context, params *ShowDocumentParams) (*ShowDocumentResult, error) { var result *ShowDocumentResult if err := s.sender.Call(ctx, "window/showDocument", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *clientDispatcher) RegisterCapability(ctx context.Context, params *RegistrationParams) error { - return s.sender.Call(ctx, "client/registerCapability", params, nil) // Call, not Notify -} - -func (s *clientDispatcher) UnregisterCapability(ctx context.Context, params *UnregistrationParams) error { - return s.sender.Call(ctx, "client/unregisterCapability", params, nil) // Call, not Notify -} - -func (s *clientDispatcher) ShowMessageRequest(ctx context.Context, params *ShowMessageRequestParams) (*MessageActionItem /*MessageActionItem | null*/, error) { - var result *MessageActionItem /*MessageActionItem | null*/ +} // 169 +func (s *clientDispatcher) ShowMessage(ctx context.Context, params *ShowMessageParams) error { + return s.sender.Notify(ctx, "window/showMessage", params) +} // 244 +func (s *clientDispatcher) ShowMessageRequest(ctx context.Context, params *ShowMessageRequestParams) (*MessageActionItem, error) { + var result *MessageActionItem if err := s.sender.Call(ctx, "window/showMessageRequest", params, &result); err != nil { return nil, err } return result, nil -} - +} // 169 +func (s *clientDispatcher) WorkDoneProgressCreate(ctx context.Context, params *WorkDoneProgressCreateParams) error { + return s.sender.Call(ctx, "window/workDoneProgress/create", params, nil) +} // 194 func (s *clientDispatcher) ApplyEdit(ctx context.Context, params *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) { var result *ApplyWorkspaceEditResult if err := s.sender.Call(ctx, "workspace/applyEdit", params, &result); err != nil { return nil, err } return result, nil -} +} // 169 +func (s *clientDispatcher) CodeLensRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/codeLens/refresh", nil, nil) +} // 209 +func (s *clientDispatcher) Configuration(ctx context.Context, params *ParamConfiguration) ([]LSPAny, error) { + var result []LSPAny + if err := s.sender.Call(ctx, "workspace/configuration", params, &result); err != nil { + return nil, err + } + return result, nil +} // 169 +func (s *clientDispatcher) WorkspaceFolders(ctx context.Context) ([]WorkspaceFolder, error) { + var result []WorkspaceFolder + if err := s.sender.Call(ctx, "workspace/workspaceFolders", nil, &result); err != nil { + return nil, err + } + return result, nil +} // 204 diff --git a/gopls/internal/lsp/protocol/tsjson.go b/gopls/internal/lsp/protocol/tsjson.go new file mode 100644 index 00000000000..a418df45e1d --- /dev/null +++ b/gopls/internal/lsp/protocol/tsjson.go @@ -0,0 +1,440 @@ +// Copyright 2019-2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protocol + +// Code generated from version 3.17.0 of protocol/metaModel.json. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Tue Sep 13 10:45:25 2022) +// Code generated; DO NOT EDIT. + +import "encoding/json" +import "errors" +import "fmt" + +func (t OrFEditRangePItemDefaults) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case FEditRangePItemDefaults: + return json.Marshal(x) + case Range: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [FEditRangePItemDefaults Range]", t) +} + +func (t *OrFEditRangePItemDefaults) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 FEditRangePItemDefaults + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 Range + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [FEditRangePItemDefaults Range]") +} + +func (t OrFNotebookPNotebookSelector) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case NotebookDocumentFilter: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) +} + +func (t *OrFNotebookPNotebookSelector) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 NotebookDocumentFilter + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [NotebookDocumentFilter string]") +} + +func (t OrPLocation_workspace_symbol) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case Location: + return json.Marshal(x) + case PLocationMsg_workspace_symbol: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [Location PLocationMsg_workspace_symbol]", t) +} + +func (t *OrPLocation_workspace_symbol) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 Location + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 PLocationMsg_workspace_symbol + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [Location PLocationMsg_workspace_symbol]") +} + +func (t OrPSection_workspace_didChangeConfiguration) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case []string: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [[]string string]", t) +} + +func (t *OrPSection_workspace_didChangeConfiguration) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 []string + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [[]string string]") +} + +func (t OrPTooltipPLabel) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case MarkupContent: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +} + +func (t *OrPTooltipPLabel) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 MarkupContent + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [MarkupContent string]") +} + +func (t OrPTooltip_textDocument_inlayHint) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case MarkupContent: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +} + +func (t *OrPTooltip_textDocument_inlayHint) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 MarkupContent + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [MarkupContent string]") +} + +func (t Or_Definition) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case Location: + return json.Marshal(x) + case []Location: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [Location []Location]", t) +} + +func (t *Or_Definition) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 Location + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 []Location + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [Location []Location]") +} + +func (t Or_DocumentDiagnosticReport) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case RelatedFullDocumentDiagnosticReport: + return json.Marshal(x) + case RelatedUnchangedDocumentDiagnosticReport: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]", t) +} + +func (t *Or_DocumentDiagnosticReport) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 RelatedFullDocumentDiagnosticReport + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 RelatedUnchangedDocumentDiagnosticReport + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]") +} + +func (t Or_DocumentFilter) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case NotebookCellTextDocumentFilter: + return json.Marshal(x) + case TextDocumentFilter: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [NotebookCellTextDocumentFilter TextDocumentFilter]", t) +} + +func (t *Or_DocumentFilter) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 NotebookCellTextDocumentFilter + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 TextDocumentFilter + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [NotebookCellTextDocumentFilter TextDocumentFilter]") +} + +func (t Or_InlineValue) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case InlineValueEvaluatableExpression: + return json.Marshal(x) + case InlineValueText: + return json.Marshal(x) + case InlineValueVariableLookup: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]", t) +} + +func (t *Or_InlineValue) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 InlineValueEvaluatableExpression + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 InlineValueText + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 InlineValueVariableLookup + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]") +} + +func (t Or_MarkedString) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case Msg_MarkedString: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [Msg_MarkedString string]", t) +} + +func (t *Or_MarkedString) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 Msg_MarkedString + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [Msg_MarkedString string]") +} + +func (t Or_RelativePattern_baseUri) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case URI: + return json.Marshal(x) + case WorkspaceFolder: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [URI WorkspaceFolder]", t) +} + +func (t *Or_RelativePattern_baseUri) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 URI + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 WorkspaceFolder + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [URI WorkspaceFolder]") +} + +func (t Or_WorkspaceDocumentDiagnosticReport) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case WorkspaceFullDocumentDiagnosticReport: + return json.Marshal(x) + case WorkspaceUnchangedDocumentDiagnosticReport: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]", t) +} + +func (t *Or_WorkspaceDocumentDiagnosticReport) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 WorkspaceFullDocumentDiagnosticReport + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 WorkspaceUnchangedDocumentDiagnosticReport + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]") +} + +func (t Or_textDocument_declaration) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case Declaration: + return json.Marshal(x) + case []DeclarationLink: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [Declaration []DeclarationLink]", t) +} + +func (t *Or_textDocument_declaration) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 Declaration + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 []DeclarationLink + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [Declaration []DeclarationLink]") +} diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go index 72c0372e8a3..09dfb48663e 100644 --- a/gopls/internal/lsp/protocol/tsprotocol.go +++ b/gopls/internal/lsp/protocol/tsprotocol.go @@ -1,63 +1,53 @@ -// Copyright 2019 The Go Authors. All rights reserved. +// Copyright 2019-2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Code generated (see typescript/README.md) DO NOT EDIT. - -// Package protocol contains data types and code for LSP json rpcs -// generated automatically from vscode-languageserver-node -// commit: 696f9285bf849b73745682fdb1c1feac73eb8772 -// last fetched Fri Apr 01 2022 10:53:41 GMT-0400 (Eastern Daylight Time) package protocol +// Code generated from version 3.17.0 of protocol/metaModel.json. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Tue Sep 13 10:45:25 2022) +// Code generated; DO NOT EDIT. + import "encoding/json" -/** +/* * A special text edit with an additional change annotation. * * @since 3.16.0. */ -type AnnotatedTextEdit struct { - /** - * The actual identifier of the change annotation - */ +type AnnotatedTextEdit struct { // line 9392 + // The actual identifier of the change annotation AnnotationID ChangeAnnotationIdentifier `json:"annotationId"` TextEdit } -/** - * The parameters passed via a apply workspace edit request. - */ -type ApplyWorkspaceEditParams struct { - /** +// The parameters passed via a apply workspace edit request. +type ApplyWorkspaceEditParams struct { // line 6003 + /* * An optional label of the workspace edit. This label is * presented in the user interface for example on an undo * stack to undo the workspace edit. */ Label string `json:"label,omitempty"` - /** - * The edits to apply. - */ + // The edits to apply. Edit WorkspaceEdit `json:"edit"` } -/** +/* * The result returned from the apply workspace edit request. * * @since 3.17 renamed from ApplyWorkspaceEditResponse */ -type ApplyWorkspaceEditResult struct { - /** - * Indicates whether the edit was applied or not. - */ +type ApplyWorkspaceEditResult struct { // line 6026 + // Indicates whether the edit was applied or not. Applied bool `json:"applied"` - /** + /* * An optional textual description for why the edit was not applied. * This may be used by the server for diagnostic logging or to provide * a suitable error for a request that triggered the edit. */ FailureReason string `json:"failureReason,omitempty"` - /** + /* * Depending on the client's failure handling strategy `failedChange` might * contain the index of the change that failed. This property is only available * if the client signals a `failureHandlingStrategy` in its client capabilities. @@ -65,11 +55,30 @@ type ApplyWorkspaceEditResult struct { FailedChange uint32 `json:"failedChange,omitempty"` } -/** - * @since 3.16.0 - */ -type CallHierarchyClientCapabilities struct { - /** +// A base for all symbol information. +type BaseSymbolInformation struct { // line 8986 + // The name of this symbol. + Name string `json:"name"` + // The kind of this symbol. + Kind SymbolKind `json:"kind"` + /* + * Tags for this symbol. + * + * @since 3.16.0 + */ + Tags []SymbolTag `json:"tags,omitempty"` + /* + * The name of the symbol containing this symbol. This information is for + * user interface purposes (e.g. to render a qualifier in the user interface + * if necessary). It can't be used to re-infer a hierarchy for the document + * symbols. + */ + ContainerName string `json:"containerName,omitempty"` +} + +// @since 3.16.0 +type CallHierarchyClientCapabilities struct { // line 12167 + /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` * return value for the corresponding server capability as well. @@ -77,97 +86,81 @@ type CallHierarchyClientCapabilities struct { DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** +/* * Represents an incoming call, e.g. a caller of a method or constructor. * * @since 3.16.0 */ -type CallHierarchyIncomingCall struct { - /** - * The item that makes the call. - */ +type CallHierarchyIncomingCall struct { // line 2801 + // The item that makes the call. From CallHierarchyItem `json:"from"` - /** + /* * The ranges at which the calls appear. This is relative to the caller * denoted by [`this.from`](#CallHierarchyIncomingCall.from). */ FromRanges []Range `json:"fromRanges"` } -/** +/* * The parameter of a `callHierarchy/incomingCalls` request. * * @since 3.16.0 */ -type CallHierarchyIncomingCallsParams struct { +type CallHierarchyIncomingCallsParams struct { // line 2777 Item CallHierarchyItem `json:"item"` WorkDoneProgressParams PartialResultParams } -/** +/* * Represents programming constructs like functions or constructors in the context * of call hierarchy. * * @since 3.16.0 */ -type CallHierarchyItem struct { - /** - * The name of this item. - */ +type CallHierarchyItem struct { // line 2678 + // The name of this item. Name string `json:"name"` - /** - * The kind of this item. - */ + // The kind of this item. Kind SymbolKind `json:"kind"` - /** - * Tags for this item. - */ + // Tags for this item. Tags []SymbolTag `json:"tags,omitempty"` - /** - * More detail for this item, e.g. the signature of a function. - */ + // More detail for this item, e.g. the signature of a function. Detail string `json:"detail,omitempty"` - /** - * The resource identifier of this item. - */ + // The resource identifier of this item. URI DocumentURI `json:"uri"` - /** - * The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. - */ + // The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. Range Range `json:"range"` - /** + /* * The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. * Must be contained by the [`range`](#CallHierarchyItem.range). */ SelectionRange Range `json:"selectionRange"` - /** + /* * A data entry field that is preserved between a call hierarchy prepare and * incoming calls or outgoing calls requests. */ - Data LSPAny `json:"data,omitempty"` + Data interface{} `json:"data,omitempty"` } -/** +/* * Call hierarchy options used during static registration. * * @since 3.16.0 */ -type CallHierarchyOptions struct { +type CallHierarchyOptions struct { // line 6539 WorkDoneProgressOptions } -/** +/* * Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. * * @since 3.16.0 */ -type CallHierarchyOutgoingCall struct { - /** - * The item that is called. - */ +type CallHierarchyOutgoingCall struct { // line 2851 + // The item that is called. To CallHierarchyItem `json:"to"` - /** + /* * The range at which this item is called. This is the range relative to the caller, e.g the item * passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls) * and not [`this.to`](#CallHierarchyOutgoingCall.to). @@ -175,140 +168,109 @@ type CallHierarchyOutgoingCall struct { FromRanges []Range `json:"fromRanges"` } -/** +/* * The parameter of a `callHierarchy/outgoingCalls` request. * * @since 3.16.0 */ -type CallHierarchyOutgoingCallsParams struct { +type CallHierarchyOutgoingCallsParams struct { // line 2827 Item CallHierarchyItem `json:"item"` WorkDoneProgressParams PartialResultParams } -/** +/* * The parameter of a `textDocument/prepareCallHierarchy` request. * * @since 3.16.0 */ -type CallHierarchyPrepareParams struct { +type CallHierarchyPrepareParams struct { // line 2660 TextDocumentPositionParams WorkDoneProgressParams } -/** +/* * Call hierarchy options used during static or dynamic registration. * * @since 3.16.0 */ -type CallHierarchyRegistrationOptions struct { +type CallHierarchyRegistrationOptions struct { // line 2755 TextDocumentRegistrationOptions CallHierarchyOptions StaticRegistrationOptions } - -type CancelParams struct { - /** - * The request id to cancel. - */ - ID interface{} /*number | string*/ `json:"id"` +type CancelParams struct { // line 6198 + // The request id to cancel. + ID interface{} `json:"id"` } -/** +/* * Additional information that describes document changes. * * @since 3.16.0 */ -type ChangeAnnotation struct { - /** - * A human-readable string describing the actual change. The string - * is rendered prominent in the user interface. - */ +type ChangeAnnotation struct { // line 6836 + /* + * A human-readable string describing the actual change. The string + * is rendered prominent in the user interface. + */ Label string `json:"label"` - /** - * A flag which indicates that user confirmation is needed - * before applying the change. - */ + /* + * A flag which indicates that user confirmation is needed + * before applying the change. + */ NeedsConfirmation bool `json:"needsConfirmation,omitempty"` - /** + /* * A human-readable string which is rendered less prominent in * the user interface. */ Description string `json:"description,omitempty"` } -/** - * An identifier to refer to a change annotation stored with a workspace edit. - */ -type ChangeAnnotationIdentifier = string - -type ClientCapabilities struct { - /** - * The workspace client capabilities - */ - Workspace Workspace3Gn `json:"workspace,omitempty"` - /** - * Text document specific client capabilities. - */ +// An identifier to refer to a change annotation stored with a workspace edit. +type ChangeAnnotationIdentifier = string // (alias) line 14002 +// Defines the capabilities provided by the client. +type ClientCapabilities struct { // line 9700 + // Workspace specific client capabilities. + Workspace WorkspaceClientCapabilities `json:"workspace,omitempty"` + // Text document specific client capabilities. TextDocument TextDocumentClientCapabilities `json:"textDocument,omitempty"` - /** - * Window specific client capabilities. - */ - Window struct { - /** - * Whether client supports server initiated progress using the - * `window/workDoneProgress/create` request. - * - * Since 3.15.0 - */ - WorkDoneProgress bool `json:"workDoneProgress,omitempty"` - /** - * Capabilities specific to the showMessage request. - * - * @since 3.16.0 - */ - ShowMessage ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` - /** - * Capabilities specific to the showDocument request. - * - * @since 3.16.0 - */ - ShowDocument ShowDocumentClientCapabilities `json:"showDocument,omitempty"` - } `json:"window,omitempty"` - /** + /* + * Capabilities specific to the notebook document support. + * + * @since 3.17.0 + */ + NotebookDocument NotebookDocumentClientCapabilities `json:"notebookDocument,omitempty"` + // Window specific client capabilities. + Window WindowClientCapabilities `json:"window,omitempty"` + /* * General client capabilities. * * @since 3.16.0 */ General GeneralClientCapabilities `json:"general,omitempty"` - /** - * Experimental client capabilities. - */ + // Experimental client capabilities. Experimental interface{} `json:"experimental,omitempty"` } -/** +/* * A code action represents a change that can be performed in code, e.g. to fix a problem or * to refactor code. * * A CodeAction must set either `edit` and/or a `command`. If both are supplied, the `edit` is applied first, then the `command` is executed. */ -type CodeAction struct { - /** - * A short, human-readable, title for this code action. - */ +type CodeAction struct { // line 5401 + // A short, human-readable, title for this code action. Title string `json:"title"` - /** + /* * The kind of the code action. * * Used to filter code actions. */ Kind CodeActionKind `json:"kind,omitempty"` - /** - * The diagnostics that this code action resolves. - */ + // The diagnostics that this code action resolves. Diagnostics []Diagnostic `json:"diagnostics,omitempty"` - /** + /* * Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted * by keybindings. * @@ -318,93 +280,66 @@ type CodeAction struct { * @since 3.15.0 */ IsPreferred bool `json:"isPreferred,omitempty"` - /** + /* * Marks that the code action cannot currently be applied. * * Clients should follow the following guidelines regarding disabled code actions: * - * - Disabled code actions are not shown in automatic [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) - * code action menu. + * - Disabled code actions are not shown in automatic [lightbulbs](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) + * code action menus. * - * - Disabled actions are shown as faded out in the code action menu when the user request a more specific type + * - Disabled actions are shown as faded out in the code action menu when the user requests a more specific type * of code action, such as refactorings. * * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) - * that auto applies a code action and only a disabled code actions are returned, the client should show the user an + * that auto applies a code action and only disabled code actions are returned, the client should show the user an * error message with `reason` in the editor. * * @since 3.16.0 */ - Disabled *struct { - /** - * Human readable description of why the code action is currently disabled. - * - * This is displayed in the code actions UI. - */ - Reason string `json:"reason"` - } `json:"disabled,omitempty"` - /** - * The workspace edit this code action performs. - */ + Disabled PDisabledMsg_textDocument_codeAction `json:"disabled,omitempty"` + // The workspace edit this code action performs. Edit WorkspaceEdit `json:"edit,omitempty"` - /** + /* * A command this code action executes. If a code action - * provides a edit and a command, first the edit is + * provides an edit and a command, first the edit is * executed and then the command. */ Command *Command `json:"command,omitempty"` - /** + /* * A data entry field that is preserved on a code action between * a `textDocument/codeAction` and a `codeAction/resolve` request. * * @since 3.16.0 */ - Data LSPAny `json:"data,omitempty"` + Data interface{} `json:"data,omitempty"` } -/** - * The Client Capabilities of a [CodeActionRequest](#CodeActionRequest). - */ -type CodeActionClientCapabilities struct { - /** - * Whether code action supports dynamic registration. - */ +// The Client Capabilities of a [CodeActionRequest](#CodeActionRequest). +type CodeActionClientCapabilities struct { // line 11747 + // Whether code action supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** + /* * The client support code action literals of type `CodeAction` as a valid * response of the `textDocument/codeAction` request. If the property is not * set the request can only return `Command` literals. * * @since 3.8.0 */ - CodeActionLiteralSupport struct { - /** - * The code action kind is support with the following value - * set. - */ - CodeActionKind struct { - /** - * The code action kind values the client supports. When this - * property exists the client also guarantees that it will - * handle values outside its set gracefully and falls back - * to a default value when unknown. - */ - ValueSet []CodeActionKind `json:"valueSet"` - } `json:"codeActionKind"` - } `json:"codeActionLiteralSupport,omitempty"` - /** + CodeActionLiteralSupport PCodeActionLiteralSupportPCodeAction `json:"codeActionLiteralSupport,omitempty"` + /* * Whether code action supports the `isPreferred` property. * * @since 3.15.0 */ IsPreferredSupport bool `json:"isPreferredSupport,omitempty"` - /** + /* * Whether code action supports the `disabled` property. * * @since 3.16.0 */ DisabledSupport bool `json:"disabledSupport,omitempty"` - /** + /* * Whether code action supports the `data` property which is * preserved between a `textDocument/codeAction` and a * `codeAction/resolve` request. @@ -412,20 +347,15 @@ type CodeActionClientCapabilities struct { * @since 3.16.0 */ DataSupport bool `json:"dataSupport,omitempty"` - /** - * Whether the client support resolving additional code action + /* + * Whether the client supports resolving additional code action * properties via a separate `codeAction/resolve` request. * * @since 3.16.0 */ - ResolveSupport struct { - /** - * The properties that a client can resolve lazily. - */ - Properties []string `json:"properties"` - } `json:"resolveSupport,omitempty"` - /** - * Whether th client honors the change annotations in + ResolveSupport PResolveSupportPCodeAction `json:"resolveSupport,omitempty"` + /* + * Whether the client honors the change annotations in * text edits and resource operations returned via the * `CodeAction#edit` property by for example presenting * the workspace edit in the user interface and asking @@ -436,12 +366,12 @@ type CodeActionClientCapabilities struct { HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` } -/** +/* * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. */ -type CodeActionContext struct { - /** +type CodeActionContext struct { // line 9052 + /* * An array of diagnostics known on the client side overlapping the range provided to the * `textDocument/codeAction` request. They are provided so that the server knows which * errors are currently presented to the user for the given range. There is no guarantee @@ -449,38 +379,31 @@ type CodeActionContext struct { * to compute code actions is the provided range. */ Diagnostics []Diagnostic `json:"diagnostics"` - /** + /* * Requested kind of actions to return. * * Actions not of this kind are filtered out by the client before being shown. So servers * can omit computing them. */ Only []CodeActionKind `json:"only,omitempty"` - /** + /* * The reason why code actions were requested. * * @since 3.17.0 */ TriggerKind CodeActionTriggerKind `json:"triggerKind,omitempty"` } - -/** - * A set of predefined code action kinds - */ -type CodeActionKind string - -/** - * Provider options for a [CodeActionRequest](#CodeActionRequest). - */ -type CodeActionOptions struct { - /** +type CodeActionKind string // line 13352 +// Provider options for a [CodeActionRequest](#CodeActionRequest). +type CodeActionOptions struct { // line 9091 + /* * CodeActionKinds that this server may return. * * The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server * may list out every specific kind they provide. */ CodeActionKinds []CodeActionKind `json:"codeActionKinds,omitempty"` - /** + /* * The server provides support to resolve additional * information for a code action. * @@ -490,107 +413,84 @@ type CodeActionOptions struct { WorkDoneProgressOptions } -/** - * The parameters of a [CodeActionRequest](#CodeActionRequest). - */ -type CodeActionParams struct { - /** - * The document in which the command was invoked. - */ +// The parameters of a [CodeActionRequest](#CodeActionRequest). +type CodeActionParams struct { // line 5327 + // The document in which the command was invoked. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The range for which the command was invoked. - */ + // The range for which the command was invoked. Range Range `json:"range"` - /** - * Context carrying additional information. - */ + // Context carrying additional information. Context CodeActionContext `json:"context"` WorkDoneProgressParams PartialResultParams } -/** - * The reason why code actions were requested. - * - * @since 3.17.0 - proposed state - */ -type CodeActionTriggerKind float64 - -/** +// Registration options for a [CodeActionRequest](#CodeActionRequest). +type CodeActionRegistrationOptions struct { // line 5495 + TextDocumentRegistrationOptions + CodeActionOptions +} +type CodeActionTriggerKind uint32 // line 13632 +/* * Structure to capture a description for an error code. * * @since 3.16.0 */ -type CodeDescription struct { - /** - * An URI to open with more information about the diagnostic error. - */ +type CodeDescription struct { // line 10052 + // An URI to open with more information about the diagnostic error. Href URI `json:"href"` } -/** +/* * A code lens represents a [command](#Command) that should be shown along with * source text, like the number of references, a way to run tests, etc. * * A code lens is _unresolved_ when no command is associated to it. For performance - * reasons the creation of a code lens and resolving should be done to two stages. + * reasons the creation of a code lens and resolving should be done in two stages. */ -type CodeLens struct { - /** - * The range in which this code lens is valid. Should only span a single line. - */ +type CodeLens struct { // line 5618 + // The range in which this code lens is valid. Should only span a single line. Range Range `json:"range"` - /** - * The command this code lens represents. - */ + // The command this code lens represents. Command Command `json:"command,omitempty"` - /** + /* * A data entry field that is preserved on a code lens item between * a [CodeLensRequest](#CodeLensRequest) and a [CodeLensResolveRequest] * (#CodeLensResolveRequest) */ - Data LSPAny `json:"data,omitempty"` + Data interface{} `json:"data,omitempty"` } -/** - * The client capabilities of a [CodeLensRequest](#CodeLensRequest). - */ -type CodeLensClientCapabilities struct { - /** - * Whether code lens supports dynamic registration. - */ +// The client capabilities of a [CodeLensRequest](#CodeLensRequest). +type CodeLensClientCapabilities struct { // line 11861 + // Whether code lens supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** - * Code Lens provider options of a [CodeLensRequest](#CodeLensRequest). - */ -type CodeLensOptions struct { - /** - * Code lens has a resolve provider as well. - */ +// Code Lens provider options of a [CodeLensRequest](#CodeLensRequest). +type CodeLensOptions struct { // line 9147 + // Code lens has a resolve provider as well. ResolveProvider bool `json:"resolveProvider,omitempty"` WorkDoneProgressOptions } -/** - * The parameters of a [CodeLensRequest](#CodeLensRequest). - */ -type CodeLensParams struct { - /** - * The document to request code lens for. - */ +// The parameters of a [CodeLensRequest](#CodeLensRequest). +type CodeLensParams struct { // line 5594 + // The document to request code lens for. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } -/** - * @since 3.16.0 - */ -type CodeLensWorkspaceClientCapabilities struct { - /** +// Registration options for a [CodeLensRequest](#CodeLensRequest). +type CodeLensRegistrationOptions struct { // line 5650 + TextDocumentRegistrationOptions + CodeLensOptions +} + +// @since 3.16.0 +type CodeLensWorkspaceClientCapabilities struct { // line 11019 + /* * Whether the client implementation supports a refresh request sent from the * server to the client. * @@ -602,263 +502,124 @@ type CodeLensWorkspaceClientCapabilities struct { RefreshSupport bool `json:"refreshSupport,omitempty"` } -/** - * Represents a color in RGBA space. - */ -type Color struct { - /** - * The red component of this color in the range [0-1]. - */ - Red Decimal `json:"red"` - /** - * The green component of this color in the range [0-1]. - */ - Green Decimal `json:"green"` - /** - * The blue component of this color in the range [0-1]. - */ - Blue Decimal `json:"blue"` - /** - * The alpha component of this color in the range [0-1]. - */ - Alpha Decimal `json:"alpha"` +// Represents a color in RGBA space. +type Color struct { // line 6438 + // The red component of this color in the range [0-1]. + Red float64 `json:"red"` + // The green component of this color in the range [0-1]. + Green float64 `json:"green"` + // The blue component of this color in the range [0-1]. + Blue float64 `json:"blue"` + // The alpha component of this color in the range [0-1]. + Alpha float64 `json:"alpha"` } -/** - * Represents a color range from a document. - */ -type ColorInformation struct { - /** - * The range in the document where this color appears. - */ +// Represents a color range from a document. +type ColorInformation struct { // line 2261 + // The range in the document where this color appears. Range Range `json:"range"` - /** - * The actual color value for this color range. - */ + // The actual color value for this color range. Color Color `json:"color"` } - -type ColorPresentation struct { - /** +type ColorPresentation struct { // line 2343 + /* * The label of this color presentation. It will be shown on the color * picker header. By default this is also the text that is inserted when selecting * this color presentation. */ Label string `json:"label"` - /** + /* * An [edit](#TextEdit) which is applied to a document when selecting * this presentation for the color. When `falsy` the [label](#ColorPresentation.label) * is used. */ TextEdit TextEdit `json:"textEdit,omitempty"` - /** + /* * An optional array of additional [text edits](#TextEdit) that are applied when * selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves. */ AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` } -/** - * Parameters for a [ColorPresentationRequest](#ColorPresentationRequest). - */ -type ColorPresentationParams struct { - /** - * The text document. - */ +// Parameters for a [ColorPresentationRequest](#ColorPresentationRequest). +type ColorPresentationParams struct { // line 2303 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The color to request presentations for. - */ + // The color to request presentations for. Color Color `json:"color"` - /** - * The range where the color would be inserted. Serves as a context. - */ + // The range where the color would be inserted. Serves as a context. Range Range `json:"range"` WorkDoneProgressParams PartialResultParams } -/** +/* * Represents a reference to a command. Provides a title which * will be used to represent a command in the UI and, optionally, * an array of arguments which will be passed to the command handler * function when invoked. */ -type Command struct { - /** - * Title of the command, like `save`. - */ +type Command struct { // line 5367 + // Title of the command, like `save`. Title string `json:"title"` - /** - * The identifier of the actual command handler. - */ + // The identifier of the actual command handler. Command string `json:"command"` - /** + /* * Arguments that the command handler should be * invoked with. */ Arguments []json.RawMessage `json:"arguments,omitempty"` } -/** - * Completion client capabilities - */ -type CompletionClientCapabilities struct { - /** - * Whether completion supports dynamic registration. - */ +// Completion client capabilities +type CompletionClientCapabilities struct { // line 11194 + // Whether completion supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** + /* * The client supports the following `CompletionItem` specific * capabilities. */ - CompletionItem struct { - /** - * Client supports snippets as insert text. - * - * A snippet can define tab stops and placeholders with `$1`, `$2` - * and `${3:foo}`. `$0` defines the final tab stop, it defaults to - * the end of the snippet. Placeholders with equal identifiers are linked, - * that is typing in one will update others too. - */ - SnippetSupport bool `json:"snippetSupport,omitempty"` - /** - * Client supports commit characters on a completion item. - */ - CommitCharactersSupport bool `json:"commitCharactersSupport,omitempty"` - /** - * Client supports the follow content formats for the documentation - * property. The order describes the preferred format of the client. - */ - DocumentationFormat []MarkupKind `json:"documentationFormat,omitempty"` - /** - * Client supports the deprecated property on a completion item. - */ - DeprecatedSupport bool `json:"deprecatedSupport,omitempty"` - /** - * Client supports the preselect property on a completion item. - */ - PreselectSupport bool `json:"preselectSupport,omitempty"` - /** - * Client supports the tag property on a completion item. Clients supporting - * tags have to handle unknown tags gracefully. Clients especially need to - * preserve unknown tags when sending a completion item back to the server in - * a resolve call. - * - * @since 3.15.0 - */ - TagSupport struct { - /** - * The tags supported by the client. - */ - ValueSet []CompletionItemTag `json:"valueSet"` - } `json:"tagSupport,omitempty"` - /** - * Client support insert replace edit to control different behavior if a - * completion item is inserted in the text or should replace text. - * - * @since 3.16.0 - */ - InsertReplaceSupport bool `json:"insertReplaceSupport,omitempty"` - /** - * Indicates which properties a client can resolve lazily on a completion - * item. Before version 3.16.0 only the predefined properties `documentation` - * and `details` could be resolved lazily. - * - * @since 3.16.0 - */ - ResolveSupport struct { - /** - * The properties that a client can resolve lazily. - */ - Properties []string `json:"properties"` - } `json:"resolveSupport,omitempty"` - /** - * The client supports the `insertTextMode` property on - * a completion item to override the whitespace handling mode - * as defined by the client (see `insertTextMode`). - * - * @since 3.16.0 - */ - InsertTextModeSupport struct { - ValueSet []InsertTextMode `json:"valueSet"` - } `json:"insertTextModeSupport,omitempty"` - /** - * The client has support for completion item label - * details (see also `CompletionItemLabelDetails`). - * - * @since 3.17.0 - proposed state - */ - LabelDetailsSupport bool `json:"labelDetailsSupport,omitempty"` - } `json:"completionItem,omitempty"` - CompletionItemKind struct { - /** - * The completion item kind values the client supports. When this - * property exists the client also guarantees that it will - * handle values outside its set gracefully and falls back - * to a default value when unknown. - * - * If this property is not present the client only supports - * the completion items kinds from `Text` to `Reference` as defined in - * the initial version of the protocol. - */ - ValueSet []CompletionItemKind `json:"valueSet,omitempty"` - } `json:"completionItemKind,omitempty"` - /** + CompletionItem PCompletionItemPCompletion `json:"completionItem,omitempty"` + CompletionItemKind PCompletionItemKindPCompletion `json:"completionItemKind,omitempty"` + /* * Defines how the client handles whitespace and indentation * when accepting a completion item that uses multi line * text in either `insertText` or `textEdit`. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` - /** + /* * The client supports to send additional context information for a * `textDocument/completion` request. */ ContextSupport bool `json:"contextSupport,omitempty"` - /** + /* * The client supports the following `CompletionList` specific * capabilities. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ - CompletionList struct { - /** - * The client supports the the following itemDefaults on - * a completion list. - * - * The value lists the supported property names of the - * `CompletionList.itemDefaults` object. If omitted - * no properties are supported. - * - * @since 3.17.0 - proposed state - */ - ItemDefaults []string `json:"itemDefaults,omitempty"` - } `json:"completionList,omitempty"` + CompletionList PCompletionListPCompletion `json:"completionList,omitempty"` } -/** - * Contains additional information about the context in which a completion request is triggered. - */ -type CompletionContext struct { - /** - * How the completion was triggered. - */ +// Contains additional information about the context in which a completion request is triggered. +type CompletionContext struct { // line 8648 + // How the completion was triggered. TriggerKind CompletionTriggerKind `json:"triggerKind"` - /** + /* * The trigger character (a single character) that has trigger code complete. * Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` */ TriggerCharacter string `json:"triggerCharacter,omitempty"` } -/** +/* * A completion item represents a text snippet that is * proposed to complete text that is being typed. */ -type CompletionItem struct { - /** +type CompletionItem struct { // line 4550 + /* * The label of this completion item. * * The label property is also by default the text that @@ -868,38 +629,36 @@ type CompletionItem struct { * be an unqualified name of the completion item. */ Label string `json:"label"` - /** + /* * Additional details for the label * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ LabelDetails CompletionItemLabelDetails `json:"labelDetails,omitempty"` - /** + /* * The kind of this completion item. Based of the kind * an icon is chosen by the editor. */ Kind CompletionItemKind `json:"kind,omitempty"` - /** + /* * Tags for this completion item. * * @since 3.15.0 */ Tags []CompletionItemTag `json:"tags,omitempty"` - /** + /* * A human-readable string with additional information * about this item, like type or symbol information. */ Detail string `json:"detail,omitempty"` - /** - * A human-readable string that represents a doc-comment. - */ - Documentation string/*string | MarkupContent*/ `json:"documentation,omitempty"` - /** + // A human-readable string that represents a doc-comment. + Documentation string `json:"documentation,omitempty"` + /* * Indicates if this item is deprecated. * @deprecated Use `tags` instead. */ Deprecated bool `json:"deprecated,omitempty"` - /** + /* * Select this item when showing. * * *Note* that only one completion item can be selected and that the @@ -907,67 +666,86 @@ type CompletionItem struct { * item of those that match best is selected. */ Preselect bool `json:"preselect,omitempty"` - /** + /* * A string that should be used when comparing this item * with other items. When `falsy` the [label](#CompletionItem.label) * is used. */ SortText string `json:"sortText,omitempty"` - /** + /* * A string that should be used when filtering a set of * completion items. When `falsy` the [label](#CompletionItem.label) * is used. */ FilterText string `json:"filterText,omitempty"` - /** + /* * A string that should be inserted into a document when selecting * this completion. When `falsy` the [label](#CompletionItem.label) * is used. * * The `insertText` is subject to interpretation by the client side. * Some tools might not take the string literally. For example - * VS Code when code complete is requested in this example `con` - * and a completion item with an `insertText` of `console` is provided it - * will only insert `sole`. Therefore it is recommended to use `textEdit` instead - * since it avoids additional client side interpretation. + * VS Code when code complete is requested in this example + * `con` and a completion item with an `insertText` of + * `console` is provided it will only insert `sole`. Therefore it is + * recommended to use `textEdit` instead since it avoids additional client + * side interpretation. */ InsertText string `json:"insertText,omitempty"` - /** - * The format of the insert text. The format applies to both the `insertText` property - * and the `newText` property of a provided `textEdit`. If omitted defaults to - * `InsertTextFormat.PlainText`. + /* + * The format of the insert text. The format applies to both the + * `insertText` property and the `newText` property of a provided + * `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`. * - * Please note that the insertTextFormat doesn't apply to `additionalTextEdits`. + * Please note that the insertTextFormat doesn't apply to + * `additionalTextEdits`. */ InsertTextFormat InsertTextFormat `json:"insertTextFormat,omitempty"` - /** + /* * How whitespace and indentation is handled during completion - * item insertion. If ignored the clients default value depends on + * item insertion. If not provided the clients default value depends on * the `textDocument.completion.insertTextMode` client capability. * * @since 3.16.0 */ InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` - /** + /* * An [edit](#TextEdit) which is applied to a document when selecting * this completion. When an edit is provided the value of * [insertText](#CompletionItem.insertText) is ignored. * - * Most editors support two different operation when accepting a completion item. One is to insert a - * completion text and the other is to replace an existing text with a completion text. Since this can - * usually not predetermined by a server it can report both ranges. Clients need to signal support for - * `InsertReplaceEdits` via the `textDocument.completion.insertReplaceSupport` client capability + * Most editors support two different operations when accepting a completion + * item. One is to insert a completion text and the other is to replace an + * existing text with a completion text. Since this can usually not be + * predetermined by a server it can report both ranges. Clients need to + * signal support for `InsertReplaceEdits` via the + * `textDocument.completion.insertReplaceSupport` client capability * property. * - * *Note 1:* The text edit's range as well as both ranges from a insert replace edit must be a - * [single line] and they must contain the position at which completion has been requested. - * *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range must be a prefix of - * the edit's replace range, that means it must be contained and starting at the same position. + * *Note 1:* The text edit's range as well as both ranges from an insert + * replace edit must be a [single line] and they must contain the position + * at which completion has been requested. + * *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range + * must be a prefix of the edit's replace range, that means it must be + * contained and starting at the same position. * * @since 3.16.0 additional type `InsertReplaceEdit` */ - TextEdit *TextEdit/*TextEdit | InsertReplaceEdit*/ `json:"textEdit,omitempty"` - /** + TextEdit *TextEdit `json:"textEdit,omitempty"` + /* + * The edit text used if the completion item is part of a CompletionList and + * CompletionList defines an item default for the text edit range. + * + * Clients will only honor this property if they opt into completion list + * item defaults using the capability `completionList.itemDefaults`. + * + * If not provided and a list's default range is provided the label + * property is used as a text. + * + * @since 3.17.0 + */ + TextEditText string `json:"textEditText,omitempty"` + /* * An optional array of additional [text edits](#TextEdit) that are applied when * selecting this completion. Edits must not overlap (including the same insert position) * with the main [edit](#CompletionItem.textEdit) nor with themselves. @@ -977,66 +755,56 @@ type CompletionItem struct { * insert an unqualified type). */ AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` - /** + /* * An optional set of characters that when pressed while this completion is active will accept it first and * then type that character. *Note* that all commit characters should have `length=1` and that superfluous * characters will be ignored. */ CommitCharacters []string `json:"commitCharacters,omitempty"` - /** + /* * An optional [command](#Command) that is executed *after* inserting this completion. *Note* that * additional modifications to the current document should be described with the * [additionalTextEdits](#CompletionItem.additionalTextEdits)-property. */ Command *Command `json:"command,omitempty"` - /** + /* * A data entry field that is preserved on a completion item between a * [CompletionRequest](#CompletionRequest) and a [CompletionResolveRequest](#CompletionResolveRequest). */ - Data LSPAny `json:"data,omitempty"` + Data interface{} `json:"data,omitempty"` } - -/** - * The kind of a completion entry. - */ -type CompletionItemKind float64 - -/** +type CompletionItemKind uint32 // line 13160 +/* * Additional details for a completion item label. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type CompletionItemLabelDetails struct { - /** +type CompletionItemLabelDetails struct { // line 8671 + /* * An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, - * without any spacing. Should be used for function signatures or type annotations. + * without any spacing. Should be used for function signatures and type annotations. */ Detail string `json:"detail,omitempty"` - /** + /* * An optional string which is rendered less prominently after {@link CompletionItem.detail}. Should be used - * for fully qualified names or file path. + * for fully qualified names and file paths. */ Description string `json:"description,omitempty"` } - -/** - * Completion item tags are extra annotations that tweak the rendering of a completion - * item. - * - * @since 3.15.0 - */ -type CompletionItemTag float64 - -/** +type CompletionItemTag uint32 // line 13270 +/* * Represents a collection of [completion items](#CompletionItem) to be presented * in the editor. */ -type CompletionList struct { - /** +type CompletionList struct { // line 4758 + /* * This list it not complete. Further typing results in recomputing this list. + * + * Recomputed lists have all their items replaced (not appended) in the + * incomplete completion sessions. */ IsIncomplete bool `json:"isIncomplete"` - /** + /* * In many cases the items of an actual completion result share the same * value for properties like `commitCharacters` or the range of a text * edit. A completion list can therefore define item defaults which will @@ -1049,45 +817,16 @@ type CompletionList struct { * signals support for this via the `completionList.itemDefaults` * capability. * - * @since 3.17.0 - proposed state - */ - ItemDefaults struct { - /** - * A default commit character set. - * - * @since 3.17.0 - proposed state - */ - CommitCharacters []string `json:"commitCharacters,omitempty"` - /** - * A default edit range - * - * @since 3.17.0 - proposed state - */ - EditRange Range/*Range | { insert: Range; replace: Range; }*/ `json:"editRange,omitempty"` - /** - * A default insert text format - * - * @since 3.17.0 - proposed state - */ - InsertTextFormat InsertTextFormat `json:"insertTextFormat,omitempty"` - /** - * A default insert text mode - * - * @since 3.17.0 - proposed state - */ - InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` - } `json:"itemDefaults,omitempty"` - /** - * The completion items. + * @since 3.17.0 */ + ItemDefaults PItemDefaultsMsg_textDocument_completion `json:"itemDefaults,omitempty"` + // The completion items. Items []CompletionItem `json:"items"` } -/** - * Completion options. - */ -type CompletionOptions struct { - /** +// Completion options. +type CompletionOptions struct { // line 8727 + /* * Most tools trigger completion request automatically without explicitly requesting * it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user * starts to type an identifier. For example if the user types `c` in a JavaScript file @@ -1098,7 +837,7 @@ type CompletionOptions struct { * an identifier (for example `.` in JavaScript) list them in `triggerCharacters`. */ TriggerCharacters []string `json:"triggerCharacters,omitempty"` - /** + /* * The list of all possible characters that commit a completion. This field can be used * if clients don't support individual commit characters per completion item. See * `ClientCapabilities.textDocument.completion.completionItem.commitCharactersSupport` @@ -1109,35 +848,24 @@ type CompletionOptions struct { * @since 3.2.0 */ AllCommitCharacters []string `json:"allCommitCharacters,omitempty"` - /** + /* * The server provides support to resolve additional * information for a completion item. */ ResolveProvider bool `json:"resolveProvider,omitempty"` - /** + /* * The server supports the following `CompletionItem` specific * capabilities. * - * @since 3.17.0 - proposed state - */ - CompletionItem struct { - /** - * The server has support for completion item label - * details (see also `CompletionItemLabelDetails`) when - * receiving a completion item in a resolve call. - * - * @since 3.17.0 - proposed state - */ - LabelDetailsSupport bool `json:"labelDetailsSupport,omitempty"` - } `json:"completionItem,omitempty"` + * @since 3.17.0 + */ + CompletionItem PCompletionItemPCompletionProvider `json:"completionItem,omitempty"` WorkDoneProgressOptions } -/** - * Completion parameters - */ -type CompletionParams struct { - /** +// Completion parameters +type CompletionParams struct { // line 4519 + /* * The completion context. This is only available it the client specifies * to send this using the client capability `textDocument.completion.contextSupport === true` */ @@ -1147,112 +875,69 @@ type CompletionParams struct { PartialResultParams } -/** - * How a completion was triggered - */ -type CompletionTriggerKind float64 - -type ConfigurationClientCapabilities struct { - /** - * The workspace client capabilities - */ - Workspace Workspace4Gn `json:"workspace,omitempty"` +// Registration options for a [CompletionRequest](#CompletionRequest). +type CompletionRegistrationOptions struct { // line 4875 + TextDocumentRegistrationOptions + CompletionOptions } - -type ConfigurationItem struct { - /** - * The scope to get the configuration section for. - */ +type CompletionTriggerKind uint32 // line 13581 +type ConfigurationItem struct { // line 6401 + // The scope to get the configuration section for. ScopeURI string `json:"scopeUri,omitempty"` - /** - * The configuration section asked for. - */ + // The configuration section asked for. Section string `json:"section,omitempty"` } -/** - * The parameters of a configuration request. - */ -type ConfigurationParams struct { +// The parameters of a configuration request. +type ConfigurationParams struct { // line 2207 Items []ConfigurationItem `json:"items"` } -/** - * Create file operation. - */ -type CreateFile struct { - /** - * A create - */ +// Create file operation. +type CreateFile struct { // line 6717 + // A create Kind string `json:"kind"` - /** - * The resource to create. - */ + // The resource to create. URI DocumentURI `json:"uri"` - /** - * Additional options - */ + // Additional options Options CreateFileOptions `json:"options,omitempty"` ResourceOperation } -/** - * Options to create a file. - */ -type CreateFileOptions struct { - /** - * Overwrite existing file. Overwrite wins over `ignoreIfExists` - */ +// Options to create a file. +type CreateFileOptions struct { // line 9437 + // Overwrite existing file. Overwrite wins over `ignoreIfExists` Overwrite bool `json:"overwrite,omitempty"` - /** - * Ignore if exists. - */ + // Ignore if exists. IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` } -/** - * The parameters sent in file create requests/notifications. +/* + * The parameters sent in notifications/requests for user-initiated creation of + * files. * * @since 3.16.0 */ -type CreateFilesParams struct { - /** - * An array of all files/folders created in this operation. - */ +type CreateFilesParams struct { // line 3197 + // An array of all files/folders created in this operation. Files []FileCreate `json:"files"` } -/** - * Defines a decimal number. Since decimal numbers are very - * rare in the language server specification we denote the - * exact range with every decimal using the mathematics - * interval notations (e.g. [0, 1] denotes all decimals d with - * 0 <= d <= 1. - */ -type Decimal = float64 - -/** - * The declaration of a symbol representation as one or many [locations](#Location). - */ -type Declaration = []Location /*Location | Location[]*/ - -/** - * @since 3.14.0 - */ -type DeclarationClientCapabilities struct { - /** +// The declaration of a symbol representation as one or many [locations](#Location). +type Declaration = []Location // (alias) line 13859 +// @since 3.14.0 +type DeclarationClientCapabilities struct { // line 11535 + /* * Whether declaration supports dynamic registration. If this is set to `true` * the client supports the new `DeclarationRegistrationOptions` return value * for the corresponding server capability as well. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client supports additional metadata in the form of declaration links. - */ + // The client supports additional metadata in the form of declaration links. LinkSupport bool `json:"linkSupport,omitempty"` } -/** +/* * Information about where a symbol is declared. * * Provides additional metadata over normal [location](#Location) declarations, including the range of @@ -1261,25 +946,22 @@ type DeclarationClientCapabilities struct { * Servers should prefer returning `DeclarationLink` over `Declaration` if supported * by the client. */ -type DeclarationLink = LocationLink - -type DeclarationOptions struct { +type DeclarationLink = LocationLink // (alias) line 13879 +type DeclarationOptions struct { // line 6496 WorkDoneProgressOptions } - -type DeclarationParams struct { +type DeclarationParams struct { // line 2516 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } - -type DeclarationRegistrationOptions struct { +type DeclarationRegistrationOptions struct { // line 2536 DeclarationOptions TextDocumentRegistrationOptions StaticRegistrationOptions } -/** +/* * The definition of a symbol represented as one or many [locations](#Location). * For most programming languages there is only one location at which a symbol is * defined. @@ -1287,17 +969,12 @@ type DeclarationRegistrationOptions struct { * Servers should prefer returning `DefinitionLink` over `Definition` if supported * by the client. */ -type Definition = []Location /*Location | Location[]*/ - -/** - * Client Capabilities for a [DefinitionRequest](#DefinitionRequest). - */ -type DefinitionClientCapabilities struct { - /** - * Whether definition supports dynamic registration. - */ +type Definition = Or_Definition // (alias) line 13777 +// Client Capabilities for a [DefinitionRequest](#DefinitionRequest). +type DefinitionClientCapabilities struct { // line 11560 + // Whether definition supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** + /* * The client supports additional metadata in the form of definition links. * * @since 3.14.0 @@ -1305,189 +982,227 @@ type DefinitionClientCapabilities struct { LinkSupport bool `json:"linkSupport,omitempty"` } -/** +/* * Information about where a symbol is defined. * * Provides additional metadata over normal [location](#Location) definitions, including the range of * the defining symbol */ -type DefinitionLink = LocationLink - -/** - * Server Capabilities for a [DefinitionRequest](#DefinitionRequest). - */ -type DefinitionOptions struct { +type DefinitionLink = LocationLink // (alias) line 13797 +// Server Capabilities for a [DefinitionRequest](#DefinitionRequest). +type DefinitionOptions struct { // line 8939 WorkDoneProgressOptions } -/** - * Parameters for a [DefinitionRequest](#DefinitionRequest). - */ -type DefinitionParams struct { +// Parameters for a [DefinitionRequest](#DefinitionRequest). +type DefinitionParams struct { // line 5039 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -/** - * Delete file operation - */ -type DeleteFile struct { - /** - * A delete - */ +// Registration options for a [DefinitionRequest](#DefinitionRequest). +type DefinitionRegistrationOptions struct { // line 5060 + TextDocumentRegistrationOptions + DefinitionOptions +} + +// Delete file operation +type DeleteFile struct { // line 6799 + // A delete Kind string `json:"kind"` - /** - * The file to delete. - */ + // The file to delete. URI DocumentURI `json:"uri"` - /** - * Delete options. - */ + // Delete options. Options DeleteFileOptions `json:"options,omitempty"` ResourceOperation } -/** - * Delete file options - */ -type DeleteFileOptions struct { - /** - * Delete the content recursively if a folder is denoted. - */ +// Delete file options +type DeleteFileOptions struct { // line 9485 + // Delete the content recursively if a folder is denoted. Recursive bool `json:"recursive,omitempty"` - /** - * Ignore the operation if the file doesn't exist. - */ + // Ignore the operation if the file doesn't exist. IgnoreIfNotExists bool `json:"ignoreIfNotExists,omitempty"` } -/** - * The parameters sent in file delete requests/notifications. +/* + * The parameters sent in notifications/requests for user-initiated deletes of + * files. * * @since 3.16.0 */ -type DeleteFilesParams struct { - /** - * An array of all files/folders deleted in this operation. - */ +type DeleteFilesParams struct { // line 3322 + // An array of all files/folders deleted in this operation. Files []FileDelete `json:"files"` } -/** +/* * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects * are only valid in the scope of a resource. */ -type Diagnostic struct { - /** - * The range at which the message applies - */ +type Diagnostic struct { // line 8545 + // The range at which the message applies Range Range `json:"range"` - /** + /* * The diagnostic's severity. Can be omitted. If omitted it is up to the * client to interpret diagnostics as error, warning, info or hint. */ Severity DiagnosticSeverity `json:"severity,omitempty"` - /** - * The diagnostic's code, which usually appear in the user interface. - */ - Code interface{}/*integer | string*/ `json:"code,omitempty"` - /** + // The diagnostic's code, which usually appear in the user interface. + Code interface{} `json:"code,omitempty"` + /* * An optional property to describe the error code. * Requires the code field (above) to be present/not null. * * @since 3.16.0 */ CodeDescription *CodeDescription `json:"codeDescription,omitempty"` - /** + /* * A human-readable string describing the source of this * diagnostic, e.g. 'typescript' or 'super lint'. It usually * appears in the user interface. */ Source string `json:"source,omitempty"` - /** - * The diagnostic's message. It usually appears in the user interface - */ + // The diagnostic's message. It usually appears in the user interface Message string `json:"message"` - /** + /* * Additional metadata about the diagnostic. * * @since 3.15.0 */ Tags []DiagnosticTag `json:"tags,omitempty"` - /** + /* * An array of related diagnostic information, e.g. when symbol-names within * a scope collide all definitions can be marked via this property. */ RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"` - /** + /* * A data entry field that is preserved between a `textDocument/publishDiagnostics` * notification and `textDocument/codeAction` request. * * @since 3.16.0 */ - Data LSPAny `json:"data,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +/* + * Client capabilities specific to diagnostic pull requests. + * + * @since 3.17.0 + */ +type DiagnosticClientCapabilities struct { // line 12434 + /* + * Whether implementation supports dynamic registration. If this is set to `true` + * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + * return value for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + // Whether the clients supports related documents for document diagnostic pulls. + RelatedDocumentSupport bool `json:"relatedDocumentSupport,omitempty"` +} + +/* + * Diagnostic options. + * + * @since 3.17.0 + */ +type DiagnosticOptions struct { // line 7298 + /* + * An optional identifier under which the diagnostics are + * managed by the client. + */ + Identifier string `json:"identifier,omitempty"` + /* + * Whether the language has inter file dependencies meaning that + * editing code in one file can result in a different diagnostic + * set in another file. Inter file dependencies are common for + * most programming languages and typically uncommon for linters. + */ + InterFileDependencies bool `json:"interFileDependencies"` + // The server provides support for workspace diagnostics as well. + WorkspaceDiagnostics bool `json:"workspaceDiagnostics"` + WorkDoneProgressOptions +} + +/* + * Diagnostic registration options. + * + * @since 3.17.0 + */ +type DiagnosticRegistrationOptions struct { // line 3877 + TextDocumentRegistrationOptions + DiagnosticOptions + StaticRegistrationOptions } -/** +/* * Represents a related message and source code location for a diagnostic. This should be * used to point to code locations that cause or related to a diagnostics, e.g when duplicating * a symbol in a scope. */ -type DiagnosticRelatedInformation struct { - /** - * The location of this related diagnostic information. - */ +type DiagnosticRelatedInformation struct { // line 10067 + // The location of this related diagnostic information. Location Location `json:"location"` - /** - * The message of this related diagnostic information. - */ + // The message of this related diagnostic information. Message string `json:"message"` } -/** - * The diagnostic's severity. +/* + * Cancellation data returned from a diagnostic request. + * + * @since 3.17.0 */ -type DiagnosticSeverity float64 - -/** - * The diagnostic tags. +type DiagnosticServerCancellationData struct { // line 3863 + RetriggerRequest bool `json:"retriggerRequest"` +} +type DiagnosticSeverity uint32 // line 13530 +type DiagnosticTag uint32 // line 13560 +/* + * Workspace client capabilities specific to diagnostic pull requests. * - * @since 3.15.0 + * @since 3.17.0 */ -type DiagnosticTag float64 - -type DidChangeConfigurationClientCapabilities struct { - /** - * Did change configuration notification supports dynamic registration. +type DiagnosticWorkspaceClientCapabilities struct { // line 11137 + /* + * Whether the client implementation supports a refresh request sent from + * the server to the client. + * + * Note that this event is global and will force the client to refresh all + * pulled diagnostics currently shown. It should be used with absolute care and + * is useful for situation where a server for example detects a project wide + * change that requires such a calculation. */ + RefreshSupport bool `json:"refreshSupport,omitempty"` +} +type DidChangeConfigurationClientCapabilities struct { // line 10863 + // Did change configuration notification supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** - * The parameters of a change configuration notification. - */ -type DidChangeConfigurationParams struct { - /** - * The actual changed settings - */ - Settings LSPAny `json:"settings"` +// The parameters of a change configuration notification. +type DidChangeConfigurationParams struct { // line 4166 + // The actual changed settings + Settings interface{} `json:"settings"` +} +type DidChangeConfigurationRegistrationOptions struct { // line 4180 + Section OrPSection_workspace_didChangeConfiguration `json:"section,omitempty"` } -/** +/* * The params sent in a change notebook document notification. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type DidChangeNotebookDocumentParams = struct { - /** +type DidChangeNotebookDocumentParams struct { // line 3996 + /* * The notebook document that did change. The version number points * to the version after all provided changes have been applied. If * only the text document content of a cell changes the notebook version * doesn't necessarily have to change. */ NotebookDocument VersionedNotebookDocumentIdentifier `json:"notebookDocument"` - /** + /* * The actual changes to the notebook document. * * The changes describe single state changes to the notebook document. @@ -1505,17 +1220,15 @@ type DidChangeNotebookDocumentParams = struct { Change NotebookDocumentChangeEvent `json:"change"` } -/** - * The change text document notification's parameters. - */ -type DidChangeTextDocumentParams struct { - /** +// The change text document notification's parameters. +type DidChangeTextDocumentParams struct { // line 4309 + /* * The document that did change. The version number points * to the version after all provided content changes have * been applied. */ TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` - /** + /* * The actual content changes. The content changes describe single state changes * to the document. So if there are two content changes c1 (at array index 0) and * c2 (at array index 1) for a document in state S then c1 moves the document from @@ -1530,299 +1243,242 @@ type DidChangeTextDocumentParams struct { */ ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` } - -type DidChangeWatchedFilesClientCapabilities struct { - /** +type DidChangeWatchedFilesClientCapabilities struct { // line 10877 + /* * Did change watched files notification supports dynamic registration. Please note * that the current protocol doesn't support static configuration for file changes * from the server side. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * Whether the client has support for {@link RelativePattern relative pattern} + * or not. + * + * @since 3.17.0 + */ + RelativePatternSupport bool `json:"relativePatternSupport,omitempty"` } -/** - * The watched files change notification's parameters. - */ -type DidChangeWatchedFilesParams struct { - /** - * The actual file events. - */ +// The watched files change notification's parameters. +type DidChangeWatchedFilesParams struct { // line 4450 + // The actual file events. Changes []FileEvent `json:"changes"` } -/** - * Describe options to be used when registered for text document change events. - */ -type DidChangeWatchedFilesRegistrationOptions struct { - /** - * The watchers to register. - */ +// Describe options to be used when registered for text document change events. +type DidChangeWatchedFilesRegistrationOptions struct { // line 4467 + // The watchers to register. Watchers []FileSystemWatcher `json:"watchers"` } -/** - * The parameters of a `workspace/didChangeWorkspaceFolders` notification. - */ -type DidChangeWorkspaceFoldersParams struct { - /** - * The actual workspace folder change event. - */ +// The parameters of a `workspace/didChangeWorkspaceFolders` notification. +type DidChangeWorkspaceFoldersParams struct { // line 2193 + // The actual workspace folder change event. Event WorkspaceFoldersChangeEvent `json:"event"` } -/** +/* * The params sent in a close notebook document notification. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type DidCloseNotebookDocumentParams = struct { - /** - * The notebook document that got closed. - */ +type DidCloseNotebookDocumentParams struct { // line 4034 + // The notebook document that got closed. NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` - /** + /* * The text documents that represent the content * of a notebook cell that got closed. */ CellTextDocuments []TextDocumentIdentifier `json:"cellTextDocuments"` } -/** - * The parameters send in a close text document notification - */ -type DidCloseTextDocumentParams struct { - /** - * The document that was closed. - */ +// The parameters sent in a close text document notification +type DidCloseTextDocumentParams struct { // line 4354 + // The document that was closed. TextDocument TextDocumentIdentifier `json:"textDocument"` } -/** - * The params sent in a open notebook document notification. +/* + * The params sent in an open notebook document notification. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type DidOpenNotebookDocumentParams = struct { - /** - * The notebook document that got opened. - */ +type DidOpenNotebookDocumentParams struct { // line 3970 + // The notebook document that got opened. NotebookDocument NotebookDocument `json:"notebookDocument"` - /** + /* * The text documents that represent the content * of a notebook cell. */ CellTextDocuments []TextDocumentItem `json:"cellTextDocuments"` } -/** - * The parameters send in a open text document notification - */ -type DidOpenTextDocumentParams struct { - /** - * The document that was opened. - */ +// The parameters sent in an open text document notification +type DidOpenTextDocumentParams struct { // line 4295 + // The document that was opened. TextDocument TextDocumentItem `json:"textDocument"` } -/** +/* * The params sent in a save notebook document notification. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type DidSaveNotebookDocumentParams = struct { - /** - * The notebook document that got saved. - */ +type DidSaveNotebookDocumentParams struct { // line 4019 + // The notebook document that got saved. NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` } -/** - * The parameters send in a save text document notification - */ -type DidSaveTextDocumentParams struct { - /** - * The document that was closed. - */ +// The parameters sent in a save text document notification +type DidSaveTextDocumentParams struct { // line 4368 + // The document that was saved. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** + /* * Optional the content when saved. Depends on the includeText value * when the save notification was requested. */ Text *string `json:"text,omitempty"` } - -type DocumentColorClientCapabilities struct { - /** +type DocumentColorClientCapabilities struct { // line 11901 + /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `DocumentColorRegistrationOptions` return value * for the corresponding server capability as well. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } - -type DocumentColorOptions struct { +type DocumentColorOptions struct { // line 6476 WorkDoneProgressOptions } -/** - * Parameters for a [DocumentColorRequest](#DocumentColorRequest). - */ -type DocumentColorParams struct { - /** - * The text document. - */ +// Parameters for a [DocumentColorRequest](#DocumentColorRequest). +type DocumentColorParams struct { // line 2237 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } - -type DocumentColorRegistrationOptions struct { +type DocumentColorRegistrationOptions struct { // line 2283 TextDocumentRegistrationOptions - StaticRegistrationOptions DocumentColorOptions + StaticRegistrationOptions } -/** +/* * Parameters of the document diagnostic request. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type DocumentDiagnosticParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` - /** - * The text document. - */ +type DocumentDiagnosticParams struct { // line 3790 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The additional identifier provided during registration. - */ + // The additional identifier provided during registration. Identifier string `json:"identifier,omitempty"` - /** - * The result id of a previous response if provided. - */ + // The result id of a previous response if provided. PreviousResultID string `json:"previousResultId,omitempty"` + WorkDoneProgressParams + PartialResultParams } -/** +/* * The result of a document diagnostic pull request. A report can * either be a full report containing all diagnostics for the - * requested document or a unchanged report indicating that nothing + * requested document or an unchanged report indicating that nothing * has changed in terms of diagnostics in comparison to the last * pull request. * - * @since 3.17.0 - proposed state + * @since 3.17.0 + */ +type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) line 13909 +type DocumentDiagnosticReportKind string // line 12748 +/* + * A partial result for a document diagnostic report. + * + * @since 3.17.0 */ -type DocumentDiagnosticReport = interface{} /*RelatedFullDocumentDiagnosticReport | RelatedUnchangedDocumentDiagnosticReport*/ +type DocumentDiagnosticReportPartialResult struct { // line 3833 + RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments"` +} -/** +/* * A document filter describes a top level text document or * a notebook cell document. * * @since 3.17.0 - proposed support for NotebookCellTextDocumentFilter. */ -type DocumentFilter = interface{} /*TextDocumentFilter | NotebookCellTextDocumentFilter*/ - -/** - * Client capabilities of a [DocumentFormattingRequest](#DocumentFormattingRequest). - */ -type DocumentFormattingClientCapabilities struct { - /** - * Whether formatting supports dynamic registration. - */ +type DocumentFilter = Or_DocumentFilter // (alias) line 14118 +// Client capabilities of a [DocumentFormattingRequest](#DocumentFormattingRequest). +type DocumentFormattingClientCapabilities struct { // line 11915 + // Whether formatting supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** - * Provider options for a [DocumentFormattingRequest](#DocumentFormattingRequest). - */ -type DocumentFormattingOptions struct { +// Provider options for a [DocumentFormattingRequest](#DocumentFormattingRequest). +type DocumentFormattingOptions struct { // line 9241 WorkDoneProgressOptions } -/** - * The parameters of a [DocumentFormattingRequest](#DocumentFormattingRequest). - */ -type DocumentFormattingParams struct { - /** - * The document to format. - */ +// The parameters of a [DocumentFormattingRequest](#DocumentFormattingRequest). +type DocumentFormattingParams struct { // line 5746 + // The document to format. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The format options - */ + // The format options. Options FormattingOptions `json:"options"` WorkDoneProgressParams } -/** +// Registration options for a [DocumentFormattingRequest](#DocumentFormattingRequest). +type DocumentFormattingRegistrationOptions struct { // line 5774 + TextDocumentRegistrationOptions + DocumentFormattingOptions +} + +/* * A document highlight is a range inside a text document which deserves * special attention. Usually a document highlight is visualized by changing * the background color of its range. */ -type DocumentHighlight struct { - /** - * The range this highlight applies to. - */ +type DocumentHighlight struct { // line 5140 + // The range this highlight applies to. Range Range `json:"range"` - /** - * The highlight kind, default is [text](#DocumentHighlightKind.Text). - */ + // The highlight kind, default is [text](#DocumentHighlightKind.Text). Kind DocumentHighlightKind `json:"kind,omitempty"` } -/** - * Client Capabilities for a [DocumentHighlightRequest](#DocumentHighlightRequest). - */ -type DocumentHighlightClientCapabilities struct { - /** - * Whether document highlight supports dynamic registration. - */ +// Client Capabilities for a [DocumentHighlightRequest](#DocumentHighlightRequest). +type DocumentHighlightClientCapabilities struct { // line 11650 + // Whether document highlight supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } - -/** - * A document highlight kind. - */ -type DocumentHighlightKind float64 - -/** - * Provider options for a [DocumentHighlightRequest](#DocumentHighlightRequest). - */ -type DocumentHighlightOptions struct { +type DocumentHighlightKind uint32 // line 13327 +// Provider options for a [DocumentHighlightRequest](#DocumentHighlightRequest). +type DocumentHighlightOptions struct { // line 8975 WorkDoneProgressOptions } -/** - * Parameters for a [DocumentHighlightRequest](#DocumentHighlightRequest). - */ -type DocumentHighlightParams struct { +// Parameters for a [DocumentHighlightRequest](#DocumentHighlightRequest). +type DocumentHighlightParams struct { // line 5119 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -/** +// Registration options for a [DocumentHighlightRequest](#DocumentHighlightRequest). +type DocumentHighlightRegistrationOptions struct { // line 5163 + TextDocumentRegistrationOptions + DocumentHighlightOptions +} + +/* * A document link is a range in a text document that links to an internal or external resource, like another * text document or a web site. */ -type DocumentLink struct { - /** - * The range this link applies to. - */ +type DocumentLink struct { // line 5689 + // The range this link applies to. Range Range `json:"range"` - /** - * The uri this link points to. - */ + // The uri this link points to. If missing a resolve request is sent later. Target string `json:"target,omitempty"` - /** + /* * The tooltip text when you hover over this link. * * If a tooltip is provided, is will be displayed in a string that includes instructions on how to @@ -1832,234 +1488,186 @@ type DocumentLink struct { * @since 3.15.0 */ Tooltip string `json:"tooltip,omitempty"` - /** + /* * A data entry field that is preserved on a document link between a * DocumentLinkRequest and a DocumentLinkResolveRequest. */ - Data LSPAny `json:"data,omitempty"` + Data interface{} `json:"data,omitempty"` } -/** - * The client capabilities of a [DocumentLinkRequest](#DocumentLinkRequest). - */ -type DocumentLinkClientCapabilities struct { - /** - * Whether document link supports dynamic registration. - */ +// The client capabilities of a [DocumentLinkRequest](#DocumentLinkRequest). +type DocumentLinkClientCapabilities struct { // line 11876 + // Whether document link supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Whether the client support the `tooltip` property on `DocumentLink`. + /* + * Whether the client supports the `tooltip` property on `DocumentLink`. * * @since 3.15.0 */ TooltipSupport bool `json:"tooltipSupport,omitempty"` } -/** - * Provider options for a [DocumentLinkRequest](#DocumentLinkRequest). - */ -type DocumentLinkOptions struct { - /** - * Document links have a resolve provider as well. - */ +// Provider options for a [DocumentLinkRequest](#DocumentLinkRequest). +type DocumentLinkOptions struct { // line 9168 + // Document links have a resolve provider as well. ResolveProvider bool `json:"resolveProvider,omitempty"` WorkDoneProgressOptions } -/** - * The parameters of a [DocumentLinkRequest](#DocumentLinkRequest). - */ -type DocumentLinkParams struct { - /** - * The document to provide document links for. - */ +// The parameters of a [DocumentLinkRequest](#DocumentLinkRequest). +type DocumentLinkParams struct { // line 5665 + // The document to provide document links for. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } -/** - * Client capabilities of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). - */ -type DocumentOnTypeFormattingClientCapabilities struct { - /** - * Whether on type formatting supports dynamic registration. - */ +// Registration options for a [DocumentLinkRequest](#DocumentLinkRequest). +type DocumentLinkRegistrationOptions struct { // line 5731 + TextDocumentRegistrationOptions + DocumentLinkOptions +} + +// Client capabilities of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). +type DocumentOnTypeFormattingClientCapabilities struct { // line 11945 + // Whether on type formatting supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** - * Provider options for a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). - */ -type DocumentOnTypeFormattingOptions struct { - /** - * A character on which formatting should be triggered, like `}`. - */ +// Provider options for a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). +type DocumentOnTypeFormattingOptions struct { // line 9263 + // A character on which formatting should be triggered, like `{`. FirstTriggerCharacter string `json:"firstTriggerCharacter"` - /** - * More trigger characters. - */ + // More trigger characters. MoreTriggerCharacter []string `json:"moreTriggerCharacter,omitempty"` } -/** - * The parameters of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). - */ -type DocumentOnTypeFormattingParams struct { - /** - * The document to format. - */ +// The parameters of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). +type DocumentOnTypeFormattingParams struct { // line 5840 + // The document to format. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The position at which this request was send. + /* + * The position around which the on type formatting should happen. + * This is not necessarily the exact position where the character denoted + * by the property `ch` got typed. */ Position Position `json:"position"` - /** - * The character that has been typed. + /* + * The character that has been typed that triggered the formatting + * on type request. That is not necessarily the last character that + * got inserted into the document since the client could auto insert + * characters as well (e.g. like automatic brace completion). */ Ch string `json:"ch"` - /** - * The format options. - */ + // The formatting options. Options FormattingOptions `json:"options"` } -/** - * Client capabilities of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). - */ -type DocumentRangeFormattingClientCapabilities struct { - /** - * Whether range formatting supports dynamic registration. - */ +// Registration options for a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). +type DocumentOnTypeFormattingRegistrationOptions struct { // line 5878 + TextDocumentRegistrationOptions + DocumentOnTypeFormattingOptions +} + +// Client capabilities of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). +type DocumentRangeFormattingClientCapabilities struct { // line 11930 + // Whether range formatting supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** - * Provider options for a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). - */ -type DocumentRangeFormattingOptions struct { +// Provider options for a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). +type DocumentRangeFormattingOptions struct { // line 9252 WorkDoneProgressOptions } -/** - * The parameters of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). - */ -type DocumentRangeFormattingParams struct { - /** - * The document to format. - */ +// The parameters of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). +type DocumentRangeFormattingParams struct { // line 5789 + // The document to format. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The range to format - */ + // The range to format Range Range `json:"range"` - /** - * The format options - */ + // The format options Options FormattingOptions `json:"options"` WorkDoneProgressParams } -/** +// Registration options for a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). +type DocumentRangeFormattingRegistrationOptions struct { // line 5825 + TextDocumentRegistrationOptions + DocumentRangeFormattingOptions +} + +/* * A document selector is the combination of one or many document filters. * * @sample `let sel:DocumentSelector = [{ language: 'typescript' }, { language: 'json', pattern: '**∕tsconfig.json' }]`; * * The use of a string as a document filter is deprecated @since 3.16.0. */ -type DocumentSelector = []string /*string | DocumentFilter*/ - -/** +type DocumentSelector = []DocumentFilter // (alias) line 13990 +/* * Represents programming constructs like variables, classes, interfaces etc. * that appear in a document. Document symbols can be hierarchical and they * have two ranges: one that encloses its definition and one that points to * its most interesting range, e.g. the range of an identifier. */ -type DocumentSymbol struct { - /** +type DocumentSymbol struct { // line 5231 + /* * The name of this symbol. Will be displayed in the user interface and therefore must not be * an empty string or a string only consisting of white spaces. */ Name string `json:"name"` - /** - * More detail for this symbol, e.g the signature of a function. - */ + // More detail for this symbol, e.g the signature of a function. Detail string `json:"detail,omitempty"` - /** - * The kind of this symbol. - */ + // The kind of this symbol. Kind SymbolKind `json:"kind"` - /** + /* * Tags for this document symbol. * * @since 3.16.0 */ Tags []SymbolTag `json:"tags,omitempty"` - /** + /* * Indicates if this symbol is deprecated. * * @deprecated Use tags instead */ Deprecated bool `json:"deprecated,omitempty"` - /** + /* * The range enclosing this symbol not including leading/trailing whitespace but everything else - * like comments. This information is typically used to determine if the the clients cursor is + * like comments. This information is typically used to determine if the clients cursor is * inside the symbol to reveal in the symbol in the UI. */ Range Range `json:"range"` - /** + /* * The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. - * Must be contained by the the `range`. + * Must be contained by the `range`. */ SelectionRange Range `json:"selectionRange"` - /** - * Children of this symbol, e.g. properties of a class. - */ + // Children of this symbol, e.g. properties of a class. Children []DocumentSymbol `json:"children,omitempty"` } -/** - * Client Capabilities for a [DocumentSymbolRequest](#DocumentSymbolRequest). - */ -type DocumentSymbolClientCapabilities struct { - /** - * Whether document symbol supports dynamic registration. - */ +// Client Capabilities for a [DocumentSymbolRequest](#DocumentSymbolRequest). +type DocumentSymbolClientCapabilities struct { // line 11665 + // Whether document symbol supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Specific capabilities for the `SymbolKind`. - */ - SymbolKind struct { - /** - * The symbol kind values the client supports. When this - * property exists the client also guarantees that it will - * handle values outside its set gracefully and falls back - * to a default value when unknown. - * - * If this property is not present the client only supports - * the symbol kinds from `File` to `Array` as defined in - * the initial version of the protocol. - */ - ValueSet []SymbolKind `json:"valueSet,omitempty"` - } `json:"symbolKind,omitempty"` - /** - * The client support hierarchical document symbols. + /* + * Specific capabilities for the `SymbolKind` in the + * `textDocument/documentSymbol` request. */ + SymbolKind PSymbolKindPDocumentSymbol `json:"symbolKind,omitempty"` + // The client supports hierarchical document symbols. HierarchicalDocumentSymbolSupport bool `json:"hierarchicalDocumentSymbolSupport,omitempty"` - /** + /* * The client supports tags on `SymbolInformation`. Tags are supported on * `DocumentSymbol` if `hierarchicalDocumentSymbolSupport` is set to true. * Clients supporting tags have to handle unknown tags gracefully. * * @since 3.16.0 */ - TagSupport struct { - /** - * The tags supported by the client. - */ - ValueSet []SymbolTag `json:"valueSet"` - } `json:"tagSupport,omitempty"` - /** + TagSupport PTagSupportPDocumentSymbol `json:"tagSupport,omitempty"` + /* * The client supports an additional label presented in the UI when * registering a document symbol provider. * @@ -2068,11 +1676,9 @@ type DocumentSymbolClientCapabilities struct { LabelSupport bool `json:"labelSupport,omitempty"` } -/** - * Provider options for a [DocumentSymbolRequest](#DocumentSymbolRequest). - */ -type DocumentSymbolOptions struct { - /** +// Provider options for a [DocumentSymbolRequest](#DocumentSymbolRequest). +type DocumentSymbolOptions struct { // line 9030 + /* * A human-readable string that is shown when multiple outlines trees * are shown for the same document. * @@ -2082,119 +1688,170 @@ type DocumentSymbolOptions struct { WorkDoneProgressOptions } -/** - * Parameters for a [DocumentSymbolRequest](#DocumentSymbolRequest). - */ -type DocumentSymbolParams struct { - /** - * The text document. - */ +// Parameters for a [DocumentSymbolRequest](#DocumentSymbolRequest). +type DocumentSymbolParams struct { // line 5178 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } -/** - * A tagging type for string properties that are actually document URIs. - */ -type DocumentURI string - -/** - * The client capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). - */ -type ExecuteCommandClientCapabilities struct { - /** - * Execute command supports dynamic registration. - */ +// Registration options for a [DocumentSymbolRequest](#DocumentSymbolRequest). +type DocumentSymbolRegistrationOptions struct { // line 5312 + TextDocumentRegistrationOptions + DocumentSymbolOptions +} +type DocumentURI string // line 0 +type ErrorCodes int32 // line 12769 +// The client capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). +type ExecuteCommandClientCapabilities struct { // line 10988 + // Execute command supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** - * The server capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). - */ -type ExecuteCommandOptions struct { - /** - * The commands to be executed on the server - */ +// The server capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). +type ExecuteCommandOptions struct { // line 9311 + // The commands to be executed on the server Commands []string `json:"commands"` WorkDoneProgressOptions } -/** - * The parameters of a [ExecuteCommandRequest](#ExecuteCommandRequest). - */ -type ExecuteCommandParams struct { - /** - * The identifier of the actual command handler. - */ +// The parameters of a [ExecuteCommandRequest](#ExecuteCommandRequest). +type ExecuteCommandParams struct { // line 5960 + // The identifier of the actual command handler. Command string `json:"command"` - /** - * Arguments that the command should be invoked with. - */ + // Arguments that the command should be invoked with. Arguments []json.RawMessage `json:"arguments,omitempty"` WorkDoneProgressParams } -type ExecutionSummary = struct { - /** +// Registration options for a [ExecuteCommandRequest](#ExecuteCommandRequest). +type ExecuteCommandRegistrationOptions struct { // line 5992 + ExecuteCommandOptions +} +type ExecutionSummary struct { // line 10188 + /* * A strict monotonically increasing value * indicating the execution order of a cell * inside a notebook. */ ExecutionOrder uint32 `json:"executionOrder"` - /** + /* * Whether the execution was successful or * not if known by the client. */ Success bool `json:"success,omitempty"` } -type FailureHandlingKind string +// created for Literal +type FCellsPNotebookSelector struct { // line 9857 + Language string `json:"Language"` +} -/** - * The file event type - */ -type FileChangeType float64 +// created for Literal +type FCodeActionKindPCodeActionLiteralSupport struct { // line 11768 + /* + * The code action kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + */ + ValueSet []CodeActionKind `json:"ValueSet"` +} + +// created for Literal +type FEditRangePItemDefaults struct { // line 4797 + Insert Range `json:"Insert"` + Replace Range `json:"Replace"` +} + +// created for Literal +type FFullPRequests struct { // line 12230 + /* + * The client will send the `textDocument/semanticTokens/full/delta` request if + * the server provides a corresponding handler. + */ + Delta bool `json:"Delta"` +} + +// created for Literal +type FInsertTextModeSupportPCompletionItem struct { // line 11321 + ValueSet []InsertTextMode `json:"ValueSet"` +} + +// created for Literal +type FParameterInformationPSignatureInformation struct { // line 11487 + /* + * The client supports processing label offsets instead of a + * simple label string. + * + * @since 3.14.0 + */ + LabelOffsetSupport bool `json:"LabelOffsetSupport"` +} + +// created for Literal +type FRangePRequests struct { // line 12210 +} + +// created for Literal +type FResolveSupportPCompletionItem struct { // line 11297 + // The properties that a client can resolve lazily. + Properties []string `json:"Properties"` +} + +// created for Literal +type FStructurePCells struct { // line 7492 + // The change to the cell array. + Array NotebookCellArrayChange `json:"Array"` + // Additional opened cell text documents. + DidOpen []TextDocumentItem `json:"DidOpen"` + // Additional closed cell text documents. + DidClose []TextDocumentIdentifier `json:"DidClose"` +} -/** +// created for Literal +type FTagSupportPCompletionItem struct { // line 11263 + // The tags supported by the client. + ValueSet []CompletionItemTag `json:"ValueSet"` +} + +// created for Literal +type FTextContentPCells struct { // line 7550 + Document VersionedTextDocumentIdentifier `json:"Document"` + Changes []TextDocumentContentChangeEvent `json:"Changes"` +} +type FailureHandlingKind string // line 13719 +type FileChangeType uint32 // line 13480 +/* * Represents information on a file/folder create. * * @since 3.16.0 */ -type FileCreate struct { - /** - * A file:// URI for the location of the file/folder being created. - */ +type FileCreate struct { // line 6667 + // A file:// URI for the location of the file/folder being created. URI string `json:"uri"` } -/** +/* * Represents information on a file/folder delete. * * @since 3.16.0 */ -type FileDelete struct { - /** - * A file:// URI for the location of the file/folder being deleted. - */ +type FileDelete struct { // line 6916 + // A file:// URI for the location of the file/folder being deleted. URI string `json:"uri"` } -/** - * An event describing a file change. - */ -type FileEvent struct { - /** - * The file's uri. - */ +// An event describing a file change. +type FileEvent struct { // line 8500 + // The file's uri. URI DocumentURI `json:"uri"` - /** - * The change type. - */ + // The change type. Type FileChangeType `json:"type"` } -/** +/* * Capabilities relating to events from file operations by the user in the client. * * These events do not come from the file system, they come from user operations @@ -2202,94 +1859,64 @@ type FileEvent struct { * * @since 3.16.0 */ -type FileOperationClientCapabilities struct { - /** - * Whether the client supports dynamic registration for file requests/notifications. - */ +type FileOperationClientCapabilities struct { // line 11035 + // Whether the client supports dynamic registration for file requests/notifications. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client has support for sending didCreateFiles notifications. - */ + // The client has support for sending didCreateFiles notifications. DidCreate bool `json:"didCreate,omitempty"` - /** - * The client has support for willCreateFiles requests. - */ + // The client has support for sending willCreateFiles requests. WillCreate bool `json:"willCreate,omitempty"` - /** - * The client has support for sending didRenameFiles notifications. - */ + // The client has support for sending didRenameFiles notifications. DidRename bool `json:"didRename,omitempty"` - /** - * The client has support for willRenameFiles requests. - */ + // The client has support for sending willRenameFiles requests. WillRename bool `json:"willRename,omitempty"` - /** - * The client has support for sending didDeleteFiles notifications. - */ + // The client has support for sending didDeleteFiles notifications. DidDelete bool `json:"didDelete,omitempty"` - /** - * The client has support for willDeleteFiles requests. - */ + // The client has support for sending willDeleteFiles requests. WillDelete bool `json:"willDelete,omitempty"` } -/** +/* * A filter to describe in which file operation requests or notifications - * the server is interested in. + * the server is interested in receiving. * * @since 3.16.0 */ -type FileOperationFilter struct { - /** - * A Uri like `file` or `untitled`. - */ +type FileOperationFilter struct { // line 6869 + // A Uri scheme like `file` or `untitled`. Scheme string `json:"scheme,omitempty"` - /** - * The actual file operation pattern. - */ + // The actual file operation pattern. Pattern FileOperationPattern `json:"pattern"` } -/** +/* * Options for notifications/requests for user operations on files. * * @since 3.16.0 */ -type FileOperationOptions struct { - /** - * The server is interested in didCreateFiles notifications. - */ +type FileOperationOptions struct { // line 9991 + // The server is interested in receiving didCreateFiles notifications. DidCreate FileOperationRegistrationOptions `json:"didCreate,omitempty"` - /** - * The server is interested in willCreateFiles requests. - */ + // The server is interested in receiving willCreateFiles requests. WillCreate FileOperationRegistrationOptions `json:"willCreate,omitempty"` - /** - * The server is interested in didRenameFiles notifications. - */ + // The server is interested in receiving didRenameFiles notifications. DidRename FileOperationRegistrationOptions `json:"didRename,omitempty"` - /** - * The server is interested in willRenameFiles requests. - */ + // The server is interested in receiving willRenameFiles requests. WillRename FileOperationRegistrationOptions `json:"willRename,omitempty"` - /** - * The server is interested in didDeleteFiles file notifications. - */ + // The server is interested in receiving didDeleteFiles file notifications. DidDelete FileOperationRegistrationOptions `json:"didDelete,omitempty"` - /** - * The server is interested in willDeleteFiles file requests. - */ + // The server is interested in receiving willDeleteFiles file requests. WillDelete FileOperationRegistrationOptions `json:"willDelete,omitempty"` } -/** +/* * A pattern to describe in which file operation requests or notifications - * the server is interested in. + * the server is interested in receiving. * * @since 3.16.0 */ -type FileOperationPattern struct { - /** +type FileOperationPattern struct { // line 9509 + /* * The glob pattern to match. Glob patterns can have the following syntax: * - `*` to match one or more characters in a path segment * - `?` to match on one character in a path segment @@ -2299,187 +1926,166 @@ type FileOperationPattern struct { * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) */ Glob string `json:"glob"` - /** + /* * Whether to match files or folders with this pattern. * * Matches both if undefined. */ Matches FileOperationPatternKind `json:"matches,omitempty"` - /** - * Additional options used during matching. - */ + // Additional options used during matching. Options FileOperationPatternOptions `json:"options,omitempty"` } - -/** - * A pattern kind describing if a glob pattern matches a file a folder or - * both. - * - * @since 3.16.0 - */ -type FileOperationPatternKind string - -/** +type FileOperationPatternKind string // line 13653 +/* * Matching options for the file operation pattern. * * @since 3.16.0 */ -type FileOperationPatternOptions struct { - /** - * The pattern should be matched ignoring casing. - */ +type FileOperationPatternOptions struct { // line 10172 + // The pattern should be matched ignoring casing. IgnoreCase bool `json:"ignoreCase,omitempty"` } -/** +/* * The options to register for file operations. * * @since 3.16.0 */ -type FileOperationRegistrationOptions struct { - /** - * The actual filters. - */ +type FileOperationRegistrationOptions struct { // line 3286 + // The actual filters. Filters []FileOperationFilter `json:"filters"` } -/** +/* * Represents information on a file/folder rename. * * @since 3.16.0 */ -type FileRename struct { - /** - * A file:// URI for the original location of the file/folder being renamed. - */ +type FileRename struct { // line 6893 + // A file:// URI for the original location of the file/folder being renamed. OldURI string `json:"oldUri"` - /** - * A file:// URI for the new location of the file/folder being renamed. - */ + // A file:// URI for the new location of the file/folder being renamed. NewURI string `json:"newUri"` } - -type FileSystemWatcher struct { - /** - * The glob pattern to watch. Glob patterns can have the following syntax: - * - `*` to match one or more characters in a path segment - * - `?` to match on one character in a path segment - * - `**` to match any number of path segments, including none - * - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) - * - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) +type FileSystemWatcher struct { // line 8522 + /* + * The glob pattern to watch. See {@link GlobPattern glob pattern} for more detail. + * + * @since 3.17.0 support for relative patterns. */ - GlobPattern string `json:"globPattern"` - /** + GlobPattern GlobPattern `json:"globPattern"` + /* * The kind of events of interest. If omitted it defaults * to WatchKind.Create | WatchKind.Change | WatchKind.Delete * which is 7. */ - Kind uint32 `json:"kind,omitempty"` + Kind WatchKind `json:"kind,omitempty"` } -/** +/* * Represents a folding range. To be valid, start and end line must be bigger than zero and smaller * than the number of lines in the document. Clients are free to ignore invalid ranges. */ -type FoldingRange struct { - /** +type FoldingRange struct { // line 2437 + /* * The zero-based start line of the range to fold. The folded area starts after the line's last character. * To be valid, the end must be zero or larger and smaller than the number of lines in the document. */ StartLine uint32 `json:"startLine"` - /** - * The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. - */ + // The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. StartCharacter uint32 `json:"startCharacter,omitempty"` - /** + /* * The zero-based end line of the range to fold. The folded area ends with the line's last character. * To be valid, the end must be zero or larger and smaller than the number of lines in the document. */ EndLine uint32 `json:"endLine"` - /** - * The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. - */ + // The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. EndCharacter uint32 `json:"endCharacter,omitempty"` - /** + /* * Describes the kind of the folding range such as `comment' or 'region'. The kind - * is used to categorize folding ranges and used by commands like 'Fold all comments'. See - * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + * is used to categorize folding ranges and used by commands like 'Fold all comments'. + * See [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. */ Kind string `json:"kind,omitempty"` + /* + * The text that the client should show when the specified range is + * collapsed. If not defined or not supported by the client, a default + * will be chosen by the client. + * + * @since 3.17.0 + */ + CollapsedText string `json:"collapsedText,omitempty"` } - -type FoldingRangeClientCapabilities struct { - /** - * Whether implementation supports dynamic registration for folding range providers. If this is set to `true` - * the client supports the new `FoldingRangeRegistrationOptions` return value for the corresponding server - * capability as well. +type FoldingRangeClientCapabilities struct { // line 12004 + /* + * Whether implementation supports dynamic registration for folding range + * providers. If this is set to `true` the client supports the new + * `FoldingRangeRegistrationOptions` return value for the corresponding + * server capability as well. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The maximum number of folding ranges that the client prefers to receive per document. The value serves as a - * hint, servers are free to follow the limit. + /* + * The maximum number of folding ranges that the client prefers to receive + * per document. The value serves as a hint, servers are free to follow the + * limit. */ RangeLimit uint32 `json:"rangeLimit,omitempty"` - /** - * If set, the client signals that it only supports folding complete lines. If set, client will - * ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange. + /* + * If set, the client signals that it only supports folding complete lines. + * If set, client will ignore specified `startCharacter` and `endCharacter` + * properties in a FoldingRange. */ LineFoldingOnly bool `json:"lineFoldingOnly,omitempty"` + /* + * Specific options for the folding range kind. + * + * @since 3.17.0 + */ + FoldingRangeKind PFoldingRangeKindPFoldingRange `json:"foldingRangeKind,omitempty"` + /* + * Specific options for the folding range. + * + * @since 3.17.0 + */ + FoldingRange PFoldingRangePFoldingRange `json:"foldingRange,omitempty"` } - -/** - * Enum of known range kinds - */ -type FoldingRangeKind string - -type FoldingRangeOptions struct { +type FoldingRangeKind string // line 12841 +type FoldingRangeOptions struct { // line 6486 WorkDoneProgressOptions } -/** - * Parameters for a [FoldingRangeRequest](#FoldingRangeRequest). - */ -type FoldingRangeParams struct { - /** - * The text document. - */ +// Parameters for a [FoldingRangeRequest](#FoldingRangeRequest). +type FoldingRangeParams struct { // line 2413 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } - -type FoldingRangeRegistrationOptions struct { +type FoldingRangeRegistrationOptions struct { // line 2496 TextDocumentRegistrationOptions FoldingRangeOptions StaticRegistrationOptions } -/** - * Value-object describing what options formatting should use. - */ -type FormattingOptions struct { - /** - * Size of a tab in spaces. - */ +// Value-object describing what options formatting should use. +type FormattingOptions struct { // line 9189 + // Size of a tab in spaces. TabSize uint32 `json:"tabSize"` - /** - * Prefer spaces over tabs. - */ + // Prefer spaces over tabs. InsertSpaces bool `json:"insertSpaces"` - /** - * Trim trailing whitespaces on a line. + /* + * Trim trailing whitespace on a line. * * @since 3.15.0 */ TrimTrailingWhitespace bool `json:"trimTrailingWhitespace,omitempty"` - /** + /* * Insert a newline character at the end of the file if one does not exist. * * @since 3.15.0 */ InsertFinalNewline bool `json:"insertFinalNewline,omitempty"` - /** + /* * Trim all newlines after the final newline at the end of the file. * * @since 3.15.0 @@ -2487,35 +2093,31 @@ type FormattingOptions struct { TrimFinalNewlines bool `json:"trimFinalNewlines,omitempty"` } -/** +/* * A diagnostic report with a full set of problems. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type FullDocumentDiagnosticReport = struct { - /** - * A full document diagnostic report. - */ +type FullDocumentDiagnosticReport struct { // line 7240 + // A full document diagnostic report. Kind string `json:"kind"` - /** + /* * An optional result id. If provided it will * be sent on the next diagnostic request for the * same document. */ ResultID string `json:"resultId,omitempty"` - /** - * The actual items. - */ + // The actual items. Items []Diagnostic `json:"items"` } -/** +/* * General client capabilities. * * @since 3.16.0 */ -type GeneralClientCapabilities struct { - /** +type GeneralClientCapabilities struct { // line 10690 + /* * Client capability that signals how the client * handles stale requests (e.g. a request * for which the client will not process the response @@ -2523,228 +2125,167 @@ type GeneralClientCapabilities struct { * * @since 3.17.0 */ - StaleRequestSupport struct { - /** - * The client will actively cancel the request. - */ - Cancel bool `json:"cancel"` - /** - * The list of requests for which the client - * will retry the request if it receives a - * response with error code `ContentModified` - */ - RetryOnContentModified []string `json:"retryOnContentModified"` - } `json:"staleRequestSupport,omitempty"` - /** + StaleRequestSupport PStaleRequestSupportPGeneral `json:"staleRequestSupport,omitempty"` + /* * Client capabilities specific to regular expressions. * * @since 3.16.0 */ RegularExpressions RegularExpressionsClientCapabilities `json:"regularExpressions,omitempty"` - /** + /* * Client capabilities specific to the client's markdown parser. * * @since 3.16.0 */ Markdown MarkdownClientCapabilities `json:"markdown,omitempty"` + /* + * The position encodings supported by the client. Client and server + * have to agree on the same position encoding to ensure that offsets + * (e.g. character position in a line) are interpreted the same on both + * sides. + * + * To keep the protocol backwards compatible the following applies: if + * the value 'utf-16' is missing from the array of position encodings + * servers can assume that the client supports UTF-16. UTF-16 is + * therefore a mandatory encoding. + * + * If omitted it defaults to ['utf-16']. + * + * Implementation considerations: since the conversion from one encoding + * into another requires the content of the file / line the conversion + * is best done where the file is read which is usually on the server + * side. + * + * @since 3.17.0 + */ + PositionEncodings []PositionEncodingKind `json:"positionEncodings,omitempty"` } -/** - * The result of a hover request. +/* + * The glob pattern. Either a string pattern or a relative pattern. + * + * @since 3.17.0 */ -type Hover struct { - /** - * The hover's content - */ - Contents MarkupContent/*MarkupContent | MarkedString | MarkedString[]*/ `json:"contents"` - /** - * An optional range +type GlobPattern = string // (alias) line 14136 +// The result of a hover request. +type Hover struct { // line 4907 + // The hover's content + Contents MarkupContent `json:"contents"` + /* + * An optional range inside the text document that is used to + * visualize the hover, e.g. by changing the background color. */ Range Range `json:"range,omitempty"` } - -type HoverClientCapabilities struct { - /** - * Whether hover supports dynamic registration. - */ +type HoverClientCapabilities struct { // line 11428 + // Whether hover supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Client supports the follow content formats for the content + /* + * Client supports the following content formats for the content * property. The order describes the preferred format of the client. */ ContentFormat []MarkupKind `json:"contentFormat,omitempty"` } -/** - * Hover options. - */ -type HoverOptions struct { +// Hover options. +type HoverOptions struct { // line 8796 WorkDoneProgressOptions } -/** - * Parameters for a [HoverRequest](#HoverRequest). - */ -type HoverParams struct { +// Parameters for a [HoverRequest](#HoverRequest). +type HoverParams struct { // line 4890 TextDocumentPositionParams WorkDoneProgressParams } -/** - * @since 3.6.0 - */ -type ImplementationClientCapabilities struct { - /** +// Registration options for a [HoverRequest](#HoverRequest). +type HoverRegistrationOptions struct { // line 4946 + TextDocumentRegistrationOptions + HoverOptions +} + +// @since 3.6.0 +type ImplementationClientCapabilities struct { // line 11609 + /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `ImplementationRegistrationOptions` return value * for the corresponding server capability as well. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** + /* * The client supports additional metadata in the form of definition links. * * @since 3.14.0 */ LinkSupport bool `json:"linkSupport,omitempty"` } - -type ImplementationOptions struct { +type ImplementationOptions struct { // line 6338 WorkDoneProgressOptions } - -type ImplementationParams struct { +type ImplementationParams struct { // line 2071 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } - -type ImplementationRegistrationOptions struct { +type ImplementationRegistrationOptions struct { // line 2111 TextDocumentRegistrationOptions ImplementationOptions StaticRegistrationOptions } -/** - * Known error codes for an `InitializeError`; +/* + * The data type of the ResponseError if the + * initialize request fails. */ -type InitializeError float64 - -type InitializeParams struct { - /** - * The process Id of the parent process that started - * the server. - */ - ProcessID int32/*integer | null*/ `json:"processId"` - /** - * Information about the client - * - * @since 3.15.0 - */ - ClientInfo struct { - /** - * The name of the client as defined by the client. - */ - Name string `json:"name"` - /** - * The client's version as defined by the client. - */ - Version string `json:"version,omitempty"` - } `json:"clientInfo,omitempty"` - /** - * The locale the client is currently showing the user interface - * in. This must not necessarily be the locale of the operating - * system. - * - * Uses IETF language tags as the value's syntax - * (See https://en.wikipedia.org/wiki/IETF_language_tag) - * - * @since 3.16.0 - */ - Locale string `json:"locale,omitempty"` - /** - * The rootPath of the workspace. Is null - * if no folder is open. - * - * @deprecated in favour of rootUri. - */ - RootPath string/*string | null*/ `json:"rootPath,omitempty"` - /** - * The rootUri of the workspace. Is null if no - * folder is open. If both `rootPath` and `rootUri` are set - * `rootUri` wins. - * - * @deprecated in favour of workspaceFolders. - */ - RootURI DocumentURI/*DocumentUri | null*/ `json:"rootUri"` - /** - * The capabilities provided by the client (editor or tool) - */ - Capabilities ClientCapabilities `json:"capabilities"` - /** - * User provided initialization options. +type InitializeError struct { // line 4148 + /* + * Indicates whether the client execute the following retry logic: + * (1) show the message provided by the ResponseError to the user + * (2) user selects retry or cancel + * (3) if user selected retry the initialize method is sent again. */ - InitializationOptions LSPAny `json:"initializationOptions,omitempty"` - /** - * The initial trace setting. If omitted trace is disabled ('off'). - */ - Trace string/* 'off' | 'messages' | 'compact' | 'verbose' */ `json:"trace,omitempty"` - /** - * The actual configured workspace folders. - */ - WorkspaceFolders []WorkspaceFolder/*WorkspaceFolder[] | null*/ `json:"workspaceFolders"` + Retry bool `json:"retry"` +} +type InitializeParams struct { // line 4090 + XInitializeParams + WorkspaceFoldersInitializeParams } -/** - * The result returned from an initialize request. - */ -type InitializeResult struct { - /** - * The capabilities the language server provides. - */ +// The result returned from an initialize request. +type InitializeResult struct { // line 4104 + // The capabilities the language server provides. Capabilities ServerCapabilities `json:"capabilities"` - /** + /* * Information about the server. * * @since 3.15.0 */ - ServerInfo struct { - /** - * The name of the server as defined by the server. - */ - Name string `json:"name"` - /** - * The server's version as defined by the server. - */ - Version string `json:"version,omitempty"` - } `json:"serverInfo,omitempty"` + ServerInfo PServerInfoMsg_initialize `json:"serverInfo,omitempty"` } - -type InitializedParams struct { +type InitializedParams struct { // line 4162 } -/** +/* * Inlay hint information. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlayHint = struct { - /** - * The position of this hint. - */ +type InlayHint struct { // line 3667 + // The position of this hint. Position *Position `json:"position"` - /** + /* * The label of this hint. A human readable string or an array of * InlayHintLabelPart label parts. * * *Note* that neither the string nor the label part can be empty. */ - Label []InlayHintLabelPart/*string | InlayHintLabelPart[]*/ `json:"label"` - /** + Label []InlayHintLabelPart `json:"label"` + /* * The kind of this hint. Can be omitted in which case the client * should fall back to a reasonable default. */ Kind InlayHintKind `json:"kind,omitempty"` - /** + /* * Optional text edits that are performed when accepting this inlay hint. * * *Note* that edits are expected to change the document so that the inlay @@ -2752,11 +2293,9 @@ type InlayHint = struct { * hint itself is now obsolete. */ TextEdits []TextEdit `json:"textEdits,omitempty"` - /** - * The tooltip text when you hover over this item. - */ - Tooltip string/*string | MarkupContent*/ `json:"tooltip,omitempty"` - /** + // The tooltip text when you hover over this item. + Tooltip OrPTooltip_textDocument_inlayHint `json:"tooltip,omitempty"` + /* * Render padding before the hint. * * Note: Padding should use the editor's background color, not the @@ -2764,7 +2303,7 @@ type InlayHint = struct { * to visually align/separate an inlay hint. */ PaddingLeft bool `json:"paddingLeft,omitempty"` - /** + /* * Render padding after the hint. * * Note: Padding should use the editor's background color, not the @@ -2772,55 +2311,44 @@ type InlayHint = struct { * to visually align/separate an inlay hint. */ PaddingRight bool `json:"paddingRight,omitempty"` + /* + * A data entry field that is preserved on an inlay hint between + * a `textDocument/inlayHint` and a `inlayHint/resolve` request. + */ + Data interface{} `json:"data,omitempty"` } -/** - * Inlay hint client capabilities +/* + * Inlay hint client capabilities. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlayHintClientCapabilities = struct { - /** - * Whether inlay hints support dynamic registration. - */ +type InlayHintClientCapabilities struct { // line 12395 + // Whether inlay hints support dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Indicates which properties a client can resolve lazily on a inlay + /* + * Indicates which properties a client can resolve lazily on an inlay * hint. */ - ResolveSupport struct { - /** - * The properties that a client can resolve lazily. - */ - Properties []string `json:"properties"` - } `json:"resolveSupport,omitempty"` + ResolveSupport PResolveSupportPInlayHint `json:"resolveSupport,omitempty"` } - -/** - * Inlay hint kinds. - * - * @since 3.17.0 - proposed state - */ -type InlayHintKind float64 - -/** +type InlayHintKind uint32 // line 13059 +/* * An inlay hint label part allows for interactive and composite labels * of inlay hints. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlayHintLabelPart = struct { - /** - * The value of this label part. - */ +type InlayHintLabelPart struct { // line 7067 + // The value of this label part. Value string `json:"value"` - /** + /* * The tooltip text when you hover over this label part. Depending on * the client capability `inlayHint.resolveSupport` clients might resolve * this property late using the resolve request. */ - Tooltip string/*string | MarkupContent*/ `json:"tooltip,omitempty"` - /** + Tooltip OrPTooltipPLabel `json:"tooltip,omitempty"` + /* * An optional source code location that represents this * label part. * @@ -2833,81 +2361,61 @@ type InlayHintLabelPart = struct { * Depending on the client capability `inlayHint.resolveSupport` clients * might resolve this property late using the resolve request. */ - Location *Location `json:"location,omitempty"` - /** + Location Location `json:"location,omitempty"` + /* * An optional command for this label part. * * Depending on the client capability `inlayHint.resolveSupport` clients * might resolve this property late using the resolve request. */ - Command *Command `json:"command,omitempty"` + Command Command `json:"command,omitempty"` } -/** +/* * Inlay hint options used during static registration. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlayHintOptions struct { - WorkDoneProgress bool `json:"workDoneProgress,omitempty"` - /** +type InlayHintOptions struct { // line 7140 + /* * The server provides support to resolve additional * information for an inlay hint item. */ ResolveProvider bool `json:"resolveProvider,omitempty"` + WorkDoneProgressOptions } -/** - * A parameter literal used in inlay hints requests. +/* + * A parameter literal used in inlay hint requests. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlayHintParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * The text document. - */ +type InlayHintParams struct { // line 3638 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The visible document range for which inlay hints should be computed. - */ + // The document range for which inlay hints should be computed. Range Range `json:"range"` + WorkDoneProgressParams } -/** +/* * Inlay hint options used during static or dynamic registration. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlayHintRegistrationOptions struct { - WorkDoneProgress bool `json:"workDoneProgress,omitempty"` - /** - * The server provides support to resolve additional - * information for an inlay hint item. - */ - ResolveProvider bool `json:"resolveProvider,omitempty"` - /** - * A document selector to identify the scope of the registration. If set to null - * the document selector provided on the client side will be used. - */ - DocumentSelector DocumentSelector/*DocumentSelector | null*/ `json:"documentSelector"` - /** - * The id used to register the request. The id can be used to deregister - * the request again. See also Registration#id. - */ - ID string `json:"id,omitempty"` +type InlayHintRegistrationOptions struct { // line 3768 + InlayHintOptions + TextDocumentRegistrationOptions + StaticRegistrationOptions } -/** +/* * Client workspace capabilities specific to inlay hints. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlayHintWorkspaceClientCapabilities = struct { - /** +type InlayHintWorkspaceClientCapabilities struct { // line 11121 + /* * Whether the client implementation supports a refresh request sent from * the server to the client. * @@ -2919,155 +2427,130 @@ type InlayHintWorkspaceClientCapabilities = struct { RefreshSupport bool `json:"refreshSupport,omitempty"` } -/** +/* * Inline value information can be provided by different means: * - directly as a text value (class InlineValueText). * - as a name to use for a variable lookup (class InlineValueVariableLookup) * - as an evaluatable expression (class InlineValueEvaluatableExpression) * The InlineValue types combines all inline value types into one type. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlineValue = interface{} /* InlineValueText | InlineValueVariableLookup | InlineValueEvaluatableExpression*/ - -/** +type InlineValue = Or_InlineValue // (alias) line 13887 +/* * Client capabilities specific to inline values. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlineValueClientCapabilities = struct { - /** - * Whether implementation supports dynamic registration for inline value providers. - */ +type InlineValueClientCapabilities struct { // line 12379 + // Whether implementation supports dynamic registration for inline value providers. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** - * @since 3.17.0 - proposed state - */ -type InlineValueContext = struct { - /** +// @since 3.17.0 +type InlineValueContext struct { // line 6953 + // The stack frame (as a DAP Id) where the execution has stopped. + FrameID int32 `json:"frameId"` + /* * The document range where execution has stopped. * Typically the end position of the range denotes the line where the inline values are shown. */ - StoppedLocation *Range `json:"stoppedLocation"` + StoppedLocation Range `json:"stoppedLocation"` } -/** +/* * Provide an inline value through an expression evaluation. * If only a range is specified, the expression will be extracted from the underlying document. * An optional expression can be used to override the extracted expression. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlineValueEvaluatableExpression = struct { - /** +type InlineValueEvaluatableExpression struct { // line 7031 + /* * The document range for which the inline value applies. * The range is used to extract the evaluatable expression from the underlying document. */ - Range *Range `json:"range"` - /** - * If specified the expression overrides the extracted expression. - */ + Range Range `json:"range"` + // If specified the expression overrides the extracted expression. Expression string `json:"expression,omitempty"` } -/** +/* * Inline value options used during static registration. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlineValueOptions = WorkDoneProgressOptions +type InlineValueOptions struct { // line 7055 + WorkDoneProgressOptions +} -/** +/* * A parameter literal used in inline value requests. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlineValueParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * The text document. - */ +type InlineValueParams struct { // line 3579 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The visible document range for which inline values should be computed. - */ + // The document range for which inline values should be computed. Range Range `json:"range"` - /** + /* * Additional information about the context in which inline values were * requested. */ Context InlineValueContext `json:"context"` + WorkDoneProgressParams } -/** +/* * Inline value options used during static or dynamic registration. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlineValueRegistrationOptions struct { - /** - * A document selector to identify the scope of the registration. If set to null - * the document selector provided on the client side will be used. - */ - DocumentSelector DocumentSelector/*DocumentSelector | null*/ `json:"documentSelector"` - /** - * The id used to register the request. The id can be used to deregister - * the request again. See also Registration#id. - */ - ID string `json:"id,omitempty"` +type InlineValueRegistrationOptions struct { // line 3616 + InlineValueOptions + TextDocumentRegistrationOptions + StaticRegistrationOptions } -/** +/* * Provide inline value as text. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlineValueText = struct { - /** - * The document range for which the inline value applies. - */ - Range *Range `json:"range"` - /** - * The text of the inline value. - */ +type InlineValueText struct { // line 6976 + // The document range for which the inline value applies. + Range Range `json:"range"` + // The text of the inline value. Text string `json:"text"` } -/** +/* * Provide inline value through a variable lookup. * If only a range is specified, the variable name will be extracted from the underlying document. * An optional variable name can be used to override the extracted name. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlineValueVariableLookup = struct { - /** +type InlineValueVariableLookup struct { // line 6999 + /* * The document range for which the inline value applies. * The range is used to extract the variable name from the underlying document. */ - Range *Range `json:"range"` - /** - * If specified the name of the variable to look up. - */ + Range Range `json:"range"` + // If specified the name of the variable to look up. VariableName string `json:"variableName,omitempty"` - /** - * How to perform the lookup. - */ + // How to perform the lookup. CaseSensitiveLookup bool `json:"caseSensitiveLookup"` } -/** +/* * Client workspace capabilities specific to inline values. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type InlineValueWorkspaceClientCapabilities = struct { - /** +type InlineValueWorkspaceClientCapabilities struct { // line 11105 + /* * Whether the client implementation supports a refresh request sent from the * server to the client. * @@ -3079,102 +2562,81 @@ type InlineValueWorkspaceClientCapabilities = struct { RefreshSupport bool `json:"refreshSupport,omitempty"` } -/** +/* * A special text edit to provide an insert and a replace operation. * * @since 3.16.0 */ -type InsertReplaceEdit struct { - /** - * The string to be inserted. - */ +type InsertReplaceEdit struct { // line 8696 + // The string to be inserted. NewText string `json:"newText"` - /** - * The range if the insert is requested - */ + // The range if the insert is requested Insert Range `json:"insert"` - /** - * The range if the replace is requested. - */ + // The range if the replace is requested. Replace Range `json:"replace"` } - -/** - * Defines whether the insert text in a completion item should be interpreted as - * plain text or a snippet. - */ -type InsertTextFormat float64 - -/** - * How whitespace and indentation is handled during completion - * item insertion. - * - * @since 3.16.0 - */ -type InsertTextMode float64 - -/** - * The LSP any type - * +type InsertTextFormat uint32 // line 13286 +type InsertTextMode uint32 // line 13306 +/* + * The LSP any type. + * Please note that strictly speaking a property with the value `undefined` + * can't be converted into JSON preserving the property name. However for + * convenience it is allowed and assumed that all these properties are + * optional as well. * @since 3.17.0 */ -type LSPAny = interface{} /* LSPObject | LSPArray | string | int32 | uint32 | Decimal | bool | float64*/ - -/** +type LSPAny = interface{} // (alias) line 13817 +/* * LSP arrays. - * * @since 3.17.0 */ -type LSPArray = []LSPAny - -/** +type LSPArray = []interface{} // (alias) line 13805 +type LSPErrorCodes int32 // line 12809 +/* * LSP object definition. - * * @since 3.17.0 */ -type LSPObject = map[string]interface{} /*[key: string]: LSPAny*/ +type LSPObject struct { // line 9618 +} -/** +/* * Client capabilities for the linked editing range request. * * @since 3.16.0 */ -type LinkedEditingRangeClientCapabilities struct { - /** +type LinkedEditingRangeClientCapabilities struct { // line 12331 + /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` * return value for the corresponding server capability as well. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } - -type LinkedEditingRangeOptions struct { +type LinkedEditingRangeOptions struct { // line 6657 WorkDoneProgressOptions } - -type LinkedEditingRangeParams struct { +type LinkedEditingRangeParams struct { // line 3134 TextDocumentPositionParams WorkDoneProgressParams } - -type LinkedEditingRangeRegistrationOptions struct { +type LinkedEditingRangeRegistrationOptions struct { // line 3177 TextDocumentRegistrationOptions LinkedEditingRangeOptions StaticRegistrationOptions } -/** +/* * The result of a linked editing range request. * * @since 3.16.0 */ -type LinkedEditingRanges struct { - /** +type LinkedEditingRanges struct { // line 3150 + /* * A list of ranges that can be edited together. The ranges must have * identical length and contain identical text content. The ranges cannot overlap. */ Ranges []Range `json:"ranges"` - /** + /* * An optional word pattern (regular expression) that describes valid contents for * the given ranges. If no pattern is provided, the client configuration's word * pattern will be used. @@ -3182,78 +2644,65 @@ type LinkedEditingRanges struct { WordPattern string `json:"wordPattern,omitempty"` } -/** +/* * Represents a location inside a resource, such as a line * inside a text file. */ -type Location struct { +type Location struct { // line 2091 URI DocumentURI `json:"uri"` Range Range `json:"range"` } -/** +/* * Represents the connection of two locations. Provides additional metadata over normal [locations](#Location), * including an origin range. */ -type LocationLink struct { - /** +type LocationLink struct { // line 6277 + /* * Span of the origin of this link. * - * Used as the underlined span for mouse definition hover. Defaults to the word range at + * Used as the underlined span for mouse interaction. Defaults to the word range at * the definition position. */ OriginSelectionRange Range `json:"originSelectionRange,omitempty"` - /** - * The target resource identifier of this link. - */ + // The target resource identifier of this link. TargetURI DocumentURI `json:"targetUri"` - /** + /* * The full target range of this link. If the target for example is a symbol then target range is the * range enclosing this symbol not including leading/trailing whitespace but everything else * like comments. This information is typically used to highlight the range in the editor. */ TargetRange Range `json:"targetRange"` - /** + /* * The range that should be selected and revealed when this link is being followed, e.g the name of a function. - * Must be contained by the the `targetRange`. See also `DocumentSymbol#range` + * Must be contained by the `targetRange`. See also `DocumentSymbol#range` */ TargetSelectionRange Range `json:"targetSelectionRange"` } -/** - * The log message parameters. - */ -type LogMessageParams struct { - /** - * The message type. See {@link MessageType} - */ +// The log message parameters. +type LogMessageParams struct { // line 4273 + // The message type. See {@link MessageType} Type MessageType `json:"type"` - /** - * The actual message - */ + // The actual message. Message string `json:"message"` } - -type LogTraceParams struct { +type LogTraceParams struct { // line 6178 Message string `json:"message"` Verbose string `json:"verbose,omitempty"` } -/** +/* * Client capabilities specific to the used markdown parser. * * @since 3.16.0 */ -type MarkdownClientCapabilities struct { - /** - * The name of the parser. - */ +type MarkdownClientCapabilities struct { // line 12550 + // The name of the parser. Parser string `json:"parser"` - /** - * The version of the parser. - */ + // The version of the parser. Version string `json:"version,omitempty"` - /** + /* * A list of HTML tags that the client allows / supports in * Markdown. * @@ -3262,7 +2711,7 @@ type MarkdownClientCapabilities struct { AllowedTags []string `json:"allowedTags,omitempty"` } -/** +/* * MarkedString can be used to render human readable text. It is either a markdown string * or a code-block that provides a language and a code snippet. The language identifier * is semantically equal to the optional language identifier in fenced code blocks in GitHub @@ -3276,9 +2725,8 @@ type MarkdownClientCapabilities struct { * Note that markdown strings will be sanitized - that means html will be escaped. * @deprecated use MarkupContent instead. */ -type MarkedString = string /*string | { language: string; value: string }*/ - -/** +type MarkedString = Or_MarkedString // (alias) line 14084 +/* * A `MarkupContent` literal represents a string value which content is interpreted base on its * kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. * @@ -3289,472 +2737,969 @@ type MarkedString = string /*string | { language: string; value: string }*/ * ```ts * let markdown: MarkdownContent = { * kind: MarkupKind.Markdown, - * value: [ - * '# Header', - * 'Some text', - * '```typescript', - * 'someCode();', - * '```' - * ].join('\n') + * value: [ + * '# Header', + * 'Some text', + * '```typescript', + * 'someCode();', + * '```' + * ].join('\ + * ') * }; * ``` * * *Please Note* that clients might sanitize the return markdown. A client could decide to * remove HTML from the markdown to avoid script execution. */ -type MarkupContent struct { - /** - * The type of the Markup - */ +type MarkupContent struct { // line 7118 + // The type of the Markup Kind MarkupKind `json:"kind"` - /** - * The content itself - */ + // The content itself Value string `json:"value"` } - -/** - * Describes the content type that a client supports in various - * result literals like `Hover`, `ParameterInfo` or `CompletionItem`. - * - * Please note that `MarkupKinds` must not start with a `$`. This kinds - * are reserved for internal usage. - */ -type MarkupKind string - -type MessageActionItem struct { - /** - * A short title like 'Retry', 'Open Log' etc. - */ +type MarkupKind string // line 13433 +type MessageActionItem struct { // line 4260 + // A short title like 'Retry', 'Open Log' etc. Title string `json:"title"` } - -/** - * The message type - */ -type MessageType float64 - -/** +type MessageType uint32 // line 13080 +/* * Moniker definition to match LSIF 0.5 moniker definition. * * @since 3.16.0 */ -type Moniker struct { - /** - * The scheme of the moniker. For example tsc or .Net - */ +type Moniker struct { // line 3360 + // The scheme of the moniker. For example tsc or .Net Scheme string `json:"scheme"` - /** + /* * The identifier of the moniker. The value is opaque in LSIF however * schema owners are allowed to define the structure if they want. */ Identifier string `json:"identifier"` - /** - * The scope in which the moniker is unique - */ + // The scope in which the moniker is unique Unique UniquenessLevel `json:"unique"` - /** - * The moniker kind if known. - */ + // The moniker kind if known. Kind MonikerKind `json:"kind,omitempty"` } -/** +/* * Client capabilities specific to the moniker request. * * @since 3.16.0 */ -type MonikerClientCapabilities struct { - /** +type MonikerClientCapabilities struct { // line 12347 + /* * Whether moniker supports dynamic registration. If this is set to `true` * the client supports the new `MonikerRegistrationOptions` return value * for the corresponding server capability as well. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } - -/** - * The moniker kind. - * - * @since 3.16.0 - */ -type MonikerKind string - -type MonikerOptions struct { +type MonikerKind string // line 13033 +type MonikerOptions struct { // line 6931 WorkDoneProgressOptions } - -type MonikerParams struct { +type MonikerParams struct { // line 3340 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } - -type MonikerRegistrationOptions struct { +type MonikerRegistrationOptions struct { // line 3400 TextDocumentRegistrationOptions MonikerOptions } -/** +// created for Literal +type Msg_MarkedString struct { // line 14093 + Language string `json:"Language"` + Value string `json:"Value"` +} + +// created for Literal +type Msg_NotebookDocumentFilter struct { // line 14268 + // The type of the enclosing notebook. + NotebookType string `json:"NotebookType"` + // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + Scheme string `json:"Scheme"` + // A glob pattern. + Pattern string `json:"Pattern"` +} + +// created for Literal +type Msg_PrepareRename2Gn struct { // line 13936 + Range Range `json:"Range"` + Placeholder string `json:"Placeholder"` +} + +// created for Literal +type Msg_TextDocumentContentChangeEvent struct { // line 14033 + // The range of the document that changed. + Range *Range `json:"Range"` + /* + * The optional length of the range that got replaced. + * + * @deprecated use range instead. + */ + RangeLength uint32 `json:"RangeLength"` + // The new text for the provided range. + Text string `json:"Text"` +} + +// created for Literal +type Msg_TextDocumentFilter struct { // line 14159 + // A language id, like `typescript`. + Language string `json:"Language"` + // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + Scheme string `json:"Scheme"` + // A glob pattern, like `*.{ts,js}`. + Pattern string `json:"Pattern"` +} + +// created for Literal +type Msg_XInitializeParams_clientInfo struct { // line 7678 + // The name of the client as defined by the client. + Name string `json:"Name"` + // The client's version as defined by the client. + Version string `json:"Version"` +} + +/* * A notebook cell. * * A cell's document URI must be unique across ALL notebook * cells and can therefore be used to uniquely identify a * notebook cell or the cell's text document. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type NotebookCell = struct { - /** - * The cell's kind - */ +type NotebookCell struct { // line 9624 + // The cell's kind Kind NotebookCellKind `json:"kind"` - /** + /* * The URI of the cell's text document * content. */ Document DocumentURI `json:"document"` - /** + /* * Additional metadata stored with the cell. + * + * Note: should always be an object literal (e.g. LSPObject) */ Metadata LSPObject `json:"metadata,omitempty"` - /** + /* * Additional execution summary information * if supported by the client. */ ExecutionSummary ExecutionSummary `json:"executionSummary,omitempty"` } -/** +/* * A change describing how to move a `NotebookCell` * array from state S to S'. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type NotebookCellArrayChange = struct { - /** - * The start oftest of the cell that changed. - */ +type NotebookCellArrayChange struct { // line 9665 + // The start oftest of the cell that changed. Start uint32 `json:"start"` - /** - * The deleted cells - */ + // The deleted cells DeleteCount uint32 `json:"deleteCount"` - /** - * The new cells, if any - */ + // The new cells, if any Cells []NotebookCell `json:"cells,omitempty"` } - -/** - * A notebook cell kind. - * - * @since 3.17.0 - proposed state - */ -type NotebookCellKind float64 - -/** +type NotebookCellKind uint32 // line 13674 +/* * A notebook cell text document filter denotes a cell text * document by different properties. * - * @since 3.17.0 - proposed state. + * @since 3.17.0 */ -type NotebookCellTextDocumentFilter = struct { - /** +type NotebookCellTextDocumentFilter struct { // line 10139 + /* * A filter that matches against the notebook - * containing the notebook cell. + * containing the notebook cell. If a string + * value is provided it matches against the + * notebook type. '*' matches every notebook. */ - NotebookDocument NotebookDocumentFilter `json:"notebookDocument"` - /** + Notebook NotebookDocumentFilter `json:"notebook"` + /* * A language id like `python`. * * Will be matched against the language id of the - * notebook cell document. + * notebook cell document. '*' matches every language. */ - CellLanguage string `json:"cellLanguage,omitempty"` + Language string `json:"language,omitempty"` } -/** +/* * A notebook document. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type NotebookDocument = struct { - /** - * The notebook document's uri. - */ +type NotebookDocument struct { // line 7359 + // The notebook document's uri. URI URI `json:"uri"` - /** - * The type of the notebook. - */ + // The type of the notebook. NotebookType string `json:"notebookType"` - /** + /* * The version number of this document (it will increase after each * change, including undo/redo). */ Version int32 `json:"version"` - /** + /* * Additional metadata stored with the notebook * document. + * + * Note: should always be an object literal (e.g. LSPObject) */ Metadata LSPObject `json:"metadata,omitempty"` - /** - * The cells of a notebook. - */ + // The cells of a notebook. Cells []NotebookCell `json:"cells"` } -/** +/* * A change event for a notebook document. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type NotebookDocumentChangeEvent = struct { - /** +type NotebookDocumentChangeEvent struct { // line 7471 + /* * The changed meta data if any. + * + * Note: should always be an object literal (e.g. LSPObject) */ Metadata LSPObject `json:"metadata,omitempty"` - /** - * Changes to cells - */ - Cells struct { - /** - * Changes to the cell structure to add or - * remove cells. - */ - Structure struct { - /** - * The change to the cell array. - */ - Array NotebookCellArrayChange `json:"array"` - /** - * Additional opened cell text documents. - */ - DidOpen []TextDocumentItem `json:"didOpen,omitempty"` - /** - * Additional closed cell text documents. - */ - DidClose []TextDocumentIdentifier `json:"didClose,omitempty"` - } `json:"structure,omitempty"` - /** - * Changes to notebook cells properties like its - * kind, execution summary or metadata. - */ - Data []NotebookCell `json:"data,omitempty"` - /** - * Changes to the text content of notebook cells. - */ - TextContent []struct { - Document VersionedTextDocumentIdentifier `json:"document"` - Changes []TextDocumentContentChangeEvent `json:"changes"` - } `json:"textContent,omitempty"` - } `json:"cells,omitempty"` -} - -/** - * A notebook document filter denotes a notebook document by - * different properties. + // Changes to cells + Cells PCellsPChange `json:"cells,omitempty"` +} + +/* + * Capabilities specific to the notebook document support. * - * @since 3.17.0 - proposed state. + * @since 3.17.0 */ -type NotebookDocumentFilter = struct { - /** The type of the enclosing notebook. */ - NotebookType string `json:"notebookType"` - /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. - * Will be matched against the URI of the notebook. */ - Scheme string `json:"scheme,omitempty"` - /** A glob pattern, like `*.ipynb`. - * Will be matched against the notebooks` URI path section.*/ - Pattern string `json:"pattern,omitempty"` +type NotebookDocumentClientCapabilities struct { // line 10639 + /* + * Capabilities specific to notebook document synchronization + * + * @since 3.17.0 + */ + Synchronization NotebookDocumentSyncClientCapabilities `json:"synchronization"` } -/** +/* + * A notebook document filter denotes a notebook document by + * different properties. The properties will be match + * against the notebook's URI (same as with documents) + * + * @since 3.17.0 + */ +type NotebookDocumentFilter = Msg_NotebookDocumentFilter // (alias) line 14263 +/* * A literal to identify a notebook document in the client. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type NotebookDocumentIdentifier = struct { - /** - * The notebook document's uri. - */ +type NotebookDocumentIdentifier struct { // line 7587 + // The notebook document's uri. URI URI `json:"uri"` } -/** - * A text document identifier to optionally denote a specific version of a text document. +/* + * Notebook specific client capabilities. + * + * @since 3.17.0 + */ +type NotebookDocumentSyncClientCapabilities struct { // line 12459 + /* + * Whether implementation supports dynamic registration. If this is + * set to `true` the client supports the new + * `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + * return value for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + // The client supports sending execution summary data per cell. + ExecutionSummarySupport bool `json:"executionSummarySupport,omitempty"` +} + +/* + * Options specific to a notebook plus its cells + * to be synced to the server. + * + * If a selector provides a notebook document + * filter but no cell selector all cells of a + * matching notebook document will be synced. + * + * If a selector provides no notebook document + * filter but only a cell selector all notebook + * document that contain at least one matching + * cell will be synced. + * + * @since 3.17.0 + */ +type NotebookDocumentSyncOptions struct { // line 9821 + // The notebooks to be synced + NotebookSelector []PNotebookSelectorPNotebookDocumentSync `json:"notebookSelector"` + /* + * Whether save notification should be forwarded to + * the server. Will only be honored if mode === `notebook`. + */ + Save bool `json:"save,omitempty"` +} + +/* + * Registration options specific to a notebook. + * + * @since 3.17.0 */ -type OptionalVersionedTextDocumentIdentifier struct { - /** +type NotebookDocumentSyncRegistrationOptions struct { // line 9941 + NotebookDocumentSyncOptions + StaticRegistrationOptions +} + +// A text document identifier to optionally denote a specific version of a text document. +type OptionalVersionedTextDocumentIdentifier struct { // line 9363 + /* * The version number of this document. If a versioned text document identifier * is sent from the server to the client and the file is not open in the editor * (the server has not received an open notification before) the server can send * `null` to indicate that the version is unknown and the content on disk is the * truth (as specified with document content ownership). */ - Version int32/*integer | null*/ `json:"version"` + Version int32 `json:"version"` TextDocumentIdentifier } -/** - * Represents a parameter of a callable-signature. A parameter can - * have a label and a doc-comment. - */ -type ParameterInformation struct { - /** - * The label of this parameter information. - * - * Either a string or an inclusive start and exclusive end offsets within its containing - * signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 - * string representation as `Position` and `Range` does. - * - * *Note*: a label of type string should be a substring of its containing signature label. - * Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. - */ - Label string/*string | [uinteger, uinteger]*/ `json:"label"` - /** - * The human-readable doc-comment of this signature. Will be shown - * in the UI but can be omitted. - */ - Documentation string/*string | MarkupContent*/ `json:"documentation,omitempty"` +// created for Or [Range FEditRangePItemDefaults] +type OrFEditRangePItemDefaults struct { // line 4791 + Value interface{} `json:"Value"` } -type PartialResultParams struct { - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` +// created for Or [string NotebookDocumentFilter] +type OrFNotebookPNotebookSelector struct { // line 9838 + Value interface{} `json:"Value"` } -/** - * Position in a text document expressed as zero-based line and character offset. - * The offsets are based on a UTF-16 string representation. So a string of the form - * `a𐐀b` the character offset of the character `a` is 0, the character offset of `𐐀` - * is 1 and the character offset of b is 3 since `𐐀` is represented using two code - * units in UTF-16. - * - * Positions are line end character agnostic. So you can not specify a position that - * denotes `\r|\n` or `\n|` where `|` represents the character offset. - */ -type Position struct { - /** - * Line position in a document (zero-based). - */ - Line uint32 `json:"line"` - /** - * Character offset on a line in a document (zero-based). Assuming that the line is - * represented as a string, the `character` value represents the gap between the - * `character` and `character + 1`. - * - * If the character value is greater than the line length it defaults back to the - * line length. - */ - Character uint32 `json:"character"` +// created for Or [Location PLocationMsg_workspace_symbol] +type OrPLocation_workspace_symbol struct { // line 5540 + Value interface{} `json:"Value"` } -type PrepareRenameParams struct { - TextDocumentPositionParams - WorkDoneProgressParams +// created for Or [string []string] +type OrPSection_workspace_didChangeConfiguration struct { // line 4186 + Value interface{} `json:"Value"` } -type PrepareSupportDefaultBehavior = interface{} +// created for Or [string MarkupContent] +type OrPTooltipPLabel struct { // line 7081 + Value interface{} `json:"Value"` +} -/** - * A previous result id in a workspace pull request. - * - * @since 3.17.0 - proposed state - */ -type PreviousResultID = struct { - /** - * The URI for which the client knowns a - * result id. +// created for Or [string MarkupContent] +type OrPTooltip_textDocument_inlayHint struct { // line 3722 + Value interface{} `json:"Value"` +} + +// created for Or [Location []Location] +type Or_Definition struct { // line 13780 + Value interface{} `json:"Value"` +} + +// created for Or [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport] +type Or_DocumentDiagnosticReport struct { // line 13912 + Value interface{} `json:"Value"` +} + +// created for Or [TextDocumentFilter NotebookCellTextDocumentFilter] +type Or_DocumentFilter struct { // line 14121 + Value interface{} `json:"Value"` +} + +// created for Or [InlineValueText InlineValueVariableLookup InlineValueEvaluatableExpression] +type Or_InlineValue struct { // line 13890 + Value interface{} `json:"Value"` +} + +// created for Or [string Msg_MarkedString] +type Or_MarkedString struct { // line 14087 + Value interface{} `json:"Value"` +} + +// created for Or [WorkspaceFolder URI] +type Or_RelativePattern_baseUri struct { // line 10768 + Value interface{} `json:"Value"` +} + +// created for Or [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport] +type Or_WorkspaceDocumentDiagnosticReport struct { // line 14013 + Value interface{} `json:"Value"` +} + +// created for Or [Declaration []DeclarationLink ] +type Or_textDocument_declaration struct { // line 257 + Value interface{} `json:"Value"` +} + +// created for Literal +type PCellsPChange struct { // line 7486 + /* + * Changes to the cell structure to add or + * remove cells. */ - URI DocumentURI `json:"uri"` - /** - * The value of the previous result id. + Structure FStructurePCells `json:"Structure"` + /* + * Changes to notebook cells properties like its + * kind, execution summary or metadata. */ - Value string `json:"value"` + Data []NotebookCell `json:"Data"` + // Changes to the text content of notebook cells. + TextContent []FTextContentPCells `json:"TextContent"` } -type ProgressParams struct { - /** - * The progress token provided by the client or server. +// created for Literal +type PChangeAnnotationSupportPWorkspaceEdit struct { // line 10842 + /* + * Whether the client groups edits with equal labels into tree nodes, + * for instance all edits labelled with \"Changes in Strings\" would + * be a tree node. */ - Token ProgressToken `json:"token"` - /** - * The progress data. + GroupsOnLabel bool `json:"GroupsOnLabel"` +} + +// created for Literal +type PCodeActionLiteralSupportPCodeAction struct { // line 11762 + /* + * The code action kind is support with the following value + * set. */ - Value interface{} `json:"value"` + CodeActionKind FCodeActionKindPCodeActionLiteralSupport `json:"CodeActionKind"` } -type ProgressToken = interface{} /*number | string*/ +// created for Literal +type PCompletionItemKindPCompletion struct { // line 11360 + /* + * The completion item kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + * + * If this property is not present the client only supports + * the completion items kinds from `Text` to `Reference` as defined in + * the initial version of the protocol. + */ + ValueSet []CompletionItemKind `json:"ValueSet"` +} -/** - * The publish diagnostic client capabilities. - */ -type PublishDiagnosticsClientCapabilities struct { - /** - * Whether the clients accepts diagnostics with related information. +// created for Literal +type PCompletionItemPCompletion struct { // line 11209 + /* + * Client supports snippets as insert text. + * + * A snippet can define tab stops and placeholders with `$1`, `$2` + * and `${3:foo}`. `$0` defines the final tab stop, it defaults to + * the end of the snippet. Placeholders with equal identifiers are linked, + * that is typing in one will update others too. */ - RelatedInformation bool `json:"relatedInformation,omitempty"` - /** - * Client supports the tag property to provide meta data about a diagnostic. - * Clients supporting tags have to handle unknown tags gracefully. + SnippetSupport bool `json:"SnippetSupport"` + // Client supports commit characters on a completion item. + CommitCharactersSupport bool `json:"CommitCharactersSupport"` + /* + * Client supports the following content formats for the documentation + * property. The order describes the preferred format of the client. + */ + DocumentationFormat []MarkupKind `json:"DocumentationFormat"` + // Client supports the deprecated property on a completion item. + DeprecatedSupport bool `json:"DeprecatedSupport"` + // Client supports the preselect property on a completion item. + PreselectSupport bool `json:"PreselectSupport"` + /* + * Client supports the tag property on a completion item. Clients supporting + * tags have to handle unknown tags gracefully. Clients especially need to + * preserve unknown tags when sending a completion item back to the server in + * a resolve call. * * @since 3.15.0 */ - TagSupport struct { - /** - * The tags supported by the client. - */ - ValueSet []DiagnosticTag `json:"valueSet"` - } `json:"tagSupport,omitempty"` - /** - * Whether the client interprets the version property of the - * `textDocument/publishDiagnostics` notification`s parameter. + TagSupport FTagSupportPCompletionItem `json:"TagSupport"` + /* + * Client support insert replace edit to control different behavior if a + * completion item is inserted in the text or should replace text. * - * @since 3.15.0 + * @since 3.16.0 */ - VersionSupport bool `json:"versionSupport,omitempty"` - /** - * Client supports a codeDescription property + InsertReplaceSupport bool `json:"InsertReplaceSupport"` + /* + * Indicates which properties a client can resolve lazily on a completion + * item. Before version 3.16.0 only the predefined properties `documentation` + * and `details` could be resolved lazily. * * @since 3.16.0 */ - CodeDescriptionSupport bool `json:"codeDescriptionSupport,omitempty"` - /** - * Whether code action supports the `data` property which is - * preserved between a `textDocument/publishDiagnostics` and - * `textDocument/codeAction` request. + ResolveSupport FResolveSupportPCompletionItem `json:"ResolveSupport"` + /* + * The client supports the `insertTextMode` property on + * a completion item to override the whitespace handling mode + * as defined by the client (see `insertTextMode`). * * @since 3.16.0 */ - DataSupport bool `json:"dataSupport,omitempty"` + InsertTextModeSupport FInsertTextModeSupportPCompletionItem `json:"InsertTextModeSupport"` + /* + * The client has support for completion item label + * details (see also `CompletionItemLabelDetails`). + * + * @since 3.17.0 + */ + LabelDetailsSupport bool `json:"LabelDetailsSupport"` } -/** - * The publish diagnostic notification's parameters. - */ -type PublishDiagnosticsParams struct { - /** - * The URI for which diagnostic information is reported. - */ - URI DocumentURI `json:"uri"` - /** - * Optional the version number of the document the diagnostics are published for. +// created for Literal +type PCompletionItemPCompletionProvider struct { // line 8767 + /* + * The server has support for completion item label + * details (see also `CompletionItemLabelDetails`) when + * receiving a completion item in a resolve call. * - * @since 3.15.0 - */ - Version int32 `json:"version,omitempty"` - /** - * An array of diagnostic information items. + * @since 3.17.0 */ - Diagnostics []Diagnostic `json:"diagnostics"` + LabelDetailsSupport bool `json:"LabelDetailsSupport"` } -/** - * A range in a text document expressed as (zero-based) start and end positions. - * +// created for Literal +type PCompletionListPCompletion struct { // line 11402 + /* + * The client supports the following itemDefaults on + * a completion list. + * + * The value lists the supported property names of the + * `CompletionList.itemDefaults` object. If omitted + * no properties are supported. + * + * @since 3.17.0 + */ + ItemDefaults []string `json:"ItemDefaults"` +} + +// created for Literal +type PDisabledMsg_textDocument_codeAction struct { // line 5446 + /* + * Human readable description of why the code action is currently disabled. + * + * This is displayed in the code actions UI. + */ + Reason string `json:"Reason"` +} + +// created for Literal +type PFoldingRangeKindPFoldingRange struct { // line 12037 + /* + * The folding range kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + */ + ValueSet []FoldingRangeKind `json:"ValueSet"` +} + +// created for Literal +type PFoldingRangePFoldingRange struct { // line 12062 + /* + * If set, the client signals that it supports setting collapsedText on + * folding ranges to display custom labels instead of the default text. + * + * @since 3.17.0 + */ + CollapsedText bool `json:"CollapsedText"` +} + +// created for Literal +type PFullESemanticTokensOptions struct { // line 6591 + // The server supports deltas for full documents. + Delta bool `json:"Delta"` +} + +// created for Literal +type PItemDefaultsMsg_textDocument_completion struct { // line 4772 + /* + * A default commit character set. + * + * @since 3.17.0 + */ + CommitCharacters []string `json:"CommitCharacters"` + /* + * A default edit range. + * + * @since 3.17.0 + */ + EditRange OrFEditRangePItemDefaults `json:"EditRange"` + /* + * A default insert text format. + * + * @since 3.17.0 + */ + InsertTextFormat InsertTextFormat `json:"InsertTextFormat"` + /* + * A default insert text mode. + * + * @since 3.17.0 + */ + InsertTextMode InsertTextMode `json:"InsertTextMode"` + /* + * A default data value. + * + * @since 3.17.0 + */ + Data interface{} `json:"Data"` +} + +// created for Literal +type PLocationMsg_workspace_symbol struct { // line 5546 + URI DocumentURI `json:"Uri"` +} + +// created for Literal +type PMessageActionItemPShowMessage struct { // line 12490 + /* + * Whether the client supports additional attributes which + * are preserved and send back to the server in the + * request's response. + */ + AdditionalPropertiesSupport bool `json:"AdditionalPropertiesSupport"` +} + +// created for Literal +type PNotebookSelectorPNotebookDocumentSync struct { // line 9831 + /* + * The notebook to be synced If a string + * value is provided it matches against the + * notebook type. '*' matches every notebook. + */ + Notebook OrFNotebookPNotebookSelector `json:"Notebook"` + // The cells of the matching notebook to be synced. + Cells []FCellsPNotebookSelector `json:"Cells"` +} + +// created for Literal +type PRangeESemanticTokensOptions struct { // line 6571 +} + +// created for Literal +type PRequestsPSemanticTokens struct { // line 12198 + /* + * The client will send the `textDocument/semanticTokens/range` request if + * the server provides a corresponding handler. + */ + Range bool `json:"Range"` + /* + * The client will send the `textDocument/semanticTokens/full` request if + * the server provides a corresponding handler. + */ + Full interface{} `json:"Full"` +} + +// created for Literal +type PResolveSupportPCodeAction struct { // line 11827 + // The properties that a client can resolve lazily. + Properties []string `json:"Properties"` +} + +// created for Literal +type PResolveSupportPInlayHint struct { // line 12410 + // The properties that a client can resolve lazily. + Properties []string `json:"Properties"` +} + +// created for Literal +type PResolveSupportPSymbol struct { // line 10964 + /* + * The properties that a client can resolve lazily. Usually + * `location.range` + */ + Properties []string `json:"Properties"` +} + +// created for Literal +type PServerInfoMsg_initialize struct { // line 4118 + // The name of the server as defined by the server. + Name string `json:"Name"` + // The server's version as defined by the server. + Version string `json:"Version"` +} + +// created for Literal +type PSignatureInformationPSignatureHelp struct { // line 11469 + /* + * Client supports the following content formats for the documentation + * property. The order describes the preferred format of the client. + */ + DocumentationFormat []MarkupKind `json:"DocumentationFormat"` + // Client capabilities specific to parameter information. + ParameterInformation FParameterInformationPSignatureInformation `json:"ParameterInformation"` + /* + * The client supports the `activeParameter` property on `SignatureInformation` + * literal. + * + * @since 3.16.0 + */ + ActiveParameterSupport bool `json:"ActiveParameterSupport"` +} + +// created for Literal +type PStaleRequestSupportPGeneral struct { // line 10696 + // The client will actively cancel the request. + Cancel bool `json:"Cancel"` + /* + * The list of requests for which the client + * will retry the request if it receives a + * response with error code `ContentModified` + */ + RetryOnContentModified []string `json:"RetryOnContentModified"` +} + +// created for Literal +type PSymbolKindPDocumentSymbol struct { // line 11680 + /* + * The symbol kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + * + * If this property is not present the client only supports + * the symbol kinds from `File` to `Array` as defined in + * the initial version of the protocol. + */ + ValueSet []SymbolKind `json:"ValueSet"` +} + +// created for Literal +type PSymbolKindPSymbol struct { // line 10916 + /* + * The symbol kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + * + * If this property is not present the client only supports + * the symbol kinds from `File` to `Array` as defined in + * the initial version of the protocol. + */ + ValueSet []SymbolKind `json:"ValueSet"` +} + +// created for Literal +type PTagSupportPDocumentSymbol struct { // line 11713 + // The tags supported by the client. + ValueSet []SymbolTag `json:"ValueSet"` +} + +// created for Literal +type PTagSupportPPublishDiagnostics struct { // line 12113 + // The tags supported by the client. + ValueSet []DiagnosticTag `json:"ValueSet"` +} + +// created for Literal +type PTagSupportPSymbol struct { // line 10940 + // The tags supported by the client. + ValueSet []SymbolTag `json:"ValueSet"` +} + +// The parameters of a configuration request. +type ParamConfiguration struct { // line 2207 + Items []ConfigurationItem `json:"items"` +} +type ParamInitialize struct { // line 4090 + XInitializeParams + WorkspaceFoldersInitializeParams +} + +/* + * Represents a parameter of a callable-signature. A parameter can + * have a label and a doc-comment. + */ +type ParameterInformation struct { // line 10089 + /* + * The label of this parameter information. + * + * Either a string or an inclusive start and exclusive end offsets within its containing + * signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 + * string representation as `Position` and `Range` does. + * + * *Note*: a label of type string should be a substring of its containing signature label. + * Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. + */ + Label string `json:"label"` + /* + * The human-readable doc-comment of this parameter. Will be shown + * in the UI but can be omitted. + */ + Documentation string `json:"documentation,omitempty"` +} +type PartialResultParams struct { // line 2223 + /* + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` +} + +/* + * The glob pattern to watch relative to the base path. Glob patterns can have the following syntax: + * - `*` to match one or more characters in a path segment + * - `?` to match on one character in a path segment + * - `**` to match any number of path segments, including none + * - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) + * - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) + * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) + * + * @since 3.17.0 + */ +type Pattern = string // (alias) line 14372 +/* + * Position in a text document expressed as zero-based line and character + * offset. Prior to 3.17 the offsets were always based on a UTF-16 string + * representation. So a string of the form `a𐐀b` the character offset of the + * character `a` is 0, the character offset of `𐐀` is 1 and the character + * offset of b is 3 since `𐐀` is represented using two code units in UTF-16. + * Since 3.17 clients and servers can agree on a different string encoding + * representation (e.g. UTF-8). The client announces it's supported encoding + * via the client capability [`general.positionEncodings`](#clientCapabilities). + * The value is an array of position encodings the client supports, with + * decreasing preference (e.g. the encoding at index `0` is the most preferred + * one). To stay backwards compatible the only mandatory encoding is UTF-16 + * represented via the string `utf-16`. The server can pick one of the + * encodings offered by the client and signals that encoding back to the + * client via the initialize result's property + * [`capabilities.positionEncoding`](#serverCapabilities). If the string value + * `utf-16` is missing from the client's capability `general.positionEncodings` + * servers can safely assume that the client supports UTF-16. If the server + * omits the position encoding in its initialize result the encoding defaults + * to the string value `utf-16`. Implementation considerations: since the + * conversion from one encoding into another requires the content of the + * file / line the conversion is best done where the file is read which is + * usually on the server side. + * + * Positions are line end character agnostic. So you can not specify a position + * that denotes `\\r|\ + * ` or `\ + * |` where `|` represents the character offset. + * + * @since 3.17.0 - support for negotiated position encoding. + */ +type Position struct { // line 6506 + /* + * Line position in a document (zero-based). + * + * If a line number is greater than the number of lines in a document, it defaults back to the number of lines in the document. + * If a line number is negative, it defaults to 0. + */ + Line uint32 `json:"line"` + /* + * Character offset on a line in a document (zero-based). + * + * The meaning of this offset is determined by the negotiated + * `PositionEncodingKind`. + * + * If the character value is greater than the line length it defaults back to the + * line length. + */ + Character uint32 `json:"character"` +} +type PositionEncodingKind string // line 13453 +type PrepareRename2Gn = Msg_PrepareRename2Gn // (alias) line 13927 +type PrepareRenameParams struct { // line 5944 + TextDocumentPositionParams + WorkDoneProgressParams +} +type PrepareRenameResult = Msg_PrepareRename2Gn // (alias) line 13927 +type PrepareSupportDefaultBehavior interface{} // line 13748 +/* + * A previous result id in a workspace pull request. + * + * @since 3.17.0 + */ +type PreviousResultID struct { // line 7336 + /* + * The URI for which the client knowns a + * result id. + */ + URI DocumentURI `json:"uri"` + // The value of the previous result id. + Value string `json:"value"` +} + +/* + * A previous result id in a workspace pull request. + * + * @since 3.17.0 + */ +type PreviousResultId struct { // line 7336 + /* + * The URI for which the client knowns a + * result id. + */ + URI DocumentURI `json:"uri"` + // The value of the previous result id. + Value string `json:"value"` +} +type ProgressParams struct { // line 6220 + // The progress token provided by the client or server. + Token ProgressToken `json:"token"` + // The progress data. + Value interface{} `json:"value"` +} +type ProgressToken = interface{} // (alias) line 13974 +// The publish diagnostic client capabilities. +type PublishDiagnosticsClientCapabilities struct { // line 12098 + // Whether the clients accepts diagnostics with related information. + RelatedInformation bool `json:"relatedInformation,omitempty"` + /* + * Client supports the tag property to provide meta data about a diagnostic. + * Clients supporting tags have to handle unknown tags gracefully. + * + * @since 3.15.0 + */ + TagSupport PTagSupportPPublishDiagnostics `json:"tagSupport,omitempty"` + /* + * Whether the client interprets the version property of the + * `textDocument/publishDiagnostics` notification's parameter. + * + * @since 3.15.0 + */ + VersionSupport bool `json:"versionSupport,omitempty"` + /* + * Client supports a codeDescription property + * + * @since 3.16.0 + */ + CodeDescriptionSupport bool `json:"codeDescriptionSupport,omitempty"` + /* + * Whether code action supports the `data` property which is + * preserved between a `textDocument/publishDiagnostics` and + * `textDocument/codeAction` request. + * + * @since 3.16.0 + */ + DataSupport bool `json:"dataSupport,omitempty"` +} + +// The publish diagnostic notification's parameters. +type PublishDiagnosticsParams struct { // line 4484 + // The URI for which diagnostic information is reported. + URI DocumentURI `json:"uri"` + /* + * Optional the version number of the document the diagnostics are published for. + * + * @since 3.15.0 + */ + Version int32 `json:"version,omitempty"` + // An array of diagnostic information items. + Diagnostics []Diagnostic `json:"diagnostics"` +} + +/* + * A range in a text document expressed as (zero-based) start and end positions. + * * If you want to specify a range that contains a line including the line ending * character(s) then use an end position denoting the start of the next line. * For example: @@ -3765,143 +3710,140 @@ type PublishDiagnosticsParams struct { * } * ``` */ -type Range struct { - /** - * The range's start position - */ +type Range struct { // line 6316 + // The range's start position. Start Position `json:"start"` - /** - * The range's end position. - */ + // The range's end position. End Position `json:"end"` } -/** - * Client Capabilities for a [ReferencesRequest](#ReferencesRequest). - */ -type ReferenceClientCapabilities struct { - /** - * Whether references supports dynamic registration. - */ +// Client Capabilities for a [ReferencesRequest](#ReferencesRequest). +type ReferenceClientCapabilities struct { // line 11635 + // Whether references supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** +/* * Value-object that contains additional information when * requesting references. */ -type ReferenceContext struct { - /** - * Include the declaration of the current symbol. - */ +type ReferenceContext struct { // line 8950 + // Include the declaration of the current symbol. IncludeDeclaration bool `json:"includeDeclaration"` } -/** - * Reference options. - */ -type ReferenceOptions struct { +// Reference options. +type ReferenceOptions struct { // line 8964 WorkDoneProgressOptions } -/** - * Parameters for a [ReferencesRequest](#ReferencesRequest). - */ -type ReferenceParams struct { +// Parameters for a [ReferencesRequest](#ReferencesRequest). +type ReferenceParams struct { // line 5075 Context ReferenceContext `json:"context"` TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -/** - * General parameters to to register for an notification or to register a provider. - */ -type Registration struct { - /** +// Registration options for a [ReferencesRequest](#ReferencesRequest). +type ReferenceRegistrationOptions struct { // line 5104 + TextDocumentRegistrationOptions + ReferenceOptions +} + +// General parameters to to register for an notification or to register a provider. +type Registration struct { // line 7602 + /* * The id used to register the request. The id can be used to deregister * the request again. */ ID string `json:"id"` - /** - * The method to register for. - */ + // The method / capability to register for. Method string `json:"method"` - /** - * Options necessary for the registration. - */ - RegisterOptions LSPAny `json:"registerOptions,omitempty"` + // Options necessary for the registration. + RegisterOptions interface{} `json:"registerOptions,omitempty"` } - -type RegistrationParams struct { +type RegistrationParams struct { // line 4060 Registrations []Registration `json:"registrations"` } -/** +/* * Client capabilities specific to regular expressions. * * @since 3.16.0 */ -type RegularExpressionsClientCapabilities struct { - /** - * The engine's name. - */ +type RegularExpressionsClientCapabilities struct { // line 12526 + // The engine's name. Engine string `json:"engine"` - /** - * The engine's version. - */ + // The engine's version. Version string `json:"version,omitempty"` } -/** +/* * A full diagnostic report with a set of related documents. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type RelatedFullDocumentDiagnosticReport struct { - /** +type RelatedFullDocumentDiagnosticReport struct { // line 7162 + /* * Diagnostics of related documents. This information is useful * in programming languages where code in a file A can generate * diagnostics in a file B which A depends on. An example of * such a language is C/C++ where marco definitions in a file * a.cpp and result in errors in a header file b.hpp. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ - RelatedDocuments map[string]interface{} /*[uri: string ** DocumentUri *]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport;*/ `json:"relatedDocuments,omitempty"` + RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments,omitempty"` + FullDocumentDiagnosticReport } -/** +/* * An unchanged diagnostic report with a set of related documents. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type RelatedUnchangedDocumentDiagnosticReport struct { - /** +type RelatedUnchangedDocumentDiagnosticReport struct { // line 7201 + /* * Diagnostics of related documents. This information is useful * in programming languages where code in a file A can generate * diagnostics in a file B which A depends on. An example of * such a language is C/C++ where marco definitions in a file * a.cpp and result in errors in a header file b.hpp. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ - RelatedDocuments map[string]interface{} /*[uri: string ** DocumentUri *]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport;*/ `json:"relatedDocuments,omitempty"` + RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments,omitempty"` + UnchangedDocumentDiagnosticReport } -type RenameClientCapabilities struct { - /** - * Whether rename supports dynamic registration. +/* + * A relative pattern is a helper to construct glob patterns that are matched + * relatively to a base URI. The common value for a `baseUri` is a workspace + * folder root, but it can be another absolute URI as well. + * + * @since 3.17.0 + */ +type RelativePattern struct { // line 10762 + /* + * A workspace folder or a base URI to which this pattern will be matched + * against relatively. */ + BaseURI Or_RelativePattern_baseUri `json:"baseUri"` + // The actual glob pattern; + Pattern Pattern `json:"pattern"` +} +type RenameClientCapabilities struct { // line 11960 + // Whether rename supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** + /* * Client supports testing for validity of rename operations * before execution. * * @since 3.12.0 */ PrepareSupport bool `json:"prepareSupport,omitempty"` - /** + /* * Client supports the default behavior result. * * The value indicates the default behavior used by the @@ -3909,9 +3851,9 @@ type RenameClientCapabilities struct { * * @since 3.16.0 */ - PrepareSupportDefaultBehavior PrepareSupportDefaultBehavior `json:"prepareSupportDefaultBehavior,omitempty"` - /** - * Whether th client honors the change annotations in + PrepareSupportDefaultBehavior interface{} `json:"prepareSupportDefaultBehavior,omitempty"` + /* + * Whether the client honors the change annotations in * text edits and resource operations returned via the * rename request's workspace edit by for example presenting * the workspace edit in the user interface and asking @@ -3922,61 +3864,44 @@ type RenameClientCapabilities struct { HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` } -/** - * Rename file operation - */ -type RenameFile struct { - /** - * A rename - */ +// Rename file operation +type RenameFile struct { // line 6754 + // A rename Kind string `json:"kind"` - /** - * The old (existing) location. - */ + // The old (existing) location. OldURI DocumentURI `json:"oldUri"` - /** - * The new location. - */ + // The new location. NewURI DocumentURI `json:"newUri"` - /** - * Rename options. - */ + // Rename options. Options RenameFileOptions `json:"options,omitempty"` ResourceOperation } -/** - * Rename file options - */ -type RenameFileOptions struct { - /** - * Overwrite target if existing. Overwrite wins over `ignoreIfExists` - */ +// Rename file options +type RenameFileOptions struct { // line 9461 + // Overwrite target if existing. Overwrite wins over `ignoreIfExists` Overwrite bool `json:"overwrite,omitempty"` - /** - * Ignores if target exists. - */ + // Ignores if target exists. IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` } -/** - * The parameters sent in file rename requests/notifications. +/* + * The parameters sent in notifications/requests for user-initiated renames of + * files. * * @since 3.16.0 */ -type RenameFilesParams struct { - /** +type RenameFilesParams struct { // line 3304 + /* * An array of all files/folders renamed in this operation. When a folder is renamed, only * the folder will be included, and not its children. */ Files []FileRename `json:"files"` } -/** - * Provider options for a [RenameRequest](#RenameRequest). - */ -type RenameOptions struct { - /** +// Provider options for a [RenameRequest](#RenameRequest). +type RenameOptions struct { // line 9289 + /* * Renames should be checked and tested before being executed. * * @since version 3.12.0 @@ -3985,19 +3910,13 @@ type RenameOptions struct { WorkDoneProgressOptions } -/** - * The parameters of a [RenameRequest](#RenameRequest). - */ -type RenameParams struct { - /** - * The document to rename. - */ +// The parameters of a [RenameRequest](#RenameRequest). +type RenameParams struct { // line 5893 + // The document to rename. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The position at which this request was sent. - */ + // The position at which this request was sent. Position Position `json:"position"` - /** + /* * The new name of the symbol. If the given name is not valid the * request must return a [ResponseError](#ResponseError) with an * appropriate message set. @@ -4006,112 +3925,90 @@ type RenameParams struct { WorkDoneProgressParams } -/** - * A generic resource operation. - */ -type ResourceOperation struct { - /** - * The resource operation kind. - */ +// Registration options for a [RenameRequest](#RenameRequest). +type RenameRegistrationOptions struct { // line 5929 + TextDocumentRegistrationOptions + RenameOptions +} + +// A generic resource operation. +type ResourceOperation struct { // line 9413 + // The resource operation kind. Kind string `json:"kind"` - /** + /* * An optional annotation identifier describing the operation. * * @since 3.16.0 */ AnnotationID ChangeAnnotationIdentifier `json:"annotationId,omitempty"` } - -type ResourceOperationKind string - -/** - * Save options. - */ -type SaveOptions struct { - /** - * The client is supposed to include the content on save. - */ +type ResourceOperationKind string // line 13695 +// Save options. +type SaveOptions struct { // line 8485 + // The client is supposed to include the content on save. IncludeText bool `json:"includeText,omitempty"` } -/** +/* * A selection range represents a part of a selection hierarchy. A selection range * may have a parent selection range that contains it. */ -type SelectionRange struct { - /** - * The [range](#Range) of this selection range. - */ +type SelectionRange struct { // line 2591 + // The [range](#Range) of this selection range. Range Range `json:"range"` - /** - * The parent selection range containing this range. Therefore `parent.range` must contain `this.range`. - */ + // The parent selection range containing this range. Therefore `parent.range` must contain `this.range`. Parent *SelectionRange `json:"parent,omitempty"` } - -type SelectionRangeClientCapabilities struct { - /** +type SelectionRangeClientCapabilities struct { // line 12084 + /* * Whether implementation supports dynamic registration for selection range providers. If this is set to `true` * the client supports the new `SelectionRangeRegistrationOptions` return value for the corresponding server * capability as well. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } - -type SelectionRangeOptions struct { +type SelectionRangeOptions struct { // line 6529 WorkDoneProgressOptions } -/** - * A parameter literal used in selection range requests. - */ -type SelectionRangeParams struct { - /** - * The text document. - */ +// A parameter literal used in selection range requests. +type SelectionRangeParams struct { // line 2556 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The positions inside the text document. - */ + // The positions inside the text document. Positions []Position `json:"positions"` WorkDoneProgressParams PartialResultParams } - -type SelectionRangeRegistrationOptions struct { +type SelectionRangeRegistrationOptions struct { // line 2614 SelectionRangeOptions TextDocumentRegistrationOptions StaticRegistrationOptions } - -/** - * @since 3.16.0 - */ -type SemanticTokens struct { - /** +type SemanticTokenModifiers string // line 12696 +type SemanticTokenTypes string // line 12589 +// @since 3.16.0 +type SemanticTokens struct { // line 2902 + /* * An optional result id. If provided and clients support delta updating * the client will include the result id in the next semantic token request. * A server can then instead of computing all semantic tokens again simply * send a delta. */ ResultID string `json:"resultId,omitempty"` - /** - * The actual tokens. - */ + // The actual tokens. Data []uint32 `json:"data"` } -/** - * @since 3.16.0 - */ -type SemanticTokensClientCapabilities struct { - /** +// @since 3.16.0 +type SemanticTokensClientCapabilities struct { // line 12183 + /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` * return value for the corresponding server capability as well. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** + /* * Which requests the client supports and might send to the server * depending on the server's capability. Please note that clients might not * show semantic tokens or degrade some of the user experience if a range @@ -4121,39 +4018,18 @@ type SemanticTokensClientCapabilities struct { * range provider the client might not render a minimap correctly or might * even decide to not show any semantic tokens at all. */ - Requests struct { - /** - * The client will send the `textDocument/semanticTokens/range` request if - * the server provides a corresponding handler. - */ - Range bool/*boolean | { }*/ `json:"range,omitempty"` - /** - * The client will send the `textDocument/semanticTokens/full` request if - * the server provides a corresponding handler. - */ - Full interface{}/*boolean | */ `json:"full,omitempty"` - } `json:"requests"` - /** - * The token types that the client supports. - */ + Requests PRequestsPSemanticTokens `json:"requests"` + // The token types that the client supports. TokenTypes []string `json:"tokenTypes"` - /** - * The token modifiers that the client supports. - */ + // The token modifiers that the client supports. TokenModifiers []string `json:"tokenModifiers"` - /** - * The token formats the clients supports. - */ - Formats []TokenFormat `json:"formats"` - /** - * Whether the client supports tokens that can overlap each other. - */ + // The token formats the clients supports. + Formats []string `json:"formats"` + // Whether the client supports tokens that can overlap each other. OverlappingTokenSupport bool `json:"overlappingTokenSupport,omitempty"` - /** - * Whether the client supports tokens that can span multiple lines. - */ + // Whether the client supports tokens that can span multiple lines. MultilineTokenSupport bool `json:"multilineTokenSupport,omitempty"` - /** + /* * Whether the client allows the server to actively cancel a * semantic token request, e.g. supports returning * LSPErrorCodes.ServerCancelled. If a server does the client @@ -4162,7 +4038,7 @@ type SemanticTokensClientCapabilities struct { * @since 3.17.0 */ ServerCancelSupport bool `json:"serverCancelSupport,omitempty"` - /** + /* * Whether the client uses semantic tokens to augment existing * syntax tokens. If set to `true` client side created syntax * tokens and semantic tokens are both used for colorization. If @@ -4177,26 +4053,18 @@ type SemanticTokensClientCapabilities struct { AugmentsSyntaxTokens bool `json:"augmentsSyntaxTokens,omitempty"` } -/** - * @since 3.16.0 - */ -type SemanticTokensDelta struct { +// @since 3.16.0 +type SemanticTokensDelta struct { // line 3001 ResultID string `json:"resultId,omitempty"` - /** - * The semantic token edits to transform a previous result into a new result. - */ + // The semantic token edits to transform a previous result into a new result. Edits []SemanticTokensEdit `json:"edits"` } -/** - * @since 3.16.0 - */ -type SemanticTokensDeltaParams struct { - /** - * The text document. - */ +// @since 3.16.0 +type SemanticTokensDeltaParams struct { // line 2968 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** + /* * The result id of a previous response. The result Id can either point to a full response * or a delta response depending on what was received last. */ @@ -4205,100 +4073,76 @@ type SemanticTokensDeltaParams struct { PartialResultParams } -/** - * @since 3.16.0 - */ -type SemanticTokensEdit struct { - /** - * The start offset of the edit. - */ +// @since 3.16.0 +type SemanticTokensDeltaPartialResult struct { // line 3027 + Edits []SemanticTokensEdit `json:"edits"` +} + +// @since 3.16.0 +type SemanticTokensEdit struct { // line 6622 + // The start offset of the edit. Start uint32 `json:"start"` - /** - * The count of elements to remove. - */ + // The count of elements to remove. DeleteCount uint32 `json:"deleteCount"` - /** - * The elements to insert. - */ + // The elements to insert. Data []uint32 `json:"data,omitempty"` } -/** - * @since 3.16.0 - */ -type SemanticTokensLegend struct { - /** - * The token types a server uses. - */ +// @since 3.16.0 +type SemanticTokensLegend struct { // line 9334 + // The token types a server uses. TokenTypes []string `json:"tokenTypes"` - /** - * The token modifiers a server uses. - */ + // The token modifiers a server uses. TokenModifiers []string `json:"tokenModifiers"` } -/** - * @since 3.16.0 - */ -type SemanticTokensOptions struct { - /** - * The legend used by the server - */ +// @since 3.16.0 +type SemanticTokensOptions struct { // line 6551 + // The legend used by the server Legend SemanticTokensLegend `json:"legend"` - /** + /* * Server supports providing semantic tokens for a specific range * of a document. */ - Range bool/*boolean | { }*/ `json:"range,omitempty"` - /** - * Server supports providing semantic tokens for a full document. - */ - Full interface{}/*boolean | */ `json:"full,omitempty"` + Range interface{} `json:"range,omitempty"` + // Server supports providing semantic tokens for a full document. + Full bool `json:"full,omitempty"` WorkDoneProgressOptions } -/** - * @since 3.16.0 - */ -type SemanticTokensParams struct { - /** - * The text document. - */ +// @since 3.16.0 +type SemanticTokensParams struct { // line 2877 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } -/** - * @since 3.16.0 - */ -type SemanticTokensRangeParams struct { - /** - * The text document. - */ +// @since 3.16.0 +type SemanticTokensPartialResult struct { // line 2929 + Data []uint32 `json:"data"` +} + +// @since 3.16.0 +type SemanticTokensRangeParams struct { // line 3044 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The range the semantic tokens are requested for. - */ + // The range the semantic tokens are requested for. Range Range `json:"range"` WorkDoneProgressParams PartialResultParams } -/** - * @since 3.16.0 - */ -type SemanticTokensRegistrationOptions struct { +// @since 3.16.0 +type SemanticTokensRegistrationOptions struct { // line 2946 TextDocumentRegistrationOptions SemanticTokensOptions StaticRegistrationOptions } -/** - * @since 3.16.0 - */ -type SemanticTokensWorkspaceClientCapabilities struct { - /** +// @since 3.16.0 +type SemanticTokensWorkspaceClientCapabilities struct { // line 11003 + /* * Whether the client implementation supports a refresh request sent from * the server to the client. * @@ -4310,197 +4154,179 @@ type SemanticTokensWorkspaceClientCapabilities struct { RefreshSupport bool `json:"refreshSupport,omitempty"` } -type ServerCapabilities struct { - /** - * Defines how text documents are synced. Is either a detailed structure defining each notification or - * for backwards compatibility the TextDocumentSyncKind number. - */ - TextDocumentSync interface{}/*TextDocumentSyncOptions | TextDocumentSyncKind*/ `json:"textDocumentSync,omitempty"` - /** - * The server provides completion support. +/* + * Defines the capabilities provided by a language + * server. + */ +type ServerCapabilities struct { // line 7829 + /* + * The position encoding the server picked from the encodings offered + * by the client via the client capability `general.positionEncodings`. + * + * If the client didn't provide any position encodings the only valid + * value that a server can return is 'utf-16'. + * + * If omitted it defaults to 'utf-16'. + * + * @since 3.17.0 */ - CompletionProvider CompletionOptions `json:"completionProvider,omitempty"` - /** - * The server provides hover support. + PositionEncoding PositionEncodingKind `json:"positionEncoding,omitempty"` + /* + * Defines how text documents are synced. Is either a detailed structure + * defining each notification or for backwards compatibility the + * TextDocumentSyncKind number. */ - HoverProvider bool/*boolean | HoverOptions*/ `json:"hoverProvider,omitempty"` - /** - * The server provides signature help support. + TextDocumentSync interface{} `json:"textDocumentSync,omitempty"` + /* + * Defines how notebook documents are synced. + * + * @since 3.17.0 */ + NotebookDocumentSync interface{} `json:"notebookDocumentSync,omitempty"` + // The server provides completion support. + CompletionProvider CompletionOptions `json:"completionProvider,omitempty"` + // The server provides hover support. + HoverProvider bool `json:"hoverProvider,omitempty"` + // The server provides signature help support. SignatureHelpProvider SignatureHelpOptions `json:"signatureHelpProvider,omitempty"` - /** - * The server provides Goto Declaration support. - */ - DeclarationProvider interface{}/* bool | DeclarationOptions | DeclarationRegistrationOptions*/ `json:"declarationProvider,omitempty"` - /** - * The server provides goto definition support. - */ - DefinitionProvider bool/*boolean | DefinitionOptions*/ `json:"definitionProvider,omitempty"` - /** - * The server provides Goto Type Definition support. - */ - TypeDefinitionProvider interface{}/* bool | TypeDefinitionOptions | TypeDefinitionRegistrationOptions*/ `json:"typeDefinitionProvider,omitempty"` - /** - * The server provides Goto Implementation support. - */ - ImplementationProvider interface{}/* bool | ImplementationOptions | ImplementationRegistrationOptions*/ `json:"implementationProvider,omitempty"` - /** - * The server provides find references support. - */ - ReferencesProvider bool/*boolean | ReferenceOptions*/ `json:"referencesProvider,omitempty"` - /** - * The server provides document highlight support. - */ - DocumentHighlightProvider bool/*boolean | DocumentHighlightOptions*/ `json:"documentHighlightProvider,omitempty"` - /** - * The server provides document symbol support. - */ - DocumentSymbolProvider bool/*boolean | DocumentSymbolOptions*/ `json:"documentSymbolProvider,omitempty"` - /** + // The server provides Goto Declaration support. + DeclarationProvider bool `json:"declarationProvider,omitempty"` + // The server provides goto definition support. + DefinitionProvider bool `json:"definitionProvider,omitempty"` + // The server provides Goto Type Definition support. + TypeDefinitionProvider interface{} `json:"typeDefinitionProvider,omitempty"` + // The server provides Goto Implementation support. + ImplementationProvider interface{} `json:"implementationProvider,omitempty"` + // The server provides find references support. + ReferencesProvider bool `json:"referencesProvider,omitempty"` + // The server provides document highlight support. + DocumentHighlightProvider bool `json:"documentHighlightProvider,omitempty"` + // The server provides document symbol support. + DocumentSymbolProvider bool `json:"documentSymbolProvider,omitempty"` + /* * The server provides code actions. CodeActionOptions may only be * specified if the client states that it supports * `codeActionLiteralSupport` in its initial `initialize` request. */ - CodeActionProvider interface{}/*boolean | CodeActionOptions*/ `json:"codeActionProvider,omitempty"` - /** - * The server provides code lens. - */ + CodeActionProvider interface{} `json:"codeActionProvider,omitempty"` + // The server provides code lens. CodeLensProvider CodeLensOptions `json:"codeLensProvider,omitempty"` - /** - * The server provides document link support. - */ + // The server provides document link support. DocumentLinkProvider DocumentLinkOptions `json:"documentLinkProvider,omitempty"` - /** - * The server provides color provider support. - */ - ColorProvider interface{}/* bool | DocumentColorOptions | DocumentColorRegistrationOptions*/ `json:"colorProvider,omitempty"` - /** - * The server provides workspace symbol support. - */ - WorkspaceSymbolProvider bool/*boolean | WorkspaceSymbolOptions*/ `json:"workspaceSymbolProvider,omitempty"` - /** - * The server provides document formatting. - */ - DocumentFormattingProvider bool/*boolean | DocumentFormattingOptions*/ `json:"documentFormattingProvider,omitempty"` - /** - * The server provides document range formatting. - */ - DocumentRangeFormattingProvider bool/*boolean | DocumentRangeFormattingOptions*/ `json:"documentRangeFormattingProvider,omitempty"` - /** - * The server provides document formatting on typing. - */ + // The server provides color provider support. + ColorProvider interface{} `json:"colorProvider,omitempty"` + // The server provides workspace symbol support. + WorkspaceSymbolProvider bool `json:"workspaceSymbolProvider,omitempty"` + // The server provides document formatting. + DocumentFormattingProvider bool `json:"documentFormattingProvider,omitempty"` + // The server provides document range formatting. + DocumentRangeFormattingProvider bool `json:"documentRangeFormattingProvider,omitempty"` + // The server provides document formatting on typing. DocumentOnTypeFormattingProvider DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"` - /** + /* * The server provides rename support. RenameOptions may only be * specified if the client states that it supports * `prepareSupport` in its initial `initialize` request. */ - RenameProvider interface{}/*boolean | RenameOptions*/ `json:"renameProvider,omitempty"` - /** - * The server provides folding provider support. - */ - FoldingRangeProvider interface{}/* bool | FoldingRangeOptions | FoldingRangeRegistrationOptions*/ `json:"foldingRangeProvider,omitempty"` - /** - * The server provides selection range support. - */ - SelectionRangeProvider interface{}/* bool | SelectionRangeOptions | SelectionRangeRegistrationOptions*/ `json:"selectionRangeProvider,omitempty"` - /** - * The server provides execute command support. - */ + RenameProvider interface{} `json:"renameProvider,omitempty"` + // The server provides folding provider support. + FoldingRangeProvider interface{} `json:"foldingRangeProvider,omitempty"` + // The server provides selection range support. + SelectionRangeProvider interface{} `json:"selectionRangeProvider,omitempty"` + // The server provides execute command support. ExecuteCommandProvider ExecuteCommandOptions `json:"executeCommandProvider,omitempty"` - /** + /* * The server provides call hierarchy support. * * @since 3.16.0 */ - CallHierarchyProvider interface{}/* bool | CallHierarchyOptions | CallHierarchyRegistrationOptions*/ `json:"callHierarchyProvider,omitempty"` - /** + CallHierarchyProvider interface{} `json:"callHierarchyProvider,omitempty"` + /* * The server provides linked editing range support. * * @since 3.16.0 */ - LinkedEditingRangeProvider interface{}/* bool | LinkedEditingRangeOptions | LinkedEditingRangeRegistrationOptions*/ `json:"linkedEditingRangeProvider,omitempty"` - /** + LinkedEditingRangeProvider interface{} `json:"linkedEditingRangeProvider,omitempty"` + /* * The server provides semantic tokens support. * * @since 3.16.0 */ - SemanticTokensProvider interface{}/*SemanticTokensOptions | SemanticTokensRegistrationOptions*/ `json:"semanticTokensProvider,omitempty"` - /** - * The workspace server capabilities - */ - Workspace Workspace6Gn `json:"workspace,omitempty"` - /** + SemanticTokensProvider interface{} `json:"semanticTokensProvider,omitempty"` + /* * The server provides moniker support. * * @since 3.16.0 */ - MonikerProvider interface{}/* bool | MonikerOptions | MonikerRegistrationOptions*/ `json:"monikerProvider,omitempty"` - /** + MonikerProvider interface{} `json:"monikerProvider,omitempty"` + /* * The server provides type hierarchy support. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ - TypeHierarchyProvider interface{}/* bool | TypeHierarchyOptions | TypeHierarchyRegistrationOptions*/ `json:"typeHierarchyProvider,omitempty"` - /** + TypeHierarchyProvider interface{} `json:"typeHierarchyProvider,omitempty"` + /* * The server provides inline values. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ - InlineValueProvider interface{}/* bool | InlineValueOptions | InlineValueRegistrationOptions*/ `json:"inlineValueProvider,omitempty"` - /** + InlineValueProvider interface{} `json:"inlineValueProvider,omitempty"` + /* * The server provides inlay hints. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ - InlayHintProvider interface{}/* bool | InlayHintOptions | InlayHintRegistrationOptions*/ `json:"inlayHintProvider,omitempty"` - /** - * Experimental server capabilities. + InlayHintProvider interface{} `json:"inlayHintProvider,omitempty"` + /* + * The server has support for pull model diagnostics. + * + * @since 3.17.0 */ + DiagnosticProvider interface{} `json:"diagnosticProvider,omitempty"` + // Workspace specific server capabilities. + Workspace Workspace6Gn `json:"workspace,omitempty"` + // Experimental server capabilities. Experimental interface{} `json:"experimental,omitempty"` } - -type SetTraceParams struct { +type SetTraceParams struct { // line 6166 Value TraceValues `json:"value"` } -/** - * Client capabilities for the show document request. +/* + * Client capabilities for the showDocument request. * * @since 3.16.0 */ -type ShowDocumentClientCapabilities struct { - /** - * The client has support for the show document +type ShowDocumentClientCapabilities struct { // line 12511 + /* + * The client has support for the showDocument * request. */ Support bool `json:"support"` } -/** +/* * Params to show a document. * * @since 3.16.0 */ -type ShowDocumentParams struct { - /** - * The document uri to show. - */ +type ShowDocumentParams struct { // line 3077 + // The document uri to show. URI URI `json:"uri"` - /** + /* * Indicates to show the resource in an external program. * To show for example `https://code.visualstudio.com/` * in the default WEB browser set `external` to `true`. */ External bool `json:"external,omitempty"` - /** + /* * An optional property to indicate whether the editor * showing the document should take focus or not. * Clients might ignore this property if an external - * program in started. + * program is started. */ TakeFocus bool `json:"takeFocus,omitempty"` - /** + /* * An optional selection range if the document is a text * document. Clients might ignore the property if an * external program is started or the file is not a text @@ -4509,75 +4335,47 @@ type ShowDocumentParams struct { Selection Range `json:"selection,omitempty"` } -/** - * The result of an show document request. +/* + * The result of a showDocument request. * * @since 3.16.0 */ -type ShowDocumentResult struct { - /** - * A boolean indicating if the show was successful. - */ +type ShowDocumentResult struct { // line 3119 + // A boolean indicating if the show was successful. Success bool `json:"success"` } -/** - * The parameters of a notification message. - */ -type ShowMessageParams struct { - /** - * The message type. See {@link MessageType} - */ +// The parameters of a notification message. +type ShowMessageParams struct { // line 4205 + // The message type. See {@link MessageType} Type MessageType `json:"type"` - /** - * The actual message - */ + // The actual message. Message string `json:"message"` } -/** - * Show message request client capabilities - */ -type ShowMessageRequestClientCapabilities struct { - /** - * Capabilities specific to the `MessageActionItem` type. - */ - MessageActionItem struct { - /** - * Whether the client supports additional attributes which - * are preserved and send back to the server in the - * request's response. - */ - AdditionalPropertiesSupport bool `json:"additionalPropertiesSupport,omitempty"` - } `json:"messageActionItem,omitempty"` +// Show message request client capabilities +type ShowMessageRequestClientCapabilities struct { // line 12484 + // Capabilities specific to the `MessageActionItem` type. + MessageActionItem PMessageActionItemPShowMessage `json:"messageActionItem,omitempty"` } - -type ShowMessageRequestParams struct { - /** - * The message type. See {@link MessageType} - */ +type ShowMessageRequestParams struct { // line 4227 + // The message type. See {@link MessageType} Type MessageType `json:"type"` - /** - * The actual message - */ + // The actual message. Message string `json:"message"` - /** - * The message action items to present. - */ + // The message action items to present. Actions []MessageActionItem `json:"actions,omitempty"` } -/** +/* * Signature help represents the signature of something * callable. There can be multiple signature but only one * active and only one active parameter. */ -type SignatureHelp struct { - /** - * One or more signatures. - */ +type SignatureHelp struct { // line 4989 + // One or more signatures. Signatures []SignatureInformation `json:"signatures"` - /** + /* * The active signature. If omitted or the value lies outside the * range of `signatures` the value defaults to zero or is ignored if * the `SignatureHelp` has no signatures. @@ -4589,7 +4387,7 @@ type SignatureHelp struct { * mandatory to better express this. */ ActiveSignature uint32 `json:"activeSignature,omitempty"` - /** + /* * The active parameter of the active signature. If omitted or the value * lies outside the range of `signatures[activeSignature].parameters` * defaults to 0 if the active signature has parameters. If @@ -4601,45 +4399,16 @@ type SignatureHelp struct { ActiveParameter uint32 `json:"activeParameter,omitempty"` } -/** - * Client Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). - */ -type SignatureHelpClientCapabilities struct { - /** - * Whether signature help supports dynamic registration. - */ +// Client Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). +type SignatureHelpClientCapabilities struct { // line 11454 + // Whether signature help supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** + /* * The client supports the following `SignatureInformation` * specific properties. */ - SignatureInformation struct { - /** - * Client supports the follow content formats for the documentation - * property. The order describes the preferred format of the client. - */ - DocumentationFormat []MarkupKind `json:"documentationFormat,omitempty"` - /** - * Client capabilities specific to parameter information. - */ - ParameterInformation struct { - /** - * The client supports processing label offsets instead of a - * simple label string. - * - * @since 3.14.0 - */ - LabelOffsetSupport bool `json:"labelOffsetSupport,omitempty"` - } `json:"parameterInformation,omitempty"` - /** - * The client support the `activeParameter` property on `SignatureInformation` - * literal. - * - * @since 3.16.0 - */ - ActiveParameterSupport bool `json:"activeParameterSupport,omitempty"` - } `json:"signatureInformation,omitempty"` - /** + SignatureInformation PSignatureInformationPSignatureHelp `json:"signatureInformation,omitempty"` + /* * The client supports to send additional context information for a * `textDocument/signatureHelp` request. A client that opts into * contextSupport will also support the `retriggerCharacters` on @@ -4650,30 +4419,28 @@ type SignatureHelpClientCapabilities struct { ContextSupport bool `json:"contextSupport,omitempty"` } -/** +/* * Additional information about the context in which a signature help request was triggered. * * @since 3.15.0 */ -type SignatureHelpContext struct { - /** - * Action that caused signature help to be triggered. - */ +type SignatureHelpContext struct { // line 8807 + // Action that caused signature help to be triggered. TriggerKind SignatureHelpTriggerKind `json:"triggerKind"` - /** + /* * Character that caused signature help to be triggered. * * This is undefined when `triggerKind !== SignatureHelpTriggerKind.TriggerCharacter` */ TriggerCharacter string `json:"triggerCharacter,omitempty"` - /** + /* * `true` if signature help was already showing when it was triggered. * - * Retrigger occurs when the signature help is already active and can be caused by actions such as + * Retriggers occurs when the signature help is already active and can be caused by actions such as * typing a trigger character, a cursor move, or document content changes. */ IsRetrigger bool `json:"isRetrigger"` - /** + /* * The currently active `SignatureHelp`. * * The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field updated based on @@ -4682,15 +4449,11 @@ type SignatureHelpContext struct { ActiveSignatureHelp SignatureHelp `json:"activeSignatureHelp,omitempty"` } -/** - * Server Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). - */ -type SignatureHelpOptions struct { - /** - * List of characters that trigger signature help. - */ +// Server Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). +type SignatureHelpOptions struct { // line 8902 + // List of characters that trigger signature help automatically. TriggerCharacters []string `json:"triggerCharacters,omitempty"` - /** + /* * List of characters that re-trigger signature help. * * These trigger characters are only active when signature help is already showing. All trigger characters @@ -4702,11 +4465,9 @@ type SignatureHelpOptions struct { WorkDoneProgressOptions } -/** - * Parameters for a [SignatureHelpRequest](#SignatureHelpRequest). - */ -type SignatureHelpParams struct { - /** +// Parameters for a [SignatureHelpRequest](#SignatureHelpRequest). +type SignatureHelpParams struct { // line 4961 + /* * The signature help context. This is only available if the client specifies * to send this using the client capability `textDocument.signatureHelp.contextSupport === true` * @@ -4717,34 +4478,31 @@ type SignatureHelpParams struct { WorkDoneProgressParams } -/** - * How a signature help was triggered. - * - * @since 3.15.0 - */ -type SignatureHelpTriggerKind float64 - -/** +// Registration options for a [SignatureHelpRequest](#SignatureHelpRequest). +type SignatureHelpRegistrationOptions struct { // line 5024 + TextDocumentRegistrationOptions + SignatureHelpOptions +} +type SignatureHelpTriggerKind uint32 // line 13606 +/* * Represents the signature of something callable. A signature * can have a label, like a function-name, a doc-comment, and * a set of parameters. */ -type SignatureInformation struct { - /** +type SignatureInformation struct { // line 8848 + /* * The label of this signature. Will be shown in * the UI. */ Label string `json:"label"` - /** + /* * The human-readable doc-comment of this signature. Will be shown * in the UI but can be omitted. */ - Documentation string/*string | MarkupContent*/ `json:"documentation,omitempty"` - /** - * The parameters of this signature. - */ + Documentation string `json:"documentation,omitempty"` + // The parameters of this signature. Parameters []ParameterInformation `json:"parameters,omitempty"` - /** + /* * The index of the active parameter. * * If provided, this is used in place of `SignatureHelp.activeParameter`. @@ -4754,56 +4512,52 @@ type SignatureInformation struct { ActiveParameter uint32 `json:"activeParameter,omitempty"` } -/** +/* * Static registration options to be returned in the initialize * request. */ -type StaticRegistrationOptions struct { - /** +type StaticRegistrationOptions struct { // line 6348 + /* * The id used to register the request. The id can be used to deregister * the request again. See also Registration#id. */ ID string `json:"id,omitempty"` } -/** +/* * Represents information about programming constructs like variables, classes, * interfaces etc. */ -type SymbolInformation struct { - /** - * The name of this symbol. - */ - Name string `json:"name"` - /** - * The kind of this symbol. - */ - Kind SymbolKind `json:"kind"` - /** - * Tags for this completion item. - * - * @since 3.16.0 - */ - Tags []SymbolTag `json:"tags,omitempty"` - /** +type SymbolInformation struct { // line 5202 + /* * Indicates if this symbol is deprecated. * * @deprecated Use tags instead */ Deprecated bool `json:"deprecated,omitempty"` - /** + /* * The location of this symbol. The location's range is used by a tool * to reveal the location in the editor. If the symbol is selected in the * tool the range's start information is used to position the cursor. So * the range usually spans more than the actual symbol's name and does - * normally include thinks like visibility modifiers. + * normally include things like visibility modifiers. * - * The range doesn't have to denote a node range in the sense of a abstract + * The range doesn't have to denote a node range in the sense of an abstract * syntax tree. It can therefore not be used to re-construct a hierarchy of * the symbols. */ Location Location `json:"location"` - /** + // The name of this symbol. + Name string `json:"name"` + // The kind of this symbol. + Kind SymbolKind `json:"kind"` + /* + * Tags for this symbol. + * + * @since 3.16.0 + */ + Tags []SymbolTag `json:"tags,omitempty"` + /* * The name of the symbol containing this symbol. This information is for * user interface purposes (e.g. to render a qualifier in the user interface * if necessary). It can't be used to re-infer a hierarchy for the document @@ -4811,206 +4565,160 @@ type SymbolInformation struct { */ ContainerName string `json:"containerName,omitempty"` } +type SymbolKind uint32 // line 12867 +type SymbolTag uint32 // line 12981 +// Describe options to be used when registered for text document change events. +type TextDocumentChangeRegistrationOptions struct { // line 4334 + // How documents are synced to the server. + SyncKind TextDocumentSyncKind `json:"syncKind"` + TextDocumentRegistrationOptions +} -/** - * A symbol kind. - */ -type SymbolKind float64 - -/** - * Symbol tags are extra annotations that tweak the rendering of a symbol. - * @since 3.16 - */ -type SymbolTag float64 - -/** - * Text document specific client capabilities. - */ -type TextDocumentClientCapabilities struct { - /** - * Defines which synchronization capabilities the client supports. - */ +// Text document specific client capabilities. +type TextDocumentClientCapabilities struct { // line 10349 + // Defines which synchronization capabilities the client supports. Synchronization TextDocumentSyncClientCapabilities `json:"synchronization,omitempty"` - /** - * Capabilities specific to the `textDocument/completion` - */ + // Capabilities specific to the `textDocument/completion` request. Completion CompletionClientCapabilities `json:"completion,omitempty"` - /** - * Capabilities specific to the `textDocument/hover` - */ + // Capabilities specific to the `textDocument/hover` request. Hover HoverClientCapabilities `json:"hover,omitempty"` - /** - * Capabilities specific to the `textDocument/signatureHelp` - */ + // Capabilities specific to the `textDocument/signatureHelp` request. SignatureHelp SignatureHelpClientCapabilities `json:"signatureHelp,omitempty"` - /** - * Capabilities specific to the `textDocument/declaration` + /* + * Capabilities specific to the `textDocument/declaration` request. * * @since 3.14.0 */ Declaration DeclarationClientCapabilities `json:"declaration,omitempty"` - /** - * Capabilities specific to the `textDocument/definition` - */ + // Capabilities specific to the `textDocument/definition` request. Definition DefinitionClientCapabilities `json:"definition,omitempty"` - /** - * Capabilities specific to the `textDocument/typeDefinition` + /* + * Capabilities specific to the `textDocument/typeDefinition` request. * * @since 3.6.0 */ TypeDefinition TypeDefinitionClientCapabilities `json:"typeDefinition,omitempty"` - /** - * Capabilities specific to the `textDocument/implementation` + /* + * Capabilities specific to the `textDocument/implementation` request. * * @since 3.6.0 */ Implementation ImplementationClientCapabilities `json:"implementation,omitempty"` - /** - * Capabilities specific to the `textDocument/references` - */ + // Capabilities specific to the `textDocument/references` request. References ReferenceClientCapabilities `json:"references,omitempty"` - /** - * Capabilities specific to the `textDocument/documentHighlight` - */ + // Capabilities specific to the `textDocument/documentHighlight` request. DocumentHighlight DocumentHighlightClientCapabilities `json:"documentHighlight,omitempty"` - /** - * Capabilities specific to the `textDocument/documentSymbol` - */ + // Capabilities specific to the `textDocument/documentSymbol` request. DocumentSymbol DocumentSymbolClientCapabilities `json:"documentSymbol,omitempty"` - /** - * Capabilities specific to the `textDocument/codeAction` - */ + // Capabilities specific to the `textDocument/codeAction` request. CodeAction CodeActionClientCapabilities `json:"codeAction,omitempty"` - /** - * Capabilities specific to the `textDocument/codeLens` - */ + // Capabilities specific to the `textDocument/codeLens` request. CodeLens CodeLensClientCapabilities `json:"codeLens,omitempty"` - /** - * Capabilities specific to the `textDocument/documentLink` - */ + // Capabilities specific to the `textDocument/documentLink` request. DocumentLink DocumentLinkClientCapabilities `json:"documentLink,omitempty"` - /** - * Capabilities specific to the `textDocument/documentColor` + /* + * Capabilities specific to the `textDocument/documentColor` and the + * `textDocument/colorPresentation` request. + * + * @since 3.6.0 */ ColorProvider DocumentColorClientCapabilities `json:"colorProvider,omitempty"` - /** - * Capabilities specific to the `textDocument/formatting` - */ + // Capabilities specific to the `textDocument/formatting` request. Formatting DocumentFormattingClientCapabilities `json:"formatting,omitempty"` - /** - * Capabilities specific to the `textDocument/rangeFormatting` - */ + // Capabilities specific to the `textDocument/rangeFormatting` request. RangeFormatting DocumentRangeFormattingClientCapabilities `json:"rangeFormatting,omitempty"` - /** - * Capabilities specific to the `textDocument/onTypeFormatting` - */ + // Capabilities specific to the `textDocument/onTypeFormatting` request. OnTypeFormatting DocumentOnTypeFormattingClientCapabilities `json:"onTypeFormatting,omitempty"` - /** - * Capabilities specific to the `textDocument/rename` - */ + // Capabilities specific to the `textDocument/rename` request. Rename RenameClientCapabilities `json:"rename,omitempty"` - /** - * Capabilities specific to `textDocument/foldingRange` request. + /* + * Capabilities specific to the `textDocument/foldingRange` request. * * @since 3.10.0 */ FoldingRange FoldingRangeClientCapabilities `json:"foldingRange,omitempty"` - /** - * Capabilities specific to `textDocument/selectionRange` request. + /* + * Capabilities specific to the `textDocument/selectionRange` request. * * @since 3.15.0 */ SelectionRange SelectionRangeClientCapabilities `json:"selectionRange,omitempty"` - /** - * Capabilities specific to `textDocument/publishDiagnostics` notification. - */ + // Capabilities specific to the `textDocument/publishDiagnostics` notification. PublishDiagnostics PublishDiagnosticsClientCapabilities `json:"publishDiagnostics,omitempty"` - /** - * Capabilities specific to the various call hierarchy request. + /* + * Capabilities specific to the various call hierarchy requests. * * @since 3.16.0 */ CallHierarchy CallHierarchyClientCapabilities `json:"callHierarchy,omitempty"` - /** + /* * Capabilities specific to the various semantic token request. * * @since 3.16.0 */ SemanticTokens SemanticTokensClientCapabilities `json:"semanticTokens,omitempty"` - /** - * Capabilities specific to the linked editing range request. + /* + * Capabilities specific to the `textDocument/linkedEditingRange` request. * * @since 3.16.0 */ LinkedEditingRange LinkedEditingRangeClientCapabilities `json:"linkedEditingRange,omitempty"` - /** - * Client capabilities specific to the moniker request. + /* + * Client capabilities specific to the `textDocument/moniker` request. * * @since 3.16.0 */ Moniker MonikerClientCapabilities `json:"moniker,omitempty"` - /** + /* * Capabilities specific to the various type hierarchy requests. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ TypeHierarchy TypeHierarchyClientCapabilities `json:"typeHierarchy,omitempty"` - /** + /* * Capabilities specific to the `textDocument/inlineValue` request. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ InlineValue InlineValueClientCapabilities `json:"inlineValue,omitempty"` - /** + /* * Capabilities specific to the `textDocument/inlayHint` request. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ InlayHint InlayHintClientCapabilities `json:"inlayHint,omitempty"` -} - -/** - * An event describing a change to a text document. If range and rangeLength are omitted - * the new text is considered to be the full content of the document. - */ -type TextDocumentContentChangeEvent = struct { - /** - * The range of the document that changed. - */ - Range *Range `json:"range,omitempty"` - /** - * The optional length of the range that got replaced. + /* + * Capabilities specific to the diagnostic pull model. * - * @deprecated use range instead. - */ - RangeLength uint32 `json:"rangeLength,omitempty"` - /** - * The new text for the provided range. + * @since 3.17.0 */ - Text string `json:"text"` + Diagnostic DiagnosticClientCapabilities `json:"diagnostic,omitempty"` } -/** +/* + * An event describing a change to a text document. If only a text is provided + * it is considered to be the full content of the document. + */ +type TextDocumentContentChangeEvent = Msg_TextDocumentContentChangeEvent // (alias) line 14028 +/* * Describes textual changes on a text document. A TextDocumentEdit describes all changes * on a document version Si and after they are applied move the document to version Si+1. * So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any * kind of ordering. However the edits must be non overlapping. */ -type TextDocumentEdit struct { - /** - * The text document to change. - */ +type TextDocumentEdit struct { // line 6682 + // The text document to change. TextDocument OptionalVersionedTextDocumentIdentifier `json:"textDocument"` - /** + /* * The edits to be applied. * * @since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a * client capability. */ - Edits []TextEdit/*TextEdit | AnnotatedTextEdit*/ `json:"edits"` + Edits []TextEdit `json:"edits"` } -/** +/* * A document filter denotes a document by different properties like * the [language](#TextDocument.languageId), the [scheme](#Uri.scheme) of * its resource, or a glob-pattern that is applied to the [path](#TextDocument.fileName). @@ -5026,196 +4734,148 @@ type TextDocumentEdit struct { * @sample A language filter that applies to typescript files on disk: `{ language: 'typescript', scheme: 'file' }` * @sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**package.json' }` * - * @since 3.17.0 - proposed state. - */ -type TextDocumentFilter = struct { - /** A language id, like `typescript`. */ - Language string `json:"language"` - /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. */ - Scheme string `json:"scheme,omitempty"` - /** A glob pattern, like `*.{ts,js}`. */ - Pattern string `json:"pattern,omitempty"` -} - -/** - * A literal to identify a text document in the client. + * @since 3.17.0 */ -type TextDocumentIdentifier struct { - /** - * The text document's uri. - */ +type TextDocumentFilter = Msg_TextDocumentFilter // (alias) line 14154 +// A literal to identify a text document in the client. +type TextDocumentIdentifier struct { // line 6424 + // The text document's uri. URI DocumentURI `json:"uri"` } -/** +/* * An item to transfer a text document from the client to the * server. */ -type TextDocumentItem struct { - /** - * The text document's uri. - */ +type TextDocumentItem struct { // line 7410 + // The text document's uri. URI DocumentURI `json:"uri"` - /** - * The text document's language identifier - */ + // The text document's language identifier. LanguageID string `json:"languageId"` - /** + /* * The version number of this document (it will increase after each * change, including undo/redo). */ Version int32 `json:"version"` - /** - * The content of the opened text document. - */ + // The content of the opened text document. Text string `json:"text"` } -/** +/* * A parameter literal used in requests to pass a text document and a position inside that * document. */ -type TextDocumentPositionParams struct { - /** - * The text document. - */ +type TextDocumentPositionParams struct { // line 6241 + // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The position inside the text document. - */ + // The position inside the text document. Position Position `json:"position"` } -/** - * General text document registration options. - */ -type TextDocumentRegistrationOptions struct { - /** +// General text document registration options. +type TextDocumentRegistrationOptions struct { // line 2390 + /* * A document selector to identify the scope of the registration. If set to null * the document selector provided on the client side will be used. */ - DocumentSelector DocumentSelector /*DocumentSelector | null*/ `json:"documentSelector"` + DocumentSelector DocumentSelector `json:"documentSelector"` } - -/** - * Represents reasons why a text document is saved. - */ -type TextDocumentSaveReason float64 - -type TextDocumentSyncClientCapabilities struct { - /** - * Whether text document synchronization supports dynamic registration. - */ +type TextDocumentSaveReason uint32 // line 13135 +// Save registration options. +type TextDocumentSaveRegistrationOptions struct { // line 4391 + TextDocumentRegistrationOptions + SaveOptions +} +type TextDocumentSyncClientCapabilities struct { // line 11153 + // Whether text document synchronization supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client supports sending will save notifications. - */ + // The client supports sending will save notifications. WillSave bool `json:"willSave,omitempty"` - /** + /* * The client supports sending a will save request and * waits for a response providing text edits which will * be applied to the document before it is saved. */ WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` - /** - * The client supports did save notifications. - */ + // The client supports did save notifications. DidSave bool `json:"didSave,omitempty"` } - -/** - * Defines how the host (editor) should sync - * document changes to the language server. - */ -type TextDocumentSyncKind float64 - -type TextDocumentSyncOptions struct { - /** +type TextDocumentSyncKind uint32 // line 13110 +type TextDocumentSyncOptions struct { // line 9762 + /* * Open and close notifications are sent to the server. If omitted open close notification should not * be sent. */ OpenClose bool `json:"openClose,omitempty"` - /** + /* * Change notifications are sent to the server. See TextDocumentSyncKind.None, TextDocumentSyncKind.Full * and TextDocumentSyncKind.Incremental. If omitted it defaults to TextDocumentSyncKind.None. */ Change TextDocumentSyncKind `json:"change,omitempty"` - /** + /* * If present will save notifications are sent to the server. If omitted the notification should not be * sent. */ WillSave bool `json:"willSave,omitempty"` - /** + /* * If present will save wait until requests are sent to the server. If omitted the request should not be * sent. */ WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` - /** + /* * If present save notifications are sent to the server. If omitted the notification should not be * sent. */ - Save SaveOptions/*boolean | SaveOptions*/ `json:"save,omitempty"` + Save SaveOptions `json:"save,omitempty"` } -/** - * A text edit applicable to a text document. - */ -type TextEdit struct { - /** +// A text edit applicable to a text document. +type TextEdit struct { // line 4428 + /* * The range of the text document to be manipulated. To insert * text into a document create a range where start === end. */ Range Range `json:"range"` - /** + /* * The string to be inserted. For delete operations use an * empty string. */ NewText string `json:"newText"` } - -type TokenFormat = string - -type TraceValues = string /* 'off' | 'messages' | 'compact' | 'verbose' */ - -/** - * Since 3.6.0 - */ -type TypeDefinitionClientCapabilities struct { - /** +type TokenFormat string // line 13762 +type TraceValues string // line 13409 +// Since 3.6.0 +type TypeDefinitionClientCapabilities struct { // line 11585 + /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `TypeDefinitionRegistrationOptions` return value * for the corresponding server capability as well. */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** + /* * The client supports additional metadata in the form of definition links. * * Since 3.14.0 */ LinkSupport bool `json:"linkSupport,omitempty"` } - -type TypeDefinitionOptions struct { +type TypeDefinitionOptions struct { // line 6363 WorkDoneProgressOptions } - -type TypeDefinitionParams struct { +type TypeDefinitionParams struct { // line 2131 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } - -type TypeDefinitionRegistrationOptions struct { +type TypeDefinitionRegistrationOptions struct { // line 2151 TextDocumentRegistrationOptions TypeDefinitionOptions StaticRegistrationOptions } -/** - * @since 3.17.0 - proposed state - */ -type TypeHierarchyClientCapabilities = struct { - /** +// @since 3.17.0 +type TypeHierarchyClientCapabilities struct { // line 12363 + /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` * return value for the corresponding server capability as well. @@ -5223,252 +4883,207 @@ type TypeHierarchyClientCapabilities = struct { DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -/** - * @since 3.17.0 - proposed state - */ -type TypeHierarchyItem = struct { - /** - * The name of this item. - */ +// @since 3.17.0 +type TypeHierarchyItem struct { // line 3432 + // The name of this item. Name string `json:"name"` - /** - * The kind of this item. - */ + // The kind of this item. Kind SymbolKind `json:"kind"` - /** - * Tags for this item. - */ + // Tags for this item. Tags []SymbolTag `json:"tags,omitempty"` - /** - * More detail for this item, e.g. the signature of a function. - */ + // More detail for this item, e.g. the signature of a function. Detail string `json:"detail,omitempty"` - /** - * The resource identifier of this item. - */ + // The resource identifier of this item. URI DocumentURI `json:"uri"` - /** + /* * The range enclosing this symbol not including leading/trailing whitespace * but everything else, e.g. comments and code. */ - Range *Range `json:"range"` - /** + Range Range `json:"range"` + /* * The range that should be selected and revealed when this symbol is being * picked, e.g. the name of a function. Must be contained by the * [`range`](#TypeHierarchyItem.range). */ - SelectionRange *Range `json:"selectionRange"` - /** + SelectionRange Range `json:"selectionRange"` + /* * A data entry field that is preserved between a type hierarchy prepare and * supertypes or subtypes requests. It could also be used to identify the * type hierarchy in the server, helping improve the performance on * resolving supertypes and subtypes. */ - Data LSPAny `json:"data,omitempty"` + Data interface{} `json:"data,omitempty"` } -/** +/* * Type hierarchy options used during static registration. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type TypeHierarchyOptions = WorkDoneProgressOptions +type TypeHierarchyOptions struct { // line 6941 + WorkDoneProgressOptions +} -/** +/* * The parameter of a `textDocument/prepareTypeHierarchy` request. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type TypeHierarchyPrepareParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The position inside the text document. - */ - Position Position `json:"position"` - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` +type TypeHierarchyPrepareParams struct { // line 3414 + TextDocumentPositionParams + WorkDoneProgressParams } -/** +/* * Type hierarchy options used during static or dynamic registration. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type TypeHierarchyRegistrationOptions struct { - /** - * A document selector to identify the scope of the registration. If set to null - * the document selector provided on the client side will be used. - */ - DocumentSelector DocumentSelector/*DocumentSelector | null*/ `json:"documentSelector"` - /** - * The id used to register the request. The id can be used to deregister - * the request again. See also Registration#id. - */ - ID string `json:"id,omitempty"` +type TypeHierarchyRegistrationOptions struct { // line 3509 + TextDocumentRegistrationOptions + TypeHierarchyOptions + StaticRegistrationOptions } -/** +/* * The parameter of a `typeHierarchy/subtypes` request. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type TypeHierarchySubtypesParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` - Item TypeHierarchyItem `json:"item"` +type TypeHierarchySubtypesParams struct { // line 3555 + Item TypeHierarchyItem `json:"item"` + WorkDoneProgressParams + PartialResultParams } -/** +/* * The parameter of a `typeHierarchy/supertypes` request. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type TypeHierarchySupertypesParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` - Item TypeHierarchyItem `json:"item"` +type TypeHierarchySupertypesParams struct { // line 3531 + Item TypeHierarchyItem `json:"item"` + WorkDoneProgressParams + PartialResultParams } -/** - * A tagging type for string properties that are actually URIs - * - * @since 3.16.0 - */ -type URI = string - -/** +// created for Tuple +type UIntCommaUInt struct { // line 10101 + Fld0 uint32 `json:"Fld0"` + Fld1 uint32 `json:"Fld1"` +} +type URI = string // (alias) line 0 +/* * A diagnostic report indicating that the last returned * report is still accurate. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type UnchangedDocumentDiagnosticReport = struct { - /** +type UnchangedDocumentDiagnosticReport struct { // line 7275 + /* * A document diagnostic report indicating * no changes to the last result. A server can * only return `unchanged` if result ids are * provided. */ Kind string `json:"kind"` - /** + /* * A result id which will be sent on the next * diagnostic request for the same document. */ ResultID string `json:"resultId"` } - -/** - * Moniker uniqueness level to define scope of the moniker. - * - * @since 3.16.0 - */ -type UniquenessLevel string - -/** - * General parameters to unregister a request or notification. - */ -type Unregistration struct { - /** +type UniquenessLevel string // line 12997 +// General parameters to unregister a request or notification. +type Unregistration struct { // line 7633 + /* * The id used to unregister the request or notification. Usually an id * provided during the register request. */ ID string `json:"id"` - /** - * The method to unregister for. - */ + // The method to unregister for. Method string `json:"method"` } - -type UnregistrationParams struct { +type UnregistrationParams struct { // line 4075 Unregisterations []Unregistration `json:"unregisterations"` } -/** +/* * A versioned notebook document identifier. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type VersionedNotebookDocumentIdentifier = struct { - /** - * The version number of this notebook document. - */ +type VersionedNotebookDocumentIdentifier struct { // line 7448 + // The version number of this notebook document. Version int32 `json:"version"` - /** - * The notebook document's uri. - */ + // The notebook document's uri. URI URI `json:"uri"` } -/** - * A text document identifier to denote a specific version of a text document. - */ -type VersionedTextDocumentIdentifier struct { - /** - * The version number of this document. - */ +// A text document identifier to denote a specific version of a text document. +type VersionedTextDocumentIdentifier struct { // line 8465 + // The version number of this document. Version int32 `json:"version"` TextDocumentIdentifier } - -type WatchKind float64 - -/** - * The parameters send in a will save text document notification. - */ -type WillSaveTextDocumentParams struct { - /** - * The document that will be saved. - */ +type WatchKind = uint32 // line 13505 +// The parameters sent in a will save text document notification. +type WillSaveTextDocumentParams struct { // line 4406 + // The document that will be saved. TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The 'TextDocumentSaveReason'. - */ + // The 'TextDocumentSaveReason'. Reason TextDocumentSaveReason `json:"reason"` } - -type WorkDoneProgressBegin struct { +type WindowClientCapabilities struct { // line 10655 + /* + * It indicates whether the client supports server initiated + * progress using the `window/workDoneProgress/create` request. + * + * The capability also controls Whether client supports handling + * of progress notifications. If set servers are allowed to report a + * `workDoneProgress` property in the request specific server + * capabilities. + * + * @since 3.15.0 + */ + WorkDoneProgress bool `json:"workDoneProgress,omitempty"` + /* + * Capabilities specific to the showMessage request. + * + * @since 3.16.0 + */ + ShowMessage ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` + /* + * Capabilities specific to the showDocument request. + * + * @since 3.16.0 + */ + ShowDocument ShowDocumentClientCapabilities `json:"showDocument,omitempty"` +} +type WorkDoneProgressBegin struct { // line 6059 Kind string `json:"kind"` - /** + /* * Mandatory title of the progress operation. Used to briefly inform about * the kind of operation being performed. * - * Examples: "Indexing" or "Linking dependencies". + * Examples: \"Indexing\" or \"Linking dependencies\". */ Title string `json:"title"` - /** + /* * Controls if a cancel button should show to allow the user to cancel the * long running operation. Clients that don't support cancellation are allowed * to ignore the setting. */ Cancellable bool `json:"cancellable,omitempty"` - /** + /* * Optional, more detailed associated progress message. Contains * complementary information to the `title`. * - * Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". + * Examples: \"3/25 files\", \"project/src/module2\", \"node_modules/some_dep\". * If unset, the previous progress message (if any) is still valid. */ Message string `json:"message,omitempty"` - /** + /* * Optional progress percentage to display (value 100 is considered 100%). * If not provided infinite progress is assumed and clients are allowed * to ignore the `percentage` value in subsequent in report notifications. @@ -5478,86 +5093,53 @@ type WorkDoneProgressBegin struct { */ Percentage uint32 `json:"percentage,omitempty"` } - -type WorkDoneProgressCancelParams struct { - /** - * The token to be used to report progress. - */ +type WorkDoneProgressCancelParams struct { // line 2647 + // The token to be used to report progress. Token ProgressToken `json:"token"` } - -type WorkDoneProgressClientCapabilities struct { - /** - * Window specific client capabilities. - */ - Window struct { - /** - * Whether client supports server initiated progress using the - * `window/workDoneProgress/create` request. - * - * Since 3.15.0 - */ - WorkDoneProgress bool `json:"workDoneProgress,omitempty"` - /** - * Capabilities specific to the showMessage request. - * - * @since 3.16.0 - */ - ShowMessage ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` - /** - * Capabilities specific to the showDocument request. - * - * @since 3.16.0 - */ - ShowDocument ShowDocumentClientCapabilities `json:"showDocument,omitempty"` - } `json:"window,omitempty"` -} - -type WorkDoneProgressCreateParams struct { - /** - * The token to be used to report progress. - */ +type WorkDoneProgressCreateParams struct { // line 2634 + // The token to be used to report progress. Token ProgressToken `json:"token"` } - -type WorkDoneProgressEnd struct { +type WorkDoneProgressEnd struct { // line 6145 Kind string `json:"kind"` - /** + /* * Optional, a final message indicating to for example indicate the outcome * of the operation. */ Message string `json:"message,omitempty"` } - -type WorkDoneProgressOptions struct { +type WorkDoneProgressOptions struct { // line 2377 WorkDoneProgress bool `json:"workDoneProgress,omitempty"` } -type WorkDoneProgressParams struct { - /** - * An optional token that a server can use to report work done progress. - */ +// created for And +type WorkDoneProgressOptionsAndTextDocumentRegistrationOptions struct { // line 204 + WorkDoneProgressOptions + TextDocumentRegistrationOptions +} +type WorkDoneProgressParams struct { // line 6263 + // An optional token that a server can use to report work done progress. WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` } - -type WorkDoneProgressReport struct { +type WorkDoneProgressReport struct { // line 6106 Kind string `json:"kind"` - /** + /* * Controls enablement state of a cancel button. * * Clients that don't support cancellation or don't support controlling the button's * enablement state are allowed to ignore the property. */ Cancellable bool `json:"cancellable,omitempty"` - /** + /* * Optional, more detailed associated progress message. Contains * complementary information to the `title`. * - * Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". + * Examples: \"3/25 files\", \"project/src/module2\", \"node_modules/some_dep\". * If unset, the previous progress message (if any) is still valid. */ Message string `json:"message,omitempty"` - /** + /* * Optional progress percentage to display (value 100 is considered 100%). * If not provided infinite progress is assumed and clients are allowed * to ignore the `percentage` value in subsequent in report notifications. @@ -5568,115 +5150,137 @@ type WorkDoneProgressReport struct { Percentage uint32 `json:"percentage,omitempty"` } -/** - * Workspace specific client capabilities. - */ -type WorkspaceClientCapabilities struct { - /** +// created for Literal +type Workspace6Gn struct { // line 8424 + /* + * The server supports workspace folder. + * + * @since 3.6.0 + */ + WorkspaceFolders WorkspaceFolders5Gn `json:"WorkspaceFolders"` + /* + * The server is interested in notifications/requests for operations on files. + * + * @since 3.16.0 + */ + FileOperations FileOperationOptions `json:"FileOperations"` +} + +// Workspace specific client capabilities. +type WorkspaceClientCapabilities struct { // line 10210 + /* * The client supports applying batch edits * to the workspace by supporting the request * 'workspace/applyEdit' */ ApplyEdit bool `json:"applyEdit,omitempty"` - /** - * Capabilities specific to `WorkspaceEdit`s - */ - WorkspaceEdit WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` - /** - * Capabilities specific to the `workspace/didChangeConfiguration` notification. - */ + // Capabilities specific to `WorkspaceEdit`s. + WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` + // Capabilities specific to the `workspace/didChangeConfiguration` notification. DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` - /** - * Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - */ + // Capabilities specific to the `workspace/didChangeWatchedFiles` notification. DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` - /** - * Capabilities specific to the `workspace/symbol` request. - */ + // Capabilities specific to the `workspace/symbol` request. Symbol WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` - /** - * Capabilities specific to the `workspace/executeCommand` request. - */ + // Capabilities specific to the `workspace/executeCommand` request. ExecuteCommand ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` - /** + /* + * The client has support for workspace folders. + * + * @since 3.6.0 + */ + WorkspaceFolders bool `json:"workspaceFolders,omitempty"` + /* + * The client supports `workspace/configuration` requests. + * + * @since 3.6.0 + */ + Configuration bool `json:"configuration,omitempty"` + /* * Capabilities specific to the semantic token requests scoped to the * workspace. * * @since 3.16.0. */ SemanticTokens SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` - /** + /* * Capabilities specific to the code lens requests scoped to the * workspace. * * @since 3.16.0. */ CodeLens CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` - /** + /* * The client has support for file notifications/requests for user operations on files. * * Since 3.16.0 */ FileOperations FileOperationClientCapabilities `json:"fileOperations,omitempty"` - /** + /* * Capabilities specific to the inline values requests scoped to the * workspace. * * @since 3.17.0. */ InlineValue InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` - /** - * Capabilities specific to the inlay hints requests scoped to the + /* + * Capabilities specific to the inlay hint requests scoped to the * workspace. * * @since 3.17.0. */ InlayHint InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` + /* + * Capabilities specific to the diagnostic requests scoped to the + * workspace. + * + * @since 3.17.0. + */ + Diagnostics DiagnosticWorkspaceClientCapabilities `json:"diagnostics,omitempty"` } -/** +/* * Parameters of the workspace diagnostic request. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type WorkspaceDiagnosticParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` - /** - * The additional identifier provided during registration. - */ +type WorkspaceDiagnosticParams struct { // line 3899 + // The additional identifier provided during registration. Identifier string `json:"identifier,omitempty"` - /** + /* * The currently known diagnostic reports with their * previous result ids. */ PreviousResultIds []PreviousResultID `json:"previousResultIds"` + WorkDoneProgressParams + PartialResultParams } -/** +/* * A workspace diagnostic report. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type WorkspaceDiagnosticReport = struct { +type WorkspaceDiagnosticReport struct { // line 3936 Items []WorkspaceDocumentDiagnosticReport `json:"items"` } -/** - * A workspace diagnostic document report. +/* + * A partial result for a workspace diagnostic report. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type WorkspaceDocumentDiagnosticReport = interface{} /*WorkspaceFullDocumentDiagnosticReport | WorkspaceUnchangedDocumentDiagnosticReport*/ +type WorkspaceDiagnosticReportPartialResult struct { // line 3953 + Items []WorkspaceDocumentDiagnosticReport `json:"items"` +} -/** +/* + * A workspace diagnostic document report. + * + * @since 3.17.0 + */ +type WorkspaceDocumentDiagnosticReport = Or_WorkspaceDocumentDiagnosticReport // (alias) line 14010 +/* * A workspace edit represents changes to many resources managed in the workspace. The edit * should either provide `changes` or `documentChanges`. If documentChanges are present * they are preferred over `changes` if the client can handle versioned document edits. @@ -5690,12 +5294,10 @@ type WorkspaceDocumentDiagnosticReport = interface{} /*WorkspaceFullDocumentDiag * cause failure of the operation. How the client recovers from the failure is described by * the client capability: `workspace.workspaceEdit.failureHandling` */ -type WorkspaceEdit struct { - /** - * Holds changes to existing resources. - */ - Changes map[DocumentURI][]TextEdit/*[uri: DocumentUri]: TextEdit[]*/ `json:"changes,omitempty"` - /** +type WorkspaceEdit struct { // line 3215 + // Holds changes to existing resources. + Changes map[DocumentURI][]TextEdit `json:"changes,omitempty"` + /* * Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes * are either an array of `TextDocumentEdit`s to express changes to n different text documents * where each text document edit addresses a specific version of a text document. Or it can contain @@ -5707,8 +5309,8 @@ type WorkspaceEdit struct { * If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then * only plain `TextEdit`s using the `changes` property are supported. */ - DocumentChanges []DocumentChanges/*TextDocumentEdit | CreateFile | RenameFile | DeleteFile*/ `json:"documentChanges,omitempty"` - /** + DocumentChanges []DocumentChanges `json:"documentChanges,omitempty"` + /* * A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and * delete file / folder operations. * @@ -5716,208 +5318,181 @@ type WorkspaceEdit struct { * * @since 3.16.0 */ - ChangeAnnotations map[string]ChangeAnnotationIdentifier/*[id: ChangeAnnotationIdentifier]: ChangeAnnotation;*/ `json:"changeAnnotations,omitempty"` + ChangeAnnotations map[ChangeAnnotationIdentifier]ChangeAnnotation `json:"changeAnnotations,omitempty"` } - -type WorkspaceEditClientCapabilities struct { - /** - * The client supports versioned document changes in `WorkspaceEdit`s - */ +type WorkspaceEditClientCapabilities struct { // line 10794 + // The client supports versioned document changes in `WorkspaceEdit`s DocumentChanges bool `json:"documentChanges,omitempty"` - /** + /* * The resource operations the client supports. Clients should at least * support 'create', 'rename' and 'delete' files and folders. * * @since 3.13.0 */ ResourceOperations []ResourceOperationKind `json:"resourceOperations,omitempty"` - /** + /* * The failure handling strategy of a client if applying the workspace edit * fails. * * @since 3.13.0 */ FailureHandling FailureHandlingKind `json:"failureHandling,omitempty"` - /** + /* * Whether the client normalizes line endings to the client specific * setting. * If set to `true` the client will normalize line ending characters - * in a workspace edit containing to the client specific new line + * in a workspace edit to the client-specified new line * character. * * @since 3.16.0 */ NormalizesLineEndings bool `json:"normalizesLineEndings,omitempty"` - /** + /* * Whether the client in general supports change annotations on text edits, * create file, rename file and delete file changes. * * @since 3.16.0 */ - ChangeAnnotationSupport struct { - /** - * Whether the client groups edits with equal labels into tree nodes, - * for instance all edits labelled with "Changes in Strings" would - * be a tree node. - */ - GroupsOnLabel bool `json:"groupsOnLabel,omitempty"` - } `json:"changeAnnotationSupport,omitempty"` + ChangeAnnotationSupport PChangeAnnotationSupportPWorkspaceEdit `json:"changeAnnotationSupport,omitempty"` } -type WorkspaceFolder struct { - /** - * The associated URI for this workspace folder. - */ - URI string `json:"uri"` - /** +// A workspace folder inside a client. +type WorkspaceFolder struct { // line 2171 + // The associated URI for this workspace folder. + URI URI `json:"uri"` + /* * The name of the workspace folder. Used to refer to this * workspace folder in the user interface. */ Name string `json:"name"` } - -/** - * The workspace folder change event. - */ -type WorkspaceFoldersChangeEvent struct { - /** - * The array of added workspace folders - */ - Added []WorkspaceFolder `json:"added"` - /** - * The array of the removed workspace folders +type WorkspaceFolders5Gn struct { // line 9959 + // The server has support for workspace folders + Supported bool `json:"supported,omitempty"` + /* + * Whether the server wants to receive workspace folder + * change notifications. + * + * If a string is provided the string is treated as an ID + * under which the notification is registered on the client + * side. The ID can be used to unregister for these events + * using the `client/unregisterCapability` request. */ - Removed []WorkspaceFolder `json:"removed"` + ChangeNotifications string `json:"changeNotifications,omitempty"` } -type WorkspaceFoldersClientCapabilities struct { - /** - * The workspace client capabilities - */ - Workspace Workspace7Gn `json:"workspace,omitempty"` +// The workspace folder change event. +type WorkspaceFoldersChangeEvent struct { // line 6373 + // The array of added workspace folders + Added []WorkspaceFolder `json:"added"` + // The array of the removed workspace folders + Removed []WorkspaceFolder `json:"removed"` } - -type WorkspaceFoldersInitializeParams struct { - /** - * The actual configured workspace folders. +type WorkspaceFoldersInitializeParams struct { // line 7802 + /* + * The workspace folders configured in the client when the server starts. + * + * This property is only available if the client supports workspace folders. + * It can be `null` if the client supports workspace folders but none are + * configured. + * + * @since 3.6.0 */ - WorkspaceFolders []WorkspaceFolder /*WorkspaceFolder[] | null*/ `json:"workspaceFolders"` + WorkspaceFolders []WorkspaceFolder `json:"workspaceFolders,omitempty"` } - -type WorkspaceFoldersServerCapabilities struct { - /** - * The workspace server capabilities +type WorkspaceFoldersServerCapabilities struct { // line 9959 + // The server has support for workspace folders + Supported bool `json:"supported,omitempty"` + /* + * Whether the server wants to receive workspace folder + * change notifications. + * + * If a string is provided the string is treated as an ID + * under which the notification is registered on the client + * side. The ID can be used to unregister for these events + * using the `client/unregisterCapability` request. */ - Workspace Workspace9Gn `json:"workspace,omitempty"` + ChangeNotifications string `json:"changeNotifications,omitempty"` } -/** +/* * A full document diagnostic report for a workspace diagnostic result. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type WorkspaceFullDocumentDiagnosticReport struct { - /** - * The URI for which diagnostic information is reported. - */ +type WorkspaceFullDocumentDiagnosticReport struct { // line 9542 + // The URI for which diagnostic information is reported. URI DocumentURI `json:"uri"` - /** + /* * The version number for which the diagnostics are reported. * If the document is not marked as open `null` can be provided. */ - Version int32/*integer | null*/ `json:"version"` + Version int32 `json:"version"` + FullDocumentDiagnosticReport } -/** - * A special workspace symbol that supports locations without a range +/* + * A special workspace symbol that supports locations without a range. + * + * See also SymbolInformation. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type WorkspaceSymbol struct { - /** - * The location of the symbol. +type WorkspaceSymbol struct { // line 5534 + /* + * The location of the symbol. Whether a server is allowed to + * return a location without a range depends on the client + * capability `workspace.symbol.resolveSupport`. * * See SymbolInformation#location for more details. */ - Location Location/*Location | { uri: DocumentUri }*/ `json:"location"` - /** + Location OrPLocation_workspace_symbol `json:"location"` + /* * A data entry field that is preserved on a workspace symbol between a * workspace symbol request and a workspace symbol resolve request. */ - Data LSPAny `json:"data,omitempty"` + Data interface{} `json:"data,omitempty"` + BaseSymbolInformation } -/** - * Client capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). - */ -type WorkspaceSymbolClientCapabilities struct { - /** - * Symbol request supports dynamic registration. - */ +// Client capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). +type WorkspaceSymbolClientCapabilities struct { // line 10901 + // Symbol request supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. - */ - SymbolKind struct { - /** - * The symbol kind values the client supports. When this - * property exists the client also guarantees that it will - * handle values outside its set gracefully and falls back - * to a default value when unknown. - * - * If this property is not present the client only supports - * the symbol kinds from `File` to `Array` as defined in - * the initial version of the protocol. - */ - ValueSet []SymbolKind `json:"valueSet,omitempty"` - } `json:"symbolKind,omitempty"` - /** + // Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. + SymbolKind PSymbolKindPSymbol `json:"symbolKind,omitempty"` + /* * The client supports tags on `SymbolInformation`. * Clients supporting tags have to handle unknown tags gracefully. * * @since 3.16.0 */ - TagSupport struct { - /** - * The tags supported by the client. - */ - ValueSet []SymbolTag `json:"valueSet"` - } `json:"tagSupport,omitempty"` - /** + TagSupport PTagSupportPSymbol `json:"tagSupport,omitempty"` + /* * The client support partial workspace symbols. The client will send the * request `workspaceSymbol/resolve` to the server to resolve additional * properties. * - * @since 3.17.0 - proposedState + * @since 3.17.0 */ - ResolveSupport struct { - /** - * The properties that a client can resolve lazily. Usually - * `location.range` - */ - Properties []string `json:"properties"` - } `json:"resolveSupport,omitempty"` + ResolveSupport PResolveSupportPSymbol `json:"resolveSupport,omitempty"` } -/** - * Server capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). - */ -type WorkspaceSymbolOptions struct { - /** +// Server capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). +type WorkspaceSymbolOptions struct { // line 9125 + /* * The server provides support to resolve additional * information for a workspace symbol. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ ResolveProvider bool `json:"resolveProvider,omitempty"` WorkDoneProgressOptions } -/** - * The parameters of a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). - */ -type WorkspaceSymbolParams struct { - /** +// The parameters of a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). +type WorkspaceSymbolParams struct { // line 5510 + /* * A query string to filter symbols by. Clients may send an empty * string here to request all symbols. */ @@ -5926,40 +5501,136 @@ type WorkspaceSymbolParams struct { PartialResultParams } -/** +// Registration options for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). +type WorkspaceSymbolRegistrationOptions struct { // line 5583 + WorkspaceSymbolOptions +} + +/* * An unchanged document diagnostic report for a workspace diagnostic result. * - * @since 3.17.0 - proposed state + * @since 3.17.0 */ -type WorkspaceUnchangedDocumentDiagnosticReport struct { - /** - * The URI for which diagnostic information is reported. - */ +type WorkspaceUnchangedDocumentDiagnosticReport struct { // line 9580 + // The URI for which diagnostic information is reported. URI DocumentURI `json:"uri"` - /** + /* * The version number for which the diagnostics are reported. * If the document is not marked as open `null` can be provided. */ - Version int32/*integer | null*/ `json:"version"` + Version int32 `json:"version"` + UnchangedDocumentDiagnosticReport } -const ( - /** - * Empty kind. +// The initialize parameters +type XInitializeParams struct { // line 7655 + /* + * The process Id of the parent process that started + * the server. + * + * Is `null` if the process has not been started by another process. + * If the parent process is not alive then the server should exit. */ - - Empty CodeActionKind = "" - /** - * Base kind for quickfix actions: 'quickfix' + ProcessID int32 `json:"processId"` + /* + * Information about the client + * + * @since 3.15.0 + */ + ClientInfo Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` + /* + * The locale the client is currently showing the user interface + * in. This must not necessarily be the locale of the operating + * system. + * + * Uses IETF language tags as the value's syntax + * (See https://en.wikipedia.org/wiki/IETF_language_tag) + * + * @since 3.16.0 + */ + Locale string `json:"locale,omitempty"` + /* + * The rootPath of the workspace. Is null + * if no folder is open. + * + * @deprecated in favour of rootUri. + */ + RootPath string `json:"rootPath,omitempty"` + /* + * The rootUri of the workspace. Is null if no + * folder is open. If both `rootPath` and `rootUri` are set + * `rootUri` wins. + * + * @deprecated in favour of workspaceFolders. */ + RootURI DocumentURI `json:"rootUri"` + // The capabilities provided by the client (editor or tool) + Capabilities ClientCapabilities `json:"capabilities"` + // User provided initialization options. + InitializationOptions interface{} `json:"initializationOptions,omitempty"` + // The initial trace setting. If omitted trace is disabled ('off'). + Trace string `json:"trace,omitempty"` +} - QuickFix CodeActionKind = "quickfix" - /** - * Base kind for refactoring actions: 'refactor' +// The initialize parameters +type _InitializeParams struct { // line 7655 + /* + * The process Id of the parent process that started + * the server. + * + * Is `null` if the process has not been started by another process. + * If the parent process is not alive then the server should exit. + */ + ProcessID int32 `json:"processId"` + /* + * Information about the client + * + * @since 3.15.0 */ + ClientInfo Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` + /* + * The locale the client is currently showing the user interface + * in. This must not necessarily be the locale of the operating + * system. + * + * Uses IETF language tags as the value's syntax + * (See https://en.wikipedia.org/wiki/IETF_language_tag) + * + * @since 3.16.0 + */ + Locale string `json:"locale,omitempty"` + /* + * The rootPath of the workspace. Is null + * if no folder is open. + * + * @deprecated in favour of rootUri. + */ + RootPath string `json:"rootPath,omitempty"` + /* + * The rootUri of the workspace. Is null if no + * folder is open. If both `rootPath` and `rootUri` are set + * `rootUri` wins. + * + * @deprecated in favour of workspaceFolders. + */ + RootURI DocumentURI `json:"rootUri"` + // The capabilities provided by the client (editor or tool) + Capabilities ClientCapabilities `json:"capabilities"` + // User provided initialization options. + InitializationOptions interface{} `json:"initializationOptions,omitempty"` + // The initial trace setting. If omitted trace is disabled ('off'). + Trace string `json:"trace,omitempty"` +} - Refactor CodeActionKind = "refactor" - /** +const ( + // A set of predefined code action kinds + // Empty kind. + Empty CodeActionKind = "" // line 13359 + // Base kind for quickfix actions: 'quickfix' + QuickFix CodeActionKind = "quickfix" // line 13364 + // Base kind for refactoring actions: 'refactor' + Refactor CodeActionKind = "refactor" // line 13369 + /* * Base kind for refactoring extraction actions: 'refactor.extract' * * Example extract actions: @@ -5970,9 +5641,8 @@ const ( * - Extract interface from class * - ... */ - - RefactorExtract CodeActionKind = "refactor.extract" - /** + RefactorExtract CodeActionKind = "refactor.extract" // line 13374 + /* * Base kind for refactoring inline actions: 'refactor.inline' * * Example inline actions: @@ -5982,9 +5652,8 @@ const ( * - Inline constant * - ... */ - - RefactorInline CodeActionKind = "refactor.inline" - /** + RefactorInline CodeActionKind = "refactor.inline" // line 13379 + /* * Base kind for refactoring rewrite actions: 'refactor.rewrite' * * Example rewrite actions: @@ -5996,21 +5665,16 @@ const ( * - Move method to base class * - ... */ - - RefactorRewrite CodeActionKind = "refactor.rewrite" - /** + RefactorRewrite CodeActionKind = "refactor.rewrite" // line 13384 + /* * Base kind for source actions: `source` * * Source code actions apply to the entire file. */ - - Source CodeActionKind = "source" - /** - * Base kind for an organize imports source action: `source.organizeImports` - */ - - SourceOrganizeImports CodeActionKind = "source.organizeImports" - /** + Source CodeActionKind = "source" // line 13389 + // Base kind for an organize imports source action: `source.organizeImports` + SourceOrganizeImports CodeActionKind = "source.organizeImports" // line 13394 + /* * Base kind for auto-fix source actions: `source.fixAll`. * * Fix all actions automatically fix errors that have a clear fix that do not require user input. @@ -6018,203 +5682,190 @@ const ( * * @since 3.15.0 */ - - SourceFixAll CodeActionKind = "source.fixAll" - /** - * Code actions were explicitly requested by the user or by an extension. + SourceFixAll CodeActionKind = "source.fixAll" // line 13399 + /* + * The reason why code actions were requested. + * + * @since 3.17.0 */ - - CodeActionInvoked CodeActionTriggerKind = 1 - /** + // Code actions were explicitly requested by the user or by an extension. + CodeActionInvoked CodeActionTriggerKind = 1 // line 13639 + /* * Code actions were requested automatically. * * This typically happens when current selection in a file changes, but can * also be triggered when file content changes. */ - - CodeActionAutomatic CodeActionTriggerKind = 2 - TextCompletion CompletionItemKind = 1 - MethodCompletion CompletionItemKind = 2 - FunctionCompletion CompletionItemKind = 3 - ConstructorCompletion CompletionItemKind = 4 - FieldCompletion CompletionItemKind = 5 - VariableCompletion CompletionItemKind = 6 - ClassCompletion CompletionItemKind = 7 - InterfaceCompletion CompletionItemKind = 8 - ModuleCompletion CompletionItemKind = 9 - PropertyCompletion CompletionItemKind = 10 - UnitCompletion CompletionItemKind = 11 - ValueCompletion CompletionItemKind = 12 - EnumCompletion CompletionItemKind = 13 - KeywordCompletion CompletionItemKind = 14 - SnippetCompletion CompletionItemKind = 15 - ColorCompletion CompletionItemKind = 16 - FileCompletion CompletionItemKind = 17 - ReferenceCompletion CompletionItemKind = 18 - FolderCompletion CompletionItemKind = 19 - EnumMemberCompletion CompletionItemKind = 20 - ConstantCompletion CompletionItemKind = 21 - StructCompletion CompletionItemKind = 22 - EventCompletion CompletionItemKind = 23 - OperatorCompletion CompletionItemKind = 24 - TypeParameterCompletion CompletionItemKind = 25 - /** - * Render a completion as obsolete, usually using a strike-out. - */ - - ComplDeprecated CompletionItemTag = 1 - /** + CodeActionAutomatic CodeActionTriggerKind = 2 // line 13644 + // The kind of a completion entry. + TextCompletion CompletionItemKind = 1 // line 13167 + MethodCompletion CompletionItemKind = 2 // line 13171 + FunctionCompletion CompletionItemKind = 3 // line 13175 + ConstructorCompletion CompletionItemKind = 4 // line 13179 + FieldCompletion CompletionItemKind = 5 // line 13183 + VariableCompletion CompletionItemKind = 6 // line 13187 + ClassCompletion CompletionItemKind = 7 // line 13191 + InterfaceCompletion CompletionItemKind = 8 // line 13195 + ModuleCompletion CompletionItemKind = 9 // line 13199 + PropertyCompletion CompletionItemKind = 10 // line 13203 + UnitCompletion CompletionItemKind = 11 // line 13207 + ValueCompletion CompletionItemKind = 12 // line 13211 + EnumCompletion CompletionItemKind = 13 // line 13215 + KeywordCompletion CompletionItemKind = 14 // line 13219 + SnippetCompletion CompletionItemKind = 15 // line 13223 + ColorCompletion CompletionItemKind = 16 // line 13227 + FileCompletion CompletionItemKind = 17 // line 13231 + ReferenceCompletion CompletionItemKind = 18 // line 13235 + FolderCompletion CompletionItemKind = 19 // line 13239 + EnumMemberCompletion CompletionItemKind = 20 // line 13243 + ConstantCompletion CompletionItemKind = 21 // line 13247 + StructCompletion CompletionItemKind = 22 // line 13251 + EventCompletion CompletionItemKind = 23 // line 13255 + OperatorCompletion CompletionItemKind = 24 // line 13259 + TypeParameterCompletion CompletionItemKind = 25 // line 13263 + /* + * Completion item tags are extra annotations that tweak the rendering of a completion + * item. + * + * @since 3.15.0 + */ + // Render a completion as obsolete, usually using a strike-out. + ComplDeprecated CompletionItemTag = 1 // line 13277 + // How a completion was triggered + /* * Completion was triggered by typing an identifier (24x7 code * complete), manual invocation (e.g Ctrl+Space) or via API. */ - - Invoked CompletionTriggerKind = 1 - /** + Invoked CompletionTriggerKind = 1 // line 13588 + /* * Completion was triggered by a trigger character specified by * the `triggerCharacters` properties of the `CompletionRegistrationOptions`. */ - - TriggerCharacter CompletionTriggerKind = 2 - /** - * Completion was re-triggered as current completion list is incomplete - */ - - TriggerForIncompleteCompletions CompletionTriggerKind = 3 - /** - * Reports an error. - */ - - SeverityError DiagnosticSeverity = 1 - /** - * Reports a warning. - */ - - SeverityWarning DiagnosticSeverity = 2 - /** - * Reports an information. - */ - - SeverityInformation DiagnosticSeverity = 3 - /** - * Reports a hint. + TriggerCharacter CompletionTriggerKind = 2 // line 13593 + // Completion was re-triggered as current completion list is incomplete + TriggerForIncompleteCompletions CompletionTriggerKind = 3 // line 13598 + // The diagnostic's severity. + // Reports an error. + SeverityError DiagnosticSeverity = 1 // line 13537 + // Reports a warning. + SeverityWarning DiagnosticSeverity = 2 // line 13542 + // Reports an information. + SeverityInformation DiagnosticSeverity = 3 // line 13547 + // Reports a hint. + SeverityHint DiagnosticSeverity = 4 // line 13552 + /* + * The diagnostic tags. + * + * @since 3.15.0 */ - - SeverityHint DiagnosticSeverity = 4 - /** + /* * Unused or unnecessary code. * * Clients are allowed to render diagnostics with this tag faded out instead of having * an error squiggle. */ - - Unnecessary DiagnosticTag = 1 - /** + Unnecessary DiagnosticTag = 1 // line 13567 + /* * Deprecated or obsolete code. * * Clients are allowed to rendered diagnostics with this tag strike through. */ - - Deprecated DiagnosticTag = 2 - /** - * A textual occurrence. - */ - - Text DocumentHighlightKind = 1 - /** - * Read-access of a symbol, like reading a variable. - */ - - Read DocumentHighlightKind = 2 - /** - * Write-access of a symbol, like writing to a variable. + Deprecated DiagnosticTag = 2 // line 13572 + /* + * The document diagnostic report kinds. + * + * @since 3.17.0 */ - - Write DocumentHighlightKind = 3 - /** + /* + * A diagnostic report with a full + * set of problems. + */ + DiagnosticFull DocumentDiagnosticReportKind = "full" // line 12755 + /* + * A report indicating that the last + * returned report is still accurate. + */ + DiagnosticUnchanged DocumentDiagnosticReportKind = "unchanged" // line 12760 + // A document highlight kind. + // A textual occurrence. + Text DocumentHighlightKind = 1 // line 13334 + // Read-access of a symbol, like reading a variable. + Read DocumentHighlightKind = 2 // line 13339 + // Write-access of a symbol, like writing to a variable. + Write DocumentHighlightKind = 3 // line 13344 + // Predefined error codes. + ParseError ErrorCodes = -32700 // line 12776 + InvalidRequest ErrorCodes = -32600 // line 12780 + MethodNotFound ErrorCodes = -32601 // line 12784 + InvalidParams ErrorCodes = -32602 // line 12788 + InternalError ErrorCodes = -32603 // line 12792 + /* + * Error code indicating that a server received a notification or + * request before the server has received the `initialize` request. + */ + ServerNotInitialized ErrorCodes = -32002 // line 12796 + UnknownErrorCode ErrorCodes = -32001 // line 12801 + /* * Applying the workspace change is simply aborted if one of the changes provided * fails. All operations executed before the failing operation stay executed. */ - - Abort FailureHandlingKind = "abort" - /** + Abort FailureHandlingKind = "abort" // line 13726 + /* * All operations are executed transactional. That means they either all * succeed or no changes at all are applied to the workspace. */ - - Transactional FailureHandlingKind = "transactional" - /** + Transactional FailureHandlingKind = "transactional" // line 13731 + /* * If the workspace edit contains only textual file changes they are executed transactional. * If resource changes (create, rename or delete file) are part of the change the failure * handling strategy is abort. */ - - TextOnlyTransactional FailureHandlingKind = "textOnlyTransactional" - /** + TextOnlyTransactional FailureHandlingKind = "textOnlyTransactional" // line 13736 + /* * The client tries to undo the operations already executed. But there is no * guarantee that this is succeeding. */ - - Undo FailureHandlingKind = "undo" - /** - * The file got created. - */ - - Created FileChangeType = 1 - /** - * The file got changed. - */ - - Changed FileChangeType = 2 - /** - * The file got deleted. - */ - - Deleted FileChangeType = 3 - /** - * The pattern matches a file only. - */ - - FileOp FileOperationPatternKind = "file" - /** - * The pattern matches a folder only. - */ - - FolderOp FileOperationPatternKind = "folder" - /** - * Folding range for a comment - */ - Comment FoldingRangeKind = "comment" - /** - * Folding range for a imports or includes - */ - Imports FoldingRangeKind = "imports" - /** - * Folding range for a region (e.g. `#region`) - */ - Region FoldingRangeKind = "region" - /** - * If the protocol version provided by the client can't be handled by the server. - * @deprecated This initialize error got replaced by client capabilities. There is - * no version handshake in version 3.0x - */ - - UnknownProtocolVersion InitializeError = 1 - /** - * An inlay hint that for a type annotation. - */ - - Type InlayHintKind = 1 - /** - * An inlay hint that is for a parameter. + Undo FailureHandlingKind = "undo" // line 13741 + // The file event type + // The file got created. + Created FileChangeType = 1 // line 13487 + // The file got changed. + Changed FileChangeType = 2 // line 13492 + // The file got deleted. + Deleted FileChangeType = 3 // line 13497 + /* + * A pattern kind describing if a glob pattern matches a file a folder or + * both. + * + * @since 3.16.0 */ - - Parameter InlayHintKind = 2 - /** - * The primary text to be inserted is treated as a plain string. + // The pattern matches a file only. + FilePattern FileOperationPatternKind = "file" // line 13660 + // The pattern matches a folder only. + FolderPattern FileOperationPatternKind = "folder" // line 13665 + // A set of predefined range kinds. + // Folding range for a comment + Comment FoldingRangeKind = "comment" // line 12848 + // Folding range for an import or include + Imports FoldingRangeKind = "imports" // line 12853 + // Folding range for a region (e.g. `#region`) + Region FoldingRangeKind = "region" // line 12858 + /* + * Inlay hint kinds. + * + * @since 3.17.0 */ - - PlainTextTextFormat InsertTextFormat = 1 - /** + // An inlay hint that for a type annotation. + Type InlayHintKind = 1 // line 13066 + // An inlay hint that is for a parameter. + Parameter InlayHintKind = 2 // line 13071 + /* + * Defines whether the insert text in a completion item should be interpreted as + * plain text or a snippet. + */ + // The primary text to be inserted is treated as a plain string. + PlainTextTextFormat InsertTextFormat = 1 // line 13293 + /* * The primary text to be inserted is treated as a snippet. * * A snippet can define tab stops and placeholders with `$1`, `$2` @@ -6224,18 +5875,22 @@ const ( * * See also: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax */ - - SnippetTextFormat InsertTextFormat = 2 - /** + SnippetTextFormat InsertTextFormat = 2 // line 13298 + /* + * How whitespace and indentation is handled during completion + * item insertion. + * + * @since 3.16.0 + */ + /* * The insertion or replace strings is taken as it is. If the * value is multi line the lines below the cursor will be * inserted using the indentation defined in the string value. * The client will not apply any kind of adjustments to the * string. */ - - AsIs InsertTextMode = 1 - /** + AsIs InsertTextMode = 1 // line 13313 + /* * The editor adjusts leading whitespace of new lines so that * they match the indentation up to the cursor of the line for * which the item is accepted. @@ -6244,515 +5899,261 @@ const ( * multi line completion item is indented using 2 tabs and all * following lines inserted will be indented using 2 tabs as well. */ - - AdjustIndentation InsertTextMode = 2 - /** - * Plain text is supported as a content format - */ - - PlainText MarkupKind = "plaintext" - /** - * Markdown is supported as a content format - */ - - Markdown MarkupKind = "markdown" - /** - * An error message. - */ - - Error MessageType = 1 - /** - * A warning message. - */ - - Warning MessageType = 2 - /** - * An information message. - */ - - Info MessageType = 3 - /** - * A log message. + AdjustIndentation InsertTextMode = 2 // line 13318 + /* + * A request failed but it was syntactically correct, e.g the + * method name was known and the parameters were valid. The error + * message should contain human readable information about why + * the request failed. + * + * @since 3.17.0 */ - - Log MessageType = 4 - /** - * The moniker represent a symbol that is imported into a project + RequestFailed LSPErrorCodes = -32803 // line 12816 + /* + * The server cancelled the request. This error code should + * only be used for requests that explicitly support being + * server cancellable. + * + * @since 3.17.0 */ - Import MonikerKind = "import" - /** - * The moniker represents a symbol that is exported from a project + ServerCancelled LSPErrorCodes = -32802 // line 12822 + /* + * The server detected that the content of a document got + * modified outside normal conditions. A server should + * NOT send this error code if it detects a content change + * in it unprocessed messages. The result even computed + * on an older state might still be useful for the client. + * + * If a client decides that a result is not of any use anymore + * the client should cancel the request. + */ + ContentModified LSPErrorCodes = -32801 // line 12828 + /* + * The client has canceled a request and a server as detected + * the cancel. + */ + RequestCancelled LSPErrorCodes = -32800 // line 12833 + /* + * Describes the content type that a client supports in various + * result literals like `Hover`, `ParameterInfo` or `CompletionItem`. + * + * Please note that `MarkupKinds` must not start with a `$`. This kinds + * are reserved for internal usage. + */ + // Plain text is supported as a content format + PlainText MarkupKind = "plaintext" // line 13440 + // Markdown is supported as a content format + Markdown MarkupKind = "markdown" // line 13445 + // The message type + // An error message. + Error MessageType = 1 // line 13087 + // A warning message. + Warning MessageType = 2 // line 13092 + // An information message. + Info MessageType = 3 // line 13097 + // A log message. + Log MessageType = 4 // line 13102 + /* + * The moniker kind. + * + * @since 3.16.0 */ - Export MonikerKind = "export" - /** + // The moniker represent a symbol that is imported into a project + Import MonikerKind = "import" // line 13040 + // The moniker represents a symbol that is exported from a project + Export MonikerKind = "export" // line 13045 + /* * The moniker represents a symbol that is local to a project (e.g. a local * variable of a function, a class not visible outside the project, ...) */ - Local MonikerKind = "local" - /** - * A markup-cell is formatted source that is used for display. - */ - - Markup NotebookCellKind = 1 - /** - * A code-cell is source code. - */ - - Code NotebookCellKind = 2 - /** - * Supports creating new files and folders. - */ - - Create ResourceOperationKind = "create" - /** - * Supports renaming existing files and folders. - */ - - Rename ResourceOperationKind = "rename" - /** - * Supports deleting existing files and folders. + Local MonikerKind = "local" // line 13050 + /* + * A notebook cell kind. + * + * @since 3.17.0 */ - - Delete ResourceOperationKind = "delete" - /** - * Signature help was invoked manually by the user or by a command. + // A markup-cell is formatted source that is used for display. + Markup NotebookCellKind = 1 // line 13681 + // A code-cell is source code. + Code NotebookCellKind = 2 // line 13686 + /* + * A set of predefined position encoding kinds. + * + * @since 3.17.0 */ - - SigInvoked SignatureHelpTriggerKind = 1 - /** - * Signature help was triggered by a trigger character. + // Character offsets count UTF-8 code units. + UTF8 PositionEncodingKind = "utf-8" // line 13460 + /* + * Character offsets count UTF-16 code units. + * + * This is the default and must always be supported + * by servers + */ + UTF16 PositionEncodingKind = "utf-16" // line 13465 + /* + * Character offsets count UTF-32 code units. + * + * Implementation note: these are the same as Unicode code points, + * so this `PositionEncodingKind` may also be used for an + * encoding-agnostic representation of character offsets. + */ + UTF32 PositionEncodingKind = "utf-32" // line 13470 + // Supports creating new files and folders. + Create ResourceOperationKind = "create" // line 13702 + // Supports renaming existing files and folders. + Rename ResourceOperationKind = "rename" // line 13707 + // Supports deleting existing files and folders. + Delete ResourceOperationKind = "delete" // line 13712 + /* + * A set of predefined token modifiers. This set is not fixed + * an clients can specify additional token types via the + * corresponding client capabilities. + * + * @since 3.16.0 */ - - SigTriggerCharacter SignatureHelpTriggerKind = 2 - /** - * Signature help was triggered by the cursor moving or by the document content changing. + ModDeclaration SemanticTokenModifiers = "declaration" // line 12703 + ModDefinition SemanticTokenModifiers = "definition" // line 12707 + ModReadonly SemanticTokenModifiers = "readonly" // line 12711 + ModStatic SemanticTokenModifiers = "static" // line 12715 + ModDeprecated SemanticTokenModifiers = "deprecated" // line 12719 + ModAbstract SemanticTokenModifiers = "abstract" // line 12723 + ModAsync SemanticTokenModifiers = "async" // line 12727 + ModModification SemanticTokenModifiers = "modification" // line 12731 + ModDocumentation SemanticTokenModifiers = "documentation" // line 12735 + ModDefaultLibrary SemanticTokenModifiers = "defaultLibrary" // line 12739 + /* + * A set of predefined token types. This set is not fixed + * an clients can specify additional token types via the + * corresponding client capabilities. + * + * @since 3.16.0 */ - - SigContentChange SignatureHelpTriggerKind = 3 - File SymbolKind = 1 - Module SymbolKind = 2 - Namespace SymbolKind = 3 - Package SymbolKind = 4 - Class SymbolKind = 5 - Method SymbolKind = 6 - Property SymbolKind = 7 - Field SymbolKind = 8 - Constructor SymbolKind = 9 - Enum SymbolKind = 10 - Interface SymbolKind = 11 - Function SymbolKind = 12 - Variable SymbolKind = 13 - Constant SymbolKind = 14 - String SymbolKind = 15 - Number SymbolKind = 16 - Boolean SymbolKind = 17 - Array SymbolKind = 18 - Object SymbolKind = 19 - Key SymbolKind = 20 - Null SymbolKind = 21 - EnumMember SymbolKind = 22 - Struct SymbolKind = 23 - Event SymbolKind = 24 - Operator SymbolKind = 25 - TypeParameter SymbolKind = 26 - /** - * Render a symbol as obsolete, usually using a strike-out. + NamespaceType SemanticTokenTypes = "namespace" // line 12596 + /* + * Represents a generic type. Acts as a fallback for types which can't be mapped to + * a specific type like class or enum. + */ + TypeType SemanticTokenTypes = "type" // line 12600 + ClassType SemanticTokenTypes = "class" // line 12605 + EnumType SemanticTokenTypes = "enum" // line 12609 + InterfaceType SemanticTokenTypes = "interface" // line 12613 + StructType SemanticTokenTypes = "struct" // line 12617 + TypeParameterType SemanticTokenTypes = "typeParameter" // line 12621 + ParameterType SemanticTokenTypes = "parameter" // line 12625 + VariableType SemanticTokenTypes = "variable" // line 12629 + PropertyType SemanticTokenTypes = "property" // line 12633 + EnumMemberType SemanticTokenTypes = "enumMember" // line 12637 + EventType SemanticTokenTypes = "event" // line 12641 + FunctionType SemanticTokenTypes = "function" // line 12645 + MethodType SemanticTokenTypes = "method" // line 12649 + MacroType SemanticTokenTypes = "macro" // line 12653 + KeywordType SemanticTokenTypes = "keyword" // line 12657 + ModifierType SemanticTokenTypes = "modifier" // line 12661 + CommentType SemanticTokenTypes = "comment" // line 12665 + StringType SemanticTokenTypes = "string" // line 12669 + NumberType SemanticTokenTypes = "number" // line 12673 + RegexpType SemanticTokenTypes = "regexp" // line 12677 + OperatorType SemanticTokenTypes = "operator" // line 12681 + // @since 3.17.0 + DecoratorType SemanticTokenTypes = "decorator" // line 12685 + /* + * How a signature help was triggered. + * + * @since 3.15.0 */ - - DeprecatedSymbol SymbolTag = 1 - /** + // Signature help was invoked manually by the user or by a command. + SigInvoked SignatureHelpTriggerKind = 1 // line 13613 + // Signature help was triggered by a trigger character. + SigTriggerCharacter SignatureHelpTriggerKind = 2 // line 13618 + // Signature help was triggered by the cursor moving or by the document content changing. + SigContentChange SignatureHelpTriggerKind = 3 // line 13623 + // A symbol kind. + File SymbolKind = 1 // line 12874 + Module SymbolKind = 2 // line 12878 + Namespace SymbolKind = 3 // line 12882 + Package SymbolKind = 4 // line 12886 + Class SymbolKind = 5 // line 12890 + Method SymbolKind = 6 // line 12894 + Property SymbolKind = 7 // line 12898 + Field SymbolKind = 8 // line 12902 + Constructor SymbolKind = 9 // line 12906 + Enum SymbolKind = 10 // line 12910 + Interface SymbolKind = 11 // line 12914 + Function SymbolKind = 12 // line 12918 + Variable SymbolKind = 13 // line 12922 + Constant SymbolKind = 14 // line 12926 + String SymbolKind = 15 // line 12930 + Number SymbolKind = 16 // line 12934 + Boolean SymbolKind = 17 // line 12938 + Array SymbolKind = 18 // line 12942 + Object SymbolKind = 19 // line 12946 + Key SymbolKind = 20 // line 12950 + Null SymbolKind = 21 // line 12954 + EnumMember SymbolKind = 22 // line 12958 + Struct SymbolKind = 23 // line 12962 + Event SymbolKind = 24 // line 12966 + Operator SymbolKind = 25 // line 12970 + TypeParameter SymbolKind = 26 // line 12974 + /* + * Symbol tags are extra annotations that tweak the rendering of a symbol. + * + * @since 3.16 + */ + // Render a symbol as obsolete, usually using a strike-out. + DeprecatedSymbol SymbolTag = 1 // line 12988 + // Represents reasons why a text document is saved. + /* * Manually triggered, e.g. by the user pressing save, by starting debugging, * or by an API call. */ - - Manual TextDocumentSaveReason = 1 - /** - * Automatic after a delay. - */ - - AfterDelay TextDocumentSaveReason = 2 - /** - * When the editor lost focus. - */ - - FocusOut TextDocumentSaveReason = 3 - /** - * Documents should not be synced at all. - */ - - None TextDocumentSyncKind = 0 - /** + Manual TextDocumentSaveReason = 1 // line 13142 + // Automatic after a delay. + AfterDelay TextDocumentSaveReason = 2 // line 13147 + // When the editor lost focus. + FocusOut TextDocumentSaveReason = 3 // line 13152 + /* + * Defines how the host (editor) should sync + * document changes to the language server. + */ + // Documents should not be synced at all. + None TextDocumentSyncKind = 0 // line 13117 + /* * Documents are synced by always sending the full content * of the document. */ - - Full TextDocumentSyncKind = 1 - /** + Full TextDocumentSyncKind = 1 // line 13122 + /* * Documents are synced by sending the full content on open. * After that only incremental updates to the document are * send. */ - - Incremental TextDocumentSyncKind = 2 - /** - * The moniker is only unique inside a document - */ - Document UniquenessLevel = "document" - /** - * The moniker is unique inside a project for which a dump got created - */ - Project UniquenessLevel = "project" - /** - * The moniker is unique inside the group to which a project belongs - */ - Group UniquenessLevel = "group" - /** - * The moniker is unique inside the moniker scheme. - */ - Scheme UniquenessLevel = "scheme" - /** - * The moniker is globally unique - */ - Global UniquenessLevel = "global" - /** - * Interested in create events. - */ - - WatchCreate WatchKind = 1 - /** - * Interested in change events - */ - - WatchChange WatchKind = 2 - /** - * Interested in delete events - */ - - WatchDelete WatchKind = 4 -) - -// Types created to name formal parameters and embedded structs -type ParamConfiguration struct { - ConfigurationParams - PartialResultParams -} -type ParamInitialize struct { - InitializeParams - WorkDoneProgressParams -} -type PrepareRename2Gn struct { - Range Range `json:"range"` - Placeholder string `json:"placeholder"` -} -type Workspace3Gn struct { - /** - * The client supports applying batch edits - * to the workspace by supporting the request - * 'workspace/applyEdit' - */ - ApplyEdit bool `json:"applyEdit,omitempty"` - - /** - * Capabilities specific to `WorkspaceEdit`s - */ - WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeConfiguration` notification. - */ - DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - */ - DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` - - /** - * Capabilities specific to the `workspace/symbol` request. - */ - Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` - - /** - * Capabilities specific to the `workspace/executeCommand` request. - */ - ExecuteCommand ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` - - /** - * Capabilities specific to the semantic token requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - SemanticTokens SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` - - /** - * Capabilities specific to the code lens requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - CodeLens CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` - - /** - * The client has support for file notifications/requests for user operations on files. - * - * Since 3.16.0 - */ - FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` - - /** - * Capabilities specific to the inline values requests scoped to the - * workspace. + Incremental TextDocumentSyncKind = 2 // line 13127 + Relative TokenFormat = "relative" // line 13769 + // Turn tracing off. + Off TraceValues = "off" // line 13416 + // Trace messages only. + Messages TraceValues = "messages" // line 13421 + // Verbose message tracing. + Verbose TraceValues = "verbose" // line 13426 + /* + * Moniker uniqueness level to define scope of the moniker. * - * @since 3.17.0. - */ - InlineValue InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` - - /** - * Capabilities specific to the inlay hints requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlayHint InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` - - /** - * The client has support for workspace folders - * - * @since 3.6.0 - */ - WorkspaceFolders bool `json:"workspaceFolders,omitempty"` - - /** - * The client supports `workspace/configuration` requests. - * - * @since 3.6.0 - */ - Configuration bool `json:"configuration,omitempty"` -} -type Workspace4Gn struct { - /** - * The client supports applying batch edits - * to the workspace by supporting the request - * 'workspace/applyEdit' - */ - ApplyEdit bool `json:"applyEdit,omitempty"` - - /** - * Capabilities specific to `WorkspaceEdit`s - */ - WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeConfiguration` notification. - */ - DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - */ - DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` - - /** - * Capabilities specific to the `workspace/symbol` request. - */ - Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` - - /** - * Capabilities specific to the `workspace/executeCommand` request. - */ - ExecuteCommand ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` - - /** - * Capabilities specific to the semantic token requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - SemanticTokens SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` - - /** - * Capabilities specific to the code lens requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - CodeLens CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` - - /** - * The client has support for file notifications/requests for user operations on files. - * - * Since 3.16.0 - */ - FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` - - /** - * Capabilities specific to the inline values requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlineValue InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` - - /** - * Capabilities specific to the inlay hints requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlayHint InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` - - /** - * The client has support for workspace folders - * - * @since 3.6.0 - */ - WorkspaceFolders bool `json:"workspaceFolders,omitempty"` - - /** - * The client supports `workspace/configuration` requests. - * - * @since 3.6.0 - */ - Configuration bool `json:"configuration,omitempty"` -} -type WorkspaceFolders5Gn struct { - /** - * The Server has support for workspace folders - */ - Supported bool `json:"supported,omitempty"` - - /** - * Whether the server wants to receive workspace folder - * change notifications. - * - * If a strings is provided the string is treated as a ID - * under which the notification is registered on the client - * side. The ID can be used to unregister for these events - * using the `client/unregisterCapability` request. - */ - ChangeNotifications string/*string | boolean*/ `json:"changeNotifications,omitempty"` -} -type Workspace6Gn struct { - /** - * The server is interested in notifications/requests for operations on files. - * - * @since 3.16.0 - */ - FileOperations *FileOperationOptions `json:"fileOperations,omitempty"` - - WorkspaceFolders WorkspaceFolders5Gn `json:"workspaceFolders,omitempty"` -} -type Workspace7Gn struct { - /** - * The client supports applying batch edits - * to the workspace by supporting the request - * 'workspace/applyEdit' - */ - ApplyEdit bool `json:"applyEdit,omitempty"` - - /** - * Capabilities specific to `WorkspaceEdit`s - */ - WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeConfiguration` notification. - */ - DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - */ - DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` - - /** - * Capabilities specific to the `workspace/symbol` request. - */ - Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` - - /** - * Capabilities specific to the `workspace/executeCommand` request. - */ - ExecuteCommand ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` - - /** - * Capabilities specific to the semantic token requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - SemanticTokens SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` - - /** - * Capabilities specific to the code lens requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - CodeLens CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` - - /** - * The client has support for file notifications/requests for user operations on files. - * - * Since 3.16.0 - */ - FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` - - /** - * Capabilities specific to the inline values requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlineValue InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` - - /** - * Capabilities specific to the inlay hints requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlayHint InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` - - /** - * The client has support for workspace folders - * - * @since 3.6.0 - */ - WorkspaceFolders bool `json:"workspaceFolders,omitempty"` - - /** - * The client supports `workspace/configuration` requests. - * - * @since 3.6.0 - */ - Configuration bool `json:"configuration,omitempty"` -} -type WorkspaceFolders8Gn struct { - /** - * The Server has support for workspace folders - */ - Supported bool `json:"supported,omitempty"` - - /** - * Whether the server wants to receive workspace folder - * change notifications. - * - * If a strings is provided the string is treated as a ID - * under which the notification is registered on the client - * side. The ID can be used to unregister for these events - * using the `client/unregisterCapability` request. - */ - ChangeNotifications string/*string | boolean*/ `json:"changeNotifications,omitempty"` -} -type Workspace9Gn struct { - /** - * The server is interested in notifications/requests for operations on files. - * - * @since 3.16.0 + * @since 3.16.0 */ - FileOperations *FileOperationOptions `json:"fileOperations,omitempty"` - - WorkspaceFolders WorkspaceFolders8Gn `json:"workspaceFolders,omitempty"` -} + // The moniker is only unique inside a document + Document UniquenessLevel = "document" // line 13004 + // The moniker is unique inside a project for which a dump got created + Project UniquenessLevel = "project" // line 13009 + // The moniker is unique inside the group to which a project belongs + Group UniquenessLevel = "group" // line 13014 + // The moniker is unique inside the moniker scheme. + Scheme UniquenessLevel = "scheme" // line 13019 + // The moniker is globally unique + Global UniquenessLevel = "global" // line 13024 + // Interested in create events. + WatchCreate WatchKind = 1 // line 13512 + // Interested in change events + WatchChange WatchKind = 2 // line 13517 + // Interested in delete events + WatchDelete WatchKind = 4 // line 13522 +) diff --git a/gopls/internal/lsp/protocol/tsserver.go b/gopls/internal/lsp/protocol/tsserver.go index a26e50cf4e5..2478586acdf 100644 --- a/gopls/internal/lsp/protocol/tsserver.go +++ b/gopls/internal/lsp/protocol/tsserver.go @@ -1,273 +1,257 @@ -// Copyright 2019 The Go Authors. All rights reserved. +// Copyright 2019-2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Code generated (see typescript/README.md) DO NOT EDIT. - package protocol -// Package protocol contains data types and code for LSP json rpcs -// generated automatically from vscode-languageserver-node -// commit: 696f9285bf849b73745682fdb1c1feac73eb8772 -// last fetched Fri Apr 01 2022 10:53:41 GMT-0400 (Eastern Daylight Time) +// Code generated from version 3.17.0 of protocol/metaModel.json. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Tue Sep 13 10:45:25 2022) +// Code generated; DO NOT EDIT. import ( "context" "encoding/json" - "fmt" "golang.org/x/tools/internal/jsonrpc2" ) type Server interface { - DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error - WorkDoneProgressCancel(context.Context, *WorkDoneProgressCancelParams) error - DidCreateFiles(context.Context, *CreateFilesParams) error - DidRenameFiles(context.Context, *RenameFilesParams) error - DidDeleteFiles(context.Context, *DeleteFilesParams) error - Initialized(context.Context, *InitializedParams) error - Exit(context.Context) error - DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error - DidOpen(context.Context, *DidOpenTextDocumentParams) error - DidChange(context.Context, *DidChangeTextDocumentParams) error - DidClose(context.Context, *DidCloseTextDocumentParams) error - DidSave(context.Context, *DidSaveTextDocumentParams) error - WillSave(context.Context, *WillSaveTextDocumentParams) error - DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error - DidOpenNotebookDocument(context.Context, *DidOpenNotebookDocumentParams) error - DidChangeNotebookDocument(context.Context, *DidChangeNotebookDocumentParams) error - DidSaveNotebookDocument(context.Context, *DidSaveNotebookDocumentParams) error - DidCloseNotebookDocument(context.Context, *DidCloseNotebookDocumentParams) error - SetTrace(context.Context, *SetTraceParams) error - LogTrace(context.Context, *LogTraceParams) error - Implementation(context.Context, *ImplementationParams) (Definition /*Definition | DefinitionLink[] | null*/, error) - TypeDefinition(context.Context, *TypeDefinitionParams) (Definition /*Definition | DefinitionLink[] | null*/, error) - DocumentColor(context.Context, *DocumentColorParams) ([]ColorInformation, error) - ColorPresentation(context.Context, *ColorPresentationParams) ([]ColorPresentation, error) - FoldingRange(context.Context, *FoldingRangeParams) ([]FoldingRange /*FoldingRange[] | null*/, error) - Declaration(context.Context, *DeclarationParams) (Declaration /*Declaration | DeclarationLink[] | null*/, error) - SelectionRange(context.Context, *SelectionRangeParams) ([]SelectionRange /*SelectionRange[] | null*/, error) - PrepareCallHierarchy(context.Context, *CallHierarchyPrepareParams) ([]CallHierarchyItem /*CallHierarchyItem[] | null*/, error) - IncomingCalls(context.Context, *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall /*CallHierarchyIncomingCall[] | null*/, error) - OutgoingCalls(context.Context, *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall /*CallHierarchyOutgoingCall[] | null*/, error) - SemanticTokensFull(context.Context, *SemanticTokensParams) (*SemanticTokens /*SemanticTokens | null*/, error) - SemanticTokensFullDelta(context.Context, *SemanticTokensDeltaParams) (interface{} /* SemanticTokens | SemanticTokensDelta | float64*/, error) - SemanticTokensRange(context.Context, *SemanticTokensRangeParams) (*SemanticTokens /*SemanticTokens | null*/, error) - SemanticTokensRefresh(context.Context) error - LinkedEditingRange(context.Context, *LinkedEditingRangeParams) (*LinkedEditingRanges /*LinkedEditingRanges | null*/, error) - WillCreateFiles(context.Context, *CreateFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) - WillRenameFiles(context.Context, *RenameFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) - WillDeleteFiles(context.Context, *DeleteFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) - Moniker(context.Context, *MonikerParams) ([]Moniker /*Moniker[] | null*/, error) - PrepareTypeHierarchy(context.Context, *TypeHierarchyPrepareParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) - Supertypes(context.Context, *TypeHierarchySupertypesParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) - Subtypes(context.Context, *TypeHierarchySubtypesParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) - InlineValue(context.Context, *InlineValueParams) ([]InlineValue /*InlineValue[] | null*/, error) - InlineValueRefresh(context.Context) error - InlayHint(context.Context, *InlayHintParams) ([]InlayHint /*InlayHint[] | null*/, error) - Resolve(context.Context, *InlayHint) (*InlayHint, error) - InlayHintRefresh(context.Context) error - Initialize(context.Context, *ParamInitialize) (*InitializeResult, error) - Shutdown(context.Context) error - WillSaveWaitUntil(context.Context, *WillSaveTextDocumentParams) ([]TextEdit /*TextEdit[] | null*/, error) - Completion(context.Context, *CompletionParams) (*CompletionList /*CompletionItem[] | CompletionList | null*/, error) - ResolveCompletionItem(context.Context, *CompletionItem) (*CompletionItem, error) - Hover(context.Context, *HoverParams) (*Hover /*Hover | null*/, error) - SignatureHelp(context.Context, *SignatureHelpParams) (*SignatureHelp /*SignatureHelp | null*/, error) - Definition(context.Context, *DefinitionParams) (Definition /*Definition | DefinitionLink[] | null*/, error) - References(context.Context, *ReferenceParams) ([]Location /*Location[] | null*/, error) - DocumentHighlight(context.Context, *DocumentHighlightParams) ([]DocumentHighlight /*DocumentHighlight[] | null*/, error) - DocumentSymbol(context.Context, *DocumentSymbolParams) ([]interface{} /*SymbolInformation[] | DocumentSymbol[] | null*/, error) - CodeAction(context.Context, *CodeActionParams) ([]CodeAction /*(Command | CodeAction)[] | null*/, error) - ResolveCodeAction(context.Context, *CodeAction) (*CodeAction, error) - Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation /*SymbolInformation[] | WorkspaceSymbol[] | null*/, error) - ResolveWorkspaceSymbol(context.Context, *WorkspaceSymbol) (*WorkspaceSymbol, error) - CodeLens(context.Context, *CodeLensParams) ([]CodeLens /*CodeLens[] | null*/, error) - ResolveCodeLens(context.Context, *CodeLens) (*CodeLens, error) - CodeLensRefresh(context.Context) error - DocumentLink(context.Context, *DocumentLinkParams) ([]DocumentLink /*DocumentLink[] | null*/, error) - ResolveDocumentLink(context.Context, *DocumentLink) (*DocumentLink, error) - Formatting(context.Context, *DocumentFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) - RangeFormatting(context.Context, *DocumentRangeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) - OnTypeFormatting(context.Context, *DocumentOnTypeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) - Rename(context.Context, *RenameParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) - PrepareRename(context.Context, *PrepareRenameParams) (*PrepareRename2Gn /*Range | { range: Range; placeholder: string } | { defaultBehavior: boolean } | null*/, error) - ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{} /* LSPAny | void | float64*/, error) - Diagnostic(context.Context, *string) (*string, error) - DiagnosticWorkspace(context.Context, *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) - DiagnosticRefresh(context.Context) error + Progress(context.Context, *ProgressParams) error // $/progress + SetTrace(context.Context, *SetTraceParams) error // $/setTrace + IncomingCalls(context.Context, *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) // callHierarchy/incomingCalls + OutgoingCalls(context.Context, *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) // callHierarchy/outgoingCalls + ResolveCodeAction(context.Context, *CodeAction) (*CodeAction, error) // codeAction/resolve + ResolveCodeLens(context.Context, *CodeLens) (*CodeLens, error) // codeLens/resolve + ResolveCompletionItem(context.Context, *CompletionItem) (*CompletionItem, error) // completionItem/resolve + ResolveDocumentLink(context.Context, *DocumentLink) (*DocumentLink, error) // documentLink/resolve + Exit(context.Context) error // exit + Initialize(context.Context, *ParamInitialize) (*InitializeResult, error) // initialize + Initialized(context.Context, *InitializedParams) error // initialized + Resolve(context.Context, *InlayHint) (*InlayHint, error) // inlayHint/resolve + DidChangeNotebookDocument(context.Context, *DidChangeNotebookDocumentParams) error // notebookDocument/didChange + DidCloseNotebookDocument(context.Context, *DidCloseNotebookDocumentParams) error // notebookDocument/didClose + DidOpenNotebookDocument(context.Context, *DidOpenNotebookDocumentParams) error // notebookDocument/didOpen + DidSaveNotebookDocument(context.Context, *DidSaveNotebookDocumentParams) error // notebookDocument/didSave + Shutdown(context.Context) error // shutdown + CodeAction(context.Context, *CodeActionParams) ([]CodeAction, error) // textDocument/codeAction + CodeLens(context.Context, *CodeLensParams) ([]CodeLens, error) // textDocument/codeLens + ColorPresentation(context.Context, *ColorPresentationParams) ([]ColorPresentation, error) // textDocument/colorPresentation + Completion(context.Context, *CompletionParams) (*CompletionList, error) // textDocument/completion + Declaration(context.Context, *DeclarationParams) (*Or_textDocument_declaration, error) // textDocument/declaration + Definition(context.Context, *DefinitionParams) ([]Location, error) // textDocument/definition + Diagnostic(context.Context, *string) (*string, error) // textDocument/diagnostic + DidChange(context.Context, *DidChangeTextDocumentParams) error // textDocument/didChange + DidClose(context.Context, *DidCloseTextDocumentParams) error // textDocument/didClose + DidOpen(context.Context, *DidOpenTextDocumentParams) error // textDocument/didOpen + DidSave(context.Context, *DidSaveTextDocumentParams) error // textDocument/didSave + DocumentColor(context.Context, *DocumentColorParams) ([]ColorInformation, error) // textDocument/documentColor + DocumentHighlight(context.Context, *DocumentHighlightParams) ([]DocumentHighlight, error) // textDocument/documentHighlight + DocumentLink(context.Context, *DocumentLinkParams) ([]DocumentLink, error) // textDocument/documentLink + DocumentSymbol(context.Context, *DocumentSymbolParams) ([]interface{}, error) // textDocument/documentSymbol + FoldingRange(context.Context, *FoldingRangeParams) ([]FoldingRange, error) // textDocument/foldingRange + Formatting(context.Context, *DocumentFormattingParams) ([]TextEdit, error) // textDocument/formatting + Hover(context.Context, *HoverParams) (*Hover, error) // textDocument/hover + Implementation(context.Context, *ImplementationParams) ([]Location, error) // textDocument/implementation + InlayHint(context.Context, *InlayHintParams) ([]InlayHint, error) // textDocument/inlayHint + InlineValue(context.Context, *InlineValueParams) ([]InlineValue, error) // textDocument/inlineValue + LinkedEditingRange(context.Context, *LinkedEditingRangeParams) (*LinkedEditingRanges, error) // textDocument/linkedEditingRange + Moniker(context.Context, *MonikerParams) ([]Moniker, error) // textDocument/moniker + OnTypeFormatting(context.Context, *DocumentOnTypeFormattingParams) ([]TextEdit, error) // textDocument/onTypeFormatting + PrepareCallHierarchy(context.Context, *CallHierarchyPrepareParams) ([]CallHierarchyItem, error) // textDocument/prepareCallHierarchy + PrepareRename(context.Context, *PrepareRenameParams) (*PrepareRename2Gn, error) // textDocument/prepareRename + PrepareTypeHierarchy(context.Context, *TypeHierarchyPrepareParams) ([]TypeHierarchyItem, error) // textDocument/prepareTypeHierarchy + RangeFormatting(context.Context, *DocumentRangeFormattingParams) ([]TextEdit, error) // textDocument/rangeFormatting + References(context.Context, *ReferenceParams) ([]Location, error) // textDocument/references + Rename(context.Context, *RenameParams) (*WorkspaceEdit, error) // textDocument/rename + SelectionRange(context.Context, *SelectionRangeParams) ([]SelectionRange, error) // textDocument/selectionRange + SemanticTokensFull(context.Context, *SemanticTokensParams) (*SemanticTokens, error) // textDocument/semanticTokens/full + SemanticTokensFullDelta(context.Context, *SemanticTokensDeltaParams) (interface{}, error) // textDocument/semanticTokens/full/delta + SemanticTokensRange(context.Context, *SemanticTokensRangeParams) (*SemanticTokens, error) // textDocument/semanticTokens/range + SignatureHelp(context.Context, *SignatureHelpParams) (*SignatureHelp, error) // textDocument/signatureHelp + TypeDefinition(context.Context, *TypeDefinitionParams) ([]Location, error) // textDocument/typeDefinition + WillSave(context.Context, *WillSaveTextDocumentParams) error // textDocument/willSave + WillSaveWaitUntil(context.Context, *WillSaveTextDocumentParams) ([]TextEdit, error) // textDocument/willSaveWaitUntil + Subtypes(context.Context, *TypeHierarchySubtypesParams) ([]TypeHierarchyItem, error) // typeHierarchy/subtypes + Supertypes(context.Context, *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) // typeHierarchy/supertypes + WorkDoneProgressCancel(context.Context, *WorkDoneProgressCancelParams) error // window/workDoneProgress/cancel + DiagnosticWorkspace(context.Context, *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) // workspace/diagnostic + DiagnosticRefresh(context.Context) error // workspace/diagnostic/refresh + DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error // workspace/didChangeConfiguration + DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error // workspace/didChangeWatchedFiles + DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error // workspace/didChangeWorkspaceFolders + DidCreateFiles(context.Context, *CreateFilesParams) error // workspace/didCreateFiles + DidDeleteFiles(context.Context, *DeleteFilesParams) error // workspace/didDeleteFiles + DidRenameFiles(context.Context, *RenameFilesParams) error // workspace/didRenameFiles + ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{}, error) // workspace/executeCommand + InlayHintRefresh(context.Context) error // workspace/inlayHint/refresh + InlineValueRefresh(context.Context) error // workspace/inlineValue/refresh + SemanticTokensRefresh(context.Context) error // workspace/semanticTokens/refresh + Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error) // workspace/symbol + WillCreateFiles(context.Context, *CreateFilesParams) (*WorkspaceEdit, error) // workspace/willCreateFiles + WillDeleteFiles(context.Context, *DeleteFilesParams) (*WorkspaceEdit, error) // workspace/willDeleteFiles + WillRenameFiles(context.Context, *RenameFilesParams) (*WorkspaceEdit, error) // workspace/willRenameFiles + ResolveWorkspaceSymbol(context.Context, *WorkspaceSymbol) (*WorkspaceSymbol, error) // workspaceSymbol/resolve NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) } func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { switch r.Method() { - case "workspace/didChangeWorkspaceFolders": // notif - var params DidChangeWorkspaceFoldersParams + case "$/progress": + var params ProgressParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidChangeWorkspaceFolders(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "window/workDoneProgress/cancel": // notif - var params WorkDoneProgressCancelParams + err := server.Progress(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "$/setTrace": + var params SetTraceParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.WorkDoneProgressCancel(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/didCreateFiles": // notif - var params CreateFilesParams + err := server.SetTrace(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "callHierarchy/incomingCalls": + var params CallHierarchyIncomingCallsParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidCreateFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/didRenameFiles": // notif - var params RenameFilesParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) + resp, err := server.IncomingCalls(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) } - err := server.DidRenameFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/didDeleteFiles": // notif - var params DeleteFilesParams + return true, reply(ctx, resp, nil) // 146 + case "callHierarchy/outgoingCalls": + var params CallHierarchyOutgoingCallsParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidDeleteFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "initialized": // notif - var params InitializedParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) + resp, err := server.OutgoingCalls(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) } - err := server.Initialized(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "exit": // notif - err := server.Exit(ctx) - return true, reply(ctx, nil, err) - case "workspace/didChangeConfiguration": // notif - var params DidChangeConfigurationParams + return true, reply(ctx, resp, nil) // 146 + case "codeAction/resolve": + var params CodeAction if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidChangeConfiguration(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/didOpen": // notif - var params DidOpenTextDocumentParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) + resp, err := server.ResolveCodeAction(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) } - err := server.DidOpen(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/didChange": // notif - var params DidChangeTextDocumentParams + return true, reply(ctx, resp, nil) // 146 + case "codeLens/resolve": + var params CodeLens if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidChange(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/didClose": // notif - var params DidCloseTextDocumentParams + resp, err := server.ResolveCodeLens(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "completionItem/resolve": + var params CompletionItem if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidClose(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/didSave": // notif - var params DidSaveTextDocumentParams + resp, err := server.ResolveCompletionItem(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "documentLink/resolve": + var params DocumentLink if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidSave(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/willSave": // notif - var params WillSaveTextDocumentParams + resp, err := server.ResolveDocumentLink(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "exit": + err := server.Exit(ctx) + return true, reply(ctx, nil, err) // 236 + case "initialize": + var params ParamInitialize if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.WillSave(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/didChangeWatchedFiles": // notif - var params DidChangeWatchedFilesParams + resp, err := server.Initialize(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "initialized": + var params InitializedParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidChangeWatchedFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "notebookDocument/didOpen": // notif - var params DidOpenNotebookDocumentParams + err := server.Initialized(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "inlayHint/resolve": + var params InlayHint if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidOpenNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "notebookDocument/didChange": // notif + resp, err := server.Resolve(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "notebookDocument/didChange": var params DidChangeNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidChangeNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "notebookDocument/didSave": // notif - var params DidSaveNotebookDocumentParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - } - err := server.DidSaveNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "notebookDocument/didClose": // notif + return true, reply(ctx, nil, err) // 231 + case "notebookDocument/didClose": var params DidCloseNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidCloseNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "$/setTrace": // notif - var params SetTraceParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - } - err := server.SetTrace(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "$/logTrace": // notif - var params LogTraceParams + return true, reply(ctx, nil, err) // 231 + case "notebookDocument/didOpen": + var params DidOpenNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.LogTrace(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/implementation": // req - var params ImplementationParams + err := server.DidOpenNotebookDocument(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "notebookDocument/didSave": + var params DidSaveNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Implementation(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/typeDefinition": // req - var params TypeDefinitionParams + err := server.DidSaveNotebookDocument(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "shutdown": + err := server.Shutdown(ctx) + return true, reply(ctx, nil, err) // 176 + case "textDocument/codeAction": + var params CodeActionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.TypeDefinition(ctx, ¶ms) + resp, err := server.CodeAction(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/documentColor": // req - var params DocumentColorParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/codeLens": + var params CodeLensParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DocumentColor(ctx, ¶ms) + resp, err := server.CodeLens(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/colorPresentation": // req + return true, reply(ctx, resp, nil) // 146 + case "textDocument/colorPresentation": var params ColorPresentationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) @@ -276,18 +260,18 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/foldingRange": // req - var params FoldingRangeParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/completion": + var params CompletionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.FoldingRange(ctx, ¶ms) + resp, err := server.Completion(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/declaration": // req + return true, reply(ctx, resp, nil) // 146 + case "textDocument/declaration": var params DeclarationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) @@ -296,164 +280,146 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/selectionRange": // req - var params SelectionRangeParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/definition": + var params DefinitionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SelectionRange(ctx, ¶ms) + resp, err := server.Definition(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/prepareCallHierarchy": // req - var params CallHierarchyPrepareParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/diagnostic": + var params string if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.PrepareCallHierarchy(ctx, ¶ms) + resp, err := server.Diagnostic(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "callHierarchy/incomingCalls": // req - var params CallHierarchyIncomingCallsParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/didChange": + var params DidChangeTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.IncomingCalls(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "callHierarchy/outgoingCalls": // req - var params CallHierarchyOutgoingCallsParams + err := server.DidChange(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/didClose": + var params DidCloseTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.OutgoingCalls(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/semanticTokens/full": // req - var params SemanticTokensParams + err := server.DidClose(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/didOpen": + var params DidOpenTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SemanticTokensFull(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/semanticTokens/full/delta": // req - var params SemanticTokensDeltaParams + err := server.DidOpen(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/didSave": + var params DidSaveTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SemanticTokensFullDelta(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/semanticTokens/range": // req - var params SemanticTokensRangeParams + err := server.DidSave(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/documentColor": + var params DocumentColorParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SemanticTokensRange(ctx, ¶ms) + resp, err := server.DocumentColor(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/semanticTokens/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.SemanticTokensRefresh(ctx) - return true, reply(ctx, nil, err) - case "textDocument/linkedEditingRange": // req - var params LinkedEditingRangeParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/documentHighlight": + var params DocumentHighlightParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.LinkedEditingRange(ctx, ¶ms) + resp, err := server.DocumentHighlight(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/willCreateFiles": // req - var params CreateFilesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/documentLink": + var params DocumentLinkParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.WillCreateFiles(ctx, ¶ms) + resp, err := server.DocumentLink(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/willRenameFiles": // req - var params RenameFilesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/documentSymbol": + var params DocumentSymbolParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.WillRenameFiles(ctx, ¶ms) + resp, err := server.DocumentSymbol(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/willDeleteFiles": // req - var params DeleteFilesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/foldingRange": + var params FoldingRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.WillDeleteFiles(ctx, ¶ms) + resp, err := server.FoldingRange(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/moniker": // req - var params MonikerParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/formatting": + var params DocumentFormattingParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Moniker(ctx, ¶ms) + resp, err := server.Formatting(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/prepareTypeHierarchy": // req - var params TypeHierarchyPrepareParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/hover": + var params HoverParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.PrepareTypeHierarchy(ctx, ¶ms) + resp, err := server.Hover(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "typeHierarchy/supertypes": // req - var params TypeHierarchySupertypesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/implementation": + var params ImplementationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Supertypes(ctx, ¶ms) + resp, err := server.Implementation(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "typeHierarchy/subtypes": // req - var params TypeHierarchySubtypesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/inlayHint": + var params InlayHintParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Subtypes(ctx, ¶ms) + resp, err := server.InlayHint(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/inlineValue": // req + return true, reply(ctx, resp, nil) // 146 + case "textDocument/inlineValue": var params InlineValueParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) @@ -462,828 +428,755 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/inlineValue/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.InlineValueRefresh(ctx) - return true, reply(ctx, nil, err) - case "textDocument/inlayHint": // req - var params InlayHintParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/linkedEditingRange": + var params LinkedEditingRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.InlayHint(ctx, ¶ms) + resp, err := server.LinkedEditingRange(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "inlayHint/resolve": // req - var params InlayHint + return true, reply(ctx, resp, nil) // 146 + case "textDocument/moniker": + var params MonikerParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Resolve(ctx, ¶ms) + resp, err := server.Moniker(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/inlayHint/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.InlayHintRefresh(ctx) - return true, reply(ctx, nil, err) - case "initialize": // req - var params ParamInitialize + return true, reply(ctx, resp, nil) // 146 + case "textDocument/onTypeFormatting": + var params DocumentOnTypeFormattingParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - if _, ok := err.(*json.UnmarshalTypeError); !ok { - return true, sendParseError(ctx, reply, err) - } + return true, sendParseError(ctx, reply, err) } - resp, err := server.Initialize(ctx, ¶ms) + resp, err := server.OnTypeFormatting(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "shutdown": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.Shutdown(ctx) - return true, reply(ctx, nil, err) - case "textDocument/willSaveWaitUntil": // req - var params WillSaveTextDocumentParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/prepareCallHierarchy": + var params CallHierarchyPrepareParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.WillSaveWaitUntil(ctx, ¶ms) + resp, err := server.PrepareCallHierarchy(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/completion": // req - var params CompletionParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/prepareRename": + var params PrepareRenameParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Completion(ctx, ¶ms) + resp, err := server.PrepareRename(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "completionItem/resolve": // req - var params CompletionItem + return true, reply(ctx, resp, nil) // 146 + case "textDocument/prepareTypeHierarchy": + var params TypeHierarchyPrepareParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveCompletionItem(ctx, ¶ms) + resp, err := server.PrepareTypeHierarchy(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/hover": // req - var params HoverParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/rangeFormatting": + var params DocumentRangeFormattingParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Hover(ctx, ¶ms) + resp, err := server.RangeFormatting(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/signatureHelp": // req - var params SignatureHelpParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/references": + var params ReferenceParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SignatureHelp(ctx, ¶ms) + resp, err := server.References(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/definition": // req - var params DefinitionParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/rename": + var params RenameParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Definition(ctx, ¶ms) + resp, err := server.Rename(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/references": // req - var params ReferenceParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/selectionRange": + var params SelectionRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.References(ctx, ¶ms) + resp, err := server.SelectionRange(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/documentHighlight": // req - var params DocumentHighlightParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/semanticTokens/full": + var params SemanticTokensParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DocumentHighlight(ctx, ¶ms) + resp, err := server.SemanticTokensFull(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/documentSymbol": // req - var params DocumentSymbolParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/semanticTokens/full/delta": + var params SemanticTokensDeltaParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DocumentSymbol(ctx, ¶ms) + resp, err := server.SemanticTokensFullDelta(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/codeAction": // req - var params CodeActionParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/semanticTokens/range": + var params SemanticTokensRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.CodeAction(ctx, ¶ms) + resp, err := server.SemanticTokensRange(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "codeAction/resolve": // req - var params CodeAction + return true, reply(ctx, resp, nil) // 146 + case "textDocument/signatureHelp": + var params SignatureHelpParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveCodeAction(ctx, ¶ms) + resp, err := server.SignatureHelp(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/symbol": // req - var params WorkspaceSymbolParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/typeDefinition": + var params TypeDefinitionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Symbol(ctx, ¶ms) + resp, err := server.TypeDefinition(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspaceSymbol/resolve": // req - var params WorkspaceSymbol + return true, reply(ctx, resp, nil) // 146 + case "textDocument/willSave": + var params WillSaveTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveWorkspaceSymbol(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/codeLens": // req - var params CodeLensParams + err := server.WillSave(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/willSaveWaitUntil": + var params WillSaveTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.CodeLens(ctx, ¶ms) + resp, err := server.WillSaveWaitUntil(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "codeLens/resolve": // req - var params CodeLens + return true, reply(ctx, resp, nil) // 146 + case "typeHierarchy/subtypes": + var params TypeHierarchySubtypesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveCodeLens(ctx, ¶ms) + resp, err := server.Subtypes(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/codeLens/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.CodeLensRefresh(ctx) - return true, reply(ctx, nil, err) - case "textDocument/documentLink": // req - var params DocumentLinkParams + return true, reply(ctx, resp, nil) // 146 + case "typeHierarchy/supertypes": + var params TypeHierarchySupertypesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DocumentLink(ctx, ¶ms) + resp, err := server.Supertypes(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "documentLink/resolve": // req - var params DocumentLink + return true, reply(ctx, resp, nil) // 146 + case "window/workDoneProgress/cancel": + var params WorkDoneProgressCancelParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveDocumentLink(ctx, ¶ms) + err := server.WorkDoneProgressCancel(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/diagnostic": + var params WorkspaceDiagnosticParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + resp, err := server.DiagnosticWorkspace(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/formatting": // req - var params DocumentFormattingParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/diagnostic/refresh": + err := server.DiagnosticRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/didChangeConfiguration": + var params DidChangeConfigurationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Formatting(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) + err := server.DidChangeConfiguration(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didChangeWatchedFiles": + var params DidChangeWatchedFilesParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) } - return true, reply(ctx, resp, nil) - case "textDocument/rangeFormatting": // req - var params DocumentRangeFormattingParams + err := server.DidChangeWatchedFiles(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didChangeWorkspaceFolders": + var params DidChangeWorkspaceFoldersParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.RangeFormatting(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) + err := server.DidChangeWorkspaceFolders(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didCreateFiles": + var params CreateFilesParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) } - return true, reply(ctx, resp, nil) - case "textDocument/onTypeFormatting": // req - var params DocumentOnTypeFormattingParams + err := server.DidCreateFiles(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didDeleteFiles": + var params DeleteFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.OnTypeFormatting(ctx, ¶ms) + err := server.DidDeleteFiles(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didRenameFiles": + var params RenameFilesParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + err := server.DidRenameFiles(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/executeCommand": + var params ExecuteCommandParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + resp, err := server.ExecuteCommand(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/rename": // req - var params RenameParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/inlayHint/refresh": + err := server.InlayHintRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/inlineValue/refresh": + err := server.InlineValueRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/semanticTokens/refresh": + err := server.SemanticTokensRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/symbol": + var params WorkspaceSymbolParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Rename(ctx, ¶ms) + resp, err := server.Symbol(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/prepareRename": // req - var params PrepareRenameParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/willCreateFiles": + var params CreateFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.PrepareRename(ctx, ¶ms) + resp, err := server.WillCreateFiles(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/executeCommand": // req - var params ExecuteCommandParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/willDeleteFiles": + var params DeleteFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ExecuteCommand(ctx, ¶ms) + resp, err := server.WillDeleteFiles(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/diagnostic": // req - var params string + return true, reply(ctx, resp, nil) // 146 + case "workspace/willRenameFiles": + var params RenameFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Diagnostic(ctx, ¶ms) + resp, err := server.WillRenameFiles(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/diagnostic": // req - var params WorkspaceDiagnosticParams + return true, reply(ctx, resp, nil) // 146 + case "workspaceSymbol/resolve": + var params WorkspaceSymbol if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DiagnosticWorkspace(ctx, ¶ms) + resp, err := server.ResolveWorkspaceSymbol(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/diagnostic/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.DiagnosticRefresh(ctx) - return true, reply(ctx, nil, err) - + return true, reply(ctx, resp, nil) // 146 default: return false, nil } } -func (s *serverDispatcher) DidChangeWorkspaceFolders(ctx context.Context, params *DidChangeWorkspaceFoldersParams) error { - return s.sender.Notify(ctx, "workspace/didChangeWorkspaceFolders", params) -} - -func (s *serverDispatcher) WorkDoneProgressCancel(ctx context.Context, params *WorkDoneProgressCancelParams) error { - return s.sender.Notify(ctx, "window/workDoneProgress/cancel", params) -} - -func (s *serverDispatcher) DidCreateFiles(ctx context.Context, params *CreateFilesParams) error { - return s.sender.Notify(ctx, "workspace/didCreateFiles", params) -} - -func (s *serverDispatcher) DidRenameFiles(ctx context.Context, params *RenameFilesParams) error { - return s.sender.Notify(ctx, "workspace/didRenameFiles", params) -} - -func (s *serverDispatcher) DidDeleteFiles(ctx context.Context, params *DeleteFilesParams) error { - return s.sender.Notify(ctx, "workspace/didDeleteFiles", params) -} - -func (s *serverDispatcher) Initialized(ctx context.Context, params *InitializedParams) error { - return s.sender.Notify(ctx, "initialized", params) -} - -func (s *serverDispatcher) Exit(ctx context.Context) error { - return s.sender.Notify(ctx, "exit", nil) -} - -func (s *serverDispatcher) DidChangeConfiguration(ctx context.Context, params *DidChangeConfigurationParams) error { - return s.sender.Notify(ctx, "workspace/didChangeConfiguration", params) -} - -func (s *serverDispatcher) DidOpen(ctx context.Context, params *DidOpenTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/didOpen", params) -} - -func (s *serverDispatcher) DidChange(ctx context.Context, params *DidChangeTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/didChange", params) -} - -func (s *serverDispatcher) DidClose(ctx context.Context, params *DidCloseTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/didClose", params) -} - -func (s *serverDispatcher) DidSave(ctx context.Context, params *DidSaveTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/didSave", params) -} - -func (s *serverDispatcher) WillSave(ctx context.Context, params *WillSaveTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/willSave", params) -} - -func (s *serverDispatcher) DidChangeWatchedFiles(ctx context.Context, params *DidChangeWatchedFilesParams) error { - return s.sender.Notify(ctx, "workspace/didChangeWatchedFiles", params) -} - -func (s *serverDispatcher) DidOpenNotebookDocument(ctx context.Context, params *DidOpenNotebookDocumentParams) error { - return s.sender.Notify(ctx, "notebookDocument/didOpen", params) -} - -func (s *serverDispatcher) DidChangeNotebookDocument(ctx context.Context, params *DidChangeNotebookDocumentParams) error { - return s.sender.Notify(ctx, "notebookDocument/didChange", params) -} - -func (s *serverDispatcher) DidSaveNotebookDocument(ctx context.Context, params *DidSaveNotebookDocumentParams) error { - return s.sender.Notify(ctx, "notebookDocument/didSave", params) -} - -func (s *serverDispatcher) DidCloseNotebookDocument(ctx context.Context, params *DidCloseNotebookDocumentParams) error { - return s.sender.Notify(ctx, "notebookDocument/didClose", params) -} - +func (s *serverDispatcher) Progress(ctx context.Context, params *ProgressParams) error { + return s.sender.Notify(ctx, "$/progress", params) +} // 244 func (s *serverDispatcher) SetTrace(ctx context.Context, params *SetTraceParams) error { return s.sender.Notify(ctx, "$/setTrace", params) -} - -func (s *serverDispatcher) LogTrace(ctx context.Context, params *LogTraceParams) error { - return s.sender.Notify(ctx, "$/logTrace", params) -} -func (s *serverDispatcher) Implementation(ctx context.Context, params *ImplementationParams) (Definition /*Definition | DefinitionLink[] | null*/, error) { - var result Definition /*Definition | DefinitionLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/implementation", params, &result); err != nil { +} // 244 +func (s *serverDispatcher) IncomingCalls(ctx context.Context, params *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) { + var result []CallHierarchyIncomingCall + if err := s.sender.Call(ctx, "callHierarchy/incomingCalls", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) TypeDefinition(ctx context.Context, params *TypeDefinitionParams) (Definition /*Definition | DefinitionLink[] | null*/, error) { - var result Definition /*Definition | DefinitionLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/typeDefinition", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) OutgoingCalls(ctx context.Context, params *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) { + var result []CallHierarchyOutgoingCall + if err := s.sender.Call(ctx, "callHierarchy/outgoingCalls", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DocumentColor(ctx context.Context, params *DocumentColorParams) ([]ColorInformation, error) { - var result []ColorInformation - if err := s.sender.Call(ctx, "textDocument/documentColor", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveCodeAction(ctx context.Context, params *CodeAction) (*CodeAction, error) { + var result *CodeAction + if err := s.sender.Call(ctx, "codeAction/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ColorPresentation(ctx context.Context, params *ColorPresentationParams) ([]ColorPresentation, error) { - var result []ColorPresentation - if err := s.sender.Call(ctx, "textDocument/colorPresentation", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveCodeLens(ctx context.Context, params *CodeLens) (*CodeLens, error) { + var result *CodeLens + if err := s.sender.Call(ctx, "codeLens/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) FoldingRange(ctx context.Context, params *FoldingRangeParams) ([]FoldingRange /*FoldingRange[] | null*/, error) { - var result []FoldingRange /*FoldingRange[] | null*/ - if err := s.sender.Call(ctx, "textDocument/foldingRange", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveCompletionItem(ctx context.Context, params *CompletionItem) (*CompletionItem, error) { + var result *CompletionItem + if err := s.sender.Call(ctx, "completionItem/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Declaration(ctx context.Context, params *DeclarationParams) (Declaration /*Declaration | DeclarationLink[] | null*/, error) { - var result Declaration /*Declaration | DeclarationLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/declaration", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveDocumentLink(ctx context.Context, params *DocumentLink) (*DocumentLink, error) { + var result *DocumentLink + if err := s.sender.Call(ctx, "documentLink/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SelectionRange(ctx context.Context, params *SelectionRangeParams) ([]SelectionRange /*SelectionRange[] | null*/, error) { - var result []SelectionRange /*SelectionRange[] | null*/ - if err := s.sender.Call(ctx, "textDocument/selectionRange", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Exit(ctx context.Context) error { + return s.sender.Notify(ctx, "exit", nil) +} // 249 +func (s *serverDispatcher) Initialize(ctx context.Context, params *ParamInitialize) (*InitializeResult, error) { + var result *InitializeResult + if err := s.sender.Call(ctx, "initialize", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) PrepareCallHierarchy(ctx context.Context, params *CallHierarchyPrepareParams) ([]CallHierarchyItem /*CallHierarchyItem[] | null*/, error) { - var result []CallHierarchyItem /*CallHierarchyItem[] | null*/ - if err := s.sender.Call(ctx, "textDocument/prepareCallHierarchy", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Initialized(ctx context.Context, params *InitializedParams) error { + return s.sender.Notify(ctx, "initialized", params) +} // 244 +func (s *serverDispatcher) Resolve(ctx context.Context, params *InlayHint) (*InlayHint, error) { + var result *InlayHint + if err := s.sender.Call(ctx, "inlayHint/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) IncomingCalls(ctx context.Context, params *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall /*CallHierarchyIncomingCall[] | null*/, error) { - var result []CallHierarchyIncomingCall /*CallHierarchyIncomingCall[] | null*/ - if err := s.sender.Call(ctx, "callHierarchy/incomingCalls", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DidChangeNotebookDocument(ctx context.Context, params *DidChangeNotebookDocumentParams) error { + return s.sender.Notify(ctx, "notebookDocument/didChange", params) +} // 244 +func (s *serverDispatcher) DidCloseNotebookDocument(ctx context.Context, params *DidCloseNotebookDocumentParams) error { + return s.sender.Notify(ctx, "notebookDocument/didClose", params) +} // 244 +func (s *serverDispatcher) DidOpenNotebookDocument(ctx context.Context, params *DidOpenNotebookDocumentParams) error { + return s.sender.Notify(ctx, "notebookDocument/didOpen", params) +} // 244 +func (s *serverDispatcher) DidSaveNotebookDocument(ctx context.Context, params *DidSaveNotebookDocumentParams) error { + return s.sender.Notify(ctx, "notebookDocument/didSave", params) +} // 244 +func (s *serverDispatcher) Shutdown(ctx context.Context) error { + return s.sender.Call(ctx, "shutdown", nil, nil) +} // 209 +func (s *serverDispatcher) CodeAction(ctx context.Context, params *CodeActionParams) ([]CodeAction, error) { + var result []CodeAction + if err := s.sender.Call(ctx, "textDocument/codeAction", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) OutgoingCalls(ctx context.Context, params *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall /*CallHierarchyOutgoingCall[] | null*/, error) { - var result []CallHierarchyOutgoingCall /*CallHierarchyOutgoingCall[] | null*/ - if err := s.sender.Call(ctx, "callHierarchy/outgoingCalls", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) CodeLens(ctx context.Context, params *CodeLensParams) ([]CodeLens, error) { + var result []CodeLens + if err := s.sender.Call(ctx, "textDocument/codeLens", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SemanticTokensFull(ctx context.Context, params *SemanticTokensParams) (*SemanticTokens /*SemanticTokens | null*/, error) { - var result *SemanticTokens /*SemanticTokens | null*/ - if err := s.sender.Call(ctx, "textDocument/semanticTokens/full", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ColorPresentation(ctx context.Context, params *ColorPresentationParams) ([]ColorPresentation, error) { + var result []ColorPresentation + if err := s.sender.Call(ctx, "textDocument/colorPresentation", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SemanticTokensFullDelta(ctx context.Context, params *SemanticTokensDeltaParams) (interface{} /* SemanticTokens | SemanticTokensDelta | float64*/, error) { - var result interface{} /* SemanticTokens | SemanticTokensDelta | float64*/ - if err := s.sender.Call(ctx, "textDocument/semanticTokens/full/delta", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Completion(ctx context.Context, params *CompletionParams) (*CompletionList, error) { + var result *CompletionList + if err := s.sender.Call(ctx, "textDocument/completion", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SemanticTokensRange(ctx context.Context, params *SemanticTokensRangeParams) (*SemanticTokens /*SemanticTokens | null*/, error) { - var result *SemanticTokens /*SemanticTokens | null*/ - if err := s.sender.Call(ctx, "textDocument/semanticTokens/range", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Declaration(ctx context.Context, params *DeclarationParams) (*Or_textDocument_declaration, error) { + var result *Or_textDocument_declaration + if err := s.sender.Call(ctx, "textDocument/declaration", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SemanticTokensRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/semanticTokens/refresh", nil, nil) -} - -func (s *serverDispatcher) LinkedEditingRange(ctx context.Context, params *LinkedEditingRangeParams) (*LinkedEditingRanges /*LinkedEditingRanges | null*/, error) { - var result *LinkedEditingRanges /*LinkedEditingRanges | null*/ - if err := s.sender.Call(ctx, "textDocument/linkedEditingRange", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Definition(ctx context.Context, params *DefinitionParams) ([]Location, error) { + var result []Location + if err := s.sender.Call(ctx, "textDocument/definition", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) WillCreateFiles(ctx context.Context, params *CreateFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) { - var result *WorkspaceEdit /*WorkspaceEdit | null*/ - if err := s.sender.Call(ctx, "workspace/willCreateFiles", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Diagnostic(ctx context.Context, params *string) (*string, error) { + var result *string + if err := s.sender.Call(ctx, "textDocument/diagnostic", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) WillRenameFiles(ctx context.Context, params *RenameFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) { - var result *WorkspaceEdit /*WorkspaceEdit | null*/ - if err := s.sender.Call(ctx, "workspace/willRenameFiles", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DidChange(ctx context.Context, params *DidChangeTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/didChange", params) +} // 244 +func (s *serverDispatcher) DidClose(ctx context.Context, params *DidCloseTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/didClose", params) +} // 244 +func (s *serverDispatcher) DidOpen(ctx context.Context, params *DidOpenTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/didOpen", params) +} // 244 +func (s *serverDispatcher) DidSave(ctx context.Context, params *DidSaveTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/didSave", params) +} // 244 +func (s *serverDispatcher) DocumentColor(ctx context.Context, params *DocumentColorParams) ([]ColorInformation, error) { + var result []ColorInformation + if err := s.sender.Call(ctx, "textDocument/documentColor", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) WillDeleteFiles(ctx context.Context, params *DeleteFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) { - var result *WorkspaceEdit /*WorkspaceEdit | null*/ - if err := s.sender.Call(ctx, "workspace/willDeleteFiles", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DocumentHighlight(ctx context.Context, params *DocumentHighlightParams) ([]DocumentHighlight, error) { + var result []DocumentHighlight + if err := s.sender.Call(ctx, "textDocument/documentHighlight", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Moniker(ctx context.Context, params *MonikerParams) ([]Moniker /*Moniker[] | null*/, error) { - var result []Moniker /*Moniker[] | null*/ - if err := s.sender.Call(ctx, "textDocument/moniker", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DocumentLink(ctx context.Context, params *DocumentLinkParams) ([]DocumentLink, error) { + var result []DocumentLink + if err := s.sender.Call(ctx, "textDocument/documentLink", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) PrepareTypeHierarchy(ctx context.Context, params *TypeHierarchyPrepareParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) { - var result []TypeHierarchyItem /*TypeHierarchyItem[] | null*/ - if err := s.sender.Call(ctx, "textDocument/prepareTypeHierarchy", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DocumentSymbol(ctx context.Context, params *DocumentSymbolParams) ([]interface{}, error) { + var result []interface{} + if err := s.sender.Call(ctx, "textDocument/documentSymbol", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Supertypes(ctx context.Context, params *TypeHierarchySupertypesParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) { - var result []TypeHierarchyItem /*TypeHierarchyItem[] | null*/ - if err := s.sender.Call(ctx, "typeHierarchy/supertypes", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) FoldingRange(ctx context.Context, params *FoldingRangeParams) ([]FoldingRange, error) { + var result []FoldingRange + if err := s.sender.Call(ctx, "textDocument/foldingRange", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Subtypes(ctx context.Context, params *TypeHierarchySubtypesParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) { - var result []TypeHierarchyItem /*TypeHierarchyItem[] | null*/ - if err := s.sender.Call(ctx, "typeHierarchy/subtypes", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Formatting(ctx context.Context, params *DocumentFormattingParams) ([]TextEdit, error) { + var result []TextEdit + if err := s.sender.Call(ctx, "textDocument/formatting", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) InlineValue(ctx context.Context, params *InlineValueParams) ([]InlineValue /*InlineValue[] | null*/, error) { - var result []InlineValue /*InlineValue[] | null*/ - if err := s.sender.Call(ctx, "textDocument/inlineValue", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Hover(ctx context.Context, params *HoverParams) (*Hover, error) { + var result *Hover + if err := s.sender.Call(ctx, "textDocument/hover", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) InlineValueRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/inlineValue/refresh", nil, nil) -} - -func (s *serverDispatcher) InlayHint(ctx context.Context, params *InlayHintParams) ([]InlayHint /*InlayHint[] | null*/, error) { - var result []InlayHint /*InlayHint[] | null*/ - if err := s.sender.Call(ctx, "textDocument/inlayHint", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Implementation(ctx context.Context, params *ImplementationParams) ([]Location, error) { + var result []Location + if err := s.sender.Call(ctx, "textDocument/implementation", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Resolve(ctx context.Context, params *InlayHint) (*InlayHint, error) { - var result *InlayHint - if err := s.sender.Call(ctx, "inlayHint/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) InlayHint(ctx context.Context, params *InlayHintParams) ([]InlayHint, error) { + var result []InlayHint + if err := s.sender.Call(ctx, "textDocument/inlayHint", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) InlayHintRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/inlayHint/refresh", nil, nil) -} - -func (s *serverDispatcher) Initialize(ctx context.Context, params *ParamInitialize) (*InitializeResult, error) { - var result *InitializeResult - if err := s.sender.Call(ctx, "initialize", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) InlineValue(ctx context.Context, params *InlineValueParams) ([]InlineValue, error) { + var result []InlineValue + if err := s.sender.Call(ctx, "textDocument/inlineValue", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Shutdown(ctx context.Context) error { - return s.sender.Call(ctx, "shutdown", nil, nil) -} - -func (s *serverDispatcher) WillSaveWaitUntil(ctx context.Context, params *WillSaveTextDocumentParams) ([]TextEdit /*TextEdit[] | null*/, error) { - var result []TextEdit /*TextEdit[] | null*/ - if err := s.sender.Call(ctx, "textDocument/willSaveWaitUntil", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) LinkedEditingRange(ctx context.Context, params *LinkedEditingRangeParams) (*LinkedEditingRanges, error) { + var result *LinkedEditingRanges + if err := s.sender.Call(ctx, "textDocument/linkedEditingRange", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Completion(ctx context.Context, params *CompletionParams) (*CompletionList /*CompletionItem[] | CompletionList | null*/, error) { - var result *CompletionList /*CompletionItem[] | CompletionList | null*/ - if err := s.sender.Call(ctx, "textDocument/completion", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Moniker(ctx context.Context, params *MonikerParams) ([]Moniker, error) { + var result []Moniker + if err := s.sender.Call(ctx, "textDocument/moniker", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveCompletionItem(ctx context.Context, params *CompletionItem) (*CompletionItem, error) { - var result *CompletionItem - if err := s.sender.Call(ctx, "completionItem/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) OnTypeFormatting(ctx context.Context, params *DocumentOnTypeFormattingParams) ([]TextEdit, error) { + var result []TextEdit + if err := s.sender.Call(ctx, "textDocument/onTypeFormatting", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Hover(ctx context.Context, params *HoverParams) (*Hover /*Hover | null*/, error) { - var result *Hover /*Hover | null*/ - if err := s.sender.Call(ctx, "textDocument/hover", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) PrepareCallHierarchy(ctx context.Context, params *CallHierarchyPrepareParams) ([]CallHierarchyItem, error) { + var result []CallHierarchyItem + if err := s.sender.Call(ctx, "textDocument/prepareCallHierarchy", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SignatureHelp(ctx context.Context, params *SignatureHelpParams) (*SignatureHelp /*SignatureHelp | null*/, error) { - var result *SignatureHelp /*SignatureHelp | null*/ - if err := s.sender.Call(ctx, "textDocument/signatureHelp", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) PrepareRename(ctx context.Context, params *PrepareRenameParams) (*PrepareRename2Gn, error) { + var result *PrepareRename2Gn + if err := s.sender.Call(ctx, "textDocument/prepareRename", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Definition(ctx context.Context, params *DefinitionParams) (Definition /*Definition | DefinitionLink[] | null*/, error) { - var result Definition /*Definition | DefinitionLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/definition", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) PrepareTypeHierarchy(ctx context.Context, params *TypeHierarchyPrepareParams) ([]TypeHierarchyItem, error) { + var result []TypeHierarchyItem + if err := s.sender.Call(ctx, "textDocument/prepareTypeHierarchy", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) References(ctx context.Context, params *ReferenceParams) ([]Location /*Location[] | null*/, error) { - var result []Location /*Location[] | null*/ - if err := s.sender.Call(ctx, "textDocument/references", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) RangeFormatting(ctx context.Context, params *DocumentRangeFormattingParams) ([]TextEdit, error) { + var result []TextEdit + if err := s.sender.Call(ctx, "textDocument/rangeFormatting", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DocumentHighlight(ctx context.Context, params *DocumentHighlightParams) ([]DocumentHighlight /*DocumentHighlight[] | null*/, error) { - var result []DocumentHighlight /*DocumentHighlight[] | null*/ - if err := s.sender.Call(ctx, "textDocument/documentHighlight", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) References(ctx context.Context, params *ReferenceParams) ([]Location, error) { + var result []Location + if err := s.sender.Call(ctx, "textDocument/references", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DocumentSymbol(ctx context.Context, params *DocumentSymbolParams) ([]interface{} /*SymbolInformation[] | DocumentSymbol[] | null*/, error) { - var result []interface{} /*SymbolInformation[] | DocumentSymbol[] | null*/ - if err := s.sender.Call(ctx, "textDocument/documentSymbol", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Rename(ctx context.Context, params *RenameParams) (*WorkspaceEdit, error) { + var result *WorkspaceEdit + if err := s.sender.Call(ctx, "textDocument/rename", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) CodeAction(ctx context.Context, params *CodeActionParams) ([]CodeAction /*(Command | CodeAction)[] | null*/, error) { - var result []CodeAction /*(Command | CodeAction)[] | null*/ - if err := s.sender.Call(ctx, "textDocument/codeAction", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SelectionRange(ctx context.Context, params *SelectionRangeParams) ([]SelectionRange, error) { + var result []SelectionRange + if err := s.sender.Call(ctx, "textDocument/selectionRange", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveCodeAction(ctx context.Context, params *CodeAction) (*CodeAction, error) { - var result *CodeAction - if err := s.sender.Call(ctx, "codeAction/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SemanticTokensFull(ctx context.Context, params *SemanticTokensParams) (*SemanticTokens, error) { + var result *SemanticTokens + if err := s.sender.Call(ctx, "textDocument/semanticTokens/full", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation /*SymbolInformation[] | WorkspaceSymbol[] | null*/, error) { - var result []SymbolInformation /*SymbolInformation[] | WorkspaceSymbol[] | null*/ - if err := s.sender.Call(ctx, "workspace/symbol", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SemanticTokensFullDelta(ctx context.Context, params *SemanticTokensDeltaParams) (interface{}, error) { + var result interface{} + if err := s.sender.Call(ctx, "textDocument/semanticTokens/full/delta", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveWorkspaceSymbol(ctx context.Context, params *WorkspaceSymbol) (*WorkspaceSymbol, error) { - var result *WorkspaceSymbol - if err := s.sender.Call(ctx, "workspaceSymbol/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SemanticTokensRange(ctx context.Context, params *SemanticTokensRangeParams) (*SemanticTokens, error) { + var result *SemanticTokens + if err := s.sender.Call(ctx, "textDocument/semanticTokens/range", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) CodeLens(ctx context.Context, params *CodeLensParams) ([]CodeLens /*CodeLens[] | null*/, error) { - var result []CodeLens /*CodeLens[] | null*/ - if err := s.sender.Call(ctx, "textDocument/codeLens", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SignatureHelp(ctx context.Context, params *SignatureHelpParams) (*SignatureHelp, error) { + var result *SignatureHelp + if err := s.sender.Call(ctx, "textDocument/signatureHelp", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveCodeLens(ctx context.Context, params *CodeLens) (*CodeLens, error) { - var result *CodeLens - if err := s.sender.Call(ctx, "codeLens/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) TypeDefinition(ctx context.Context, params *TypeDefinitionParams) ([]Location, error) { + var result []Location + if err := s.sender.Call(ctx, "textDocument/typeDefinition", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) CodeLensRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/codeLens/refresh", nil, nil) -} - -func (s *serverDispatcher) DocumentLink(ctx context.Context, params *DocumentLinkParams) ([]DocumentLink /*DocumentLink[] | null*/, error) { - var result []DocumentLink /*DocumentLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/documentLink", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WillSave(ctx context.Context, params *WillSaveTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/willSave", params) +} // 244 +func (s *serverDispatcher) WillSaveWaitUntil(ctx context.Context, params *WillSaveTextDocumentParams) ([]TextEdit, error) { + var result []TextEdit + if err := s.sender.Call(ctx, "textDocument/willSaveWaitUntil", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveDocumentLink(ctx context.Context, params *DocumentLink) (*DocumentLink, error) { - var result *DocumentLink - if err := s.sender.Call(ctx, "documentLink/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Subtypes(ctx context.Context, params *TypeHierarchySubtypesParams) ([]TypeHierarchyItem, error) { + var result []TypeHierarchyItem + if err := s.sender.Call(ctx, "typeHierarchy/subtypes", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Formatting(ctx context.Context, params *DocumentFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) { - var result []TextEdit /*TextEdit[] | null*/ - if err := s.sender.Call(ctx, "textDocument/formatting", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Supertypes(ctx context.Context, params *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) { + var result []TypeHierarchyItem + if err := s.sender.Call(ctx, "typeHierarchy/supertypes", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) RangeFormatting(ctx context.Context, params *DocumentRangeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) { - var result []TextEdit /*TextEdit[] | null*/ - if err := s.sender.Call(ctx, "textDocument/rangeFormatting", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WorkDoneProgressCancel(ctx context.Context, params *WorkDoneProgressCancelParams) error { + return s.sender.Notify(ctx, "window/workDoneProgress/cancel", params) +} // 244 +func (s *serverDispatcher) DiagnosticWorkspace(ctx context.Context, params *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) { + var result *WorkspaceDiagnosticReport + if err := s.sender.Call(ctx, "workspace/diagnostic", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) OnTypeFormatting(ctx context.Context, params *DocumentOnTypeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) { - var result []TextEdit /*TextEdit[] | null*/ - if err := s.sender.Call(ctx, "textDocument/onTypeFormatting", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DiagnosticRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/diagnostic/refresh", nil, nil) +} // 209 +func (s *serverDispatcher) DidChangeConfiguration(ctx context.Context, params *DidChangeConfigurationParams) error { + return s.sender.Notify(ctx, "workspace/didChangeConfiguration", params) +} // 244 +func (s *serverDispatcher) DidChangeWatchedFiles(ctx context.Context, params *DidChangeWatchedFilesParams) error { + return s.sender.Notify(ctx, "workspace/didChangeWatchedFiles", params) +} // 244 +func (s *serverDispatcher) DidChangeWorkspaceFolders(ctx context.Context, params *DidChangeWorkspaceFoldersParams) error { + return s.sender.Notify(ctx, "workspace/didChangeWorkspaceFolders", params) +} // 244 +func (s *serverDispatcher) DidCreateFiles(ctx context.Context, params *CreateFilesParams) error { + return s.sender.Notify(ctx, "workspace/didCreateFiles", params) +} // 244 +func (s *serverDispatcher) DidDeleteFiles(ctx context.Context, params *DeleteFilesParams) error { + return s.sender.Notify(ctx, "workspace/didDeleteFiles", params) +} // 244 +func (s *serverDispatcher) DidRenameFiles(ctx context.Context, params *RenameFilesParams) error { + return s.sender.Notify(ctx, "workspace/didRenameFiles", params) +} // 244 +func (s *serverDispatcher) ExecuteCommand(ctx context.Context, params *ExecuteCommandParams) (interface{}, error) { + var result interface{} + if err := s.sender.Call(ctx, "workspace/executeCommand", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Rename(ctx context.Context, params *RenameParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) { - var result *WorkspaceEdit /*WorkspaceEdit | null*/ - if err := s.sender.Call(ctx, "textDocument/rename", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) InlayHintRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/inlayHint/refresh", nil, nil) +} // 209 +func (s *serverDispatcher) InlineValueRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/inlineValue/refresh", nil, nil) +} // 209 +func (s *serverDispatcher) SemanticTokensRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/semanticTokens/refresh", nil, nil) +} // 209 +func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) { + var result []SymbolInformation + if err := s.sender.Call(ctx, "workspace/symbol", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) PrepareRename(ctx context.Context, params *PrepareRenameParams) (*PrepareRename2Gn /*Range | { range: Range; placeholder: string } | { defaultBehavior: boolean } | null*/, error) { - var result *PrepareRename2Gn /*Range | { range: Range; placeholder: string } | { defaultBehavior: boolean } | null*/ - if err := s.sender.Call(ctx, "textDocument/prepareRename", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WillCreateFiles(ctx context.Context, params *CreateFilesParams) (*WorkspaceEdit, error) { + var result *WorkspaceEdit + if err := s.sender.Call(ctx, "workspace/willCreateFiles", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ExecuteCommand(ctx context.Context, params *ExecuteCommandParams) (interface{} /* LSPAny | void | float64*/, error) { - var result interface{} /* LSPAny | void | float64*/ - if err := s.sender.Call(ctx, "workspace/executeCommand", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WillDeleteFiles(ctx context.Context, params *DeleteFilesParams) (*WorkspaceEdit, error) { + var result *WorkspaceEdit + if err := s.sender.Call(ctx, "workspace/willDeleteFiles", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Diagnostic(ctx context.Context, params *string) (*string, error) { - var result *string - if err := s.sender.Call(ctx, "textDocument/diagnostic", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WillRenameFiles(ctx context.Context, params *RenameFilesParams) (*WorkspaceEdit, error) { + var result *WorkspaceEdit + if err := s.sender.Call(ctx, "workspace/willRenameFiles", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DiagnosticWorkspace(ctx context.Context, params *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) { - var result *WorkspaceDiagnosticReport - if err := s.sender.Call(ctx, "workspace/diagnostic", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveWorkspaceSymbol(ctx context.Context, params *WorkspaceSymbol) (*WorkspaceSymbol, error) { + var result *WorkspaceSymbol + if err := s.sender.Call(ctx, "workspaceSymbol/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DiagnosticRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/diagnostic/refresh", nil, nil) -} - +} // 169 func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { var result interface{} if err := s.sender.Call(ctx, method, params, &result); err != nil { diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go index 25ef5654d9c..a344b5933e6 100644 --- a/gopls/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -10,10 +10,10 @@ import ( "fmt" "sync" - "golang.org/x/tools/internal/jsonrpc2" "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/internal/jsonrpc2" "golang.org/x/tools/internal/span" ) @@ -123,7 +123,7 @@ type pendingModificationSet struct { changes []source.FileModification } -func (s *Server) workDoneProgressCancel(params *protocol.WorkDoneProgressCancelParams) error { +func (s *Server) workDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error { return s.progress.Cancel(params.Token) } diff --git a/gopls/internal/lsp/server_gen.go b/gopls/internal/lsp/server_gen.go index 42635c69c29..8f4ab10a71f 100644 --- a/gopls/internal/lsp/server_gen.go +++ b/gopls/internal/lsp/server_gen.go @@ -20,10 +20,6 @@ func (s *Server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) return s.codeLens(ctx, params) } -func (s *Server) CodeLensRefresh(context.Context) error { - return notImplemented("CodeLensRefresh") -} - func (s *Server) ColorPresentation(context.Context, *protocol.ColorPresentationParams) ([]protocol.ColorPresentation, error) { return nil, notImplemented("ColorPresentation") } @@ -32,11 +28,11 @@ func (s *Server) Completion(ctx context.Context, params *protocol.CompletionPara return s.completion(ctx, params) } -func (s *Server) Declaration(context.Context, *protocol.DeclarationParams) (protocol.Declaration, error) { +func (s *Server) Declaration(context.Context, *protocol.DeclarationParams) (*protocol.Or_textDocument_declaration, error) { return nil, notImplemented("Declaration") } -func (s *Server) Definition(ctx context.Context, params *protocol.DefinitionParams) (protocol.Definition, error) { +func (s *Server) Definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) { return s.definition(ctx, params) } @@ -144,7 +140,7 @@ func (s *Server) Hover(ctx context.Context, params *protocol.HoverParams) (*prot return s.hover(ctx, params) } -func (s *Server) Implementation(ctx context.Context, params *protocol.ImplementationParams) (protocol.Definition, error) { +func (s *Server) Implementation(ctx context.Context, params *protocol.ImplementationParams) ([]protocol.Location, error) { return s.implementation(ctx, params) } @@ -180,10 +176,6 @@ func (s *Server) LinkedEditingRange(context.Context, *protocol.LinkedEditingRang return nil, notImplemented("LinkedEditingRange") } -func (s *Server) LogTrace(context.Context, *protocol.LogTraceParams) error { - return notImplemented("LogTrace") -} - func (s *Server) Moniker(context.Context, *protocol.MonikerParams) ([]protocol.Moniker, error) { return nil, notImplemented("Moniker") } @@ -212,6 +204,10 @@ func (s *Server) PrepareTypeHierarchy(context.Context, *protocol.TypeHierarchyPr return nil, notImplemented("PrepareTypeHierarchy") } +func (s *Server) Progress(context.Context, *protocol.ProgressParams) error { + return notImplemented("Progress") +} + func (s *Server) RangeFormatting(context.Context, *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) { return nil, notImplemented("RangeFormatting") } @@ -292,7 +288,7 @@ func (s *Server) Symbol(ctx context.Context, params *protocol.WorkspaceSymbolPar return s.symbol(ctx, params) } -func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) (protocol.Definition, error) { +func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) { return s.typeDefinition(ctx, params) } @@ -317,5 +313,5 @@ func (s *Server) WillSaveWaitUntil(context.Context, *protocol.WillSaveTextDocume } func (s *Server) WorkDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error { - return s.workDoneProgressCancel(params) + return s.workDoneProgressCancel(ctx, params) } diff --git a/gopls/test/json_test.go b/gopls/test/json_test.go index 69b7e485ddf..993af3095a1 100644 --- a/gopls/test/json_test.go +++ b/gopls/test/json_test.go @@ -99,7 +99,7 @@ func allDeltas(t *testing.T, v [][]int, repls ...string) { } func tryChange(start, end int, repl string) error { - var p, q protocol.InitializeParams + var p, q protocol.ParamInitialize mod := input[:start] + repl + input[end:] excerpt := func() (string, string) { a := start - 5 From 0398b3de2bc82fc5d2685aca84922f732704d284 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 17 Aug 2022 11:05:40 -0400 Subject: [PATCH 244/723] internal/gocommand: add instrumentation for hanging go commands When a go command hangs during gopls regression tests, print out additional information about processes and file descriptors. For golang/go#54461 Change-Id: I92aa4665e9056d15a274c154fce2783bed79718e Reviewed-on: https://go-review.googlesource.com/c/tools/+/424075 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/regtest/regtest.go | 4 ++ internal/gocommand/invoke.go | 80 +++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/gopls/internal/lsp/regtest/regtest.go b/gopls/internal/lsp/regtest/regtest.go index 2698aaba743..b2ef3575e5d 100644 --- a/gopls/internal/lsp/regtest/regtest.go +++ b/gopls/internal/lsp/regtest/regtest.go @@ -17,6 +17,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/cmd" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/tool" @@ -94,6 +95,9 @@ func DefaultModes() Mode { // Main sets up and tears down the shared regtest state. func Main(m *testing.M, hook func(*source.Options)) { + // golang/go#54461: enable additional debugging around hanging Go commands. + gocommand.DebugHangingGoCommands = true + // If this magic environment variable is set, run gopls instead of the test // suite. See the documentation for runTestAsGoplsEnvvar for more details. if os.Getenv(runTestAsGoplsEnvvar) == "true" { diff --git a/internal/gocommand/invoke.go b/internal/gocommand/invoke.go index 67256dc3974..5e445663fa6 100644 --- a/internal/gocommand/invoke.go +++ b/internal/gocommand/invoke.go @@ -10,8 +10,10 @@ import ( "context" "fmt" "io" + "log" "os" "regexp" + "runtime" "strconv" "strings" "sync" @@ -232,6 +234,12 @@ func (i *Invocation) run(ctx context.Context, stdout, stderr io.Writer) error { return runCmdContext(ctx, cmd) } +// DebugHangingGoCommands may be set by tests to enable additional +// instrumentation (including panics) for debugging hanging Go commands. +// +// See golang/go#54461 for details. +var DebugHangingGoCommands = false + // runCmdContext is like exec.CommandContext except it sends os.Interrupt // before os.Kill. func runCmdContext(ctx context.Context, cmd *exec.Cmd) error { @@ -243,11 +251,24 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) error { resChan <- cmd.Wait() }() - select { - case err := <-resChan: - return err - case <-ctx.Done(): + // If we're interested in debugging hanging Go commands, stop waiting after a + // minute and panic with interesting information. + if DebugHangingGoCommands { + select { + case err := <-resChan: + return err + case <-time.After(1 * time.Minute): + HandleHangingGoCommand() + case <-ctx.Done(): + } + } else { + select { + case err := <-resChan: + return err + case <-ctx.Done(): + } } + // Cancelled. Interrupt and see if it ends voluntarily. cmd.Process.Signal(os.Interrupt) select { @@ -256,10 +277,59 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) error { case <-time.After(time.Second): } // Didn't shut down in response to interrupt. Kill it hard. - cmd.Process.Kill() + if err := cmd.Process.Kill(); err != nil && DebugHangingGoCommands { + // Don't panic here as this reliably fails on windows with EINVAL. + log.Printf("error killing the Go command: %v", err) + } + + // See above: only wait for a minute if we're debugging hanging Go commands. + if DebugHangingGoCommands { + select { + case err := <-resChan: + return err + case <-time.After(1 * time.Minute): + HandleHangingGoCommand() + } + } return <-resChan } +func HandleHangingGoCommand() { + switch runtime.GOOS { + case "linux", "darwin", "freebsd", "netbsd": + fmt.Fprintln(os.Stderr, `DETECTED A HANGING GO COMMAND + +The gopls test runner has detected a hanging go command. In order to debug +this, the output of ps and lsof/fstat is printed below. + +See golang/go#54461 for more details.`) + + fmt.Fprintln(os.Stderr, "\nps axo ppid,pid,command:") + fmt.Fprintln(os.Stderr, "-------------------------") + psCmd := exec.Command("ps", "axo", "ppid,pid,command") + psCmd.Stdout = os.Stderr + psCmd.Stderr = os.Stderr + if err := psCmd.Run(); err != nil { + panic(fmt.Sprintf("running ps: %v", err)) + } + + listFiles := "lsof" + if runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" { + listFiles = "fstat" + } + + fmt.Fprintln(os.Stderr, "\n"+listFiles+":") + fmt.Fprintln(os.Stderr, "-----") + listFilesCmd := exec.Command(listFiles) + listFilesCmd.Stdout = os.Stderr + listFilesCmd.Stderr = os.Stderr + if err := listFilesCmd.Run(); err != nil { + panic(fmt.Sprintf("running %s: %v", listFiles, err)) + } + } + panic("detected hanging go command: see golang/go#54461 for more details") +} + func cmdDebugStr(cmd *exec.Cmd) string { env := make(map[string]string) for _, kv := range cmd.Env { From cdd69867e3096c2c37d44fd2b12a57c7f3a53bf3 Mon Sep 17 00:00:00 2001 From: Peter Weinbergr Date: Thu, 15 Sep 2022 14:30:25 -0400 Subject: [PATCH 245/723] gopls/tsprotocol: make Disabled in CodeAction optional The new LSP stubs were returning a "disabled" struct in CodeActions, which is wrong, and this CL fixes. Tested by hand. Fixes golang/go#55080 Change-Id: Id38df18f1e3f20ef2aa1cb1adfe96dacacad9ad6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431216 TryBot-Result: Gopher Robot Run-TryBot: Peter Weinberger gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/protocol/tsprotocol.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go index 09dfb48663e..79ed309da39 100644 --- a/gopls/internal/lsp/protocol/tsprotocol.go +++ b/gopls/internal/lsp/protocol/tsprotocol.go @@ -297,7 +297,7 @@ type CodeAction struct { // line 5401 * * @since 3.16.0 */ - Disabled PDisabledMsg_textDocument_codeAction `json:"disabled,omitempty"` + Disabled *PDisabledMsg_textDocument_codeAction `json:"disabled,omitempty"` // The workspace edit this code action performs. Edit WorkspaceEdit `json:"edit,omitempty"` /* From a61f20e1aa172e30522c65973ead5509cecf01c2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 15 Sep 2022 10:45:13 -0400 Subject: [PATCH 246/723] internal/gocommand: tweak debugging for hanging go commands Add a TODO and wait for a shorter period of time following Kill, per post-submit advice from bcmills on CL 424075. For golang/go#54461 Change-Id: Ia0e388c0119660844dad32629ebca4f122fded12 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431075 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Bryan Mills --- internal/gocommand/invoke.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/gocommand/invoke.go b/internal/gocommand/invoke.go index 5e445663fa6..2327f45b565 100644 --- a/internal/gocommand/invoke.go +++ b/internal/gocommand/invoke.go @@ -276,18 +276,21 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) error { return err case <-time.After(time.Second): } + // Didn't shut down in response to interrupt. Kill it hard. + // TODO(rfindley): per advice from bcmills@, it may be better to send SIGQUIT + // on certain platforms, such as unix. if err := cmd.Process.Kill(); err != nil && DebugHangingGoCommands { // Don't panic here as this reliably fails on windows with EINVAL. log.Printf("error killing the Go command: %v", err) } - // See above: only wait for a minute if we're debugging hanging Go commands. + // See above: don't wait indefinitely if we're debugging hanging Go commands. if DebugHangingGoCommands { select { case err := <-resChan: return err - case <-time.After(1 * time.Minute): + case <-time.After(10 * time.Second): // a shorter wait as resChan should return quickly following Kill HandleHangingGoCommand() } } From 45ad958c90d3151a985237bdb081b88a0f517409 Mon Sep 17 00:00:00 2001 From: Peter Weinbergr Date: Fri, 16 Sep 2022 13:22:56 -0400 Subject: [PATCH 247/723] gopls/internal/lsp/protocol: fix json tags and indirect some zero values Some of the json tags, especially for constructed structs, had improper json tags (e.g., Range *Range `json:"Range"`, not `json:"range"). These now start with lower-case letters. Also, an optional field which is a struct type could be either F FsType `json:"f,omitempty"`, or F *FsType `json:"f,omitempty". The second is generally better, as zero values should not be sent, but there are many cases where the existing code needs the first, to avoid allocations. That is, if we're sending an empty optional field, there should be a *, but if we're intializing a complicated structure (x.A.B.C.D = true, with B and C optional) existing code wants no *. (These two cases could conflict, and then the existing code would have to be changed.) Fixes golang/go#55099 Change-Id: I947a69322bcddf4c963d34662b72b464b738d17c Reviewed-on: https://go-review.googlesource.com/c/tools/+/431218 Run-TryBot: Peter Weinberger TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/protocol/tsclient.go | 2 +- gopls/internal/lsp/protocol/tsjson.go | 2 +- gopls/internal/lsp/protocol/tsprotocol.go | 360 +++++++++++----------- gopls/internal/lsp/protocol/tsserver.go | 2 +- 4 files changed, 183 insertions(+), 183 deletions(-) diff --git a/gopls/internal/lsp/protocol/tsclient.go b/gopls/internal/lsp/protocol/tsclient.go index 6dd81596bca..b8f11b50f37 100644 --- a/gopls/internal/lsp/protocol/tsclient.go +++ b/gopls/internal/lsp/protocol/tsclient.go @@ -5,7 +5,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Tue Sep 13 10:45:25 2022) +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) // Code generated; DO NOT EDIT. import ( diff --git a/gopls/internal/lsp/protocol/tsjson.go b/gopls/internal/lsp/protocol/tsjson.go index a418df45e1d..3c9781ade0b 100644 --- a/gopls/internal/lsp/protocol/tsjson.go +++ b/gopls/internal/lsp/protocol/tsjson.go @@ -5,7 +5,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Tue Sep 13 10:45:25 2022) +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) // Code generated; DO NOT EDIT. import "encoding/json" diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go index 79ed309da39..8272d7e80b3 100644 --- a/gopls/internal/lsp/protocol/tsprotocol.go +++ b/gopls/internal/lsp/protocol/tsprotocol.go @@ -5,7 +5,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Tue Sep 13 10:45:25 2022) +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) // Code generated; DO NOT EDIT. import "encoding/json" @@ -240,7 +240,7 @@ type ClientCapabilities struct { // line 9700 * * @since 3.17.0 */ - NotebookDocument NotebookDocumentClientCapabilities `json:"notebookDocument,omitempty"` + NotebookDocument *NotebookDocumentClientCapabilities `json:"notebookDocument,omitempty"` // Window specific client capabilities. Window WindowClientCapabilities `json:"window,omitempty"` /* @@ -248,7 +248,7 @@ type ClientCapabilities struct { // line 9700 * * @since 3.16.0 */ - General GeneralClientCapabilities `json:"general,omitempty"` + General *GeneralClientCapabilities `json:"general,omitempty"` // Experimental client capabilities. Experimental interface{} `json:"experimental,omitempty"` } @@ -353,7 +353,7 @@ type CodeActionClientCapabilities struct { // line 11747 * * @since 3.16.0 */ - ResolveSupport PResolveSupportPCodeAction `json:"resolveSupport,omitempty"` + ResolveSupport *PResolveSupportPCodeAction `json:"resolveSupport,omitempty"` /* * Whether the client honors the change annotations in * text edits and resource operations returned via the @@ -533,7 +533,7 @@ type ColorPresentation struct { // line 2343 * this presentation for the color. When `falsy` the [label](#ColorPresentation.label) * is used. */ - TextEdit TextEdit `json:"textEdit,omitempty"` + TextEdit *TextEdit `json:"textEdit,omitempty"` /* * An optional array of additional [text edits](#TextEdit) that are applied when * selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves. @@ -579,8 +579,8 @@ type CompletionClientCapabilities struct { // line 11194 * The client supports the following `CompletionItem` specific * capabilities. */ - CompletionItem PCompletionItemPCompletion `json:"completionItem,omitempty"` - CompletionItemKind PCompletionItemKindPCompletion `json:"completionItemKind,omitempty"` + CompletionItem PCompletionItemPCompletion `json:"completionItem,omitempty"` + CompletionItemKind *PCompletionItemKindPCompletion `json:"completionItemKind,omitempty"` /* * Defines how the client handles whitespace and indentation * when accepting a completion item that uses multi line @@ -600,7 +600,7 @@ type CompletionClientCapabilities struct { // line 11194 * * @since 3.17.0 */ - CompletionList PCompletionListPCompletion `json:"completionList,omitempty"` + CompletionList *PCompletionListPCompletion `json:"completionList,omitempty"` } // Contains additional information about the context in which a completion request is triggered. @@ -634,7 +634,7 @@ type CompletionItem struct { // line 4550 * * @since 3.17.0 */ - LabelDetails CompletionItemLabelDetails `json:"labelDetails,omitempty"` + LabelDetails *CompletionItemLabelDetails `json:"labelDetails,omitempty"` /* * The kind of this completion item. Based of the kind * an icon is chosen by the editor. @@ -819,7 +819,7 @@ type CompletionList struct { // line 4758 * * @since 3.17.0 */ - ItemDefaults PItemDefaultsMsg_textDocument_completion `json:"itemDefaults,omitempty"` + ItemDefaults *PItemDefaultsMsg_textDocument_completion `json:"itemDefaults,omitempty"` // The completion items. Items []CompletionItem `json:"items"` } @@ -859,7 +859,7 @@ type CompletionOptions struct { // line 8727 * * @since 3.17.0 */ - CompletionItem PCompletionItemPCompletionProvider `json:"completionItem,omitempty"` + CompletionItem *PCompletionItemPCompletionProvider `json:"completionItem,omitempty"` WorkDoneProgressOptions } @@ -900,7 +900,7 @@ type CreateFile struct { // line 6717 // The resource to create. URI DocumentURI `json:"uri"` // Additional options - Options CreateFileOptions `json:"options,omitempty"` + Options *CreateFileOptions `json:"options,omitempty"` ResourceOperation } @@ -1014,7 +1014,7 @@ type DeleteFile struct { // line 6799 // The file to delete. URI DocumentURI `json:"uri"` // Delete options. - Options DeleteFileOptions `json:"options,omitempty"` + Options *DeleteFileOptions `json:"options,omitempty"` ResourceOperation } @@ -1186,7 +1186,7 @@ type DidChangeConfigurationParams struct { // line 4166 Settings interface{} `json:"settings"` } type DidChangeConfigurationRegistrationOptions struct { // line 4180 - Section OrPSection_workspace_didChangeConfiguration `json:"section,omitempty"` + Section *OrPSection_workspace_didChangeConfiguration `json:"section,omitempty"` } /* @@ -1656,7 +1656,7 @@ type DocumentSymbolClientCapabilities struct { // line 11665 * Specific capabilities for the `SymbolKind` in the * `textDocument/documentSymbol` request. */ - SymbolKind PSymbolKindPDocumentSymbol `json:"symbolKind,omitempty"` + SymbolKind *PSymbolKindPDocumentSymbol `json:"symbolKind,omitempty"` // The client supports hierarchical document symbols. HierarchicalDocumentSymbolSupport bool `json:"hierarchicalDocumentSymbolSupport,omitempty"` /* @@ -1666,7 +1666,7 @@ type DocumentSymbolClientCapabilities struct { // line 11665 * * @since 3.16.0 */ - TagSupport PTagSupportPDocumentSymbol `json:"tagSupport,omitempty"` + TagSupport *PTagSupportPDocumentSymbol `json:"tagSupport,omitempty"` /* * The client supports an additional label presented in the UI when * registering a document symbol provider. @@ -1745,7 +1745,7 @@ type ExecutionSummary struct { // line 10188 // created for Literal type FCellsPNotebookSelector struct { // line 9857 - Language string `json:"Language"` + Language string `json:"language"` } // created for Literal @@ -1756,13 +1756,13 @@ type FCodeActionKindPCodeActionLiteralSupport struct { // line 11768 * handle values outside its set gracefully and falls back * to a default value when unknown. */ - ValueSet []CodeActionKind `json:"ValueSet"` + ValueSet []CodeActionKind `json:"valueSet"` } // created for Literal type FEditRangePItemDefaults struct { // line 4797 - Insert Range `json:"Insert"` - Replace Range `json:"Replace"` + Insert Range `json:"insert"` + Replace Range `json:"replace"` } // created for Literal @@ -1771,12 +1771,12 @@ type FFullPRequests struct { // line 12230 * The client will send the `textDocument/semanticTokens/full/delta` request if * the server provides a corresponding handler. */ - Delta bool `json:"Delta"` + Delta bool `json:"delta"` } // created for Literal type FInsertTextModeSupportPCompletionItem struct { // line 11321 - ValueSet []InsertTextMode `json:"ValueSet"` + ValueSet []InsertTextMode `json:"valueSet"` } // created for Literal @@ -1787,7 +1787,7 @@ type FParameterInformationPSignatureInformation struct { // line 11487 * * @since 3.14.0 */ - LabelOffsetSupport bool `json:"LabelOffsetSupport"` + LabelOffsetSupport bool `json:"labelOffsetSupport"` } // created for Literal @@ -1797,29 +1797,29 @@ type FRangePRequests struct { // line 12210 // created for Literal type FResolveSupportPCompletionItem struct { // line 11297 // The properties that a client can resolve lazily. - Properties []string `json:"Properties"` + Properties []string `json:"properties"` } // created for Literal type FStructurePCells struct { // line 7492 // The change to the cell array. - Array NotebookCellArrayChange `json:"Array"` + Array NotebookCellArrayChange `json:"array"` // Additional opened cell text documents. - DidOpen []TextDocumentItem `json:"DidOpen"` + DidOpen []TextDocumentItem `json:"didOpen"` // Additional closed cell text documents. - DidClose []TextDocumentIdentifier `json:"DidClose"` + DidClose []TextDocumentIdentifier `json:"didClose"` } // created for Literal type FTagSupportPCompletionItem struct { // line 11263 // The tags supported by the client. - ValueSet []CompletionItemTag `json:"ValueSet"` + ValueSet []CompletionItemTag `json:"valueSet"` } // created for Literal type FTextContentPCells struct { // line 7550 - Document VersionedTextDocumentIdentifier `json:"Document"` - Changes []TextDocumentContentChangeEvent `json:"Changes"` + Document VersionedTextDocumentIdentifier `json:"document"` + Changes []TextDocumentContentChangeEvent `json:"changes"` } type FailureHandlingKind string // line 13719 type FileChangeType uint32 // line 13480 @@ -1896,17 +1896,17 @@ type FileOperationFilter struct { // line 6869 */ type FileOperationOptions struct { // line 9991 // The server is interested in receiving didCreateFiles notifications. - DidCreate FileOperationRegistrationOptions `json:"didCreate,omitempty"` + DidCreate *FileOperationRegistrationOptions `json:"didCreate,omitempty"` // The server is interested in receiving willCreateFiles requests. - WillCreate FileOperationRegistrationOptions `json:"willCreate,omitempty"` + WillCreate *FileOperationRegistrationOptions `json:"willCreate,omitempty"` // The server is interested in receiving didRenameFiles notifications. - DidRename FileOperationRegistrationOptions `json:"didRename,omitempty"` + DidRename *FileOperationRegistrationOptions `json:"didRename,omitempty"` // The server is interested in receiving willRenameFiles requests. - WillRename FileOperationRegistrationOptions `json:"willRename,omitempty"` + WillRename *FileOperationRegistrationOptions `json:"willRename,omitempty"` // The server is interested in receiving didDeleteFiles file notifications. - DidDelete FileOperationRegistrationOptions `json:"didDelete,omitempty"` + DidDelete *FileOperationRegistrationOptions `json:"didDelete,omitempty"` // The server is interested in receiving willDeleteFiles file requests. - WillDelete FileOperationRegistrationOptions `json:"willDelete,omitempty"` + WillDelete *FileOperationRegistrationOptions `json:"willDelete,omitempty"` } /* @@ -1933,7 +1933,7 @@ type FileOperationPattern struct { // line 9509 */ Matches FileOperationPatternKind `json:"matches,omitempty"` // Additional options used during matching. - Options FileOperationPatternOptions `json:"options,omitempty"` + Options *FileOperationPatternOptions `json:"options,omitempty"` } type FileOperationPatternKind string // line 13653 /* @@ -2041,13 +2041,13 @@ type FoldingRangeClientCapabilities struct { // line 12004 * * @since 3.17.0 */ - FoldingRangeKind PFoldingRangeKindPFoldingRange `json:"foldingRangeKind,omitempty"` + FoldingRangeKind *PFoldingRangeKindPFoldingRange `json:"foldingRangeKind,omitempty"` /* * Specific options for the folding range. * * @since 3.17.0 */ - FoldingRange PFoldingRangePFoldingRange `json:"foldingRange,omitempty"` + FoldingRange *PFoldingRangePFoldingRange `json:"foldingRange,omitempty"` } type FoldingRangeKind string // line 12841 type FoldingRangeOptions struct { // line 6486 @@ -2125,19 +2125,19 @@ type GeneralClientCapabilities struct { // line 10690 * * @since 3.17.0 */ - StaleRequestSupport PStaleRequestSupportPGeneral `json:"staleRequestSupport,omitempty"` + StaleRequestSupport *PStaleRequestSupportPGeneral `json:"staleRequestSupport,omitempty"` /* * Client capabilities specific to regular expressions. * * @since 3.16.0 */ - RegularExpressions RegularExpressionsClientCapabilities `json:"regularExpressions,omitempty"` + RegularExpressions *RegularExpressionsClientCapabilities `json:"regularExpressions,omitempty"` /* * Client capabilities specific to the client's markdown parser. * * @since 3.16.0 */ - Markdown MarkdownClientCapabilities `json:"markdown,omitempty"` + Markdown *MarkdownClientCapabilities `json:"markdown,omitempty"` /* * The position encodings supported by the client. Client and server * have to agree on the same position encoding to ensure that offsets @@ -2294,7 +2294,7 @@ type InlayHint struct { // line 3667 */ TextEdits []TextEdit `json:"textEdits,omitempty"` // The tooltip text when you hover over this item. - Tooltip OrPTooltip_textDocument_inlayHint `json:"tooltip,omitempty"` + Tooltip *OrPTooltip_textDocument_inlayHint `json:"tooltip,omitempty"` /* * Render padding before the hint. * @@ -2330,7 +2330,7 @@ type InlayHintClientCapabilities struct { // line 12395 * Indicates which properties a client can resolve lazily on an inlay * hint. */ - ResolveSupport PResolveSupportPInlayHint `json:"resolveSupport,omitempty"` + ResolveSupport *PResolveSupportPInlayHint `json:"resolveSupport,omitempty"` } type InlayHintKind uint32 // line 13059 /* @@ -2347,7 +2347,7 @@ type InlayHintLabelPart struct { // line 7067 * the client capability `inlayHint.resolveSupport` clients might resolve * this property late using the resolve request. */ - Tooltip OrPTooltipPLabel `json:"tooltip,omitempty"` + Tooltip *OrPTooltipPLabel `json:"tooltip,omitempty"` /* * An optional source code location that represents this * label part. @@ -2361,14 +2361,14 @@ type InlayHintLabelPart struct { // line 7067 * Depending on the client capability `inlayHint.resolveSupport` clients * might resolve this property late using the resolve request. */ - Location Location `json:"location,omitempty"` + Location *Location `json:"location,omitempty"` /* * An optional command for this label part. * * Depending on the client capability `inlayHint.resolveSupport` clients * might resolve this property late using the resolve request. */ - Command Command `json:"command,omitempty"` + Command *Command `json:"command,omitempty"` } /* @@ -2664,7 +2664,7 @@ type LocationLink struct { // line 6277 * Used as the underlined span for mouse interaction. Defaults to the word range at * the definition position. */ - OriginSelectionRange Range `json:"originSelectionRange,omitempty"` + OriginSelectionRange *Range `json:"originSelectionRange,omitempty"` // The target resource identifier of this link. TargetURI DocumentURI `json:"targetUri"` /* @@ -2811,56 +2811,56 @@ type MonikerRegistrationOptions struct { // line 3400 // created for Literal type Msg_MarkedString struct { // line 14093 - Language string `json:"Language"` - Value string `json:"Value"` + Language string `json:"language"` + Value string `json:"value"` } // created for Literal type Msg_NotebookDocumentFilter struct { // line 14268 // The type of the enclosing notebook. - NotebookType string `json:"NotebookType"` + NotebookType string `json:"notebookType"` // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. - Scheme string `json:"Scheme"` + Scheme string `json:"scheme"` // A glob pattern. - Pattern string `json:"Pattern"` + Pattern string `json:"pattern"` } // created for Literal type Msg_PrepareRename2Gn struct { // line 13936 - Range Range `json:"Range"` - Placeholder string `json:"Placeholder"` + Range Range `json:"range"` + Placeholder string `json:"placeholder"` } // created for Literal type Msg_TextDocumentContentChangeEvent struct { // line 14033 // The range of the document that changed. - Range *Range `json:"Range"` + Range *Range `json:"range"` /* * The optional length of the range that got replaced. * * @deprecated use range instead. */ - RangeLength uint32 `json:"RangeLength"` + RangeLength uint32 `json:"rangeLength"` // The new text for the provided range. - Text string `json:"Text"` + Text string `json:"text"` } // created for Literal type Msg_TextDocumentFilter struct { // line 14159 // A language id, like `typescript`. - Language string `json:"Language"` + Language string `json:"language"` // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. - Scheme string `json:"Scheme"` + Scheme string `json:"scheme"` // A glob pattern, like `*.{ts,js}`. - Pattern string `json:"Pattern"` + Pattern string `json:"pattern"` } // created for Literal type Msg_XInitializeParams_clientInfo struct { // line 7678 // The name of the client as defined by the client. - Name string `json:"Name"` + Name string `json:"name"` // The client's version as defined by the client. - Version string `json:"Version"` + Version string `json:"version"` } /* @@ -2885,12 +2885,12 @@ type NotebookCell struct { // line 9624 * * Note: should always be an object literal (e.g. LSPObject) */ - Metadata LSPObject `json:"metadata,omitempty"` + Metadata *LSPObject `json:"metadata,omitempty"` /* * Additional execution summary information * if supported by the client. */ - ExecutionSummary ExecutionSummary `json:"executionSummary,omitempty"` + ExecutionSummary *ExecutionSummary `json:"executionSummary,omitempty"` } /* @@ -2952,7 +2952,7 @@ type NotebookDocument struct { // line 7359 * * Note: should always be an object literal (e.g. LSPObject) */ - Metadata LSPObject `json:"metadata,omitempty"` + Metadata *LSPObject `json:"metadata,omitempty"` // The cells of a notebook. Cells []NotebookCell `json:"cells"` } @@ -2968,9 +2968,9 @@ type NotebookDocumentChangeEvent struct { // line 7471 * * Note: should always be an object literal (e.g. LSPObject) */ - Metadata LSPObject `json:"metadata,omitempty"` + Metadata *LSPObject `json:"metadata,omitempty"` // Changes to cells - Cells PCellsPChange `json:"cells,omitempty"` + Cells *PCellsPChange `json:"cells,omitempty"` } /* @@ -3072,72 +3072,72 @@ type OptionalVersionedTextDocumentIdentifier struct { // line 9363 // created for Or [Range FEditRangePItemDefaults] type OrFEditRangePItemDefaults struct { // line 4791 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [string NotebookDocumentFilter] type OrFNotebookPNotebookSelector struct { // line 9838 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [Location PLocationMsg_workspace_symbol] type OrPLocation_workspace_symbol struct { // line 5540 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [string []string] type OrPSection_workspace_didChangeConfiguration struct { // line 4186 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [string MarkupContent] type OrPTooltipPLabel struct { // line 7081 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [string MarkupContent] type OrPTooltip_textDocument_inlayHint struct { // line 3722 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [Location []Location] type Or_Definition struct { // line 13780 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport] type Or_DocumentDiagnosticReport struct { // line 13912 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [TextDocumentFilter NotebookCellTextDocumentFilter] type Or_DocumentFilter struct { // line 14121 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [InlineValueText InlineValueVariableLookup InlineValueEvaluatableExpression] type Or_InlineValue struct { // line 13890 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [string Msg_MarkedString] type Or_MarkedString struct { // line 14087 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [WorkspaceFolder URI] type Or_RelativePattern_baseUri struct { // line 10768 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport] type Or_WorkspaceDocumentDiagnosticReport struct { // line 14013 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Or [Declaration []DeclarationLink ] type Or_textDocument_declaration struct { // line 257 - Value interface{} `json:"Value"` + Value interface{} `json:"value"` } // created for Literal @@ -3146,14 +3146,14 @@ type PCellsPChange struct { // line 7486 * Changes to the cell structure to add or * remove cells. */ - Structure FStructurePCells `json:"Structure"` + Structure FStructurePCells `json:"structure"` /* * Changes to notebook cells properties like its * kind, execution summary or metadata. */ - Data []NotebookCell `json:"Data"` + Data []NotebookCell `json:"data"` // Changes to the text content of notebook cells. - TextContent []FTextContentPCells `json:"TextContent"` + TextContent []FTextContentPCells `json:"textContent"` } // created for Literal @@ -3163,7 +3163,7 @@ type PChangeAnnotationSupportPWorkspaceEdit struct { // line 10842 * for instance all edits labelled with \"Changes in Strings\" would * be a tree node. */ - GroupsOnLabel bool `json:"GroupsOnLabel"` + GroupsOnLabel bool `json:"groupsOnLabel"` } // created for Literal @@ -3172,7 +3172,7 @@ type PCodeActionLiteralSupportPCodeAction struct { // line 11762 * The code action kind is support with the following value * set. */ - CodeActionKind FCodeActionKindPCodeActionLiteralSupport `json:"CodeActionKind"` + CodeActionKind FCodeActionKindPCodeActionLiteralSupport `json:"codeActionKind"` } // created for Literal @@ -3187,7 +3187,7 @@ type PCompletionItemKindPCompletion struct { // line 11360 * the completion items kinds from `Text` to `Reference` as defined in * the initial version of the protocol. */ - ValueSet []CompletionItemKind `json:"ValueSet"` + ValueSet []CompletionItemKind `json:"valueSet"` } // created for Literal @@ -3200,18 +3200,18 @@ type PCompletionItemPCompletion struct { // line 11209 * the end of the snippet. Placeholders with equal identifiers are linked, * that is typing in one will update others too. */ - SnippetSupport bool `json:"SnippetSupport"` + SnippetSupport bool `json:"snippetSupport"` // Client supports commit characters on a completion item. - CommitCharactersSupport bool `json:"CommitCharactersSupport"` + CommitCharactersSupport bool `json:"commitCharactersSupport"` /* * Client supports the following content formats for the documentation * property. The order describes the preferred format of the client. */ - DocumentationFormat []MarkupKind `json:"DocumentationFormat"` + DocumentationFormat []MarkupKind `json:"documentationFormat"` // Client supports the deprecated property on a completion item. - DeprecatedSupport bool `json:"DeprecatedSupport"` + DeprecatedSupport bool `json:"deprecatedSupport"` // Client supports the preselect property on a completion item. - PreselectSupport bool `json:"PreselectSupport"` + PreselectSupport bool `json:"preselectSupport"` /* * Client supports the tag property on a completion item. Clients supporting * tags have to handle unknown tags gracefully. Clients especially need to @@ -3220,14 +3220,14 @@ type PCompletionItemPCompletion struct { // line 11209 * * @since 3.15.0 */ - TagSupport FTagSupportPCompletionItem `json:"TagSupport"` + TagSupport FTagSupportPCompletionItem `json:"tagSupport"` /* * Client support insert replace edit to control different behavior if a * completion item is inserted in the text or should replace text. * * @since 3.16.0 */ - InsertReplaceSupport bool `json:"InsertReplaceSupport"` + InsertReplaceSupport bool `json:"insertReplaceSupport"` /* * Indicates which properties a client can resolve lazily on a completion * item. Before version 3.16.0 only the predefined properties `documentation` @@ -3235,7 +3235,7 @@ type PCompletionItemPCompletion struct { // line 11209 * * @since 3.16.0 */ - ResolveSupport FResolveSupportPCompletionItem `json:"ResolveSupport"` + ResolveSupport FResolveSupportPCompletionItem `json:"resolveSupport"` /* * The client supports the `insertTextMode` property on * a completion item to override the whitespace handling mode @@ -3243,14 +3243,14 @@ type PCompletionItemPCompletion struct { // line 11209 * * @since 3.16.0 */ - InsertTextModeSupport FInsertTextModeSupportPCompletionItem `json:"InsertTextModeSupport"` + InsertTextModeSupport FInsertTextModeSupportPCompletionItem `json:"insertTextModeSupport"` /* * The client has support for completion item label * details (see also `CompletionItemLabelDetails`). * * @since 3.17.0 */ - LabelDetailsSupport bool `json:"LabelDetailsSupport"` + LabelDetailsSupport bool `json:"labelDetailsSupport"` } // created for Literal @@ -3262,7 +3262,7 @@ type PCompletionItemPCompletionProvider struct { // line 8767 * * @since 3.17.0 */ - LabelDetailsSupport bool `json:"LabelDetailsSupport"` + LabelDetailsSupport bool `json:"labelDetailsSupport"` } // created for Literal @@ -3277,7 +3277,7 @@ type PCompletionListPCompletion struct { // line 11402 * * @since 3.17.0 */ - ItemDefaults []string `json:"ItemDefaults"` + ItemDefaults []string `json:"itemDefaults"` } // created for Literal @@ -3287,7 +3287,7 @@ type PDisabledMsg_textDocument_codeAction struct { // line 5446 * * This is displayed in the code actions UI. */ - Reason string `json:"Reason"` + Reason string `json:"reason"` } // created for Literal @@ -3298,7 +3298,7 @@ type PFoldingRangeKindPFoldingRange struct { // line 12037 * handle values outside its set gracefully and falls back * to a default value when unknown. */ - ValueSet []FoldingRangeKind `json:"ValueSet"` + ValueSet []FoldingRangeKind `json:"valueSet"` } // created for Literal @@ -3309,13 +3309,13 @@ type PFoldingRangePFoldingRange struct { // line 12062 * * @since 3.17.0 */ - CollapsedText bool `json:"CollapsedText"` + CollapsedText bool `json:"collapsedText"` } // created for Literal type PFullESemanticTokensOptions struct { // line 6591 // The server supports deltas for full documents. - Delta bool `json:"Delta"` + Delta bool `json:"delta"` } // created for Literal @@ -3325,36 +3325,36 @@ type PItemDefaultsMsg_textDocument_completion struct { // line 4772 * * @since 3.17.0 */ - CommitCharacters []string `json:"CommitCharacters"` + CommitCharacters []string `json:"commitCharacters"` /* * A default edit range. * * @since 3.17.0 */ - EditRange OrFEditRangePItemDefaults `json:"EditRange"` + EditRange OrFEditRangePItemDefaults `json:"editRange"` /* * A default insert text format. * * @since 3.17.0 */ - InsertTextFormat InsertTextFormat `json:"InsertTextFormat"` + InsertTextFormat InsertTextFormat `json:"insertTextFormat"` /* * A default insert text mode. * * @since 3.17.0 */ - InsertTextMode InsertTextMode `json:"InsertTextMode"` + InsertTextMode InsertTextMode `json:"insertTextMode"` /* * A default data value. * * @since 3.17.0 */ - Data interface{} `json:"Data"` + Data interface{} `json:"data"` } // created for Literal type PLocationMsg_workspace_symbol struct { // line 5546 - URI DocumentURI `json:"Uri"` + URI DocumentURI `json:"uri"` } // created for Literal @@ -3364,7 +3364,7 @@ type PMessageActionItemPShowMessage struct { // line 12490 * are preserved and send back to the server in the * request's response. */ - AdditionalPropertiesSupport bool `json:"AdditionalPropertiesSupport"` + AdditionalPropertiesSupport bool `json:"additionalPropertiesSupport"` } // created for Literal @@ -3374,9 +3374,9 @@ type PNotebookSelectorPNotebookDocumentSync struct { // line 9831 * value is provided it matches against the * notebook type. '*' matches every notebook. */ - Notebook OrFNotebookPNotebookSelector `json:"Notebook"` + Notebook OrFNotebookPNotebookSelector `json:"notebook"` // The cells of the matching notebook to be synced. - Cells []FCellsPNotebookSelector `json:"Cells"` + Cells []FCellsPNotebookSelector `json:"cells"` } // created for Literal @@ -3389,24 +3389,24 @@ type PRequestsPSemanticTokens struct { // line 12198 * The client will send the `textDocument/semanticTokens/range` request if * the server provides a corresponding handler. */ - Range bool `json:"Range"` + Range bool `json:"range"` /* * The client will send the `textDocument/semanticTokens/full` request if * the server provides a corresponding handler. */ - Full interface{} `json:"Full"` + Full interface{} `json:"full"` } // created for Literal type PResolveSupportPCodeAction struct { // line 11827 // The properties that a client can resolve lazily. - Properties []string `json:"Properties"` + Properties []string `json:"properties"` } // created for Literal type PResolveSupportPInlayHint struct { // line 12410 // The properties that a client can resolve lazily. - Properties []string `json:"Properties"` + Properties []string `json:"properties"` } // created for Literal @@ -3415,15 +3415,15 @@ type PResolveSupportPSymbol struct { // line 10964 * The properties that a client can resolve lazily. Usually * `location.range` */ - Properties []string `json:"Properties"` + Properties []string `json:"properties"` } // created for Literal type PServerInfoMsg_initialize struct { // line 4118 // The name of the server as defined by the server. - Name string `json:"Name"` + Name string `json:"name"` // The server's version as defined by the server. - Version string `json:"Version"` + Version string `json:"version"` } // created for Literal @@ -3432,28 +3432,28 @@ type PSignatureInformationPSignatureHelp struct { // line 11469 * Client supports the following content formats for the documentation * property. The order describes the preferred format of the client. */ - DocumentationFormat []MarkupKind `json:"DocumentationFormat"` + DocumentationFormat []MarkupKind `json:"documentationFormat"` // Client capabilities specific to parameter information. - ParameterInformation FParameterInformationPSignatureInformation `json:"ParameterInformation"` + ParameterInformation FParameterInformationPSignatureInformation `json:"parameterInformation"` /* * The client supports the `activeParameter` property on `SignatureInformation` * literal. * * @since 3.16.0 */ - ActiveParameterSupport bool `json:"ActiveParameterSupport"` + ActiveParameterSupport bool `json:"activeParameterSupport"` } // created for Literal type PStaleRequestSupportPGeneral struct { // line 10696 // The client will actively cancel the request. - Cancel bool `json:"Cancel"` + Cancel bool `json:"cancel"` /* * The list of requests for which the client * will retry the request if it receives a * response with error code `ContentModified` */ - RetryOnContentModified []string `json:"RetryOnContentModified"` + RetryOnContentModified []string `json:"retryOnContentModified"` } // created for Literal @@ -3468,7 +3468,7 @@ type PSymbolKindPDocumentSymbol struct { // line 11680 * the symbol kinds from `File` to `Array` as defined in * the initial version of the protocol. */ - ValueSet []SymbolKind `json:"ValueSet"` + ValueSet []SymbolKind `json:"valueSet"` } // created for Literal @@ -3483,25 +3483,25 @@ type PSymbolKindPSymbol struct { // line 10916 * the symbol kinds from `File` to `Array` as defined in * the initial version of the protocol. */ - ValueSet []SymbolKind `json:"ValueSet"` + ValueSet []SymbolKind `json:"valueSet"` } // created for Literal type PTagSupportPDocumentSymbol struct { // line 11713 // The tags supported by the client. - ValueSet []SymbolTag `json:"ValueSet"` + ValueSet []SymbolTag `json:"valueSet"` } // created for Literal type PTagSupportPPublishDiagnostics struct { // line 12113 // The tags supported by the client. - ValueSet []DiagnosticTag `json:"ValueSet"` + ValueSet []DiagnosticTag `json:"valueSet"` } // created for Literal type PTagSupportPSymbol struct { // line 10940 // The tags supported by the client. - ValueSet []SymbolTag `json:"ValueSet"` + ValueSet []SymbolTag `json:"valueSet"` } // The parameters of a configuration request. @@ -3659,7 +3659,7 @@ type PublishDiagnosticsClientCapabilities struct { // line 12098 * * @since 3.15.0 */ - TagSupport PTagSupportPPublishDiagnostics `json:"tagSupport,omitempty"` + TagSupport *PTagSupportPPublishDiagnostics `json:"tagSupport,omitempty"` /* * Whether the client interprets the version property of the * `textDocument/publishDiagnostics` notification's parameter. @@ -3873,7 +3873,7 @@ type RenameFile struct { // line 6754 // The new location. NewURI DocumentURI `json:"newUri"` // Rename options. - Options RenameFileOptions `json:"options,omitempty"` + Options *RenameFileOptions `json:"options,omitempty"` ResourceOperation } @@ -4210,7 +4210,7 @@ type ServerCapabilities struct { // line 7829 */ CodeActionProvider interface{} `json:"codeActionProvider,omitempty"` // The server provides code lens. - CodeLensProvider CodeLensOptions `json:"codeLensProvider,omitempty"` + CodeLensProvider *CodeLensOptions `json:"codeLensProvider,omitempty"` // The server provides document link support. DocumentLinkProvider DocumentLinkOptions `json:"documentLinkProvider,omitempty"` // The server provides color provider support. @@ -4222,7 +4222,7 @@ type ServerCapabilities struct { // line 7829 // The server provides document range formatting. DocumentRangeFormattingProvider bool `json:"documentRangeFormattingProvider,omitempty"` // The server provides document formatting on typing. - DocumentOnTypeFormattingProvider DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"` + DocumentOnTypeFormattingProvider *DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"` /* * The server provides rename support. RenameOptions may only be * specified if the client states that it supports @@ -4332,7 +4332,7 @@ type ShowDocumentParams struct { // line 3077 * external program is started or the file is not a text * file. */ - Selection Range `json:"selection,omitempty"` + Selection *Range `json:"selection,omitempty"` } /* @@ -4356,7 +4356,7 @@ type ShowMessageParams struct { // line 4205 // Show message request client capabilities type ShowMessageRequestClientCapabilities struct { // line 12484 // Capabilities specific to the `MessageActionItem` type. - MessageActionItem PMessageActionItemPShowMessage `json:"messageActionItem,omitempty"` + MessageActionItem *PMessageActionItemPShowMessage `json:"messageActionItem,omitempty"` } type ShowMessageRequestParams struct { // line 4227 // The message type. See {@link MessageType} @@ -4407,7 +4407,7 @@ type SignatureHelpClientCapabilities struct { // line 11454 * The client supports the following `SignatureInformation` * specific properties. */ - SignatureInformation PSignatureInformationPSignatureHelp `json:"signatureInformation,omitempty"` + SignatureInformation *PSignatureInformationPSignatureHelp `json:"signatureInformation,omitempty"` /* * The client supports to send additional context information for a * `textDocument/signatureHelp` request. A client that opts into @@ -4446,7 +4446,7 @@ type SignatureHelpContext struct { // line 8807 * The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field updated based on * the user navigating through available signatures. */ - ActiveSignatureHelp SignatureHelp `json:"activeSignatureHelp,omitempty"` + ActiveSignatureHelp *SignatureHelp `json:"activeSignatureHelp,omitempty"` } // Server Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). @@ -4473,7 +4473,7 @@ type SignatureHelpParams struct { // line 4961 * * @since 3.15.0 */ - Context SignatureHelpContext `json:"context,omitempty"` + Context *SignatureHelpContext `json:"context,omitempty"` TextDocumentPositionParams WorkDoneProgressParams } @@ -4577,58 +4577,58 @@ type TextDocumentChangeRegistrationOptions struct { // line 4334 // Text document specific client capabilities. type TextDocumentClientCapabilities struct { // line 10349 // Defines which synchronization capabilities the client supports. - Synchronization TextDocumentSyncClientCapabilities `json:"synchronization,omitempty"` + Synchronization *TextDocumentSyncClientCapabilities `json:"synchronization,omitempty"` // Capabilities specific to the `textDocument/completion` request. Completion CompletionClientCapabilities `json:"completion,omitempty"` // Capabilities specific to the `textDocument/hover` request. Hover HoverClientCapabilities `json:"hover,omitempty"` // Capabilities specific to the `textDocument/signatureHelp` request. - SignatureHelp SignatureHelpClientCapabilities `json:"signatureHelp,omitempty"` + SignatureHelp *SignatureHelpClientCapabilities `json:"signatureHelp,omitempty"` /* * Capabilities specific to the `textDocument/declaration` request. * * @since 3.14.0 */ - Declaration DeclarationClientCapabilities `json:"declaration,omitempty"` + Declaration *DeclarationClientCapabilities `json:"declaration,omitempty"` // Capabilities specific to the `textDocument/definition` request. - Definition DefinitionClientCapabilities `json:"definition,omitempty"` + Definition *DefinitionClientCapabilities `json:"definition,omitempty"` /* * Capabilities specific to the `textDocument/typeDefinition` request. * * @since 3.6.0 */ - TypeDefinition TypeDefinitionClientCapabilities `json:"typeDefinition,omitempty"` + TypeDefinition *TypeDefinitionClientCapabilities `json:"typeDefinition,omitempty"` /* * Capabilities specific to the `textDocument/implementation` request. * * @since 3.6.0 */ - Implementation ImplementationClientCapabilities `json:"implementation,omitempty"` + Implementation *ImplementationClientCapabilities `json:"implementation,omitempty"` // Capabilities specific to the `textDocument/references` request. - References ReferenceClientCapabilities `json:"references,omitempty"` + References *ReferenceClientCapabilities `json:"references,omitempty"` // Capabilities specific to the `textDocument/documentHighlight` request. - DocumentHighlight DocumentHighlightClientCapabilities `json:"documentHighlight,omitempty"` + DocumentHighlight *DocumentHighlightClientCapabilities `json:"documentHighlight,omitempty"` // Capabilities specific to the `textDocument/documentSymbol` request. DocumentSymbol DocumentSymbolClientCapabilities `json:"documentSymbol,omitempty"` // Capabilities specific to the `textDocument/codeAction` request. CodeAction CodeActionClientCapabilities `json:"codeAction,omitempty"` // Capabilities specific to the `textDocument/codeLens` request. - CodeLens CodeLensClientCapabilities `json:"codeLens,omitempty"` + CodeLens *CodeLensClientCapabilities `json:"codeLens,omitempty"` // Capabilities specific to the `textDocument/documentLink` request. - DocumentLink DocumentLinkClientCapabilities `json:"documentLink,omitempty"` + DocumentLink *DocumentLinkClientCapabilities `json:"documentLink,omitempty"` /* * Capabilities specific to the `textDocument/documentColor` and the * `textDocument/colorPresentation` request. * * @since 3.6.0 */ - ColorProvider DocumentColorClientCapabilities `json:"colorProvider,omitempty"` + ColorProvider *DocumentColorClientCapabilities `json:"colorProvider,omitempty"` // Capabilities specific to the `textDocument/formatting` request. - Formatting DocumentFormattingClientCapabilities `json:"formatting,omitempty"` + Formatting *DocumentFormattingClientCapabilities `json:"formatting,omitempty"` // Capabilities specific to the `textDocument/rangeFormatting` request. - RangeFormatting DocumentRangeFormattingClientCapabilities `json:"rangeFormatting,omitempty"` + RangeFormatting *DocumentRangeFormattingClientCapabilities `json:"rangeFormatting,omitempty"` // Capabilities specific to the `textDocument/onTypeFormatting` request. - OnTypeFormatting DocumentOnTypeFormattingClientCapabilities `json:"onTypeFormatting,omitempty"` + OnTypeFormatting *DocumentOnTypeFormattingClientCapabilities `json:"onTypeFormatting,omitempty"` // Capabilities specific to the `textDocument/rename` request. Rename RenameClientCapabilities `json:"rename,omitempty"` /* @@ -4642,7 +4642,7 @@ type TextDocumentClientCapabilities struct { // line 10349 * * @since 3.15.0 */ - SelectionRange SelectionRangeClientCapabilities `json:"selectionRange,omitempty"` + SelectionRange *SelectionRangeClientCapabilities `json:"selectionRange,omitempty"` // Capabilities specific to the `textDocument/publishDiagnostics` notification. PublishDiagnostics PublishDiagnosticsClientCapabilities `json:"publishDiagnostics,omitempty"` /* @@ -4650,7 +4650,7 @@ type TextDocumentClientCapabilities struct { // line 10349 * * @since 3.16.0 */ - CallHierarchy CallHierarchyClientCapabilities `json:"callHierarchy,omitempty"` + CallHierarchy *CallHierarchyClientCapabilities `json:"callHierarchy,omitempty"` /* * Capabilities specific to the various semantic token request. * @@ -4662,37 +4662,37 @@ type TextDocumentClientCapabilities struct { // line 10349 * * @since 3.16.0 */ - LinkedEditingRange LinkedEditingRangeClientCapabilities `json:"linkedEditingRange,omitempty"` + LinkedEditingRange *LinkedEditingRangeClientCapabilities `json:"linkedEditingRange,omitempty"` /* * Client capabilities specific to the `textDocument/moniker` request. * * @since 3.16.0 */ - Moniker MonikerClientCapabilities `json:"moniker,omitempty"` + Moniker *MonikerClientCapabilities `json:"moniker,omitempty"` /* * Capabilities specific to the various type hierarchy requests. * * @since 3.17.0 */ - TypeHierarchy TypeHierarchyClientCapabilities `json:"typeHierarchy,omitempty"` + TypeHierarchy *TypeHierarchyClientCapabilities `json:"typeHierarchy,omitempty"` /* * Capabilities specific to the `textDocument/inlineValue` request. * * @since 3.17.0 */ - InlineValue InlineValueClientCapabilities `json:"inlineValue,omitempty"` + InlineValue *InlineValueClientCapabilities `json:"inlineValue,omitempty"` /* * Capabilities specific to the `textDocument/inlayHint` request. * * @since 3.17.0 */ - InlayHint InlayHintClientCapabilities `json:"inlayHint,omitempty"` + InlayHint *InlayHintClientCapabilities `json:"inlayHint,omitempty"` /* * Capabilities specific to the diagnostic pull model. * * @since 3.17.0 */ - Diagnostic DiagnosticClientCapabilities `json:"diagnostic,omitempty"` + Diagnostic *DiagnosticClientCapabilities `json:"diagnostic,omitempty"` } /* @@ -4969,8 +4969,8 @@ type TypeHierarchySupertypesParams struct { // line 3531 // created for Tuple type UIntCommaUInt struct { // line 10101 - Fld0 uint32 `json:"Fld0"` - Fld1 uint32 `json:"Fld1"` + Fld0 uint32 `json:"fld0"` + Fld1 uint32 `json:"fld1"` } type URI = string // (alias) line 0 /* @@ -5052,13 +5052,13 @@ type WindowClientCapabilities struct { // line 10655 * * @since 3.16.0 */ - ShowMessage ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` + ShowMessage *ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` /* * Capabilities specific to the showDocument request. * * @since 3.16.0 */ - ShowDocument ShowDocumentClientCapabilities `json:"showDocument,omitempty"` + ShowDocument *ShowDocumentClientCapabilities `json:"showDocument,omitempty"` } type WorkDoneProgressBegin struct { // line 6059 Kind string `json:"kind"` @@ -5157,13 +5157,13 @@ type Workspace6Gn struct { // line 8424 * * @since 3.6.0 */ - WorkspaceFolders WorkspaceFolders5Gn `json:"WorkspaceFolders"` + WorkspaceFolders WorkspaceFolders5Gn `json:"workspaceFolders"` /* * The server is interested in notifications/requests for operations on files. * * @since 3.16.0 */ - FileOperations FileOperationOptions `json:"FileOperations"` + FileOperations FileOperationOptions `json:"fileOperations"` } // Workspace specific client capabilities. @@ -5181,9 +5181,9 @@ type WorkspaceClientCapabilities struct { // line 10210 // Capabilities specific to the `workspace/didChangeWatchedFiles` notification. DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` // Capabilities specific to the `workspace/symbol` request. - Symbol WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` + Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` // Capabilities specific to the `workspace/executeCommand` request. - ExecuteCommand ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` + ExecuteCommand *ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` /* * The client has support for workspace folders. * @@ -5202,41 +5202,41 @@ type WorkspaceClientCapabilities struct { // line 10210 * * @since 3.16.0. */ - SemanticTokens SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` + SemanticTokens *SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` /* * Capabilities specific to the code lens requests scoped to the * workspace. * * @since 3.16.0. */ - CodeLens CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` + CodeLens *CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` /* * The client has support for file notifications/requests for user operations on files. * * Since 3.16.0 */ - FileOperations FileOperationClientCapabilities `json:"fileOperations,omitempty"` + FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` /* * Capabilities specific to the inline values requests scoped to the * workspace. * * @since 3.17.0. */ - InlineValue InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` + InlineValue *InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` /* * Capabilities specific to the inlay hint requests scoped to the * workspace. * * @since 3.17.0. */ - InlayHint InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` + InlayHint *InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` /* * Capabilities specific to the diagnostic requests scoped to the * workspace. * * @since 3.17.0. */ - Diagnostics DiagnosticWorkspaceClientCapabilities `json:"diagnostics,omitempty"` + Diagnostics *DiagnosticWorkspaceClientCapabilities `json:"diagnostics,omitempty"` } /* @@ -5353,7 +5353,7 @@ type WorkspaceEditClientCapabilities struct { // line 10794 * * @since 3.16.0 */ - ChangeAnnotationSupport PChangeAnnotationSupportPWorkspaceEdit `json:"changeAnnotationSupport,omitempty"` + ChangeAnnotationSupport *PChangeAnnotationSupportPWorkspaceEdit `json:"changeAnnotationSupport,omitempty"` } // A workspace folder inside a client. @@ -5460,14 +5460,14 @@ type WorkspaceSymbolClientCapabilities struct { // line 10901 // Symbol request supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` // Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. - SymbolKind PSymbolKindPSymbol `json:"symbolKind,omitempty"` + SymbolKind *PSymbolKindPSymbol `json:"symbolKind,omitempty"` /* * The client supports tags on `SymbolInformation`. * Clients supporting tags have to handle unknown tags gracefully. * * @since 3.16.0 */ - TagSupport PTagSupportPSymbol `json:"tagSupport,omitempty"` + TagSupport *PTagSupportPSymbol `json:"tagSupport,omitempty"` /* * The client support partial workspace symbols. The client will send the * request `workspaceSymbol/resolve` to the server to resolve additional @@ -5475,7 +5475,7 @@ type WorkspaceSymbolClientCapabilities struct { // line 10901 * * @since 3.17.0 */ - ResolveSupport PResolveSupportPSymbol `json:"resolveSupport,omitempty"` + ResolveSupport *PResolveSupportPSymbol `json:"resolveSupport,omitempty"` } // Server capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). @@ -5587,7 +5587,7 @@ type _InitializeParams struct { // line 7655 * * @since 3.15.0 */ - ClientInfo Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` + ClientInfo *Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` /* * The locale the client is currently showing the user interface * in. This must not necessarily be the locale of the operating diff --git a/gopls/internal/lsp/protocol/tsserver.go b/gopls/internal/lsp/protocol/tsserver.go index 2478586acdf..a93ee80082b 100644 --- a/gopls/internal/lsp/protocol/tsserver.go +++ b/gopls/internal/lsp/protocol/tsserver.go @@ -5,7 +5,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Tue Sep 13 10:45:25 2022) +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) // Code generated; DO NOT EDIT. import ( From 4d18923f060e1e78b64cc09f3d2aaa73d669e565 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 16 Sep 2022 17:18:39 -0400 Subject: [PATCH 248/723] gopls/internal/fake: sort edits by both start and end position When processing edits, they must be non-overlapping but they may be adjacent: we must allow for insertions adjacent to replacements, e.g. an edit of 3:1-3:1 followed by an edit of 3:1-3:15. Achieve this in the fake editor by sorting edits by end position after start position. Longer term, we should eliminate this ad-hoc editing in favor of diff.ApplyEdit. Change-Id: I72a252952585d0a652d97287486aa61c167db485 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431219 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Dylan Le Run-TryBot: Robert Findley --- gopls/internal/lsp/fake/edit.go | 37 ++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go index b45f23f9e81..7c833383d0b 100644 --- a/gopls/internal/lsp/fake/edit.go +++ b/gopls/internal/lsp/fake/edit.go @@ -110,17 +110,23 @@ func inText(p Pos, content []string) bool { // invalid for the current content. // // TODO(rfindley): this function does not handle non-ascii text correctly. +// TODO(rfindley): replace this with diff.ApplyEdits: we should not be +// maintaining an additional representation of edits. func editContent(content []string, edits []Edit) ([]string, error) { newEdits := make([]Edit, len(edits)) copy(newEdits, edits) - sort.Slice(newEdits, func(i, j int) bool { - if newEdits[i].Start.Line < newEdits[j].Start.Line { - return true + sort.SliceStable(newEdits, func(i, j int) bool { + ei := newEdits[i] + ej := newEdits[j] + + // Sort by edit start position followed by end position. Given an edit + // 3:1-3:1 followed by an edit 3:1-3:15, we must process the empty edit + // first. + if cmp := comparePos(ei.Start, ej.Start); cmp != 0 { + return cmp < 0 } - if newEdits[i].Start.Line > newEdits[j].Start.Line { - return false - } - return newEdits[i].Start.Column < newEdits[j].Start.Column + + return comparePos(ei.End, ej.End) < 0 }) // Validate edits. @@ -157,3 +163,20 @@ func editContent(content []string, edits []Edit) ([]string, error) { advance(len(content)-1, len([]rune(content[len(content)-1]))) return strings.Split(b.String(), "\n"), nil } + +// comparePos returns -1 if left < right, 0 if left == right, and 1 if left > right. +func comparePos(left, right Pos) int { + if left.Line < right.Line { + return -1 + } + if left.Line > right.Line { + return 1 + } + if left.Column < right.Column { + return -1 + } + if left.Column > right.Column { + return 1 + } + return 0 +} From 0e011a0e6cac416edb5931427109991c00490cfb Mon Sep 17 00:00:00 2001 From: cui fliter Date: Mon, 19 Sep 2022 15:20:59 +0000 Subject: [PATCH 249/723] all: use constant to avoid repeated definitions Change-Id: I9e0a167cc3a9772412f3ec809eeb4720ae331c98 GitHub-Last-Rev: 4a909d88f4ddb99195de6fdfee040d883236c2da GitHub-Pull-Request: golang/tools#398 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431637 Reviewed-by: Ian Lance Taylor Auto-Submit: Dmitri Shuralyov TryBot-Result: Gopher Robot Run-TryBot: Dmitri Shuralyov Reviewed-by: Dmitri Shuralyov --- go/ast/inspector/typeof.go | 3 ++- godoc/index.go | 3 ++- internal/jsonrpc2/serve.go | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/go/ast/inspector/typeof.go b/go/ast/inspector/typeof.go index 11ab2bc85aa..703c8139544 100644 --- a/go/ast/inspector/typeof.go +++ b/go/ast/inspector/typeof.go @@ -11,6 +11,7 @@ package inspector import ( "go/ast" + "math" "golang.org/x/tools/internal/typeparams" ) @@ -218,7 +219,7 @@ func typeOf(n ast.Node) uint64 { func maskOf(nodes []ast.Node) uint64 { if nodes == nil { - return 1<<64 - 1 // match all node types + return math.MaxUint64 // match all node types } var mask uint64 for _, n := range nodes { diff --git a/godoc/index.go b/godoc/index.go index d3f9f64fc5c..4dc3362a7e2 100644 --- a/godoc/index.go +++ b/godoc/index.go @@ -50,6 +50,7 @@ import ( "index/suffixarray" "io" "log" + "math" "os" pathpkg "path" "path/filepath" @@ -161,7 +162,7 @@ func newKindRun(h RunList) interface{} { // bit is always the same for all infos in one // list we can simply compare the entire info. k := 0 - prev := SpotInfo(1<<32 - 1) // an unlikely value + prev := SpotInfo(math.MaxUint32) // an unlikely value for _, x := range run { if x != prev { run[k] = x diff --git a/internal/jsonrpc2/serve.go b/internal/jsonrpc2/serve.go index 4181bf10c33..cfbcbcb021c 100644 --- a/internal/jsonrpc2/serve.go +++ b/internal/jsonrpc2/serve.go @@ -8,6 +8,7 @@ import ( "context" "errors" "io" + "math" "net" "os" "time" @@ -100,7 +101,7 @@ func Serve(ctx context.Context, ln net.Listener, server StreamServer, idleTimeou }() // Max duration: ~290 years; surely that's long enough. - const forever = 1<<63 - 1 + const forever = math.MaxInt64 if idleTimeout <= 0 { idleTimeout = forever } From b256f1f4a1897262b0c190ab286df45f6073c3f3 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 19 Sep 2022 13:58:40 -0400 Subject: [PATCH 250/723] gopls/internal/lsp/cache: remove distracting "safe trimming" log This log is noisy and of little value, often confusing users. Remove it. Updates golang/go#51947 Change-Id: I2b2dff8383de52467bf3953d3efda4e6d4b792dd Reviewed-on: https://go-review.googlesource.com/c/tools/+/431836 Reviewed-by: Heschi Kreinick gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- gopls/internal/lsp/cache/check.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 1fbd9c002db..3974f516f88 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -21,11 +21,11 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/span" @@ -355,8 +355,6 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF missing, unexpected = filter.ProcessErrors(pkg.typeErrors) } if len(unexpected) != 0 || len(missing) != 0 { - // TODO(rfindley): remove this distracting log - event.Log(ctx, fmt.Sprintf("falling back to safe trimming due to type errors: %v or still-missing identifiers: %v", unexpected, missing), tag.Package.Of(string(m.ID))) pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, nil) if err != nil { return nil, err From fdf581fdab091d9dd12425be67e59db67a2ad501 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Thu, 23 Sep 2021 19:52:37 -0600 Subject: [PATCH 251/723] internal/lsp: allow extract func ranges to begin/end with comments CanExtract strips of the whitespace on either end of the range in order to get an exact range to extract to function. We can do the same thing for comments by moving adjusting the range if the start or end positions contain the position. Updates golang/go#37170 Fixes golang/go#54816 Change-Id: I3508c822434400f084a273730380c89611803e97 Reviewed-on: https://go-review.googlesource.com/c/tools/+/351989 Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Suzy Mueller TryBot-Result: Gopher Robot --- gopls/internal/lsp/source/extract.go | 94 +++++++++++++------ .../extract_function/extract_basic_comment.go | 10 +- .../extract_basic_comment.go.golden | 46 ++++++++- .../internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- 5 files changed, 115 insertions(+), 39 deletions(-) diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index b09ed9ee6c3..9a143a78c29 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -12,14 +12,15 @@ import ( "go/parser" "go/token" "go/types" + "sort" "strings" - "unicode" + "text/scanner" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/bug" - "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/span" ) @@ -650,47 +651,78 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file }, nil } -// adjustRangeForWhitespace adjusts the given range to exclude unnecessary leading or -// trailing whitespace characters from selection. In the following example, each line -// of the if statement is indented once. There are also two extra spaces after the -// closing bracket before the line break. +// adjustRangeForCommentsAndWhitespace adjusts the given range to exclude unnecessary leading or +// trailing whitespace characters from selection as well as leading or trailing comments. +// In the following example, each line of the if statement is indented once. There are also two +// extra spaces after the sclosing bracket before the line break and a comment. // // \tif (true) { // \t _ = 1 -// \t} \n +// \t} // hello \n // // By default, a valid range begins at 'if' and ends at the first whitespace character // after the '}'. But, users are likely to highlight full lines rather than adjusting // their cursors for whitespace. To support this use case, we must manually adjust the // ranges to match the correct AST node. In this particular example, we would adjust -// rng.Start forward by one byte, and rng.End backwards by two bytes. -func adjustRangeForWhitespace(rng span.Range, tok *token.File, content []byte) (span.Range, error) { - offset, err := safetoken.Offset(tok, rng.Start) - if err != nil { - return span.Range{}, err - } - for offset < len(content) { - if !unicode.IsSpace(rune(content[offset])) { - break +// rng.Start forward to the start of 'if' and rng.End backward to after '}'. +func adjustRangeForCommentsAndWhiteSpace(rng span.Range, tok *token.File, content []byte, file *ast.File) (span.Range, error) { + // Adjust the end of the range to after leading whitespace and comments. + prevStart, start := token.NoPos, rng.Start + startComment := sort.Search(len(file.Comments), func(i int) bool { + // Find the index for the first comment that ends after range start. + return file.Comments[i].End() > rng.Start + }) + for prevStart != start { + prevStart = start + // If start is within a comment, move start to the end + // of the comment group. + if file.Comments[startComment].Pos() <= start && start < file.Comments[startComment].End() { + start = file.Comments[startComment].End() + startComment++ + } + // Move forwards to find a non-whitespace character. + offset, err := safetoken.Offset(tok, start) + if err != nil { + return span.Range{}, err } - // Move forwards one byte to find a non-whitespace character. - offset += 1 + for offset < len(content) && isGoWhiteSpace(content[offset]) { + offset++ + } + start = tok.Pos(offset) } - rng.Start = tok.Pos(offset) - // Move backwards to find a non-whitespace character. - offset, err = safetoken.Offset(tok, rng.End) - if err != nil { - return span.Range{}, err - } - for o := offset - 1; 0 <= o && o < len(content); o-- { - if !unicode.IsSpace(rune(content[o])) { - break + // Adjust the end of the range to before trailing whitespace and comments. + prevEnd, end := token.NoPos, rng.End + endComment := sort.Search(len(file.Comments), func(i int) bool { + // Find the index for the first comment that ends after the range end. + return file.Comments[i].End() >= rng.End + }) + for prevEnd != end { + prevEnd = end + // If end is within a comment, move end to the start + // of the comment group. + if file.Comments[endComment].Pos() < end && end <= file.Comments[endComment].End() { + end = file.Comments[endComment].Pos() + endComment-- + } + // Move backwards to find a non-whitespace character. + offset, err := safetoken.Offset(tok, end) + if err != nil { + return span.Range{}, err } - offset = o + for offset > 0 && isGoWhiteSpace(content[offset-1]) { + offset-- + } + end = tok.Pos(offset) } - rng.End = tok.Pos(offset) - return rng, nil + + return span.NewRange(tok, start, end), nil +} + +// isGoWhiteSpace returns true if b is a considered white space in +// Go as defined by scanner.GoWhitespace. +func isGoWhiteSpace(b byte) bool { + return uint64(scanner.GoWhitespace)&(1< Date: Mon, 19 Sep 2022 14:13:01 -0400 Subject: [PATCH 252/723] gopls: set codelensProvider in initialize response gopls needs to register that it is a codelensProvider. Fixes golang/go#55155 Change-Id: I91d6eb320871386175b2fb95ab5e9bf935a84014 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431837 Reviewed-by: Robert Findley Run-TryBot: Suzy Mueller --- gopls/internal/lsp/general.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index 1e7ad65613a..01f8242d0b0 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -137,6 +137,7 @@ See https://github.com/golang/go/issues/45732 for more information.`, Capabilities: protocol.ServerCapabilities{ CallHierarchyProvider: true, CodeActionProvider: codeActionProvider, + CodeLensProvider: &protocol.CodeLensOptions{}, // must be non-nil to enable the code lens capability CompletionProvider: protocol.CompletionOptions{ TriggerCharacters: []string{"."}, }, From df2eb9381fb80be786bc1f85fdeb3d941104d6e8 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 19 Sep 2022 17:38:47 -0400 Subject: [PATCH 253/723] gopls/test: fix erroneously skipped tests, remove redundant cmd tests When code was moved to the golang.org/x/tools/gopls module, the gopls/test package tests we silently skipped, because of logic that skipped the tests if the testdata directory was not found. Narrow that check to only skip tests on android, which is where we expect testdata to be missing. Also, now that gopls code lives in the gopls module, there is little reason to have two copies of the command-line tests. Delete gopls/internal/lsp/cmd/cmd_test.go. Change-Id: I6d57244dd77a983b1f83c1cf653d5345e4fa0434 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431842 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- gopls/internal/lsp/cmd/cmd_test.go | 25 ------------------------- gopls/internal/lsp/cmd/test/cmdtest.go | 7 ++++--- gopls/test/gopls_test.go | 2 +- 3 files changed, 5 insertions(+), 29 deletions(-) delete mode 100644 gopls/internal/lsp/cmd/cmd_test.go diff --git a/gopls/internal/lsp/cmd/cmd_test.go b/gopls/internal/lsp/cmd/cmd_test.go deleted file mode 100644 index 877ceb66eb9..00000000000 --- a/gopls/internal/lsp/cmd/cmd_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmd_test - -import ( - "os" - "testing" - - "golang.org/x/tools/internal/bug" - cmdtest "golang.org/x/tools/gopls/internal/lsp/cmd/test" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/internal/testenv" -) - -func TestMain(m *testing.M) { - bug.PanicOnBugs = true - testenv.ExitIfSmallMachine() - os.Exit(m.Run()) -} - -func TestCommandLine(t *testing.T) { - cmdtest.TestCommandLine(t, "../testdata", tests.DefaultOptions) -} diff --git a/gopls/internal/lsp/cmd/test/cmdtest.go b/gopls/internal/lsp/cmd/test/cmdtest.go index 86608c7c953..e2a3e546e23 100644 --- a/gopls/internal/lsp/cmd/test/cmdtest.go +++ b/gopls/internal/lsp/cmd/test/cmdtest.go @@ -12,10 +12,10 @@ import ( "fmt" "io" "os" + "runtime" "sync" "testing" - "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/cmd" "golang.org/x/tools/gopls/internal/lsp/debug" @@ -23,6 +23,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) @@ -37,8 +38,8 @@ type runner struct { func TestCommandLine(t *testing.T, testdata string, options func(*source.Options)) { // On Android, the testdata directory is not copied to the runner. - if stat, err := os.Stat(testdata); err != nil || !stat.IsDir() { - t.Skip("testdata directory not present") + if runtime.GOOS == "android" { + t.Skip("testdata directory not present on android") } tests.RunTests(t, testdata, false, func(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) diff --git a/gopls/test/gopls_test.go b/gopls/test/gopls_test.go index 4f266baaa1e..248a03b789d 100644 --- a/gopls/test/gopls_test.go +++ b/gopls/test/gopls_test.go @@ -23,7 +23,7 @@ func TestMain(m *testing.M) { } func TestCommandLine(t *testing.T) { - cmdtest.TestCommandLine(t, "../../internal/lsp/testdata", commandLineOptions) + cmdtest.TestCommandLine(t, "../internal/lsp/testdata", commandLineOptions) } func commandLineOptions(options *source.Options) { From f901623876544c984e79bd993e30294458d31af7 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 19 Sep 2022 18:02:00 -0400 Subject: [PATCH 254/723] gopls/internal/lsp: suppress noisy log output in tests Disable logging for lsp tests: logs are not scoped to the failing test and are therefore misleading. For golang/go#54845 Change-Id: I232e4cfc114382121923e8e697452007793ec3c9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431843 gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- gopls/internal/lsp/lsp_test.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 6656549cd75..7a8dd809549 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -15,14 +15,15 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -30,6 +31,14 @@ import ( func TestMain(m *testing.M) { bug.PanicOnBugs = true testenv.ExitIfSmallMachine() + + // Set the global exporter to nil so that we don't log to stderr. This avoids + // a lot of misleading noise in test output. + // + // TODO(rfindley): investigate whether we can/should capture logs scoped to + // individual tests by passing in a context with a local exporter. + event.SetExporter(nil) + os.Exit(m.Run()) } From 6782af031e202cef57450347adde23bdff33bccc Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 19 Sep 2022 15:18:43 -0400 Subject: [PATCH 255/723] gopls/internal/lsp/source: clarify qualifiedObject Clarify documentation around qualifiedObject and its field invariants. (Initially I tried to delete it but it's not so easy.) Also, hoist KnownPackages et al outside of loop over qualifiedIdentifiers in implementations() to avoid unnecessary recomputation. Change-Id: Idf087634b918a2277eabf8bbab2fdf49a8fc946c Reviewed-on: https://go-review.googlesource.com/c/tools/+/431839 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan --- gopls/internal/lsp/source/implementation.go | 82 ++++++++++----------- gopls/internal/lsp/source/references.go | 16 +++- gopls/internal/lsp/source/rename.go | 4 +- 3 files changed, 55 insertions(+), 47 deletions(-) diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index f612e35d253..b3896802aac 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -13,9 +13,9 @@ import ( "go/types" "sort" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/span" ) @@ -59,23 +59,48 @@ var ErrNotAType = errors.New("not a type name or method") // implementations returns the concrete implementations of the specified // interface, or the interfaces implemented by the specified concrete type. +// It populates only the definition-related fields of qualifiedObject. +// (Arguably it should return a smaller data type.) func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) ([]qualifiedObject, error) { + // Find all named types, even local types + // (which can have methods due to promotion). var ( - impls []qualifiedObject - seen = make(map[token.Position]bool) - fset = s.FileSet() + allNamed []*types.Named + pkgs = make(map[*types.Package]Package) ) + knownPkgs, err := s.KnownPackages(ctx) + if err != nil { + return nil, err + } + for _, pkg := range knownPkgs { + pkgs[pkg.GetTypes()] = pkg + for _, obj := range pkg.GetTypesInfo().Defs { + obj, ok := obj.(*types.TypeName) + // We ignore aliases 'type M = N' to avoid duplicate reporting + // of the Named type N. + if !ok || obj.IsAlias() { + continue + } + if named, ok := obj.Type().(*types.Named); ok { + allNamed = append(allNamed, named) + } + } + } qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) if err != nil { return nil, err } + var ( + impls []qualifiedObject + seen = make(map[token.Position]bool) + ) for _, qo := range qos { + // Ascertain the query identifier (type or method). var ( queryType types.Type queryMethod *types.Func ) - switch obj := qo.obj.(type) { case *types.Func: queryMethod = obj @@ -94,32 +119,6 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. return nil, nil } - // Find all named types, even local types (which can have methods - // due to promotion). - var ( - allNamed []*types.Named - pkgs = make(map[*types.Package]Package) - ) - knownPkgs, err := s.KnownPackages(ctx) - if err != nil { - return nil, err - } - for _, pkg := range knownPkgs { - pkgs[pkg.GetTypes()] = pkg - info := pkg.GetTypesInfo() - for _, obj := range info.Defs { - obj, ok := obj.(*types.TypeName) - // We ignore aliases 'type M = N' to avoid duplicate reporting - // of the Named type N. - if !ok || obj.IsAlias() { - continue - } - if named, ok := obj.Type().(*types.Named); ok { - allNamed = append(allNamed, named) - } - } - } - // Find all the named types that match our query. for _, named := range allNamed { var ( @@ -146,7 +145,7 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. candObj = sel.Obj() } - pos := fset.Position(candObj.Pos()) + pos := s.FileSet().Position(candObj.Pos()) if candObj == queryMethod || seen[pos] { continue } @@ -155,7 +154,7 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. impls = append(impls, qualifiedObject{ obj: candObj, - pkg: pkgs[candObj.Pkg()], + pkg: pkgs[candObj.Pkg()], // may be nil (e.g. error) }) } } @@ -192,17 +191,16 @@ func ensurePointer(T types.Type) types.Type { return T } +// A qualifiedObject is the result of resolving a reference from an +// identifier to an object. type qualifiedObject struct { - obj types.Object - - // pkg is the Package that contains obj's definition. - pkg Package - - // node is the *ast.Ident or *ast.ImportSpec we followed to find obj, if any. - node ast.Node + // definition + obj types.Object // the referenced object + pkg Package // the Package that defines the object (nil => universe) - // sourcePkg is the Package that contains node, if any. - sourcePkg Package + // reference (optional) + node ast.Node // the reference (*ast.Ident or *ast.ImportSpec) to the object + sourcePkg Package // the Package containing node } var ( diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index eacadab8540..d1cf1eaa8fc 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -15,9 +15,9 @@ import ( "sort" "strconv" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/span" ) @@ -126,6 +126,10 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit } // references is a helper function to avoid recomputing qualifiedObjsAtProtocolPos. +// The first element of qos is considered to be the declaration; +// if isDeclaration, the first result is an extra item for it. +// Only the definition-related fields of qualifiedObject are used. +// (Arguably it should accept a smaller data type.) func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, includeDeclaration, includeInterfaceRefs, includeEmbeddedRefs bool) ([]*ReferenceInfo, error) { var ( references []*ReferenceInfo @@ -134,8 +138,10 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i pos := qos[0].obj.Pos() if pos == token.NoPos { - return nil, fmt.Errorf("no position for %s", qos[0].obj) + return nil, fmt.Errorf("no position for %s", qos[0].obj) // e.g. error.Error } + // Inv: qos[0].pkg != nil, since Pos is valid. + // Inv: qos[*].pkg != nil, since all qos are logically the same declaration. filename := snapshot.FileSet().Position(pos).Filename pgf, err := qos[0].pkg.File(span.URIFromPath(filename)) if err != nil { @@ -220,6 +226,8 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i // happened to have a String method. _, isType := declIdent.Declaration.obj.(*types.TypeName) if includeInterfaceRefs && !isType { + // TODO(adonovan): opt: don't go back into the position domain: + // we have complete type information already. declRange, err := declIdent.Range() if err != nil { return nil, err @@ -256,6 +264,8 @@ func interfaceReferences(ctx context.Context, s Snapshot, f FileHandle, pp proto return nil, err } + // Make a separate call to references() for each element + // since it treats the first qualifiedObject as a definition. var refs []*ReferenceInfo for _, impl := range implementations { implRefs, err := references(ctx, s, []qualifiedObject{impl}, false, false, false) diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index e31dfd5e095..bb4d9a843e1 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -17,9 +17,9 @@ import ( "strings" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/span" "golang.org/x/tools/refactor/satisfy" ) From 835bfdfb4167b05921d42ee1a6b3248f1b4297fa Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 20 Sep 2022 12:06:02 -0400 Subject: [PATCH 256/723] gopls: update x/vuln to pick fix for incorrect version suggestion Fixed in https://go-review.git.corp.google.com/c/vuln/+/430684 Updates golang/go#54913 Change-Id: I1932a09bdd2a2192c6b61458cef28978c2e7f660 Reviewed-on: https://go-review.googlesource.com/c/tools/+/432076 gopls-CI: kokoro Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot Run-TryBot: Hyang-Ah Hana Kim --- gopls/go.mod | 2 +- gopls/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gopls/go.mod b/gopls/go.mod index d6bf740c4ce..6cd8643cc42 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -12,7 +12,7 @@ require ( golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 golang.org/x/text v0.3.7 golang.org/x/tools v0.1.13-0.20220810174125-0ad49fdeb955 - golang.org/x/vuln v0.0.0-20220901221904-62b0186a1058 + golang.org/x/vuln v0.0.0-20220919155316-41b1fc70d0a6 honnef.co/go/tools v0.3.3 mvdan.cc/gofumpt v0.3.1 mvdan.cc/xurls/v2 v2.4.0 diff --git a/gopls/go.sum b/gopls/go.sum index f647316716b..c26382551c2 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -71,6 +71,8 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/vuln v0.0.0-20220901221904-62b0186a1058 h1:YnB27EXBD8XxB0JcaOeluuvhF2kS4DrH0k+lpopG2xc= golang.org/x/vuln v0.0.0-20220901221904-62b0186a1058/go.mod h1:7tDfEDtOLlzHQRi4Yzfg5seVBSvouUIjyPzBx4q5CxQ= +golang.org/x/vuln v0.0.0-20220919155316-41b1fc70d0a6 h1:tQFrcJZ95V1wiLLPoAIaEuEVXJ7JkhbZI4Hws7Fx69c= +golang.org/x/vuln v0.0.0-20220919155316-41b1fc70d0a6/go.mod h1:7tDfEDtOLlzHQRi4Yzfg5seVBSvouUIjyPzBx4q5CxQ= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 00ae48efafb3897783fec5815ce956ef0c12d798 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Tue, 20 Sep 2022 06:28:13 +0000 Subject: [PATCH 257/723] go/internal/pkgbits: replace os.SEEK_CUR with io.SeekCurrent Change-Id: I68ee64ecc4e9ec3ec5894b878b64cc5611684dd2 GitHub-Last-Rev: 3092826e516c86367bffc799d0547b8f4667f21d GitHub-Pull-Request: golang/tools#399 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431995 Reviewed-by: Robert Findley Auto-Submit: Hyang-Ah Hana Kim gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Cherry Mui --- go/internal/pkgbits/decoder.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/go/internal/pkgbits/decoder.go b/go/internal/pkgbits/decoder.go index 2bc793668ec..e08099c6635 100644 --- a/go/internal/pkgbits/decoder.go +++ b/go/internal/pkgbits/decoder.go @@ -9,6 +9,7 @@ import ( "fmt" "go/constant" "go/token" + "io" "math/big" "os" "runtime" @@ -94,7 +95,7 @@ func NewPkgDecoder(pkgPath, input string) PkgDecoder { pr.elemEnds = make([]uint32, pr.elemEndsEnds[len(pr.elemEndsEnds)-1]) assert(binary.Read(r, binary.LittleEndian, pr.elemEnds[:]) == nil) - pos, err := r.Seek(0, os.SEEK_CUR) + pos, err := r.Seek(0, io.SeekCurrent) assert(err == nil) pr.elemData = input[pos:] @@ -237,7 +238,7 @@ func (r *Decoder) Sync(mWant SyncMarker) { return } - pos, _ := r.Data.Seek(0, os.SEEK_CUR) // TODO(mdempsky): io.SeekCurrent after #44505 is resolved + pos, _ := r.Data.Seek(0, io.SeekCurrent) mHave := SyncMarker(r.rawUvarint()) writerPCs := make([]int, r.rawUvarint()) for i := range writerPCs { From 14462efca913ebbe3bc1211c3b0b69c0f2cb7724 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 14 Sep 2022 13:37:26 -0400 Subject: [PATCH 258/723] go/analysis/passes/loopclosure: experiment with checking t.Run+Parallel Add experimental new logic to the loopclosure analyzer that checks for access to loop variables from parallel subtests. For now, this is gated behind an internal variable so that we may experiment with it without yet affecting cmd/vet. Add an internal/loopclosure command that enables the experimental new check. Change-Id: Ibf7e388a58a2b3e712e66e095d06612cf3ea918c Reviewed-on: https://go-review.googlesource.com/c/tools/+/430916 Reviewed-by: David Chase TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan --- go/analysis/passes/loopclosure/loopclosure.go | 202 +++++++++++++----- .../passes/loopclosure/loopclosure_test.go | 12 +- .../passes/loopclosure/testdata/src/a/a.go | 10 +- .../testdata/src/subtests/subtest.go | 99 +++++++++ gopls/doc/analyzers.md | 14 +- gopls/internal/lsp/source/api_json.go | 4 +- internal/analysisinternal/analysis.go | 7 +- internal/loopclosure/main.go | 22 ++ 8 files changed, 298 insertions(+), 72 deletions(-) create mode 100644 go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go create mode 100644 internal/loopclosure/main.go diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 98de9a9bacd..aed3e431438 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -14,15 +14,20 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysisinternal" ) const Doc = `check references to loop variables from within nested functions -This analyzer checks for references to loop variables from within a -function literal inside the loop body. It checks only instances where -the function literal is called in a defer or go statement that is the -last statement in the loop body, as otherwise we would need whole -program analysis. +This analyzer checks for references to loop variables from within a function +literal inside the loop body. It checks for patterns where access to a loop +variable is known to escape the current loop iteration: + 1. a call to go or defer at the end of the loop body + 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body + +The analyzer only considers references in the last statement of the loop body +as it is not deep enough to understand the effects of subsequent statements +which might render the reference benign. For example: @@ -34,6 +39,10 @@ For example: See: https://golang.org/doc/go_faq.html#closures_and_goroutines` +// TODO(rfindley): enable support for checking parallel subtests, pending +// investigation, adding: +// 3. a call testing.T.Run where the subtest body invokes t.Parallel() + var Analyzer = &analysis.Analyzer{ Name: "loopclosure", Doc: Doc, @@ -79,52 +88,71 @@ func run(pass *analysis.Pass) (interface{}, error) { return } - // Inspect a go or defer statement - // if it's the last one in the loop body. - // (We give up if there are following statements, - // because it's hard to prove go isn't followed by wait, - // or defer by return.) - if len(body.List) == 0 { - return - } - // The function invoked in the last return statement. - var fun ast.Expr - switch s := body.List[len(body.List)-1].(type) { - case *ast.GoStmt: - fun = s.Call.Fun - case *ast.DeferStmt: - fun = s.Call.Fun - case *ast.ExprStmt: // check for errgroup.Group.Go() - if call, ok := s.X.(*ast.CallExpr); ok { - fun = goInvokes(pass.TypesInfo, call) - } - } - lit, ok := fun.(*ast.FuncLit) - if !ok { - return - } - ast.Inspect(lit.Body, func(n ast.Node) bool { - id, ok := n.(*ast.Ident) - if !ok || id.Obj == nil { - return true + // Inspect statements to find function literals that may be run outside of + // the current loop iteration. + // + // For go, defer, and errgroup.Group.Go, we ignore all but the last + // statement, because it's hard to prove go isn't followed by wait, or + // defer by return. + // + // We consider every t.Run statement in the loop body, because there is + // no such commonly used mechanism for synchronizing parallel subtests. + // It is of course theoretically possible to synchronize parallel subtests, + // though such a pattern is likely to be exceedingly rare as it would be + // fighting against the test runner. + lastStmt := len(body.List) - 1 + for i, s := range body.List { + var fun ast.Expr // if non-nil, a function that escapes the loop iteration + switch s := s.(type) { + case *ast.GoStmt: + if i == lastStmt { + fun = s.Call.Fun + } + + case *ast.DeferStmt: + if i == lastStmt { + fun = s.Call.Fun + } + + case *ast.ExprStmt: // check for errgroup.Group.Go and testing.T.Run (with T.Parallel) + if call, ok := s.X.(*ast.CallExpr); ok { + if i == lastStmt { + fun = goInvoke(pass.TypesInfo, call) + } + if fun == nil && analysisinternal.LoopclosureParallelSubtests { + fun = parallelSubtest(pass.TypesInfo, call) + } + } } - if pass.TypesInfo.Types[id].Type == nil { - // Not referring to a variable (e.g. struct field name) - return true + + lit, ok := fun.(*ast.FuncLit) + if !ok { + continue } - for _, v := range vars { - if v.Obj == id.Obj { - pass.ReportRangef(id, "loop variable %s captured by func literal", - id.Name) + + ast.Inspect(lit.Body, func(n ast.Node) bool { + id, ok := n.(*ast.Ident) + if !ok || id.Obj == nil { + return true } - } - return true - }) + if pass.TypesInfo.Types[id].Type == nil { + // Not referring to a variable (e.g. struct field name) + return true + } + for _, v := range vars { + if v.Obj == id.Obj { + pass.ReportRangef(id, "loop variable %s captured by func literal", + id.Name) + } + } + return true + }) + } }) return nil, nil } -// goInvokes returns a function expression that would be called asynchronously +// goInvoke returns a function expression that would be called asynchronously // (but not awaited) in another goroutine as a consequence of the call. // For example, given the g.Go call below, it returns the function literal expression. // @@ -133,33 +161,89 @@ func run(pass *analysis.Pass) (interface{}, error) { // g.Go(func() error { ... }) // // Currently only "golang.org/x/sync/errgroup.Group()" is considered. -func goInvokes(info *types.Info, call *ast.CallExpr) ast.Expr { - f := typeutil.StaticCallee(info, call) - // Note: Currently only supports: golang.org/x/sync/errgroup.Go. - if f == nil || f.Name() != "Go" { +func goInvoke(info *types.Info, call *ast.CallExpr) ast.Expr { + if !isMethodCall(info, call, "golang.org/x/sync/errgroup", "Group", "Go") { return nil } - recv := f.Type().(*types.Signature).Recv() - if recv == nil { + return call.Args[0] +} + +// parallelSubtest returns a function expression that would be called +// asynchronously via the go test runner, as t.Run has been invoked with a +// function literal that calls t.Parallel. +// +// import "testing" +// +// func TestFoo(t *testing.T) { +// tests := []int{0, 1, 2} +// for i, t := range tests { +// t.Run("subtest", func(t *testing.T) { +// t.Parallel() +// println(i, t) +// }) +// } +// } +func parallelSubtest(info *types.Info, call *ast.CallExpr) ast.Expr { + if !isMethodCall(info, call, "testing", "T", "Run") { return nil } - rtype, ok := recv.Type().(*types.Pointer) + + lit, ok := call.Args[1].(*ast.FuncLit) if !ok { return nil } - named, ok := rtype.Elem().(*types.Named) + + for _, stmt := range lit.Body.List { + exprStmt, ok := stmt.(*ast.ExprStmt) + if !ok { + continue + } + if isMethodCall(info, exprStmt.X, "testing", "T", "Parallel") { + return lit + } + } + + return nil +} + +// isMethodCall reports whether expr is a method call of +// ... +func isMethodCall(info *types.Info, expr ast.Expr, pkgPath, typeName, method string) bool { + call, ok := expr.(*ast.CallExpr) if !ok { - return nil + return false } - if named.Obj().Name() != "Group" { - return nil + + // Check that we are calling a method + f := typeutil.StaticCallee(info, call) + if f == nil || f.Name() != method { + return false + } + recv := f.Type().(*types.Signature).Recv() + if recv == nil { + return false + } + + // Check that the receiver is a . or + // *.. + rtype := recv.Type() + if ptr, ok := recv.Type().(*types.Pointer); ok { + rtype = ptr.Elem() + } + named, ok := rtype.(*types.Named) + if !ok { + return false + } + if named.Obj().Name() != typeName { + return false } pkg := f.Pkg() if pkg == nil { - return nil + return false } - if pkg.Path() != "golang.org/x/sync/errgroup" { - return nil + if pkg.Path() != pkgPath { + return false } - return call.Args[0] + + return true } diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go index 1498838d7ff..e00ae21fa8a 100644 --- a/go/analysis/passes/loopclosure/loopclosure_test.go +++ b/go/analysis/passes/loopclosure/loopclosure_test.go @@ -5,9 +5,11 @@ package loopclosure_test import ( - "golang.org/x/tools/internal/typeparams" "testing" + "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/loopclosure" ) @@ -19,4 +21,12 @@ func Test(t *testing.T) { tests = append(tests, "typeparams") } analysistest.Run(t, testdata, loopclosure.Analyzer, tests...) + + // Enable checking of parallel subtests. + defer func(parallelSubtest bool) { + analysisinternal.LoopclosureParallelSubtests = parallelSubtest + }(analysisinternal.LoopclosureParallelSubtests) + analysisinternal.LoopclosureParallelSubtests = true + + analysistest.Run(t, testdata, loopclosure.Analyzer, "subtests") } diff --git a/go/analysis/passes/loopclosure/testdata/src/a/a.go b/go/analysis/passes/loopclosure/testdata/src/a/a.go index 2c8e2e6c411..2fc3f21f1cb 100644 --- a/go/analysis/passes/loopclosure/testdata/src/a/a.go +++ b/go/analysis/passes/loopclosure/testdata/src/a/a.go @@ -6,7 +6,9 @@ package testdata -import "golang.org/x/sync/errgroup" +import ( + "golang.org/x/sync/errgroup" +) func _() { var s []int @@ -91,9 +93,9 @@ func _() { } } -// Group is used to test that loopclosure does not match on any type named "Group". -// The checker only matches on methods "(*...errgroup.Group).Go". -type Group struct{}; +// Group is used to test that loopclosure only matches Group.Go when Group is +// from the golang.org/x/sync/errgroup package. +type Group struct{} func (g *Group) Go(func() error) {} diff --git a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go new file mode 100644 index 00000000000..4bcd495367c --- /dev/null +++ b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go @@ -0,0 +1,99 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains tests that the loopclosure analyzer detects leaked +// references via parallel subtests. + +package subtests + +import ( + "testing" +) + +// T is used to test that loopclosure only matches T.Run when T is from the +// testing package. +type T struct{} + +// Run should not match testing.T.Run. Note that the second argument is +// intentionally a *testing.T, not a *T, so that we can check both +// testing.T.Parallel inside a T.Run, and a T.Parallel inside a testing.T.Run. +func (t *T) Run(string, func(*testing.T)) { // The second argument here is testing.T +} + +func (t *T) Parallel() {} + +func _(t *testing.T) { + for i, test := range []int{1, 2, 3} { + // Check that parallel subtests are identified. + t.Run("", func(t *testing.T) { + t.Parallel() + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + }) + + // Check that serial tests are OK. + t.Run("", func(t *testing.T) { + println(i) + println(test) + }) + + // Check that the location of t.Parallel does not matter. + t.Run("", func(t *testing.T) { + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + t.Parallel() + }) + + // Check uses in nested blocks. + t.Run("", func(t *testing.T) { + t.Parallel() + { + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + } + }) + + // Check that we catch uses in nested subtests. + t.Run("", func(t *testing.T) { + t.Parallel() + t.Run("", func(t *testing.T) { + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + }) + }) + + // Check that there is no diagnostic if t is not a *testing.T. + t.Run("", func(_ *testing.T) { + t := &T{} + t.Parallel() + println(i) + println(test) + }) + } +} + +// Check that there is no diagnostic when loop variables are shadowed within +// the loop body. +func _(t *testing.T) { + for i, test := range []int{1, 2, 3} { + i := i + test := test + t.Run("", func(t *testing.T) { + t.Parallel() + println(i) + println(test) + }) + } +} + +// Check that t.Run must be *testing.T.Run. +func _(t *T) { + for i, test := range []int{1, 2, 3} { + t.Run("", func(t *testing.T) { + t.Parallel() + println(i) + println(test) + }) + } +} diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 90a5118a4b5..dfe2a43c2b9 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -218,11 +218,15 @@ inferred from function arguments, or from other type arguments: check references to loop variables from within nested functions -This analyzer checks for references to loop variables from within a -function literal inside the loop body. It checks only instances where -the function literal is called in a defer or go statement that is the -last statement in the loop body, as otherwise we would need whole -program analysis. +This analyzer checks for references to loop variables from within a function +literal inside the loop body. It checks for patterns where access to a loop +variable is known to escape the current loop iteration: + 1. a call to go or defer at the end of the loop body + 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body + +The analyzer only considers references in the last statement of the loop body +as it is not deep enough to understand the effects of subsequent statements +which might render the reference benign. For example: diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 62315079a06..957319a3719 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -300,7 +300,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "\"loopclosure\"", - Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a\nfunction literal inside the loop body. It checks only instances where\nthe function literal is called in a defer or go statement that is the\nlast statement in the loop body, as otherwise we would need whole\nprogram analysis.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", + Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n\nThe analyzer only considers references in the last statement of the loop body\nas it is not deep enough to understand the effects of subsequent statements\nwhich might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", Default: "true", }, { @@ -926,7 +926,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "loopclosure", - Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a\nfunction literal inside the loop body. It checks only instances where\nthe function literal is called in a defer or go statement that is the\nlast statement in the loop body, as otherwise we would need whole\nprogram analysis.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", + Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n\nThe analyzer only considers references in the last statement of the loop body\nas it is not deep enough to understand the effects of subsequent statements\nwhich might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", Default: true, }, { diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index e32152ac223..d538e07403a 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -14,9 +14,14 @@ import ( "strconv" ) -// Flag to gate diagnostics for fuzz tests in 1.18. +// DiagnoseFuzzTests controls whether the 'tests' analyzer diagnoses fuzz tests +// in Go 1.18+. var DiagnoseFuzzTests bool = false +// LoopclosureParallelSubtests controls whether the 'loopclosure' analyzer +// diagnoses loop variables references in parallel subtests. +var LoopclosureParallelSubtests = false + var ( GetTypeErrors func(p interface{}) []types.Error SetTypeErrors func(p interface{}, errors []types.Error) diff --git a/internal/loopclosure/main.go b/internal/loopclosure/main.go new file mode 100644 index 00000000000..03238edae13 --- /dev/null +++ b/internal/loopclosure/main.go @@ -0,0 +1,22 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The loopclosure command applies the golang.org/x/tools/go/analysis/passes/loopclosure +// analysis to the specified packages of Go source code. It enables +// experimental checking of parallel subtests. +// +// TODO: Once the parallel subtest experiment is complete, this can be made +// public at go/analysis/passes/loopclosure/cmd, or deleted. +package main + +import ( + "golang.org/x/tools/go/analysis/passes/loopclosure" + "golang.org/x/tools/go/analysis/singlechecker" + "golang.org/x/tools/internal/analysisinternal" +) + +func main() { + analysisinternal.LoopclosureParallelSubtests = true + singlechecker.Main(loopclosure.Analyzer) +} From 81a42f0a4f41405e4acb722755c0eb65fd0b08b5 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 19 Sep 2022 18:36:08 -0400 Subject: [PATCH 259/723] gopls/internal/lsp/tests: pass in a *testing.T to Data.Golden Data.Golden is called from subtests: use the *testing.T from the caller, so that we can get a more meaningful failure. For golang/go#54845 Change-Id: I136df0c6a7a11bcf364b78ecac42ba2b51a15bb0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431844 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/cmd/test/definition.go | 2 +- gopls/internal/lsp/cmd/test/folding_range.go | 2 +- gopls/internal/lsp/cmd/test/format.go | 5 ++-- gopls/internal/lsp/cmd/test/imports.go | 2 +- gopls/internal/lsp/cmd/test/rename.go | 2 +- gopls/internal/lsp/cmd/test/semanticdriver.go | 2 +- gopls/internal/lsp/cmd/test/signature.go | 2 +- gopls/internal/lsp/cmd/test/suggested_fix.go | 2 +- gopls/internal/lsp/cmd/test/symbols.go | 2 +- .../internal/lsp/cmd/test/workspace_symbol.go | 2 +- gopls/internal/lsp/lsp_test.go | 26 +++++++++---------- gopls/internal/lsp/source/source_test.go | 22 ++++++++-------- gopls/internal/lsp/tests/tests.go | 14 +++++----- 13 files changed, 43 insertions(+), 42 deletions(-) diff --git a/gopls/internal/lsp/cmd/test/definition.go b/gopls/internal/lsp/cmd/test/definition.go index 2f8c5ab24d4..6241867fa20 100644 --- a/gopls/internal/lsp/cmd/test/definition.go +++ b/gopls/internal/lsp/cmd/test/definition.go @@ -45,7 +45,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if mode&jsonGoDef != 0 && runtime.GOOS == "windows" { got = strings.Replace(got, "file:///", "file://", -1) } - expect := strings.TrimSpace(string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + expect := strings.TrimSpace(string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil }))) if expect != "" && !strings.HasPrefix(got, expect) { diff --git a/gopls/internal/lsp/cmd/test/folding_range.go b/gopls/internal/lsp/cmd/test/folding_range.go index 4478687b549..ac41697a260 100644 --- a/gopls/internal/lsp/cmd/test/folding_range.go +++ b/gopls/internal/lsp/cmd/test/folding_range.go @@ -15,7 +15,7 @@ func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { uri := spn.URI() filename := uri.Filename() got, _ := r.NormalizeGoplsCmd(t, "folding_ranges", filename) - expect := string(r.data.Golden(goldenTag, filename, func() ([]byte, error) { + expect := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { return []byte(got), nil })) diff --git a/gopls/internal/lsp/cmd/test/format.go b/gopls/internal/lsp/cmd/test/format.go index 77eedd440e4..6523e665c62 100644 --- a/gopls/internal/lsp/cmd/test/format.go +++ b/gopls/internal/lsp/cmd/test/format.go @@ -6,13 +6,14 @@ package cmdtest import ( "bytes" - exec "golang.org/x/sys/execabs" "io/ioutil" "os" "regexp" "strings" "testing" + exec "golang.org/x/sys/execabs" + "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -21,7 +22,7 @@ func (r *runner) Format(t *testing.T, spn span.Span) { tag := "gofmt" uri := spn.URI() filename := uri.Filename() - expect := string(r.data.Golden(tag, filename, func() ([]byte, error) { + expect := string(r.data.Golden(t, tag, filename, func() ([]byte, error) { cmd := exec.Command("gofmt", filename) contents, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files contents = []byte(r.Normalize(fixFileHeader(string(contents)))) diff --git a/gopls/internal/lsp/cmd/test/imports.go b/gopls/internal/lsp/cmd/test/imports.go index 6bb8af9a852..fd4162687c2 100644 --- a/gopls/internal/lsp/cmd/test/imports.go +++ b/gopls/internal/lsp/cmd/test/imports.go @@ -16,7 +16,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) { uri := spn.URI() filename := uri.Filename() got, _ := r.NormalizeGoplsCmd(t, "imports", filename) - want := string(r.data.Golden("goimports", filename, func() ([]byte, error) { + want := string(r.data.Golden(t, "goimports", filename, func() ([]byte, error) { return []byte(got), nil })) if want != got { diff --git a/gopls/internal/lsp/cmd/test/rename.go b/gopls/internal/lsp/cmd/test/rename.go index 0fe2d1e1825..4748c471501 100644 --- a/gopls/internal/lsp/cmd/test/rename.go +++ b/gopls/internal/lsp/cmd/test/rename.go @@ -17,7 +17,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { loc := fmt.Sprintf("%v", spn) got, err := r.NormalizeGoplsCmd(t, "rename", loc, newText) got += err - expect := string(r.data.Golden(goldenTag, filename, func() ([]byte, error) { + expect := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { return []byte(got), nil })) if expect != got { diff --git a/gopls/internal/lsp/cmd/test/semanticdriver.go b/gopls/internal/lsp/cmd/test/semanticdriver.go index 247f755bf20..24a1fbfdab0 100644 --- a/gopls/internal/lsp/cmd/test/semanticdriver.go +++ b/gopls/internal/lsp/cmd/test/semanticdriver.go @@ -18,7 +18,7 @@ func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { if stderr != "" { t.Fatalf("%s: %q", filename, stderr) } - want := string(r.data.Golden("semantic", filename, func() ([]byte, error) { + want := string(r.data.Golden(t, "semantic", filename, func() ([]byte, error) { return []byte(got), nil })) if want != got { diff --git a/gopls/internal/lsp/cmd/test/signature.go b/gopls/internal/lsp/cmd/test/signature.go index 9419d45218a..273e6349121 100644 --- a/gopls/internal/lsp/cmd/test/signature.go +++ b/gopls/internal/lsp/cmd/test/signature.go @@ -25,7 +25,7 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa return } goldenTag := want.Signatures[0].Label + "-signature" - expect := string(r.data.Golden(goldenTag, filename, func() ([]byte, error) { + expect := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { return []byte(got), nil })) if tests.NormalizeAny(expect) != tests.NormalizeAny(got) { diff --git a/gopls/internal/lsp/cmd/test/suggested_fix.go b/gopls/internal/lsp/cmd/test/suggested_fix.go index 0981e267b5b..6d67f28807a 100644 --- a/gopls/internal/lsp/cmd/test/suggested_fix.go +++ b/gopls/internal/lsp/cmd/test/suggested_fix.go @@ -29,7 +29,7 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, suggestedFixes []test if stderr == "ExecuteCommand is not yet supported on the command line" { return // don't skip to keep the summary counts correct } - want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), filename, func() ([]byte, error) { + want := string(r.data.Golden(t, "suggestedfix_"+tests.SpanName(spn), filename, func() ([]byte, error) { return []byte(got), nil })) if want != got { diff --git a/gopls/internal/lsp/cmd/test/symbols.go b/gopls/internal/lsp/cmd/test/symbols.go index f75b3a4426a..fbaf679336f 100644 --- a/gopls/internal/lsp/cmd/test/symbols.go +++ b/gopls/internal/lsp/cmd/test/symbols.go @@ -14,7 +14,7 @@ import ( func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { filename := uri.Filename() got, _ := r.NormalizeGoplsCmd(t, "symbols", filename) - expect := string(r.data.Golden("symbols", filename, func() ([]byte, error) { + expect := string(r.data.Golden(t, "symbols", filename, func() ([]byte, error) { return []byte(got), nil })) if expect != got { diff --git a/gopls/internal/lsp/cmd/test/workspace_symbol.go b/gopls/internal/lsp/cmd/test/workspace_symbol.go index 876887b20ab..4c19d1dd5ca 100644 --- a/gopls/internal/lsp/cmd/test/workspace_symbol.go +++ b/gopls/internal/lsp/cmd/test/workspace_symbol.go @@ -44,7 +44,7 @@ func (r *runner) runWorkspaceSymbols(t *testing.T, uri span.URI, matcher, query sort.Strings(filtered) got := r.Normalize(strings.Join(filtered, "\n") + "\n") - expect := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { + expect := string(r.data.Golden(t, fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 7a8dd809549..1cb5b3abb4d 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -287,7 +287,7 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges continue } tag := fmt.Sprintf("%s-%d", prefix, i) - want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -314,7 +314,7 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges continue } tag := fmt.Sprintf("%s-%s-%d", prefix, kind, i) - want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -388,7 +388,7 @@ func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.Fol func (r *runner) Format(t *testing.T, spn span.Span) { uri := spn.URI() filename := uri.Filename() - gofmted := string(r.data.Golden("gofmt", filename, func() ([]byte, error) { + gofmted := string(r.data.Golden(t, "gofmt", filename, func() ([]byte, error) { cmd := exec.Command("gofmt", filename) out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files return out, nil @@ -475,7 +475,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) { } got = res[uri] } - want := string(r.data.Golden("goimports", filename, func() ([]byte, error) { + want := string(r.data.Golden(t, "goimports", filename, func() ([]byte, error) { return []byte(got), nil })) @@ -574,7 +574,7 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.S } } for u, got := range res { - want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, "suggestedfix_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { return []byte(got), nil })) if want != got { @@ -626,7 +626,7 @@ func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span } res := <-r.editRecv for u, got := range res { - want := string(r.data.Golden("functionextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, "functionextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { return []byte(got), nil })) if want != got { @@ -678,7 +678,7 @@ func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) } res := <-r.editRecv for u, got := range res { - want := string(r.data.Golden("methodextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, "methodextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { return []byte(got), nil })) if want != got { @@ -730,7 +730,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if hover != nil { didSomething = true tag := fmt.Sprintf("%s-hoverdef", d.Name) - expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) { + expectHover := string(r.data.Golden(t, tag, d.Src.URI().Filename(), func() ([]byte, error) { return []byte(hover.Contents.Value), nil })) got := tests.StripSubscripts(hover.Contents.Value) @@ -982,7 +982,7 @@ func (r *runner) InlayHints(t *testing.T, spn span.Span) { } got := diff.ApplyEdits(string(m.Content), sedits) - withinlayHints := string(r.data.Golden("inlayHint", filename, func() ([]byte, error) { + withinlayHints := string(r.data.Golden(t, "inlayHint", filename, func() ([]byte, error) { return []byte(got), nil })) @@ -1013,7 +1013,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { NewName: newText, }) if err != nil { - renamed := string(r.data.Golden(tag, filename, func() ([]byte, error) { + renamed := string(r.data.Golden(t, tag, filename, func() ([]byte, error) { return []byte(err.Error()), nil })) if err.Error() != renamed { @@ -1043,7 +1043,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { val := res[uri] got += val } - want := string(r.data.Golden(tag, filename, func() ([]byte, error) { + want := string(r.data.Golden(t, tag, filename, func() ([]byte, error) { return []byte(got), nil })) if want != got { @@ -1191,7 +1191,7 @@ func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, t.Fatal(err) } got = filepath.ToSlash(tests.Normalize(got, r.normalizers)) - want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) if diff := compare.Text(want, got); diff != "" { @@ -1301,7 +1301,7 @@ func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) { t.Fatal(err) } got := (<-r.editRecv)[uri] - want := r.data.Golden("addimport", uri.Filename(), func() ([]byte, error) { + want := r.data.Golden(t, "addimport", uri.Filename(), func() ([]byte, error) { return []byte(got), nil }) if want == nil { diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 25a642a2cbf..ce2cc0aa36e 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -15,15 +15,15 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/completion" "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -366,7 +366,7 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, data s continue } tag := fmt.Sprintf("%s-%d", prefix, i) - want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -393,7 +393,7 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, data s continue } tag := fmt.Sprintf("%s-%s-%d", prefix, kind, i) - want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -463,7 +463,7 @@ func foldRanges(contents string, ranges []*source.FoldingRangeInfo) (string, err } func (r *runner) Format(t *testing.T, spn span.Span) { - gofmted := string(r.data.Golden("gofmt", spn.URI().Filename(), func() ([]byte, error) { + gofmted := string(r.data.Golden(t, "gofmt", spn.URI().Filename(), func() ([]byte, error) { cmd := exec.Command("gofmt", spn.URI().Filename()) out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files return out, nil @@ -523,7 +523,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) { t.Error(err) } got := diff.ApplyEdits(string(data), diffEdits) - want := string(r.data.Golden("goimports", spn.URI().Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, "goimports", spn.URI().Filename(), func() ([]byte, error) { return []byte(got), nil })) if d := compare.Text(got, want); d != "" { @@ -567,7 +567,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if hover != "" { didSomething = true tag := fmt.Sprintf("%s-hoverdef", d.Name) - expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) { + expectHover := string(r.data.Golden(t, tag, d.Src.URI().Filename(), func() ([]byte, error) { return []byte(hover), nil })) hover = tests.StripSubscripts(hover) @@ -768,7 +768,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { } changes, err := source.Rename(r.ctx, r.snapshot, fh, srcRng.Start, newText) if err != nil { - renamed := string(r.data.Golden(tag, spn.URI().Filename(), func() ([]byte, error) { + renamed := string(r.data.Golden(t, tag, spn.URI().Filename(), func() ([]byte, error) { return []byte(err.Error()), nil })) if err.Error() != renamed { @@ -814,7 +814,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { got += val } - renamed := string(r.data.Golden(tag, spn.URI().Filename(), func() ([]byte, error) { + renamed := string(r.data.Golden(t, tag, spn.URI().Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -913,7 +913,7 @@ func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, t.Fatal(err) } got = filepath.ToSlash(tests.Normalize(got, r.normalizers)) - want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) if d := compare.Text(want, got); d != "" { diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index cad0e9a5b33..cfce843a974 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -1013,7 +1013,7 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "LinksCount = %v\n", linksCount) fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations)) - want := string(data.Golden("summary", summaryFile, func() ([]byte, error) { + want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) { return buf.Bytes(), nil })) got := buf.String() @@ -1039,19 +1039,19 @@ func (data *Data) Mapper(uri span.URI) (*protocol.ColumnMapper, error) { return data.mappers[uri], nil } -func (data *Data) Golden(tag string, target string, update func() ([]byte, error)) []byte { - data.t.Helper() +func (data *Data) Golden(t *testing.T, tag, target string, update func() ([]byte, error)) []byte { + t.Helper() fragment, found := data.fragments[target] if !found { if filepath.IsAbs(target) { - data.t.Fatalf("invalid golden file fragment %v", target) + t.Fatalf("invalid golden file fragment %v", target) } fragment = target } golden := data.golden[fragment] if golden == nil { if !*UpdateGolden { - data.t.Fatalf("could not find golden file %v: %v", fragment, tag) + t.Fatalf("could not find golden file %v: %v", fragment, tag) } golden = &Golden{ Filename: filepath.Join(data.dir, fragment+goldenFileSuffix), @@ -1077,14 +1077,14 @@ func (data *Data) Golden(tag string, target string, update func() ([]byte, error } contents, err := update() if err != nil { - data.t.Fatalf("could not update golden file %v: %v", fragment, err) + t.Fatalf("could not update golden file %v: %v", fragment, err) } file.Data = append(contents, '\n') // add trailing \n for txtar golden.Modified = true } if file == nil { - data.t.Fatalf("could not find golden contents %v: %v", fragment, tag) + t.Fatalf("could not find golden contents %v: %v", fragment, tag) } if len(file.Data) == 0 { return file.Data From 16b974289fe50bc5c949118ac08d09af63e093f3 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 20 Sep 2022 17:19:59 -0400 Subject: [PATCH 260/723] go/analysis/passes/loopclosure: use go/types' object resolution Object resolution in go/types is more accurate than the purely syntactic object resolution in go/parser (for example, golang/go#45160), and spans multiple files. Use it for more accurate diagnostics in the loopclosure analyzer. Change-Id: I2160876dd4b3fd83aa07a9da971f27c17d7d6043 Reviewed-on: https://go-review.googlesource.com/c/tools/+/432137 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan --- go/analysis/passes/loopclosure/loopclosure.go | 19 ++++++++++--------- .../passes/loopclosure/loopclosure_test.go | 5 ++--- .../passes/loopclosure/testdata/src/a/a.go | 15 +++++++++++++++ .../passes/loopclosure/testdata/src/a/b.go | 9 +++++++++ 4 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 go/analysis/passes/loopclosure/testdata/src/a/b.go diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index aed3e431438..645e5895bb6 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -59,10 +59,12 @@ func run(pass *analysis.Pass) (interface{}, error) { } inspect.Preorder(nodeFilter, func(n ast.Node) { // Find the variables updated by the loop statement. - var vars []*ast.Ident + var vars []types.Object addVar := func(expr ast.Expr) { - if id, ok := expr.(*ast.Ident); ok { - vars = append(vars, id) + if id, _ := expr.(*ast.Ident); id != nil { + if obj := pass.TypesInfo.ObjectOf(id); obj != nil { + vars = append(vars, obj) + } } } var body *ast.BlockStmt @@ -132,17 +134,16 @@ func run(pass *analysis.Pass) (interface{}, error) { ast.Inspect(lit.Body, func(n ast.Node) bool { id, ok := n.(*ast.Ident) - if !ok || id.Obj == nil { + if !ok { return true } - if pass.TypesInfo.Types[id].Type == nil { - // Not referring to a variable (e.g. struct field name) + obj := pass.TypesInfo.Uses[id] + if obj == nil { return true } for _, v := range vars { - if v.Obj == id.Obj { - pass.ReportRangef(id, "loop variable %s captured by func literal", - id.Name) + if v == obj { + pass.ReportRangef(id, "loop variable %s captured by func literal", id.Name) } } return true diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go index e00ae21fa8a..8bf1d7a7bcc 100644 --- a/go/analysis/passes/loopclosure/loopclosure_test.go +++ b/go/analysis/passes/loopclosure/loopclosure_test.go @@ -7,11 +7,10 @@ package loopclosure_test import ( "testing" - "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/typeparams" - "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/loopclosure" + "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/typeparams" ) func Test(t *testing.T) { diff --git a/go/analysis/passes/loopclosure/testdata/src/a/a.go b/go/analysis/passes/loopclosure/testdata/src/a/a.go index 2fc3f21f1cb..2a17d16bc86 100644 --- a/go/analysis/passes/loopclosure/testdata/src/a/a.go +++ b/go/analysis/passes/loopclosure/testdata/src/a/a.go @@ -10,6 +10,8 @@ import ( "golang.org/x/sync/errgroup" ) +var A int + func _() { var s []int for i, v := range s { @@ -51,6 +53,19 @@ func _() { println(i, v) }() } + + // iteration variable declared outside the loop + for A = range s { + go func() { + println(A) // want "loop variable A captured by func literal" + }() + } + // iteration variable declared in a different file + for B = range s { + go func() { + println(B) // want "loop variable B captured by func literal" + }() + } // If the key of the range statement is not an identifier // the code should not panic (it used to). var x [2]int diff --git a/go/analysis/passes/loopclosure/testdata/src/a/b.go b/go/analysis/passes/loopclosure/testdata/src/a/b.go new file mode 100644 index 00000000000..d4e5da418e5 --- /dev/null +++ b/go/analysis/passes/loopclosure/testdata/src/a/b.go @@ -0,0 +1,9 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testdata + +// B is declared in a separate file to test that object resolution spans the +// entire package. +var B int From 2f04713366409088fb7139d5b092b7e82914dbd0 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 20 Sep 2022 14:59:24 -0400 Subject: [PATCH 261/723] gopls: fix out of bounds bug in extract There was a bug where the code would try to access an out of bounds index when trimming extra space and comments in extract. This change also adds a regtest for extract. Fixes golang/go#55271 Change-Id: I7da716a6a68a9678011abf15def47acdea0b33fe Reviewed-on: https://go-review.googlesource.com/c/tools/+/432135 Run-TryBot: Suzy Mueller TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/source/extract.go | 9 ++- gopls/internal/regtest/misc/extract_test.go | 69 +++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 gopls/internal/regtest/misc/extract_test.go diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index 9a143a78c29..8e6f77d1e6b 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -676,7 +676,7 @@ func adjustRangeForCommentsAndWhiteSpace(rng span.Range, tok *token.File, conten prevStart = start // If start is within a comment, move start to the end // of the comment group. - if file.Comments[startComment].Pos() <= start && start < file.Comments[startComment].End() { + if startComment < len(file.Comments) && file.Comments[startComment].Pos() <= start && start < file.Comments[startComment].End() { start = file.Comments[startComment].End() startComment++ } @@ -697,11 +697,16 @@ func adjustRangeForCommentsAndWhiteSpace(rng span.Range, tok *token.File, conten // Find the index for the first comment that ends after the range end. return file.Comments[i].End() >= rng.End }) + // Search will return n if not found, so we need to adjust if there are no + // comments that would match. + if endComment == len(file.Comments) { + endComment = -1 + } for prevEnd != end { prevEnd = end // If end is within a comment, move end to the start // of the comment group. - if file.Comments[endComment].Pos() < end && end <= file.Comments[endComment].End() { + if endComment >= 0 && file.Comments[endComment].Pos() < end && end <= file.Comments[endComment].End() { end = file.Comments[endComment].Pos() endComment-- } diff --git a/gopls/internal/regtest/misc/extract_test.go b/gopls/internal/regtest/misc/extract_test.go new file mode 100644 index 00000000000..45ddb5408a5 --- /dev/null +++ b/gopls/internal/regtest/misc/extract_test.go @@ -0,0 +1,69 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package misc + +import ( + "testing" + + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + + "golang.org/x/tools/gopls/internal/lsp/protocol" +) + +func TestExtractFunction(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- main.go -- +package main + +func Foo() int { + a := 5 + return a +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + + start := env.RegexpSearch("main.go", "a := 5").ToProtocolPosition() + end := env.RegexpSearch("main.go", "return a").ToProtocolPosition() + + actions, err := env.Editor.CodeAction(env.Ctx, "main.go", &protocol.Range{Start: start, End: end}, nil) + if err != nil { + t.Fatal(err) + } + + // Find the extract function code action. + var extractFunc *protocol.CodeAction + for _, action := range actions { + if action.Kind == protocol.RefactorExtract && action.Title == "Extract function" { + extractFunc = &action + break + } + } + if extractFunc == nil { + t.Fatal("could not find extract function action") + } + + env.ApplyCodeAction(*extractFunc) + want := `package main + +func Foo() int { + a := newFunction() + return a +} + +func newFunction() int { + a := 5 + return a +} +` + if got := env.Editor.BufferText("main.go"); got != want { + t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) + } + }) +} From be3ff020ad71cec50614f6d4737fc91b21ada474 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 21 Sep 2022 10:25:41 +0700 Subject: [PATCH 262/723] go/analysis/passes/sortslice: correct diagnostic for sort.Slice(f()) Fixes golang/go#55098 Change-Id: I801279532c448bc4c9d5fce0cc3236e95fb77fdb Reviewed-on: https://go-review.googlesource.com/c/tools/+/431295 Reviewed-by: Johan Brandhorst-Satzkorn Auto-Submit: Cuong Manh Le Reviewed-by: Tim King Run-TryBot: Cuong Manh Le TryBot-Result: Gopher Robot Reviewed-by: Cherry Mui gopls-CI: kokoro --- go/analysis/passes/sortslice/analyzer.go | 9 +++++++ .../passes/sortslice/testdata/src/a/a.go | 24 +++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/go/analysis/passes/sortslice/analyzer.go b/go/analysis/passes/sortslice/analyzer.go index 5eb957a1883..f85837d66bf 100644 --- a/go/analysis/passes/sortslice/analyzer.go +++ b/go/analysis/passes/sortslice/analyzer.go @@ -52,11 +52,20 @@ func run(pass *analysis.Pass) (interface{}, error) { arg := call.Args[0] typ := pass.TypesInfo.Types[arg].Type + + if tuple, ok := typ.(*types.Tuple); ok { + typ = tuple.At(0).Type() // special case for Slice(f(...)) + } + switch typ.Underlying().(type) { case *types.Slice, *types.Interface: return } + // Restore typ to the original type, we may unwrap the tuple above, + // typ might not be the type of arg. + typ = pass.TypesInfo.Types[arg].Type + var fixes []analysis.SuggestedFix switch v := typ.Underlying().(type) { case *types.Array: diff --git a/go/analysis/passes/sortslice/testdata/src/a/a.go b/go/analysis/passes/sortslice/testdata/src/a/a.go index bc6cc16e9f1..c6aca8df13b 100644 --- a/go/analysis/passes/sortslice/testdata/src/a/a.go +++ b/go/analysis/passes/sortslice/testdata/src/a/a.go @@ -6,8 +6,8 @@ import "sort" func IncorrectSort() { i := 5 sortFn := func(i, j int) bool { return false } - sort.Slice(i, sortFn) // want "sort.Slice's argument must be a slice; is called with int" - sort.SliceStable(i, sortFn) // want "sort.SliceStable's argument must be a slice; is called with int" + sort.Slice(i, sortFn) // want "sort.Slice's argument must be a slice; is called with int" + sort.SliceStable(i, sortFn) // want "sort.SliceStable's argument must be a slice; is called with int" sort.SliceIsSorted(i, sortFn) // want "sort.SliceIsSorted's argument must be a slice; is called with int" } @@ -62,3 +62,23 @@ func UnderlyingSlice() { sort.SliceStable(s, sortFn) sort.SliceIsSorted(s, sortFn) } + +// FunctionResultsAsArguments passes a function which returns two values +// that satisfy sort.Slice signature. It should not produce a diagnostic. +func FunctionResultsAsArguments() { + s := []string{"a", "z", "ooo"} + sort.Slice(less(s)) + sort.Slice(lessPtr(s)) // want `sort.Slice's argument must be a slice; is called with \(\*\[\]string,.*` +} + +func less(s []string) ([]string, func(i, j int) bool) { + return s, func(i, j int) bool { + return s[i] < s[j] + } +} + +func lessPtr(s []string) (*[]string, func(i, j int) bool) { + return &s, func(i, j int) bool { + return s[i] < s[j] + } +} From 62ae58610f8ab1524dda5a51d8d7bc0ce2373acd Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 22 Sep 2022 14:21:49 -0400 Subject: [PATCH 263/723] gopls/test: disable stderr output for command line tests as well Suppress output in gopls command line tests, similarly to CL 431843. For golang/go#54845 Change-Id: I7f1e1de52c9a8fb0d902bfdd61bc99d4e2e308b9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/432955 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/test/gopls_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gopls/test/gopls_test.go b/gopls/test/gopls_test.go index 248a03b789d..95534d44327 100644 --- a/gopls/test/gopls_test.go +++ b/gopls/test/gopls_test.go @@ -13,12 +13,20 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/testenv" ) func TestMain(m *testing.M) { bug.PanicOnBugs = true testenv.ExitIfSmallMachine() + + // Set the global exporter to nil so that we don't log to stderr. This avoids + // a lot of misleading noise in test output. + // + // See also ../internal/lsp/lsp_test.go. + event.SetExporter(nil) + os.Exit(m.Run()) } From b3ab50b78241a44f00a4b526798d6179789cb8ba Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 21 Sep 2022 14:37:50 -0700 Subject: [PATCH 264/723] go/analysis/passes/stdmethods: recognize Unwrap() []error Recognize the new multiple-error-wrapping method signature. For golang/go#53435 Change-Id: Ifba25746323d036d1e6d3e6d3c34cd6ce904b60a Reviewed-on: https://go-review.googlesource.com/c/tools/+/432575 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Cherry Mui Run-TryBot: Damien Neil Reviewed-by: Joseph Tsai --- go/analysis/passes/stdmethods/stdmethods.go | 13 +++++++++++++ go/analysis/passes/stdmethods/testdata/src/a/a.go | 14 ++++++++++++-- .../testdata/src/typeparams/typeparams.go | 4 ++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/go/analysis/passes/stdmethods/stdmethods.go b/go/analysis/passes/stdmethods/stdmethods.go index cc9497179da..41f455d1003 100644 --- a/go/analysis/passes/stdmethods/stdmethods.go +++ b/go/analysis/passes/stdmethods/stdmethods.go @@ -134,6 +134,19 @@ func canonicalMethod(pass *analysis.Pass, id *ast.Ident) { } } + // Special case: Unwrap has two possible signatures. + // Check for Unwrap() []error here. + if id.Name == "Unwrap" { + if args.Len() == 0 && results.Len() == 1 { + t := typeString(results.At(0).Type()) + if t == "error" || t == "[]error" { + return + } + } + pass.ReportRangef(id, "method Unwrap() should have signature Unwrap() error or Unwrap() []error") + return + } + // Do the =s (if any) all match? if !matchParams(pass, expect.args, args, "=") || !matchParams(pass, expect.results, results, "=") { return diff --git a/go/analysis/passes/stdmethods/testdata/src/a/a.go b/go/analysis/passes/stdmethods/testdata/src/a/a.go index c95cf5d2b76..2b01f46932f 100644 --- a/go/analysis/passes/stdmethods/testdata/src/a/a.go +++ b/go/analysis/passes/stdmethods/testdata/src/a/a.go @@ -49,7 +49,7 @@ func (E) Error() string { return "" } // E implements error. func (E) As() {} // want `method As\(\) should have signature As\((any|interface\{\})\) bool` func (E) Is() {} // want `method Is\(\) should have signature Is\(error\) bool` -func (E) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error` +func (E) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error or Unwrap\(\) \[\]error` type F int @@ -57,8 +57,18 @@ func (F) Error() string { return "" } // Both F and *F implement error. func (*F) As() {} // want `method As\(\) should have signature As\((any|interface\{\})\) bool` func (*F) Is() {} // want `method Is\(\) should have signature Is\(error\) bool` -func (*F) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error` +func (*F) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error or Unwrap\(\) \[\]error` type G int func (G) As(interface{}) bool // ok + +type W int + +func (W) Error() string { return "" } +func (W) Unwrap() error { return nil } // ok + +type M int + +func (M) Error() string { return "" } +func (M) Unwrap() []error { return nil } // ok diff --git a/go/analysis/passes/stdmethods/testdata/src/typeparams/typeparams.go b/go/analysis/passes/stdmethods/testdata/src/typeparams/typeparams.go index 72df30d4960..3d4146e9b2c 100644 --- a/go/analysis/passes/stdmethods/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/stdmethods/testdata/src/typeparams/typeparams.go @@ -30,7 +30,7 @@ func (E[_]) Error() string { return "" } // E implements error. func (E[P]) As() {} // want `method As\(\) should have signature As\((any|interface\{\})\) bool` func (E[_]) Is() {} // want `method Is\(\) should have signature Is\(error\) bool` -func (E[_]) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error` +func (E[_]) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error or Unwrap\(\) \[\]error` type F[P any] int @@ -38,4 +38,4 @@ func (F[_]) Error() string { return "" } // Both F and *F implement error. func (*F[_]) As() {} // want `method As\(\) should have signature As\((any|interface\{\})\) bool` func (*F[_]) Is() {} // want `method Is\(\) should have signature Is\(error\) bool` -func (*F[_]) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error` +func (*F[_]) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error or Unwrap\(\) \[\]error` From 1877b5f33c7fcf17cf2d1624385115edd44260b1 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 21 Sep 2022 18:05:19 -0700 Subject: [PATCH 265/723] go/analysis/passes/printf: permit multiple %w format verbs For golang/go#53435 Change-Id: Icdc664585fbcf7ac07ef92df8b43b20c1d7733e2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/432576 Run-TryBot: Damien Neil gopls-CI: kokoro Reviewed-by: Cherry Mui Reviewed-by: Joseph Tsai TryBot-Result: Gopher Robot --- go/analysis/passes/printf/printf.go | 6 ------ go/analysis/passes/printf/testdata/src/a/a.go | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 87f171e16c8..19ca4527af2 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -583,7 +583,6 @@ func checkPrintf(pass *analysis.Pass, kind Kind, call *ast.CallExpr, fn *types.F argNum := firstArg maxArgNum := firstArg anyIndex := false - anyW := false for i, w := 0, 0; i < len(format); i += w { w = 1 if format[i] != '%' { @@ -606,11 +605,6 @@ func checkPrintf(pass *analysis.Pass, kind Kind, call *ast.CallExpr, fn *types.F pass.Reportf(call.Pos(), "%s does not support error-wrapping directive %%w", state.name) return } - if anyW { - pass.Reportf(call.Pos(), "%s call has more than one error-wrapping directive %%w", state.name) - return - } - anyW = true } if len(state.argNums) > 0 { // Continue with the next sequential argument. diff --git a/go/analysis/passes/printf/testdata/src/a/a.go b/go/analysis/passes/printf/testdata/src/a/a.go index 1721ecc1406..0c4d11bf0c0 100644 --- a/go/analysis/passes/printf/testdata/src/a/a.go +++ b/go/analysis/passes/printf/testdata/src/a/a.go @@ -342,7 +342,7 @@ func PrintfTests() { _ = fmt.Errorf("%[2]w %[1]s", "x", err) // OK _ = fmt.Errorf("%[2]w %[1]s", e, "x") // want `fmt.Errorf format %\[2\]w has arg "x" of wrong type string` _ = fmt.Errorf("%w", "x") // want `fmt.Errorf format %w has arg "x" of wrong type string` - _ = fmt.Errorf("%w %w", err, err) // want `fmt.Errorf call has more than one error-wrapping directive %w` + _ = fmt.Errorf("%w %w", err, err) // OK _ = fmt.Errorf("%w", interface{}(nil)) // want `fmt.Errorf format %w has arg interface{}\(nil\) of wrong type interface{}` _ = fmt.Errorf("%w", errorTestOK(0)) // concrete value implements error _ = fmt.Errorf("%w", errSubset) // interface value implements error From 3dda4ba24c58e00bc2c883dc54985f4a20fef843 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Wed, 21 Sep 2022 19:36:22 +0000 Subject: [PATCH 266/723] all: replace deprecated egrep with grep -E egrep command is about to be deprecated, use grep -E instead. Reference: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xcu_chap04.html Change-Id: Ib37f5b244ead4c2d46078bea34d4cb6363c48b1b GitHub-Last-Rev: b1e16d0212d682c780668e62ac8659973bb11a9d GitHub-Pull-Request: golang/tools#400 Reviewed-on: https://go-review.googlesource.com/c/tools/+/432295 Reviewed-by: Bryan Mills Reviewed-by: Robert Findley --- cmd/toolstash/buildall | 4 ++-- gopls/internal/hooks/gen-licenses.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/toolstash/buildall b/cmd/toolstash/buildall index 0c6492c9efa..4fc22f7f8fc 100755 --- a/cmd/toolstash/buildall +++ b/cmd/toolstash/buildall @@ -38,10 +38,10 @@ if [ "$pattern" = "" ]; then fi targets="$(go tool dist list; echo linux/386/softfloat)" -targets="$(echo "$targets" | tr '/' '-' | sort | egrep "$pattern" | egrep -v 'android-arm|darwin-arm')" +targets="$(echo "$targets" | tr '/' '-' | sort | grep -E "$pattern" | grep -E -v 'android-arm|darwin-arm')" # put linux first in the target list to get all the architectures up front. -targets="$(echo "$targets" | egrep 'linux') $(echo "$targets" | egrep -v 'linux')" +targets="$(echo "$targets" | grep -E 'linux') $(echo "$targets" | grep -E -v 'linux')" if [ "$sete" = true ]; then set -e diff --git a/gopls/internal/hooks/gen-licenses.sh b/gopls/internal/hooks/gen-licenses.sh index 7d6bab79f54..c35c91260d4 100755 --- a/gopls/internal/hooks/gen-licenses.sh +++ b/gopls/internal/hooks/gen-licenses.sh @@ -27,7 +27,7 @@ mods=$(go list -deps -f '{{with .Module}}{{.Path}}{{end}}' golang.org/x/tools/go for mod in $mods; do # Find the license file, either LICENSE or COPYING, and add it to the result. dir=$(go list -m -f {{.Dir}} $mod) - license=$(ls -1 $dir | egrep -i '^(LICENSE|COPYING)$') + license=$(ls -1 $dir | grep -E -i '^(LICENSE|COPYING)$') echo "-- $mod $license --" >> $tempfile echo >> $tempfile sed 's/^-- / &/' $dir/$license >> $tempfile From b9adce94b7f1960056e7c2866b1a5b70a237adfe Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 9 May 2022 12:14:36 -0400 Subject: [PATCH 267/723] internal/lsp/source: derive document symbols from syntax alone The documentSymbols handler joined syntax information with type information, meaning that it was only able to satisfy requests for files in valid workspace packages. However, the value added by type information was limited, and in many cases could be derived from syntax alone. For example, while generating symbols for a const declaration, we don't need the type checker to tell us that the symbol kind is const. Refactor the documentSymbols handler to derive symbols from syntax alone. This leads to some simplifications from the code, in addition to eliminating the dependency on package data. Also, simplify symbol details to just use types.ExprString, which includes some missing information such as function return values. Also, update handling to support Go 1.18 type embedding in interfaces. Notably, this reverts decisions like golang/go#31202, in which we went to effort to make the symbol kind more accurate. In my opinion (and the opinion expressed in golang/go#52797), the cost of requiring type information is not worth the minor improvement in accuracy of the symbol kind, which (as far as I know) is only used for UI elements. To facilitate testing (and start to clean up the test framework), make several simplifications / improvements to the marker tests: - simplify the collection of symbol data - eliminate unused marks - just use cmp.Diff for comparing results - allow for arbitrary nesting of symbols. - remove unnecessary @symbol annotations from workspace_symbol tests -- their data is not used by workspace_symbol handlers - remove Symbol and WorkspaceSymbol handlers from source_test.go. On inspection, these handlers were redundant with lsp_test.go. Notably, the collection and assembly of @symbol annotations is still way too complicated. It would be much simpler to just have a single golden file summarizing the entire output, rather than weaving it together from annotations. However, I realized this too late, and so it will have to wait for a separate CL. Fixes golang/go#52797 Fixes golang/vscode-go#2242 Updates golang/go#54845 Change-Id: I3a2c2d39f59f9d045a6cedf8023ff0c80a69d974 Reviewed-on: https://go-review.googlesource.com/c/tools/+/405254 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/parse.go | 27 +- gopls/internal/lsp/cmd/test/symbols.go | 5 +- gopls/internal/lsp/lsp_test.go | 30 +- gopls/internal/lsp/lsppos/token.go | 7 + gopls/internal/lsp/protocol/span.go | 7 + gopls/internal/lsp/source/source_test.go | 39 +-- gopls/internal/lsp/source/symbols.go | 270 ++++++++---------- gopls/internal/lsp/source/util.go | 61 +++- .../internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- gopls/internal/lsp/testdata/symbols/go1.18.go | 16 ++ .../lsp/testdata/symbols/go1.18.go.golden | 7 + gopls/internal/lsp/testdata/symbols/main.go | 85 ++++-- .../lsp/testdata/symbols/main.go.golden | 63 ++-- .../lsp/testdata/workspacesymbol/a/a.go | 6 +- .../testdata/workspacesymbol/a/a.go.golden | 5 - .../lsp/testdata/workspacesymbol/a/a_test.go | 2 +- .../workspacesymbol/a/a_test.go.golden | 3 - .../testdata/workspacesymbol/a/a_x_test.go | 2 +- .../workspacesymbol/a/a_x_test.go.golden | 3 - .../lsp/testdata/workspacesymbol/b/b.go | 6 +- .../testdata/workspacesymbol/b/b.go.golden | 5 - gopls/internal/lsp/tests/tests.go | 104 ++++--- gopls/internal/lsp/tests/util.go | 49 +--- 24 files changed, 403 insertions(+), 403 deletions(-) create mode 100644 gopls/internal/lsp/testdata/symbols/go1.18.go create mode 100644 gopls/internal/lsp/testdata/symbols/go1.18.go.golden delete mode 100644 gopls/internal/lsp/testdata/workspacesymbol/a/a.go.golden delete mode 100644 gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden delete mode 100644 gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden delete mode 100644 gopls/internal/lsp/testdata/workspacesymbol/b/b.go.golden diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index 5cb5ef7203e..981e698fb6e 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -18,13 +18,13 @@ import ( "strconv" "strings" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/myers" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/memoize" ) @@ -237,7 +237,7 @@ func (f *unexportedFilter) keep(ident *ast.Ident) bool { func (f *unexportedFilter) filterDecl(decl ast.Decl) bool { switch decl := decl.(type) { case *ast.FuncDecl: - if ident := recvIdent(decl); ident != nil && !f.keep(ident) { + if ident := source.RecvIdent(decl.Recv); ident != nil && !f.keep(ident) { return false } return f.keep(decl.Name) @@ -312,7 +312,7 @@ func (f *unexportedFilter) recordUses(file *ast.File) { switch decl := decl.(type) { case *ast.FuncDecl: // Ignore methods on dropped types. - if ident := recvIdent(decl); ident != nil && !f.keep(ident) { + if ident := source.RecvIdent(decl.Recv); ident != nil && !f.keep(ident) { break } // Ignore functions with dropped names. @@ -356,21 +356,6 @@ func (f *unexportedFilter) recordUses(file *ast.File) { } } -// recvIdent returns the identifier of a method receiver, e.g. *int. -func recvIdent(decl *ast.FuncDecl) *ast.Ident { - if decl.Recv == nil || len(decl.Recv.List) == 0 { - return nil - } - x := decl.Recv.List[0].Type - if star, ok := x.(*ast.StarExpr); ok { - x = star.X - } - if ident, ok := x.(*ast.Ident); ok { - return ident - } - return nil -} - // recordIdents records unexported identifiers in an Expr in uses. // These may be types, e.g. in map[key]value, function names, e.g. in foo(), // or simple variable references. References that will be discarded, such diff --git a/gopls/internal/lsp/cmd/test/symbols.go b/gopls/internal/lsp/cmd/test/symbols.go index fbaf679336f..3bd2fc02247 100644 --- a/gopls/internal/lsp/cmd/test/symbols.go +++ b/gopls/internal/lsp/cmd/test/symbols.go @@ -8,6 +8,7 @@ import ( "testing" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/span" ) @@ -17,7 +18,7 @@ func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol. expect := string(r.data.Golden(t, "symbols", filename, func() ([]byte, error) { return []byte(got), nil })) - if expect != got { - t.Errorf("symbols failed for %s expected:\n%s\ngot:\n%s", filename, expect, got) + if diff := compare.Text(expect, got); diff != "" { + t.Errorf("symbols differ from expected:\n%s", diff) } } diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 1cb5b3abb4d..02b57273368 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -15,6 +15,8 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -1147,10 +1149,7 @@ func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol. if err != nil { t.Fatal(err) } - if len(got) != len(expectedSymbols) { - t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(got)) - return - } + symbols := make([]protocol.DocumentSymbol, len(got)) for i, s := range got { s, ok := s.(protocol.DocumentSymbol) @@ -1159,18 +1158,25 @@ func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol. } symbols[i] = s } - if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" { - t.Error(diff) + + // Sort by position to make it easier to find errors. + sortSymbols := func(s []protocol.DocumentSymbol) { + sort.Slice(s, func(i, j int) bool { + return protocol.CompareRange(s[i].SelectionRange, s[j].SelectionRange) < 0 + }) } -} + sortSymbols(expectedSymbols) + sortSymbols(symbols) -func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - r.callWorkspaceSymbols(t, uri, query, typ) + // Ignore 'Range' here as it is difficult (impossible?) to express + // multi-line ranges in the packagestest framework. + ignoreRange := cmpopts.IgnoreFields(protocol.DocumentSymbol{}, "Range") + if diff := cmp.Diff(expectedSymbols, symbols, ignoreRange); diff != "" { + t.Errorf("mismatching symbols (-want +got)\n%s", diff) + } } -func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - t.Helper() - +func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { matcher := tests.WorkspaceSymbolsTestTypeToMatcher(typ) original := r.server.session.Options() diff --git a/gopls/internal/lsp/lsppos/token.go b/gopls/internal/lsp/lsppos/token.go index 2a16ba283ed..c1b726fe24c 100644 --- a/gopls/internal/lsp/lsppos/token.go +++ b/gopls/internal/lsp/lsppos/token.go @@ -6,6 +6,7 @@ package lsppos import ( "errors" + "go/ast" "go/token" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -58,3 +59,9 @@ func (m *TokenMapper) Range(start, end token.Pos) (protocol.Range, error) { return protocol.Range{Start: startPos, End: endPos}, nil } + +// NodeRange returns the protocol range corresponding to the span of the given +// node. +func (m *TokenMapper) NodeRange(n ast.Node) (protocol.Range, error) { + return m.Range(n.Pos(), n.End()) +} diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 744746d3538..23721ee703a 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -148,6 +148,11 @@ func IsPoint(r Range) bool { return r.Start.Line == r.End.Line && r.Start.Character == r.End.Character } +// CompareRange returns -1 if a is before b, 0 if a == b, and 1 if a is after +// b. +// +// A range a is defined to be 'before' b if a.Start is before b.Start, or +// a.Start == b.Start and a.End is before b.End. func CompareRange(a, b Range) int { if r := ComparePosition(a.Start, b.Start); r != 0 { return r @@ -155,6 +160,8 @@ func CompareRange(a, b Range) int { return ComparePosition(a.End, b.End) } +// ComparePosition returns -1 if a is before b, 0 if a == b, and 1 if a is +// after b. func ComparePosition(a, b Position) int { if a.Line < b.Line { return -1 diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index ce2cc0aa36e..ad2ca6b72af 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -879,46 +879,11 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare } func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { - fh, err := r.snapshot.GetFile(r.ctx, uri) - if err != nil { - t.Fatal(err) - } - symbols, err := source.DocumentSymbols(r.ctx, r.snapshot, fh) - if err != nil { - t.Errorf("symbols failed for %s: %v", uri, err) - } - if len(symbols) != len(expectedSymbols) { - t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols)) - return - } - if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" { - t.Error(diff) - } + // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go } func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - r.callWorkspaceSymbols(t, uri, query, typ) -} - -func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - t.Helper() - - matcher := tests.WorkspaceSymbolsTestTypeToMatcher(typ) - gotSymbols, err := source.WorkspaceSymbols(r.ctx, matcher, r.view.Options().SymbolStyle, []source.View{r.view}, query) - if err != nil { - t.Fatal(err) - } - got, err := tests.WorkspaceSymbolsString(r.ctx, r.data, uri, gotSymbols) - if err != nil { - t.Fatal(err) - } - got = filepath.ToSlash(tests.Normalize(got, r.normalizers)) - want := string(r.data.Golden(t, fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { - return []byte(got), nil - })) - if d := compare.Text(want, got); d != "" { - t.Error(d) - } + // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go } func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) { diff --git a/gopls/internal/lsp/source/symbols.go b/gopls/internal/lsp/source/symbols.go index 802de37c342..2bab241e1d5 100644 --- a/gopls/internal/lsp/source/symbols.go +++ b/gopls/internal/lsp/source/symbols.go @@ -8,25 +8,34 @@ import ( "context" "fmt" "go/ast" + "go/token" "go/types" - "golang.org/x/tools/internal/event" + "golang.org/x/tools/gopls/internal/lsp/lsppos" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/event" ) func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.DocumentSymbol, error) { ctx, done := event.Start(ctx, "source.DocumentSymbols") defer done() - pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + content, err := fh.Read() + if err != nil { + return nil, err + } + + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, fmt.Errorf("getting file for DocumentSymbols: %w", err) } - info := pkg.GetTypesInfo() - q := Qualifier(pgf.File, pkg.GetTypes(), info) + m := lsppos.NewTokenMapper(content, pgf.Tok) - symbolsToReceiver := make(map[types.Type]int) + // Build symbols for file declarations. When encountering a declaration with + // errors (typically because positions are invalid), we skip the declaration + // entirely. VS Code fails to show any symbols if one of the top-level + // symbols is missing position information. var symbols []protocol.DocumentSymbol for _, decl := range pgf.File.Decls { switch decl := decl.(type) { @@ -34,15 +43,11 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p if decl.Name.Name == "_" { continue } - if obj := info.ObjectOf(decl.Name); obj != nil { - fs, err := funcSymbol(snapshot, pkg, decl, obj, q) - if err != nil { - return nil, err - } + fs, err := funcSymbol(m, decl) + if err == nil { // If function is a method, prepend the type of the method. - if fs.Kind == protocol.Method { - rtype := obj.Type().(*types.Signature).Recv().Type() - fs.Name = fmt.Sprintf("(%s).%s", types.TypeString(rtype, q), fs.Name) + if decl.Recv != nil && len(decl.Recv.List) > 0 { + fs.Name = fmt.Sprintf("(%s).%s", types.ExprString(decl.Recv.List[0].Type), fs.Name) } symbols = append(symbols, fs) } @@ -53,24 +58,17 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p if spec.Name.Name == "_" { continue } - if obj := info.ObjectOf(spec.Name); obj != nil { - ts, err := typeSymbol(snapshot, pkg, info, spec, obj, q) - if err != nil { - return nil, err - } + ts, err := typeSymbol(m, spec) + if err == nil { symbols = append(symbols, ts) - symbolsToReceiver[obj.Type()] = len(symbols) - 1 } case *ast.ValueSpec: for _, name := range spec.Names { if name.Name == "_" { continue } - if obj := info.ObjectOf(name); obj != nil { - vs, err := varSymbol(snapshot, pkg, decl, name, obj, q) - if err != nil { - return nil, err - } + vs, err := varSymbol(m, spec, name, decl.Tok == token.CONST) + if err == nil { symbols = append(symbols, vs) } } @@ -81,185 +79,157 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p return symbols, nil } -func funcSymbol(snapshot Snapshot, pkg Package, decl *ast.FuncDecl, obj types.Object, q types.Qualifier) (protocol.DocumentSymbol, error) { +func funcSymbol(m *lsppos.TokenMapper, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) { s := protocol.DocumentSymbol{ - Name: obj.Name(), + Name: decl.Name.Name, Kind: protocol.Function, } + if decl.Recv != nil { + s.Kind = protocol.Method + } var err error - s.Range, err = nodeToProtocolRange(snapshot, pkg, decl) + s.Range, err = m.Range(decl.Pos(), decl.End()) if err != nil { return protocol.DocumentSymbol{}, err } - s.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, decl.Name) + s.SelectionRange, err = m.Range(decl.Name.Pos(), decl.Name.End()) if err != nil { return protocol.DocumentSymbol{}, err } - sig, _ := obj.Type().(*types.Signature) - if sig != nil { - if sig.Recv() != nil { - s.Kind = protocol.Method - } - s.Detail += "(" - for i := 0; i < sig.Params().Len(); i++ { - if i > 0 { - s.Detail += ", " - } - param := sig.Params().At(i) - label := types.TypeString(param.Type(), q) - if param.Name() != "" { - label = fmt.Sprintf("%s %s", param.Name(), label) - } - s.Detail += label - } - s.Detail += ")" - } + s.Detail = types.ExprString(decl.Type) return s, nil } -func typeSymbol(snapshot Snapshot, pkg Package, info *types.Info, spec *ast.TypeSpec, obj types.Object, qf types.Qualifier) (protocol.DocumentSymbol, error) { +func typeSymbol(m *lsppos.TokenMapper, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) { s := protocol.DocumentSymbol{ - Name: obj.Name(), + Name: spec.Name.Name, } - s.Detail, _ = FormatType(obj.Type(), qf) - s.Kind = typeToKind(obj.Type()) - var err error - s.Range, err = nodeToProtocolRange(snapshot, pkg, spec) + s.Range, err = m.NodeRange(spec) if err != nil { return protocol.DocumentSymbol{}, err } - s.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, spec.Name) + s.SelectionRange, err = m.NodeRange(spec.Name) if err != nil { return protocol.DocumentSymbol{}, err } - t, objIsStruct := obj.Type().Underlying().(*types.Struct) - st, specIsStruct := spec.Type.(*ast.StructType) - if objIsStruct && specIsStruct { - for i := 0; i < t.NumFields(); i++ { - f := t.Field(i) - child := protocol.DocumentSymbol{ - Name: f.Name(), - Kind: protocol.Field, - } - child.Detail, _ = FormatType(f.Type(), qf) + s.Kind, s.Detail, s.Children = typeDetails(m, spec.Type) + return s, nil +} - spanNode, selectionNode := nodesForStructField(i, st) - if span, err := nodeToProtocolRange(snapshot, pkg, spanNode); err == nil { - child.Range = span - } - if span, err := nodeToProtocolRange(snapshot, pkg, selectionNode); err == nil { - child.SelectionRange = span - } - s.Children = append(s.Children, child) +func typeDetails(m *lsppos.TokenMapper, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) { + switch typExpr := typExpr.(type) { + case *ast.StructType: + kind = protocol.Struct + children = fieldListSymbols(m, typExpr.Fields, protocol.Field) + if len(children) > 0 { + detail = "struct{...}" + } else { + detail = "struct{}" + } + + // Find interface methods and embedded types. + case *ast.InterfaceType: + kind = protocol.Interface + children = fieldListSymbols(m, typExpr.Methods, protocol.Method) + if len(children) > 0 { + detail = "interface{...}" + } else { + detail = "interface{}" } + + case *ast.FuncType: + kind = protocol.Function + detail = types.ExprString(typExpr) + + default: + kind = protocol.Class // catch-all, for cases where we don't know the kind syntactically + detail = types.ExprString(typExpr) + } + return +} + +func fieldListSymbols(m *lsppos.TokenMapper, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol { + if fields == nil { + return nil } - ti, objIsInterface := obj.Type().Underlying().(*types.Interface) - ai, specIsInterface := spec.Type.(*ast.InterfaceType) - if objIsInterface && specIsInterface { - for i := 0; i < ti.NumExplicitMethods(); i++ { - method := ti.ExplicitMethod(i) + var symbols []protocol.DocumentSymbol + for _, field := range fields.List { + detail, children := "", []protocol.DocumentSymbol(nil) + if field.Type != nil { + _, detail, children = typeDetails(m, field.Type) + } + if len(field.Names) == 0 { // embedded interface or struct field + // By default, use the formatted type details as the name of this field. + // This handles potentially invalid syntax, as well as type embeddings in + // interfaces. child := protocol.DocumentSymbol{ - Name: method.Name(), - Kind: protocol.Method, + Name: detail, + Kind: protocol.Field, // consider all embeddings to be fields + Children: children, } - var spanNode, selectionNode ast.Node - Methods: - for _, f := range ai.Methods.List { - for _, id := range f.Names { - if id.Name == method.Name() { - spanNode, selectionNode = f, id - break Methods - } - } - } - child.Range, err = nodeToProtocolRange(snapshot, pkg, spanNode) - if err != nil { - return protocol.DocumentSymbol{}, err + // If the field is a valid embedding, promote the type name to field + // name. + selection := field.Type + if id := embeddedIdent(field.Type); id != nil { + child.Name = id.Name + child.Detail = detail + selection = id } - child.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, selectionNode) - if err != nil { - return protocol.DocumentSymbol{}, err - } - s.Children = append(s.Children, child) - } - for i := 0; i < ti.NumEmbeddeds(); i++ { - embedded := ti.EmbeddedType(i) - nt, isNamed := embedded.(*types.Named) - if !isNamed { - continue + if rng, err := m.NodeRange(field.Type); err == nil { + child.Range = rng } - - child := protocol.DocumentSymbol{ - Name: types.TypeString(embedded, qf), + if rng, err := m.NodeRange(selection); err == nil { + child.SelectionRange = rng } - child.Kind = typeToKind(embedded) - var spanNode, selectionNode ast.Node - Embeddeds: - for _, f := range ai.Methods.List { - if len(f.Names) > 0 { - continue + + symbols = append(symbols, child) + } else { + for _, name := range field.Names { + child := protocol.DocumentSymbol{ + Name: name.Name, + Kind: fieldKind, + Detail: detail, + Children: children, } - if t := info.TypeOf(f.Type); types.Identical(nt, t) { - spanNode, selectionNode = f, f.Type - break Embeddeds + if rng, err := m.NodeRange(field); err == nil { + child.Range = rng + } + if rng, err := m.NodeRange(name); err == nil { + child.SelectionRange = rng } - } - child.Range, err = nodeToProtocolRange(snapshot, pkg, spanNode) - if err != nil { - return protocol.DocumentSymbol{}, err - } - child.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, selectionNode) - if err != nil { - return protocol.DocumentSymbol{}, err - } - s.Children = append(s.Children, child) - } - } - return s, nil -} -func nodesForStructField(i int, st *ast.StructType) (span, selection ast.Node) { - j := 0 - for _, field := range st.Fields.List { - if len(field.Names) == 0 { - if i == j { - return field, field.Type - } - j++ - continue - } - for _, name := range field.Names { - if i == j { - return field, name + symbols = append(symbols, child) } - j++ } + } - return nil, nil + return symbols } -func varSymbol(snapshot Snapshot, pkg Package, decl ast.Node, name *ast.Ident, obj types.Object, q types.Qualifier) (protocol.DocumentSymbol, error) { +func varSymbol(m *lsppos.TokenMapper, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) { s := protocol.DocumentSymbol{ - Name: obj.Name(), + Name: name.Name, Kind: protocol.Variable, } - if _, ok := obj.(*types.Const); ok { + if isConst { s.Kind = protocol.Constant } var err error - s.Range, err = nodeToProtocolRange(snapshot, pkg, decl) + s.Range, err = m.NodeRange(spec) if err != nil { return protocol.DocumentSymbol{}, err } - s.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, name) + s.SelectionRange, err = m.NodeRange(name) if err != nil { return protocol.DocumentSymbol{}, err } - s.Detail = types.TypeString(obj.Type(), q) + if spec.Type != nil { // type may be missing from the syntax + _, s.Detail, s.Children = typeDetails(m, spec.Type) + } return s, nil } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index da9c3b5d436..d1c90b65d5e 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -18,9 +18,10 @@ import ( "strings" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/typeparams" ) // MappedRange provides mapped protocol.Range for a span.Range, accounting for @@ -122,16 +123,6 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { return false } -func nodeToProtocolRange(snapshot Snapshot, pkg Package, n ast.Node) (protocol.Range, error) { - mrng, err := posToMappedRange(snapshot, pkg, n.Pos(), n.End()) - if err != nil { - return protocol.Range{}, err - } - return mrng.Range() -} - -// objToMappedRange returns the MappedRange for the object's declaring -// identifier (or string literal, for an import). func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedRange, error) { nameLen := len(obj.Name()) if pkgName, ok := obj.(*types.PkgName); ok { @@ -589,3 +580,51 @@ func ByteOffsetsToRange(m *protocol.ColumnMapper, uri span.URI, start, end int) e := span.NewPoint(line, col, end) return m.Range(span.New(uri, s, e)) } + +// RecvIdent returns the type identifier of a method receiver. +// e.g. A for all of A, *A, A[T], *A[T], etc. +func RecvIdent(recv *ast.FieldList) *ast.Ident { + if recv == nil || len(recv.List) == 0 { + return nil + } + x := recv.List[0].Type + if star, ok := x.(*ast.StarExpr); ok { + x = star.X + } + switch ix := x.(type) { // check for instantiated receivers + case *ast.IndexExpr: + x = ix.X + case *typeparams.IndexListExpr: + x = ix.X + } + if ident, ok := x.(*ast.Ident); ok { + return ident + } + return nil +} + +// embeddedIdent returns the type name identifier for an embedding x, if x in a +// valid embedding. Otherwise, it returns nil. +// +// Spec: An embedded field must be specified as a type name T or as a pointer +// to a non-interface type name *T +func embeddedIdent(x ast.Expr) *ast.Ident { + if star, ok := x.(*ast.StarExpr); ok { + x = star.X + } + switch ix := x.(type) { // check for instantiated receivers + case *ast.IndexExpr: + x = ix.X + case *typeparams.IndexListExpr: + x = ix.X + } + switch x := x.(type) { + case *ast.Ident: + return x + case *ast.SelectorExpr: + if _, ok := x.X.(*ast.Ident); ok { + return x.Sel + } + } + return nil +} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index c8e10d15d60..107e900a731 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -23,7 +23,7 @@ InlayHintsCount = 4 ReferencesCount = 27 RenamesCount = 41 PrepareRenamesCount = 7 -SymbolsCount = 5 +SymbolsCount = 1 WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index c3ac0089f31..93d9c638193 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -23,7 +23,7 @@ InlayHintsCount = 5 ReferencesCount = 27 RenamesCount = 48 PrepareRenamesCount = 7 -SymbolsCount = 5 +SymbolsCount = 2 WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 diff --git a/gopls/internal/lsp/testdata/symbols/go1.18.go b/gopls/internal/lsp/testdata/symbols/go1.18.go new file mode 100644 index 00000000000..cdf99dc20ff --- /dev/null +++ b/gopls/internal/lsp/testdata/symbols/go1.18.go @@ -0,0 +1,16 @@ +//go:build go1.18 +// +build go1.18 + +package main + +type T[P any] struct { //@symbol("T", "T", "Struct", "struct{...}", "T", "") + F P //@symbol("F", "F", "Field", "P", "", "T") +} + +type Constraint interface { //@symbol("Constraint", "Constraint", "Interface", "interface{...}", "Constraint", "") + ~int | struct{ int } //@symbol("~int | struct{int}", "~int | struct{ int }", "Field", "", "", "Constraint") + + // TODO(rfindley): the selection range below is the entire interface field. + // Can we reduce it? + interface{ M() } //@symbol("interface{...}", "interface{ M() }", "Field", "", "iFaceField", "Constraint"), symbol("M", "M", "Method", "func()", "", "iFaceField") +} diff --git a/gopls/internal/lsp/testdata/symbols/go1.18.go.golden b/gopls/internal/lsp/testdata/symbols/go1.18.go.golden new file mode 100644 index 00000000000..5a0c1a94d7a --- /dev/null +++ b/gopls/internal/lsp/testdata/symbols/go1.18.go.golden @@ -0,0 +1,7 @@ +-- symbols -- +T Struct 6:6-6:7 + F Field 7:2-7:3 +Constraint Interface 10:6-10:16 + interface{...} Field 15:2-15:18 + ~int | struct{int} Field 11:2-11:22 + diff --git a/gopls/internal/lsp/testdata/symbols/main.go b/gopls/internal/lsp/testdata/symbols/main.go index 8111250f349..65e0869fd5f 100644 --- a/gopls/internal/lsp/testdata/symbols/main.go +++ b/gopls/internal/lsp/testdata/symbols/main.go @@ -4,61 +4,88 @@ import ( "io" ) +// Each symbol marker in this file defines the following information: +// symbol(name, selectionSpan, kind, detail, id, parentID) +// - name: DocumentSymbol.Name +// - selectionSpan: DocumentSymbol.SelectionRange +// - kind: DocumentSymbol.Kind +// - detail: DocumentSymbol.Detail +// - id: if non-empty, a unique identifier for this symbol +// - parentID: if non-empty, the id of the parent of this symbol +// +// This data in aggregate defines a set of document symbols and their +// parent-child relationships, which is compared against the DocummentSymbols +// response from gopls for the current file. +// +// TODO(rfindley): the symbol annotations here are complicated and difficult to +// maintain. It would be simpler to just write out the full expected response +// in the golden file, perhaps as raw JSON. + var _ = 1 -var x = 42 //@mark(symbolsx, "x"), symbol("x", "x", "Variable", "", "main.x") +var x = 42 //@symbol("x", "x", "Variable", "", "", "") + +var nested struct { //@symbol("nested", "nested", "Variable", "struct{...}", "nested", "") + nestedField struct { //@symbol("nestedField", "nestedField", "Field", "struct{...}", "nestedField", "nested") + f int //@symbol("f", "f", "Field", "int", "", "nestedField") + } +} -const y = 43 //@symbol("y", "y", "Constant", "", "main.y") +const y = 43 //@symbol("y", "y", "Constant", "", "", "") -type Number int //@symbol("Number", "Number", "Number", "", "main.Number") +type Number int //@symbol("Number", "Number", "Class", "int", "", "") -type Alias = string //@symbol("Alias", "Alias", "String", "", "main.Alias") +type Alias = string //@symbol("Alias", "Alias", "Class", "string", "", "") -type NumberAlias = Number //@symbol("NumberAlias", "NumberAlias", "Number", "", "main.NumberAlias") +type NumberAlias = Number //@symbol("NumberAlias", "NumberAlias", "Class", "Number", "", "") type ( - Boolean bool //@symbol("Boolean", "Boolean", "Boolean", "", "main.Boolean") - BoolAlias = bool //@symbol("BoolAlias", "BoolAlias", "Boolean", "", "main.BoolAlias") + Boolean bool //@symbol("Boolean", "Boolean", "Class", "bool", "", "") + BoolAlias = bool //@symbol("BoolAlias", "BoolAlias", "Class", "bool", "", "") ) -type Foo struct { //@mark(symbolsFoo, "Foo"), symbol("Foo", "Foo", "Struct", "", "main.Foo") - Quux //@mark(fQuux, "Quux"), symbol("Quux", "Quux", "Field", "Foo", "main.Foo.Quux") - W io.Writer //@symbol("W" , "W", "Field", "Foo", "main.Foo.W") - Bar int //@mark(fBar, "Bar"), symbol("Bar", "Bar", "Field", "Foo", "main.Foo.Bar") - baz string //@symbol("baz", "baz", "Field", "Foo", "main.Foo.baz") +type Foo struct { //@symbol("Foo", "Foo", "Struct", "struct{...}", "Foo", "") + Quux //@symbol("Quux", "Quux", "Field", "Quux", "", "Foo") + W io.Writer //@symbol("W", "W", "Field", "io.Writer", "", "Foo") + Bar int //@symbol("Bar", "Bar", "Field", "int", "", "Foo") + baz string //@symbol("baz", "baz", "Field", "string", "", "Foo") + funcField func(int) int //@symbol("funcField", "funcField", "Field", "func(int) int", "", "Foo") } -type Quux struct { //@symbol("Quux", "Quux", "Struct", "", "main.Quux") - X, Y float64 //@mark(qX, "X"), symbol("X", "X", "Field", "Quux", "main.X"), symbol("Y", "Y", "Field", "Quux", "main.Y") +type Quux struct { //@symbol("Quux", "Quux", "Struct", "struct{...}", "Quux", "") + X, Y float64 //@symbol("X", "X", "Field", "float64", "", "Quux"), symbol("Y", "Y", "Field", "float64", "", "Quux") } -func (f Foo) Baz() string { //@symbol("(Foo).Baz", "Baz", "Method", "", "main.Foo.Baz") +type EmptyStruct struct{} //@symbol("EmptyStruct", "EmptyStruct", "Struct", "struct{}", "", "") + +func (f Foo) Baz() string { //@symbol("(Foo).Baz", "Baz", "Method", "func() string", "", "") return f.baz } func _() {} -func (q *Quux) Do() {} //@mark(qDo, "Do"), symbol("(*Quux).Do", "Do", "Method", "", "main.Quux.Do") - -func main() { //@symbol("main", "main", "Function", "", "main.main") +func (q *Quux) Do() {} //@symbol("(*Quux).Do", "Do", "Method", "func()", "", "") +func main() { //@symbol("main", "main", "Function", "func()", "", "") } -type Stringer interface { //@symbol("Stringer", "Stringer", "Interface", "", "main.Stringer") - String() string //@symbol("String", "String", "Method", "Stringer", "main.Stringer.String") +type Stringer interface { //@symbol("Stringer", "Stringer", "Interface", "interface{...}", "Stringer", "") + String() string //@symbol("String", "String", "Method", "func() string", "", "Stringer") } -type ABer interface { //@mark(ABerInterface, "ABer"), symbol("ABer", "ABer", "Interface", "", "main.ABer") - B() //@symbol("B", "B", "Method", "ABer", "main.ABer.B") - A() string //@mark(ABerA, "A"), symbol("A", "A", "Method", "ABer", "main.ABer.A") +type ABer interface { //@symbol("ABer", "ABer", "Interface", "interface{...}", "ABer", "") + B() //@symbol("B", "B", "Method", "func()", "", "ABer") + A() string //@symbol("A", "A", "Method", "func() string", "", "ABer") } -type WithEmbeddeds interface { //@symbol("WithEmbeddeds", "WithEmbeddeds", "Interface", "", "main.WithEmbeddeds") - Do() //@symbol("Do", "Do", "Method", "WithEmbeddeds", "main.WithEmbeddeds.Do") - ABer //@symbol("ABer", "ABer", "Interface", "WithEmbeddeds", "main.WithEmbeddeds.ABer") - io.Writer //@mark(ioWriter, "io.Writer"), symbol("io.Writer", "io.Writer", "Interface", "WithEmbeddeds", "main.WithEmbeddeds.Writer") +type WithEmbeddeds interface { //@symbol("WithEmbeddeds", "WithEmbeddeds", "Interface", "interface{...}", "WithEmbeddeds", "") + Do() //@symbol("Do", "Do", "Method", "func()", "", "WithEmbeddeds") + ABer //@symbol("ABer", "ABer", "Field", "ABer", "", "WithEmbeddeds") + io.Writer //@symbol("Writer", "Writer", "Field", "io.Writer", "", "WithEmbeddeds") } -func Dunk() int { return 0 } //@symbol("Dunk", "Dunk", "Function", "", "main.Dunk") +type EmptyInterface interface{} //@symbol("EmptyInterface", "EmptyInterface", "Interface", "interface{}", "", "") + +func Dunk() int { return 0 } //@symbol("Dunk", "Dunk", "Function", "func() int", "", "") -func dunk() {} //@symbol("dunk", "dunk", "Function", "", "main.dunk") +func dunk() {} //@symbol("dunk", "dunk", "Function", "func()", "", "") diff --git a/gopls/internal/lsp/testdata/symbols/main.go.golden b/gopls/internal/lsp/testdata/symbols/main.go.golden index ebb6a8a5dd1..98009b02d68 100644 --- a/gopls/internal/lsp/testdata/symbols/main.go.golden +++ b/gopls/internal/lsp/testdata/symbols/main.go.golden @@ -1,31 +1,36 @@ -- symbols -- -x Variable 9:5-9:6 -y Constant 11:7-11:8 -Number Number 13:6-13:12 -Alias String 15:6-15:11 -NumberAlias Number 17:6-17:17 -Boolean Boolean 20:2-20:9 -BoolAlias Boolean 21:2-21:11 -Foo Struct 24:6-24:9 - Bar Field 27:2-27:5 - Quux Field 25:2-25:6 - W Field 26:2-26:3 - baz Field 28:2-28:5 -Quux Struct 31:6-31:10 - X Field 32:2-32:3 - Y Field 32:5-32:6 -(Foo).Baz Method 35:14-35:17 -(*Quux).Do Method 41:16-41:18 -main Function 43:6-43:10 -Stringer Interface 47:6-47:14 - String Method 48:2-48:8 -ABer Interface 51:6-51:10 - A Method 53:2-53:3 - B Method 52:2-52:3 -WithEmbeddeds Interface 56:6-56:19 - ABer Interface 58:2-58:6 - Do Method 57:2-57:4 - io.Writer Interface 59:2-59:11 -Dunk Function 62:6-62:10 -dunk Function 64:6-64:10 +x Variable 26:5-26:6 +nested Variable 28:5-28:11 + nestedField Field 29:2-29:13 +y Constant 34:7-34:8 +Number Class 36:6-36:12 +Alias Class 38:6-38:11 +NumberAlias Class 40:6-40:17 +Boolean Class 43:2-43:9 +BoolAlias Class 44:2-44:11 +Foo Struct 47:6-47:9 + Bar Field 50:2-50:5 + Quux Field 48:2-48:6 + W Field 49:2-49:3 + baz Field 51:2-51:5 + funcField Field 52:2-52:11 +Quux Struct 55:6-55:10 + X Field 56:2-56:3 + Y Field 56:5-56:6 +EmptyStruct Struct 59:6-59:17 +(Foo).Baz Method 61:14-61:17 +(*Quux).Do Method 67:16-67:18 +main Function 69:6-69:10 +Stringer Interface 72:6-72:14 + String Method 73:2-73:8 +ABer Interface 76:6-76:10 + A Method 78:2-78:3 + B Method 77:2-77:3 +WithEmbeddeds Interface 81:6-81:19 + ABer Field 83:2-83:6 + Do Method 82:2-82:4 + Writer Field 84:5-84:11 +EmptyInterface Interface 87:6-87:20 +Dunk Function 89:6-89:10 +dunk Function 91:6-91:10 diff --git a/gopls/internal/lsp/testdata/workspacesymbol/a/a.go b/gopls/internal/lsp/testdata/workspacesymbol/a/a.go index 6e5a68b16fe..4ae9997a03e 100644 --- a/gopls/internal/lsp/testdata/workspacesymbol/a/a.go +++ b/gopls/internal/lsp/testdata/workspacesymbol/a/a.go @@ -1,9 +1,9 @@ package a -var RandomGopherVariableA = "a" //@symbol("RandomGopherVariableA", "RandomGopherVariableA", "Variable", "", "a.RandomGopherVariableA") +var RandomGopherVariableA = "a" -const RandomGopherConstantA = "a" //@symbol("RandomGopherConstantA", "RandomGopherConstantA", "Constant", "", "a.RandomGopherConstantA") +const RandomGopherConstantA = "a" const ( - randomgopherinvariable = iota //@symbol("randomgopherinvariable", "randomgopherinvariable", "Constant", "", "a.randomgopherinvariable") + randomgopherinvariable = iota ) diff --git a/gopls/internal/lsp/testdata/workspacesymbol/a/a.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/a/a.go.golden deleted file mode 100644 index c3f088577ba..00000000000 --- a/gopls/internal/lsp/testdata/workspacesymbol/a/a.go.golden +++ /dev/null @@ -1,5 +0,0 @@ --- symbols -- -RandomGopherVariableA Variable 3:5-3:26 -RandomGopherConstantA Constant 5:7-5:28 -randomgopherinvariable Constant 8:2-8:24 - diff --git a/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go b/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go index 30d5340970a..0d97c50d623 100644 --- a/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go +++ b/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go @@ -1,3 +1,3 @@ package a -var RandomGopherTestVariableA = "a" //@symbol("RandomGopherTestVariableA", "RandomGopherTestVariableA", "Variable", "", "a.RandomGopherTestVariableA") +var RandomGopherTestVariableA = "a" diff --git a/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden deleted file mode 100644 index af74619439a..00000000000 --- a/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden +++ /dev/null @@ -1,3 +0,0 @@ --- symbols -- -RandomGopherTestVariableA Variable 3:5-3:30 - diff --git a/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go b/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go index 76eb8487d8e..747cd17eccd 100644 --- a/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go +++ b/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go @@ -1,3 +1,3 @@ package a_test -var RandomGopherXTestVariableA = "a" //@symbol("RandomGopherXTestVariableA", "RandomGopherXTestVariableA", "Variable", "", "a_test.RandomGopherXTestVariableA") +var RandomGopherXTestVariableA = "a" diff --git a/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden deleted file mode 100644 index dfd02a5c449..00000000000 --- a/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden +++ /dev/null @@ -1,3 +0,0 @@ --- symbols -- -RandomGopherXTestVariableA Variable 3:5-3:31 - diff --git a/gopls/internal/lsp/testdata/workspacesymbol/b/b.go b/gopls/internal/lsp/testdata/workspacesymbol/b/b.go index 89ce0d92e06..b2e2092eed6 100644 --- a/gopls/internal/lsp/testdata/workspacesymbol/b/b.go +++ b/gopls/internal/lsp/testdata/workspacesymbol/b/b.go @@ -1,7 +1,7 @@ package b -var RandomGopherVariableB = "b" //@symbol("RandomGopherVariableB", "RandomGopherVariableB", "Variable", "", "b.RandomGopherVariableB") +var RandomGopherVariableB = "b" -type RandomGopherStructB struct { //@symbol("RandomGopherStructB", "RandomGopherStructB", "Struct", "", "b.RandomGopherStructB") - Bar int //@mark(bBar, "Bar"), symbol("Bar", "Bar", "Field", "RandomGopherStructB", "b.RandomGopherStructB.Bar") +type RandomGopherStructB struct { + Bar int } diff --git a/gopls/internal/lsp/testdata/workspacesymbol/b/b.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/b/b.go.golden deleted file mode 100644 index 4711c9d91ad..00000000000 --- a/gopls/internal/lsp/testdata/workspacesymbol/b/b.go.golden +++ /dev/null @@ -1,5 +0,0 @@ --- symbols -- -RandomGopherVariableB Variable 3:5-3:26 -RandomGopherStructB Struct 5:6-5:25 - Bar Field 6:2-6:5 - diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index cfce843a974..f953a8faf0e 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -86,8 +86,7 @@ type Highlights = map[span.Span][]span.Span type References = map[span.Span][]span.Span type Renames = map[span.Span]string type PrepareRenames = map[span.Span]*source.PrepareItem -type Symbols = map[span.URI][]protocol.DocumentSymbol -type SymbolsChildren = map[string][]protocol.DocumentSymbol +type Symbols = map[span.URI][]*symbol type SymbolInformation = map[span.Span]protocol.SymbolInformation type InlayHints = []span.Span type WorkspaceSymbols = map[WorkspaceSymbolsTestType]map[span.URI][]string @@ -125,8 +124,6 @@ type Data struct { InlayHints InlayHints PrepareRenames PrepareRenames Symbols Symbols - symbolsChildren SymbolsChildren - symbolInformation SymbolInformation WorkspaceSymbols WorkspaceSymbols Signatures Signatures Links Links @@ -250,6 +247,12 @@ type SuggestedFix struct { ActionKind, Title string } +// A symbol holds a DocumentSymbol along with its parent-child edge. +type symbol struct { + pSymbol protocol.DocumentSymbol + id, parentID string +} + type Golden struct { Filename string Archive *txtar.Archive @@ -328,8 +331,6 @@ func load(t testing.TB, mode string, dir string) *Data { FunctionExtractions: make(FunctionExtractions), MethodExtractions: make(MethodExtractions), Symbols: make(Symbols), - symbolsChildren: make(SymbolsChildren), - symbolInformation: make(SymbolInformation), WorkspaceSymbols: make(WorkspaceSymbols), Signatures: make(Signatures), Links: make(Links), @@ -504,12 +505,7 @@ func load(t testing.TB, mode string, dir string) *Data { }); err != nil { t.Fatal(err) } - for _, symbols := range datum.Symbols { - for i := range symbols { - children := datum.symbolsChildren[symbols[i].Name] - symbols[i].Children = children - } - } + // Collect names for the entries that require golden files. if err := datum.Exported.Expect(map[string]interface{}{ "godef": datum.collectDefinitionNames, @@ -847,10 +843,44 @@ func Run(t *testing.T, tests Tests, data *Data) { t.Run("Symbols", func(t *testing.T) { t.Helper() - for uri, expectedSymbols := range data.Symbols { + for uri, allSymbols := range data.Symbols { + byParent := make(map[string][]*symbol) + for _, sym := range allSymbols { + if sym.parentID != "" { + byParent[sym.parentID] = append(byParent[sym.parentID], sym) + } + } + + // collectChildren does a depth-first traversal of the symbol tree, + // computing children of child nodes before returning to their parent. + // This is necessary as the Children field is slice of non-pointer types, + // and therefore we need to be careful to mutate children first before + // assigning them to their parent. + var collectChildren func(id string) []protocol.DocumentSymbol + collectChildren = func(id string) []protocol.DocumentSymbol { + children := byParent[id] + // delete from byParent before recursing, to ensure that + // collectChildren terminates even in the presence of cycles. + delete(byParent, id) + var result []protocol.DocumentSymbol + for _, child := range children { + child.pSymbol.Children = collectChildren(child.id) + result = append(result, child.pSymbol) + } + return result + } + + var topLevel []protocol.DocumentSymbol + for _, sym := range allSymbols { + if sym.parentID == "" { + sym.pSymbol.Children = collectChildren(sym.id) + topLevel = append(topLevel, sym.pSymbol) + } + } + t.Run(uriName(uri), func(t *testing.T) { t.Helper() - tests.Symbols(t, uri, expectedSymbols) + tests.Symbols(t, uri, topLevel) }) } }) @@ -1356,36 +1386,32 @@ func (data *Data) collectPrepareRenames(src span.Span, rng span.Range, placehold } // collectSymbols is responsible for collecting @symbol annotations. -func (data *Data) collectSymbols(name string, spn span.Span, kind string, parentName string, siName string) { +func (data *Data) collectSymbols(name string, selectionRng span.Span, kind, detail, id, parentID string) { + // We don't set 'Range' here as it is difficult (impossible?) to express + // multi-line ranges in the packagestest framework. + uri := selectionRng.URI() + data.Symbols[uri] = append(data.Symbols[uri], &symbol{ + pSymbol: protocol.DocumentSymbol{ + Name: name, + Kind: protocol.ParseSymbolKind(kind), + SelectionRange: data.mustRange(selectionRng), + Detail: detail, + }, + id: id, + parentID: parentID, + }) +} + +// mustRange converts spn into a protocol.Range, calling t.Fatal on any error. +func (data *Data) mustRange(spn span.Span) protocol.Range { m, err := data.Mapper(spn.URI()) - if err != nil { - data.t.Fatal(err) - } rng, err := m.Range(spn) if err != nil { + // TODO(rfindley): this can probably just be a panic, at which point we + // don't need to close over t. data.t.Fatal(err) } - sym := protocol.DocumentSymbol{ - Name: name, - Kind: protocol.ParseSymbolKind(kind), - SelectionRange: rng, - } - if parentName == "" { - data.Symbols[spn.URI()] = append(data.Symbols[spn.URI()], sym) - } else { - data.symbolsChildren[parentName] = append(data.symbolsChildren[parentName], sym) - } - - // Reuse @symbol in the workspace symbols tests. - si := protocol.SymbolInformation{ - Name: siName, - Kind: sym.Kind, - Location: protocol.Location{ - URI: protocol.URIFromSpanURI(spn.URI()), - Range: sym.SelectionRange, - }, - } - data.symbolInformation[spn] = si + return rng } func (data *Data) collectWorkspaceSymbols(typ WorkspaceSymbolsTestType) func(*expect.Note, string) { diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index d697f184237..8a08b044b53 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -14,13 +14,12 @@ import ( "sort" "strconv" "strings" - "testing" - "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/diff/myers" "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/internal/diff" + "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/span" ) @@ -66,50 +65,6 @@ func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []proto return "" } -// DiffSymbols prints the diff between expected and actual symbols test results. -func DiffSymbols(t *testing.T, uri span.URI, want, got []protocol.DocumentSymbol) string { - sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name }) - sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name }) - if len(got) != len(want) { - return summarizeSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want)) - } - for i, w := range want { - g := got[i] - if w.Name != g.Name { - return summarizeSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name) - } - if w.Kind != g.Kind { - return summarizeSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind) - } - if protocol.CompareRange(w.SelectionRange, g.SelectionRange) != 0 { - return summarizeSymbols(i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange) - } - if msg := DiffSymbols(t, uri, w.Children, g.Children); msg != "" { - return fmt.Sprintf("children of %s: %s", w.Name, msg) - } - } - return "" -} - -func summarizeSymbols(i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string { - msg := &bytes.Buffer{} - fmt.Fprint(msg, "document symbols failed") - if i >= 0 { - fmt.Fprintf(msg, " at %d", i) - } - fmt.Fprint(msg, " because of ") - fmt.Fprintf(msg, reason, args...) - fmt.Fprint(msg, ":\nexpected:\n") - for _, s := range want { - fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange) - } - fmt.Fprintf(msg, "got:\n") - for _, s := range got { - fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange) - } - return msg.String() -} - // DiffDiagnostics prints the diff between expected and actual diagnostics test // results. If the sole expectation is "no_diagnostics", the check is suppressed. // The Message field of each want element must be a regular expression. From c7ac94217faf546b95d699fb3749371b61213690 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 20 Sep 2022 18:46:19 -0400 Subject: [PATCH 268/723] gopls/internal/lsp: simplify prepareRename tests - simplify collection to use an expected span.Span and use mustRange - remove the source_test.go version of the test, as it is redundant For golang/go#54845 Change-Id: I3a7da8547e27dc157fb513486a151031ec135746 Reviewed-on: https://go-review.googlesource.com/c/tools/+/432138 gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/lsp_test.go | 9 ++++-- gopls/internal/lsp/rename.go | 6 ++++ gopls/internal/lsp/source/source_test.go | 38 +----------------------- gopls/internal/lsp/tests/tests.go | 17 ++--------- 4 files changed, 16 insertions(+), 54 deletions(-) diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 02b57273368..bdf8a6b4990 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -1074,7 +1074,12 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare t.Errorf("prepare rename failed for %v: got error: %v", src, err) return } - // we all love typed nils + + // TODO(rfindley): can we consolidate on a single representation for + // PrepareRename results, and use cmp.Diff here? + + // PrepareRename may fail with no error if there was no object found at the + // position. if got == nil { if want.Text != "" { // expected an ident. t.Errorf("prepare rename failed for %v: got nil", src) @@ -1088,7 +1093,7 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare t.Errorf("prepare rename failed: incorrect point, got %v want %v", got.Range.Start, want.Range.Start) } } else { - if protocol.CompareRange(got.Range, want.Range) != 0 { + if got.Range != want.Range { t.Errorf("prepare rename failed: incorrect range got %v want %v", got.Range, want.Range) } } diff --git a/gopls/internal/lsp/rename.go b/gopls/internal/lsp/rename.go index 5127b5df1f4..aff2f304ebd 100644 --- a/gopls/internal/lsp/rename.go +++ b/gopls/internal/lsp/rename.go @@ -35,6 +35,12 @@ func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*pr }, nil } +// prepareRename implements the textDocument/prepareRename handler. It may +// return (nil, nil) if there is no rename at the cursor position, but it is +// not desirable to display an error to the user. +// +// TODO(rfindley): why wouldn't we want to show an error to the user, if the +// user initiated a rename request at the cursor? func (s *Server) prepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.PrepareRename2Gn, error) { snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) defer release() diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index ad2ca6b72af..155832a4825 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -839,43 +839,7 @@ func applyEdits(contents string, edits []diff.TextEdit) string { } func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) { - _, srcRng, err := spanToRange(r.data, src) - if err != nil { - t.Fatal(err) - } - // Find the identifier at the position. - fh, err := r.snapshot.GetFile(r.ctx, src.URI()) - if err != nil { - t.Fatal(err) - } - item, _, err := source.PrepareRename(r.ctx, r.snapshot, fh, srcRng.Start) - if err != nil { - if want.Text != "" { // expected an ident. - t.Errorf("prepare rename failed for %v: got error: %v", src, err) - } - return - } - if item == nil { - if want.Text != "" { - t.Errorf("prepare rename failed for %v: got nil", src) - } - return - } - if want.Text == "" { - t.Errorf("prepare rename failed for %v: expected nil, got %v", src, item) - return - } - if item.Range.Start == item.Range.End { - // Special case for 0-length ranges. Marks can't specify a 0-length range, - // so just compare the start. - if item.Range.Start != want.Range.Start { - t.Errorf("prepare rename failed: incorrect point, got %v want %v", item.Range.Start, want.Range.Start) - } - } else { - if protocol.CompareRange(item.Range, want.Range) != 0 { - t.Errorf("prepare rename failed: incorrect range got %v want %v", item.Range, want.Range) - } - } + // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go } func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index f953a8faf0e..39849ca2956 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -1365,22 +1365,9 @@ func (data *Data) collectRenames(src span.Span, newText string) { data.Renames[src] = newText } -func (data *Data) collectPrepareRenames(src span.Span, rng span.Range, placeholder string) { - m, err := data.Mapper(src.URI()) - if err != nil { - data.t.Fatal(err) - } - // Convert range to span and then to protocol.Range. - spn, err := rng.Span() - if err != nil { - data.t.Fatal(err) - } - prng, err := m.Range(spn) - if err != nil { - data.t.Fatal(err) - } +func (data *Data) collectPrepareRenames(src, spn span.Span, placeholder string) { data.PrepareRenames[src] = &source.PrepareItem{ - Range: prng, + Range: data.mustRange(spn), Text: placeholder, } } From 88b5529c9e152818a95a94bba07f34ce60532bb0 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 21 Sep 2022 09:17:10 -0400 Subject: [PATCH 269/723] gopls/internal/lsp/tests: use the mustRange helper in more places In order to narrow usage of tests.Data.t, use the mustRange helper in more places. For golang/go#54845 Change-Id: I446ca520fa76afb2bc10c1fd5a5765859176dd6a Reviewed-on: https://go-review.googlesource.com/c/tools/+/432336 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/tests/tests.go | 43 +++---------------------------- 1 file changed, 4 insertions(+), 39 deletions(-) diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 39849ca2956..cae4af25ad0 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -1123,19 +1123,8 @@ func (data *Data) Golden(t *testing.T, tag, target string, update func() ([]byte } func (data *Data) collectCodeLens(spn span.Span, title, cmd string) { - if _, ok := data.CodeLens[spn.URI()]; !ok { - data.CodeLens[spn.URI()] = []protocol.CodeLens{} - } - m, err := data.Mapper(spn.URI()) - if err != nil { - data.t.Fatalf("Mapper: %v", err) - } - rng, err := m.Range(spn) - if err != nil { - data.t.Fatalf("Range: %v", err) - } data.CodeLens[spn.URI()] = append(data.CodeLens[spn.URI()], protocol.CodeLens{ - Range: rng, + Range: data.mustRange(spn), Command: protocol.Command{ Title: title, Command: cmd, @@ -1144,15 +1133,6 @@ func (data *Data) collectCodeLens(spn span.Span, title, cmd string) { } func (data *Data) collectDiagnostics(spn span.Span, msgSource, msgPattern, msgSeverity string) { - m, err := data.Mapper(spn.URI()) - if err != nil { - data.t.Fatalf("Mapper: %v", err) - } - rng, err := m.Range(spn) - if err != nil { - data.t.Fatalf("Range: %v", err) - } - severity := protocol.SeverityError switch msgSeverity { case "error": @@ -1166,7 +1146,7 @@ func (data *Data) collectDiagnostics(spn span.Span, msgSource, msgPattern, msgSe } data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], &source.Diagnostic{ - Range: rng, + Range: data.mustRange(spn), Severity: severity, Source: source.DiagnosticSource(msgSource), Message: msgPattern, @@ -1275,14 +1255,7 @@ func (data *Data) collectImplementations(src span.Span, targets []span.Span) { func (data *Data) collectIncomingCalls(src span.Span, calls []span.Span) { for _, call := range calls { - m, err := data.Mapper(call.URI()) - if err != nil { - data.t.Fatal(err) - } - rng, err := m.Range(call) - if err != nil { - data.t.Fatal(err) - } + rng := data.mustRange(call) // we're only comparing protocol.range if data.CallHierarchy[src] != nil { data.CallHierarchy[src].IncomingCalls = append(data.CallHierarchy[src].IncomingCalls, @@ -1305,19 +1278,11 @@ func (data *Data) collectOutgoingCalls(src span.Span, calls []span.Span) { data.CallHierarchy[src] = &CallHierarchyResult{} } for _, call := range calls { - m, err := data.Mapper(call.URI()) - if err != nil { - data.t.Fatal(err) - } - rng, err := m.Range(call) - if err != nil { - data.t.Fatal(err) - } // we're only comparing protocol.range data.CallHierarchy[src].OutgoingCalls = append(data.CallHierarchy[src].OutgoingCalls, protocol.CallHierarchyItem{ URI: protocol.DocumentURI(call.URI()), - Range: rng, + Range: data.mustRange(call), }) } } From b243e57ea8189f67f74aa2fdadabbf72db1c16fa Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 21 Sep 2022 09:28:11 -0400 Subject: [PATCH 270/723] gopls/internal/lsp/tests: simplify collectCompletionItems, remove Data.t The marker function collectCompletionItems required at least three arguments. Express this in its signature, leaving a final variadic argument for documentation. I considered just making this final argument mandatory, but opted for minimizing the diff given that there are 400+ existing @item annotations. With this change the only use of tests.Data.t is in mustRange. Since conversion to range should always succeed, I switched this usage to a panic and removed the t field. For golang/go#54845 Change-Id: I407f07cb85fa1356ceb6dba366007f69d1b6a068 Reviewed-on: https://go-review.googlesource.com/c/tools/+/432337 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/tests/tests.go | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index cae4af25ad0..689fcca94f0 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -130,7 +130,6 @@ type Data struct { AddImport AddImport Hovers Hovers - t testing.TB fragments map[string]string dir string golden map[string]*Golden @@ -337,7 +336,6 @@ func load(t testing.TB, mode string, dir string) *Data { AddImport: make(AddImport), Hovers: make(Hovers), - t: t, dir: dir, fragments: map[string]string{}, golden: map[string]*Golden{}, @@ -1187,15 +1185,9 @@ func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []t } } -func (data *Data) collectCompletionItems(pos token.Pos, args []string) { - if len(args) < 3 { - loc := data.Exported.ExpectFileSet.Position(pos) - data.t.Fatalf("%s:%d: @item expects at least 3 args, got %d", - loc.Filename, loc.Line, len(args)) - } - label, detail, kind := args[0], args[1], args[2] +func (data *Data) collectCompletionItems(pos token.Pos, label, detail, kind string, args []string) { var documentation string - if len(args) == 4 { + if len(args) > 3 { documentation = args[3] } data.CompletionItems[pos] = &completion.CompletionItem{ @@ -1354,14 +1346,12 @@ func (data *Data) collectSymbols(name string, selectionRng span.Span, kind, deta }) } -// mustRange converts spn into a protocol.Range, calling t.Fatal on any error. +// mustRange converts spn into a protocol.Range, panicking on any error. func (data *Data) mustRange(spn span.Span) protocol.Range { m, err := data.Mapper(spn.URI()) rng, err := m.Range(spn) if err != nil { - // TODO(rfindley): this can probably just be a panic, at which point we - // don't need to close over t. - data.t.Fatal(err) + panic(fmt.Sprintf("converting span %s to range: %v", spn, err)) } return rng } From eb25de6e2a94ebbd4401db4c7695bbb8d0501159 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 23 Sep 2022 10:55:58 -0400 Subject: [PATCH 271/723] go/analysis/passes/loopclosure: only check statements after t.Parallel After experimenting with the new t.Run+t.Parallel check in the loopclosure analyzer, we discovered that users rely on the fact that statements before the call to t.Parallel are executed synchronously, for example by declaring test := test inside the function literal, but before the call to t.Parallel. To avoid such false positives, only consider statements occurring after the first call to t.Parallel. Change-Id: I88466ea3bfd318d42d734c320677fbe5e3f6cb00 Reviewed-on: https://go-review.googlesource.com/c/tools/+/433535 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- go/analysis/passes/loopclosure/loopclosure.go | 98 +++++++++++-------- .../testdata/src/subtests/subtest.go | 26 ++++- 2 files changed, 81 insertions(+), 43 deletions(-) diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 645e5895bb6..35fe15c9a20 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -104,55 +104,64 @@ func run(pass *analysis.Pass) (interface{}, error) { // fighting against the test runner. lastStmt := len(body.List) - 1 for i, s := range body.List { - var fun ast.Expr // if non-nil, a function that escapes the loop iteration + var stmts []ast.Stmt // statements that must be checked for escaping references switch s := s.(type) { case *ast.GoStmt: if i == lastStmt { - fun = s.Call.Fun + stmts = litStmts(s.Call.Fun) } case *ast.DeferStmt: if i == lastStmt { - fun = s.Call.Fun + stmts = litStmts(s.Call.Fun) } case *ast.ExprStmt: // check for errgroup.Group.Go and testing.T.Run (with T.Parallel) if call, ok := s.X.(*ast.CallExpr); ok { if i == lastStmt { - fun = goInvoke(pass.TypesInfo, call) + stmts = litStmts(goInvoke(pass.TypesInfo, call)) } - if fun == nil && analysisinternal.LoopclosureParallelSubtests { - fun = parallelSubtest(pass.TypesInfo, call) + if stmts == nil && analysisinternal.LoopclosureParallelSubtests { + stmts = parallelSubtest(pass.TypesInfo, call) } } } - lit, ok := fun.(*ast.FuncLit) - if !ok { - continue - } - - ast.Inspect(lit.Body, func(n ast.Node) bool { - id, ok := n.(*ast.Ident) - if !ok { - return true - } - obj := pass.TypesInfo.Uses[id] - if obj == nil { - return true - } - for _, v := range vars { - if v == obj { - pass.ReportRangef(id, "loop variable %s captured by func literal", id.Name) + for _, stmt := range stmts { + ast.Inspect(stmt, func(n ast.Node) bool { + id, ok := n.(*ast.Ident) + if !ok { + return true } - } - return true - }) + obj := pass.TypesInfo.Uses[id] + if obj == nil { + return true + } + for _, v := range vars { + if v == obj { + pass.ReportRangef(id, "loop variable %s captured by func literal", id.Name) + } + } + return true + }) + } } }) return nil, nil } +// litStmts returns all statements from the function body of a function +// literal. +// +// If fun is not a function literal, it returns nil. +func litStmts(fun ast.Expr) []ast.Stmt { + lit, _ := fun.(*ast.FuncLit) + if lit == nil { + return nil + } + return lit.Body.List +} + // goInvoke returns a function expression that would be called asynchronously // (but not awaited) in another goroutine as a consequence of the call. // For example, given the g.Go call below, it returns the function literal expression. @@ -169,38 +178,45 @@ func goInvoke(info *types.Info, call *ast.CallExpr) ast.Expr { return call.Args[0] } -// parallelSubtest returns a function expression that would be called +// parallelSubtest returns statements that would would be executed // asynchronously via the go test runner, as t.Run has been invoked with a // function literal that calls t.Parallel. // -// import "testing" +// In practice, users rely on the fact that statements before the call to +// t.Parallel are synchronous. For example by declaring test := test inside the +// function literal, but before the call to t.Parallel. +// +// Therefore, we only flag references that occur after the call to t.Parallel: // -// func TestFoo(t *testing.T) { -// tests := []int{0, 1, 2} -// for i, t := range tests { -// t.Run("subtest", func(t *testing.T) { -// t.Parallel() -// println(i, t) -// }) -// } +// import "testing" +// +// func TestFoo(t *testing.T) { +// tests := []int{0, 1, 2} +// for i, test := range tests { +// t.Run("subtest", func(t *testing.T) { +// println(i, test) // OK +// t.Parallel() +// println(i, test) // Not OK +// }) // } -func parallelSubtest(info *types.Info, call *ast.CallExpr) ast.Expr { +// } +func parallelSubtest(info *types.Info, call *ast.CallExpr) []ast.Stmt { if !isMethodCall(info, call, "testing", "T", "Run") { return nil } - lit, ok := call.Args[1].(*ast.FuncLit) - if !ok { + lit, _ := call.Args[1].(*ast.FuncLit) + if lit == nil { return nil } - for _, stmt := range lit.Body.List { + for i, stmt := range lit.Body.List { exprStmt, ok := stmt.(*ast.ExprStmt) if !ok { continue } if isMethodCall(info, exprStmt.X, "testing", "T", "Parallel") { - return lit + return lit.Body.List[i+1:] } } diff --git a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go index 4bcd495367c..2a97244a1a5 100644 --- a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go +++ b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go @@ -18,7 +18,7 @@ type T struct{} // Run should not match testing.T.Run. Note that the second argument is // intentionally a *testing.T, not a *T, so that we can check both // testing.T.Parallel inside a T.Run, and a T.Parallel inside a testing.T.Run. -func (t *T) Run(string, func(*testing.T)) { // The second argument here is testing.T +func (t *T) Run(string, func(*testing.T)) { } func (t *T) Parallel() {} @@ -38,11 +38,33 @@ func _(t *testing.T) { println(test) }) - // Check that the location of t.Parallel does not matter. + // Check that the location of t.Parallel matters. t.Run("", func(t *testing.T) { + println(i) + println(test) + t.Parallel() println(i) // want "loop variable i captured by func literal" println(test) // want "loop variable test captured by func literal" + }) + + // Check that shadowing the loop variables within the test literal is OK if + // it occurs before t.Parallel(). + t.Run("", func(t *testing.T) { + i := i + test := test + t.Parallel() + println(i) + println(test) + }) + + // Check that shadowing the loop variables within the test literal is Not + // OK if it occurs after t.Parallel(). + t.Run("", func(t *testing.T) { t.Parallel() + i := i // want "loop variable i captured by func literal" + test := test // want "loop variable test captured by func literal" + println(i) // OK + println(test) // OK }) // Check uses in nested blocks. From 49a686d2a23a20b8f98e6b0432b4c475d05e87c4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 23 Sep 2022 11:30:48 -0400 Subject: [PATCH 272/723] go/analysis: update explanation of (no) Diagnostics.Severity The previous explanation from CL 230312 suggested that multiple Analyzers be used to distinguish severity levels, which is unwieldy to implement and sounds more like a workaround than a principle. I argue there is a principle here: that severity is best determined by the user, not the analyzer itself. This does require that drivers support some kind of filtering or ranking based on Analyzer.Name and Diagnostic.Category. Some already do; perhaps the unitchecker and multichecker drivers should too. Change-Id: I484877baf7963c444285f35c9f03ee3c5e6d7729 Reviewed-on: https://go-review.googlesource.com/c/tools/+/433536 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro Reviewed-by: Robert Findley --- go/analysis/doc.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/go/analysis/doc.go b/go/analysis/doc.go index 03c31525e36..2c49e335892 100644 --- a/go/analysis/doc.go +++ b/go/analysis/doc.go @@ -177,14 +177,14 @@ Diagnostic is defined as: The optional Category field is a short identifier that classifies the kind of message when an analysis produces several kinds of diagnostic. -Many analyses want to associate diagnostics with a severity level. -Because Diagnostic does not have a severity level field, an Analyzer's -diagnostics effectively all have the same severity level. To separate which -diagnostics are high severity and which are low severity, expose multiple -Analyzers instead. Analyzers should also be separated when their -diagnostics belong in different groups, or could be tagged differently -before being shown to the end user. Analyzers should document their severity -level to help downstream tools surface diagnostics properly. +The Diagnostic struct does not have a field to indicate its severity +because opinions about the relative importance of Analyzers and their +diagnostics vary widely among users. The design of this framework does +not hold each Analyzer responsible for identifying the severity of its +diagnostics. Instead, we expect that drivers will allow the user to +customize the filtering and prioritization of diagnostics based on the +producing Analyzer and optional Category, according to the user's +preferences. Most Analyzers inspect typed Go syntax trees, but a few, such as asmdecl and buildtag, inspect the raw text of Go source files or even non-Go From c5cd9430779e9d1fb77da7b0f1fe24f05f86370c Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 26 Sep 2022 12:16:58 -0400 Subject: [PATCH 273/723] gopls: fix the reset_golden.sh script and regenerate golden files Update the reset_golden.sh script to run the ./test and ./internal/lsp directories, following the deprecation of ./internal/lsp/cmd and ./internal/lsp/source tests. Also, fix it to generate golden data for tests that only run at 1.17 and earlier. Use the newly fixed script to sync the golden data. For golang/go#54845 Change-Id: I2b42dac91cf1c7e2e8e25fd2aa8ab23c91e05c6c Reviewed-on: https://go-review.googlesource.com/c/tools/+/434635 Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/reset_golden.sh | 24 +++++++-- .../internal/lsp/testdata/godef/a/a.go.golden | 50 +++++++++---------- .../internal/lsp/testdata/godef/a/d.go.golden | 24 ++++----- .../godef/hover_generics/hover.go.golden | 8 --- 4 files changed, 57 insertions(+), 49 deletions(-) diff --git a/gopls/internal/lsp/reset_golden.sh b/gopls/internal/lsp/reset_golden.sh index ef9dacf2dc1..ff7f4d08208 100755 --- a/gopls/internal/lsp/reset_golden.sh +++ b/gopls/internal/lsp/reset_golden.sh @@ -4,11 +4,27 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. # -# Regenerates the *.golden files in the ./internal/lsp/testdata directory. +# Updates the *.golden files ... to match the tests' current behavior. set -eu +GO117BIN="go1.17.9" + +command -v $GO117BIN >/dev/null 2>&1 || { + go install golang.org/dl/$GO117BIN@latest + $GO117BIN download +} + find ./internal/lsp/testdata -name *.golden ! -name summary*.txt.golden -delete -go test ./internal/lsp/source -golden -go test ./internal/lsp/ -golden -go test ./internal/lsp/cmd -golden +# Here we intentionally do not run the ./internal/lsp/source tests with +# -golden. Eventually these tests will be deleted, and in the meantime they are +# redundant with the ./internal/lsp tests. +# +# Note: go1.17.9 tests must be run *before* go tests, as by convention the +# golden output should match the output of gopls built with the most recent +# version of Go. If output differs at 1.17, tests must be tolerant of the 1.17 +# output. +$GO117BIN test ./internal/lsp -golden +go test ./internal/lsp -golden +$GO117BIN test ./test -golden +go test ./test -golden diff --git a/gopls/internal/lsp/testdata/godef/a/a.go.golden b/gopls/internal/lsp/testdata/godef/a/a.go.golden index 3363100ce0d..470396d068c 100644 --- a/gopls/internal/lsp/testdata/godef/a/a.go.golden +++ b/gopls/internal/lsp/testdata/godef/a/a.go.golden @@ -1,3 +1,28 @@ +-- H-hoverdef -- +```go +type H interface { + Goodbye() //@mark(AGoodbye, "Goodbye") +} +``` + +[`a.H` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#H) +-- I-hoverdef -- +```go +type I interface { + B() //@mark(AB, "B") + J +} +``` + +[`a.I` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#I) +-- J-hoverdef -- +```go +type J interface { + Hello() //@mark(AHello, "Hello") +} +``` + +[`a.J` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#J) -- Lock-hoverdef -- ```go func (*sync.Mutex).Lock() @@ -165,31 +190,6 @@ const h untyped int = 2 Constant block. --- H-hoverdef -- -```go -type H interface { - Goodbye() //@mark(AGoodbye, "Goodbye") -} -``` - -[`a.H` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#H) --- I-hoverdef -- -```go -type I interface { - B() //@mark(AB, "B") - J -} -``` - -[`a.I` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#I) --- J-hoverdef -- -```go -type J interface { - Hello() //@mark(AHello, "Hello") -} -``` - -[`a.J` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#J) -- make-hoverdef -- ```go func make(t Type, size ...int) Type diff --git a/gopls/internal/lsp/testdata/godef/a/d.go.golden b/gopls/internal/lsp/testdata/godef/a/d.go.golden index 840dbaf60ac..ee687750c3e 100644 --- a/gopls/internal/lsp/testdata/godef/a/d.go.golden +++ b/gopls/internal/lsp/testdata/godef/a/d.go.golden @@ -64,6 +64,18 @@ func (Thing).Method(i int) string ``` [`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method) +-- NextThing-hoverdef -- +```go +type NextThing struct { + Thing + Value int +} + +func (*NextThing).Method3() int +func (NextThing).another() string +``` + +[`a.NextThing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#NextThing) -- Other-definition -- godef/a/d.go:9:5-10: defined here as ```go var Other Thing @@ -177,15 +189,3 @@ func Things(val []string) []Thing -- a-hoverdef -- Package a is a package for testing go to definition. --- NextThing-hoverdef -- -```go -type NextThing struct { - Thing - Value int -} - -func (*NextThing).Method3() int -func (NextThing).another() string -``` - -[`a.NextThing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#NextThing) diff --git a/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden index 7f302ab6865..fb03865bf8b 100644 --- a/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden +++ b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden @@ -1,11 +1,3 @@ --- Pparam-hoverdef -- -```go -type parameter P interface{~int|string} -``` --- Pvar-hoverdef -- -```go -type parameter P interface{~int|string} -``` -- ValueQfield-hoverdef -- ```go field Q int From 5214f412aecffbca887fc0c26f8d59e3a2342d44 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 26 Sep 2022 15:01:21 -0400 Subject: [PATCH 274/723] internal/gocommand: show pid of process We're almost certain that the go process shown by ps is not the one that we're waiting for in runCmdContext, but only by printing the pid can we be sure. Updates golang/go#54461 Change-Id: I1ce9580de6ee6bc4557d76c2a6b05f5a3e2eb6c2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/434637 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- internal/gocommand/invoke.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/gocommand/invoke.go b/internal/gocommand/invoke.go index 2327f45b565..d50551693f3 100644 --- a/internal/gocommand/invoke.go +++ b/internal/gocommand/invoke.go @@ -258,7 +258,7 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) error { case err := <-resChan: return err case <-time.After(1 * time.Minute): - HandleHangingGoCommand() + HandleHangingGoCommand(cmd.Process) case <-ctx.Done(): } } else { @@ -291,13 +291,13 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) error { case err := <-resChan: return err case <-time.After(10 * time.Second): // a shorter wait as resChan should return quickly following Kill - HandleHangingGoCommand() + HandleHangingGoCommand(cmd.Process) } } return <-resChan } -func HandleHangingGoCommand() { +func HandleHangingGoCommand(proc *os.Process) { switch runtime.GOOS { case "linux", "darwin", "freebsd", "netbsd": fmt.Fprintln(os.Stderr, `DETECTED A HANGING GO COMMAND @@ -330,7 +330,7 @@ See golang/go#54461 for more details.`) panic(fmt.Sprintf("running %s: %v", listFiles, err)) } } - panic("detected hanging go command: see golang/go#54461 for more details") + panic(fmt.Sprintf("detected hanging go command (pid %d): see golang/go#54461 for more details", proc.Pid)) } func cmdDebugStr(cmd *exec.Cmd) string { From 1bfc4699d4d11e931ee8b9bea7534c1f77c42074 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 26 Sep 2022 14:21:24 -0400 Subject: [PATCH 275/723] gopls: update to handle 'undefined:' versus 'undeclared' in type errors Update gopls to handle the new form of "undeclared name: ..." error messages, "undefined: ...", and update tests to be tolerant of both. Also do some minor cleanup of test error messages related to mismatching diagnostics. With this change, TryBots should succeed at CL 432556. Updates golang/go#54845 Change-Id: I9214d00c59110cd34470845b72d3e2f5e73291c1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/434636 Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- .../undeclaredname/testdata/src/a/a.go | 8 +++---- .../undeclaredname/testdata/src/a/channels.go | 2 +- .../testdata/src/a/consecutive_params.go | 2 +- .../testdata/src/a/error_param.go | 2 +- .../undeclaredname/testdata/src/a/literals.go | 2 +- .../testdata/src/a/operation.go | 2 +- .../undeclaredname/testdata/src/a/selector.go | 2 +- .../undeclaredname/testdata/src/a/slice.go | 2 +- .../undeclaredname/testdata/src/a/tuple.go | 2 +- .../testdata/src/a/unique_params.go | 2 +- .../lsp/analysis/undeclaredname/undeclared.go | 12 +++++++--- gopls/internal/lsp/lsp_test.go | 11 ++++----- gopls/internal/lsp/protocol/span.go | 14 ++++++++++- gopls/internal/lsp/testdata/bad/bad1.go | 17 ++++++------- .../internal/lsp/testdata/godef/a/a_x_test.go | 2 +- gopls/internal/lsp/testdata/undeclared/var.go | 8 +++---- .../lsp/testdata/undeclared/var.go.golden | 24 +++++++++---------- gopls/internal/lsp/tests/tests.go | 2 ++ gopls/internal/lsp/tests/util.go | 15 ++++++------ 19 files changed, 75 insertions(+), 56 deletions(-) diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go index 81c732001af..c5d8a2d789c 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go @@ -6,22 +6,22 @@ package undeclared func x() int { var z int - z = y // want "undeclared name: y" + z = y // want "(undeclared name|undefined): y" - if z == m { // want "undeclared name: m" + if z == m { // want "(undeclared name|undefined): m" z = 1 } if z == 1 { z = 1 - } else if z == n+1 { // want "undeclared name: n" + } else if z == n+1 { // want "(undeclared name|undefined): n" z = 1 } switch z { case 10: z = 1 - case a: // want "undeclared name: a" + case a: // want "(undeclared name|undefined): a" z = 1 } return z diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go index ecf00ecfc20..76c7ba685e1 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go @@ -5,7 +5,7 @@ package undeclared func channels(s string) { - undefinedChannels(c()) // want "undeclared name: undefinedChannels" + undefinedChannels(c()) // want "(undeclared name|undefined): undefinedChannels" } func c() (<-chan string, chan string) { diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go index ab7b2ba5c18..73beace102c 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go @@ -6,5 +6,5 @@ package undeclared func consecutiveParams() { var s string - undefinedConsecutiveParams(s, s) // want "undeclared name: undefinedConsecutiveParams" + undefinedConsecutiveParams(s, s) // want "(undeclared name|undefined): undefinedConsecutiveParams" } diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go index 341a9d2a453..5de9254112d 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go @@ -6,5 +6,5 @@ package undeclared func errorParam() { var err error - undefinedErrorParam(err) // want "undeclared name: undefinedErrorParam" + undefinedErrorParam(err) // want "(undeclared name|undefined): undefinedErrorParam" } diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go index ab82463d00e..c62174ec947 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go @@ -7,5 +7,5 @@ package undeclared type T struct{} func literals() { - undefinedLiterals("hey compiler", T{}, &T{}) // want "undeclared name: undefinedLiterals" + undefinedLiterals("hey compiler", T{}, &T{}) // want "(undeclared name|undefined): undefinedLiterals" } diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go index 9a543821ee6..9396da4bd9d 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go @@ -7,5 +7,5 @@ package undeclared import "time" func operation() { - undefinedOperation(10 * time.Second) // want "undeclared name: undefinedOperation" + undefinedOperation(10 * time.Second) // want "(undeclared name|undefined): undefinedOperation" } diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go index 9ed09a27f24..a4ed290d466 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go @@ -6,5 +6,5 @@ package undeclared func selector() { m := map[int]bool{} - undefinedSelector(m[1]) // want "undeclared name: undefinedSelector" + undefinedSelector(m[1]) // want "(undeclared name|undefined): undefinedSelector" } diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go index d741c68f68d..5cde299add3 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go @@ -5,5 +5,5 @@ package undeclared func slice() { - undefinedSlice([]int{1, 2}) // want "undeclared name: undefinedSlice" + undefinedSlice([]int{1, 2}) // want "(undeclared name|undefined): undefinedSlice" } diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go index 3148e8f4d4c..9e91c59c25e 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go @@ -5,7 +5,7 @@ package undeclared func tuple() { - undefinedTuple(b()) // want "undeclared name: undefinedTuple" + undefinedTuple(b()) // want "(undeclared name|undefined): undefinedTuple" } func b() (string, error) { diff --git a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go index 98f77a43cd1..5b4241425e5 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go @@ -7,5 +7,5 @@ package undeclared func uniqueArguments() { var s string var i int - undefinedUniqueArguments(s, i, s) // want "undeclared name: undefinedUniqueArguments" + undefinedUniqueArguments(s, i, s) // want "(undeclared name|undefined): undefinedUniqueArguments" } diff --git a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go index faa14091aee..c06ae3538c6 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -45,7 +45,7 @@ var Analyzer = &analysis.Analyzer{ RunDespiteErrors: true, } -const undeclaredNamePrefix = "undeclared name: " +var undeclaredNamePrefixes = []string{"undeclared name: ", "undefined: "} func run(pass *analysis.Pass) (interface{}, error) { for _, err := range analysisinternal.GetTypeErrors(pass) { @@ -55,10 +55,16 @@ func run(pass *analysis.Pass) (interface{}, error) { } func runForError(pass *analysis.Pass, err types.Error) { - if !strings.HasPrefix(err.Msg, undeclaredNamePrefix) { + var name string + for _, prefix := range undeclaredNamePrefixes { + if !strings.HasPrefix(err.Msg, prefix) { + continue + } + name = strings.TrimPrefix(err.Msg, prefix) + } + if name == "" { return } - name := strings.TrimPrefix(err.Msg, undeclaredNamePrefix) var file *ast.File for _, f := range pass.Files { if f.Pos() <= err.Pos && err.Pos < f.End() { diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index bdf8a6b4990..269fdaf3fb8 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -513,7 +513,7 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.S break } } - codeActionKinds := []protocol.CodeActionKind{} + var codeActionKinds []protocol.CodeActionKind for _, k := range actionKinds { codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k.ActionKind)) } @@ -541,12 +541,11 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.S } if len(actions) != expectedActions { - // Hack: We assume that we only get one code action per range. - var cmds []string + var summaries []string for _, a := range actions { - cmds = append(cmds, fmt.Sprintf("%s (%s)", a.Command, a.Title)) + summaries = append(summaries, fmt.Sprintf("%q (%s)", a.Title, a.Kind)) } - t.Fatalf("unexpected number of code actions, want %d, got %d: %v", expectedActions, len(actions), cmds) + t.Fatalf("CodeAction(...): got %d code actions (%v), want %d", len(actions), summaries, expectedActions) } action := actions[0] var match bool @@ -557,7 +556,7 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.S } } if !match { - t.Fatalf("unexpected kind for code action %s, expected one of %v, got %v", action.Title, codeActionKinds, action.Kind) + t.Fatalf("unexpected kind for code action %s, got %v, want one of %v", action.Title, action.Kind, codeActionKinds) } var res map[span.URI]string if cmd := action.Command; cmd != nil { diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 23721ee703a..58601a6b347 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -186,6 +186,18 @@ func Intersect(a, b Range) bool { (a.End.Line == b.Start.Line) && a.End.Character < b.Start.Character) } +// Format implements fmt.Formatter. +// +// Note: Formatter is implemented instead of Stringer (presumably) for +// performance reasons, though it is not clear that it matters in practice. func (r Range) Format(f fmt.State, _ rune) { - fmt.Fprintf(f, "%v:%v-%v:%v", r.Start.Line, r.Start.Character, r.End.Line, r.End.Character) + fmt.Fprintf(f, "%v-%v", r.Start, r.End) +} + +// Format implements fmt.Formatter. +// +// See Range.Format for discussion of why the Formatter interface is +// implemented rather than Stringer. +func (p Position) Format(f fmt.State, _ rune) { + fmt.Fprintf(f, "%v:%v", p.Line, p.Character) } diff --git a/gopls/internal/lsp/testdata/bad/bad1.go b/gopls/internal/lsp/testdata/bad/bad1.go index 512f2d9869b..04472feae45 100644 --- a/gopls/internal/lsp/testdata/bad/bad1.go +++ b/gopls/internal/lsp/testdata/bad/bad1.go @@ -1,3 +1,4 @@ +//go:build go1.11 // +build go1.11 package bad @@ -5,7 +6,7 @@ package bad // See #36637 type stateFunc func() stateFunc //@item(stateFunc, "stateFunc", "func() stateFunc", "type") -var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", "compiler", "undeclared name: unknown", "error") +var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", "compiler", "(undeclared name|undefined): unknown", "error") func random() int { //@item(random, "random", "func() int", "func") //@complete("", global_a, bob, random, random2, random3, stateFunc, stuff) @@ -14,8 +15,8 @@ func random() int { //@item(random, "random", "func() int", "func") func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func"),item(bad_y_param, "y", "int", "var") x := 6 //@item(x, "x", "int", "var"),diag("x", "compiler", "x declared but not used", "error") - var q blah //@item(q, "q", "blah", "var"),diag("q", "compiler", "q declared but not used", "error"),diag("blah", "compiler", "undeclared name: blah", "error") - var t **blob //@item(t, "t", "**blob", "var"),diag("t", "compiler", "t declared but not used", "error"),diag("blob", "compiler", "undeclared name: blob", "error") + var q blah //@item(q, "q", "blah", "var"),diag("q", "compiler", "q declared but not used", "error"),diag("blah", "compiler", "(undeclared name|undefined): blah", "error") + var t **blob //@item(t, "t", "**blob", "var"),diag("t", "compiler", "t declared but not used", "error"),diag("blob", "compiler", "(undeclared name|undefined): blob", "error") //@complete("", q, t, x, bad_y_param, global_a, bob, random, random2, random3, stateFunc, stuff) return y @@ -24,10 +25,10 @@ func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func") func random3(y ...int) { //@item(random3, "random3", "func(y ...int)", "func"),item(y_variadic_param, "y", "[]int", "var") //@complete("", y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) - var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", "compiler", "ch declared but not used", "error"),diag("favType1", "compiler", "undeclared name: favType1", "error") - var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", "compiler", "m declared but not used", "error"),diag("keyType", "compiler", "undeclared name: keyType", "error") - var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", "compiler", "arr declared but not used", "error"),diag("favType2", "compiler", "undeclared name: favType2", "error") - var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", "compiler", "fn1 declared but not used", "error"),diag("badResult", "compiler", "undeclared name: badResult", "error") - var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", "compiler", "fn2 declared but not used", "error"),diag("badParam", "compiler", "undeclared name: badParam", "error") + var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", "compiler", "ch declared but not used", "error"),diag("favType1", "compiler", "(undeclared name|undefined): favType1", "error") + var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", "compiler", "m declared but not used", "error"),diag("keyType", "compiler", "(undeclared name|undefined): keyType", "error") + var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", "compiler", "arr declared but not used", "error"),diag("favType2", "compiler", "(undeclared name|undefined): favType2", "error") + var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", "compiler", "fn1 declared but not used", "error"),diag("badResult", "compiler", "(undeclared name|undefined): badResult", "error") + var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", "compiler", "fn2 declared but not used", "error"),diag("badParam", "compiler", "(undeclared name|undefined): badParam", "error") //@complete("", arr, ch, fn1, fn2, m, y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) } diff --git a/gopls/internal/lsp/testdata/godef/a/a_x_test.go b/gopls/internal/lsp/testdata/godef/a/a_x_test.go index 4631eba2c0a..f166f055084 100644 --- a/gopls/internal/lsp/testdata/godef/a/a_x_test.go +++ b/gopls/internal/lsp/testdata/godef/a/a_x_test.go @@ -5,5 +5,5 @@ import ( ) func TestA2(t *testing.T) { //@TestA2,godef(TestA2, TestA2) - Nonexistant() //@diag("Nonexistant", "compiler", "undeclared name: Nonexistant", "error") + Nonexistant() //@diag("Nonexistant", "compiler", "(undeclared name|undefined): Nonexistant", "error") } diff --git a/gopls/internal/lsp/testdata/undeclared/var.go b/gopls/internal/lsp/testdata/undeclared/var.go index e27a733d942..3fda582ce1f 100644 --- a/gopls/internal/lsp/testdata/undeclared/var.go +++ b/gopls/internal/lsp/testdata/undeclared/var.go @@ -1,14 +1,14 @@ package undeclared func m() int { - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix", "") + z, _ := 1+y, 11 //@diag("y", "compiler", "(undeclared name|undefined): y", "error"),suggestedfix("y", "quickfix", "") if 100 < 90 { z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix", "") + } else if 100 > n+2 { //@diag("n", "compiler", "(undeclared name|undefined): n", "error"),suggestedfix("n", "quickfix", "") z = 4 } - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix", "") + for i < 200 { //@diag("i", "compiler", "(undeclared name|undefined): i", "error"),suggestedfix("i", "quickfix", "") } - r() //@diag("r", "compiler", "undeclared name: r", "error") + r() //@diag("r", "compiler", "(undeclared name|undefined): r", "error") return z } diff --git a/gopls/internal/lsp/testdata/undeclared/var.go.golden b/gopls/internal/lsp/testdata/undeclared/var.go.golden index a266df7c0c7..de5cbb42fbb 100644 --- a/gopls/internal/lsp/testdata/undeclared/var.go.golden +++ b/gopls/internal/lsp/testdata/undeclared/var.go.golden @@ -2,16 +2,16 @@ package undeclared func m() int { - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix", "") + z, _ := 1+y, 11 //@diag("y", "compiler", "(undeclared name|undefined): y", "error"),suggestedfix("y", "quickfix", "") if 100 < 90 { z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix", "") + } else if 100 > n+2 { //@diag("n", "compiler", "(undeclared name|undefined): n", "error"),suggestedfix("n", "quickfix", "") z = 4 } i := - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix", "") + for i < 200 { //@diag("i", "compiler", "(undeclared name|undefined): i", "error"),suggestedfix("i", "quickfix", "") } - r() //@diag("r", "compiler", "undeclared name: r", "error") + r() //@diag("r", "compiler", "(undeclared name|undefined): r", "error") return z } @@ -20,15 +20,15 @@ package undeclared func m() int { y := - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix", "") + z, _ := 1+y, 11 //@diag("y", "compiler", "(undeclared name|undefined): y", "error"),suggestedfix("y", "quickfix", "") if 100 < 90 { z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix", "") + } else if 100 > n+2 { //@diag("n", "compiler", "(undeclared name|undefined): n", "error"),suggestedfix("n", "quickfix", "") z = 4 } - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix", "") + for i < 200 { //@diag("i", "compiler", "(undeclared name|undefined): i", "error"),suggestedfix("i", "quickfix", "") } - r() //@diag("r", "compiler", "undeclared name: r", "error") + r() //@diag("r", "compiler", "(undeclared name|undefined): r", "error") return z } @@ -36,16 +36,16 @@ func m() int { package undeclared func m() int { - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix", "") + z, _ := 1+y, 11 //@diag("y", "compiler", "(undeclared name|undefined): y", "error"),suggestedfix("y", "quickfix", "") n := if 100 < 90 { z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix", "") + } else if 100 > n+2 { //@diag("n", "compiler", "(undeclared name|undefined): n", "error"),suggestedfix("n", "quickfix", "") z = 4 } - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix", "") + for i < 200 { //@diag("i", "compiler", "(undeclared name|undefined): i", "error"),suggestedfix("i", "quickfix", "") } - r() //@diag("r", "compiler", "undeclared name: r", "error") + r() //@diag("r", "compiler", "(undeclared name|undefined): r", "error") return z } diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 689fcca94f0..253e28aaf91 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -1404,6 +1404,8 @@ func uriName(uri span.URI) string { return filepath.Base(strings.TrimSuffix(uri.Filename(), ".go")) } +// TODO(golang/go#54845): improve the formatting here to match standard +// line:column position formatting. func SpanName(spn span.Span) string { return fmt.Sprintf("%v_%v_%v", uriName(spn.URI()), spn.Start().Line(), spn.Start().Column()) } diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index 8a08b044b53..6f2d01cd8d1 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -86,20 +86,19 @@ func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string { } for i, w := range want { g := got[i] + if !rangeOverlaps(g.Range, w.Range) { + return summarizeDiagnostics(i, uri, want, got, "got Range %v, want overlap with %v", g.Range, w.Range) + } if match, err := regexp.MatchString(w.Message, g.Message); err != nil { - return summarizeDiagnostics(i, uri, want, got, "invalid regular expression %q: %v", w.Message, err) - + return summarizeDiagnostics(i, uri, want, got, "%s: invalid regular expression %q: %v", w.Range.Start, w.Message, err) } else if !match { - return summarizeDiagnostics(i, uri, want, got, "got Message %q, want match for pattern %q", g.Message, w.Message) + return summarizeDiagnostics(i, uri, want, got, "%s: got Message %q, want match for pattern %q", g.Range.Start, g.Message, w.Message) } if w.Severity != g.Severity { - return summarizeDiagnostics(i, uri, want, got, "got Severity %v, want %v", g.Severity, w.Severity) + return summarizeDiagnostics(i, uri, want, got, "%s: got Severity %v, want %v", g.Range.Start, g.Severity, w.Severity) } if w.Source != g.Source { - return summarizeDiagnostics(i, uri, want, got, "got Source %v, want %v", g.Source, w.Source) - } - if !rangeOverlaps(g.Range, w.Range) { - return summarizeDiagnostics(i, uri, want, got, "got Range %v, want overlap with %v", g.Range, w.Range) + return summarizeDiagnostics(i, uri, want, got, "%s: got Source %v, want %v", g.Range.Start, g.Source, w.Source) } } return "" From eb45e986a7f1b6c4699cac573516739609bffd8a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 26 Sep 2022 18:01:05 -0400 Subject: [PATCH 276/723] gopls/internal/regtest: fix regtest failures from "undefined" errors Following-up on CL 434636, also update gopls regtests to handle the new "undefined: ..." errors replacing "undeclared name: ..." errors in go/types. Change-Id: I53a05623b63851e8165ab3685aff2cdf494fa5b6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/434639 Reviewed-by: Robert Griesemer Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/code_action.go | 6 ++++++ .../internal/regtest/diagnostics/diagnostics_test.go | 12 ++++++------ gopls/internal/regtest/workspace/workspace_test.go | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 1d062d717e8..dea83f5bdd6 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -261,6 +261,12 @@ func importDiagnostics(fix *imports.ImportFix, diagnostics []protocol.Diagnostic if ident == fix.IdentName { results = append(results, diagnostic) } + // "undefined: X" may be an unresolved import at Go 1.20+. + case strings.HasPrefix(diagnostic.Message, "undefined: "): + ident := strings.TrimPrefix(diagnostic.Message, "undefined: ") + if ident == fix.IdentName { + results = append(results, diagnostic) + } // "could not import: X" may be an invalid import. case strings.HasPrefix(diagnostic.Message, "could not import: "): ident := strings.TrimPrefix(diagnostic.Message, "could not import: ") diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 473d9b74174..f12bdbae759 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -618,7 +618,7 @@ func main() { env.RegexpReplace("x/x.go", `package x`, `package main`) env.Await(OnceMet( env.DoneWithChange(), - env.DiagnosticAtRegexpWithMessage("x/main.go", `fmt`, "undeclared name"))) + env.DiagnosticAtRegexp("x/main.go", `fmt`))) } }) } @@ -1800,7 +1800,7 @@ var Bar = Foo Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo.go") - env.Await(env.DiagnosticAtRegexpWithMessage("bar.go", `Foo`, "undeclared name")) + env.Await(env.DiagnosticAtRegexp("bar.go", `Foo`)) env.RegexpReplace("foo.go", `\+build`, "") env.Await(EmptyDiagnostics("bar.go")) }) @@ -1831,15 +1831,15 @@ package main env.OpenFile("main.go") env.OpenFile("other.go") env.Await( - env.DiagnosticAtRegexpWithMessage("main.go", "asdf", "undeclared name"), - env.DiagnosticAtRegexpWithMessage("main.go", "fdas", "undeclared name"), + env.DiagnosticAtRegexp("main.go", "asdf"), + env.DiagnosticAtRegexp("main.go", "fdas"), ) env.SetBufferContent("other.go", "package main\n\nasdf") // The new diagnostic in other.go should not suppress diagnostics in main.go. env.Await( OnceMet( env.DiagnosticAtRegexpWithMessage("other.go", "asdf", "expected declaration"), - env.DiagnosticAtRegexpWithMessage("main.go", "asdf", "undeclared name"), + env.DiagnosticAtRegexp("main.go", "asdf"), ), ) }) @@ -2082,7 +2082,7 @@ func F[T C](_ T) { var d protocol.PublishDiagnosticsParams env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("main.go", `C`, "undeclared name"), + env.DiagnosticAtRegexp("main.go", `C`), ReadDiagnostics("main.go", &d), ), ) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index cf24886cea2..92cd174c2e6 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1157,8 +1157,8 @@ func (Server) Foo() {} // as invalid. So we need to wait for the metadata of main_test.go to be // updated before moving other_test.go back to the main_test package. env.Await( - env.DiagnosticAtRegexpWithMessage("other_test.go", "Server", "undeclared"), - env.DiagnosticAtRegexpWithMessage("main_test.go", "otherConst", "undeclared"), + env.DiagnosticAtRegexp("other_test.go", "Server"), + env.DiagnosticAtRegexp("main_test.go", "otherConst"), ) env.RegexpReplace("other_test.go", "main", "main_test") env.Await( From 10e9d3cefa8752e7ddbc2b6316539e6b578a306b Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 27 Sep 2022 13:04:59 -0400 Subject: [PATCH 277/723] gopls/internal/lsp: tolerate new 'imported and not used' error message Tolerate the new form of the "... imported but not used" error message, to allow landing this change in go/types. Along the way, improve the test output when comparing diagnostics, and formatting results. For golang/go#54845 Change-Id: I998d539f82e0f70c85bdb4c40858be5e01dd08b6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/435355 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan Reviewed-by: Robert Griesemer Auto-Submit: Robert Findley --- .../lsp/analysis/undeclaredname/undeclared.go | 1 + .../unusedvariable/testdata/src/assign/a.go | 34 +++---- .../testdata/src/assign/a.go.golden | 20 ++-- .../unusedvariable/testdata/src/decl/a.go | 8 +- .../testdata/src/decl/a.go.golden | 2 +- .../analysis/unusedvariable/unusedvariable.go | 15 +-- gopls/internal/lsp/cmd/test/check.go | 4 +- gopls/internal/lsp/lsp_test.go | 8 +- gopls/internal/lsp/source/source_test.go | 4 +- gopls/internal/lsp/testdata/bad/bad0.go | 2 +- gopls/internal/lsp/testdata/bad/bad1.go | 16 ++-- .../lsp/testdata/format/bad_format.go.golden | 2 +- .../lsp/testdata/format/bad_format.go.in | 2 +- .../lsp/testdata/generated/generated.go | 2 +- .../lsp/testdata/generated/generator.go | 2 +- .../internal/lsp/testdata/testy/testy_test.go | 2 +- gopls/internal/lsp/tests/util.go | 93 ++++++++++--------- .../regtest/workspace/workspace_test.go | 2 +- 18 files changed, 113 insertions(+), 106 deletions(-) diff --git a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go index c06ae3538c6..eaecc8f0e12 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -45,6 +45,7 @@ var Analyzer = &analysis.Analyzer{ RunDespiteErrors: true, } +// The prefix for this error message changed in Go 1.20. var undeclaredNamePrefixes = []string{"undeclared name: ", "undefined: "} func run(pass *analysis.Pass) (interface{}, error) { diff --git a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go index eccfe14c3aa..aa9f46e5b31 100644 --- a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go +++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go @@ -14,55 +14,55 @@ type A struct { } func singleAssignment() { - v := "s" // want `v declared but not used` + v := "s" // want `v declared (and|but) not used` - s := []int{ // want `s declared but not used` + s := []int{ // want `s declared (and|but) not used` 1, 2, } - a := func(s string) bool { // want `a declared but not used` + a := func(s string) bool { // want `a declared (and|but) not used` return false } if 1 == 1 { - s := "v" // want `s declared but not used` + s := "v" // want `s declared (and|but) not used` } panic("I should survive") } func noOtherStmtsInBlock() { - v := "s" // want `v declared but not used` + v := "s" // want `v declared (and|but) not used` } func partOfMultiAssignment() { - f, err := os.Open("file") // want `f declared but not used` + f, err := os.Open("file") // want `f declared (and|but) not used` panic(err) } func sideEffects(cBool chan bool, cInt chan int) { - b := <-c // want `b declared but not used` - s := fmt.Sprint("") // want `s declared but not used` - a := A{ // want `a declared but not used` + b := <-c // want `b declared (and|but) not used` + s := fmt.Sprint("") // want `s declared (and|but) not used` + a := A{ // want `a declared (and|but) not used` b: func() int { return 1 }(), } - c := A{<-cInt} // want `c declared but not used` - d := fInt() + <-cInt // want `d declared but not used` - e := fBool() && <-cBool // want `e declared but not used` - f := map[int]int{ // want `f declared but not used` + c := A{<-cInt} // want `c declared (and|but) not used` + d := fInt() + <-cInt // want `d declared (and|but) not used` + e := fBool() && <-cBool // want `e declared (and|but) not used` + f := map[int]int{ // want `f declared (and|but) not used` fInt(): <-cInt, } - g := []int{<-cInt} // want `g declared but not used` - h := func(s string) {} // want `h declared but not used` - i := func(s string) {}() // want `i declared but not used` + g := []int{<-cInt} // want `g declared (and|but) not used` + h := func(s string) {} // want `h declared (and|but) not used` + i := func(s string) {}() // want `i declared (and|but) not used` } func commentAbove() { // v is a variable - v := "s" // want `v declared but not used` + v := "s" // want `v declared (and|but) not used` } func fBool() bool { diff --git a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden index 8d6e561fa60..18173ce0bf9 100644 --- a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden +++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden @@ -24,26 +24,26 @@ func noOtherStmtsInBlock() { } func partOfMultiAssignment() { - _, err := os.Open("file") // want `f declared but not used` + _, err := os.Open("file") // want `f declared (and|but) not used` panic(err) } func sideEffects(cBool chan bool, cInt chan int) { - <-c // want `b declared but not used` - fmt.Sprint("") // want `s declared but not used` - A{ // want `a declared but not used` + <-c // want `b declared (and|but) not used` + fmt.Sprint("") // want `s declared (and|but) not used` + A{ // want `a declared (and|but) not used` b: func() int { return 1 }(), } - A{<-cInt} // want `c declared but not used` - fInt() + <-cInt // want `d declared but not used` - fBool() && <-cBool // want `e declared but not used` - map[int]int{ // want `f declared but not used` + A{<-cInt} // want `c declared (and|but) not used` + fInt() + <-cInt // want `d declared (and|but) not used` + fBool() && <-cBool // want `e declared (and|but) not used` + map[int]int{ // want `f declared (and|but) not used` fInt(): <-cInt, } - []int{<-cInt} // want `g declared but not used` - func(s string) {}() // want `i declared but not used` + []int{<-cInt} // want `g declared (and|but) not used` + func(s string) {}() // want `i declared (and|but) not used` } func commentAbove() { diff --git a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go index 024e49db9c4..8e843024a54 100644 --- a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go +++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go @@ -5,17 +5,17 @@ package decl func a() { - var b, c bool // want `b declared but not used` + var b, c bool // want `b declared (and|but) not used` panic(c) if 1 == 1 { - var s string // want `s declared but not used` + var s string // want `s declared (and|but) not used` } } func b() { // b is a variable - var b bool // want `b declared but not used` + var b bool // want `b declared (and|but) not used` } func c() { @@ -23,7 +23,7 @@ func c() { d string // some comment for c - c bool // want `c declared but not used` + c bool // want `c declared (and|but) not used` ) panic(d) diff --git a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden index a589a47af1f..6ed97332eea 100644 --- a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden +++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden @@ -5,7 +5,7 @@ package decl func a() { - var c bool // want `b declared but not used` + var c bool // want `b declared (and|but) not used` panic(c) if 1 == 1 { diff --git a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go index 47564f1f154..026befb7dff 100644 --- a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go +++ b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go @@ -34,15 +34,18 @@ var Analyzer = &analysis.Analyzer{ type fixesForError map[types.Error][]analysis.SuggestedFix -const unusedVariableSuffix = " declared but not used" +// The suffix for this error message changed in Go 1.20. +var unusedVariableSuffixes = []string{" declared and not used", " declared but not used"} func run(pass *analysis.Pass) (interface{}, error) { for _, typeErr := range analysisinternal.GetTypeErrors(pass) { - if strings.HasSuffix(typeErr.Msg, unusedVariableSuffix) { - varName := strings.TrimSuffix(typeErr.Msg, unusedVariableSuffix) - err := runForError(pass, typeErr, varName) - if err != nil { - return nil, err + for _, suffix := range unusedVariableSuffixes { + if strings.HasSuffix(typeErr.Msg, suffix) { + varName := strings.TrimSuffix(typeErr.Msg, suffix) + err := runForError(pass, typeErr, varName) + if err != nil { + return nil, err + } } } } diff --git a/gopls/internal/lsp/cmd/test/check.go b/gopls/internal/lsp/cmd/test/check.go index 4f5e471dd31..819863dadaf 100644 --- a/gopls/internal/lsp/cmd/test/check.go +++ b/gopls/internal/lsp/cmd/test/check.go @@ -59,7 +59,5 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost diag.Severity = 0 } - if diff := tests.DiffDiagnostics(uri, want, got); diff != "" { - t.Error(diff) - } + tests.CompareDiagnostics(t, uri, want, got) } diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 269fdaf3fb8..a61f6830789 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -223,9 +223,7 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost v := r.server.session.View(r.data.Config.Dir) r.collectDiagnostics(v) got := append([]*source.Diagnostic(nil), r.diagnostics[uri]...) // copy - if diff := tests.DiffDiagnostics(uri, want, got); diff != "" { - t.Error(diff) - } + tests.CompareDiagnostics(t, uri, want, got) } func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { @@ -416,8 +414,8 @@ func (r *runner) Format(t *testing.T, spn span.Span) { t.Error(err) } got := diff.ApplyEdits(string(m.Content), sedits) - if gofmted != got { - t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got) + if diff := compare.Text(gofmted, got); diff != "" { + t.Errorf("format failed for %s (-want +got):\n%s", filename, diff) } } diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 155832a4825..1f348764cfe 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -156,9 +156,7 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost if err != nil { t.Fatal(err) } - if diff := tests.DiffDiagnostics(fileID.URI, want, got); diff != "" { - t.Error(diff) - } + tests.CompareDiagnostics(t, fileID.URI, want, got) } func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { diff --git a/gopls/internal/lsp/testdata/bad/bad0.go b/gopls/internal/lsp/testdata/bad/bad0.go index e8f3c2f2978..9eedf4aead0 100644 --- a/gopls/internal/lsp/testdata/bad/bad0.go +++ b/gopls/internal/lsp/testdata/bad/bad0.go @@ -9,7 +9,7 @@ func stuff() { //@item(stuff, "stuff", "func()", "func") x := "heeeeyyyy" random2(x) //@diag("x", "compiler", "cannot use x \\(variable of type string\\) as int value in argument to random2", "error") random2(1) //@complete("dom", random, random2, random3) - y := 3 //@diag("y", "compiler", "y declared but not used", "error") + y := 3 //@diag("y", "compiler", "y declared (and|but) not used", "error") } type bob struct { //@item(bob, "bob", "struct{...}", "struct") diff --git a/gopls/internal/lsp/testdata/bad/bad1.go b/gopls/internal/lsp/testdata/bad/bad1.go index 04472feae45..13b3d0af61c 100644 --- a/gopls/internal/lsp/testdata/bad/bad1.go +++ b/gopls/internal/lsp/testdata/bad/bad1.go @@ -14,9 +14,9 @@ func random() int { //@item(random, "random", "func() int", "func") } func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func"),item(bad_y_param, "y", "int", "var") - x := 6 //@item(x, "x", "int", "var"),diag("x", "compiler", "x declared but not used", "error") - var q blah //@item(q, "q", "blah", "var"),diag("q", "compiler", "q declared but not used", "error"),diag("blah", "compiler", "(undeclared name|undefined): blah", "error") - var t **blob //@item(t, "t", "**blob", "var"),diag("t", "compiler", "t declared but not used", "error"),diag("blob", "compiler", "(undeclared name|undefined): blob", "error") + x := 6 //@item(x, "x", "int", "var"),diag("x", "compiler", "x declared (and|but) not used", "error") + var q blah //@item(q, "q", "blah", "var"),diag("q", "compiler", "q declared (and|but) not used", "error"),diag("blah", "compiler", "(undeclared name|undefined): blah", "error") + var t **blob //@item(t, "t", "**blob", "var"),diag("t", "compiler", "t declared (and|but) not used", "error"),diag("blob", "compiler", "(undeclared name|undefined): blob", "error") //@complete("", q, t, x, bad_y_param, global_a, bob, random, random2, random3, stateFunc, stuff) return y @@ -25,10 +25,10 @@ func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func") func random3(y ...int) { //@item(random3, "random3", "func(y ...int)", "func"),item(y_variadic_param, "y", "[]int", "var") //@complete("", y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) - var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", "compiler", "ch declared but not used", "error"),diag("favType1", "compiler", "(undeclared name|undefined): favType1", "error") - var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", "compiler", "m declared but not used", "error"),diag("keyType", "compiler", "(undeclared name|undefined): keyType", "error") - var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", "compiler", "arr declared but not used", "error"),diag("favType2", "compiler", "(undeclared name|undefined): favType2", "error") - var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", "compiler", "fn1 declared but not used", "error"),diag("badResult", "compiler", "(undeclared name|undefined): badResult", "error") - var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", "compiler", "fn2 declared but not used", "error"),diag("badParam", "compiler", "(undeclared name|undefined): badParam", "error") + var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", "compiler", "ch declared (and|but) not used", "error"),diag("favType1", "compiler", "(undeclared name|undefined): favType1", "error") + var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", "compiler", "m declared (and|but) not used", "error"),diag("keyType", "compiler", "(undeclared name|undefined): keyType", "error") + var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", "compiler", "arr declared (and|but) not used", "error"),diag("favType2", "compiler", "(undeclared name|undefined): favType2", "error") + var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", "compiler", "fn1 declared (and|but) not used", "error"),diag("badResult", "compiler", "(undeclared name|undefined): badResult", "error") + var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", "compiler", "fn2 declared (and|but) not used", "error"),diag("badParam", "compiler", "(undeclared name|undefined): badParam", "error") //@complete("", arr, ch, fn1, fn2, m, y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) } diff --git a/gopls/internal/lsp/testdata/format/bad_format.go.golden b/gopls/internal/lsp/testdata/format/bad_format.go.golden index c2ac5a1a13e..f0c24d6356e 100644 --- a/gopls/internal/lsp/testdata/format/bad_format.go.golden +++ b/gopls/internal/lsp/testdata/format/bad_format.go.golden @@ -9,7 +9,7 @@ import ( func hello() { - var x int //@diag("x", "compiler", "x declared but not used", "error") + var x int //@diag("x", "compiler", "x declared (and|but) not used", "error") } func hi() { diff --git a/gopls/internal/lsp/testdata/format/bad_format.go.in b/gopls/internal/lsp/testdata/format/bad_format.go.in index 06187238ebe..995ec399a11 100644 --- a/gopls/internal/lsp/testdata/format/bad_format.go.in +++ b/gopls/internal/lsp/testdata/format/bad_format.go.in @@ -11,7 +11,7 @@ func hello() { - var x int //@diag("x", "compiler", "x declared but not used", "error") + var x int //@diag("x", "compiler", "x declared (and|but) not used", "error") } func hi() { diff --git a/gopls/internal/lsp/testdata/generated/generated.go b/gopls/internal/lsp/testdata/generated/generated.go index c92bd9eb8c3..c7adc180409 100644 --- a/gopls/internal/lsp/testdata/generated/generated.go +++ b/gopls/internal/lsp/testdata/generated/generated.go @@ -3,5 +3,5 @@ package generated // Code generated by generator.go. DO NOT EDIT. func _() { - var y int //@diag("y", "compiler", "y declared but not used", "error") + var y int //@diag("y", "compiler", "y declared (and|but) not used", "error") } diff --git a/gopls/internal/lsp/testdata/generated/generator.go b/gopls/internal/lsp/testdata/generated/generator.go index f26e33c8064..8e2a4fab722 100644 --- a/gopls/internal/lsp/testdata/generated/generator.go +++ b/gopls/internal/lsp/testdata/generated/generator.go @@ -1,5 +1,5 @@ package generated func _() { - var x int //@diag("x", "compiler", "x declared but not used", "error") + var x int //@diag("x", "compiler", "x declared (and|but) not used", "error") } diff --git a/gopls/internal/lsp/testdata/testy/testy_test.go b/gopls/internal/lsp/testdata/testy/testy_test.go index 487b7301761..a7e897840aa 100644 --- a/gopls/internal/lsp/testdata/testy/testy_test.go +++ b/gopls/internal/lsp/testdata/testy/testy_test.go @@ -8,7 +8,7 @@ import ( ) func TestSomething(t *testing.T) { //@item(TestSomething, "TestSomething(t *testing.T)", "", "func") - var x int //@mark(testyX, "x"),diag("x", "compiler", "x declared but not used", "error"),refs("x", testyX) + var x int //@mark(testyX, "x"),diag("x", "compiler", "x declared (and|but) not used", "error"),refs("x", testyX) a() //@mark(testyA, "a") } diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index 6f2d01cd8d1..aa9381d4cac 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -9,11 +9,13 @@ import ( "context" "fmt" "go/token" + "path" "path/filepath" "regexp" "sort" "strconv" "strings" + "testing" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" @@ -65,43 +67,69 @@ func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []proto return "" } -// DiffDiagnostics prints the diff between expected and actual diagnostics test -// results. If the sole expectation is "no_diagnostics", the check is suppressed. -// The Message field of each want element must be a regular expression. -func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string { +// CompareDiagnostics reports testing errors to t when the diagnostic set got +// does not match want. If the sole expectation has source "no_diagnostics", +// the test expects that no diagnostics were received for the given document. +func CompareDiagnostics(t *testing.T, uri span.URI, want, got []*source.Diagnostic) { + t.Helper() + fileName := path.Base(string(uri)) + // A special case to test that there are no diagnostics for a file. if len(want) == 1 && want[0].Source == "no_diagnostics" { - if len(got) != 0 { - return fmt.Sprintf("expected no diagnostics for %s, got %v", uri, got) + want = nil + } + + // Build a helper function to match an actual diagnostic to an overlapping + // expected diagnostic (if any). + unmatched := make([]*source.Diagnostic, len(want)) + copy(unmatched, want) + source.SortDiagnostics(unmatched) + match := func(g *source.Diagnostic) *source.Diagnostic { + // Find the last expected diagnostic d for which start(d) < end(g), and + // check to see if it overlaps. + i := sort.Search(len(unmatched), func(i int) bool { + d := unmatched[i] + // See rangeOverlaps: if a range is a single point, we consider End to be + // included in the range... + if g.Range.Start == g.Range.End { + return protocol.ComparePosition(d.Range.Start, g.Range.End) > 0 + } + // ...otherwise the end position of a range is not included. + return protocol.ComparePosition(d.Range.Start, g.Range.End) >= 0 + }) + if i == 0 { + return nil } - return "" + w := unmatched[i-1] + if rangeOverlaps(w.Range, g.Range) { + unmatched = append(unmatched[:i-1], unmatched[i:]...) + return w + } + return nil } - source.SortDiagnostics(want) - source.SortDiagnostics(got) - - if len(got) != len(want) { - // TODO(adonovan): print the actual difference, not the difference in length! - return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want)) - } - for i, w := range want { - g := got[i] - if !rangeOverlaps(g.Range, w.Range) { - return summarizeDiagnostics(i, uri, want, got, "got Range %v, want overlap with %v", g.Range, w.Range) + for _, g := range got { + w := match(g) + if w == nil { + t.Errorf("%s:%s: unexpected diagnostic %q", fileName, g.Range, g.Message) + continue } if match, err := regexp.MatchString(w.Message, g.Message); err != nil { - return summarizeDiagnostics(i, uri, want, got, "%s: invalid regular expression %q: %v", w.Range.Start, w.Message, err) + t.Errorf("%s:%s: invalid regular expression %q: %v", fileName, w.Range.Start, w.Message, err) } else if !match { - return summarizeDiagnostics(i, uri, want, got, "%s: got Message %q, want match for pattern %q", g.Range.Start, g.Message, w.Message) + t.Errorf("%s:%s: got Message %q, want match for pattern %q", fileName, g.Range.Start, g.Message, w.Message) } if w.Severity != g.Severity { - return summarizeDiagnostics(i, uri, want, got, "%s: got Severity %v, want %v", g.Range.Start, g.Severity, w.Severity) + t.Errorf("%s:%s: got Severity %v, want %v", fileName, g.Range.Start, g.Severity, w.Severity) } if w.Source != g.Source { - return summarizeDiagnostics(i, uri, want, got, "%s: got Source %v, want %v", g.Range.Start, g.Source, w.Source) + t.Errorf("%s:%s: got Source %v, want %v", fileName, g.Range.Start, g.Source, w.Source) } } - return "" + + for _, w := range unmatched { + t.Errorf("%s:%s: unmatched diagnostic pattern %q", fileName, w.Range, w.Message) + } } // rangeOverlaps reports whether r1 and r2 overlap. @@ -125,25 +153,6 @@ func inRange(p protocol.Position, r protocol.Range) bool { return false } -func summarizeDiagnostics(i int, uri span.URI, want, got []*source.Diagnostic, reason string, args ...interface{}) string { - msg := &bytes.Buffer{} - fmt.Fprint(msg, "diagnostics failed") - if i >= 0 { - fmt.Fprintf(msg, " at %d", i) - } - fmt.Fprint(msg, " because of ") - fmt.Fprintf(msg, reason, args...) - fmt.Fprint(msg, ":\nexpected:\n") - for _, d := range want { - fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message) - } - fmt.Fprintf(msg, "got:\n") - for _, d := range got { - fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message) - } - return msg.String() -} - func DiffCodeLens(uri span.URI, want, got []protocol.CodeLens) string { sortCodeLens(want) sortCodeLens(got) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 92cd174c2e6..5d3053c2851 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1215,7 +1215,7 @@ import ( env.Await( OnceMet( env.DoneWithOpen(), - env.DiagnosticAtRegexpWithMessage("a/main.go", "V", "declared but not used"), + env.DiagnosticAtRegexpWithMessage("a/main.go", "V", "not used"), ), ) env.CloseBuffer("a/main.go") From 4dd4ddb97210b591e5a302b999ce5c68402ac241 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 27 Sep 2022 13:53:03 -0400 Subject: [PATCH 278/723] go/packages: issue error if 'go list' on PATH is too new An application that links in version 1.18 of (say) go/types cannot process Go source files reported by version 1.19 of 'go list'. There is no way to tell go list to behave as if it was go1.18, so for now we proceed with parsing/typechecking but, in case of errors, we issue an additional informative diagnostic. Fixes golang/go#55883 Fixes golang/go#55045 Updates golang/go#50825 Updates golang/go#52078 Change-Id: I5fd99b09742c136f4db7f71d2a75e4cdb730460d Reviewed-on: https://go-review.googlesource.com/c/tools/+/435356 gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan Reviewed-by: Bryan Mills Auto-Submit: Alan Donovan Reviewed-by: Zvonimir Pavlinovic --- go/packages/golist.go | 9 ++++++-- go/packages/packages.go | 42 ++++++++++++++++++++++++++++++++--- internal/gocommand/version.go | 13 ++++++++--- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/go/packages/golist.go b/go/packages/golist.go index de881562de1..d9a7915bab0 100644 --- a/go/packages/golist.go +++ b/go/packages/golist.go @@ -60,6 +60,7 @@ func (r *responseDeduper) addAll(dr *driverResponse) { for _, root := range dr.Roots { r.addRoot(root) } + r.dr.GoVersion = dr.GoVersion } func (r *responseDeduper) addPackage(p *Package) { @@ -454,11 +455,14 @@ func (state *golistState) createDriverResponse(words ...string) (*driverResponse if err != nil { return nil, err } + seen := make(map[string]*jsonPackage) pkgs := make(map[string]*Package) additionalErrors := make(map[string][]Error) // Decode the JSON and convert it to Package form. - var response driverResponse + response := &driverResponse{ + GoVersion: goVersion, + } for dec := json.NewDecoder(buf); dec.More(); { p := new(jsonPackage) if err := dec.Decode(p); err != nil { @@ -730,7 +734,7 @@ func (state *golistState) createDriverResponse(words ...string) (*driverResponse } sort.Slice(response.Packages, func(i, j int) bool { return response.Packages[i].ID < response.Packages[j].ID }) - return &response, nil + return response, nil } func (state *golistState) shouldAddFilenameFromError(p *jsonPackage) bool { @@ -756,6 +760,7 @@ func (state *golistState) shouldAddFilenameFromError(p *jsonPackage) bool { return len(p.Error.ImportStack) == 0 || p.Error.ImportStack[len(p.Error.ImportStack)-1] == p.ImportPath } +// getGoVersion returns the effective minor version of the go command. func (state *golistState) getGoVersion() (int, error) { state.goVersionOnce.Do(func() { state.goVersion, state.goVersionError = gocommand.GoVersion(state.ctx, state.cfgInvocation(), state.cfg.gocmdRunner) diff --git a/go/packages/packages.go b/go/packages/packages.go index a93dc6add4d..54d880d206e 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -19,6 +19,7 @@ import ( "log" "os" "path/filepath" + "runtime" "strings" "sync" "time" @@ -233,6 +234,11 @@ type driverResponse struct { // Imports will be connected and then type and syntax information added in a // later pass (see refine). Packages []*Package + + // GoVersion is the minor version number used by the driver + // (e.g. the go command on the PATH) when selecting .go files. + // Zero means unknown. + GoVersion int } // Load loads and returns the Go packages named by the given patterns. @@ -256,7 +262,7 @@ func Load(cfg *Config, patterns ...string) ([]*Package, error) { return nil, err } l.sizes = response.Sizes - return l.refine(response.Roots, response.Packages...) + return l.refine(response) } // defaultDriver is a driver that implements go/packages' fallback behavior. @@ -532,6 +538,7 @@ type loaderPackage struct { needsrc bool // load from source (Mode >= LoadTypes) needtypes bool // type information is either requested or depended on initial bool // package was matched by a pattern + goVersion int // minor version number of go command on PATH } // loader holds the working state of a single call to load. @@ -618,7 +625,8 @@ func newLoader(cfg *Config) *loader { // refine connects the supplied packages into a graph and then adds type and // and syntax information as requested by the LoadMode. -func (ld *loader) refine(roots []string, list ...*Package) ([]*Package, error) { +func (ld *loader) refine(response *driverResponse) ([]*Package, error) { + roots := response.Roots rootMap := make(map[string]int, len(roots)) for i, root := range roots { rootMap[root] = i @@ -626,7 +634,7 @@ func (ld *loader) refine(roots []string, list ...*Package) ([]*Package, error) { ld.pkgs = make(map[string]*loaderPackage) // first pass, fixup and build the map and roots var initial = make([]*loaderPackage, len(roots)) - for _, pkg := range list { + for _, pkg := range response.Packages { rootIndex := -1 if i, found := rootMap[pkg.ID]; found { rootIndex = i @@ -648,6 +656,7 @@ func (ld *loader) refine(roots []string, list ...*Package) ([]*Package, error) { Package: pkg, needtypes: needtypes, needsrc: needsrc, + goVersion: response.GoVersion, } ld.pkgs[lpkg.ID] = lpkg if rootIndex >= 0 { @@ -923,6 +932,33 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { lpkg.Errors = append(lpkg.Errors, errs...) } + // If the go command on the PATH is newer than the runtime, + // then the go/{scanner,ast,parser,types} packages from the + // standard library may be unable to process the files + // selected by go list. + // + // There is currently no way to downgrade the effective + // version of the go command (see issue 52078), so we proceed + // with the newer go command but, in case of parse or type + // errors, we emit an additional diagnostic. + // + // See: + // - golang.org/issue/52078 (flag to set release tags) + // - golang.org/issue/50825 (gopls legacy version support) + // - golang.org/issue/55883 (go/packages confusing error) + var runtimeVersion int + if _, err := fmt.Sscanf(runtime.Version(), "go1.%d", &runtimeVersion); err == nil && runtimeVersion < lpkg.goVersion { + defer func() { + if len(lpkg.Errors) > 0 { + appendError(Error{ + Pos: "-", + Msg: fmt.Sprintf("This application uses version go1.%d of the source-processing packages but runs version go1.%d of 'go list'. It may fail to process source files that rely on newer language features. If so, rebuild the application using a newer version of Go.", runtimeVersion, lpkg.goVersion), + Kind: UnknownError, + }) + } + }() + } + if ld.Config.Mode&NeedTypes != 0 && len(lpkg.CompiledGoFiles) == 0 && lpkg.ExportFile != "" { // The config requested loading sources and types, but sources are missing. // Add an error to the package and fall back to loading from export data. diff --git a/internal/gocommand/version.go b/internal/gocommand/version.go index 71304368020..8db5ceb9d51 100644 --- a/internal/gocommand/version.go +++ b/internal/gocommand/version.go @@ -10,8 +10,15 @@ import ( "strings" ) -// GoVersion checks the go version by running "go list" with modules off. -// It returns the X in Go 1.X. +// GoVersion reports the minor version number of the highest release +// tag built into the go command on the PATH. +// +// Note that this may be higher than the version of the go tool used +// to build this application, and thus the versions of the standard +// go/{scanner,parser,ast,types} packages that are linked into it. +// In that case, callers should either downgrade to the version of +// go used to build the application, or report an error that the +// application is too old to use the go command on the PATH. func GoVersion(ctx context.Context, inv Invocation, r *Runner) (int, error) { inv.Verb = "list" inv.Args = []string{"-e", "-f", `{{context.ReleaseTags}}`, `--`, `unsafe`} @@ -38,7 +45,7 @@ func GoVersion(ctx context.Context, inv Invocation, r *Runner) (int, error) { if len(stdout) < 3 { return 0, fmt.Errorf("bad ReleaseTags output: %q", stdout) } - // Split up "[go1.1 go1.15]" + // Split up "[go1.1 go1.15]" and return highest go1.X value. tags := strings.Fields(stdout[1 : len(stdout)-2]) for i := len(tags) - 1; i >= 0; i-- { var version int From bddb3720ba870cf1e2156ce9147e1b89a255c1dc Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 26 Sep 2022 18:17:59 -0400 Subject: [PATCH 279/723] gopls: deprecate three experimental features Deprecate the following features, which never made it out of experimental: - experimentalWorkspaceModule (golang/go#52897) - experimentalWatchedFileDelay (golang/go#55268) - experimentalUseInvalidMetadata (golang/go#54180) See the associated issues for rationale behind the deprecations. With this change, any users configuring these settings will get a ShowMessageRequest warning them of the deprecation. Fixes golang/go#52897 Fixes golang/go#55268 Fixes golang/go#54180 Change-Id: I49f178e68793df4e4f9edb63e9b03cad127c5f51 Reviewed-on: https://go-review.googlesource.com/c/tools/+/434640 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/doc/settings.md | 12 ++- gopls/doc/workspace.md | 6 +- gopls/internal/lsp/source/api_json.go | 6 +- gopls/internal/lsp/source/options.go | 85 +++++++++++++------ .../regtest/misc/configuration_test.go | 26 +++++- 5 files changed, 103 insertions(+), 32 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 58e97503f36..01b5d1a4da6 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -123,6 +123,9 @@ Default: `true`. experimentalWorkspaceModule opts a user into the experimental support for multi-module workspaces. +Deprecated: this feature is deprecated and will be removed in a future +version of gopls (https://go.dev/issue/55331). + Default: `false`. #### **experimentalPackageCacheKey** *bool* @@ -164,8 +167,10 @@ Default: `false`. experimentalUseInvalidMetadata enables gopls to fall back on outdated package metadata to provide editor features if the go command fails to -load packages for some reason (like an invalid go.mod file). This will -eventually be the default behavior, and this setting will be removed. +load packages for some reason (like an invalid go.mod file). + +Deprecated: this setting is deprecated and will be removed in a future +version of gopls (https://go.dev/issue/55333). Default: `false`. @@ -353,6 +358,9 @@ file system notifications. This option must be set to a valid duration string, for example `"100ms"`. +Deprecated: this setting is deprecated and will be removed in a future +version of gopls (https://go.dev/issue/55332) + Default: `"0s"`. #### Documentation diff --git a/gopls/doc/workspace.md b/gopls/doc/workspace.md index 734eaddbab9..cb166131fba 100644 --- a/gopls/doc/workspace.md +++ b/gopls/doc/workspace.md @@ -44,7 +44,11 @@ go work use tools tools/gopls ...followed by opening the `$WORK` directory in our editor. -#### Experimental workspace module (Go 1.17 and earlier) +#### DEPRECATED: Experimental workspace module (Go 1.17 and earlier) + +**This feature is deprecated and will be removed in future versions of gopls. +Please see [issue #52897](https://go.dev/issue/52897) for additional +information.** With earlier versions of Go, `gopls` can simulate multi-module workspaces by creating a synthetic module requiring the modules in the workspace root. diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 957319a3719..e40ecc2a16d 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -59,7 +59,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "experimentalWorkspaceModule", Type: "bool", - Doc: "experimentalWorkspaceModule opts a user into the experimental support\nfor multi-module workspaces.\n", + Doc: "experimentalWorkspaceModule opts a user into the experimental support\nfor multi-module workspaces.\n\nDeprecated: this feature is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55331).\n", Default: "false", Status: "experimental", Hierarchy: "build", @@ -91,7 +91,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "experimentalUseInvalidMetadata", Type: "bool", - Doc: "experimentalUseInvalidMetadata enables gopls to fall back on outdated\npackage metadata to provide editor features if the go command fails to\nload packages for some reason (like an invalid go.mod file). This will\neventually be the default behavior, and this setting will be removed.\n", + Doc: "experimentalUseInvalidMetadata enables gopls to fall back on outdated\npackage metadata to provide editor features if the go command fails to\nload packages for some reason (like an invalid go.mod file).\n\nDeprecated: this setting is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55333).\n", Default: "false", Status: "experimental", Hierarchy: "build", @@ -510,7 +510,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "experimentalWatchedFileDelay", Type: "time.Duration", - Doc: "experimentalWatchedFileDelay controls the amount of time that gopls waits\nfor additional workspace/didChangeWatchedFiles notifications to arrive,\nbefore processing all such notifications in a single batch. This is\nintended for use by LSP clients that don't support their own batching of\nfile system notifications.\n\nThis option must be set to a valid duration string, for example `\"100ms\"`.\n", + Doc: "experimentalWatchedFileDelay controls the amount of time that gopls waits\nfor additional workspace/didChangeWatchedFiles notifications to arrive,\nbefore processing all such notifications in a single batch. This is\nintended for use by LSP clients that don't support their own batching of\nfile system notifications.\n\nThis option must be set to a valid duration string, for example `\"100ms\"`.\n\nDeprecated: this setting is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55332)\n", Default: "\"0s\"", Status: "experimental", Hierarchy: "ui.diagnostic", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index b15d7144bf6..110621c26b8 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -266,6 +266,9 @@ type BuildOptions struct { // ExperimentalWorkspaceModule opts a user into the experimental support // for multi-module workspaces. + // + // Deprecated: this feature is deprecated and will be removed in a future + // version of gopls (https://go.dev/issue/55331). ExperimentalWorkspaceModule bool `status:"experimental"` // ExperimentalPackageCacheKey controls whether to use a coarser cache key @@ -288,8 +291,10 @@ type BuildOptions struct { // ExperimentalUseInvalidMetadata enables gopls to fall back on outdated // package metadata to provide editor features if the go command fails to - // load packages for some reason (like an invalid go.mod file). This will - // eventually be the default behavior, and this setting will be removed. + // load packages for some reason (like an invalid go.mod file). + // + // Deprecated: this setting is deprecated and will be removed in a future + // version of gopls (https://go.dev/issue/55333). ExperimentalUseInvalidMetadata bool `status:"experimental"` } @@ -425,6 +430,9 @@ type DiagnosticOptions struct { // file system notifications. // // This option must be set to a valid duration string, for example `"100ms"`. + // + // Deprecated: this setting is deprecated and will be removed in a future + // version of gopls (https://go.dev/issue/55332) ExperimentalWatchedFileDelay time.Duration `status:"experimental"` } @@ -865,7 +873,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result := OptionResult{Name: name, Value: value} if _, ok := seen[name]; ok { - result.errorf("duplicate configuration for %s", name) + result.parseErrorf("duplicate configuration for %s", name) } seen[name] = struct{}{} @@ -873,7 +881,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "env": menv, ok := value.(map[string]interface{}) if !ok { - result.errorf("invalid type %T, expect map", value) + result.parseErrorf("invalid type %T, expect map", value) break } if o.Env == nil { @@ -886,7 +894,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "buildFlags": iflags, ok := value.([]interface{}) if !ok { - result.errorf("invalid type %T, expect list", value) + result.parseErrorf("invalid type %T, expect list", value) break } flags := make([]string, 0, len(iflags)) @@ -897,14 +905,14 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "directoryFilters": ifilters, ok := value.([]interface{}) if !ok { - result.errorf("invalid type %T, expect list", value) + result.parseErrorf("invalid type %T, expect list", value) break } var filters []string for _, ifilter := range ifilters { filter, err := validateDirectoryFilter(fmt.Sprintf("%v", ifilter)) if err != nil { - result.errorf(err.Error()) + result.parseErrorf("%v", err) return result } filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/")) @@ -1048,7 +1056,12 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "experimentalPostfixCompletions": result.setBool(&o.ExperimentalPostfixCompletions) - case "experimentalWorkspaceModule": // TODO(rfindley): suggest go.work on go1.18+ + case "experimentalWorkspaceModule": + const msg = "The experimentalWorkspaceModule feature has been replaced by go workspaces, " + + "and will be removed in a future version of gopls (https://go.dev/issue/55331). " + + "Please see https://github.com/golang/tools/blob/master/gopls/doc/workspace.md " + + "for information on setting up multi-module workspaces using go.work files." + result.softErrorf(msg) result.setBool(&o.ExperimentalWorkspaceModule) case "experimentalTemplateSupport": // TODO(pjw): remove after June 2022 @@ -1067,14 +1080,18 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) o.TemplateExtensions = nil break } - result.errorf(fmt.Sprintf("unexpected type %T not []string", value)) - case "experimentalDiagnosticsDelay", "diagnosticsDelay": - if name == "experimentalDiagnosticsDelay" { - result.deprecated("diagnosticsDelay") - } + result.parseErrorf("unexpected type %T not []string", value) + + case "experimentalDiagnosticsDelay": + result.deprecated("diagnosticsDelay") + + case "diagnosticsDelay": result.setDuration(&o.DiagnosticsDelay) case "experimentalWatchedFileDelay": + const msg = "The experimentalWatchedFileDelay setting is deprecated, and will " + + "be removed in a future version of gopls (https://go.dev/issue/55332)." + result.softErrorf(msg) result.setDuration(&o.ExperimentalWatchedFileDelay) case "experimentalPackageCacheKey": @@ -1087,6 +1104,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.AllowImplicitNetworkAccess) case "experimentalUseInvalidMetadata": + const msg = "The experimentalUseInvalidMetadata setting is deprecated, and will be removed" + + "in a future version of gopls (https://go.dev/issue/55333)." + result.softErrorf(msg) result.setBool(&o.ExperimentalUseInvalidMetadata) case "allExperiments": @@ -1140,7 +1160,11 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) return result } -func (r *OptionResult) errorf(msg string, values ...interface{}) { +// parseErrorf reports an error parsing the current configuration value. +func (r *OptionResult) parseErrorf(msg string, values ...interface{}) { + if false { + _ = fmt.Sprintf(msg, values...) // this causes vet to check this like printf + } prefix := fmt.Sprintf("parsing setting %q: ", r.Name) r.Error = fmt.Errorf(prefix+msg, values...) } @@ -1154,6 +1178,16 @@ func (e *SoftError) Error() string { return e.msg } +// softErrorf reports an error that does not affect the functionality of gopls +// (a warning in the UI). +// The formatted message will be shown to the user unmodified. +func (r *OptionResult) softErrorf(format string, values ...interface{}) { + msg := fmt.Sprintf(format, values...) + r.Error = &SoftError{msg} +} + +// deprecated reports the current setting as deprecated. If 'replacement' is +// non-nil, it is suggested to the user. func (r *OptionResult) deprecated(replacement string) { msg := fmt.Sprintf("gopls setting %q is deprecated", r.Name) if replacement != "" { @@ -1162,6 +1196,7 @@ func (r *OptionResult) deprecated(replacement string) { r.Error = &SoftError{msg} } +// unexpected reports that the current setting is not known to gopls. func (r *OptionResult) unexpected() { r.Error = fmt.Errorf("unexpected gopls setting %q", r.Name) } @@ -1169,7 +1204,7 @@ func (r *OptionResult) unexpected() { func (r *OptionResult) asBool() (bool, bool) { b, ok := r.Value.(bool) if !ok { - r.errorf("invalid type %T, expect bool", r.Value) + r.parseErrorf("invalid type %T, expect bool", r.Value) return false, false } return b, true @@ -1185,7 +1220,7 @@ func (r *OptionResult) setDuration(d *time.Duration) { if v, ok := r.asString(); ok { parsed, err := time.ParseDuration(v) if err != nil { - r.errorf("failed to parse duration %q: %v", v, err) + r.parseErrorf("failed to parse duration %q: %v", v, err) return } *d = parsed @@ -1217,18 +1252,18 @@ func (r *OptionResult) setAnnotationMap(bm *map[Annotation]bool) { switch k { case "noEscape": m[Escape] = false - r.errorf(`"noEscape" is deprecated, set "Escape: false" instead`) + r.parseErrorf(`"noEscape" is deprecated, set "Escape: false" instead`) case "noNilcheck": m[Nil] = false - r.errorf(`"noNilcheck" is deprecated, set "Nil: false" instead`) + r.parseErrorf(`"noNilcheck" is deprecated, set "Nil: false" instead`) case "noInline": m[Inline] = false - r.errorf(`"noInline" is deprecated, set "Inline: false" instead`) + r.parseErrorf(`"noInline" is deprecated, set "Inline: false" instead`) case "noBounds": m[Bounds] = false - r.errorf(`"noBounds" is deprecated, set "Bounds: false" instead`) + r.parseErrorf(`"noBounds" is deprecated, set "Bounds: false" instead`) default: - r.errorf(err.Error()) + r.parseErrorf("%v", err) } continue } @@ -1240,7 +1275,7 @@ func (r *OptionResult) setAnnotationMap(bm *map[Annotation]bool) { func (r *OptionResult) asBoolMap() map[string]bool { all, ok := r.Value.(map[string]interface{}) if !ok { - r.errorf("invalid type %T for map[string]bool option", r.Value) + r.parseErrorf("invalid type %T for map[string]bool option", r.Value) return nil } m := make(map[string]bool) @@ -1248,7 +1283,7 @@ func (r *OptionResult) asBoolMap() map[string]bool { if enabled, ok := enabled.(bool); ok { m[a] = enabled } else { - r.errorf("invalid type %T for map key %q", enabled, a) + r.parseErrorf("invalid type %T for map key %q", enabled, a) return m } } @@ -1258,7 +1293,7 @@ func (r *OptionResult) asBoolMap() map[string]bool { func (r *OptionResult) asString() (string, bool) { b, ok := r.Value.(string) if !ok { - r.errorf("invalid type %T, expect string", r.Value) + r.parseErrorf("invalid type %T, expect string", r.Value) return "", false } return b, true @@ -1271,7 +1306,7 @@ func (r *OptionResult) asOneOf(options ...string) (string, bool) { } s, err := asOneOf(s, options...) if err != nil { - r.errorf(err.Error()) + r.parseErrorf("%v", err) } return s, err == nil } diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index 9cd0f0ff91e..a1d46f036c6 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -71,6 +71,30 @@ var FooErr = errors.New("foo") WithOptions( Settings{"staticcheck": true}, ).Run(t, files, func(t *testing.T, env *Env) { - env.Await(ShownMessage("staticcheck is not supported")) + env.Await( + OnceMet( + InitialWorkspaceLoad, + ShownMessage("staticcheck is not supported"), + ), + ) + }) +} + +func TestDeprecatedSettings(t *testing.T) { + WithOptions( + Settings{ + "experimentalUseInvalidMetadata": true, + "experimentalWatchedFileDelay": "1s", + "experimentalWorkspaceModule": true, + }, + ).Run(t, "", func(t *testing.T, env *Env) { + env.Await( + OnceMet( + InitialWorkspaceLoad, + ShownMessage("experimentalWorkspaceModule"), + ShownMessage("experimentalUseInvalidMetadata"), + ShownMessage("experimentalWatchedFileDelay"), + ), + ) }) } From ae737bc61995b4052a4efd8cc46a01d6cba0be85 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 28 Sep 2022 10:42:33 -0400 Subject: [PATCH 280/723] gopls/internal/lsp: remove the source.Session interface The source.Session interface is not needed: the source package should never need to know about sessions. Remove it in favor of the concrete *cache.Session type. Change-Id: I220a1fb1525c57f9fa2a4a4f80152c31a81565ff Reviewed-on: https://go-review.googlesource.com/c/tools/+/435359 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/cache/session.go | 32 +++++++++++++-- gopls/internal/lsp/debug/serve.go | 16 +++----- gopls/internal/lsp/server.go | 5 ++- gopls/internal/lsp/source/view.go | 63 ----------------------------- 4 files changed, 37 insertions(+), 79 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 59318131516..5d40ff6be55 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -13,11 +13,11 @@ import ( "sync" "sync/atomic" + "golang.org/x/tools/gopls/internal/lsp/progress" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/gopls/internal/lsp/progress" - "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/persistent" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" @@ -120,26 +120,31 @@ func (c *closedFile) Version() int32 { return 0 } +// ID returns the unique identifier for this session on this server. func (s *Session) ID() string { return s.id } func (s *Session) String() string { return s.id } +// Options returns a copy of the SessionOptions for this session. func (s *Session) Options() *source.Options { s.optionsMu.Lock() defer s.optionsMu.Unlock() return s.options } +// SetOptions sets the options of this session to new values. func (s *Session) SetOptions(options *source.Options) { s.optionsMu.Lock() defer s.optionsMu.Unlock() s.options = options } +// SetProgressTracker sets the progress tracker for the session. func (s *Session) SetProgressTracker(tracker *progress.Tracker) { // The progress tracker should be set before any view is initialized. s.progress = tracker } +// Shutdown the session and all views it has created. func (s *Session) Shutdown(ctx context.Context) { var views []*View s.viewMu.Lock() @@ -153,10 +158,16 @@ func (s *Session) Shutdown(ctx context.Context) { event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s)) } -func (s *Session) Cache() interface{} { +// Cache returns the cache that created this session, for debugging only. +func (s *Session) Cache() *Cache { return s.cache } +// NewView creates a new View, returning it and its first snapshot. If a +// non-empty tempWorkspace directory is provided, the View will record a copy +// of its gopls workspace module in that directory, so that client tooling +// can execute in the same main module. On success it also returns a release +// function that must be called when the Snapshot is no longer needed. func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (source.View, source.Snapshot, func(), error) { s.viewMu.Lock() defer s.viewMu.Unlock() @@ -292,7 +303,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, return v, snapshot, snapshot.Acquire(), nil } -// View returns the view by name. +// View returns a view with a matching name, if the session has one. func (s *Session) View(name string) source.View { s.viewMu.RLock() defer s.viewMu.RUnlock() @@ -446,6 +457,11 @@ type fileChange struct { isUnchanged bool } +// DidModifyFiles reports a file modification to the session. It returns +// the new snapshots after the modifications have been applied, paired with +// the affected file URIs for those snapshots. +// On success, it returns a release function that +// must be called when the snapshots are no longer needed. func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]span.URI, func(), error) { s.viewMu.RLock() defer s.viewMu.RUnlock() @@ -556,6 +572,9 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif return snapshotURIs, release, nil } +// ExpandModificationsToDirectories returns the set of changes with the +// directory changes removed and expanded to include all of the files in +// the directory. func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []source.FileModification) []source.FileModification { s.viewMu.RLock() defer s.viewMu.RUnlock() @@ -731,6 +750,7 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif return overlays, nil } +// GetFile returns a handle for the specified file. func (s *Session) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { if overlay := s.readOverlay(uri); overlay != nil { return overlay, nil @@ -749,6 +769,7 @@ func (s *Session) readOverlay(uri span.URI) *overlay { return nil } +// Overlays returns a slice of file overlays for the session. func (s *Session) Overlays() []source.Overlay { s.overlayMu.Lock() defer s.overlayMu.Unlock() @@ -760,6 +781,9 @@ func (s *Session) Overlays() []source.Overlay { return overlays } +// FileWatchingGlobPatterns returns glob patterns to watch every directory +// known by the view. For views within a module, this is the module root, +// any directory in the module root, and any replace targets. func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} { s.viewMu.RLock() defer s.viewMu.RUnlock() diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go index da0d68d41d8..248041ed68a 100644 --- a/gopls/internal/lsp/debug/serve.go +++ b/gopls/internal/lsp/debug/serve.go @@ -26,6 +26,10 @@ import ( "sync" "time" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug/log" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/core" "golang.org/x/tools/internal/event/export" @@ -34,12 +38,7 @@ import ( "golang.org/x/tools/internal/event/export/prometheus" "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/bug" - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/debug/log" "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" ) type contextKeyType int @@ -88,10 +87,7 @@ func (st *State) Caches() []*cache.Cache { var caches []*cache.Cache seen := make(map[string]struct{}) for _, client := range st.Clients() { - cache, ok := client.Session.Cache().(*cache.Cache) - if !ok { - continue - } + cache := client.Session.Cache() if _, found := seen[cache.ID()]; found { continue } @@ -208,7 +204,7 @@ func (st *State) addClient(session *cache.Session) { } // DropClient removes a client from the set being served. -func (st *State) dropClient(session source.Session) { +func (st *State) dropClient(session *cache.Session) { st.mu.Lock() defer st.mu.Unlock() for i, c := range st.clients { diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go index a344b5933e6..0d257b26ebf 100644 --- a/gopls/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -10,6 +10,7 @@ import ( "fmt" "sync" + "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" @@ -21,7 +22,7 @@ const concurrentAnalyses = 1 // NewServer creates an LSP server and binds it to handle incoming client // messages on on the supplied stream. -func NewServer(session source.Session, client protocol.ClientCloser) *Server { +func NewServer(session *cache.Session, client protocol.ClientCloser) *Server { tracker := progress.NewTracker(client) session.SetProgressTracker(tracker) return &Server{ @@ -70,7 +71,7 @@ type Server struct { // notifications generated before serverInitialized notifications []*protocol.ShowMessageParams - session source.Session + session *cache.Session tempDir string diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 47f463e13bb..2565087336b 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -21,7 +21,6 @@ import ( "golang.org/x/mod/module" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" @@ -332,68 +331,6 @@ type Metadata interface { ModuleInfo() *packages.Module } -// Session represents a single connection from a client. -// This is the level at which things like open files are maintained on behalf -// of the client. -// A session may have many active views at any given time. -type Session interface { - // ID returns the unique identifier for this session on this server. - ID() string - // NewView creates a new View, returning it and its first snapshot. If a - // non-empty tempWorkspace directory is provided, the View will record a copy - // of its gopls workspace module in that directory, so that client tooling - // can execute in the same main module. On success it also returns a release - // function that must be called when the Snapshot is no longer needed. - NewView(ctx context.Context, name string, folder span.URI, options *Options) (View, Snapshot, func(), error) - - // Cache returns the cache that created this session, for debugging only. - Cache() interface{} - - // View returns a view with a matching name, if the session has one. - View(name string) View - - // ViewOf returns a view corresponding to the given URI. - ViewOf(uri span.URI) (View, error) - - // Views returns the set of active views built by this session. - Views() []View - - // Shutdown the session and all views it has created. - Shutdown(ctx context.Context) - - // GetFile returns a handle for the specified file. - GetFile(ctx context.Context, uri span.URI) (FileHandle, error) - - // DidModifyFile reports a file modification to the session. It returns - // the new snapshots after the modifications have been applied, paired with - // the affected file URIs for those snapshots. - // On success, it returns a release function that - // must be called when the snapshots are no longer needed. - DidModifyFiles(ctx context.Context, changes []FileModification) (map[Snapshot][]span.URI, func(), error) - - // ExpandModificationsToDirectories returns the set of changes with the - // directory changes removed and expanded to include all of the files in - // the directory. - ExpandModificationsToDirectories(ctx context.Context, changes []FileModification) []FileModification - - // Overlays returns a slice of file overlays for the session. - Overlays() []Overlay - - // Options returns a copy of the SessionOptions for this session. - Options() *Options - - // SetOptions sets the options of this session to new values. - SetOptions(*Options) - - // FileWatchingGlobPatterns returns glob patterns to watch every directory - // known by the view. For views within a module, this is the module root, - // any directory in the module root, and any replace targets. - FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} - - // SetProgressTracker sets the progress tracker for the session. - SetProgressTracker(tracker *progress.Tracker) -} - var ErrViewExists = errors.New("view already exists for session") // Overlay is the type for a file held in memory on a session. From 9c63911fcfbccff5843f932813a5a857feaa9948 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 28 Sep 2022 12:03:51 -0400 Subject: [PATCH 281/723] gopls/internal/lsp: delete dead code Delete some gopls code that is no longer used. Change-Id: Ib211b01b6c2ac8a03700b1e47d32ac69fccd1b83 Reviewed-on: https://go-review.googlesource.com/c/tools/+/435360 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro --- .../lsp/analysis/unusedvariable/unusedvariable.go | 2 -- gopls/internal/lsp/cache/snapshot.go | 15 +++------------ gopls/internal/lsp/cmd/export_test.go | 11 ----------- gopls/internal/lsp/command/interface.go | 15 --------------- gopls/internal/lsp/debug/info.go | 11 ----------- gopls/internal/lsp/regtest/expectation.go | 8 -------- gopls/internal/lsp/source/options.go | 10 ---------- gopls/internal/lsp/template/completion.go | 14 -------------- gopls/internal/lsp/template/parse.go | 11 +---------- gopls/internal/lsp/tests/tests.go | 1 - gopls/internal/lsp/work/diagnostics.go | 4 ++-- 11 files changed, 6 insertions(+), 96 deletions(-) delete mode 100644 gopls/internal/lsp/cmd/export_test.go diff --git a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go index 026befb7dff..134cbb2c436 100644 --- a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go +++ b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go @@ -32,8 +32,6 @@ var Analyzer = &analysis.Analyzer{ RunDespiteErrors: true, // an unusedvariable diagnostic is a compile error } -type fixesForError map[types.Error][]analysis.SuggestedFix - // The suffix for this error message changed in Go 1.20. var unusedVariableSuffixes = []string{" declared and not used", " declared but not used"} diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index aa088bb4081..4398142eb04 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -31,11 +31,11 @@ import ( "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/gocommand" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/persistent" @@ -1612,15 +1612,6 @@ func (s *snapshot) orphanedFiles() []source.VersionedFileHandle { return files } -func contains(views []*View, view *View) bool { - for _, v := range views { - if v == view { - return true - } - } - return false -} - // TODO(golang/go#53756): this function needs to consider more than just the // absolute URI, for example: // - the position of /vendor/ with respect to the relevant module root diff --git a/gopls/internal/lsp/cmd/export_test.go b/gopls/internal/lsp/cmd/export_test.go deleted file mode 100644 index 05b3cd31261..00000000000 --- a/gopls/internal/lsp/cmd/export_test.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmd - -const ( - ExampleLine = exampleLine - ExampleColumn = exampleColumn - ExampleOffset = exampleOffset -) diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index d49a4330d89..fc5057580f5 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -272,21 +272,6 @@ type PackageImport struct { Path string } -type WorkspaceMetadataArgs struct { -} - -type WorkspaceMetadataResult struct { - // All workspaces for this session. - Workspaces []Workspace -} - -type Workspace struct { - // The workspace name. - Name string - // The workspace module directory. - ModuleDir string -} - type DebuggingArgs struct { // Optional: the address (including port) for the debug server to listen on. // If not provided, the debug server will bind to "localhost:0", and the diff --git a/gopls/internal/lsp/debug/info.go b/gopls/internal/lsp/debug/info.go index 8784e7455bb..00752e6f9a3 100644 --- a/gopls/internal/lsp/debug/info.go +++ b/gopls/internal/lsp/debug/info.go @@ -38,17 +38,6 @@ type ServerVersion struct { Version string } -type Module struct { - ModuleVersion - Replace *ModuleVersion `json:"replace,omitempty"` -} - -type ModuleVersion struct { - Path string `json:"path,omitempty"` - Version string `json:"version,omitempty"` - Sum string `json:"sum,omitempty"` -} - // VersionInfo returns the build info for the gopls process. If it was not // built in module mode, we return a GOPATH-specific message with the // hardcoded version. diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 5234f7e9b94..eddd8136fd9 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -678,14 +678,6 @@ func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation { return DiagnosticExpectation{path: name, pos: &pos, re: re, present: false} } -// NoDiagnosticAt asserts that there is no diagnostic entry at the position -// specified by line and col, for the workdir-relative path name. -// This should only be used in combination with OnceMet for a given condition, -// otherwise it may always succeed. -func NoDiagnosticAt(name string, line, col int) DiagnosticExpectation { - return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: false} -} - // NoDiagnosticWithMessage asserts that there is no diagnostic entry with the // given message. // diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 110621c26b8..93d09489e7e 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -682,16 +682,6 @@ type OptionResult struct { Error error } -type OptionState int - -const ( - OptionHandled = OptionState(iota) - OptionDeprecated - OptionUnexpected -) - -type LinkTarget string - func SetOptions(options *Options, opts interface{}) OptionResults { var results OptionResults switch opts := opts.(type) { diff --git a/gopls/internal/lsp/template/completion.go b/gopls/internal/lsp/template/completion.go index 7040081e8aa..140c674747d 100644 --- a/gopls/internal/lsp/template/completion.go +++ b/gopls/internal/lsp/template/completion.go @@ -285,17 +285,3 @@ func weakMatch(choice, pattern string) float64 { } return 1 } - -// for debug printing -func strContext(c protocol.CompletionContext) string { - switch c.TriggerKind { - case protocol.Invoked: - return "invoked" - case protocol.TriggerCharacter: - return fmt.Sprintf("triggered(%s)", c.TriggerCharacter) - case protocol.TriggerForIncompleteCompletions: - // gopls doesn't seem to handle these explicitly anywhere - return "incomplete" - } - return fmt.Sprintf("?%v", c) -} diff --git a/gopls/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go index f2e4f25bff5..679bd4d16a5 100644 --- a/gopls/internal/lsp/template/parse.go +++ b/gopls/internal/lsp/template/parse.go @@ -24,9 +24,9 @@ import ( "text/template/parse" "unicode/utf8" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/span" ) @@ -492,15 +492,6 @@ func (wr wrNode) writeNode(n parse.Node, indent string) { } } -// short prints at most 40 bytes of node.String(), for debugging -func short(n parse.Node) string { - s := fmt.Sprint(n) // recovers from panic - if len(s) > 40 { - return s[:40] + "..." - } - return s -} - var kindNames = []string{"", "File", "Module", "Namespace", "Package", "Class", "Method", "Property", "Field", "Constructor", "Enum", "Interface", "Function", "Variable", "Constant", "String", "Number", "Boolean", "Array", "Object", "Key", "Null", "EnumMember", "Struct", "Event", diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 253e28aaf91..2b8e8300af4 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -87,7 +87,6 @@ type References = map[span.Span][]span.Span type Renames = map[span.Span]string type PrepareRenames = map[span.Span]*source.PrepareItem type Symbols = map[span.URI][]*symbol -type SymbolInformation = map[span.Span]protocol.SymbolInformation type InlayHints = []span.Span type WorkspaceSymbols = map[WorkspaceSymbolsTestType]map[span.URI][]string type Signatures = map[span.Span]*protocol.SignatureHelp diff --git a/gopls/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go index 0a382194a47..c383777438e 100644 --- a/gopls/internal/lsp/work/diagnostics.go +++ b/gopls/internal/lsp/work/diagnostics.go @@ -11,10 +11,10 @@ import ( "path/filepath" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/span" ) From f80e98464e273cef1d9777ca1415e2a204a2edb2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 28 Sep 2022 12:35:32 -0400 Subject: [PATCH 282/723] gopls/internal/lsp/work: use the WorkFileError diagnostics source It looks like this source was added but never used. Use it. Change-Id: I19d7c49ab1fda11d078a7850a7c0c3a78133ff52 Reviewed-on: https://go-review.googlesource.com/c/tools/+/435361 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/work/diagnostics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go index c383777438e..72c06e07f24 100644 --- a/gopls/internal/lsp/work/diagnostics.go +++ b/gopls/internal/lsp/work/diagnostics.go @@ -73,7 +73,7 @@ func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh source URI: fh.URI(), Range: rng, Severity: protocol.SeverityError, - Source: source.UnknownError, // Do we need a new source for this? + Source: source.WorkFileError, Message: fmt.Sprintf("directory %v does not contain a module", use.Path), }) } From a4274a8a0e89e82c7ee0ecf3c62dfd8187e4f208 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Fri, 23 Sep 2022 14:51:39 -0400 Subject: [PATCH 283/723] gopls: add diagnostics for non-stdlib vulnerabilities Show the vulnerabilities found by the runvulncheck codelens in the go.mod file using diagnostics. This uses a similar strategy to upgrade codelenses to store the vulnerabilities in the view so the diagnostics can be calculated at a later time. Change-Id: Ie9744712d9a7f8d78cbe3b54aa4cd3a380a304bc Reviewed-on: https://go-review.googlesource.com/c/tools/+/433537 Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Suzy Mueller --- gopls/internal/lsp/cache/session.go | 2 + gopls/internal/lsp/cache/view.go | 31 +++- gopls/internal/lsp/code_action.go | 1 + gopls/internal/lsp/command.go | 11 +- gopls/internal/lsp/diagnostics.go | 16 +++ gopls/internal/lsp/mod/diagnostics.go | 79 +++++++++- gopls/internal/lsp/source/view.go | 11 ++ .../misc/testdata/vulndb/golang.org/amod.json | 34 +++++ .../misc/testdata/vulndb/golang.org/bmod.json | 18 +++ gopls/internal/regtest/misc/vuln_test.go | 135 +++++++++++++++++- 10 files changed, 321 insertions(+), 17 deletions(-) create mode 100644 gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json create mode 100644 gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 5d40ff6be55..8a45ae24af5 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -13,6 +13,7 @@ import ( "sync" "sync/atomic" + "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" @@ -238,6 +239,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, name: name, folder: folder, moduleUpgrades: map[span.URI]map[string]string{}, + vulns: map[span.URI][]command.Vuln{}, filesByURI: map[span.URI]*fileBase{}, filesByBase: map[string][]*fileBase{}, rootURI: root, diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 025419d71df..af4ab9afbbd 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -24,6 +24,7 @@ import ( "golang.org/x/mod/semver" exec "golang.org/x/sys/execabs" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/bug" @@ -60,6 +61,8 @@ type View struct { // Each modfile has a map of module name to upgrade version. moduleUpgrades map[span.URI]map[string]string + vulns map[span.URI][]command.Vuln + // keep track of files by uri and by basename, a single file may be mapped // to multiple uris, and the same basename may map to multiple files filesByURI map[span.URI]*fileBase @@ -1001,18 +1004,18 @@ func (v *View) IsGoPrivatePath(target string) bool { return globsMatchPath(v.goprivate, target) } -func (v *View) ModuleUpgrades(uri span.URI) map[string]string { +func (v *View) ModuleUpgrades(modfile span.URI) map[string]string { v.mu.Lock() defer v.mu.Unlock() upgrades := map[string]string{} - for mod, ver := range v.moduleUpgrades[uri] { + for mod, ver := range v.moduleUpgrades[modfile] { upgrades[mod] = ver } return upgrades } -func (v *View) RegisterModuleUpgrades(uri span.URI, upgrades map[string]string) { +func (v *View) RegisterModuleUpgrades(modfile span.URI, upgrades map[string]string) { // Return early if there are no upgrades. if len(upgrades) == 0 { return @@ -1021,10 +1024,10 @@ func (v *View) RegisterModuleUpgrades(uri span.URI, upgrades map[string]string) v.mu.Lock() defer v.mu.Unlock() - m := v.moduleUpgrades[uri] + m := v.moduleUpgrades[modfile] if m == nil { m = make(map[string]string) - v.moduleUpgrades[uri] = m + v.moduleUpgrades[modfile] = m } for mod, ver := range upgrades { m[mod] = ver @@ -1035,7 +1038,23 @@ func (v *View) ClearModuleUpgrades(modfile span.URI) { v.mu.Lock() defer v.mu.Unlock() - v.moduleUpgrades[modfile] = nil + delete(v.moduleUpgrades, modfile) +} + +func (v *View) Vulnerabilities(modfile span.URI) []command.Vuln { + v.mu.Lock() + defer v.mu.Unlock() + + vulns := make([]command.Vuln, len(v.vulns[modfile])) + copy(vulns, v.vulns[modfile]) + return vulns +} + +func (v *View) SetVulnerabilities(modfile span.URI, vulns []command.Vuln) { + v.mu.Lock() + defer v.mu.Unlock() + + v.vulns[modfile] = vulns } // Copied from diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index dea83f5bdd6..11013b5bb19 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -81,6 +81,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara if err != nil { return nil, err } + // TODO(suzmue): get upgrades code actions from vulnerabilities. quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, append(diags, udiags...)) if err != nil { return nil, err diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 811307c1ff6..5ded3467602 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -207,10 +207,12 @@ func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, uri command. forURI: uri.URI, }, func(ctx context.Context, deps commandDeps) error { deps.snapshot.View().ClearModuleUpgrades(uri.URI.SpanURI()) - // Clear all diagnostics coming from the upgrade check source. + deps.snapshot.View().SetVulnerabilities(uri.URI.SpanURI(), nil) + // Clear all diagnostics coming from the upgrade check source and vulncheck. // This will clear the diagnostics in all go.mod files, but they // will be re-calculated when the snapshot is diagnosed again. c.s.clearDiagnosticSource(modCheckUpgradesSource) + c.s.clearDiagnosticSource(modVulncheckSource) // Re-diagnose the snapshot to remove the diagnostics. c.s.diagnoseSnapshot(deps.snapshot, nil, false) @@ -889,10 +891,9 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc return fmt.Errorf("failed to parse govulncheck output: %v", err) } - // TODO(jamalc,suzmue): convert the results to diagnostics & code actions. - // Or should we just write to a file (*.vulncheck.json) or text format - // and send "Show Document" request? If *.vulncheck.json is open, - // VSCode Go extension will open its custom editor. + deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), vulns.Vuln) + c.s.diagnoseSnapshot(deps.snapshot, nil, false) + set := make(map[string]bool) for _, v := range vulns.Vuln { if len(v.CallStackSummaries) > 0 { diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index c00eb4206eb..3efcaffcda4 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -38,6 +38,7 @@ const ( orphanedSource workSource modCheckUpgradesSource + modVulncheckSource ) // A diagnosticReport holds results for a single diagnostic source. @@ -256,6 +257,21 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn } s.storeDiagnostics(snapshot, id.URI, modCheckUpgradesSource, diags) } + vulnerabilityReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) + if ctx.Err() != nil { + log.Trace.Log(ctx, "diagnose cancelled") + return + } + if vulnErr != nil { + event.Error(ctx, "warning: checking vulnerabilities", vulnErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID())) + } + for id, diags := range vulnerabilityReports { + if id.URI == "" { + event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) + continue + } + s.storeDiagnostics(snapshot, id.URI, modVulncheckSource, diags) + } // Diagnose the go.work file, if it exists. workReports, workErr := work.Diagnostics(ctx, snapshot) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 1efbfdd7de2..0405fc76876 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -10,6 +10,7 @@ import ( "context" "fmt" + "golang.org/x/mod/semver" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" @@ -17,6 +18,7 @@ import ( "golang.org/x/tools/internal/event/tag" ) +// Diagnostics returns diagnostics for the modules in the workspace. func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { ctx, done := event.Start(ctx, "mod.Diagnostics", tag.Snapshot.Of(snapshot.ID())) defer done() @@ -24,6 +26,8 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.Vers return collectDiagnostics(ctx, snapshot, ModDiagnostics) } +// UpgradeDiagnostics returns upgrade diagnostics for the modules in the +// workspace with known upgrades. func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", tag.Snapshot.Of(snapshot.ID())) defer done() @@ -31,6 +35,15 @@ func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[sour return collectDiagnostics(ctx, snapshot, ModUpgradeDiagnostics) } +// VulnerabilityDiagnostics returns vulnerability diagnostics for the active modules in the +// workspace with known vulnerabilites. +func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { + ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", tag.Snapshot.Of(snapshot.ID())) + defer done() + + return collectDiagnostics(ctx, snapshot, ModVulnerabilityDiagnostics) +} + func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { reports := make(map[source.VersionedFileIdentity][]*source.Diagnostic) for _, uri := range snapshot.ModFiles() { @@ -103,10 +116,12 @@ func ModDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.Fil func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (upgradeDiagnostics []*source.Diagnostic, err error) { pm, err := snapshot.ParseMod(ctx, fh) if err != nil { - if pm == nil || len(pm.ParseErrors) == 0 { - return nil, err + // Don't return an error if there are parse error diagnostics to be shown, but also do not + // continue since we won't be able to show the upgrade diagnostics. + if pm != nil && len(pm.ParseErrors) != 0 { + return nil, nil } - return nil, nil + return nil, err } upgrades := snapshot.View().ModuleUpgrades(fh.URI()) @@ -141,3 +156,61 @@ func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh sou return upgradeDiagnostics, nil } + +// ModVulnerabilityDiagnostics adds diagnostics for vulnerabilities in individual modules +// if the vulnerability is recorded in the view. +func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (vulnDiagnostics []*source.Diagnostic, err error) { + pm, err := snapshot.ParseMod(ctx, fh) + if err != nil { + // Don't return an error if there are parse error diagnostics to be shown, but also do not + // continue since we won't be able to show the vulnerability diagnostics. + if pm != nil && len(pm.ParseErrors) != 0 { + return nil, nil + } + return nil, err + } + + vs := snapshot.View().Vulnerabilities(fh.URI()) + // TODO(suzmue): should we just store the vulnerabilities like this? + vulns := make(map[string][]command.Vuln) + for _, v := range vs { + vulns[v.ModPath] = append(vulns[v.ModPath], v) + } + + for _, req := range pm.File.Require { + vulnList, ok := vulns[req.Mod.Path] + if !ok { + continue + } + rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End) + if err != nil { + return nil, err + } + for _, v := range vulnList { + // Only show the diagnostic if the vulnerability was calculated + // for the module at the current version. + if semver.Compare(req.Mod.Version, v.CurrentVersion) != 0 { + continue + } + + severity := protocol.SeverityInformation + if len(v.CallStacks) > 0 { + severity = protocol.SeverityWarning + } + + vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: severity, + Source: source.Vulncheck, + Code: v.ID, + CodeHref: v.URL, + // TODO(suzmue): replace the newlines in v.Details to allow the editor to handle formatting. + Message: v.Details, + }) + } + + } + + return vulnDiagnostics, nil +} diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 2565087336b..8b1dbdbcf39 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -21,6 +21,7 @@ import ( "golang.org/x/mod/module" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" @@ -269,6 +270,15 @@ type View interface { // ClearModuleUpgrades clears all upgrades for the modules in modfile. ClearModuleUpgrades(modfile span.URI) + // Vulnerabilites returns known vulnerabilities for the given modfile. + // TODO(suzmue): replace command.Vuln with a different type, maybe + // https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary? + Vulnerabilities(modfile span.URI) []command.Vuln + + // SetVulnerabilities resets the list of vulnerabilites that exists for the given modules + // required by modfile. + SetVulnerabilities(modfile span.URI, vulnerabilities []command.Vuln) + // FileKind returns the type of a file FileKind(FileHandle) FileKind } @@ -638,6 +648,7 @@ const ( ModTidyError DiagnosticSource = "go mod tidy" OptimizationDetailsError DiagnosticSource = "optimizer details" UpgradeNotification DiagnosticSource = "upgrade available" + Vulncheck DiagnosticSource = "govulncheck" TemplateError DiagnosticSource = "template" WorkFileError DiagnosticSource = "go.work file" ) diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json new file mode 100644 index 00000000000..b6790bf29ee --- /dev/null +++ b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json @@ -0,0 +1,34 @@ +[ + { + "id":"GO-2022-01", + "details": "vuln in amod", + "affected":[ + { + "package":{"name":"golang.org/amod","ecosystem":"Go"}, + "ranges":[{"type":"SEMVER","events":[{"introduced":"1.0.0"},{"fixed":"1.0.4"}, {"introduced": "1.1.2"}]}], + "ecosystem_specific":{ + "imports":[{"path":"golang.org/amod/avuln","symbols":["VulnData.Vuln1", "VulnData.Vuln2"]}] + } + } + ], + "references":[ + {"type":"href","url":"pkg.go.dev/vuln/GO-2022-01"} + ] + }, + { + "id":"GO-2022-03", + "details": "unaffecting vulnerability", + "affected":[ + { + "package":{"name":"golang.org/amod","ecosystem":"Go"}, + "ranges":[{"type":"SEMVER","events":[{"introduced":"1.0.0"},{"fixed":"1.0.4"}]}], + "ecosystem_specific":{ + "imports":[{"path":"golang.org/amod/avuln","symbols":["nonExisting"]}] + } + } + ], + "references":[ + {"type":"href","url":"pkg.go.dev/vuln/GO-2022-03"} + ] + } +] \ No newline at end of file diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json new file mode 100644 index 00000000000..b08b20d256b --- /dev/null +++ b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json @@ -0,0 +1,18 @@ +[ + { + "id":"GO-2022-99", + "details": "vuln in bmod", + "affected":[ + { + "package":{"name":"golang.org/bmod","ecosystem":"Go"}, + "ranges":[{"type":"SEMVER"}], + "ecosystem_specific":{ + "imports":[{"path":"golang.org/bmod/bvuln","symbols":["Vuln"]}] + } + } + ], + "references":[ + {"type":"href","url":"pkg.go.dev/vuln/GO-2022-03"} + ] + } +] diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index b17a279141d..f05a9594704 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -6,6 +6,7 @@ package misc import ( "os" + "path" "path/filepath" "testing" @@ -45,7 +46,7 @@ package foo }) } -func TestRunVulncheckExp(t *testing.T) { +func TestRunVulncheckExpStd(t *testing.T) { testenv.NeedsGo1Point(t, 18) const files = ` -- go.mod -- @@ -70,14 +71,14 @@ func main() { WithOptions( EnvVars{ // Let the analyzer read vulnerabilities data from the testdata/vulndb. - "GOVULNDB": "file://" + filepath.Join(cwd, "testdata", "vulndb"), + "GOVULNDB": "file://" + path.Join(filepath.ToSlash(cwd), "testdata", "vulndb"), // When fetchinging stdlib package vulnerability info, // behave as if our go version is go1.18 for this testing. // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). // See gopls/internal/vulncheck.goVersion // which follows the convention used in golang.org/x/vuln/cmd/govulncheck. "GOVERSION": "go1.18", - "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", + "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. }, Settings{ "codelenses": map[string]bool{ @@ -115,3 +116,131 @@ func main() { ) }) } + +const workspace1 = ` +-- go.mod -- +module golang.org/entry + +go 1.18 + +require golang.org/cmod v1.1.3 + +require ( + golang.org/amod v1.1.3 // indirect + golang.org/bmod v0.5.0 // indirect +) +-- go.sum -- +golang.org/amod v1.1.3 h1:E9ohW9ayc6iCFrT/VNq8tCI4hgYM+tEbo8txbtbyS3o= +golang.org/amod v1.1.3/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E= +golang.org/bmod v0.5.0 h1:0kt1EI53298Ta9w4RPEAzNUQjtDoHUA6cc0c7Rwxhlk= +golang.org/bmod v0.5.0/go.mod h1:f6o+OhF66nz/0BBc/sbCsshyPRKMSxZIlG50B/bsM4c= +golang.org/cmod v1.1.3 h1:PJ7rZFTk7xGAunBRDa0wDe7rZjZ9R/vr1S2QkVVCngQ= +golang.org/cmod v1.1.3/go.mod h1:eCR8dnmvLYQomdeAZRCPgS5JJihXtqOQrpEkNj5feQA= +-- x/x.go -- +package x + +import ( + "golang.org/cmod/c" + "golang.org/entry/y" +) + +func X() { + c.C1().Vuln1() // vuln use: X -> Vuln1 +} + +func CallY() { + y.Y() // vuln use: CallY -> y.Y -> bvuln.Vuln +} + +-- y/y.go -- +package y + +import "golang.org/cmod/c" + +func Y() { + c.C2()() // vuln use: Y -> bvuln.Vuln +} +` + +const proxy1 = ` +-- golang.org/cmod@v1.1.3/go.mod -- +module golang.org/cmod + +go 1.12 +-- golang.org/cmod@v1.1.3/c/c.go -- +package c + +import ( + "golang.org/amod/avuln" + "golang.org/bmod/bvuln" +) + +type I interface { + Vuln1() +} + +func C1() I { + v := avuln.VulnData{} + v.Vuln2() // vuln use + return v +} + +func C2() func() { + return bvuln.Vuln +} +-- golang.org/amod@v1.1.3/go.mod -- +module golang.org/amod + +go 1.14 +-- golang.org/amod@v1.1.3/avuln/avuln.go -- +package avuln + +type VulnData struct {} +func (v VulnData) Vuln1() {} +func (v VulnData) Vuln2() {} +-- golang.org/bmod@v0.5.0/go.mod -- +module golang.org/bmod + +go 1.14 +-- golang.org/bmod@v0.5.0/bvuln/bvuln.go -- +package bvuln + +func Vuln() { + // something evil +} +` + +func TestRunVulncheckExp(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + + cwd, _ := os.Getwd() + WithOptions( + ProxyFiles(proxy1), + EnvVars{ + // Let the analyzer read vulnerabilities data from the testdata/vulndb. + "GOVULNDB": "file://" + path.Join(filepath.ToSlash(cwd), "testdata", "vulndb"), + // When fetching stdlib package vulnerability info, + // behave as if our go version is go1.18 for this testing. + // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). + // See gopls/internal/vulncheck.goVersion + // which follows the convention used in golang.org/x/vuln/cmd/govulncheck. + "GOVERSION": "go1.18", + "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. + }, + Settings{ + "codelenses": map[string]bool{ + "run_vulncheck_exp": true, + }, + }, + ).Run(t, workspace1, func(t *testing.T, env *Env) { + env.OpenFile("go.mod") + env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp) + + env.Await( + CompletedWork("govulncheck", 1, true), + ShownMessage("Found"), + env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "vuln in amod"), + env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/bmod`, "vuln in bmod"), + ) + }) +} From 3db607bf98f37c89113264ea4c416d7696fa0aea Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 28 Sep 2022 16:59:29 -0400 Subject: [PATCH 284/723] gopls/internal/lsp/cache: remove "discovered missing identifiers" log It is noisy and not useful. Change-Id: I9576e921f6dd177dc573dbf1b10c1701eda104d8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/436215 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/cache/check.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 3974f516f88..7058394f873 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -347,7 +347,6 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF // time keeping those names. missing, unexpected := filter.ProcessErrors(pkg.typeErrors) if len(unexpected) == 0 && len(missing) != 0 { - event.Log(ctx, fmt.Sprintf("discovered missing identifiers: %v", missing), tag.Package.Of(string(m.ID))) pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, filter) if err != nil { return nil, err From 199742a5a6ce5eea240b95050127c1b464df7368 Mon Sep 17 00:00:00 2001 From: Zvonimir Pavlinovic Date: Fri, 23 Sep 2022 15:41:06 -0700 Subject: [PATCH 285/723] go/analysis/passes/printf: check for nil scope of instance methods Fixes golang/go#55350 Change-Id: I28784b42de72e9d489e40d546e171af23b9e5c13 Reviewed-on: https://go-review.googlesource.com/c/tools/+/433755 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Tim King Run-TryBot: Zvonimir Pavlinovic Reviewed-by: Robert Findley --- go/analysis/passes/printf/printf.go | 9 ++++++-- .../testdata/src/typeparams/diagnostics.go | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 19ca4527af2..3ac4fcaa28e 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -945,11 +945,16 @@ func recursiveStringer(pass *analysis.Pass, e ast.Expr) (string, bool) { return "", false } + // inScope returns true if e is in the scope of f. + inScope := func(e ast.Expr, f *types.Func) bool { + return f.Scope() != nil && f.Scope().Contains(e.Pos()) + } + // Is the expression e within the body of that String or Error method? var method *types.Func - if strOk && strMethod.Pkg() == pass.Pkg && strMethod.Scope().Contains(e.Pos()) { + if strOk && strMethod.Pkg() == pass.Pkg && inScope(e, strMethod) { method = strMethod - } else if errOk && errMethod.Pkg() == pass.Pkg && errMethod.Scope().Contains(e.Pos()) { + } else if errOk && errMethod.Pkg() == pass.Pkg && inScope(e, errMethod) { method = errMethod } else { return "", false diff --git a/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go b/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go index 76a9a205a70..c4d7e530d93 100644 --- a/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go +++ b/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go @@ -121,3 +121,25 @@ func TestTermReduction[T1 interface{ ~int | string }, T2 interface { fmt.Printf("%d", t2) fmt.Printf("%s", t2) // want "wrong type.*contains typeparams.myInt" } + +type U[T any] struct{} + +func (u U[T]) String() string { + fmt.Println(u) // want `fmt.Println arg u causes recursive call to \(typeparams.U\[T\]\).String method` + return "" +} + +type S[T comparable] struct { + t T +} + +func (s S[T]) String() T { + fmt.Println(s) // Not flagged. We currently do not consider String() T to implement fmt.Stringer (see #55928). + return s.t +} + +func TestInstanceStringer() { + // Tests String method with nil Scope (#55350) + fmt.Println(&S[string]{}) + fmt.Println(&U[string]{}) +} From 27641fbc7c81239bb0d55ca26d6364b6ca02676b Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 28 Sep 2022 17:54:14 -0400 Subject: [PATCH 286/723] gopls: suggest upgrading to fixed version for vulncheck diagnostics If there is a fixed version of the module with a vulnerability, provide a code action to upgrade to that version. Change-Id: I2e0d72e7a86dc097f139d60893c204d1ec55dad1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/436216 Run-TryBot: Suzy Mueller TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/code_action.go | 11 ++-- gopls/internal/lsp/mod/diagnostics.go | 19 ++++++- .../misc/testdata/vulndb/golang.org/amod.json | 2 +- gopls/internal/regtest/misc/vuln_test.go | 50 ++++++++++++++++--- 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 11013b5bb19..e1f0dc23c99 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -81,8 +81,12 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara if err != nil { return nil, err } - // TODO(suzmue): get upgrades code actions from vulnerabilities. - quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, append(diags, udiags...)) + vdiags, err := mod.ModVulnerabilityDiagnostics(ctx, snapshot, fh) + if err != nil { + return nil, err + } + // TODO(suzmue): Consider deduping upgrades from ModUpgradeDiagnostics and ModVulnerabilityDiagnostics. + quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, append(append(diags, udiags...), vdiags...)) if err != nil { return nil, err } @@ -426,7 +430,8 @@ func codeActionsForDiagnostic(ctx context.Context, snapshot source.Snapshot, sd } func sameDiagnostic(pd protocol.Diagnostic, sd *source.Diagnostic) bool { - return pd.Message == sd.Message && protocol.CompareRange(pd.Range, sd.Range) == 0 && pd.Source == string(sd.Source) + return pd.Message == strings.TrimSpace(sd.Message) && // extra space may have been trimmed when converting to protocol.Diagnostic + protocol.CompareRange(pd.Range, sd.Range) == 0 && pd.Source == string(sd.Source) } func goTest(ctx context.Context, snapshot source.Snapshot, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) { diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 0405fc76876..af5009e7fb4 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -193,6 +193,22 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, continue } + // Upgrade to the exact version we offer the user, not the most recent. + // TODO(suzmue): Add an upgrade for module@latest. + var fixes []source.SuggestedFix + if fixedVersion := v.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { + title := fmt.Sprintf("Upgrade to %v", fixedVersion) + cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ + URI: protocol.URIFromSpanURI(fh.URI()), + AddRequire: false, + GoCmdArgs: []string{req.Mod.Path + "@" + fixedVersion}, + }) + if err != nil { + return nil, err + } + fixes = append(fixes, source.SuggestedFixFromCommand(cmd, protocol.QuickFix)) + } + severity := protocol.SeverityInformation if len(v.CallStacks) > 0 { severity = protocol.SeverityWarning @@ -206,7 +222,8 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, Code: v.ID, CodeHref: v.URL, // TODO(suzmue): replace the newlines in v.Details to allow the editor to handle formatting. - Message: v.Details, + Message: v.Details, + SuggestedFixes: fixes, }) } diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json index b6790bf29ee..59808ed5fff 100644 --- a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json +++ b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json @@ -21,7 +21,7 @@ "affected":[ { "package":{"name":"golang.org/amod","ecosystem":"Go"}, - "ranges":[{"type":"SEMVER","events":[{"introduced":"1.0.0"},{"fixed":"1.0.4"}]}], + "ranges":[{"type":"SEMVER","events":[{"introduced":"1.0.0"},{"fixed":"1.0.6"}]}], "ecosystem_specific":{ "imports":[{"path":"golang.org/amod/avuln","symbols":["nonExisting"]}] } diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index f05a9594704..c13acf82a24 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -13,6 +13,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/testenv" ) @@ -126,12 +127,12 @@ go 1.18 require golang.org/cmod v1.1.3 require ( - golang.org/amod v1.1.3 // indirect + golang.org/amod v1.0.0 // indirect golang.org/bmod v0.5.0 // indirect ) -- go.sum -- -golang.org/amod v1.1.3 h1:E9ohW9ayc6iCFrT/VNq8tCI4hgYM+tEbo8txbtbyS3o= -golang.org/amod v1.1.3/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E= +golang.org/amod v1.0.0 h1:EUQOI2m5NhQZijXZf8WimSnnWubaFNrrKUH/PopTN8k= +golang.org/amod v1.0.0/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E= golang.org/bmod v0.5.0 h1:0kt1EI53298Ta9w4RPEAzNUQjtDoHUA6cc0c7Rwxhlk= golang.org/bmod v0.5.0/go.mod h1:f6o+OhF66nz/0BBc/sbCsshyPRKMSxZIlG50B/bsM4c= golang.org/cmod v1.1.3 h1:PJ7rZFTk7xGAunBRDa0wDe7rZjZ9R/vr1S2QkVVCngQ= @@ -188,16 +189,27 @@ func C1() I { func C2() func() { return bvuln.Vuln } --- golang.org/amod@v1.1.3/go.mod -- +-- golang.org/amod@v1.0.0/go.mod -- module golang.org/amod go 1.14 --- golang.org/amod@v1.1.3/avuln/avuln.go -- +-- golang.org/amod@v1.0.0/avuln/avuln.go -- package avuln type VulnData struct {} func (v VulnData) Vuln1() {} func (v VulnData) Vuln2() {} +-- golang.org/amod@v1.0.4/go.mod -- +module golang.org/amod + +go 1.14 +-- golang.org/amod@v1.0.4/avuln/avuln.go -- +package avuln + +type VulnData struct {} +func (v VulnData) Vuln1() {} +func (v VulnData) Vuln2() {} + -- golang.org/bmod@v0.5.0/go.mod -- module golang.org/bmod @@ -234,13 +246,35 @@ func TestRunVulncheckExp(t *testing.T) { }, ).Run(t, workspace1, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp) + env.ExecuteCodeLensCommand("go.mod", command.Tidy) + env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp) + d := &protocol.PublishDiagnosticsParams{} env.Await( CompletedWork("govulncheck", 1, true), ShownMessage("Found"), - env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "vuln in amod"), - env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/bmod`, "vuln in bmod"), + OnceMet( + env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "vuln in amod"), + env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/bmod`, "vuln in bmod"), + ReadDiagnostics("go.mod", d), + ), ) + + env.ApplyQuickFixes("go.mod", d.Diagnostics) + env.Await(env.DoneWithChangeWatchedFiles()) + wantGoMod := `module golang.org/entry + +go 1.18 + +require golang.org/cmod v1.1.3 + +require ( + golang.org/amod v1.0.4 // indirect + golang.org/bmod v0.5.0 // indirect +) +` + if got := env.Editor.BufferText("go.mod"); got != wantGoMod { + t.Fatalf("go.mod vulncheck fix failed:\n%s", compare.Text(wantGoMod, got)) + } }) } From d49f960b6a980c6c48994c7356e751978ce08fb6 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 31 Aug 2022 14:41:23 -0400 Subject: [PATCH 287/723] internal/lsp/cache: report analysis panics during testing Report a bug when an analysis dependency is found to be neither "vertical" (for facts) or "horizontal" (for results). This has been observed in practise: the lookup from a package ID to a package object is not consistent. The bug report is suppressed for command-line-arguments packages to see whether it occurs on ordinary packages too. Also, add disabled logic to disable recovery and dump all goroutines, for temporary use when debugging. Unrelated things found during debugging: - simplify package key used in analysis cache: the mode is always the same constant. - move TypeErrorAnalyzers logic closer to where needed. Updates golang/go#54655 Updates golang/go#54762 Change-Id: I0c2afd9ddd4fcc21748a03f8fa0769a2de09a56f Reviewed-on: https://go-review.googlesource.com/c/tools/+/426018 Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/analysis.go | 74 +++++++++++++++++++--------- gopls/internal/lsp/cache/maps.go | 2 +- gopls/internal/lsp/cache/snapshot.go | 2 +- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 0b5ce8995ec..e7f2d415421 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -10,14 +10,16 @@ import ( "go/ast" "go/types" "reflect" + "runtime/debug" "sync" "golang.org/x/sync/errgroup" "golang.org/x/tools/go/analysis" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/span" ) @@ -60,7 +62,7 @@ func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.A } type actionKey struct { - pkg packageKey + pkgid PackageID analyzer *analysis.Analyzer } @@ -102,11 +104,7 @@ type packageFactKey struct { } func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.Analyzer) (*actionHandle, error) { - const mode = source.ParseFull - key := actionKey{ - pkg: packageKey{id: id, mode: mode}, - analyzer: a, - } + key := actionKey{id, a} s.mu.Lock() entry, hit := s.actions.Get(key) @@ -126,7 +124,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A // use of concurrency would lead to an exponential amount of duplicated // work. We should instead use an atomically updated future cache // and a parallel graph traversal. - ph, err := s.buildPackageHandle(ctx, id, mode) + ph, err := s.buildPackageHandle(ctx, id, source.ParseFull) if err != nil { return nil, err } @@ -138,6 +136,8 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A // Add a dependency on each required analyzer. var deps []*actionHandle for _, req := range a.Requires { + // TODO(adonovan): opt: there's no need to repeat the package-handle + // portion of the recursion here, since we have the pkg already. reqActionHandle, err := s.actionHandle(ctx, id, req) if err != nil { return nil, err @@ -227,7 +227,8 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a // in-memory outputs of prerequisite analyzers // become inputs to this analysis pass. inputs[dep.analyzer] = data.result - } else if dep.analyzer == analyzer { // (always true) + + } else if dep.analyzer == analyzer { // Same analysis, different package (vertical edge): // serialized facts produced by prerequisite analysis // become available to this analysis pass. @@ -246,12 +247,34 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a // to prevent side channels. packageFacts[key] = fact } + + } else { + // Edge is neither vertical nor horizontal. + // This should never happen, yet an assertion here was + // observed to fail due to an edge (bools, p) -> (inspector, p') + // where p and p' are distinct packages with the + // same ID ("command-line-arguments:file=.../main.go"). + // + // It is not yet clear whether the command-line-arguments + // package is significant, but it is clear that package + // loading (the mapping from ID to *pkg) is inconsistent + // within a single graph. + + // Use the bug package so that we detect whether our tests + // discover this problem in regular packages. + // For command-line-arguments we quietly abort the analysis + // for now since we already know there is a bug. + errorf := bug.Errorf // report this discovery + if source.IsCommandLineArguments(pkg.ID()) { + errorf = fmt.Errorf // suppress reporting + } + return errorf("internal error: unexpected analysis dependency %s@%s -> %s", analyzer.Name, pkg.ID(), dep) } return nil }) } if err := g.Wait(); err != nil { - return nil, err // e.g. cancelled + return nil, err // cancelled, or dependency failed } // Now run the (pkg, analyzer) analysis. @@ -338,13 +361,19 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a var result interface{} var err error func() { - defer func() { - if r := recover(); r != nil { - // TODO(adonovan): use bug.Errorf here so that we - // detect crashes covered by our test suite. - err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) - } - }() + // Set this flag temporarily when debugging crashes. + // See https://github.com/golang/go/issues/54762. + const norecover = false + if norecover { + debug.SetTraceback("all") // show all goroutines + } else { + defer func() { + if r := recover(); r != nil { + // Use bug.Errorf so that we detect panics during testing. + err = bug.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) + } + }() + } result, err = pass.Analyzer.Run(pass) }() if err != nil { @@ -414,13 +443,14 @@ func factType(fact analysis.Fact) reflect.Type { func (s *snapshot) DiagnosePackage(ctx context.Context, spkg source.Package) (map[span.URI][]*source.Diagnostic, error) { pkg := spkg.(*pkg) - // Apply type error analyzers. They augment type error diagnostics with their own fixes. - var analyzers []*source.Analyzer - for _, a := range s.View().Options().TypeErrorAnalyzers { - analyzers = append(analyzers, a) - } var errorAnalyzerDiag []*source.Diagnostic if pkg.HasTypeErrors() { + // Apply type error analyzers. + // They augment type error diagnostics with their own fixes. + var analyzers []*source.Analyzer + for _, a := range s.View().Options().TypeErrorAnalyzers { + analyzers = append(analyzers, a) + } var err error errorAnalyzerDiag, err = s.Analyze(ctx, pkg.ID(), analyzers) if err != nil { diff --git a/gopls/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go index 0493398798d..d3fe4e45e5f 100644 --- a/gopls/internal/lsp/cache/maps.go +++ b/gopls/internal/lsp/cache/maps.go @@ -212,5 +212,5 @@ func actionKeyLessInterface(a, b interface{}) bool { if x.analyzer.Name != y.analyzer.Name { return x.analyzer.Name < y.analyzer.Name } - return packageKeyLess(x.pkg, y.pkg) + return x.pkgid < y.pkgid } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 4398142eb04..d1d2b1fb7b9 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1880,7 +1880,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC var actionsToDelete []actionKey s.actions.Range(func(k, _ interface{}) { key := k.(actionKey) - if _, ok := idsToInvalidate[key.pkg.id]; ok { + if _, ok := idsToInvalidate[key.pkgid]; ok { actionsToDelete = append(actionsToDelete, key) } }) From b180effdbe73f40787505b5bed519d33cff9a96c Mon Sep 17 00:00:00 2001 From: Lasse Folger Date: Tue, 27 Sep 2022 16:09:31 +0200 Subject: [PATCH 288/723] x/tools/go/analysis: extend json output by SuggestedFixes + The JSON output now contains SuggestedFixes from the Diagnostics. The edits are encoded as replacements for ranges defined by 0 based byte start and end indicies into the original file. + This change also exports the structs that are used for JSON encoding and thus documents the JSON schema. Fixes golang/go#55138 Change-Id: I93411626279a866de2986ff78d93775a86ae2213 Reviewed-on: https://go-review.googlesource.com/c/tools/+/435255 Reviewed-by: Tim King TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Cherry Mui --- go/analysis/internal/analysisflags/flags.go | 68 ++++++++++++++++----- go/analysis/unitchecker/unitchecker_test.go | 57 ++++++++++++++++- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go index 4b7be2d1f5f..2ea630608c4 100644 --- a/go/analysis/internal/analysisflags/flags.go +++ b/go/analysis/internal/analysisflags/flags.go @@ -339,9 +339,38 @@ func PrintPlain(fset *token.FileSet, diag analysis.Diagnostic) { } // A JSONTree is a mapping from package ID to analysis name to result. -// Each result is either a jsonError or a list of jsonDiagnostic. +// Each result is either a jsonError or a list of JSONDiagnostic. type JSONTree map[string]map[string]interface{} +// A TextEdit describes the replacement of a portion of a file. +// Start and End are zero-based half-open indices into the original byte +// sequence of the file, and New is the new text. +type JSONTextEdit struct { + Filename string `json:"filename"` + Start int `json:"start"` + End int `json:"end"` + New string `json:"new"` +} + +// A JSONSuggestedFix describes an edit that should be applied as a whole or not +// at all. It might contain multiple TextEdits/text_edits if the SuggestedFix +// consists of multiple non-contiguous edits. +type JSONSuggestedFix struct { + Message string `json:"message"` + Edits []JSONTextEdit `json:"edits"` +} + +// A JSONDiagnostic can be used to encode and decode analysis.Diagnostics to and +// from JSON. +// TODO(matloob): Should the JSON diagnostics contain ranges? +// If so, how should they be formatted? +type JSONDiagnostic struct { + Category string `json:"category,omitempty"` + Posn string `json:"posn"` + Message string `json:"message"` + SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"` +} + // Add adds the result of analysis 'name' on package 'id'. // The result is either a list of diagnostics or an error. func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) { @@ -352,20 +381,31 @@ func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis. } v = jsonError{err.Error()} } else if len(diags) > 0 { - type jsonDiagnostic struct { - Category string `json:"category,omitempty"` - Posn string `json:"posn"` - Message string `json:"message"` - } - var diagnostics []jsonDiagnostic - // TODO(matloob): Should the JSON diagnostics contain ranges? - // If so, how should they be formatted? + diagnostics := make([]JSONDiagnostic, 0, len(diags)) for _, f := range diags { - diagnostics = append(diagnostics, jsonDiagnostic{ - Category: f.Category, - Posn: fset.Position(f.Pos).String(), - Message: f.Message, - }) + var fixes []JSONSuggestedFix + for _, fix := range f.SuggestedFixes { + var edits []JSONTextEdit + for _, edit := range fix.TextEdits { + edits = append(edits, JSONTextEdit{ + Filename: fset.Position(edit.Pos).Filename, + Start: fset.Position(edit.Pos).Offset, + End: fset.Position(edit.End).Offset, + New: string(edit.NewText), + }) + } + fixes = append(fixes, JSONSuggestedFix{ + Message: fix.Message, + Edits: edits, + }) + } + jdiag := JSONDiagnostic{ + Category: f.Category, + Posn: fset.Position(f.Pos).String(), + Message: f.Message, + SuggestedFixes: fixes, + } + diagnostics = append(diagnostics, jdiag) } v = diagnostics } diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go index 7e5b848de86..a2393b7265f 100644 --- a/go/analysis/unitchecker/unitchecker_test.go +++ b/go/analysis/unitchecker/unitchecker_test.go @@ -20,6 +20,7 @@ import ( "strings" "testing" + "golang.org/x/tools/go/analysis/passes/assign" "golang.org/x/tools/go/analysis/passes/findcall" "golang.org/x/tools/go/analysis/passes/printf" "golang.org/x/tools/go/analysis/unitchecker" @@ -41,6 +42,7 @@ func main() { unitchecker.Main( findcall.Analyzer, printf.Analyzer, + assign.Analyzer, ) } @@ -74,6 +76,13 @@ func _() { } func MyFunc123() {} +`, + "c/c.go": `package c + +func _() { + i := 5 + i = i +} `, }}}) defer exported.Cleanup() @@ -84,6 +93,9 @@ func MyFunc123() {} const wantB = `# golang.org/fake/b ([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?b/b.go:6:13: call of MyFunc123\(...\) ([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?b/b.go:7:11: call of MyFunc123\(...\) +` + const wantC = `# golang.org/fake/c +([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go:5:5: self-assignment of i to i ` const wantAJSON = `# golang.org/fake/a \{ @@ -91,13 +103,50 @@ func MyFunc123() {} "findcall": \[ \{ "posn": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?a/a.go:4:11", - "message": "call of MyFunc123\(...\)" + "message": "call of MyFunc123\(...\)", + "suggested_fixes": \[ + \{ + "message": "Add '_TEST_'", + "edits": \[ + \{ + "filename": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?a/a.go", + "start": 32, + "end": 32, + "new": "_TEST_" + \} + \] + \} + \] + \} + \] + \} +\} +` + const wantCJSON = `# golang.org/fake/c +\{ + "golang.org/fake/c": \{ + "assign": \[ + \{ + "posn": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go:5:5", + "message": "self-assignment of i to i", + "suggested_fixes": \[ + \{ + "message": "Remove", + "edits": \[ + \{ + "filename": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go", + "start": 37, + "end": 42, + "new": "" + \} + \] + \} + \] \} \] \} \} ` - for _, test := range []struct { args string wantOut string @@ -105,8 +154,10 @@ func MyFunc123() {} }{ {args: "golang.org/fake/a", wantOut: wantA, wantExit: 2}, {args: "golang.org/fake/b", wantOut: wantB, wantExit: 2}, + {args: "golang.org/fake/c", wantOut: wantC, wantExit: 2}, {args: "golang.org/fake/a golang.org/fake/b", wantOut: wantA + wantB, wantExit: 2}, {args: "-json golang.org/fake/a", wantOut: wantAJSON, wantExit: 0}, + {args: "-json golang.org/fake/c", wantOut: wantCJSON, wantExit: 0}, {args: "-c=0 golang.org/fake/a", wantOut: wantA + "4 MyFunc123\\(\\)\n", wantExit: 2}, } { cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "-findcall.name=MyFunc123") @@ -125,7 +176,7 @@ func MyFunc123() {} matched, err := regexp.Match(test.wantOut, out) if err != nil { - t.Fatal(err) + t.Fatalf("regexp.Match(<<%s>>): %v", test.wantOut, err) } if !matched { t.Errorf("%s: got <<%s>>, want match of regexp <<%s>>", test.args, out, test.wantOut) From ed98f10ce9cb536b3e5d16b7ead411eb94862470 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 28 Sep 2022 18:25:38 -0400 Subject: [PATCH 289/723] gopls: prefix vulncheck diagnostic message with title To help indicate that the diagnostic is for an error in a dependency, not in the user's code prefix the diagnostic with "[module] has a known vulnerability:" Change-Id: Ib48f2e2cb2620fc6d56c4b0fd5d125cb4789283c Reviewed-on: https://go-review.googlesource.com/c/tools/+/436217 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Suzy Mueller Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/mod/diagnostics.go | 2 +- gopls/internal/regtest/misc/vuln_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index af5009e7fb4..2b15ba7488b 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -222,7 +222,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, Code: v.ID, CodeHref: v.URL, // TODO(suzmue): replace the newlines in v.Details to allow the editor to handle formatting. - Message: v.Details, + Message: fmt.Sprintf("%s has a known vulnerability: %s", v.ModPath, v.Details), SuggestedFixes: fixes, }) } diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index c13acf82a24..b0fb1f94d06 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -254,8 +254,8 @@ func TestRunVulncheckExp(t *testing.T) { CompletedWork("govulncheck", 1, true), ShownMessage("Found"), OnceMet( - env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "vuln in amod"), - env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/bmod`, "vuln in bmod"), + env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "golang.org/amod has a known vulnerability: vuln in amod"), + env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/bmod`, "golang.org/bmod has a known vulnerability: vuln in bmod"), ReadDiagnostics("go.mod", d), ), ) From 3e0355b8981a315051cde0dd068026edde2bec94 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 29 Sep 2022 17:11:53 -0400 Subject: [PATCH 290/723] gopls/.../fillstruct: support generic types The code that generates a name for a struct type, and a default value for a struct field, now supports the case where either contains a type parameter. Also: - use type-checker's name for the struct type unless it is a bare struct. - populateValue: return *new(T) for a type parameter. - various minor cleanups. - various TODO comments for clarifications. - fuzzy.FindBestMatch: use strings not identifiers. Remove Find prefix (also FindMatchingIdentifiers). Fixes golang/go#54836 Change-Id: I4f6132598b4ac7e72ea1405e4a14d6a23c1eeeaa Reviewed-on: https://go-review.googlesource.com/c/tools/+/436777 Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- .../lsp/analysis/fillreturns/fillreturns.go | 21 +-- .../lsp/analysis/fillstruct/fillstruct.go | 170 ++++++++++-------- .../analysis/fillstruct/testdata/src/a/a.go | 32 ++-- .../testdata/src/typeparams/typeparams.go | 21 ++- .../lsp/testdata/fillstruct/typeparams.go | 7 +- .../testdata/fillstruct/typeparams.go.golden | 104 +++++++++-- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- internal/analysisinternal/analysis.go | 42 +++-- internal/fuzzy/matcher.go | 21 ++- 9 files changed, 263 insertions(+), 157 deletions(-) diff --git a/gopls/internal/lsp/analysis/fillreturns/fillreturns.go b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go index d71defb3b83..4415ddd66ec 100644 --- a/gopls/internal/lsp/analysis/fillreturns/fillreturns.go +++ b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go @@ -169,8 +169,7 @@ outer: } retTyps = append(retTyps, retTyp) } - matches := - analysisinternal.FindMatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg) + matches := analysisinternal.MatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg) for i, retTyp := range retTyps { var match ast.Expr var idx int @@ -192,21 +191,19 @@ outer: fixed[i] = match remaining = append(remaining[:idx], remaining[idx+1:]...) } else { - idents, ok := matches[retTyp] + names, ok := matches[retTyp] if !ok { return nil, fmt.Errorf("invalid return type: %v", retTyp) } - // Find the identifier whose name is most similar to the return type. - // If we do not find any identifier that matches the pattern, - // generate a zero value. - value := fuzzy.FindBestMatch(retTyp.String(), idents) - if value == nil { - value = analysisinternal.ZeroValue(file, pass.Pkg, retTyp) - } - if value == nil { + // Find the identifier most similar to the return type. + // If no identifier matches the pattern, generate a zero value. + if best := fuzzy.BestMatch(retTyp.String(), names); best != "" { + fixed[i] = ast.NewIdent(best) + } else if zero := analysisinternal.ZeroValue(file, pass.Pkg, retTyp); zero != nil { + fixed[i] = zero + } else { return nil, nil } - fixed[i] = value } } diff --git a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go index aaa3075f6c2..931b2190060 100644 --- a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -4,6 +4,12 @@ // Package fillstruct defines an Analyzer that automatically // fills in a struct declaration with zero value elements for each field. +// +// The analyzer's diagnostic is merely a prompt. +// The actual fix is created by a separate direct call from gopls to +// the SuggestedFixes function. +// Tests of Analyzer.Run can be found in ./testdata/src. +// Tests of the SuggestedFixes logic live in ../../testdata/fillstruct. package fillstruct import ( @@ -46,12 +52,10 @@ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { - info := pass.TypesInfo - if info == nil { - return - } expr := n.(*ast.CompositeLit) + // Find enclosing file. + // TODO(adonovan): use inspect.WithStack? var file *ast.File for _, f := range pass.Files { if f.Pos() <= expr.Pos() && expr.Pos() <= f.End() { @@ -63,65 +67,49 @@ func run(pass *analysis.Pass) (interface{}, error) { return } - typ := info.TypeOf(expr) + typ := pass.TypesInfo.TypeOf(expr) if typ == nil { return } // Find reference to the type declaration of the struct being initialized. - for { - p, ok := typ.Underlying().(*types.Pointer) - if !ok { - break - } - typ = p.Elem() - } - typ = typ.Underlying() - - obj, ok := typ.(*types.Struct) + typ = deref(typ) + tStruct, ok := typ.Underlying().(*types.Struct) if !ok { return } - fieldCount := obj.NumFields() + // Inv: typ is the possibly-named struct type. + + fieldCount := tStruct.NumFields() // Skip any struct that is already populated or that has no fields. if fieldCount == 0 || fieldCount == len(expr.Elts) { return } - var fillable bool + // Are any fields in need of filling? var fillableFields []string for i := 0; i < fieldCount; i++ { - field := obj.Field(i) + field := tStruct.Field(i) // Ignore fields that are not accessible in the current package. if field.Pkg() != nil && field.Pkg() != pass.Pkg && !field.Exported() { continue } - // Ignore structs containing fields that have type parameters for now. - // TODO: support type params. - if typ, ok := field.Type().(*types.Named); ok { - if tparams := typeparams.ForNamed(typ); tparams != nil && tparams.Len() > 0 { - return - } - } - if _, ok := field.Type().(*typeparams.TypeParam); ok { - return - } - fillable = true fillableFields = append(fillableFields, fmt.Sprintf("%s: %s", field.Name(), field.Type().String())) } - if !fillable { + if len(fillableFields) == 0 { return } + + // Derive a name for the struct type. var name string - switch typ := expr.Type.(type) { - case *ast.Ident: - name = typ.Name - case *ast.SelectorExpr: - name = fmt.Sprintf("%s.%s", typ.X, typ.Sel.Name) - default: + if typ != tStruct { + // named struct type (e.g. pkg.S[T]) + name = types.TypeString(typ, types.RelativeTo(pass.Pkg)) + } else { + // anonymous struct type totalFields := len(fillableFields) - maxLen := 20 + const maxLen = 20 // Find the index to cut off printing of fields. var i, fieldLen int for i = range fillableFields { @@ -145,7 +133,13 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } +// SuggestedFix computes the suggested fix for the kinds of +// diagnostics produced by the Analyzer above. func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { + if info == nil { + return nil, fmt.Errorf("nil types.Info") + } + pos := rng.Start // don't use the end // TODO(rstambler): Using ast.Inspect would probably be more efficient than @@ -162,37 +156,29 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast } } - if info == nil { - return nil, fmt.Errorf("nil types.Info") - } typ := info.TypeOf(expr) if typ == nil { return nil, fmt.Errorf("no composite literal") } // Find reference to the type declaration of the struct being initialized. - for { - p, ok := typ.Underlying().(*types.Pointer) - if !ok { - break - } - typ = p.Elem() - } - typ = typ.Underlying() - - obj, ok := typ.(*types.Struct) + typ = deref(typ) + tStruct, ok := typ.Underlying().(*types.Struct) if !ok { - return nil, fmt.Errorf("unexpected type %v (%T), expected *types.Struct", typ, typ) + return nil, fmt.Errorf("%s is not a (pointer to) struct type", + types.TypeString(typ, types.RelativeTo(pkg))) } - fieldCount := obj.NumFields() + // Inv: typ is the the possibly-named struct type. + + fieldCount := tStruct.NumFields() // Check which types have already been filled in. (we only want to fill in // the unfilled types, or else we'll blat user-supplied details) - prefilledTypes := map[string]ast.Expr{} + prefilledFields := map[string]ast.Expr{} for _, e := range expr.Elts { if kv, ok := e.(*ast.KeyValueExpr); ok { if key, ok := kv.Key.(*ast.Ident); ok { - prefilledTypes[key.Name] = kv.Value + prefilledFields[key.Name] = kv.Value } } } @@ -202,14 +188,16 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast // each field we're going to set. format.Node only cares about line // numbers, so we don't need to set columns, and each line can be // 1 byte long. + // TODO(adonovan): why is this necessary? The position information + // is going to be wrong for the existing trees in prefilledFields. + // Can't the formatter just do its best with an empty fileset? fakeFset := token.NewFileSet() tok := fakeFset.AddFile("", -1, fieldCount+2) line := 2 // account for 1-based lines and the left brace - var elts []ast.Expr var fieldTyps []types.Type for i := 0; i < fieldCount; i++ { - field := obj.Field(i) + field := tStruct.Field(i) // Ignore fields that are not accessible in the current package. if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() { fieldTyps = append(fieldTyps, nil) @@ -217,11 +205,13 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast } fieldTyps = append(fieldTyps, field.Type()) } - matches := analysisinternal.FindMatchingIdents(fieldTyps, file, rng.Start, info, pkg) + matches := analysisinternal.MatchingIdents(fieldTyps, file, rng.Start, info, pkg) + var elts []ast.Expr for i, fieldTyp := range fieldTyps { if fieldTyp == nil { - continue + continue // TODO(adonovan): is this reachable? } + fieldName := tStruct.Field(i).Name() tok.AddLine(line - 1) // add 1 byte per line if line > tok.LineCount() { @@ -232,30 +222,28 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast kv := &ast.KeyValueExpr{ Key: &ast.Ident{ NamePos: pos, - Name: obj.Field(i).Name(), + Name: fieldName, }, Colon: pos, } - if expr, ok := prefilledTypes[obj.Field(i).Name()]; ok { + if expr, ok := prefilledFields[fieldName]; ok { kv.Value = expr } else { - idents, ok := matches[fieldTyp] + names, ok := matches[fieldTyp] if !ok { return nil, fmt.Errorf("invalid struct field type: %v", fieldTyp) } - // Find the identifier whose name is most similar to the name of the field's key. - // If we do not find any identifier that matches the pattern, generate a new value. + // Find the name most similar to the field name. + // If no name matches the pattern, generate a zero value. // NOTE: We currently match on the name of the field key rather than the field type. - value := fuzzy.FindBestMatch(obj.Field(i).Name(), idents) - if value == nil { - value = populateValue(file, pkg, fieldTyp) - } - if value == nil { + if best := fuzzy.BestMatch(fieldName, names); best != "" { + kv.Value = ast.NewIdent(best) + } else if v := populateValue(file, pkg, fieldTyp); v != nil { + kv.Value = v + } else { return nil, nil } - - kv.Value = value } elts = append(elts, kv) line++ @@ -299,7 +287,7 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast } sug := indent(formatBuf.Bytes(), whitespace) - if len(prefilledTypes) > 0 { + if len(prefilledFields) > 0 { // Attempt a second pass through the formatter to line up columns. sourced, err := format.Source(sug) if err == nil { @@ -343,16 +331,12 @@ func indent(str, ind []byte) []byte { // // When the type of a struct field is a basic literal or interface, we return // default values. For other types, such as maps, slices, and channels, we create -// expressions rather than using default values. +// empty expressions such as []T{} or make(chan T) rather than using default values. // // The reasoning here is that users will call fillstruct with the intention of // initializing the struct, in which case setting these fields to nil has no effect. func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { - under := typ - if n, ok := typ.(*types.Named); ok { - under = n.Underlying() - } - switch u := under.(type) { + switch u := typ.Underlying().(type) { case *types.Basic: switch { case u.Info()&types.IsNumeric != 0: @@ -366,6 +350,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { default: panic("unknown basic type") } + case *types.Map: k := analysisinternal.TypeExpr(f, pkg, u.Key()) v := analysisinternal.TypeExpr(f, pkg, u.Elem()) @@ -388,6 +373,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { Elt: s, }, } + case *types.Array: a := analysisinternal.TypeExpr(f, pkg, u.Elem()) if a == nil { @@ -401,6 +387,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { }, }, } + case *types.Chan: v := analysisinternal.TypeExpr(f, pkg, u.Elem()) if v == nil { @@ -419,6 +406,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { }, }, } + case *types.Struct: s := analysisinternal.TypeExpr(f, pkg, typ) if s == nil { @@ -427,6 +415,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { return &ast.CompositeLit{ Type: s, } + case *types.Signature: var params []*ast.Field for i := 0; i < u.Params().Len(); i++ { @@ -464,6 +453,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { }, Body: &ast.BlockStmt{}, } + case *types.Pointer: switch u.Elem().(type) { case *types.Basic: @@ -483,8 +473,34 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { X: populateValue(f, pkg, u.Elem()), } } + case *types.Interface: + if param, ok := typ.(*typeparams.TypeParam); ok { + // *new(T) is the zero value of a type parameter T. + // TODO(adonovan): one could give a more specific zero + // value if the type has a core type that is, say, + // always a number or a pointer. See go/ssa for details. + return &ast.StarExpr{ + X: &ast.CallExpr{ + Fun: ast.NewIdent("new"), + Args: []ast.Expr{ + ast.NewIdent(param.Obj().Name()), + }, + }, + } + } + return ast.NewIdent("nil") } return nil } + +func deref(t types.Type) types.Type { + for { + ptr, ok := t.Underlying().(*types.Pointer) + if !ok { + return t + } + t = ptr.Elem() + } +} diff --git a/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go index 68560092105..9ee3860fcae 100644 --- a/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go +++ b/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go @@ -19,16 +19,16 @@ type basicStruct struct { foo int } -var _ = basicStruct{} // want "" +var _ = basicStruct{} // want `Fill basicStruct` type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} // want "" +var _ = twoArgStruct{} // want `Fill twoArgStruct` -var _ = twoArgStruct{ // want "" +var _ = twoArgStruct{ // want `Fill twoArgStruct` bar: "bar", } @@ -37,9 +37,9 @@ type nestedStruct struct { basic basicStruct } -var _ = nestedStruct{} // want "" +var _ = nestedStruct{} // want `Fill nestedStruct` -var _ = data.B{} // want "" +var _ = data.B{} // want `Fill b.B` type typedStruct struct { m map[string]int @@ -49,25 +49,25 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} // want "" +var _ = typedStruct{} // want `Fill typedStruct` type funStruct struct { fn func(i int) int } -var _ = funStruct{} // want "" +var _ = funStruct{} // want `Fill funStruct` -type funStructCompex struct { +type funStructComplex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} // want "" +var _ = funStructComplex{} // want `Fill funStructComplex` type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} // want "" +var _ = funStructEmpty{} // want `Fill funStructEmpty` type Foo struct { A int @@ -78,7 +78,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} // want "" +var _ = Bar{} // want `Fill Bar` type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -89,7 +89,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} // want "" +var _ = importedStruct{} // want `Fill importedStruct` type pointerBuiltinStruct struct { b *bool @@ -97,17 +97,17 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} // want "" +var _ = pointerBuiltinStruct{} // want `Fill pointerBuiltinStruct` var _ = []ast.BasicLit{ - {}, // want "" + {}, // want `Fill go/ast.BasicLit` } -var _ = []ast.BasicLit{{}, // want "" +var _ = []ast.BasicLit{{}, // want "go/ast.BasicLit" } type unsafeStruct struct { foo unsafe.Pointer } -var _ = unsafeStruct{} // want "" +var _ = unsafeStruct{} // want `Fill unsafeStruct` diff --git a/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go index 7972bd3e12b..46bb8ae4027 100644 --- a/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go +++ b/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go @@ -12,16 +12,16 @@ type basicStruct[T any] struct { foo T } -var _ = basicStruct[int]{} // want "" +var _ = basicStruct[int]{} // want `Fill basicStruct\[int\]` type twoArgStruct[F, B any] struct { foo F bar B } -var _ = twoArgStruct[string, int]{} // want "" +var _ = twoArgStruct[string, int]{} // want `Fill twoArgStruct\[string, int\]` -var _ = twoArgStruct[int, string]{ // want "" +var _ = twoArgStruct[int, string]{ // want `Fill twoArgStruct\[int, string\]` bar: "bar", } @@ -30,10 +30,21 @@ type nestedStruct struct { basic basicStruct[int] } -var _ = nestedStruct{} +var _ = nestedStruct{} // want "Fill nestedStruct" func _[T any]() { type S struct{ t T } - x := S{} + x := S{} // want "Fill S" _ = x } + +func Test() { + var tests = []struct { + a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p string + }{ + {}, // want "Fill anonymous struct { a: string, b: string, c: string, ... }" + } + for _, test := range tests { + _ = test + } +} diff --git a/gopls/internal/lsp/testdata/fillstruct/typeparams.go b/gopls/internal/lsp/testdata/fillstruct/typeparams.go index c60cd68ada5..c0b702f57c7 100644 --- a/gopls/internal/lsp/testdata/fillstruct/typeparams.go +++ b/gopls/internal/lsp/testdata/fillstruct/typeparams.go @@ -5,7 +5,7 @@ package fillstruct type emptyStructWithTypeParams[A any] struct{} -var _ = emptyStructWithTypeParams[int]{} +var _ = emptyStructWithTypeParams[int]{} // no suggested fix type basicStructWithTypeParams[T any] struct { foo T @@ -29,10 +29,9 @@ type nestedStructWithTypeParams struct { basic basicStructWithTypeParams[int] } -var _ = nestedStructWithTypeParams{} +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") func _[T any]() { type S struct{ t T } - x := S{} - _ = x + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden b/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden index 12e2f844444..625df7577b7 100644 --- a/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden @@ -6,7 +6,7 @@ package fillstruct type emptyStructWithTypeParams[A any] struct{} -var _ = emptyStructWithTypeParams[int]{} +var _ = emptyStructWithTypeParams[int]{} // no suggested fix type basicStructWithTypeParams[T any] struct { foo T @@ -32,12 +32,11 @@ type nestedStructWithTypeParams struct { basic basicStructWithTypeParams[int] } -var _ = nestedStructWithTypeParams{} +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") func _[T any]() { type S struct{ t T } - x := S{} - _ = x + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_typeparams_21_49 -- @@ -48,7 +47,7 @@ package fillstruct type emptyStructWithTypeParams[A any] struct{} -var _ = emptyStructWithTypeParams[int]{} +var _ = emptyStructWithTypeParams[int]{} // no suggested fix type basicStructWithTypeParams[T any] struct { foo T @@ -75,12 +74,11 @@ type nestedStructWithTypeParams struct { basic basicStructWithTypeParams[int] } -var _ = nestedStructWithTypeParams{} +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") func _[T any]() { type S struct{ t T } - x := S{} - _ = x + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_typeparams_25_1 -- @@ -91,7 +89,7 @@ package fillstruct type emptyStructWithTypeParams[A any] struct{} -var _ = emptyStructWithTypeParams[int]{} +var _ = emptyStructWithTypeParams[int]{} // no suggested fix type basicStructWithTypeParams[T any] struct { foo T @@ -116,11 +114,93 @@ type nestedStructWithTypeParams struct { basic basicStructWithTypeParams[int] } -var _ = nestedStructWithTypeParams{} +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") func _[T any]() { type S struct{ t T } - x := S{} - _ = x + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") +} + +-- suggestedfix_typeparams_32_36 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} // no suggested fix + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{ + bar: "", + basic: basicStructWithTypeParams{}, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +func _[T any]() { + type S struct{ t T } + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") +} + +-- suggestedfix_typeparams_36_8 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} // no suggested fix + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +func _[T any]() { + type S struct{ t T } + _ = S{ + t: *new(T), + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 93d9c638193..1e1c8762a8f 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -13,7 +13,7 @@ FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 SemanticTokenCount = 3 -SuggestedFixCount = 67 +SuggestedFixCount = 69 FunctionExtractionCount = 27 MethodExtractionCount = 6 DefinitionsCount = 110 diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index d538e07403a..3b983ccf7d8 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -83,6 +83,9 @@ func IsZeroValue(expr ast.Expr) bool { } } +// TypeExpr returns syntax for the specified type. References to +// named types from packages other than pkg are qualified by an appropriate +// package name, as defined by the import environment of file. func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { switch t := typ.(type) { case *types.Basic: @@ -312,19 +315,21 @@ func WalkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) { }) } -// FindMatchingIdents finds all identifiers in 'node' that match any of the given types. +// MatchingIdents finds the names of all identifiers in 'node' that match any of the given types. // 'pos' represents the position at which the identifiers may be inserted. 'pos' must be within // the scope of each of identifier we select. Otherwise, we will insert a variable at 'pos' that // is unrecognized. -func FindMatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]*ast.Ident { - matches := map[types.Type][]*ast.Ident{} +func MatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]string { + // Initialize matches to contain the variable types we are searching for. + matches := make(map[types.Type][]string) for _, typ := range typs { if typ == nil { - continue + continue // TODO(adonovan): is this reachable? } - matches[typ] = []*ast.Ident{} + matches[typ] = nil // create entry } + seen := map[types.Object]struct{}{} ast.Inspect(node, func(n ast.Node) bool { if n == nil { @@ -336,8 +341,7 @@ func FindMatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *t // // x := fakeStruct{f0: x} // - assignment, ok := n.(*ast.AssignStmt) - if ok && pos > assignment.Pos() && pos <= assignment.End() { + if assign, ok := n.(*ast.AssignStmt); ok && pos > assign.Pos() && pos <= assign.End() { return false } if n.End() > pos { @@ -370,17 +374,17 @@ func FindMatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *t return true } // The object must match one of the types that we are searching for. - if idents, ok := matches[obj.Type()]; ok { - matches[obj.Type()] = append(idents, ast.NewIdent(ident.Name)) - } - // If the object type does not exactly match any of the target types, greedily - // find the first target type that the object type can satisfy. - for typ := range matches { - if obj.Type() == typ { - continue - } - if equivalentTypes(obj.Type(), typ) { - matches[typ] = append(matches[typ], ast.NewIdent(ident.Name)) + // TODO(adonovan): opt: use typeutil.Map? + if names, ok := matches[obj.Type()]; ok { + matches[obj.Type()] = append(names, ident.Name) + } else { + // If the object type does not exactly match + // any of the target types, greedily find the first + // target type that the object type can satisfy. + for typ := range matches { + if equivalentTypes(obj.Type(), typ) { + matches[typ] = append(matches[typ], ident.Name) + } } } return true @@ -389,7 +393,7 @@ func FindMatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *t } func equivalentTypes(want, got types.Type) bool { - if want == got || types.Identical(want, got) { + if types.Identical(want, got) { return true } // Code segment to help check for untyped equality from (golang/go#32146). diff --git a/internal/fuzzy/matcher.go b/internal/fuzzy/matcher.go index 92e1001fd12..c0efd30dd9a 100644 --- a/internal/fuzzy/matcher.go +++ b/internal/fuzzy/matcher.go @@ -8,7 +8,6 @@ package fuzzy import ( "bytes" "fmt" - "go/ast" ) const ( @@ -407,29 +406,29 @@ func (m *Matcher) poorMatch() bool { return false } -// FindBestMatch employs fuzzy matching to evaluate the similarity of each given identifier to the -// given pattern. We return the identifier whose name is most similar to the pattern. -func FindBestMatch(pattern string, idents []*ast.Ident) ast.Expr { +// BestMatch returns the name most similar to the +// pattern, using fuzzy matching, or the empty string. +func BestMatch(pattern string, names []string) string { fuzz := NewMatcher(pattern) - var bestFuzz ast.Expr + best := "" highScore := float32(0) // minimum score is 0 (no match) - for _, ident := range idents { + for _, name := range names { // TODO: Improve scoring algorithm. - score := fuzz.Score(ident.Name) + score := fuzz.Score(name) if score > highScore { highScore = score - bestFuzz = ident + best = name } else if score == 0 { // Order matters in the fuzzy matching algorithm. If we find no match // when matching the target to the identifier, try matching the identifier // to the target. - revFuzz := NewMatcher(ident.Name) + revFuzz := NewMatcher(name) revScore := revFuzz.Score(pattern) if revScore > highScore { highScore = revScore - bestFuzz = ident + best = name } } } - return bestFuzz + return best } From 55e5cff61126558edd0ba9c5c2d06c8c29b8a499 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sat, 1 Oct 2022 13:03:22 -0400 Subject: [PATCH 291/723] internal/diff: unified: match GNU diff for empty file GNU diff -u prints -0,0 for the special case of inserting into an empty file. (I don't understand why this case is special, but it's fair to assume it has been debugged; perhaps patch(1) has a bug.) This change matches that special behavior, and adds a test. It also adds a (disabled) test for another bug I encountered. Also, remove some overzealous uses of t.Helper() that served to obscure the failure. Change-Id: I3e0c389c478cde45163353130e36f2f8741a8659 Reviewed-on: https://go-review.googlesource.com/c/tools/+/436782 gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan --- internal/diff/diff_test.go | 11 ++-- internal/diff/difftest/difftest.go | 83 ++++++++++++++++--------- internal/diff/difftest/difftest_test.go | 1 - internal/diff/unified.go | 3 + 4 files changed, 62 insertions(+), 36 deletions(-) diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index 1bf03ea3dec..b19929801f0 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -18,13 +18,12 @@ import ( func TestApplyEdits(t *testing.T) { for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { - t.Helper() if got := diff.ApplyEdits(tc.In, tc.Edits); got != tc.Out { - t.Errorf("ApplyEdits edits got %q, want %q", got, tc.Out) + t.Errorf("ApplyEdits(Edits): got %q, want %q", got, tc.Out) } if tc.LineEdits != nil { if got := diff.ApplyEdits(tc.In, tc.LineEdits); got != tc.Out { - t.Errorf("ApplyEdits lineEdits got %q, want %q", got, tc.Out) + t.Errorf("ApplyEdits(LineEdits): got %q, want %q", got, tc.Out) } } }) @@ -99,7 +98,6 @@ func TestNLinesRandom(t *testing.T) { func TestLineEdits(t *testing.T) { for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { - t.Helper() // if line edits not specified, it is the same as edits edits := tc.LineEdits if edits == nil { @@ -115,15 +113,14 @@ func TestLineEdits(t *testing.T) { func TestUnified(t *testing.T) { for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { - t.Helper() unified := fmt.Sprint(diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.Edits)) if unified != tc.Unified { - t.Errorf("edits got diff:\n%v\nexpected:\n%v", unified, tc.Unified) + t.Errorf("Unified(Edits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) } if tc.LineEdits != nil { unified := fmt.Sprint(diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.LineEdits)) if unified != tc.Unified { - t.Errorf("lineEdits got diff:\n%v\nexpected:\n%v", unified, tc.Unified) + t.Errorf("Unified(LineEdits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) } } }) diff --git a/internal/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go index 616125693a3..e42e0d7cbc5 100644 --- a/internal/diff/difftest/difftest.go +++ b/internal/diff/difftest/difftest.go @@ -115,35 +115,64 @@ var TestCases = []struct { `[1:], Edits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "B"}}, }, { - Name: "add_end", - In: "A", - Out: "AB", + Name: "append_empty", + In: "", // GNU diff -u special case: -0,0 + Out: "AB\nC", Unified: UnifiedPrefix + ` +@@ -0,0 +1,2 @@ ++AB ++C +\ No newline at end of file +`[1:], + Edits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "AB\nC"}}, + LineEdits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "AB\nC"}}, +}, + // TODO(adonovan): fix this test: GNU diff -u prints "+1,2", Unifies prints "+1,3". + // { + // Name: "add_start", + // In: "A", + // Out: "B\nCA", + // Unified: UnifiedPrefix + ` + // @@ -1 +1,2 @@ + // -A + // \ No newline at end of file + // +B + // +CA + // \ No newline at end of file + // `[1:], + // Edits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "B\nC"}}, + // LineEdits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "B\nC"}}, + // }, + { + Name: "add_end", + In: "A", + Out: "AB", + Unified: UnifiedPrefix + ` @@ -1 +1 @@ -A \ No newline at end of file +AB \ No newline at end of file `[1:], - Edits: []diff.TextEdit{{Span: newSpan(1, 1), NewText: "B"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "AB"}}, -}, { - Name: "add_newline", - In: "A", - Out: "A\n", - Unified: UnifiedPrefix + ` + Edits: []diff.TextEdit{{Span: newSpan(1, 1), NewText: "B"}}, + LineEdits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "AB"}}, + }, { + Name: "add_newline", + In: "A", + Out: "A\n", + Unified: UnifiedPrefix + ` @@ -1 +1 @@ -A \ No newline at end of file +A `[1:], - Edits: []diff.TextEdit{{Span: newSpan(1, 1), NewText: "\n"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "A\n"}}, -}, { - Name: "delete_front", - In: "A\nB\nC\nA\nB\nB\nA\n", - Out: "C\nB\nA\nB\nA\nC\n", - Unified: UnifiedPrefix + ` + Edits: []diff.TextEdit{{Span: newSpan(1, 1), NewText: "\n"}}, + LineEdits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "A\n"}}, + }, { + Name: "delete_front", + In: "A\nB\nC\nA\nB\nB\nA\n", + Out: "C\nB\nA\nB\nA\nC\n", + Unified: UnifiedPrefix + ` @@ -1,7 +1,6 @@ -A -B @@ -155,14 +184,14 @@ var TestCases = []struct { A +C `[1:], - Edits: []diff.TextEdit{ - {Span: newSpan(0, 4), NewText: ""}, - {Span: newSpan(6, 6), NewText: "B\n"}, - {Span: newSpan(10, 12), NewText: ""}, - {Span: newSpan(14, 14), NewText: "C\n"}, + Edits: []diff.TextEdit{ + {Span: newSpan(0, 4), NewText: ""}, + {Span: newSpan(6, 6), NewText: "B\n"}, + {Span: newSpan(10, 12), NewText: ""}, + {Span: newSpan(14, 14), NewText: "C\n"}, + }, + NoDiff: true, // diff algorithm produces different delete/insert pattern }, - NoDiff: true, // diff algorithm produces different delete/insert pattern -}, { Name: "replace_last_line", In: "A\nB\n", @@ -218,10 +247,8 @@ func init() { } func DiffTest(t *testing.T, compute diff.ComputeEdits) { - t.Helper() for _, test := range TestCases { t.Run(test.Name, func(t *testing.T) { - t.Helper() edits, err := compute(span.URIFromPath("/"+test.Name), test.In, test.Out) if err != nil { t.Fatal(err) @@ -229,10 +256,10 @@ func DiffTest(t *testing.T, compute diff.ComputeEdits) { got := diff.ApplyEdits(test.In, edits) unified := fmt.Sprint(diff.ToUnified(FileA, FileB, test.In, edits)) if got != test.Out { - t.Errorf("got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out) + t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out) } if !test.NoDiff && unified != test.Unified { - t.Errorf("got diff:\n%v\nexpected:\n%v", unified, test.Unified) + t.Errorf("Unified: got diff:\n%v\nexpected:\n%v", unified, test.Unified) } }) } diff --git a/internal/diff/difftest/difftest_test.go b/internal/diff/difftest/difftest_test.go index d070d3276fc..c7614325125 100644 --- a/internal/diff/difftest/difftest_test.go +++ b/internal/diff/difftest/difftest_test.go @@ -23,7 +23,6 @@ func TestVerifyUnified(t *testing.T) { testenv.NeedsTool(t, "diff") for _, test := range difftest.TestCases { t.Run(test.Name, func(t *testing.T) { - t.Helper() if test.NoDiff { t.Skip("diff tool produces expected different results") } diff --git a/internal/diff/unified.go b/internal/diff/unified.go index f9eaf4cdd4b..3ea06974b84 100644 --- a/internal/diff/unified.go +++ b/internal/diff/unified.go @@ -195,6 +195,9 @@ func (u Unified) String() string { fmt.Fprint(b, "@@") if fromCount > 1 { fmt.Fprintf(b, " -%d,%d", hunk.FromLine, fromCount) + } else if hunk.FromLine == 1 && fromCount == 0 { + // Match odd GNU diff -u behavior adding to empty file. + fmt.Fprintf(b, " -0,0") } else { fmt.Fprintf(b, " -%d", hunk.FromLine) } From 40dabfa8588dfa55c3e1a503fae5302326cc4a9c Mon Sep 17 00:00:00 2001 From: Dylan Le Date: Wed, 3 Aug 2022 14:45:51 -0400 Subject: [PATCH 292/723] gopls/internal/lsp: add support for package renaming Users can now rename packages by performing a rename on the package name in package declarations. The editor will prompt user with a dialogue text field to change the package name. This rename feature then do the following: - Rename all the external imports of the renaming package. In case there is renaming conflicts within a file, the feature repeatedly try fresh names constructed by appending new package name with number of try times so far until succeed. - Rename all the internal references to the renaming package from its files. - Rename the directory of the renamed package and update the imports' paths of any packages nested under the renamed directory. - Rename the test package with the new package name and suffix "_test" with the current test package name ends with "_test", or just the new package name otherwise. The regression tests accounts for these edge cases: - Only rename other known packages if the module path is same for both the renaming package and the other known packages. - Prevent renaming main package. - Test if the renaming package name is different from its path base. Todo: - Add a test for the case when the renaming package's path contains "internal" as a segment. - Allow edit to the whole package path not just only the last segment of the package path - Reject renaming if the renamed subpackages don't belong to the same module with the renaming package - Check the go.mod files in the workspace to see if any replace directives need to be fixed if the renaming affects the locations of any go.mod files Change-Id: I4ccb700df5a142c3fc1c06f3e5835f0f23da1ec5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420958 Run-TryBot: Dylan Le gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/cmd/rename.go | 3 +- gopls/internal/lsp/fake/editor.go | 5 + gopls/internal/lsp/rename.go | 19 +- gopls/internal/lsp/source/implementation.go | 22 +- gopls/internal/lsp/source/rename.go | 322 +++++++++++--- gopls/internal/lsp/source/source_test.go | 2 +- gopls/internal/regtest/misc/rename_test.go | 462 +++++++++++++++++++- 7 files changed, 748 insertions(+), 87 deletions(-) diff --git a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go index eb7e10a3ef5..5f160038480 100644 --- a/gopls/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -13,9 +13,9 @@ import ( "path/filepath" "sort" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) @@ -81,7 +81,6 @@ func (r *rename) Run(ctx context.Context, args ...string) error { var orderedURIs []string edits := map[span.URI][]protocol.TextEdit{} for _, c := range edit.DocumentChanges { - // Todo: Add handler for RenameFile edits if c.TextDocumentEdit != nil { uri := fileURI(c.TextDocumentEdit.TextDocument.URI) edits[uri] = append(edits[uri], c.TextDocumentEdit.Edits...) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 2c988ce6669..17bec286b7d 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -271,6 +271,11 @@ func (e *Editor) initialize(ctx context.Context) error { // editor does send didChangeWatchedFiles notifications, so set this to // true. params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true + params.Capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{ + ResourceOperations: []protocol.ResourceOperationKind{ + "rename", + }, + } params.Trace = "messages" // TODO: support workspace folders. diff --git a/gopls/internal/lsp/rename.go b/gopls/internal/lsp/rename.go index aff2f304ebd..2d9ad69323f 100644 --- a/gopls/internal/lsp/rename.go +++ b/gopls/internal/lsp/rename.go @@ -6,9 +6,11 @@ package lsp import ( "context" + "path/filepath" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/span" ) func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { @@ -17,7 +19,10 @@ func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*pr if !ok { return nil, err } - edits, err := source.Rename(ctx, snapshot, fh, params.Position, params.NewName) + // Because we don't handle directory renaming within source.Rename, source.Rename returns + // boolean value isPkgRenaming to determine whether an DocumentChanges of type RenameFile should + // be added to the return protocol.WorkspaceEdit value. + edits, isPkgRenaming, err := source.Rename(ctx, snapshot, fh, params.Position, params.NewName) if err != nil { return nil, err } @@ -30,6 +35,18 @@ func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*pr } docChanges = append(docChanges, documentChanges(fh, e)...) } + if isPkgRenaming { + uri := params.TextDocument.URI.SpanURI() + oldBase := filepath.Dir(span.URI.Filename(uri)) + newURI := filepath.Join(filepath.Dir(oldBase), params.NewName) + docChanges = append(docChanges, protocol.DocumentChanges{ + RenameFile: &protocol.RenameFile{ + Kind: "rename", + OldURI: protocol.URIFromPath(oldBase), + NewURI: protocol.URIFromPath(newURI), + }, + }) + } return &protocol.WorkspaceEdit{ DocumentChanges: docChanges, }, nil diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index b3896802aac..4626bae5ac1 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -213,27 +213,31 @@ var ( // every package that the file belongs to, in every typechecking mode // applicable. func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, pp protocol.Position) ([]qualifiedObject, error) { - pkgs, err := s.PackagesForFile(ctx, uri, TypecheckAll, false) + offset, err := protocolPositionToOffset(ctx, s, uri, pp) if err != nil { return nil, err } + return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{}) +} + +func protocolPositionToOffset(ctx context.Context, s Snapshot, uri span.URI, pp protocol.Position) (int, error) { + pkgs, err := s.PackagesForFile(ctx, uri, TypecheckAll, false) + if err != nil { + return 0, err + } if len(pkgs) == 0 { - return nil, errNoObjectFound + return 0, errNoObjectFound } pkg := pkgs[0] pgf, err := pkg.File(uri) if err != nil { - return nil, err + return 0, err } pos, err := pgf.Mapper.Pos(pp) if err != nil { - return nil, err + return 0, err } - offset, err := safetoken.Offset(pgf.Tok, pos) - if err != nil { - return nil, err - } - return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{}) + return safetoken.Offset(pgf.Tok, pos) } // A positionKey identifies a byte offset within a file (URI). diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index bb4d9a843e1..d0225b52a85 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -5,15 +5,16 @@ package source import ( - "bytes" "context" "errors" "fmt" "go/ast" - "go/format" "go/token" "go/types" + "path" "regexp" + "sort" + "strconv" "strings" "golang.org/x/tools/go/types/typeutil" @@ -33,7 +34,7 @@ type renamer struct { errors string from, to string satisfyConstraints map[satisfy.Constraint]bool - packages map[*types.Package]Package // may include additional packages that are a dep of pkg + packages map[*types.Package]Package // may include additional packages that are a dep of pkg. msets typeutil.MethodSetCache changeMethods bool } @@ -49,15 +50,9 @@ 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 Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) { - fileRenameSupported := false - for _, op := range snapshot.View().Options().SupportedResourceOperations { - if op == protocol.Rename { - fileRenameSupported = true - break - } - } - - // Find position of the package name declaration + // Find position of the package name declaration. + ctx, done := event.Start(ctx, "source.PrepareRename") + defer done() pgf, err := snapshot.ParseGo(ctx, f, ParseFull) if err != nil { return nil, err, err @@ -67,13 +62,44 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot return nil, err, err } - if inPackageName && !fileRenameSupported { - err := errors.New("can't rename packages: LSP client does not support file renaming") - return nil, err, err - } + if inPackageName { + fileRenameSupported := false + for _, op := range snapshot.View().Options().SupportedResourceOperations { + if op == protocol.Rename { + fileRenameSupported = true + break + } + } - ctx, done := event.Start(ctx, "source.PrepareRename") - defer done() + if !fileRenameSupported { + err := errors.New("can't rename package: LSP client does not support file renaming") + return nil, err, err + } + renamingPkg, err := snapshot.PackageForFile(ctx, f.URI(), TypecheckAll, NarrowestPackage) + if err != nil { + return nil, err, err + } + + if renamingPkg.Name() == "main" { + err := errors.New("can't rename package \"main\"") + return nil, err, err + } + + if renamingPkg.Version() == nil { + err := fmt.Errorf("can't rename package: missing module information for package %q", renamingPkg.PkgPath()) + return nil, err, err + } + + if renamingPkg.Version().Path == renamingPkg.PkgPath() { + err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", renamingPkg.PkgPath(), renamingPkg.Version().Path) + return nil, err, err + } + result, err := computePrepareRenameResp(snapshot, renamingPkg, pgf.File.Name, renamingPkg.Name()) + if err != nil { + return nil, nil, err + } + return result, nil, nil + } qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp) if err != nil { @@ -81,15 +107,23 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot } node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg if err := checkRenamable(obj); err != nil { - return nil, err, err + return nil, nil, err } - mr, err := posToMappedRange(snapshot, pkg, node.Pos(), node.End()) + result, err := computePrepareRenameResp(snapshot, pkg, node, obj.Name()) if err != nil { return nil, nil, err } + return result, nil, nil +} + +func computePrepareRenameResp(snapshot Snapshot, pkg Package, node ast.Node, text string) (*PrepareItem, error) { + mr, err := posToMappedRange(snapshot, pkg, node.Pos(), node.End()) + if err != nil { + return nil, err + } rng, err := mr.Range() if err != nil { - return nil, nil, err + return nil, err } if _, isImport := node.(*ast.ImportSpec); isImport { // We're not really renaming the import path. @@ -97,8 +131,8 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot } return &PrepareItem{ Range: rng, - Text: obj.Name(), - }, nil, nil + Text: text, + }, nil } // checkRenamable verifies if an obj may be renamed. @@ -113,51 +147,229 @@ func checkRenamable(obj types.Object) error { } // Rename returns a map of TextEdits for each file modified when renaming a -// given identifier within a package. -func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, error) { +// given identifier within a package and a boolean value of true for renaming +// package and false otherwise. +func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, bool, error) { ctx, done := event.Start(ctx, "source.Rename") defer done() pgf, err := s.ParseGo(ctx, f, ParseFull) if err != nil { - return nil, err + return nil, false, err } inPackageName, err := isInPackageName(ctx, s, f, pgf, pp) if err != nil { - return nil, err + return nil, false, err } if inPackageName { - renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckAll, NarrowestPackage) + pkgs, err := s.PackagesForFile(ctx, f.URI(), TypecheckAll, true) if err != nil { - return nil, err + return nil, true, err } - - result := make(map[span.URI][]protocol.TextEdit) - // Rename internal references to the package in the renaming package - // Todo(dle): need more investigation on case when pkg.GoFiles != pkg.CompiledGoFiles if using cgo. - for _, f := range renamingPkg.CompiledGoFiles() { - pkgNameMappedRange := NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()) - rng, err := pkgNameMappedRange.Range() + var pkg Package + for _, p := range pkgs { + // pgf.File.Name must not be nil, else this will panic. + if pgf.File.Name.Name == p.Name() { + pkg = p + break + } + } + activePkgs, err := s.ActivePackages(ctx) + if err != nil { + return nil, true, err + } + renamingEdits, err := computeImportRenamingEdits(ctx, s, pkg, activePkgs, newName) + if err != nil { + return nil, true, err + } + pkgNameEdits, err := computePackageNameRenamingEdits(pkg, newName) + if err != nil { + return nil, true, err + } + for uri, edits := range pkgNameEdits { + renamingEdits[uri] = edits + } + // Rename test packages + for _, activePkg := range activePkgs { + if activePkg.ForTest() != pkg.PkgPath() { + continue + } + // Filter out intermediate test variants. + if activePkg.PkgPath() != pkg.PkgPath() && activePkg.PkgPath() != pkg.PkgPath()+"_test" { + continue + } + newTestPkgName := newName + if strings.HasSuffix(activePkg.Name(), "_test") { + newTestPkgName += "_test" + } + perPackageEdits, err := computeRenamePackageImportEditsPerPackage(ctx, s, activePkg, newTestPkgName, pkg.PkgPath()) + for uri, edits := range perPackageEdits { + renamingEdits[uri] = append(renamingEdits[uri], edits...) + } + pkgNameEdits, err := computePackageNameRenamingEdits(activePkg, newTestPkgName) if err != nil { - return nil, err + return nil, true, err } - result[f.URI] = []protocol.TextEdit{ - { - Range: rng, - NewText: newName, - }, + for uri, edits := range pkgNameEdits { + if _, ok := renamingEdits[uri]; !ok { + renamingEdits[uri] = edits + } } } - return result, nil + return renamingEdits, true, nil } qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) + if err != nil { + return nil, false, err + } + result, err := renameObj(ctx, s, newName, qos) + if err != nil { + return nil, false, err + } + + return result, false, nil +} + +// computeImportRenamingEdits computes all edits to files in other packages that import +// the renaming package. +func computeImportRenamingEdits(ctx context.Context, s Snapshot, renamingPkg Package, pkgs []Package, newName string) (map[span.URI][]protocol.TextEdit, error) { + result := make(map[span.URI][]protocol.TextEdit) + // Rename imports to the renamed package from other packages. + for _, pkg := range pkgs { + if renamingPkg.Version() == nil { + return nil, fmt.Errorf("cannot rename package: missing module information for package %q", renamingPkg.PkgPath()) + } + renamingPkgModulePath := renamingPkg.Version().Path + activePkgModulePath := pkg.Version().Path + if !strings.HasPrefix(pkg.PkgPath()+"/", renamingPkg.PkgPath()+"/") { + continue // not a nested package or the renaming package. + } + + if activePkgModulePath == pkg.PkgPath() { + continue // don't edit imports to nested package whose path and module path is the same. + } + + if renamingPkgModulePath != "" && renamingPkgModulePath != activePkgModulePath { + continue // don't edit imports if nested package and renaming package has different module path. + } + + // Compute all edits for other files that import this nested package + // when updating the its path. + perFileEdits, err := computeRenamePackageImportEditsPerPackage(ctx, s, pkg, newName, renamingPkg.PkgPath()) + if err != nil { + return nil, err + } + for uri, edits := range perFileEdits { + result[uri] = append(result[uri], edits...) + } + } + + return result, nil +} + +// computeImportRenamingEdits computes all edits to files within the renming packages. +func computePackageNameRenamingEdits(renamingPkg Package, newName string) (map[span.URI][]protocol.TextEdit, error) { + result := make(map[span.URI][]protocol.TextEdit) + // Rename internal references to the package in the renaming package. + for _, f := range renamingPkg.CompiledGoFiles() { + if f.File.Name == nil { + continue + } + pkgNameMappedRange := NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()) + // Invalid range for the package name. + rng, err := pkgNameMappedRange.Range() + if err != nil { + return nil, err + } + result[f.URI] = append(result[f.URI], protocol.TextEdit{ + Range: rng, + NewText: newName, + }) + } + + return result, nil +} + +// computeRenamePackageImportEditsPerPackage computes the set of edits (to imports) +// among the files of package nestedPkg that are necessary when package renamedPkg +// is renamed to newName. +func computeRenamePackageImportEditsPerPackage(ctx context.Context, s Snapshot, nestedPkg Package, newName, renamingPath string) (map[span.URI][]protocol.TextEdit, error) { + rdeps, err := s.GetReverseDependencies(ctx, nestedPkg.ID()) if err != nil { return nil, err } + result := make(map[span.URI][]protocol.TextEdit) + for _, dep := range rdeps { + for _, f := range dep.CompiledGoFiles() { + for _, imp := range f.File.Imports { + if impPath, _ := strconv.Unquote(imp.Path.Value); impPath != nestedPkg.PkgPath() { + continue // not the import we're looking for. + } + + // Create text edit for the import path (string literal). + impPathMappedRange := NewMappedRange(f.Tok, f.Mapper, imp.Path.Pos(), imp.Path.End()) + rng, err := impPathMappedRange.Range() + if err != nil { + return nil, err + } + newText := strconv.Quote(path.Join(path.Dir(renamingPath), newName) + strings.TrimPrefix(nestedPkg.PkgPath(), renamingPath)) + result[f.URI] = append(result[f.URI], protocol.TextEdit{ + Range: rng, + NewText: newText, + }) + + // If the nested package is not the renaming package or its import path already + // has an local package name then we don't need to update the local package name. + if nestedPkg.PkgPath() != renamingPath || imp.Name != nil { + continue + } + + // Rename the types.PkgName locally within this file. + pkgname := dep.GetTypesInfo().Implicits[imp].(*types.PkgName) + qos := []qualifiedObject{{obj: pkgname, pkg: dep}} + + pkgScope := dep.GetTypes().Scope() + fileScope := dep.GetTypesInfo().Scopes[f.File] + + var changes map[span.URI][]protocol.TextEdit + localName := newName + try := 0 + // Keep trying with fresh names until one succeeds. + for fileScope.Lookup(localName) != nil || pkgScope.Lookup(localName) != nil { + try++ + localName = fmt.Sprintf("%s%d", newName, try) + } + changes, err = renameObj(ctx, s, localName, qos) + if err != nil { + return nil, err + } + // If the chosen local package name matches the package's new name, delete the + // change that would have inserted an explicit local name, which is always + // the lexically first change. + if localName == newName { + v := changes[f.URI] + sort.Slice(v, func(i, j int) bool { + return protocol.CompareRange(v[i].Range, v[j].Range) < 0 + }) + changes[f.URI] = v[1:] + } + for uri, edits := range changes { + result[uri] = append(result[uri], edits...) + } + } + } + } + + return result, nil +} + +// renameObj returns a map of TextEdits for renaming an identifier within a file +// and boolean value of true if there is no renaming conflicts and false otherwise. +func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedObject) (map[span.URI][]protocol.TextEdit, error) { obj := qos[0].obj if err := checkRenamable(obj); err != nil { @@ -369,7 +581,8 @@ func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { return nil } -// updatePkgName returns the updates to rename a pkgName in the import spec +// updatePkgName returns the updates to rename a pkgName in the import spec by +// only modifying the package name portion of the import declaration. func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) { // Modify ImportSpec syntax to add or remove the Name as needed. pkg := r.packages[pkgName.Pkg()] @@ -382,29 +595,20 @@ func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) return nil, fmt.Errorf("failed to update PkgName for %s", pkgName.Name()) } - var astIdent *ast.Ident // will be nil if ident is removed + newText := "" if pkgName.Imported().Name() != r.to { - // ImportSpec.Name needed - astIdent = &ast.Ident{NamePos: spec.Path.Pos(), Name: r.to} + newText = r.to + " " } - // Make a copy of the ident that just has the name and path. - updated := &ast.ImportSpec{ - Name: astIdent, - Path: spec.Path, - EndPos: spec.EndPos, - } - - rng := span.NewRange(tokFile, spec.Pos(), spec.End()) + // Replace the portion (possibly empty) of the spec before the path: + // local "path" or "path" + // -> <- -><- + rng := span.NewRange(tokFile, spec.Pos(), spec.Path.Pos()) spn, err := rng.Span() if err != nil { return nil, err } - var buf bytes.Buffer - format.Node(&buf, r.fset, updated) - newText := buf.String() - return &diff.TextEdit{ Span: spn, NewText: newText, diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 1f348764cfe..d899f4ffdb2 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -764,7 +764,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { if err != nil { t.Fatal(err) } - changes, err := source.Rename(r.ctx, r.snapshot, fh, srcRng.Start, newText) + changes, _, err := source.Rename(r.ctx, r.snapshot, fh, srcRng.Start, newText) if err != nil { renamed := string(r.data.Golden(t, tag, spn.URI().Filename(), func() ([]byte, error) { return []byte(err.Error()), nil diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index a0447c5026d..1f6ee1c2b85 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/internal/testenv" ) -func TestPrepareRenamePackage(t *testing.T) { +func TestPrepareRenameMainPackage(t *testing.T) { const files = ` -- go.mod -- module mod.com @@ -30,7 +30,7 @@ func main() { fmt.Println(1) } ` - const wantErr = "can't rename packages: LSP client does not support file renaming" + const wantErr = "can't rename package \"main\"" Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") pos := env.RegexpSearch("main.go", `main`) @@ -43,7 +43,7 @@ func main() { } _, err := env.Editor.Server.PrepareRename(env.Ctx, params) if err == nil { - t.Errorf("missing can't rename package error from PrepareRename") + t.Errorf("missing can't rename package main error from PrepareRename") } if err.Error() != wantErr { @@ -52,38 +52,255 @@ func main() { }) } -func TestRenamePackageInRenamedPackage(t *testing.T) { - // Failed at Go 1.13; not investigated - testenv.NeedsGo1Point(t, 14) +func TestPrepareRenameWithNoPackageDeclaration(t *testing.T) { + testenv.NeedsGo1Point(t, 15) + const files = ` +go 1.14 +-- lib/a.go -- +import "fmt" + +const A = 1 + +func bar() { + fmt.Println("Bar") +} + +-- main.go -- +package main + +import "fmt" + +func main() { + fmt.Println("Hello") +} +` + const wantErr = "no object found" + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "fmt") + + err := env.Editor.Rename(env.Ctx, "lib/a.go", pos, "fmt1") + if err == nil { + t.Errorf("missing no object found from Rename") + } + + if err.Error() != wantErr { + t.Errorf("got %v, want %v", err.Error(), wantErr) + } + }) +} + +func TestPrepareRenameFailWithUnknownModule(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +go 1.14 +-- lib/a.go -- +package lib + +const A = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" +) + +func main() { + println("Hello") +} +` + const wantErr = "can't rename package: missing module information for package" + Run(t, files, func(t *testing.T, env *Env) { + pos := env.RegexpSearch("lib/a.go", "lib") + tdpp := protocol.TextDocumentPositionParams{ + TextDocument: env.Editor.TextDocumentIdentifier("lib/a.go"), + Position: pos.ToProtocolPosition(), + } + params := &protocol.PrepareRenameParams{ + TextDocumentPositionParams: tdpp, + } + _, err := env.Editor.Server.PrepareRename(env.Ctx, params) + if err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("missing cannot rename packages with unknown module from PrepareRename") + } + }) +} + +func TestRenamePackageWithConflicts(t *testing.T) { + testenv.NeedsGo1Point(t, 17) const files = ` -- go.mod -- module mod.com go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package nested + +const B = 1 + +-- lib/x/a.go -- +package nested1 + +const C = 1 + -- main.go -- package main import ( - "fmt" - "a.go" + "mod.com/lib" + "mod.com/lib/nested" + nested1 "mod.com/lib/x" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", `nested2 "mod.com/nested"`) + env.RegexpSearch("main.go", "mod.com/nested/nested") + env.RegexpSearch("main.go", `nested1 "mod.com/nested/x"`) + }) +} + +func TestRenamePackageWithAlias(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package nested + +const B = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + lib1 "mod.com/lib/nested" ) func main() { - fmt.Println(a.C) + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) + }) } --- a.go -- + +func TestRenamePackageWithDifferentDirectoryPath(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package foo + +const B = 1 + +-- main.go -- package main +import ( + "mod.com/lib" + foo "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `foo "mod.com/nested/nested"`) + }) +} + +func TestRenamePackage(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/b.go -- +package lib + +const B = 1 + +-- lib/nested/a.go -- +package nested + const C = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + "mod.com/lib/nested" +) + +func main() { + println("Hello") +} ` Run(t, files, func(t *testing.T, env *Env) { - env.OpenFile("main.go") - pos := env.RegexpSearch("main.go", "main") - env.Rename("main.go", pos, "pkg") + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "lib1") // Check if the new package name exists. - env.RegexpSearch("main.go", "package pkg") - env.RegexpSearch("a.go", "package pkg") + env.RegexpSearch("lib1/a.go", "package lib1") + env.RegexpSearch("lib1/b.go", "package lib1") + env.RegexpSearch("main.go", "mod.com/lib1") + env.RegexpSearch("main.go", "mod.com/lib1/nested") }) } @@ -200,3 +417,218 @@ package b } }) } + +func TestRenameWithTestPackage(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/b.go -- +package lib + +const B = 1 + +-- lib/a_test.go -- +package lib_test + +import ( + "mod.com/lib" + "fmt +) + +const C = 1 + +-- lib/b_test.go -- +package lib + +import ( + "fmt +) + +const D = 1 + +-- lib/nested/a.go -- +package nested + +const D = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "lib1") + + // Check if the new package name exists. + env.RegexpSearch("lib1/a.go", "package lib1") + env.RegexpSearch("lib1/b.go", "package lib1") + env.RegexpSearch("main.go", "mod.com/lib1") + env.RegexpSearch("main.go", "mod.com/lib1/nested") + + // Check if the test package is renamed + env.RegexpSearch("lib1/a_test.go", "package lib1_test") + env.RegexpSearch("lib1/b_test.go", "package lib1") + }) +} + +func TestRenameWithNestedModule(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 + +require ( + mod.com/foo/bar v0.0.0 +) + +replace mod.com/foo/bar => ./foo/bar +-- foo/foo.go -- +package foo + +import "fmt" + +func Bar() { + fmt.Println("In foo before renamed to foox.") +} + +-- foo/bar/go.mod -- +module mod.com/foo/bar + +-- foo/bar/bar.go -- +package bar + +const Msg = "Hi" + +-- main.go -- +package main + +import ( + "fmt" + "mod.com/foo/bar" + "mod.com/foo" +) + +func main() { + foo.Bar() + fmt.Println(bar.Msg) +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo/foo.go") + pos := env.RegexpSearch("foo/foo.go", "foo") + env.Rename("foo/foo.go", pos, "foox") + + env.RegexpSearch("foox/foo.go", "package foox") + env.OpenFile("foox/bar/bar.go") + env.OpenFile("foox/bar/go.mod") + + env.RegexpSearch("main.go", "mod.com/foo/bar") + env.RegexpSearch("main.go", "mod.com/foox") + env.RegexpSearch("main.go", "foox.Bar()") + }) +} + +func TestRenamePackageWithNonBlankSameImportPaths(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package nested + +const B = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + lib1 "mod.com/lib" + lib2 "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `lib1 "mod.com/nested"`) + env.RegexpSearch("main.go", `lib2 "mod.com/nested/nested"`) + }) +} + +func TestRenamePackageWithBlankSameImportPaths(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package nested + +const B = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + _ "mod.com/lib" + lib1 "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `_ "mod.com/nested"`) + env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) + }) +} From 7f79a022cb115b6e1c6b52f221ac8cc098949c3d Mon Sep 17 00:00:00 2001 From: cui fliter Date: Tue, 4 Oct 2022 15:07:38 +0000 Subject: [PATCH 293/723] gopls: use fmt.Fprintf Change-Id: Ife7f3269df554d2ecb6d1ccf6d1c1fa6de49587c GitHub-Last-Rev: 9faefaefc2cc344859edeb2912cc60e9f2be4850 GitHub-Pull-Request: golang/tools#406 Reviewed-on: https://go-review.googlesource.com/c/tools/+/438537 Reviewed-by: Robert Griesemer Run-TryBot: Robert Griesemer TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/doc/generate.go | 2 +- gopls/internal/hooks/diff.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go index 449751e8e02..9ab2d01a3f2 100644 --- a/gopls/doc/generate.go +++ b/gopls/doc/generate.go @@ -621,7 +621,7 @@ func rewriteSettings(doc []byte, api *source.APIJSON) ([]byte, error) { writeTitle(section, h.final, level) for _, opt := range h.options { header := strMultiply("#", level+1) - section.Write([]byte(fmt.Sprintf("%s ", header))) + fmt.Fprintf(section, "%s ", header) opt.Write(section) } } diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go index 01a37aed4b8..46faae12f3e 100644 --- a/gopls/internal/hooks/diff.go +++ b/gopls/internal/hooks/diff.go @@ -113,7 +113,7 @@ func disaster(before, after string) string { fname := fmt.Sprintf("%s/gopls-failed-%x", os.TempDir(), os.Getpid()) fd, err := os.Create(fname) defer fd.Close() - _, err = fd.Write([]byte(fmt.Sprintf("%s\n%s\n", string(first), string(second)))) + _, err = fmt.Fprintf(fd, "%s\n%s\n", first, second) if err != nil { // what do we tell the user? return "" From dc88e7b4a22257343da9eb1e59cb29308f833738 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Thu, 29 Sep 2022 04:11:29 +0000 Subject: [PATCH 294/723] godoc: fix some comments Change-Id: Ia43fc5183e97d7d612216722e5255734d28ac508 GitHub-Last-Rev: aa1b6bbb3740a32c8d141d122e880498ed46adba GitHub-Pull-Request: golang/tools#403 Reviewed-on: https://go-review.googlesource.com/c/tools/+/436455 Reviewed-by: Ian Lance Taylor Auto-Submit: Dmitri Shuralyov Reviewed-by: Dmitri Shuralyov gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Dmitri Shuralyov Reviewed-by: Dmitri Shuralyov --- godoc/godoc.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/godoc/godoc.go b/godoc/godoc.go index 6edb8f93ce9..dfac2111a67 100644 --- a/godoc/godoc.go +++ b/godoc/godoc.go @@ -453,7 +453,7 @@ func srcToPkgLinkFunc(relpath string) string { return fmt.Sprintf(`%s`, relpath, relpath[len("pkg/"):]) } -// srcBreadcrumbFun converts each segment of relpath to a HTML . +// srcBreadcrumbFunc converts each segment of relpath to a HTML . // Each segment links to its corresponding src directories. func srcBreadcrumbFunc(relpath string) string { segments := strings.Split(relpath, "/") @@ -663,7 +663,7 @@ func (p *Presentation) example_suffixFunc(name string) string { return suffix } -// implements_html returns the "> Implements" toggle for a package-level named type. +// implements_htmlFunc returns the "> Implements" toggle for a package-level named type. // Its contents are populated from JSON data by client-side JS at load time. func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string { if p.ImplementsHTML == nil { @@ -681,7 +681,7 @@ func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) stri return buf.String() } -// methodset_html returns the "> Method set" toggle for a package-level named type. +// methodset_htmlFunc returns the "> Method set" toggle for a package-level named type. // Its contents are populated from JSON data by client-side JS at load time. func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string { if p.MethodSetHTML == nil { @@ -699,7 +699,7 @@ func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) strin return buf.String() } -// callgraph_html returns the "> Call graph" toggle for a package-level func. +// callgraph_htmlFunc returns the "> Call graph" toggle for a package-level func. // Its contents are populated from JSON data by client-side JS at load time. func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string { if p.CallGraphHTML == nil { From 2d32e152db8505daf46592ba8ce716fa4d1dd145 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 5 Oct 2022 11:12:16 -0400 Subject: [PATCH 295/723] gopls/internal/lsp/cache: in tests, show all stacks during analyzer crash We recently started using bug.Errorf to report analyzer crashes, causing tests to fail sporadically. Unfortunately the log included only the "panicked" message but not the relevant stack of the panic or of the other goroutines, giving limited evidence on which to investigate the underlying cause, suspected to be in package loading, not the analyzer itself. This change causes such crashes to display all stacks so that we can gather more evidence over the next few days before we suppress the crashes again. Updates golang/go#54762 Updates golang/go#56035 Change-Id: I2d29e05b5910540918816e695b458463a05f94d9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439116 Reviewed-by: Bryan Mills Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/cache/analysis.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index e7f2d415421..ffd14059403 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -361,19 +361,20 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a var result interface{} var err error func() { - // Set this flag temporarily when debugging crashes. - // See https://github.com/golang/go/issues/54762. - const norecover = false - if norecover { - debug.SetTraceback("all") // show all goroutines - } else { - defer func() { - if r := recover(); r != nil { - // Use bug.Errorf so that we detect panics during testing. - err = bug.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) + defer func() { + if r := recover(); r != nil { + // An Analyzer crashed. This is often merely a symptom + // of a problem in package loading. + if bug.PanicOnBugs { + // During testing, crash. See issues 54762, 56035. + debug.SetTraceback("all") // show all goroutines + panic(r) + } else { + // In production, suppress the panic and press on. + err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) } - }() - } + } + }() result, err = pass.Analyzer.Run(pass) }() if err != nil { From ff4ff8bf36e30ecdedff686d1a9ea24ca991c50b Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 4 Oct 2022 09:19:27 -0400 Subject: [PATCH 296/723] gopls/internal/lsp/source: don't type-check in FindPackageFromPos In all cases where we call FindPackageFromPos, we know that the given position must be in the forward transitive closure of an originating package. Refactor to use this information, potentially saving significant type-checking when searching for a package. As a result of this change, we never need to search intermediate test variants when querying PackagesForFile. Also replace snapshot arguments with token.FileSet arguments, when the snapshot is only needed for its FileSet. For golang/go#55293 Change-Id: Icf6236bea76ab5105a6bab24ce3afc574147882b Reviewed-on: https://go-review.googlesource.com/c/tools/+/438495 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/snapshot.go | 4 +- .../internal/lsp/source/completion/format.go | 10 +-- .../internal/lsp/source/completion/literal.go | 12 +-- gopls/internal/lsp/source/highlight.go | 4 +- gopls/internal/lsp/source/hover.go | 6 +- gopls/internal/lsp/source/identifier.go | 22 +++--- gopls/internal/lsp/source/implementation.go | 2 +- gopls/internal/lsp/source/references.go | 2 +- gopls/internal/lsp/source/rename.go | 11 ++- gopls/internal/lsp/source/signature_help.go | 4 +- gopls/internal/lsp/source/types_format.go | 12 +-- gopls/internal/lsp/source/util.go | 76 +++++++++---------- gopls/internal/lsp/source/view.go | 5 +- 13 files changed, 87 insertions(+), 83 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index d1d2b1fb7b9..d13f56ca370 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -671,7 +671,7 @@ func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source return ph.await(ctx, s) } -func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]*packageHandle, error) { +func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, withIntermediateTestVariants bool) ([]*packageHandle, error) { // TODO(rfindley): why can't/shouldn't we awaitLoaded here? It seems that if // we ask for package handles for a file, we should wait for pending loads. // Else we will reload orphaned files before the initial load completes. @@ -695,7 +695,7 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode for _, id := range knownIDs { // Filter out any intermediate test variants. We typically aren't // interested in these packages for file= style queries. - if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant && !includeTestVariants { + if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant && !withIntermediateTestVariants { continue } var parseModes []source.ParseMode diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go index bcf523fac3e..4a76eefdaae 100644 --- a/gopls/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -13,12 +13,12 @@ import ( "go/types" "strings" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) @@ -80,7 +80,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e if _, ok := obj.Type().(*types.Struct); ok { detail = "struct{...}" // for anonymous structs } else if obj.IsField() { - detail = source.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qf) + detail = source.FormatVarType(c.snapshot.FileSet(), c.pkg, obj, c.qf) } if obj.IsField() { kind = protocol.FieldCompletion @@ -237,7 +237,7 @@ Suffixes: uri := span.URIFromPath(pos.Filename) // Find the source file of the candidate. - pkg, err := source.FindPackageFromPos(ctx, c.snapshot, obj.Pos()) + pkg, err := source.FindPackageFromPos(c.snapshot.FileSet(), c.pkg, obj.Pos()) if err != nil { return item, nil } diff --git a/gopls/internal/lsp/source/completion/literal.go b/gopls/internal/lsp/source/completion/literal.go index 8f0e43c1af6..0a9fc83e66f 100644 --- a/gopls/internal/lsp/source/completion/literal.go +++ b/gopls/internal/lsp/source/completion/literal.go @@ -11,10 +11,10 @@ import ( "strings" "unicode" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" ) @@ -162,7 +162,7 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im if score := c.matcher.Score("func"); !cand.hasMod(reference) && score > 0 && !source.IsInterface(expType) { switch t := literalType.Underlying().(type) { case *types.Signature: - c.functionLiteral(ctx, t, float64(score)) + c.functionLiteral(t, float64(score)) } } } @@ -175,7 +175,7 @@ const literalCandidateScore = highScore / 2 // functionLiteral adds a function literal completion item for the // given signature. -func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, matchScore float64) { +func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) { snip := &snippet.Builder{} snip.WriteText("func(") @@ -202,7 +202,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m // If the param has no name in the signature, guess a name based // on the type. Use an empty qualifier to ignore the package. // For example, we want to name "http.Request" "r", not "hr". - name = source.FormatVarType(ctx, c.snapshot, c.pkg, p, func(p *types.Package) string { + name = source.FormatVarType(c.snapshot.FileSet(), c.pkg, p, func(p *types.Package) string { return "" }) name = abbreviateTypeName(name) @@ -264,7 +264,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m // of "i int, j int". if i == sig.Params().Len()-1 || !types.Identical(p.Type(), sig.Params().At(i+1).Type()) { snip.WriteText(" ") - typeStr := source.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qf) + typeStr := source.FormatVarType(c.snapshot.FileSet(), c.pkg, p, c.qf) if sig.Variadic() && i == sig.Params().Len()-1 { typeStr = strings.Replace(typeStr, "[]", "...", 1) } @@ -314,7 +314,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m snip.WriteText(name + " ") } - text := source.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qf) + text := source.FormatVarType(c.snapshot.FileSet(), c.pkg, r, c.qf) if tp, _ := r.Type().(*typeparams.TypeParam); tp != nil && !c.typeParamInScope(tp) { snip.WritePlaceholder(func(snip *snippet.Builder) { snip.WriteText(text) diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index 0cde62825ae..5ec71d075a8 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -13,8 +13,8 @@ import ( "strings" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/event" ) func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Range, error) { @@ -59,7 +59,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p } var ranges []protocol.Range for rng := range result { - mRng, err := posToMappedRange(snapshot, pkg, rng.start, rng.end) + mRng, err := posToMappedRange(snapshot.FileSet(), pkg, rng.start, rng.end) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 74b7e57b07c..0dc8d8ab0f6 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -22,10 +22,10 @@ import ( "golang.org/x/text/unicode/runenames" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" ) @@ -237,7 +237,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr return 0, MappedRange{}, ErrNoRuneFound } - mappedRange, err := posToMappedRange(snapshot, pkg, start, end) + mappedRange, err := posToMappedRange(snapshot.FileSet(), pkg, start, end) if err != nil { return 0, MappedRange{}, err } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 9ab3fe7ad97..7e856b528f7 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -16,10 +16,10 @@ import ( "strconv" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) @@ -81,6 +81,8 @@ func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, position ctx, done := event.Start(ctx, "source.Identifier") defer done() + // TODO(rfindley): Why isn't this PackageForFile? A single package should + // suffice to find identifier info. pkgs, err := snapshot.PackagesForFile(ctx, fh.URI(), TypecheckAll, false) if err != nil { return nil, err @@ -141,7 +143,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // Special case for package declarations, since they have no // corresponding types.Object. if ident == file.Name { - rng, err := posToMappedRange(snapshot, pkg, file.Name.Pos(), file.Name.End()) + rng, err := posToMappedRange(snapshot.FileSet(), pkg, file.Name.Pos(), file.Name.End()) if err != nil { return nil, err } @@ -155,7 +157,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if declAST == nil { declAST = file } - declRng, err := posToMappedRange(snapshot, pkg, declAST.Name.Pos(), declAST.Name.End()) + declRng, err := posToMappedRange(snapshot.FileSet(), pkg, declAST.Name.Pos(), declAST.Name.End()) if err != nil { return nil, err } @@ -183,7 +185,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa result.Name = result.ident.Name var err error - if result.MappedRange, err = posToMappedRange(snapshot, pkg, result.ident.Pos(), result.ident.End()); err != nil { + if result.MappedRange, err = posToMappedRange(snapshot.FileSet(), pkg, result.ident.Pos(), result.ident.End()); err != nil { return nil, err } @@ -282,13 +284,13 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa } } - rng, err := objToMappedRange(snapshot, pkg, result.Declaration.obj) + rng, err := objToMappedRange(snapshot.FileSet(), pkg, result.Declaration.obj) if err != nil { return nil, err } result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) - declPkg, err := FindPackageFromPos(ctx, snapshot, result.Declaration.obj.Pos()) + declPkg, err := FindPackageFromPos(snapshot.FileSet(), pkg, result.Declaration.obj.Pos()) if err != nil { return nil, err } @@ -312,7 +314,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if hasErrorType(result.Type.Object) { return result, nil } - if result.Type.MappedRange, err = objToMappedRange(snapshot, pkg, result.Type.Object); err != nil { + if result.Type.MappedRange, err = objToMappedRange(snapshot.FileSet(), pkg, result.Type.Object); err != nil { return nil, err } } @@ -480,7 +482,7 @@ func importSpec(snapshot Snapshot, pkg Package, file *ast.File, pos token.Pos) ( Name: importPath, pkg: pkg, } - if result.MappedRange, err = posToMappedRange(snapshot, pkg, imp.Path.Pos(), imp.Path.End()); err != nil { + if result.MappedRange, err = posToMappedRange(snapshot.FileSet(), pkg, imp.Path.Pos(), imp.Path.End()); err != nil { return nil, err } // Consider the "declaration" of an import spec to be the imported package. @@ -490,7 +492,7 @@ func importSpec(snapshot Snapshot, pkg Package, file *ast.File, pos token.Pos) ( } // Return all of the files in the package as the definition of the import spec. for _, dst := range importedPkg.GetSyntax() { - rng, err := posToMappedRange(snapshot, pkg, dst.Pos(), dst.End()) + rng, err := posToMappedRange(snapshot.FileSet(), pkg, dst.Pos(), dst.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 4626bae5ac1..3533c92d670 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -32,7 +32,7 @@ func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp pro if impl.pkg == nil || len(impl.pkg.CompiledGoFiles()) == 0 { continue } - rng, err := objToMappedRange(snapshot, impl.pkg, impl.obj) + rng, err := objToMappedRange(snapshot.FileSet(), impl.pkg, impl.obj) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index d1cf1eaa8fc..714a3cb7ade 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -206,7 +206,7 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i continue } seen[key] = true - rng, err := posToMappedRange(snapshot, pkg, ident.Pos(), ident.End()) + rng, err := posToMappedRange(snapshot.FileSet(), pkg, ident.Pos(), ident.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index d0225b52a85..93ded0fda75 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -117,7 +117,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot } func computePrepareRenameResp(snapshot Snapshot, pkg Package, node ast.Node, text string) (*PrepareItem, error) { - mr, err := posToMappedRange(snapshot, pkg, node.Pos(), node.End()) + mr, err := posToMappedRange(snapshot.FileSet(), pkg, node.Pos(), node.End()) if err != nil { return nil, err } @@ -163,11 +163,16 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, } if inPackageName { - pkgs, err := s.PackagesForFile(ctx, f.URI(), TypecheckAll, true) + // Since we only take one package below, no need to include test variants. + // + // TODO(rfindley): but is this correct? What about x_test packages that + // import the renaming package? + const includeTestVariants = false + pkgs, err := s.PackagesForFile(ctx, f.URI(), TypecheckAll, includeTestVariants) if err != nil { return nil, true, err } - var pkg Package + var pkg Package // TODO(rfindley): we should consider all packages, so that we get the full reverse transitive closure. for _, p := range pkgs { // pgf.File.Name must not be nil, else this will panic. if pgf.File.Name.Name == p.Name() { diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index f1a4bacce01..68ac1beeb25 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -12,8 +12,8 @@ import ( "go/types" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/event" ) func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.SignatureInformation, int, error) { @@ -94,7 +94,7 @@ FindCall: comment *ast.CommentGroup ) if obj != nil { - declPkg, err := FindPackageFromPos(ctx, snapshot, obj.Pos()) + declPkg, err := FindPackageFromPos(snapshot.FileSet(), pkg, obj.Pos()) if err != nil { return nil, 0, err } diff --git a/gopls/internal/lsp/source/types_format.go b/gopls/internal/lsp/source/types_format.go index 036c8c77206..f1c03cea658 100644 --- a/gopls/internal/lsp/source/types_format.go +++ b/gopls/internal/lsp/source/types_format.go @@ -15,9 +15,9 @@ import ( "go/types" "strings" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/typeparams" ) @@ -205,7 +205,7 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa params := make([]string, 0, sig.Params().Len()) for i := 0; i < sig.Params().Len(); i++ { el := sig.Params().At(i) - typ := FormatVarType(ctx, s, pkg, el, qf) + typ := FormatVarType(s.FileSet(), pkg, el, qf) p := typ if el.Name() != "" { p = el.Name() + " " + typ @@ -220,7 +220,7 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa needResultParens = true } el := sig.Results().At(i) - typ := FormatVarType(ctx, s, pkg, el, qf) + typ := FormatVarType(s.FileSet(), pkg, el, qf) if el.Name() == "" { results = append(results, typ) } else { @@ -253,8 +253,8 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa // FormatVarType formats a *types.Var, accounting for type aliases. // To do this, it looks in the AST of the file in which the object is declared. // On any errors, it always falls back to types.TypeString. -func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj *types.Var, qf types.Qualifier) string { - pkg, err := FindPackageFromPos(ctx, snapshot, obj.Pos()) +func FormatVarType(fset *token.FileSet, srcpkg Package, obj *types.Var, qf types.Qualifier) string { + pkg, err := FindPackageFromPos(fset, srcpkg, obj.Pos()) if err != nil { return types.TypeString(obj.Type(), qf) } @@ -283,7 +283,7 @@ func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj * // If the request came from a different package than the one in which the // types are defined, we may need to modify the qualifiers. qualified = qualifyExpr(qualified, srcpkg, pkg, clonedInfo, qf) - fmted := FormatNode(snapshot.FileSet(), qualified) + fmted := FormatNode(fset, qualified) return fmted } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index d1c90b65d5e..9939ca02a9c 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -123,7 +123,7 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { return false } -func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedRange, error) { +func objToMappedRange(fset *token.FileSet, pkg Package, obj types.Object) (MappedRange, error) { nameLen := len(obj.Name()) if pkgName, ok := obj.(*types.PkgName); ok { // An imported Go package has a package-local, unqualified name. @@ -140,13 +140,20 @@ func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedR nameLen = len(pkgName.Imported().Path()) + len(`""`) } } - return posToMappedRange(snapshot, pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen)) + return posToMappedRange(fset, pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen)) } // posToMappedRange returns the MappedRange for the given [start, end) span, // which must be among the transitive dependencies of pkg. -func posToMappedRange(snapshot Snapshot, pkg Package, pos, end token.Pos) (MappedRange, error) { - tokFile := snapshot.FileSet().File(pos) +func posToMappedRange(fset *token.FileSet, pkg Package, pos, end token.Pos) (MappedRange, error) { + if !pos.IsValid() { + return MappedRange{}, fmt.Errorf("invalid start position") + } + if !end.IsValid() { + return MappedRange{}, fmt.Errorf("invalid end position") + } + + tokFile := fset.File(pos) // Subtle: it is not safe to simplify this to tokFile.Name // because, due to //line directives, a Position within a // token.File may have a different filename than the File itself. @@ -155,19 +162,35 @@ func posToMappedRange(snapshot Snapshot, pkg Package, pos, end token.Pos) (Mappe if err != nil { return MappedRange{}, err } - if !pos.IsValid() { - return MappedRange{}, fmt.Errorf("invalid start position") - } - if !end.IsValid() { - return MappedRange{}, fmt.Errorf("invalid end position") - } - // It is fishy that pgf.Mapper (from the parsed Go file) is + // It is problematic that pgf.Mapper (from the parsed Go file) is // accompanied here not by pgf.Tok but by tokFile from the global // FileSet, which is a distinct token.File that doesn't - // contain [pos,end). TODO(adonovan): clean this up. + // contain [pos,end). + // + // This is done because tokFile is the *token.File for the compiled go file + // containing pos, whereas Mapper is the UTF16 mapper for the go file pointed + // to by line directives. + // + // TODO(golang/go#55043): clean this up. return NewMappedRange(tokFile, pgf.Mapper, pos, end), nil } +// FindPackageFromPos returns the Package for the given position, which must be +// among the transitive dependencies of pkg. +// +// TODO(rfindley): is this the best factoring of this API? This function is +// really a trivial wrapper around findFileInDeps, which may be a more useful +// function to expose. +func FindPackageFromPos(fset *token.FileSet, pkg Package, pos token.Pos) (Package, error) { + if !pos.IsValid() { + return nil, fmt.Errorf("invalid position") + } + fileName := fset.File(pos).Name() + uri := span.URIFromPath(fileName) + _, pkg, err := findFileInDeps(pkg, uri) + return pkg, err +} + // Matches cgo generated comment as well as the proposed standard: // // https://golang.org/s/generatedcode @@ -286,35 +309,6 @@ func CompareDiagnostic(a, b *Diagnostic) int { return 0 } -// FindPackageFromPos finds the first package containing pos in its -// type-checked AST. -func FindPackageFromPos(ctx context.Context, snapshot Snapshot, pos token.Pos) (Package, error) { - tok := snapshot.FileSet().File(pos) - if tok == nil { - return nil, fmt.Errorf("no file for pos %v", pos) - } - uri := span.URIFromPath(tok.Name()) - pkgs, err := snapshot.PackagesForFile(ctx, uri, TypecheckAll, true) - if err != nil { - return nil, err - } - // Only return the package if it actually type-checked the given position. - for _, pkg := range pkgs { - parsed, err := pkg.File(uri) - if err != nil { - // TODO(adonovan): should this be a bug.Report or log.Fatal? - // The logic in Identifier seems to think so. - // Should it be a postcondition of PackagesForFile? - // And perhaps PackagesForFile should return the PGFs too. - return nil, err - } - if parsed != nil && parsed.Tok.Base() == tok.Base() { - return pkg, nil - } - } - return nil, fmt.Errorf("no package for given file position") -} - // findFileInDeps finds uri in pkg or its dependencies. func findFileInDeps(pkg Package, uri span.URI) (*ParsedGoFile, Package, error) { queue := []Package{pkg} diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 8b1dbdbcf39..fa9d6a901ec 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -138,7 +138,10 @@ type Snapshot interface { // PackagesForFile returns an unordered list of packages that contain // the file denoted by uri, type checked in the specified mode. - PackagesForFile(ctx context.Context, uri span.URI, mode TypecheckMode, includeTestVariants bool) ([]Package, error) + // + // If withIntermediateTestVariants is set, the resulting package set includes + // intermediate test variants. + PackagesForFile(ctx context.Context, uri span.URI, mode TypecheckMode, withIntermediateTestVariants bool) ([]Package, error) // PackageForFile returns a single package that this file belongs to, // checked in mode and filtered by the package policy. From c5514b75d9ec8869b64f91f48b7a97c935ce591a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 4 Oct 2022 09:37:38 -0400 Subject: [PATCH 297/723] gopls/internal/lsp/source: use PackageFromFile in Identifier When searching for declaration information about a position, it should suffice to search the narrowest fully type-checked package containing the file. This should significantly improve performance when there are many test variants of the current package, that have not yet been type-checked in the ParseFull mode (as reported in golang/go#55293). For golang/go#55293 Change-Id: I89a1999f9fe82dea51dd47db769c90b69be5e454 Reviewed-on: https://go-review.googlesource.com/c/tools/+/438496 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/source/identifier.go | 43 +++++++------------------ 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 7e856b528f7..91c7b6b30d8 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -12,7 +12,6 @@ import ( "go/parser" "go/token" "go/types" - "sort" "strconv" "golang.org/x/tools/go/ast/astutil" @@ -81,42 +80,22 @@ func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, position ctx, done := event.Start(ctx, "source.Identifier") defer done() - // TODO(rfindley): Why isn't this PackageForFile? A single package should - // suffice to find identifier info. - pkgs, err := snapshot.PackagesForFile(ctx, fh.URI(), TypecheckAll, false) + pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return nil, err } - if len(pkgs) == 0 { - return nil, fmt.Errorf("no packages for file %v", fh.URI()) + pgf, err := pkg.File(fh.URI()) + if err != nil { + // We shouldn't get a package from PackagesForFile that doesn't actually + // contain the file. + bug.Report("missing package file", bug.Data{"pkg": pkg.ID(), "file": fh.URI()}) + return nil, err } - sort.Slice(pkgs, func(i, j int) bool { - // Prefer packages with a more complete parse mode. - if pkgs[i].ParseMode() != pkgs[j].ParseMode() { - return pkgs[i].ParseMode() > pkgs[j].ParseMode() - } - return len(pkgs[i].CompiledGoFiles()) < len(pkgs[j].CompiledGoFiles()) - }) - var findErr error - for _, pkg := range pkgs { - pgf, err := pkg.File(fh.URI()) - if err != nil { - // We shouldn't get a package from PackagesForFile that doesn't actually - // contain the file. - bug.Report("missing package file", bug.Data{"pkg": pkg.ID(), "file": fh.URI()}) - return nil, err - } - pos, err := pgf.Mapper.Pos(position) - if err != nil { - return nil, err - } - var ident *IdentifierInfo - ident, findErr = findIdentifier(ctx, snapshot, pkg, pgf, pos) - if findErr == nil { - return ident, nil - } + pos, err := pgf.Mapper.Pos(position) + if err != nil { + return nil, err } - return nil, findErr + return findIdentifier(ctx, snapshot, pkg, pgf, pos) } // ErrNoIdentFound is error returned when no identifier is found at a particular position From b280e27dc741a4ab9d9520e2319f4fb1653e88e3 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 4 Oct 2022 09:49:34 -0400 Subject: [PATCH 298/723] gopls/internal/lsp/cache: make IsIntermediateTestVariant a method Metadata.IsIntermediateTestVariant can be derived from PkgPath and ForTest, so make it a method. Along the way, document the easily missed fact that intermediate test variants are not workspace packages. For golang/go#55293 Change-Id: Ie03011aef9c91ebce89e8aad01ef39b65bdde09a Reviewed-on: https://go-review.googlesource.com/c/tools/+/438497 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley --- gopls/internal/lsp/cache/load.go | 15 +++---- gopls/internal/lsp/cache/metadata.go | 62 +++++++++++++++------------- gopls/internal/lsp/cache/snapshot.go | 2 +- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 2bf4a337dfe..287c310a1f8 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -17,11 +17,11 @@ import ( "time" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/span" ) @@ -504,12 +504,6 @@ func buildMetadata(ctx context.Context, pkgPath PackagePath, pkg *packages.Packa } updates[id] = m - // Identify intermediate test variants for later filtering. See the - // documentation of IsIntermediateTestVariant for more information. - if m.ForTest != "" && m.ForTest != m.PkgPath && m.ForTest+"_test" != m.PkgPath { - m.IsIntermediateTestVariant = true - } - for _, err := range pkg.Errors { // Filter out parse errors from go list. We'll get them when we // actually parse, and buggy overlay support may generate spurious @@ -686,6 +680,9 @@ func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[Packag case m.ForTest == m.PkgPath, m.ForTest+"_test" == m.PkgPath: // The test variant of some workspace package or its x_test. // To load it, we need to load the non-test variant with -test. + // + // Notably, this excludes intermediate test variants from workspace + // packages. workspacePackages[m.ID] = m.ForTest } } diff --git a/gopls/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go index 711508c9a11..7778996061b 100644 --- a/gopls/internal/lsp/cache/metadata.go +++ b/gopls/internal/lsp/cache/metadata.go @@ -39,34 +39,6 @@ type Metadata struct { // Config is the *packages.Config associated with the loaded package. Config *packages.Config - - // IsIntermediateTestVariant reports whether the given package is an - // intermediate test variant, e.g. - // "golang.org/x/tools/gopls/internal/lsp/cache [golang.org/x/tools/gopls/internal/lsp/source.test]". - // - // Such test variants arise when an x_test package (in this case source_test) - // imports a package (in this case cache) that itself imports the the - // non-x_test package (in this case source). - // - // This is done so that the forward transitive closure of source_test has - // only one package for the "golang.org/x/tools/gopls/internal/lsp/source" import. - // The intermediate test variant exists to hold the test variant import: - // - // golang.org/x/tools/gopls/internal/lsp/source_test [golang.org/x/tools/gopls/internal/lsp/source.test] - // | "golang.org/x/tools/gopls/internal/lsp/cache" -> golang.org/x/tools/gopls/internal/lsp/cache [golang.org/x/tools/gopls/internal/lsp/source.test] - // | "golang.org/x/tools/gopls/internal/lsp/source" -> golang.org/x/tools/gopls/internal/lsp/source [golang.org/x/tools/gopls/internal/lsp/source.test] - // | ... - // - // golang.org/x/tools/gopls/internal/lsp/cache [golang.org/x/tools/gopls/internal/lsp/source.test] - // | "golang.org/x/tools/gopls/internal/lsp/source" -> golang.org/x/tools/gopls/internal/lsp/source [golang.org/x/tools/gopls/internal/lsp/source.test] - // | ... - // - // We filter these variants out in certain places. For example, there is - // generally no reason to run diagnostics or analysis on them. - // - // TODO(rfindley): this can probably just be a method, since it is derived - // from other fields. - IsIntermediateTestVariant bool } // Name implements the source.Metadata interface. @@ -79,6 +51,40 @@ func (m *Metadata) PackagePath() string { return string(m.PkgPath) } +// IsIntermediateTestVariant reports whether the given package is an +// intermediate test variant, e.g. "net/http [net/url.test]". +// +// Such test variants arise when an x_test package (in this case net/url_test) +// imports a package (in this case net/http) that itself imports the the +// non-x_test package (in this case net/url). +// +// This is done so that the forward transitive closure of net/url_test has +// only one package for the "net/url" import. +// The intermediate test variant exists to hold the test variant import: +// +// net/url_test [net/url.test] +// +// | "net/http" -> net/http [net/url.test] +// | "net/url" -> net/url [net/url.test] +// | ... +// +// net/http [net/url.test] +// +// | "net/url" -> net/url [net/url.test] +// | ... +// +// This restriction propagates throughout the import graph of net/http: for +// every package imported by net/http that imports net/url, there must be an +// intermediate test variant that instead imports "net/url [net/url.test]". +// +// As one can see from the example of net/url and net/http, intermediate test +// variants can result in many additional packages that are essentially (but +// not quite) identical. For this reason, we filter these variants wherever +// possible. +func (m *Metadata) IsIntermediateTestVariant() bool { + return m.ForTest != "" && m.ForTest != m.PkgPath && m.ForTest+"_test" != m.PkgPath +} + // ModuleInfo implements the source.Metadata interface. func (m *Metadata) ModuleInfo() *packages.Module { return m.Module diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index d13f56ca370..fb4e78bfde7 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -695,7 +695,7 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode for _, id := range knownIDs { // Filter out any intermediate test variants. We typically aren't // interested in these packages for file= style queries. - if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant && !withIntermediateTestVariants { + if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant() && !withIntermediateTestVariants { continue } var parseModes []source.ParseMode From 33c2dbf38023a9d7e4263fa0d0c5681c7d5d545f Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Thu, 29 Sep 2022 13:12:00 -0400 Subject: [PATCH 299/723] gopls/internal/lsp: remove extra newlines in vulncheck diagnostics Diagnostics are populated with vuln details that are already formatted with newlines to limit line length. The client UI has its own wrapping logic, so we can remove this to allow the client to do its own formatting. Change-Id: Ida0ce71886add995fad7815e6a302d4b44de65e8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/436775 Run-TryBot: Suzy Mueller TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro --- gopls/internal/lsp/mod/diagnostics.go | 27 +++++++++++++------ .../misc/testdata/vulndb/golang.org/bmod.json | 2 +- gopls/internal/regtest/misc/vuln_test.go | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 2b15ba7488b..61cf594f65d 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -7,6 +7,7 @@ package mod import ( + "bytes" "context" "fmt" @@ -215,14 +216,13 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, } vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: severity, - Source: source.Vulncheck, - Code: v.ID, - CodeHref: v.URL, - // TODO(suzmue): replace the newlines in v.Details to allow the editor to handle formatting. - Message: fmt.Sprintf("%s has a known vulnerability: %s", v.ModPath, v.Details), + URI: fh.URI(), + Range: rng, + Severity: severity, + Source: source.Vulncheck, + Code: v.ID, + CodeHref: v.URL, + Message: formatMessage(&v), SuggestedFixes: fixes, }) } @@ -231,3 +231,14 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return vulnDiagnostics, nil } + +func formatMessage(v *command.Vuln) string { + details := []byte(v.Details) + // Remove any new lines that are not preceded or followed by a new line. + for i, r := range details { + if r == '\n' && i > 0 && details[i-1] != '\n' && i+1 < len(details) && details[i+1] != '\n' { + details[i] = ' ' + } + } + return fmt.Sprintf("%s has a known vulnerability: %s", v.ModPath, string(bytes.TrimSpace(details))) +} diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json index b08b20d256b..5e69cd9fdc6 100644 --- a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json +++ b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json @@ -1,7 +1,7 @@ [ { "id":"GO-2022-99", - "details": "vuln in bmod", + "details": "vuln in bmod\n\nThis is a long description\nof this vulnerability.\n", "affected":[ { "package":{"name":"golang.org/bmod","ecosystem":"Go"}, diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index b0fb1f94d06..180431c8ab4 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -255,7 +255,7 @@ func TestRunVulncheckExp(t *testing.T) { ShownMessage("Found"), OnceMet( env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "golang.org/amod has a known vulnerability: vuln in amod"), - env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/bmod`, "golang.org/bmod has a known vulnerability: vuln in bmod"), + env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/bmod`, "golang.org/bmod has a known vulnerability: vuln in bmod\n\nThis is a long description of this vulnerability."), ReadDiagnostics("go.mod", d), ), ) From 9856077059c266cee0293924a2e80e630973289d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 30 Sep 2022 14:18:37 -0400 Subject: [PATCH 300/723] internal/diff: abolish errors Computing the difference between two strings is logically an infallible operation. This change makes the code reflect that. The actual failures were unreachable given consistent inputs, but that was hard to see from the complexity of the logic surrounding span.Span. (The problem only occurs when converting offsets beyond the end of the file to Spans, but the code preserves the integrity of offsets.) gopls' "old" hooks.ComputeEdits impl (based on sergi/go-diff) now reports a bug and returns a single diff for the entire file if it panics. Also, first steps towards simpler API and a reusable diff package in x/tools: - add TODO for new API. In particular, the diff package shouldn't care about filenames, spans, and URIs. These are gopls concerns. - diff.String is the main diff function. - diff.Unified prints edits in unified form; all its internals are now hidden. - the ComputeEdits func type is moved to gopls (source.DiffFunction) - remove all non-critical uses of myers.ComputeEdits. The only remaining one is in gopls' defaults, but perhaps that gets overridden by the default GoDiff setting in hooks, to BothDiffs (sergi + pjw impls), so maybe it's now actually unused in practice? Change-Id: I6ceb5c670897abbf285b243530a7372dfa41edf6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/436778 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- go/analysis/analysistest/analysistest.go | 15 ++---- gopls/api-diff/api_diff.go | 23 +++------ gopls/internal/hooks/diff.go | 53 ++++++++++++++------ gopls/internal/hooks/hooks.go | 2 +- gopls/internal/lsp/cache/mod_tidy.go | 18 +++---- gopls/internal/lsp/cache/parse.go | 11 ++--- gopls/internal/lsp/cmd/format.go | 4 +- gopls/internal/lsp/cmd/imports.go | 4 +- gopls/internal/lsp/cmd/rename.go | 2 +- gopls/internal/lsp/cmd/suggested_fix.go | 4 +- gopls/internal/lsp/cmd/test/imports.go | 8 +-- gopls/internal/lsp/command.go | 10 +--- gopls/internal/lsp/mod/format.go | 9 ++-- gopls/internal/lsp/source/format.go | 16 ++---- gopls/internal/lsp/source/options.go | 8 ++- gopls/internal/lsp/source/stub.go | 7 +-- gopls/internal/lsp/tests/compare/text.go | 15 +----- gopls/internal/lsp/tests/util.go | 8 +-- gopls/internal/lsp/work/format.go | 9 ++-- internal/diff/diff.go | 20 ++++++-- internal/diff/diff_test.go | 29 +++-------- internal/diff/difftest/difftest.go | 10 ++-- internal/diff/myers/diff.go | 4 +- internal/diff/ndiff.go | 63 ++++++++++++++---------- internal/diff/unified.go | 51 +++++++++---------- 25 files changed, 185 insertions(+), 218 deletions(-) diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index 1fd0a84603f..e65a6951297 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -25,7 +25,6 @@ import ( "golang.org/x/tools/go/analysis/internal/checker" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" @@ -201,11 +200,8 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns continue } if want != string(formatted) { - d, err := myers.ComputeEdits("", want, string(formatted)) - if err != nil { - t.Errorf("failed to compute suggested fix diff: %v", err) - } - t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, d)) + edits := diff.Strings("", want, string(formatted)) + t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.Unified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, edits)) } break } @@ -231,11 +227,8 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns continue } if want != string(formatted) { - d, err := myers.ComputeEdits("", want, string(formatted)) - if err != nil { - t.Errorf("%s: failed to compute suggested fix diff: %s", file.Name(), err) - } - t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(file.Name()+".golden", "actual", want, d)) + edits := diff.Strings("", want, string(formatted)) + t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.Unified(file.Name()+".golden", "actual", want, edits)) } } } diff --git a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go index 8e6c09e275f..8aa333ec612 100644 --- a/gopls/api-diff/api_diff.go +++ b/gopls/api-diff/api_diff.go @@ -22,8 +22,7 @@ import ( "strings" "golang.org/x/tools/gopls/internal/lsp/source" - difflib "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/diff/myers" + diffpkg "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/gocommand" ) @@ -125,7 +124,7 @@ func diff[T JSON](b *strings.Builder, previous, new []T, kind string, uniqueKey c, p := bytes.NewBuffer(nil), bytes.NewBuffer(nil) prev.Write(p) current.Write(c) - if diff, err := diffStr(p.String(), c.String()); err == nil && diff != "" { + if diff := diffStr(p.String(), c.String()); diff != "" { diffFunc(b, prev, current) b.WriteString("\n--\n") } @@ -225,11 +224,8 @@ func diffLenses(b *strings.Builder, previous, current *source.LensJSON) { func diffOptions(b *strings.Builder, previous, current *source.OptionJSON) { b.WriteString(fmt.Sprintf("Changes to option %s:\n\n", current.Name)) if previous.Doc != current.Doc { - diff, err := diffStr(previous.Doc, current.Doc) - if err != nil { - panic(err) - } - b.WriteString(fmt.Sprintf("Documentation changed:\n%s\n", diff)) + diff := diffStr(previous.Doc, current.Doc) + fmt.Fprintf(b, "Documentation changed:\n%s\n", diff) } if previous.Default != current.Default { b.WriteString(fmt.Sprintf("Default changed from %q to %q\n", previous.Default, current.Default)) @@ -259,16 +255,13 @@ func formatBlock(str string) string { return "\n```\n" + str + "\n```\n" } -func diffStr(before, after string) (string, error) { +func diffStr(before, after string) string { // Add newlines to avoid newline messages in diff. if before == after { - return "", nil + return "" } before += "\n" after += "\n" - d, err := myers.ComputeEdits("", before, after) - if err != nil { - return "", err - } - return fmt.Sprintf("%q", difflib.ToUnified("previous", "current", before, d)), err + edits := diffpkg.Strings("irrelevant", before, after) + return fmt.Sprintf("%q", diffpkg.Unified("previous", "current", before, edits)) } diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go index 46faae12f3e..25b1d9566dd 100644 --- a/gopls/internal/hooks/diff.go +++ b/gopls/internal/hooks/diff.go @@ -8,6 +8,7 @@ import ( "crypto/rand" "encoding/json" "fmt" + "go/token" "io" "log" "math/big" @@ -20,6 +21,7 @@ import ( "unicode" "github.com/sergi/go-diff/diffmatchpatch" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/span" ) @@ -142,48 +144,67 @@ func initrepl(n int) []rune { // BothDiffs edits calls both the new and old diffs, checks that the new diffs // change before into after, and attempts to preserve some statistics. -func BothDiffs(uri span.URI, before, after string) (edits []diff.TextEdit, err error) { +func BothDiffs(uri span.URI, before, after string) (edits []diff.TextEdit) { // The new diff code contains a lot of internal checks that panic when they // fail. This code catches the panics, or other failures, tries to save // the failing example (and ut wiykd ask the user to send it back to us, and // changes options.newDiff to 'old', if only we could figure out how.) stat := diffstat{Before: len(before), After: len(after)} now := time.Now() - Oldedits, oerr := ComputeEdits(uri, before, after) - if oerr != nil { - stat.Msg += fmt.Sprintf("old:%v", oerr) - } - stat.Oldedits = len(Oldedits) + oldedits := ComputeEdits(uri, before, after) + stat.Oldedits = len(oldedits) stat.Oldtime = time.Since(now) defer func() { if r := recover(); r != nil { disaster(before, after) - edits, err = Oldedits, oerr + edits = oldedits } }() now = time.Now() - Newedits, rerr := diff.NComputeEdits(uri, before, after) - stat.Newedits = len(Newedits) + newedits := diff.Strings(uri, before, after) + stat.Newedits = len(newedits) stat.Newtime = time.Now().Sub(now) - got := diff.ApplyEdits(before, Newedits) + got := diff.ApplyEdits(before, newedits) if got != after { stat.Msg += "FAIL" disaster(before, after) stat.save() - return Oldedits, oerr + return oldedits } stat.save() - return Newedits, rerr + return newedits } -func ComputeEdits(uri span.URI, before, after string) (edits []diff.TextEdit, err error) { +// ComputeEdits computes a diff using the github.com/sergi/go-diff implementation. +func ComputeEdits(uri span.URI, before, after string) (edits []diff.TextEdit) { // The go-diff library has an unresolved panic (see golang/go#278774). // TODO(rstambler): Remove the recover once the issue has been fixed // upstream. defer func() { if r := recover(); r != nil { - edits = nil - err = fmt.Errorf("unable to compute edits for %s: %s", uri.Filename(), r) + bug.Reportf("unable to compute edits for %s: %s", uri.Filename(), r) + + // Report one big edit for the whole file. + + // Reuse the machinery of go/token to convert (content, offset) + // to (line, column). + tf := token.NewFileSet().AddFile(uri.Filename(), -1, len(before)) + tf.SetLinesForContent([]byte(before)) + offsetToPoint := func(offset int) span.Point { + // Re-use span.ToPosition since it contains more than + // just token.File operations (bug workarounds). + // But it can only fail if the diff was ill-formed. + line, col, err := span.ToPosition(tf, offset) + if err != nil { + log.Fatalf("invalid offset: %v", err) + } + return span.NewPoint(line, col, offset) + } + all := span.New(uri, offsetToPoint(0), offsetToPoint(len(before))) + edits = []diff.TextEdit{{ + Span: all, + NewText: after, + }} } }() diffs := diffmatchpatch.New().DiffMain(before, after, true) @@ -201,5 +222,5 @@ func ComputeEdits(uri span.URI, before, after string) (edits []diff.TextEdit, er edits = append(edits, diff.TextEdit{Span: span.New(uri, start, span.Point{}), NewText: d.Text}) } } - return edits, nil + return edits } diff --git a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go index 7c4ac9df8af..085fa53a39a 100644 --- a/gopls/internal/hooks/hooks.go +++ b/gopls/internal/hooks/hooks.go @@ -23,7 +23,7 @@ func Options(options *source.Options) { case "old": options.ComputeEdits = ComputeEdits case "new": - options.ComputeEdits = diff.NComputeEdits + options.ComputeEdits = diff.Strings default: options.ComputeEdits = BothDiffs } diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index 1bbc6238056..5570417967c 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -16,13 +16,12 @@ import ( "strings" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/span" ) @@ -313,7 +312,7 @@ func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagno // directnessDiagnostic extracts errors when a dependency is labeled indirect when // it should be direct and vice versa. -func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Diagnostic, error) { +func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits source.DiffFunction) (*source.Diagnostic, error) { rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End) if err != nil { return nil, err @@ -386,7 +385,7 @@ func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*so // switchDirectness gets the edits needed to change an indirect dependency to // direct and vice versa. -func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) { +func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits source.DiffFunction) ([]protocol.TextEdit, error) { // We need a private copy of the parsed go.mod file, since we're going to // modify it. copied, err := modfile.Parse("", m.Content, nil) @@ -420,11 +419,8 @@ func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdi return nil, err } // Calculate the edits to be made due to the change. - diff, err := computeEdits(m.URI, string(m.Content), string(newContent)) - if err != nil { - return nil, err - } - return source.ToProtocolEdits(m, diff) + edits := computeEdits(m.URI, string(m.Content), string(newContent)) + return source.ToProtocolEdits(m, edits) } // missingModuleForImport creates an error for a given import path that comes diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index 981e698fb6e..5c084fac0c4 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -22,7 +22,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/memoize" @@ -158,13 +157,9 @@ func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, // it is likely we got stuck in a loop somehow. Log out a diff // of the last changes we made to aid in debugging. if i == 9 { - edits, err := myers.ComputeEdits(fh.URI(), string(src), string(newSrc)) - if err != nil { - event.Error(ctx, "error generating fixSrc diff", err, tag.File.Of(tok.Name())) - } else { - unified := diff.ToUnified("before", "after", string(src), edits) - event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name())) - } + edits := diff.Strings(fh.URI(), string(src), string(newSrc)) + unified := diff.Unified("before", "after", string(src), edits) + event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name())) } newFile, _ := parser.ParseFile(fset, fh.URI().Filename(), newSrc, parserMode) diff --git a/gopls/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go index 409850642fe..b2f2afa1c0b 100644 --- a/gopls/internal/lsp/cmd/format.go +++ b/gopls/internal/lsp/cmd/format.go @@ -10,9 +10,9 @@ import ( "fmt" "io/ioutil" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/span" ) @@ -96,7 +96,7 @@ func (c *format) Run(ctx context.Context, args ...string) error { } if c.Diff { printIt = false - u := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + u := diff.Unified(filename+".orig", filename, string(file.mapper.Content), sedits) fmt.Print(u) } if printIt { diff --git a/gopls/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go index d6a968d5e03..eb564ee16ab 100644 --- a/gopls/internal/lsp/cmd/imports.go +++ b/gopls/internal/lsp/cmd/imports.go @@ -10,9 +10,9 @@ import ( "fmt" "io/ioutil" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) @@ -94,7 +94,7 @@ func (t *imports) Run(ctx context.Context, args ...string) error { ioutil.WriteFile(filename, []byte(newContent), 0644) } case t.Diff: - diffs := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + diffs := diff.Unified(filename+".orig", filename, string(file.mapper.Content), sedits) fmt.Print(diffs) default: fmt.Print(string(newContent)) diff --git a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go index 5f160038480..a330bae9480 100644 --- a/gopls/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -112,7 +112,7 @@ func (r *rename) Run(ctx context.Context, args ...string) error { } ioutil.WriteFile(filename, []byte(newContent), 0644) case r.Diff: - diffs := diff.ToUnified(filename+".orig", filename, string(cmdFile.mapper.Content), renameEdits) + diffs := diff.Unified(filename+".orig", filename, string(cmdFile.mapper.Content), renameEdits) fmt.Print(diffs) default: if len(orderedURIs) > 1 { diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go index 048dcd62e2a..faf681f856a 100644 --- a/gopls/internal/lsp/cmd/suggested_fix.go +++ b/gopls/internal/lsp/cmd/suggested_fix.go @@ -10,9 +10,9 @@ import ( "fmt" "io/ioutil" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) @@ -155,7 +155,7 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { ioutil.WriteFile(filename, []byte(newContent), 0644) } case s.Diff: - diffs := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + diffs := diff.Unified(filename+".orig", filename, string(file.mapper.Content), sedits) fmt.Print(diffs) default: fmt.Print(string(newContent)) diff --git a/gopls/internal/lsp/cmd/test/imports.go b/gopls/internal/lsp/cmd/test/imports.go index fd4162687c2..67826a47095 100644 --- a/gopls/internal/lsp/cmd/test/imports.go +++ b/gopls/internal/lsp/cmd/test/imports.go @@ -8,7 +8,6 @@ import ( "testing" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/span" ) @@ -20,10 +19,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) { return []byte(got), nil })) if want != got { - d, err := myers.ComputeEdits(uri, want, got) - if err != nil { - t.Fatal(err) - } - t.Errorf("imports failed for %s, expected:\n%s", filename, diff.ToUnified("want", "got", want, d)) + edits := diff.Strings(uri, want, got) + t.Errorf("imports failed for %s, expected:\n%s", filename, diff.Unified("want", "got", want, edits)) } } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 5ded3467602..e9483b6df1b 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -390,10 +390,7 @@ func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePat return nil, err } // Calculate the edits to be made due to the change. - diff, err := snapshot.View().Options().ComputeEdits(pm.URI, string(pm.Mapper.Content), string(newContent)) - if err != nil { - return nil, err - } + diff := snapshot.View().Options().ComputeEdits(pm.URI, string(pm.Mapper.Content), string(newContent)) return source.ToProtocolEdits(pm.Mapper, diff) } @@ -615,10 +612,7 @@ func applyFileEdits(ctx context.Context, snapshot source.Snapshot, uri span.URI, } m := protocol.NewColumnMapper(fh.URI(), oldContent) - diff, err := snapshot.View().Options().ComputeEdits(uri, string(oldContent), string(newContent)) - if err != nil { - return nil, err - } + diff := snapshot.View().Options().ComputeEdits(uri, string(oldContent), string(newContent)) edits, err := source.ToProtocolEdits(m, diff) if err != nil { return nil, err diff --git a/gopls/internal/lsp/mod/format.go b/gopls/internal/lsp/mod/format.go index d2bce4c8a75..010d2ace2b4 100644 --- a/gopls/internal/lsp/mod/format.go +++ b/gopls/internal/lsp/mod/format.go @@ -7,9 +7,9 @@ package mod import ( "context" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" ) func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) { @@ -25,9 +25,6 @@ func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) return nil, err } // Calculate the edits to be made due to the change. - diff, err := snapshot.View().Options().ComputeEdits(fh.URI(), string(pm.Mapper.Content), string(formatted)) - if err != nil { - return nil, err - } - return source.ToProtocolEdits(pm.Mapper, diff) + diffs := snapshot.View().Options().ComputeEdits(fh.URI(), string(pm.Mapper.Content), string(formatted)) + return source.ToProtocolEdits(pm.Mapper, diffs) } diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index e2301171be6..90805957544 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -16,12 +16,12 @@ import ( "strings" "text/scanner" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/gopls/internal/lsp/lsppos" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/imports" ) // Format formats a file with a given range. @@ -199,10 +199,7 @@ func computeFixEdits(snapshot Snapshot, pgf *ParsedGoFile, options *imports.Opti if fixedData == nil || fixedData[len(fixedData)-1] != '\n' { fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure. } - edits, err := snapshot.View().Options().ComputeEdits(pgf.URI, left, string(fixedData)) - if err != nil { - return nil, err - } + edits := snapshot.View().Options().ComputeEdits(pgf.URI, left, string(fixedData)) return ProtocolEditsFromSource([]byte(left), edits, pgf.Mapper.TokFile) } @@ -312,10 +309,7 @@ func computeTextEdits(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile, _, done := event.Start(ctx, "source.computeTextEdits") defer done() - edits, err := snapshot.View().Options().ComputeEdits(pgf.URI, string(pgf.Src), formatted) - if err != nil { - return nil, err - } + edits := snapshot.View().Options().ComputeEdits(pgf.URI, string(pgf.Src), formatted) return ToProtocolEdits(pgf.Mapper, edits) } diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 93d09489e7e..815922115ee 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -67,6 +67,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/diff/myers" + "golang.org/x/tools/internal/span" ) var ( @@ -167,6 +168,7 @@ func DefaultOptions() *Options { ChattyDiagnostics: true, }, Hooks: Hooks{ + // TODO(adonovan): switch to new diff.Strings implementation. ComputeEdits: myers.ComputeEdits, URLRegexp: urlRegexp(), DefaultAnalyzers: defaultAnalyzers(), @@ -497,6 +499,10 @@ func (u *UserOptions) SetEnvSlice(env []string) { } } +// DiffFunction is the type for a function that produces a set of edits that +// convert from the before content to the after content. +type DiffFunction func(uri span.URI, before, after string) []diff.TextEdit + // Hooks contains configuration that is provided to the Gopls command by the // main package. type Hooks struct { @@ -510,7 +516,7 @@ type Hooks struct { StaticcheckSupported bool // ComputeEdits is used to compute edits between file versions. - ComputeEdits diff.ComputeEdits + ComputeEdits DiffFunction // URLRegexp is used to find potential URLs in comments/strings. // diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 9a17802cca8..d0dff4f72f8 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -80,12 +80,9 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi if err != nil { return nil, fmt.Errorf("format.Node: %w", err) } - diffEdits, err := snapshot.View().Options().ComputeEdits(parsedConcreteFile.URI, string(parsedConcreteFile.Src), source.String()) - if err != nil { - return nil, err - } + diffs := snapshot.View().Options().ComputeEdits(parsedConcreteFile.URI, string(parsedConcreteFile.Src), source.String()) var edits []analysis.TextEdit - for _, edit := range diffEdits { + for _, edit := range diffs { rng, err := edit.Span.Range(parsedConcreteFile.Mapper.TokFile) if err != nil { return nil, err diff --git a/gopls/internal/lsp/tests/compare/text.go b/gopls/internal/lsp/tests/compare/text.go index a3023967385..66d2e620ab6 100644 --- a/gopls/internal/lsp/tests/compare/text.go +++ b/gopls/internal/lsp/tests/compare/text.go @@ -5,8 +5,6 @@ package compare import ( - "fmt" - "golang.org/x/tools/internal/diff" ) @@ -24,17 +22,8 @@ func Text(want, got string) string { want += "\n" got += "\n" - d, err := diff.NComputeEdits("", want, got) - - // Panic on errors. - // - // TODO(rfindley): refactor so that this function doesn't need to panic. - // Computing diffs should never fail. - if err != nil { - panic(fmt.Sprintf("computing edits failed: %v", err)) - } - - diff := diff.ToUnified("want", "got", want, d).String() + edits := diff.Strings("irrelevant", want, got) + diff := diff.Unified("want", "got", want, edits) // Defensively assert that we get an actual diff, so that we guarantee the // invariant that we return "" if and only if want == got. diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index aa9381d4cac..d463ba0e8ea 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -21,7 +21,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/completion" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/span" ) @@ -231,11 +230,8 @@ func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) (string, e w := want.Signatures[0] if NormalizeAny(w.Label) != NormalizeAny(g.Label) { wLabel := w.Label + "\n" - d, err := myers.ComputeEdits("", wLabel, g.Label+"\n") - if err != nil { - return "", err - } - return decorate("mismatched labels:\n%q", diff.ToUnified("want", "got", wLabel, d)), err + edits := diff.Strings("", wLabel, g.Label+"\n") + return decorate("mismatched labels:\n%q", diff.Unified("want", "got", wLabel, edits)), nil } var paramParts []string for _, p := range g.Parameters { diff --git a/gopls/internal/lsp/work/format.go b/gopls/internal/lsp/work/format.go index ef5ac46dd8c..bc84464af96 100644 --- a/gopls/internal/lsp/work/format.go +++ b/gopls/internal/lsp/work/format.go @@ -8,9 +8,9 @@ import ( "context" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" ) func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) { @@ -23,9 +23,6 @@ func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) } formatted := modfile.Format(pw.File.Syntax) // Calculate the edits to be made due to the change. - diff, err := snapshot.View().Options().ComputeEdits(fh.URI(), string(pw.Mapper.Content), string(formatted)) - if err != nil { - return nil, err - } - return source.ToProtocolEdits(pw.Mapper, diff) + diffs := snapshot.View().Options().ComputeEdits(fh.URI(), string(pw.Mapper.Content), string(formatted)) + return source.ToProtocolEdits(pw.Mapper, diffs) } diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 8fd6824e530..b00ffbc7cb2 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -12,6 +12,19 @@ import ( "golang.org/x/tools/internal/span" ) +// TODO(adonovan): simplify this package to just: +// +// package diff +// type Edit struct { Start, End int; New string } +// func Strings(old, new string) []Edit +// func Unified(oldLabel, newLabel string, old string, edits []Edit) string +// func Apply(old string, edits []Edit) (string, error) +// +// and move everything else into gopls, including the concepts of filenames and spans. +// Observe that TextEdit.URI is irrelevant to Unified. +// - delete LineEdits? (used only by Unified and test) +// - delete Lines (unused except by its test) + // TextEdit represents a change to a section of a document. // The text within the specified span should be replaced by the supplied new text. type TextEdit struct { @@ -19,10 +32,6 @@ type TextEdit struct { NewText string } -// ComputeEdits is the type for a function that produces a set of edits that -// convert from the before content to the after content. -type ComputeEdits func(uri span.URI, before, after string) ([]TextEdit, error) - // SortTextEdits attempts to order all edits by their starting points. // The sort is stable so that edits with the same starting point will not // be reordered. @@ -37,6 +46,9 @@ func SortTextEdits(d []TextEdit) { // content. // It may panic or produce garbage if the edits are not valid for the provided // before content. +// TODO(adonovan): this function must not panic! Make it either cope +// or report an error. We should not trust that (e.g.) patches supplied +// as RPC inputs to gopls are consistent. func ApplyEdits(before string, edits []TextEdit) string { // Preconditions: // - all of the edits apply to before diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index b19929801f0..11930dea4c1 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -33,10 +33,7 @@ func TestApplyEdits(t *testing.T) { func TestNEdits(t *testing.T) { for i, tc := range difftest.TestCases { sp := fmt.Sprintf("file://%s.%d", tc.Name, i) - edits, err := diff.NComputeEdits(span.URI(sp), tc.In, tc.Out) - if err != nil { - t.Fatal(err) - } + edits := diff.Strings(span.URI(sp), tc.In, tc.Out) got := diff.ApplyEdits(tc.In, edits) if got != tc.Out { t.Fatalf("%s: got %q wanted %q", tc.Name, got, tc.Out) @@ -53,10 +50,7 @@ func TestNRandom(t *testing.T) { fname := fmt.Sprintf("file://%x", i) a := randstr("abω", 16) b := randstr("abωc", 16) - edits, err := diff.NComputeEdits(span.URI(fname), a, b) - if err != nil { - t.Fatalf("%q,%q %v", a, b, err) - } + edits := diff.Strings(span.URI(fname), a, b) got := diff.ApplyEdits(a, edits) if got != b { t.Fatalf("%d: got %q, wanted %q, starting with %q", i, got, b, a) @@ -84,10 +78,7 @@ func TestNLinesRandom(t *testing.T) { y = y[:len(y)-1] } a, b := strings.SplitAfter(x, "\n"), strings.SplitAfter(y, "\n") - edits, err := diff.NComputeLineEdits(span.URI(fname), a, b) - if err != nil { - t.Fatalf("%q,%q %v", a, b, err) - } + edits := diff.Lines(span.URI(fname), a, b) got := diff.ApplyEdits(x, edits) if got != y { t.Fatalf("%d: got\n%q, wanted\n%q, starting with %q", i, got, y, a) @@ -113,12 +104,12 @@ func TestLineEdits(t *testing.T) { func TestUnified(t *testing.T) { for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { - unified := fmt.Sprint(diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.Edits)) + unified := diff.Unified(difftest.FileA, difftest.FileB, tc.In, tc.Edits) if unified != tc.Unified { t.Errorf("Unified(Edits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) } if tc.LineEdits != nil { - unified := fmt.Sprint(diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.LineEdits)) + unified := diff.Unified(difftest.FileA, difftest.FileB, tc.In, tc.LineEdits) if unified != tc.Unified { t.Errorf("Unified(LineEdits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) } @@ -131,10 +122,7 @@ func TestRegressionOld001(t *testing.T) { a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" - diffs, err := diff.NComputeEdits(span.URI("file://one"), a, b) - if err != nil { - t.Error(err) - } + diffs := diff.Strings(span.URI("file://one"), a, b) got := diff.ApplyEdits(a, diffs) if got != b { i := 0 @@ -148,10 +136,7 @@ func TestRegressionOld001(t *testing.T) { func TestRegressionOld002(t *testing.T) { a := "n\"\n)\n" b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n" - diffs, err := diff.NComputeEdits(span.URI("file://two"), a, b) - if err != nil { - t.Error(err) - } + diffs := diff.Strings(span.URI("file://two"), a, b) got := diff.ApplyEdits(a, diffs) if got != b { i := 0 diff --git a/internal/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go index e42e0d7cbc5..c9808a55027 100644 --- a/internal/diff/difftest/difftest.go +++ b/internal/diff/difftest/difftest.go @@ -8,7 +8,6 @@ package difftest import ( - "fmt" "testing" "golang.org/x/tools/internal/diff" @@ -246,15 +245,12 @@ func init() { } } -func DiffTest(t *testing.T, compute diff.ComputeEdits) { +func DiffTest(t *testing.T, compute func(uri span.URI, before, after string) []diff.TextEdit) { for _, test := range TestCases { t.Run(test.Name, func(t *testing.T) { - edits, err := compute(span.URIFromPath("/"+test.Name), test.In, test.Out) - if err != nil { - t.Fatal(err) - } + edits := compute(span.URIFromPath("/"+test.Name), test.In, test.Out) got := diff.ApplyEdits(test.In, edits) - unified := fmt.Sprint(diff.ToUnified(FileA, FileB, test.In, edits)) + unified := diff.Unified(FileA, FileB, test.In, edits) if got != test.Out { t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out) } diff --git a/internal/diff/myers/diff.go b/internal/diff/myers/diff.go index 07e40281107..2188c0d4548 100644 --- a/internal/diff/myers/diff.go +++ b/internal/diff/myers/diff.go @@ -16,7 +16,7 @@ import ( // https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/ // https://www.codeproject.com/Articles/42279/%2FArticles%2F42279%2FInvestigating-Myers-diff-algorithm-Part-1-of-2 -func ComputeEdits(uri span.URI, before, after string) ([]diff.TextEdit, error) { +func ComputeEdits(uri span.URI, before, after string) []diff.TextEdit { ops := operations(splitLines(before), splitLines(after)) edits := make([]diff.TextEdit, 0, len(ops)) for _, op := range ops { @@ -32,7 +32,7 @@ func ComputeEdits(uri span.URI, before, after string) ([]diff.TextEdit, error) { } } } - return edits, nil + return edits } type operation struct { diff --git a/internal/diff/ndiff.go b/internal/diff/ndiff.go index e537b72458d..e369f4600ab 100644 --- a/internal/diff/ndiff.go +++ b/internal/diff/ndiff.go @@ -5,6 +5,8 @@ package diff import ( + "go/token" + "log" "strings" "unicode/utf8" @@ -16,51 +18,66 @@ import ( // the value is just a guess const maxDiffs = 30 -// NComputeEdits computes TextEdits for strings -// (both it and the diff in the myers package have type ComputeEdits, which +// Strings computes the differences between two strings. +// (Both it and the diff in the myers package have type ComputeEdits, which // is why the arguments are strings, not []bytes.) -func NComputeEdits(uri span.URI, before, after string) ([]TextEdit, error) { +// TODO(adonovan): opt: consider switching everything to []bytes, if +// that's the more common type in practice. Or provide both flavors? +func Strings(uri span.URI, before, after string) []TextEdit { if before == after { // very frequently true - return nil, nil + return nil } // the diffs returned by the lcs package use indexes into whatever slice - // was passed in. TextEdits need a span.Span which is computed with + // was passed in. TextEdits need a span.Span which is computed with // byte offsets, so rune or line offsets need to be converted. + // TODO(adonovan): opt: eliminate all the unnecessary allocations. if needrunes(before) || needrunes(after) { diffs, _ := lcs.Compute([]rune(before), []rune(after), maxDiffs/2) diffs = runeOffsets(diffs, []rune(before)) - ans, err := convertDiffs(uri, diffs, []byte(before)) - return ans, err + return convertDiffs(uri, diffs, []byte(before)) } else { diffs, _ := lcs.Compute([]byte(before), []byte(after), maxDiffs/2) - ans, err := convertDiffs(uri, diffs, []byte(before)) - return ans, err + return convertDiffs(uri, diffs, []byte(before)) } } -// NComputeLineEdits computes TextEdits for []strings -func NComputeLineEdits(uri span.URI, before, after []string) ([]TextEdit, error) { +// Lines computes the differences between two list of lines. +// TODO(adonovan): unused except by its test. Do we actually need it? +func Lines(uri span.URI, before, after []string) []TextEdit { diffs, _ := lcs.Compute(before, after, maxDiffs/2) diffs = lineOffsets(diffs, before) - ans, err := convertDiffs(uri, diffs, []byte(strJoin(before))) + return convertDiffs(uri, diffs, []byte(strJoin(before))) // the code is not coping with possible missing \ns at the ends - return ans, err } // convert diffs with byte offsets into diffs with line and column -func convertDiffs(uri span.URI, diffs []lcs.Diff, src []byte) ([]TextEdit, error) { +func convertDiffs(uri span.URI, diffs []lcs.Diff, src []byte) []TextEdit { ans := make([]TextEdit, len(diffs)) - tf := span.NewTokenFile(uri.Filename(), src) - for i, d := range diffs { - s := newSpan(uri, d.Start, d.End) - s, err := s.WithPosition(tf) + + // Reuse the machinery of go/token to convert (content, offset) to (line, column). + tf := token.NewFileSet().AddFile("", -1, len(src)) + tf.SetLinesForContent(src) + + offsetToPoint := func(offset int) span.Point { + // Re-use span.ToPosition's EOF workaround. + // It is infallible if the diffs are consistent with src. + line, col, err := span.ToPosition(tf, offset) if err != nil { - return nil, err + log.Fatalf("invalid offset: %v", err) } - ans[i] = TextEdit{s, d.Text} + return span.NewPoint(line, col, offset) } - return ans, nil + + for i, d := range diffs { + start := offsetToPoint(d.Start) + end := start + if d.End != d.Start { + end = offsetToPoint(d.End) + } + ans[i] = TextEdit{span.New(uri, start, end), d.Text} + } + return ans } // convert diffs with rune offsets into diffs with byte offsets @@ -114,10 +131,6 @@ func strJoin(elems []string) string { return b.String() } -func newSpan(uri span.URI, left, right int) span.Span { - return span.New(uri, span.NewPoint(0, 0, left), span.NewPoint(0, 0, right)) -} - // need runes is true if the string needs to be converted to []rune // for random access func needrunes(s string) bool { diff --git a/internal/diff/unified.go b/internal/diff/unified.go index 3ea06974b84..13ef677b296 100644 --- a/internal/diff/unified.go +++ b/internal/diff/unified.go @@ -9,28 +9,34 @@ import ( "strings" ) -// Unified represents a set of edits as a unified diff. -type Unified struct { +// Unified applies the edits to oldContent and presents a unified diff. +// The two labels are the names of the old and new files. +func Unified(oldLabel, newLabel string, oldContent string, edits []TextEdit) string { + return toUnified(oldLabel, newLabel, oldContent, edits).String() +} + +// unified represents a set of edits as a unified diff. +type unified struct { // From is the name of the original file. From string // To is the name of the modified file. To string // Hunks is the set of edit hunks needed to transform the file content. - Hunks []*Hunk + Hunks []*hunk } // Hunk represents a contiguous set of line edits to apply. -type Hunk struct { +type hunk struct { // The line in the original source where the hunk starts. FromLine int // The line in the original source where the hunk finishes. ToLine int // The set of line based edits to apply. - Lines []Line + Lines []line } // Line represents a single line operation to apply as part of a Hunk. -type Line struct { +type line struct { // Kind is the type of line this represents, deletion, insertion or copy. Kind OpKind // Content is the content of this line. @@ -40,6 +46,7 @@ type Line struct { } // OpKind is used to denote the type of operation a line represents. +// TODO(adonovan): hide this once the myers package no longer references it. type OpKind int const ( @@ -73,10 +80,10 @@ const ( gap = edge * 2 ) -// ToUnified takes a file contents and a sequence of edits, and calculates +// toUnified takes a file contents and a sequence of edits, and calculates // a unified diff that represents those edits. -func ToUnified(fromName, toName string, content string, edits []TextEdit) Unified { - u := Unified{ +func toUnified(fromName, toName string, content string, edits []TextEdit) unified { + u := unified{ From: fromName, To: toName, } @@ -88,7 +95,7 @@ func ToUnified(fromName, toName string, content string, edits []TextEdit) Unifie edits = lineEdits(content, edits) } lines := splitLines(content) - var h *Hunk + var h *hunk last := 0 toLine := 0 for _, edit := range edits { @@ -108,7 +115,7 @@ func ToUnified(fromName, toName string, content string, edits []TextEdit) Unifie u.Hunks = append(u.Hunks, h) } toLine += start - last - h = &Hunk{ + h = &hunk{ FromLine: start + 1, ToLine: toLine + 1, } @@ -119,12 +126,12 @@ func ToUnified(fromName, toName string, content string, edits []TextEdit) Unifie } last = start for i := start; i < end; i++ { - h.Lines = append(h.Lines, Line{Kind: Delete, Content: lines[i]}) + h.Lines = append(h.Lines, line{Kind: Delete, Content: lines[i]}) last++ } if edit.NewText != "" { - for _, line := range splitLines(edit.NewText) { - h.Lines = append(h.Lines, Line{Kind: Insert, Content: line}) + for _, content := range splitLines(edit.NewText) { + h.Lines = append(h.Lines, line{Kind: Insert, Content: content}) toLine++ } } @@ -145,7 +152,7 @@ func splitLines(text string) []string { return lines } -func addEqualLines(h *Hunk, lines []string, start, end int) int { +func addEqualLines(h *hunk, lines []string, start, end int) int { delta := 0 for i := start; i < end; i++ { if i < 0 { @@ -154,25 +161,15 @@ func addEqualLines(h *Hunk, lines []string, start, end int) int { if i >= len(lines) { return delta } - h.Lines = append(h.Lines, Line{Kind: Equal, Content: lines[i]}) + h.Lines = append(h.Lines, line{Kind: Equal, Content: lines[i]}) delta++ } return delta } -// Format write a textual representation of u to f (see the String method). -// -// TODO(rfindley): investigate (and possibly remove) this method. It's not -// clear why Unified implements fmt.Formatter, since the formatting rune is not -// used. Probably it is sufficient to only implement Stringer, but this method -// was left here defensively. -func (u Unified) Format(f fmt.State, r rune) { - fmt.Fprintf(f, "%s", u.String()) -} - // String converts a unified diff to the standard textual form for that diff. // The output of this function can be passed to tools like patch. -func (u Unified) String() string { +func (u unified) String() string { if len(u.Hunks) == 0 { return "" } From d96b2388c6140f7db7021cead0968958a343057a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 30 Sep 2022 21:58:21 -0400 Subject: [PATCH 301/723] internal/diff: simplify API, break span dependency diff.TextEdit (now called simply Edit) no longer has a Span, and no longer depends on the span package, which is really part of gopls. Instead, it records only start/end byte offsets within an (implied) file. The diff algorithms have been simplified to avoid the need to map offsets to line/column numbers (e.g. using a token.File). All the conditions actually needed by the logic can be derived by local string operations on the source text. This change will allow us to move the span package into the gopls module. I was expecting that gopls would want to define its own Span-augmented TextEdit type but, surprisingly, diff.Edit is quite convenient to use throughout the entire repo: in all places in gopls that manipulate Edits, the implied file is obvious. In most cases, less conversion boilerplate is required than before. API Notes: - diff.TextEdit -> Edit (it needn't be text) - diff.ApplyEdits -> Apply - source.protocolEditsFromSource is now private Change-Id: I4d7cef078dfbd189b4aef477f845db320af6c5f6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/436781 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- go/analysis/analysistest/analysistest.go | 27 +-- gopls/api-diff/api_diff.go | 2 +- gopls/internal/hooks/diff.go | 46 ++-- gopls/internal/lsp/cache/mod_tidy.go | 2 +- gopls/internal/lsp/cache/parse.go | 2 +- gopls/internal/lsp/cmd/format.go | 2 +- gopls/internal/lsp/cmd/imports.go | 2 +- gopls/internal/lsp/cmd/rename.go | 2 +- gopls/internal/lsp/cmd/suggested_fix.go | 2 +- gopls/internal/lsp/cmd/test/imports.go | 2 +- gopls/internal/lsp/command.go | 4 +- gopls/internal/lsp/fake/edit.go | 2 +- gopls/internal/lsp/lsp_test.go | 11 +- gopls/internal/lsp/mod/format.go | 2 +- gopls/internal/lsp/protocol/span.go | 29 +++ gopls/internal/lsp/source/completion/util.go | 19 +- gopls/internal/lsp/source/format.go | 40 ++-- gopls/internal/lsp/source/options.go | 3 +- gopls/internal/lsp/source/rename.go | 43 ++-- gopls/internal/lsp/source/source_test.go | 11 +- gopls/internal/lsp/source/stub.go | 13 +- gopls/internal/lsp/tests/compare/text.go | 2 +- gopls/internal/lsp/tests/util.go | 2 +- gopls/internal/lsp/work/format.go | 2 +- internal/diff/diff.go | 220 ++++++++----------- internal/diff/diff_test.go | 69 +++--- internal/diff/difftest/difftest.go | 104 ++++----- internal/diff/difftest/difftest_test.go | 2 +- internal/diff/myers/diff.go | 28 ++- internal/diff/ndiff.go | 62 ++---- internal/diff/unified.go | 31 +-- internal/span/span.go | 2 +- internal/span/token.go | 2 +- 33 files changed, 367 insertions(+), 425 deletions(-) diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index e65a6951297..bc25b9f2b78 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -25,7 +25,6 @@ import ( "golang.org/x/tools/go/analysis/internal/checker" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" ) @@ -113,7 +112,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns // should match up. for _, act := range r { // file -> message -> edits - fileEdits := make(map[*token.File]map[string][]diff.TextEdit) + fileEdits := make(map[*token.File]map[string][]diff.Edit) fileContents := make(map[*token.File][]byte) // Validate edits, prepare the fileEdits map and read the file contents. @@ -141,17 +140,13 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns } fileContents[file] = contents } - spn, err := span.NewRange(file, edit.Pos, edit.End).Span() - if err != nil { - t.Errorf("error converting edit to span %s: %v", file.Name(), err) - } - if _, ok := fileEdits[file]; !ok { - fileEdits[file] = make(map[string][]diff.TextEdit) + fileEdits[file] = make(map[string][]diff.Edit) } - fileEdits[file][sf.Message] = append(fileEdits[file][sf.Message], diff.TextEdit{ - Span: spn, - NewText: string(edit.NewText), + fileEdits[file][sf.Message] = append(fileEdits[file][sf.Message], diff.Edit{ + Start: file.Offset(edit.Pos), + End: file.Offset(edit.End), + New: string(edit.NewText), }) } } @@ -188,7 +183,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns for _, vf := range ar.Files { if vf.Name == sf { found = true - out := diff.ApplyEdits(string(orig), edits) + out := diff.Apply(string(orig), edits) // the file may contain multiple trailing // newlines if the user places empty lines // between files in the archive. normalize @@ -200,7 +195,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns continue } if want != string(formatted) { - edits := diff.Strings("", want, string(formatted)) + edits := diff.Strings(want, string(formatted)) t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.Unified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, edits)) } break @@ -213,12 +208,12 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns } else { // all suggested fixes are represented by a single file - var catchallEdits []diff.TextEdit + var catchallEdits []diff.Edit for _, edits := range fixes { catchallEdits = append(catchallEdits, edits...) } - out := diff.ApplyEdits(string(orig), catchallEdits) + out := diff.Apply(string(orig), catchallEdits) want := string(ar.Comment) formatted, err := format.Source([]byte(out)) @@ -227,7 +222,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns continue } if want != string(formatted) { - edits := diff.Strings("", want, string(formatted)) + edits := diff.Strings(want, string(formatted)) t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.Unified(file.Name()+".golden", "actual", want, edits)) } } diff --git a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go index 8aa333ec612..0fabae11a02 100644 --- a/gopls/api-diff/api_diff.go +++ b/gopls/api-diff/api_diff.go @@ -262,6 +262,6 @@ func diffStr(before, after string) string { } before += "\n" after += "\n" - edits := diffpkg.Strings("irrelevant", before, after) + edits := diffpkg.Strings(before, after) return fmt.Sprintf("%q", diffpkg.Unified("previous", "current", before, edits)) } diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go index 25b1d9566dd..3aa1f0b7806 100644 --- a/gopls/internal/hooks/diff.go +++ b/gopls/internal/hooks/diff.go @@ -8,7 +8,6 @@ import ( "crypto/rand" "encoding/json" "fmt" - "go/token" "io" "log" "math/big" @@ -23,7 +22,6 @@ import ( "github.com/sergi/go-diff/diffmatchpatch" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" ) // structure for saving information about diffs @@ -144,14 +142,14 @@ func initrepl(n int) []rune { // BothDiffs edits calls both the new and old diffs, checks that the new diffs // change before into after, and attempts to preserve some statistics. -func BothDiffs(uri span.URI, before, after string) (edits []diff.TextEdit) { +func BothDiffs(before, after string) (edits []diff.Edit) { // The new diff code contains a lot of internal checks that panic when they // fail. This code catches the panics, or other failures, tries to save // the failing example (and ut wiykd ask the user to send it back to us, and // changes options.newDiff to 'old', if only we could figure out how.) stat := diffstat{Before: len(before), After: len(after)} now := time.Now() - oldedits := ComputeEdits(uri, before, after) + oldedits := ComputeEdits(before, after) stat.Oldedits = len(oldedits) stat.Oldtime = time.Since(now) defer func() { @@ -161,10 +159,10 @@ func BothDiffs(uri span.URI, before, after string) (edits []diff.TextEdit) { } }() now = time.Now() - newedits := diff.Strings(uri, before, after) + newedits := diff.Strings(before, after) stat.Newedits = len(newedits) stat.Newtime = time.Now().Sub(now) - got := diff.ApplyEdits(before, newedits) + got := diff.Apply(before, newedits) if got != after { stat.Msg += "FAIL" disaster(before, after) @@ -176,50 +174,34 @@ func BothDiffs(uri span.URI, before, after string) (edits []diff.TextEdit) { } // ComputeEdits computes a diff using the github.com/sergi/go-diff implementation. -func ComputeEdits(uri span.URI, before, after string) (edits []diff.TextEdit) { +func ComputeEdits(before, after string) (edits []diff.Edit) { // The go-diff library has an unresolved panic (see golang/go#278774). // TODO(rstambler): Remove the recover once the issue has been fixed // upstream. defer func() { if r := recover(); r != nil { - bug.Reportf("unable to compute edits for %s: %s", uri.Filename(), r) - + bug.Reportf("unable to compute edits: %s", r) // Report one big edit for the whole file. - - // Reuse the machinery of go/token to convert (content, offset) - // to (line, column). - tf := token.NewFileSet().AddFile(uri.Filename(), -1, len(before)) - tf.SetLinesForContent([]byte(before)) - offsetToPoint := func(offset int) span.Point { - // Re-use span.ToPosition since it contains more than - // just token.File operations (bug workarounds). - // But it can only fail if the diff was ill-formed. - line, col, err := span.ToPosition(tf, offset) - if err != nil { - log.Fatalf("invalid offset: %v", err) - } - return span.NewPoint(line, col, offset) - } - all := span.New(uri, offsetToPoint(0), offsetToPoint(len(before))) - edits = []diff.TextEdit{{ - Span: all, - NewText: after, + edits = []diff.Edit{{ + Start: 0, + End: len(before), + New: after, }} } }() diffs := diffmatchpatch.New().DiffMain(before, after, true) - edits = make([]diff.TextEdit, 0, len(diffs)) + edits = make([]diff.Edit, 0, len(diffs)) offset := 0 for _, d := range diffs { - start := span.NewPoint(0, 0, offset) + start := offset switch d.Type { case diffmatchpatch.DiffDelete: offset += len(d.Text) - edits = append(edits, diff.TextEdit{Span: span.New(uri, start, span.NewPoint(0, 0, offset))}) + edits = append(edits, diff.Edit{Start: start, End: offset}) case diffmatchpatch.DiffEqual: offset += len(d.Text) case diffmatchpatch.DiffInsert: - edits = append(edits, diff.TextEdit{Span: span.New(uri, start, span.Point{}), NewText: d.Text}) + edits = append(edits, diff.Edit{Start: start, End: start, New: d.Text}) } } return edits diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index 5570417967c..e471a9bf6ab 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -419,7 +419,7 @@ func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdi return nil, err } // Calculate the edits to be made due to the change. - edits := computeEdits(m.URI, string(m.Content), string(newContent)) + edits := computeEdits(string(m.Content), string(newContent)) return source.ToProtocolEdits(m, edits) } diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index 5c084fac0c4..ec3e563cf0d 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -157,7 +157,7 @@ func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, // it is likely we got stuck in a loop somehow. Log out a diff // of the last changes we made to aid in debugging. if i == 9 { - edits := diff.Strings(fh.URI(), string(src), string(newSrc)) + edits := diff.Strings(string(src), string(newSrc)) unified := diff.Unified("before", "after", string(src), edits) event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name())) } diff --git a/gopls/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go index b2f2afa1c0b..17e3e6a5011 100644 --- a/gopls/internal/lsp/cmd/format.go +++ b/gopls/internal/lsp/cmd/format.go @@ -80,7 +80,7 @@ func (c *format) Run(ctx context.Context, args ...string) error { if err != nil { return fmt.Errorf("%v: %v", spn, err) } - formatted := diff.ApplyEdits(string(file.mapper.Content), sedits) + formatted := diff.Apply(string(file.mapper.Content), sedits) printIt := true if c.List { printIt = false diff --git a/gopls/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go index eb564ee16ab..7fd548381fe 100644 --- a/gopls/internal/lsp/cmd/imports.go +++ b/gopls/internal/lsp/cmd/imports.go @@ -85,7 +85,7 @@ func (t *imports) Run(ctx context.Context, args ...string) error { if err != nil { return fmt.Errorf("%v: %v", edits, err) } - newContent := diff.ApplyEdits(string(file.mapper.Content), sedits) + newContent := diff.Apply(string(file.mapper.Content), sedits) filename := file.uri.Filename() switch { diff --git a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go index a330bae9480..e0ffa663bfe 100644 --- a/gopls/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -100,7 +100,7 @@ func (r *rename) Run(ctx context.Context, args ...string) error { if err != nil { return fmt.Errorf("%v: %v", edits, err) } - newContent := diff.ApplyEdits(string(cmdFile.mapper.Content), renameEdits) + newContent := diff.Apply(string(cmdFile.mapper.Content), renameEdits) switch { case r.Write: diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go index faf681f856a..b6022a7ee55 100644 --- a/gopls/internal/lsp/cmd/suggested_fix.go +++ b/gopls/internal/lsp/cmd/suggested_fix.go @@ -146,7 +146,7 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { if err != nil { return fmt.Errorf("%v: %v", edits, err) } - newContent := diff.ApplyEdits(string(file.mapper.Content), sedits) + newContent := diff.Apply(string(file.mapper.Content), sedits) filename := file.uri.Filename() switch { diff --git a/gopls/internal/lsp/cmd/test/imports.go b/gopls/internal/lsp/cmd/test/imports.go index 67826a47095..eab46681e7d 100644 --- a/gopls/internal/lsp/cmd/test/imports.go +++ b/gopls/internal/lsp/cmd/test/imports.go @@ -19,7 +19,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) { return []byte(got), nil })) if want != got { - edits := diff.Strings(uri, want, got) + edits := diff.Strings(want, got) t.Errorf("imports failed for %s, expected:\n%s", filename, diff.Unified("want", "got", want, edits)) } } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index e9483b6df1b..cb4c6982835 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -390,7 +390,7 @@ func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePat return nil, err } // Calculate the edits to be made due to the change. - diff := snapshot.View().Options().ComputeEdits(pm.URI, string(pm.Mapper.Content), string(newContent)) + diff := snapshot.View().Options().ComputeEdits(string(pm.Mapper.Content), string(newContent)) return source.ToProtocolEdits(pm.Mapper, diff) } @@ -612,7 +612,7 @@ func applyFileEdits(ctx context.Context, snapshot source.Snapshot, uri span.URI, } m := protocol.NewColumnMapper(fh.URI(), oldContent) - diff := snapshot.View().Options().ComputeEdits(uri, string(oldContent), string(newContent)) + diff := snapshot.View().Options().ComputeEdits(string(oldContent), string(newContent)) edits, err := source.ToProtocolEdits(m, diff) if err != nil { return nil, err diff --git a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go index 7c833383d0b..bb5fb80900b 100644 --- a/gopls/internal/lsp/fake/edit.go +++ b/gopls/internal/lsp/fake/edit.go @@ -110,7 +110,7 @@ func inText(p Pos, content []string) bool { // invalid for the current content. // // TODO(rfindley): this function does not handle non-ascii text correctly. -// TODO(rfindley): replace this with diff.ApplyEdits: we should not be +// TODO(rfindley): replace this with diff.Apply: we should not be // maintaining an additional representation of edits. func editContent(content []string, edits []Edit) ([]string, error) { newEdits := make([]Edit, len(edits)) diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index a61f6830789..78032f04368 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -413,7 +413,7 @@ func (r *runner) Format(t *testing.T, spn span.Span) { if err != nil { t.Error(err) } - got := diff.ApplyEdits(string(m.Content), sedits) + got := diff.Apply(string(m.Content), sedits) if diff := compare.Text(gofmted, got); diff != "" { t.Errorf("format failed for %s (-want +got):\n%s", filename, diff) } @@ -979,7 +979,7 @@ func (r *runner) InlayHints(t *testing.T, spn span.Span) { if err != nil { t.Error(err) } - got := diff.ApplyEdits(string(m.Content), sedits) + got := diff.Apply(string(m.Content), sedits) withinlayHints := string(r.data.Golden(t, "inlayHint", filename, func() ([]byte, error) { return []byte(got), nil @@ -1126,17 +1126,14 @@ func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[sp return res, nil } -func applyEdits(contents string, edits []diff.TextEdit) string { +func applyEdits(contents string, edits []diff.Edit) string { res := contents // Apply the edits from the end of the file forward // to preserve the offsets for i := len(edits) - 1; i >= 0; i-- { edit := edits[i] - start := edit.Span.Start().Offset() - end := edit.Span.End().Offset() - tmp := res[0:start] + edit.NewText - res = tmp + res[end:] + res = res[:edit.Start] + edit.New + res[edit.End:] } return res } diff --git a/gopls/internal/lsp/mod/format.go b/gopls/internal/lsp/mod/format.go index 010d2ace2b4..9c3942ee06d 100644 --- a/gopls/internal/lsp/mod/format.go +++ b/gopls/internal/lsp/mod/format.go @@ -25,6 +25,6 @@ func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) return nil, err } // Calculate the edits to be made due to the change. - diffs := snapshot.View().Options().ComputeEdits(fh.URI(), string(pm.Mapper.Content), string(formatted)) + diffs := snapshot.View().Options().ComputeEdits(string(pm.Mapper.Content), string(formatted)) return source.ToProtocolEdits(pm.Mapper, diffs) } diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 58601a6b347..f24a28e1124 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -70,6 +70,35 @@ func (m *ColumnMapper) Range(s span.Span) (Range, error) { return Range{Start: start, End: end}, nil } +// OffsetRange returns a Range for the byte-offset interval Content[start:end], +func (m *ColumnMapper) OffsetRange(start, end int) (Range, error) { + // TODO(adonovan): this can surely be simplified by expressing + // it terms of more primitive operations. + + // We use span.ToPosition for its "line+1 at EOF" workaround. + startLine, startCol, err := span.ToPosition(m.TokFile, start) + if err != nil { + return Range{}, fmt.Errorf("start line/col: %v", err) + } + startPoint := span.NewPoint(startLine, startCol, start) + startPosition, err := m.Position(startPoint) + if err != nil { + return Range{}, fmt.Errorf("start position: %v", err) + } + + endLine, endCol, err := span.ToPosition(m.TokFile, end) + if err != nil { + return Range{}, fmt.Errorf("end line/col: %v", err) + } + endPoint := span.NewPoint(endLine, endCol, end) + endPosition, err := m.Position(endPoint) + if err != nil { + return Range{}, fmt.Errorf("end position: %v", err) + } + + return Range{Start: startPosition, End: endPosition}, nil +} + func (m *ColumnMapper) Position(p span.Point) (Position, error) { chr, err := span.ToUTF16Column(p, m.Content) if err != nil { diff --git a/gopls/internal/lsp/source/completion/util.go b/gopls/internal/lsp/source/completion/util.go index f40c0b31dd0..72877a38a35 100644 --- a/gopls/internal/lsp/source/completion/util.go +++ b/gopls/internal/lsp/source/completion/util.go @@ -10,9 +10,10 @@ import ( "go/types" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/typeparams" ) @@ -311,14 +312,18 @@ func isBasicKind(t types.Type, k types.BasicInfo) bool { } func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.TextEdit, error) { - rng := source.NewMappedRange(c.tokFile, c.mapper, from, to) - spn, err := rng.Span() + start, err := safetoken.Offset(c.tokFile, from) + if err != nil { + return nil, err // can't happen: from came from c + } + end, err := safetoken.Offset(c.tokFile, to) if err != nil { - return nil, err + return nil, err // can't happen: to came from c } - return source.ToProtocolEdits(c.mapper, []diff.TextEdit{{ - Span: spn, - NewText: newText, + return source.ToProtocolEdits(c.mapper, []diff.Edit{{ + Start: start, + End: end, + New: newText, }}) } diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index 90805957544..dc7445a025c 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -199,8 +199,8 @@ func computeFixEdits(snapshot Snapshot, pgf *ParsedGoFile, options *imports.Opti if fixedData == nil || fixedData[len(fixedData)-1] != '\n' { fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure. } - edits := snapshot.View().Options().ComputeEdits(pgf.URI, left, string(fixedData)) - return ProtocolEditsFromSource([]byte(left), edits, pgf.Mapper.TokFile) + edits := snapshot.View().Options().ComputeEdits(left, string(fixedData)) + return protocolEditsFromSource([]byte(left), edits, pgf.Mapper.TokFile) } // importPrefix returns the prefix of the given file content through the final @@ -309,69 +309,63 @@ func computeTextEdits(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile, _, done := event.Start(ctx, "source.computeTextEdits") defer done() - edits := snapshot.View().Options().ComputeEdits(pgf.URI, string(pgf.Src), formatted) + edits := snapshot.View().Options().ComputeEdits(string(pgf.Src), formatted) return ToProtocolEdits(pgf.Mapper, edits) } -// ProtocolEditsFromSource converts text edits to LSP edits using the original +// protocolEditsFromSource converts text edits to LSP edits using the original // source. -func ProtocolEditsFromSource(src []byte, edits []diff.TextEdit, tf *token.File) ([]protocol.TextEdit, error) { +func protocolEditsFromSource(src []byte, edits []diff.Edit, tf *token.File) ([]protocol.TextEdit, error) { m := lsppos.NewMapper(src) var result []protocol.TextEdit for _, edit := range edits { - spn, err := edit.Span.WithOffset(tf) - if err != nil { - return nil, fmt.Errorf("computing offsets: %v", err) - } - rng, err := m.Range(spn.Start().Offset(), spn.End().Offset()) + rng, err := m.Range(edit.Start, edit.End) if err != nil { return nil, err } - if rng.Start == rng.End && edit.NewText == "" { + if rng.Start == rng.End && edit.New == "" { // Degenerate case, which may result from a diff tool wanting to delete // '\r' in line endings. Filter it out. continue } result = append(result, protocol.TextEdit{ Range: rng, - NewText: edit.NewText, + NewText: edit.New, }) } return result, nil } -func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) { - if edits == nil { - return nil, nil - } +func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.Edit) ([]protocol.TextEdit, error) { result := make([]protocol.TextEdit, len(edits)) for i, edit := range edits { - rng, err := m.Range(edit.Span) + rng, err := m.OffsetRange(edit.Start, edit.End) if err != nil { return nil, err } result[i] = protocol.TextEdit{ Range: rng, - NewText: edit.NewText, + NewText: edit.New, } } return result, nil } -func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.TextEdit, error) { +func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.Edit, error) { if edits == nil { return nil, nil } - result := make([]diff.TextEdit, len(edits)) + result := make([]diff.Edit, len(edits)) for i, edit := range edits { spn, err := m.RangeSpan(edit.Range) if err != nil { return nil, err } - result[i] = diff.TextEdit{ - Span: spn, - NewText: edit.NewText, + result[i] = diff.Edit{ + Start: spn.Start().Offset(), + End: spn.End().Offset(), + New: edit.NewText, } } return result, nil diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 815922115ee..04a614c973f 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -67,7 +67,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/diff/myers" - "golang.org/x/tools/internal/span" ) var ( @@ -501,7 +500,7 @@ func (u *UserOptions) SetEnvSlice(env []string) { // DiffFunction is the type for a function that produces a set of edits that // convert from the before content to the after content. -type DiffFunction func(uri span.URI, before, after string) []diff.TextEdit +type DiffFunction func(before, after string) []diff.Edit // Hooks contains configuration that is provided to the Gopls command by the // main package. diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 93ded0fda75..842b1a9850b 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -19,6 +19,7 @@ import ( "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/span" @@ -445,8 +446,7 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO return nil, err } m := protocol.NewColumnMapper(uri, data) - // Sort the edits first. - diff.SortTextEdits(edits) + diff.SortEdits(edits) protocolEdits, err := ToProtocolEdits(m, edits) if err != nil { return nil, err @@ -457,8 +457,8 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO } // Rename all references to the identifier. -func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) { - result := make(map[span.URI][]diff.TextEdit) +func (r *renamer) update() (map[span.URI][]diff.Edit, error) { + result := make(map[span.URI][]diff.Edit) seen := make(map[span.Span]bool) docRegexp, err := regexp.Compile(`\b` + r.from + `\b`) @@ -487,9 +487,10 @@ func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) { } // Replace the identifier with r.to. - edit := diff.TextEdit{ - Span: refSpan, - NewText: r.to, + edit := diff.Edit{ + Start: refSpan.Start().Offset(), + End: refSpan.End().Offset(), + New: r.to, } result[refSpan.URI()] = append(result[refSpan.URI()], edit) @@ -510,23 +511,26 @@ func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) { if isDirective(comment.Text) { continue } + // TODO(adonovan): why are we looping over lines? + // Just run the loop body once over the entire multiline comment. lines := strings.Split(comment.Text, "\n") tokFile := r.fset.File(comment.Pos()) commentLine := tokFile.Line(comment.Pos()) + uri := span.URIFromPath(tokFile.Name()) for i, line := range lines { lineStart := comment.Pos() if i > 0 { lineStart = tokFile.LineStart(commentLine + i) } for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) { - rng := span.NewRange(tokFile, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1])) - spn, err := rng.Span() - if err != nil { - return nil, err - } - result[spn.URI()] = append(result[spn.URI()], diff.TextEdit{ - Span: spn, - NewText: r.to, + // The File.Offset static check complains + // even though these uses are manifestly safe. + start, _ := safetoken.Offset(tokFile, lineStart+token.Pos(locs[0])) + end, _ := safetoken.Offset(tokFile, lineStart+token.Pos(locs[1])) + result[uri] = append(result[uri], diff.Edit{ + Start: start, + End: end, + New: r.to, }) } } @@ -588,7 +592,7 @@ func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { // updatePkgName returns the updates to rename a pkgName in the import spec by // only modifying the package name portion of the import declaration. -func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) { +func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.Edit, error) { // Modify ImportSpec syntax to add or remove the Name as needed. pkg := r.packages[pkgName.Pkg()] _, tokFile, path, _ := pathEnclosingInterval(r.fset, pkg, pkgName.Pos(), pkgName.Pos()) @@ -614,8 +618,9 @@ func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) return nil, err } - return &diff.TextEdit{ - Span: spn, - NewText: newText, + return &diff.Edit{ + Start: spn.Start().Offset(), + End: spn.End().Offset(), + New: newText, }, nil } diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index d899f4ffdb2..0a4e70cb6f5 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -489,7 +489,7 @@ func (r *runner) Format(t *testing.T, spn span.Span) { if err != nil { t.Error(err) } - got := diff.ApplyEdits(string(data), diffEdits) + got := diff.Apply(string(data), diffEdits) if gofmted != got { t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", spn.URI().Filename(), gofmted, got) } @@ -520,7 +520,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) { if err != nil { t.Error(err) } - got := diff.ApplyEdits(string(data), diffEdits) + got := diff.Apply(string(data), diffEdits) want := string(r.data.Golden(t, "goimports", spn.URI().Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -821,17 +821,14 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { } } -func applyEdits(contents string, edits []diff.TextEdit) string { +func applyEdits(contents string, edits []diff.Edit) string { res := contents // Apply the edits from the end of the file forward // to preserve the offsets for i := len(edits) - 1; i >= 0; i-- { edit := edits[i] - start := edit.Span.Start().Offset() - end := edit.Span.End().Offset() - tmp := res[0:start] + edit.NewText - res = tmp + res[end:] + res = res[:edit.Start] + edit.New + res[edit.End:] } return res } diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index d0dff4f72f8..3aab0b4e8aa 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -80,17 +80,14 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi if err != nil { return nil, fmt.Errorf("format.Node: %w", err) } - diffs := snapshot.View().Options().ComputeEdits(parsedConcreteFile.URI, string(parsedConcreteFile.Src), source.String()) + diffs := snapshot.View().Options().ComputeEdits(string(parsedConcreteFile.Src), source.String()) + tf := parsedConcreteFile.Mapper.TokFile var edits []analysis.TextEdit for _, edit := range diffs { - rng, err := edit.Span.Range(parsedConcreteFile.Mapper.TokFile) - if err != nil { - return nil, err - } edits = append(edits, analysis.TextEdit{ - Pos: rng.Start, - End: rng.End, - NewText: []byte(edit.NewText), + Pos: tf.Pos(edit.Start), + End: tf.Pos(edit.End), + NewText: []byte(edit.New), }) } return &analysis.SuggestedFix{ diff --git a/gopls/internal/lsp/tests/compare/text.go b/gopls/internal/lsp/tests/compare/text.go index 66d2e620ab6..0563fcd06f3 100644 --- a/gopls/internal/lsp/tests/compare/text.go +++ b/gopls/internal/lsp/tests/compare/text.go @@ -22,7 +22,7 @@ func Text(want, got string) string { want += "\n" got += "\n" - edits := diff.Strings("irrelevant", want, got) + edits := diff.Strings(want, got) diff := diff.Unified("want", "got", want, edits) // Defensively assert that we get an actual diff, so that we guarantee the diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index d463ba0e8ea..ce5ab5b7924 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -230,7 +230,7 @@ func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) (string, e w := want.Signatures[0] if NormalizeAny(w.Label) != NormalizeAny(g.Label) { wLabel := w.Label + "\n" - edits := diff.Strings("", wLabel, g.Label+"\n") + edits := diff.Strings(wLabel, g.Label+"\n") return decorate("mismatched labels:\n%q", diff.Unified("want", "got", wLabel, edits)), nil } var paramParts []string diff --git a/gopls/internal/lsp/work/format.go b/gopls/internal/lsp/work/format.go index bc84464af96..e852eb4d27e 100644 --- a/gopls/internal/lsp/work/format.go +++ b/gopls/internal/lsp/work/format.go @@ -23,6 +23,6 @@ func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) } formatted := modfile.Format(pw.File.Syntax) // Calculate the edits to be made due to the change. - diffs := snapshot.View().Options().ComputeEdits(fh.URI(), string(pw.Mapper.Content), string(formatted)) + diffs := snapshot.View().Options().ComputeEdits(string(pw.Mapper.Content), string(formatted)) return source.ToProtocolEdits(pw.Mapper, diffs) } diff --git a/internal/diff/diff.go b/internal/diff/diff.go index b00ffbc7cb2..e7f8469d13d 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -2,170 +2,130 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package diff supports a pluggable diff algorithm. +// Package diff computes differences between files or strings. package diff import ( "sort" "strings" - - "golang.org/x/tools/internal/span" ) -// TODO(adonovan): simplify this package to just: -// -// package diff -// type Edit struct { Start, End int; New string } -// func Strings(old, new string) []Edit -// func Unified(oldLabel, newLabel string, old string, edits []Edit) string -// func Apply(old string, edits []Edit) (string, error) -// -// and move everything else into gopls, including the concepts of filenames and spans. -// Observe that TextEdit.URI is irrelevant to Unified. +// TODO(adonovan): switch to []byte throughout. +// But make clear that the operation is defined on runes, not bytes. +// Also: // - delete LineEdits? (used only by Unified and test) // - delete Lines (unused except by its test) -// TextEdit represents a change to a section of a document. -// The text within the specified span should be replaced by the supplied new text. -type TextEdit struct { - Span span.Span - NewText string +// An Edit describes the replacement of a portion of a file. +type Edit struct { + Start, End int // byte offsets of the region to replace + New string // the replacement } -// SortTextEdits attempts to order all edits by their starting points. -// The sort is stable so that edits with the same starting point will not -// be reordered. -func SortTextEdits(d []TextEdit) { - // Use a stable sort to maintain the order of edits inserted at the same position. - sort.SliceStable(d, func(i int, j int) bool { - return span.Compare(d[i].Span, d[j].Span) < 0 +// SortEdits orders edits by their start offset. The sort is stable +// so that edits with the same start offset will not be reordered. +func SortEdits(edits []Edit) { + sort.SliceStable(edits, func(i int, j int) bool { + return edits[i].Start < edits[j].Start }) } -// ApplyEdits applies the set of edits to the before and returns the resulting -// content. -// It may panic or produce garbage if the edits are not valid for the provided -// before content. -// TODO(adonovan): this function must not panic! Make it either cope -// or report an error. We should not trust that (e.g.) patches supplied -// as RPC inputs to gopls are consistent. -func ApplyEdits(before string, edits []TextEdit) string { - // Preconditions: - // - all of the edits apply to before - // - and all the spans for each TextEdit have the same URI - if len(edits) == 0 { - return before - } - edits, _ = prepareEdits(before, edits) - after := strings.Builder{} +// Apply applies a sequence of edits to the src buffer and +// returns the result. It may panic or produce garbage if the edits +// are overlapping, out of bounds of src, or out of order. +// +// TODO(adonovan): this function must not panic if the edits aren't +// consistent with src, or with each other---especially when fed +// information from an untrusted source. It should probably be +// defensive against bad input and report an error in any of the above +// situations. +func Apply(src string, edits []Edit) string { + SortEdits(edits) // TODO(adonovan): move to caller? What's the contract? Don't mutate arguments. + + var out strings.Builder + // TODO(adonovan): opt: preallocate correct final size + // by scanning the list of edits. (This can be done + // in the same pass as detecting inconsistent edits.) last := 0 for _, edit := range edits { - start := edit.Span.Start().Offset() + start := edit.Start if start > last { - after.WriteString(before[last:start]) + out.WriteString(src[last:start]) last = start } - after.WriteString(edit.NewText) - last = edit.Span.End().Offset() + out.WriteString(edit.New) + last = edit.End } - if last < len(before) { - after.WriteString(before[last:]) + if last < len(src) { + out.WriteString(src[last:]) } - return after.String() + return out.String() } -// LineEdits takes a set of edits and expands and merges them as necessary -// to ensure that there are only full line edits left when it is done. -func LineEdits(before string, edits []TextEdit) []TextEdit { - if len(edits) == 0 { - return nil - } - edits, partial := prepareEdits(before, edits) - if partial { - edits = lineEdits(before, edits) - } - return edits -} +// LineEdits expands and merges a sequence of edits so that each +// resulting edit replaces one or more complete lines. +// +// It may panic or produce garbage if the edits +// are overlapping, out of bounds of src, or out of order. +// TODO(adonovan): see consistency note at Apply. +// We could hide this from the API so that we can enforce +// the precondition... but it seems like a reasonable feature. +func LineEdits(src string, edits []Edit) []Edit { + SortEdits(edits) // TODO(adonovan): is this necessary? Move burden to caller? -// prepareEdits returns a sorted copy of the edits -func prepareEdits(before string, edits []TextEdit) ([]TextEdit, bool) { - partial := false - tf := span.NewTokenFile("", []byte(before)) - copied := make([]TextEdit, len(edits)) - for i, edit := range edits { - edit.Span, _ = edit.Span.WithAll(tf) - copied[i] = edit - partial = partial || - edit.Span.Start().Offset() >= len(before) || - edit.Span.Start().Column() > 1 || edit.Span.End().Column() > 1 + // Do all edits begin and end at the start of a line? + // TODO(adonovan): opt: is this fast path necessary? + // (Also, it complicates the result ownership.) + for _, edit := range edits { + if edit.Start >= len(src) || // insertion at EOF + edit.Start > 0 && src[edit.Start-1] != '\n' || // not at line start + edit.End > 0 && src[edit.End-1] != '\n' { // not at line start + goto expand + } } - SortTextEdits(copied) - return copied, partial -} + return edits // aligned -// lineEdits rewrites the edits to always be full line edits -func lineEdits(before string, edits []TextEdit) []TextEdit { - adjusted := make([]TextEdit, 0, len(edits)) - current := TextEdit{Span: span.Invalid} - for _, edit := range edits { - if current.Span.IsValid() && edit.Span.Start().Line() <= current.Span.End().Line() { - // overlaps with the current edit, need to combine - // first get the gap from the previous edit - gap := before[current.Span.End().Offset():edit.Span.Start().Offset()] - // now add the text of this edit - current.NewText += gap + edit.NewText - // and then adjust the end position - current.Span = span.New(current.Span.URI(), current.Span.Start(), edit.Span.End()) +expand: + expanded := make([]Edit, 0, len(edits)) // a guess + prev := edits[0] + // TODO(adonovan): opt: start from the first misaligned edit. + // TODO(adonovan): opt: avoid quadratic cost of string += string. + for _, edit := range edits[1:] { + between := src[prev.End:edit.Start] + if !strings.Contains(between, "\n") { + // overlapping lines: combine with previous edit. + prev.New += between + edit.New + prev.End = edit.End } else { - // does not overlap, add previous run (if there is one) - adjusted = addEdit(before, adjusted, current) - // and then remember this edit as the start of the next run - current = edit + // non-overlapping lines: flush previous edit. + expanded = append(expanded, expandEdit(prev, src)) + prev = edit } } - // add the current pending run if there is one - return addEdit(before, adjusted, current) + return append(expanded, expandEdit(prev, src)) // flush final edit } -func addEdit(before string, edits []TextEdit, edit TextEdit) []TextEdit { - if !edit.Span.IsValid() { - return edits - } - // if edit is partial, expand it to full line now - start := edit.Span.Start() - end := edit.Span.End() - if start.Column() > 1 { - // prepend the text and adjust to start of line - delta := start.Column() - 1 - start = span.NewPoint(start.Line(), 1, start.Offset()-delta) - edit.Span = span.New(edit.Span.URI(), start, end) - edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText +// expandEdit returns edit expanded to complete whole lines. +func expandEdit(edit Edit, src string) Edit { + // Expand start left to start of line. + // (delta is the zero-based column number of of start.) + start := edit.Start + if delta := start - 1 - strings.LastIndex(src[:start], "\n"); delta > 0 { + edit.Start -= delta + edit.New = src[start-delta:start] + edit.New } - if start.Offset() >= len(before) && start.Line() > 1 && before[len(before)-1] != '\n' { - // after end of file that does not end in eol, so join to last line of file - // to do this we need to know where the start of the last line was - eol := strings.LastIndex(before, "\n") - if eol < 0 { - // file is one non terminated line - eol = 0 - } - delta := len(before) - eol - start = span.NewPoint(start.Line()-1, 1, start.Offset()-delta) - edit.Span = span.New(edit.Span.URI(), start, end) - edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText - } - if end.Column() > 1 { - remains := before[end.Offset():] - eol := strings.IndexRune(remains, '\n') - if eol < 0 { - eol = len(remains) + + // Expand end right to end of line. + // (endCol is the zero-based column number of end.) + end := edit.End + if endCol := end - 1 - strings.LastIndex(src[:end], "\n"); endCol > 0 { + if nl := strings.IndexByte(src[end:], '\n'); nl < 0 { + edit.End = len(src) // extend to EOF } else { - eol++ + edit.End = end + nl + 1 // extend beyond \n } - end = span.NewPoint(end.Line()+1, 1, end.Offset()+eol) - edit.Span = span.New(edit.Span.URI(), start, end) - edit.NewText = edit.NewText + remains[:eol] + edit.New += src[end:edit.End] } - edits = append(edits, edit) - return edits + + return edit } diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index 11930dea4c1..8b8b6489e06 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -5,25 +5,25 @@ package diff_test import ( - "fmt" "math/rand" + "reflect" "strings" "testing" + "unicode/utf8" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/diff/difftest" - "golang.org/x/tools/internal/span" ) -func TestApplyEdits(t *testing.T) { +func TestApply(t *testing.T) { for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { - if got := diff.ApplyEdits(tc.In, tc.Edits); got != tc.Out { - t.Errorf("ApplyEdits(Edits): got %q, want %q", got, tc.Out) + if got := diff.Apply(tc.In, tc.Edits); got != tc.Out { + t.Errorf("Apply(Edits): got %q, want %q", got, tc.Out) } if tc.LineEdits != nil { - if got := diff.ApplyEdits(tc.In, tc.LineEdits); got != tc.Out { - t.Errorf("ApplyEdits(LineEdits): got %q, want %q", got, tc.Out) + if got := diff.Apply(tc.In, tc.LineEdits); got != tc.Out { + t.Errorf("Apply(LineEdits): got %q, want %q", got, tc.Out) } } }) @@ -31,10 +31,9 @@ func TestApplyEdits(t *testing.T) { } func TestNEdits(t *testing.T) { - for i, tc := range difftest.TestCases { - sp := fmt.Sprintf("file://%s.%d", tc.Name, i) - edits := diff.Strings(span.URI(sp), tc.In, tc.Out) - got := diff.ApplyEdits(tc.In, edits) + for _, tc := range difftest.TestCases { + edits := diff.Strings(tc.In, tc.Out) + got := diff.Apply(tc.In, edits) if got != tc.Out { t.Fatalf("%s: got %q wanted %q", tc.Name, got, tc.Out) } @@ -47,21 +46,33 @@ func TestNEdits(t *testing.T) { func TestNRandom(t *testing.T) { rand.Seed(1) for i := 0; i < 1000; i++ { - fname := fmt.Sprintf("file://%x", i) a := randstr("abω", 16) b := randstr("abωc", 16) - edits := diff.Strings(span.URI(fname), a, b) - got := diff.ApplyEdits(a, edits) + edits := diff.Strings(a, b) + got := diff.Apply(a, edits) if got != b { t.Fatalf("%d: got %q, wanted %q, starting with %q", i, got, b, a) } } } +// $ go test -fuzz=FuzzRoundTrip ./internal/diff +func FuzzRoundTrip(f *testing.F) { + f.Fuzz(func(t *testing.T, a, b string) { + if !utf8.ValidString(a) || !utf8.ValidString(b) { + return // inputs must be text + } + edits := diff.Strings(a, b) + got := diff.Apply(a, edits) + if got != b { + t.Fatalf("applying diff(%q, %q) gives %q; edits=%v", a, b, got, edits) + } + }) +} + func TestNLinesRandom(t *testing.T) { rand.Seed(2) for i := 0; i < 1000; i++ { - fname := fmt.Sprintf("file://%x", i) x := randlines("abω", 4) // avg line length is 6, want a change every 3rd line or so v := []rune(x) for i := 0; i < len(v); i++ { @@ -78,8 +89,8 @@ func TestNLinesRandom(t *testing.T) { y = y[:len(y)-1] } a, b := strings.SplitAfter(x, "\n"), strings.SplitAfter(y, "\n") - edits := diff.Lines(span.URI(fname), a, b) - got := diff.ApplyEdits(x, edits) + edits := diff.Lines(a, b) + got := diff.Apply(x, edits) if got != y { t.Fatalf("%d: got\n%q, wanted\n%q, starting with %q", i, got, y, a) } @@ -122,8 +133,8 @@ func TestRegressionOld001(t *testing.T) { a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" - diffs := diff.Strings(span.URI("file://one"), a, b) - got := diff.ApplyEdits(a, diffs) + diffs := diff.Strings(a, b) + got := diff.Apply(a, diffs) if got != b { i := 0 for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ { @@ -136,8 +147,8 @@ func TestRegressionOld001(t *testing.T) { func TestRegressionOld002(t *testing.T) { a := "n\"\n)\n" b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n" - diffs := diff.Strings(span.URI("file://two"), a, b) - got := diff.ApplyEdits(a, diffs) + diffs := diff.Strings(a, b) + got := diff.Apply(a, diffs) if got != b { i := 0 for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ { @@ -147,20 +158,8 @@ func TestRegressionOld002(t *testing.T) { } } -func diffEdits(got, want []diff.TextEdit) bool { - if len(got) != len(want) { - return true - } - for i, w := range want { - g := got[i] - if span.Compare(w.Span, g.Span) != 0 { - return true - } - if w.NewText != g.NewText { - return true - } - } - return false +func diffEdits(got, want []diff.Edit) bool { + return !reflect.DeepEqual(got, want) } // return a random string of length n made of characters from s diff --git a/internal/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go index c9808a55027..998a90f109f 100644 --- a/internal/diff/difftest/difftest.go +++ b/internal/diff/difftest/difftest.go @@ -11,7 +11,6 @@ import ( "testing" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" ) const ( @@ -22,7 +21,7 @@ const ( var TestCases = []struct { Name, In, Out, Unified string - Edits, LineEdits []diff.TextEdit + Edits, LineEdits []diff.Edit NoDiff bool }{{ Name: "empty", @@ -41,8 +40,8 @@ var TestCases = []struct { -fruit +cheese `[1:], - Edits: []diff.TextEdit{{Span: newSpan(0, 5), NewText: "cheese"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 6), NewText: "cheese\n"}}, + Edits: []diff.Edit{{Start: 0, End: 5, New: "cheese"}}, + LineEdits: []diff.Edit{{Start: 0, End: 6, New: "cheese\n"}}, }, { Name: "insert_rune", In: "gord\n", @@ -52,8 +51,8 @@ var TestCases = []struct { -gord +gourd `[1:], - Edits: []diff.TextEdit{{Span: newSpan(2, 2), NewText: "u"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 5), NewText: "gourd\n"}}, + Edits: []diff.Edit{{Start: 2, End: 2, New: "u"}}, + LineEdits: []diff.Edit{{Start: 0, End: 5, New: "gourd\n"}}, }, { Name: "delete_rune", In: "groat\n", @@ -63,8 +62,8 @@ var TestCases = []struct { -groat +goat `[1:], - Edits: []diff.TextEdit{{Span: newSpan(1, 2), NewText: ""}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 6), NewText: "goat\n"}}, + Edits: []diff.Edit{{Start: 1, End: 2, New: ""}}, + LineEdits: []diff.Edit{{Start: 0, End: 6, New: "goat\n"}}, }, { Name: "replace_rune", In: "loud\n", @@ -74,8 +73,8 @@ var TestCases = []struct { -loud +lord `[1:], - Edits: []diff.TextEdit{{Span: newSpan(2, 3), NewText: "r"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 5), NewText: "lord\n"}}, + Edits: []diff.Edit{{Start: 2, End: 3, New: "r"}}, + LineEdits: []diff.Edit{{Start: 0, End: 5, New: "lord\n"}}, }, { Name: "replace_partials", In: "blanket\n", @@ -85,11 +84,11 @@ var TestCases = []struct { -blanket +bunker `[1:], - Edits: []diff.TextEdit{ - {Span: newSpan(1, 3), NewText: "u"}, - {Span: newSpan(6, 7), NewText: "r"}, + Edits: []diff.Edit{ + {Start: 1, End: 3, New: "u"}, + {Start: 6, End: 7, New: "r"}, }, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 8), NewText: "bunker\n"}}, + LineEdits: []diff.Edit{{Start: 0, End: 8, New: "bunker\n"}}, }, { Name: "insert_line", In: "1: one\n3: three\n", @@ -100,7 +99,7 @@ var TestCases = []struct { +2: two 3: three `[1:], - Edits: []diff.TextEdit{{Span: newSpan(7, 7), NewText: "2: two\n"}}, + Edits: []diff.Edit{{Start: 7, End: 7, New: "2: two\n"}}, }, { Name: "replace_no_newline", In: "A", @@ -112,7 +111,7 @@ var TestCases = []struct { +B \ No newline at end of file `[1:], - Edits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "B"}}, + Edits: []diff.Edit{{Start: 0, End: 1, New: "B"}}, }, { Name: "append_empty", In: "", // GNU diff -u special case: -0,0 @@ -123,8 +122,8 @@ var TestCases = []struct { +C \ No newline at end of file `[1:], - Edits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "AB\nC"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "AB\nC"}}, + Edits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}}, + LineEdits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}}, }, // TODO(adonovan): fix this test: GNU diff -u prints "+1,2", Unifies prints "+1,3". // { @@ -153,8 +152,20 @@ var TestCases = []struct { +AB \ No newline at end of file `[1:], - Edits: []diff.TextEdit{{Span: newSpan(1, 1), NewText: "B"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "AB"}}, + Edits: []diff.Edit{{Start: 1, End: 1, New: "B"}}, + LineEdits: []diff.Edit{{Start: 0, End: 1, New: "AB"}}, + }, { + Name: "add_empty", + In: "", + Out: "AB\nC", + Unified: UnifiedPrefix + ` +@@ -0,0 +1,2 @@ ++AB ++C +\ No newline at end of file +`[1:], + Edits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}}, + LineEdits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}}, }, { Name: "add_newline", In: "A", @@ -165,8 +176,8 @@ var TestCases = []struct { \ No newline at end of file +A `[1:], - Edits: []diff.TextEdit{{Span: newSpan(1, 1), NewText: "\n"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "A\n"}}, + Edits: []diff.Edit{{Start: 1, End: 1, New: "\n"}}, + LineEdits: []diff.Edit{{Start: 0, End: 1, New: "A\n"}}, }, { Name: "delete_front", In: "A\nB\nC\nA\nB\nB\nA\n", @@ -183,15 +194,14 @@ var TestCases = []struct { A +C `[1:], - Edits: []diff.TextEdit{ - {Span: newSpan(0, 4), NewText: ""}, - {Span: newSpan(6, 6), NewText: "B\n"}, - {Span: newSpan(10, 12), NewText: ""}, - {Span: newSpan(14, 14), NewText: "C\n"}, + NoDiff: true, // unified diff is different but valid + Edits: []diff.Edit{ + {Start: 0, End: 4, New: ""}, + {Start: 6, End: 6, New: "B\n"}, + {Start: 10, End: 12, New: ""}, + {Start: 14, End: 14, New: "C\n"}, }, - NoDiff: true, // diff algorithm produces different delete/insert pattern - }, - { + }, { Name: "replace_last_line", In: "A\nB\n", Out: "A\nC\n\n", @@ -202,8 +212,8 @@ var TestCases = []struct { +C + `[1:], - Edits: []diff.TextEdit{{Span: newSpan(2, 3), NewText: "C\n"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(2, 4), NewText: "C\n\n"}}, + Edits: []diff.Edit{{Start: 2, End: 3, New: "C\n"}}, + LineEdits: []diff.Edit{{Start: 2, End: 4, New: "C\n\n"}}, }, { Name: "multiple_replace", @@ -223,33 +233,19 @@ var TestCases = []struct { -G +K `[1:], - Edits: []diff.TextEdit{ - {Span: newSpan(2, 8), NewText: "H\nI\nJ\n"}, - {Span: newSpan(12, 14), NewText: "K\n"}, + Edits: []diff.Edit{ + {Start: 2, End: 8, New: "H\nI\nJ\n"}, + {Start: 12, End: 14, New: "K\n"}, }, NoDiff: true, // diff algorithm produces different delete/insert pattern }, } -func init() { - // expand all the spans to full versions - // we need them all to have their line number and column - for _, tc := range TestCases { - tf := span.NewTokenFile("", []byte(tc.In)) - for i := range tc.Edits { - tc.Edits[i].Span, _ = tc.Edits[i].Span.WithAll(tf) - } - for i := range tc.LineEdits { - tc.LineEdits[i].Span, _ = tc.LineEdits[i].Span.WithAll(tf) - } - } -} - -func DiffTest(t *testing.T, compute func(uri span.URI, before, after string) []diff.TextEdit) { +func DiffTest(t *testing.T, compute func(before, after string) []diff.Edit) { for _, test := range TestCases { t.Run(test.Name, func(t *testing.T) { - edits := compute(span.URIFromPath("/"+test.Name), test.In, test.Out) - got := diff.ApplyEdits(test.In, edits) + edits := compute(test.In, test.Out) + got := diff.Apply(test.In, edits) unified := diff.Unified(FileA, FileB, test.In, edits) if got != test.Out { t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out) @@ -260,7 +256,3 @@ func DiffTest(t *testing.T, compute func(uri span.URI, before, after string) []d }) } } - -func newSpan(start, end int) span.Span { - return span.New("", span.NewPoint(0, 0, start), span.NewPoint(0, 0, end)) -} diff --git a/internal/diff/difftest/difftest_test.go b/internal/diff/difftest/difftest_test.go index c7614325125..a990e522438 100644 --- a/internal/diff/difftest/difftest_test.go +++ b/internal/diff/difftest/difftest_test.go @@ -34,7 +34,7 @@ func TestVerifyUnified(t *testing.T) { diff = difftest.UnifiedPrefix + diff } if diff != test.Unified { - t.Errorf("unified:\n%q\ndiff -u:\n%q", test.Unified, diff) + t.Errorf("unified:\n%s\ndiff -u:\n%s", test.Unified, diff) } }) } diff --git a/internal/diff/myers/diff.go b/internal/diff/myers/diff.go index 2188c0d4548..7c2d4356b42 100644 --- a/internal/diff/myers/diff.go +++ b/internal/diff/myers/diff.go @@ -9,26 +9,36 @@ import ( "strings" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" ) // Sources: // https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/ // https://www.codeproject.com/Articles/42279/%2FArticles%2F42279%2FInvestigating-Myers-diff-algorithm-Part-1-of-2 -func ComputeEdits(uri span.URI, before, after string) []diff.TextEdit { - ops := operations(splitLines(before), splitLines(after)) - edits := make([]diff.TextEdit, 0, len(ops)) +func ComputeEdits(before, after string) []diff.Edit { + beforeLines := splitLines(before) + ops := operations(beforeLines, splitLines(after)) + + // Build a table mapping line number to offset. + lineOffsets := make([]int, 0, len(beforeLines)+1) + total := 0 + for i := range beforeLines { + lineOffsets = append(lineOffsets, total) + total += len(beforeLines[i]) + } + lineOffsets = append(lineOffsets, total) // EOF + + edits := make([]diff.Edit, 0, len(ops)) for _, op := range ops { - s := span.New(uri, span.NewPoint(op.I1+1, 1, 0), span.NewPoint(op.I2+1, 1, 0)) + start, end := lineOffsets[op.I1], lineOffsets[op.I2] switch op.Kind { case diff.Delete: - // Delete: unformatted[i1:i2] is deleted. - edits = append(edits, diff.TextEdit{Span: s}) + // Delete: before[I1:I2] is deleted. + edits = append(edits, diff.Edit{Start: start, End: end}) case diff.Insert: - // Insert: formatted[j1:j2] is inserted at unformatted[i1:i1]. + // Insert: after[J1:J2] is inserted at before[I1:I1]. if content := strings.Join(op.Content, ""); content != "" { - edits = append(edits, diff.TextEdit{Span: s, NewText: content}) + edits = append(edits, diff.Edit{Start: start, End: end, New: content}) } } } diff --git a/internal/diff/ndiff.go b/internal/diff/ndiff.go index e369f4600ab..e76d2db775c 100644 --- a/internal/diff/ndiff.go +++ b/internal/diff/ndiff.go @@ -5,13 +5,10 @@ package diff import ( - "go/token" - "log" "strings" "unicode/utf8" "golang.org/x/tools/internal/diff/lcs" - "golang.org/x/tools/internal/span" ) // maxDiffs is a limit on how deeply the lcs algorithm should search @@ -23,59 +20,39 @@ const maxDiffs = 30 // is why the arguments are strings, not []bytes.) // TODO(adonovan): opt: consider switching everything to []bytes, if // that's the more common type in practice. Or provide both flavors? -func Strings(uri span.URI, before, after string) []TextEdit { +func Strings(before, after string) []Edit { if before == after { // very frequently true return nil } - // the diffs returned by the lcs package use indexes into whatever slice - // was passed in. TextEdits need a span.Span which is computed with - // byte offsets, so rune or line offsets need to be converted. + // The diffs returned by the lcs package use indexes into + // whatever slice was passed in. Edits use byte offsets, so + // rune or line offsets need to be converted. // TODO(adonovan): opt: eliminate all the unnecessary allocations. - if needrunes(before) || needrunes(after) { - diffs, _ := lcs.Compute([]rune(before), []rune(after), maxDiffs/2) + var diffs []lcs.Diff + if !isASCII(before) || !isASCII(after) { + diffs, _ = lcs.Compute([]rune(before), []rune(after), maxDiffs/2) diffs = runeOffsets(diffs, []rune(before)) - return convertDiffs(uri, diffs, []byte(before)) } else { - diffs, _ := lcs.Compute([]byte(before), []byte(after), maxDiffs/2) - return convertDiffs(uri, diffs, []byte(before)) + // Common case: pure ASCII. Avoid expansion to []rune slice. + diffs, _ = lcs.Compute([]byte(before), []byte(after), maxDiffs/2) } + return convertDiffs(diffs) } // Lines computes the differences between two list of lines. // TODO(adonovan): unused except by its test. Do we actually need it? -func Lines(uri span.URI, before, after []string) []TextEdit { +func Lines(before, after []string) []Edit { diffs, _ := lcs.Compute(before, after, maxDiffs/2) diffs = lineOffsets(diffs, before) - return convertDiffs(uri, diffs, []byte(strJoin(before))) + return convertDiffs(diffs) // the code is not coping with possible missing \ns at the ends } -// convert diffs with byte offsets into diffs with line and column -func convertDiffs(uri span.URI, diffs []lcs.Diff, src []byte) []TextEdit { - ans := make([]TextEdit, len(diffs)) - - // Reuse the machinery of go/token to convert (content, offset) to (line, column). - tf := token.NewFileSet().AddFile("", -1, len(src)) - tf.SetLinesForContent(src) - - offsetToPoint := func(offset int) span.Point { - // Re-use span.ToPosition's EOF workaround. - // It is infallible if the diffs are consistent with src. - line, col, err := span.ToPosition(tf, offset) - if err != nil { - log.Fatalf("invalid offset: %v", err) - } - return span.NewPoint(line, col, offset) - } - +func convertDiffs(diffs []lcs.Diff) []Edit { + ans := make([]Edit, len(diffs)) for i, d := range diffs { - start := offsetToPoint(d.Start) - end := start - if d.End != d.Start { - end = offsetToPoint(d.End) - } - ans[i] = TextEdit{span.New(uri, start, end), d.Text} + ans[i] = Edit{d.Start, d.End, d.Text} } return ans } @@ -131,13 +108,12 @@ func strJoin(elems []string) string { return b.String() } -// need runes is true if the string needs to be converted to []rune -// for random access -func needrunes(s string) bool { +// isASCII reports whether s contains only ASCII. +func isASCII(s string) bool { for i := 0; i < len(s); i++ { if s[i] >= utf8.RuneSelf { - return true + return false } } - return false + return true } diff --git a/internal/diff/unified.go b/internal/diff/unified.go index 13ef677b296..f8618328f0c 100644 --- a/internal/diff/unified.go +++ b/internal/diff/unified.go @@ -9,10 +9,12 @@ import ( "strings" ) -// Unified applies the edits to oldContent and presents a unified diff. -// The two labels are the names of the old and new files. -func Unified(oldLabel, newLabel string, oldContent string, edits []TextEdit) string { - return toUnified(oldLabel, newLabel, oldContent, edits).String() +// TODO(adonovan): API: hide all but func Unified. + +// Unified applies the edits to content and presents a unified diff. +// The old and new labels are the names of the content and result files. +func Unified(oldLabel, newLabel string, content string, edits []Edit) string { + return toUnified(oldLabel, newLabel, content, edits).String() } // unified represents a set of edits as a unified diff. @@ -82,7 +84,7 @@ const ( // toUnified takes a file contents and a sequence of edits, and calculates // a unified diff that represents those edits. -func toUnified(fromName, toName string, content string, edits []TextEdit) unified { +func toUnified(fromName, toName string, content string, edits []Edit) unified { u := unified{ From: fromName, To: toName, @@ -90,17 +92,20 @@ func toUnified(fromName, toName string, content string, edits []TextEdit) unifie if len(edits) == 0 { return u } - edits, partial := prepareEdits(content, edits) - if partial { - edits = lineEdits(content, edits) - } + edits = LineEdits(content, edits) // expand to whole lines lines := splitLines(content) var h *hunk last := 0 toLine := 0 for _, edit := range edits { - start := edit.Span.Start().Line() - 1 - end := edit.Span.End().Line() - 1 + // Compute the zero-based line numbers of the edit start and end. + // TODO(adonovan): opt: compute incrementally, avoid O(n^2). + start := strings.Count(content[:edit.Start], "\n") + end := strings.Count(content[:edit.End], "\n") + if edit.End == len(content) && len(content) > 0 && content[len(content)-1] != '\n' { + end++ // EOF counts as an implicit newline + } + switch { case h != nil && start == last: //direct extension @@ -129,8 +134,8 @@ func toUnified(fromName, toName string, content string, edits []TextEdit) unifie h.Lines = append(h.Lines, line{Kind: Delete, Content: lines[i]}) last++ } - if edit.NewText != "" { - for _, content := range splitLines(edit.NewText) { + if edit.New != "" { + for _, content := range splitLines(edit.New) { h.Lines = append(h.Lines, line{Kind: Insert, Content: content}) toLine++ } diff --git a/internal/span/span.go b/internal/span/span.go index 502145bbea7..a714cca8447 100644 --- a/internal/span/span.go +++ b/internal/span/span.go @@ -42,7 +42,7 @@ var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}} var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}} -func New(uri URI, start Point, end Point) Span { +func New(uri URI, start, end Point) Span { s := Span{v: span{URI: uri, Start: start.v, End: end.v}} s.v.clean() return s diff --git a/internal/span/token.go b/internal/span/token.go index d3827eb5fd0..da4dc22febb 100644 --- a/internal/span/token.go +++ b/internal/span/token.go @@ -141,7 +141,7 @@ func position(tf *token.File, pos token.Pos) (string, int, int, error) { func positionFromOffset(tf *token.File, offset int) (string, int, int, error) { if offset > tf.Size() { - return "", 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, tf.Size()) + return "", 0, 0, fmt.Errorf("offset %d is beyond EOF (%d) in file %s", offset, tf.Size(), tf.Name()) } pos := tf.Pos(offset) p := tf.Position(pos) From 2f57270232c2da03fc5fbd810738cd19d76ae4a2 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 4 Oct 2022 20:20:36 -0400 Subject: [PATCH 302/723] gopls: update golang.org/x/vuln This is to pick up the fix for the file-scheme url handling bug cd gopls GOPROXY=direct go get golang.org/x/vuln@2aa0553d353b go mod tidy -compat=1.16 Also updates the tests to use span.URIFromPath to generate correct test database file uris. Change-Id: I6de296cd21f3b98d72700ea57d1aa867658e7ac3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/438756 Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/go.mod | 4 ++-- gopls/go.sum | 8 +++----- gopls/internal/regtest/misc/vuln_test.go | 8 +++++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gopls/go.mod b/gopls/go.mod index 6cd8643cc42..27f8d44e6a1 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -11,8 +11,8 @@ require ( golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 golang.org/x/text v0.3.7 - golang.org/x/tools v0.1.13-0.20220810174125-0ad49fdeb955 - golang.org/x/vuln v0.0.0-20220919155316-41b1fc70d0a6 + golang.org/x/tools v0.1.13-0.20220928184430-f80e98464e27 + golang.org/x/vuln v0.0.0-20221004232641-2aa0553d353b honnef.co/go/tools v0.3.3 mvdan.cc/gofumpt v0.3.1 mvdan.cc/xurls/v2 v2.4.0 diff --git a/gopls/go.sum b/gopls/go.sum index c26382551c2..b7a866c8b13 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -10,7 +10,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/google/go-cmdtest v0.4.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= +github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= @@ -69,10 +69,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/vuln v0.0.0-20220901221904-62b0186a1058 h1:YnB27EXBD8XxB0JcaOeluuvhF2kS4DrH0k+lpopG2xc= -golang.org/x/vuln v0.0.0-20220901221904-62b0186a1058/go.mod h1:7tDfEDtOLlzHQRi4Yzfg5seVBSvouUIjyPzBx4q5CxQ= -golang.org/x/vuln v0.0.0-20220919155316-41b1fc70d0a6 h1:tQFrcJZ95V1wiLLPoAIaEuEVXJ7JkhbZI4Hws7Fx69c= -golang.org/x/vuln v0.0.0-20220919155316-41b1fc70d0a6/go.mod h1:7tDfEDtOLlzHQRi4Yzfg5seVBSvouUIjyPzBx4q5CxQ= +golang.org/x/vuln v0.0.0-20221004232641-2aa0553d353b h1:8Tu9pgIV7kt8ulNtzidzpLl9E9l1i+U4QLdKG0ZzHyE= +golang.org/x/vuln v0.0.0-20221004232641-2aa0553d353b/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 180431c8ab4..5ba545454e7 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -6,7 +6,6 @@ package misc import ( "os" - "path" "path/filepath" "testing" @@ -14,6 +13,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -69,10 +69,11 @@ func main() { ` cwd, _ := os.Getwd() + uri := span.URIFromPath(filepath.Join(cwd, "testdata", "vulndb")) WithOptions( EnvVars{ // Let the analyzer read vulnerabilities data from the testdata/vulndb. - "GOVULNDB": "file://" + path.Join(filepath.ToSlash(cwd), "testdata", "vulndb"), + "GOVULNDB": string(uri), // When fetchinging stdlib package vulnerability info, // behave as if our go version is go1.18 for this testing. // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). @@ -226,11 +227,12 @@ func TestRunVulncheckExp(t *testing.T) { testenv.NeedsGo1Point(t, 18) cwd, _ := os.Getwd() + uri := span.URIFromPath(filepath.Join(cwd, "testdata", "vulndb")) WithOptions( ProxyFiles(proxy1), EnvVars{ // Let the analyzer read vulnerabilities data from the testdata/vulndb. - "GOVULNDB": "file://" + path.Join(filepath.ToSlash(cwd), "testdata", "vulndb"), + "GOVULNDB": string(uri), // When fetching stdlib package vulnerability info, // behave as if our go version is go1.18 for this testing. // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). From 4ef38dc8f29a375c82cd4ee8d6501fa28af1a958 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 27 Sep 2022 17:12:41 -0400 Subject: [PATCH 303/723] gopls/internal/vulncheck/vulntest: add test helpers This package hosts helper utilities for vulncheck features. This package requires go1.18+. Most of the code were adopted from golang.org/x/vulndb/internal. The first batch is NewDatabase reads YAML-format vulnerability information files (https://github.com/golang/vulndb/blob/master/doc/format.md) packaged in txtar, and creates a filesystem-based vuln database that can be used as a data source of golang.org/x/vuln/client APIs. See db_test.go for example. * Source of the code db.go: golang.org/x/vulndb/internal/database#Generate report.go: golang.org/x/vulndb/internal/report#Report stdlib.go: golang.org/x/vulndb/internal/stdlib This change adds a new dependency on "gopkg.in/yaml.v3" for parsing YAMLs in testing Change-Id: Ica5da4284c38a8a9531b1f943deb4288a2058c9b Reviewed-on: https://go-review.googlesource.com/c/tools/+/435358 Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/go.mod | 1 + gopls/go.sum | 3 + gopls/internal/vulncheck/vulntest/db.go | 303 ++++++++++++++++++ gopls/internal/vulncheck/vulntest/db_test.go | 61 ++++ gopls/internal/vulncheck/vulntest/report.go | 176 ++++++++++ .../vulncheck/vulntest/report_test.go | 52 +++ gopls/internal/vulncheck/vulntest/stdlib.go | 26 ++ .../vulncheck/vulntest/stdlib_test.go | 27 ++ .../vulncheck/vulntest/testdata/report.yaml | 15 + 9 files changed, 664 insertions(+) create mode 100644 gopls/internal/vulncheck/vulntest/db.go create mode 100644 gopls/internal/vulncheck/vulntest/db_test.go create mode 100644 gopls/internal/vulncheck/vulntest/report.go create mode 100644 gopls/internal/vulncheck/vulntest/report_test.go create mode 100644 gopls/internal/vulncheck/vulntest/stdlib.go create mode 100644 gopls/internal/vulncheck/vulntest/stdlib_test.go create mode 100644 gopls/internal/vulncheck/vulntest/testdata/report.yaml diff --git a/gopls/go.mod b/gopls/go.mod index 27f8d44e6a1..e42749e71b8 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -13,6 +13,7 @@ require ( golang.org/x/text v0.3.7 golang.org/x/tools v0.1.13-0.20220928184430-f80e98464e27 golang.org/x/vuln v0.0.0-20221004232641-2aa0553d353b + gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.3.3 mvdan.cc/gofumpt v0.3.1 mvdan.cc/xurls/v2 v2.4.0 diff --git a/gopls/go.sum b/gopls/go.sum index b7a866c8b13..9ced54e9681 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -76,11 +76,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.3.3 h1:oDx7VAwstgpYpb3wv0oxiZlxY+foCpRAwY7Vk6XpAgA= honnef.co/go/tools v0.3.3/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= diff --git a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/vulntest/db.go new file mode 100644 index 00000000000..674e920b5f2 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/db.go @@ -0,0 +1,303 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +// Package vulntest provides helpers for vulncheck functionality testing. +package vulntest + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "golang.org/x/tools/internal/span" + "golang.org/x/tools/txtar" + "golang.org/x/vuln/client" + "golang.org/x/vuln/osv" +) + +// NewDatabase returns a read-only DB containing the provided +// txtar-format collection of vulnerability reports. +// Each vulnerability report is a YAML file whose format +// is defined in golang.org/x/vulndb/doc/format.md. +// A report file name must have the id as its base name, +// and have .yaml as its extension. +// +// db, err := NewDatabase(ctx, reports) +// ... +// defer db.Clean() +// client, err := NewClient(db) +// ... +// +// The returned DB's Clean method must be called to clean up the +// generated database. +func NewDatabase(ctx context.Context, txtarReports []byte) (*DB, error) { + disk, err := ioutil.TempDir("", "vulndb-test") + if err != nil { + return nil, err + } + if err := generateDB(ctx, txtarReports, disk, false); err != nil { + os.RemoveAll(disk) + return nil, err + } + + return &DB{disk: disk}, nil +} + +// DB is a read-only vulnerability database on disk. +// Users can use this database with golang.org/x/vuln APIs +// by setting the `VULNDB“ environment variable. +type DB struct { + disk string +} + +// URI returns the file URI that can be used for VULNDB environment +// variable. +func (db *DB) URI() string { + u := span.URIFromPath(db.disk) + return string(u) +} + +// Clean deletes the database. +func (db *DB) Clean() error { + return os.RemoveAll(db.disk) +} + +// NewClient returns a vuln DB client that works with the given DB. +func NewClient(db *DB) (client.Client, error) { + return client.NewClient([]string{db.URI()}, client.Options{}) +} + +// +// The following was selectively copied from golang.org/x/vulndb/internal/database +// + +const ( + dbURL = "https://pkg.go.dev/vuln/" + + // idDirectory is the name of the directory that contains entries + // listed by their IDs. + idDirectory = "ID" + + // stdFileName is the name of the .json file in the vulndb repo + // that will contain info on standard library vulnerabilities. + stdFileName = "stdlib" + + // toolchainFileName is the name of the .json file in the vulndb repo + // that will contain info on toolchain (cmd/...) vulnerabilities. + toolchainFileName = "toolchain" + + // cmdModule is the name of the module containing Go toolchain + // binaries. + cmdModule = "cmd" + + // stdModule is the name of the module containing Go std packages. + stdModule = "std" +) + +// generateDB generates the file-based vuln DB in the directory jsonDir. +func generateDB(ctx context.Context, txtarData []byte, jsonDir string, indent bool) error { + archive := txtar.Parse(txtarData) + + jsonVulns, entries, err := generateEntries(ctx, archive) + if err != nil { + return err + } + + index := make(client.DBIndex, len(jsonVulns)) + for modulePath, vulns := range jsonVulns { + epath, err := client.EscapeModulePath(modulePath) + if err != nil { + return err + } + if err := writeVulns(filepath.Join(jsonDir, epath), vulns, indent); err != nil { + return err + } + for _, v := range vulns { + if v.Modified.After(index[modulePath]) { + index[modulePath] = v.Modified + } + } + } + if err := writeJSON(filepath.Join(jsonDir, "index.json"), index, indent); err != nil { + return err + } + if err := writeAliasIndex(jsonDir, entries, indent); err != nil { + return err + } + return writeEntriesByID(filepath.Join(jsonDir, idDirectory), entries, indent) +} + +func generateEntries(_ context.Context, archive *txtar.Archive) (map[string][]osv.Entry, []osv.Entry, error) { + now := time.Now() + jsonVulns := map[string][]osv.Entry{} + var entries []osv.Entry + for _, f := range archive.Files { + if !strings.HasSuffix(f.Name, ".yaml") { + continue + } + r, err := readReport(bytes.NewReader(f.Data)) + if err != nil { + return nil, nil, err + } + name := strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name)) + linkName := fmt.Sprintf("%s%s", dbURL, name) + entry, modulePaths := generateOSVEntry(name, linkName, now, *r) + for _, modulePath := range modulePaths { + jsonVulns[modulePath] = append(jsonVulns[modulePath], entry) + } + entries = append(entries, entry) + } + return jsonVulns, entries, nil +} + +func writeVulns(outPath string, vulns []osv.Entry, indent bool) error { + if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { + return fmt.Errorf("failed to create directory %q: %s", filepath.Dir(outPath), err) + } + return writeJSON(outPath+".json", vulns, indent) +} + +func writeEntriesByID(idDir string, entries []osv.Entry, indent bool) error { + // Write a directory containing entries by ID. + if err := os.MkdirAll(idDir, 0755); err != nil { + return fmt.Errorf("failed to create directory %q: %v", idDir, err) + } + var idIndex []string + for _, e := range entries { + outPath := filepath.Join(idDir, e.ID+".json") + if err := writeJSON(outPath, e, indent); err != nil { + return err + } + idIndex = append(idIndex, e.ID) + } + // Write an index.json in the ID directory with a list of all the IDs. + return writeJSON(filepath.Join(idDir, "index.json"), idIndex, indent) +} + +// Write a JSON file containing a map from alias to GO IDs. +func writeAliasIndex(dir string, entries []osv.Entry, indent bool) error { + aliasToGoIDs := map[string][]string{} + for _, e := range entries { + for _, a := range e.Aliases { + aliasToGoIDs[a] = append(aliasToGoIDs[a], e.ID) + } + } + return writeJSON(filepath.Join(dir, "aliases.json"), aliasToGoIDs, indent) +} + +func writeJSON(filename string, value any, indent bool) (err error) { + j, err := jsonMarshal(value, indent) + if err != nil { + return err + } + return os.WriteFile(filename, j, 0644) +} + +func jsonMarshal(v any, indent bool) ([]byte, error) { + if indent { + return json.MarshalIndent(v, "", " ") + } + return json.Marshal(v) +} + +// generateOSVEntry create an osv.Entry for a report. In addition to the report, it +// takes the ID for the vuln and a URL that will point to the entry in the vuln DB. +// It returns the osv.Entry and a list of module paths that the vuln affects. +func generateOSVEntry(id, url string, lastModified time.Time, r Report) (osv.Entry, []string) { + entry := osv.Entry{ + ID: id, + Published: r.Published, + Modified: lastModified, + Withdrawn: r.Withdrawn, + Details: r.Description, + } + + moduleMap := make(map[string]bool) + for _, m := range r.Modules { + switch m.Module { + case stdModule: + moduleMap[stdFileName] = true + case cmdModule: + moduleMap[toolchainFileName] = true + default: + moduleMap[m.Module] = true + } + entry.Affected = append(entry.Affected, generateAffected(m, url)) + } + for _, ref := range r.References { + entry.References = append(entry.References, osv.Reference{ + Type: string(ref.Type), + URL: ref.URL, + }) + } + + var modulePaths []string + for module := range moduleMap { + modulePaths = append(modulePaths, module) + } + // TODO: handle missing fields - Aliases + + return entry, modulePaths +} + +func generateAffectedRanges(versions []VersionRange) osv.Affects { + a := osv.AffectsRange{Type: osv.TypeSemver} + if len(versions) == 0 || versions[0].Introduced == "" { + a.Events = append(a.Events, osv.RangeEvent{Introduced: "0"}) + } + for _, v := range versions { + if v.Introduced != "" { + a.Events = append(a.Events, osv.RangeEvent{Introduced: v.Introduced.Canonical()}) + } + if v.Fixed != "" { + a.Events = append(a.Events, osv.RangeEvent{Fixed: v.Fixed.Canonical()}) + } + } + return osv.Affects{a} +} + +func generateImports(m *Module) (imps []osv.EcosystemSpecificImport) { + for _, p := range m.Packages { + syms := append([]string{}, p.Symbols...) + syms = append(syms, p.DerivedSymbols...) + sort.Strings(syms) + imps = append(imps, osv.EcosystemSpecificImport{ + Path: p.Package, + GOOS: p.GOOS, + GOARCH: p.GOARCH, + Symbols: syms, + }) + } + return imps +} +func generateAffected(m *Module, url string) osv.Affected { + name := m.Module + switch name { + case stdModule: + name = "stdlib" + case cmdModule: + name = "toolchain" + } + return osv.Affected{ + Package: osv.Package{ + Name: name, + Ecosystem: osv.GoEcosystem, + }, + Ranges: generateAffectedRanges(m.Versions), + DatabaseSpecific: osv.DatabaseSpecific{URL: url}, + EcosystemSpecific: osv.EcosystemSpecific{ + Imports: generateImports(m), + }, + } +} diff --git a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulncheck/vulntest/db_test.go new file mode 100644 index 00000000000..7d939421c94 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/db_test.go @@ -0,0 +1,61 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import ( + "context" + "encoding/json" + "testing" +) + +func TestNewDatabase(t *testing.T) { + ctx := context.Background() + in := []byte(` +-- GO-2020-0001.yaml -- +modules: + - module: github.com/gin-gonic/gin + versions: + - fixed: 1.6.0 + packages: + - package: github.com/gin-gonic/gin + symbols: + - defaultLogFormatter +description: | + Something. +published: 2021-04-14T20:04:52Z +references: + - fix: https://github.com/gin-gonic/gin/pull/2237 +`) + + db, err := NewDatabase(ctx, in) + if err != nil { + t.Fatal(err) + } + defer db.Clean() + + cli, err := NewClient(db) + if err != nil { + t.Fatal(err) + } + got, err := cli.GetByID(ctx, "GO-2020-0001") + if err != nil { + t.Fatal(err) + } + if got.ID != "GO-2020-0001" { + m, _ := json.Marshal(got) + t.Errorf("got %s\nwant GO-2020-0001 entry", m) + } + gotAll, err := cli.GetByModule(ctx, "github.com/gin-gonic/gin") + if err != nil { + t.Fatal(err) + } + if len(gotAll) != 1 || gotAll[0].ID != "GO-2020-0001" { + m, _ := json.Marshal(got) + t.Errorf("got %s\nwant GO-2020-0001 entry", m) + } +} diff --git a/gopls/internal/vulncheck/vulntest/report.go b/gopls/internal/vulncheck/vulntest/report.go new file mode 100644 index 00000000000..e5595e8ba06 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/report.go @@ -0,0 +1,176 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import ( + "fmt" + "io" + "os" + "strings" + "time" + + "golang.org/x/mod/semver" + "gopkg.in/yaml.v3" +) + +// +// The following was selectively copied from golang.org/x/vulndb/internal/report +// + +// readReport reads a Report in YAML format. +func readReport(in io.Reader) (*Report, error) { + d := yaml.NewDecoder(in) + // Require that all fields in the file are in the struct. + // This corresponds to v2's UnmarshalStrict. + d.KnownFields(true) + var r Report + if err := d.Decode(&r); err != nil { + return nil, fmt.Errorf("yaml.Decode: %v", err) + } + return &r, nil +} + +// Report represents a vulnerability report in the vulndb. +// Remember to update doc/format.md when this structure changes. +type Report struct { + Modules []*Module `yaml:",omitempty"` + + // Description is the CVE description from an existing CVE. If we are + // assigning a CVE ID ourselves, use CVEMetadata.Description instead. + Description string `yaml:",omitempty"` + Published time.Time `yaml:",omitempty"` + Withdrawn *time.Time `yaml:",omitempty"` + + References []*Reference `yaml:",omitempty"` +} + +// Write writes r to filename in YAML format. +func (r *Report) Write(filename string) (err error) { + f, err := os.Create(filename) + if err != nil { + return err + } + err = r.encode(f) + err2 := f.Close() + if err == nil { + err = err2 + } + return err +} + +// ToString encodes r to a YAML string. +func (r *Report) ToString() (string, error) { + var b strings.Builder + if err := r.encode(&b); err != nil { + return "", err + } + return b.String(), nil +} + +func (r *Report) encode(w io.Writer) error { + e := yaml.NewEncoder(w) + defer e.Close() + e.SetIndent(4) + return e.Encode(r) +} + +type VersionRange struct { + Introduced Version `yaml:"introduced,omitempty"` + Fixed Version `yaml:"fixed,omitempty"` +} + +type Module struct { + Module string `yaml:",omitempty"` + Versions []VersionRange `yaml:",omitempty"` + Packages []*Package `yaml:",omitempty"` +} + +type Package struct { + Package string `yaml:",omitempty"` + GOOS []string `yaml:"goos,omitempty"` + GOARCH []string `yaml:"goarch,omitempty"` + // Symbols originally identified as vulnerable. + Symbols []string `yaml:",omitempty"` + // Additional vulnerable symbols, computed from Symbols via static analysis + // or other technique. + DerivedSymbols []string `yaml:"derived_symbols,omitempty"` +} + +// Version is an SemVer 2.0.0 semantic version with no leading "v" prefix, +// as used by OSV. +type Version string + +// V returns the version with a "v" prefix. +func (v Version) V() string { + return "v" + string(v) +} + +// IsValid reports whether v is a valid semantic version string. +func (v Version) IsValid() bool { + return semver.IsValid(v.V()) +} + +// Before reports whether v < v2. +func (v Version) Before(v2 Version) bool { + return semver.Compare(v.V(), v2.V()) < 0 +} + +// Canonical returns the canonical formatting of the version. +func (v Version) Canonical() string { + return strings.TrimPrefix(semver.Canonical(v.V()), "v") +} + +// Reference type is a reference (link) type. +type ReferenceType string + +const ( + ReferenceTypeAdvisory = ReferenceType("ADVISORY") + ReferenceTypeArticle = ReferenceType("ARTICLE") + ReferenceTypeReport = ReferenceType("REPORT") + ReferenceTypeFix = ReferenceType("FIX") + ReferenceTypePackage = ReferenceType("PACKAGE") + ReferenceTypeEvidence = ReferenceType("EVIDENCE") + ReferenceTypeWeb = ReferenceType("WEB") +) + +// ReferenceTypes is the set of reference types defined in OSV. +var ReferenceTypes = []ReferenceType{ + ReferenceTypeAdvisory, + ReferenceTypeArticle, + ReferenceTypeReport, + ReferenceTypeFix, + ReferenceTypePackage, + ReferenceTypeEvidence, + ReferenceTypeWeb, +} + +// A Reference is a link to some external resource. +// +// For ease of typing, References are represented in the YAML as a +// single-element mapping of type to URL. +type Reference struct { + Type ReferenceType `json:"type,omitempty"` + URL string `json:"url,omitempty"` +} + +func (r *Reference) MarshalYAML() (interface{}, error) { + return map[string]string{ + strings.ToLower(string(r.Type)): r.URL, + }, nil +} + +func (r *Reference) UnmarshalYAML(n *yaml.Node) (err error) { + if n.Kind != yaml.MappingNode || len(n.Content) != 2 || n.Content[0].Kind != yaml.ScalarNode || n.Content[1].Kind != yaml.ScalarNode { + return &yaml.TypeError{Errors: []string{ + fmt.Sprintf("line %d: report.Reference must contain a mapping with one value", n.Line), + }} + } + r.Type = ReferenceType(strings.ToUpper(n.Content[0].Value)) + r.URL = n.Content[1].Value + return nil +} diff --git a/gopls/internal/vulncheck/vulntest/report_test.go b/gopls/internal/vulncheck/vulntest/report_test.go new file mode 100644 index 00000000000..c42dae805fa --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/report_test.go @@ -0,0 +1,52 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func readAll(t *testing.T, filename string) io.Reader { + d, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + return bytes.NewReader(d) +} + +func TestRoundTrip(t *testing.T) { + // A report shouldn't change after being read and then written. + in := filepath.Join("testdata", "report.yaml") + r, err := readReport(readAll(t, in)) + if err != nil { + t.Fatal(err) + } + out := filepath.Join(t.TempDir(), "report.yaml") + if err := r.Write(out); err != nil { + t.Fatal(err) + } + + want, err := os.ReadFile(in) + if err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("mismatch (-want, +got):\n%s", diff) + } +} diff --git a/gopls/internal/vulncheck/vulntest/stdlib.go b/gopls/internal/vulncheck/vulntest/stdlib.go new file mode 100644 index 00000000000..9bf4d4ef0d4 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/stdlib.go @@ -0,0 +1,26 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import ( + "strings" + + "golang.org/x/mod/module" +) + +// maybeStdlib reports whether the given import path could be part of the Go +// standard library, by reporting whether the first component lacks a '.'. +func maybeStdlib(path string) bool { + if err := module.CheckImportPath(path); err != nil { + return false + } + if i := strings.IndexByte(path, '/'); i != -1 { + path = path[:i] + } + return !strings.Contains(path, ".") +} diff --git a/gopls/internal/vulncheck/vulntest/stdlib_test.go b/gopls/internal/vulncheck/vulntest/stdlib_test.go new file mode 100644 index 00000000000..8f893f3ec42 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/stdlib_test.go @@ -0,0 +1,27 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import "testing" + +func TestMaybeStdlib(t *testing.T) { + for _, test := range []struct { + in string + want bool + }{ + {"", false}, + {"math/crypto", true}, + {"github.com/pkg/errors", false}, + {"Path is unknown", false}, + } { + got := maybeStdlib(test.in) + if got != test.want { + t.Errorf("%q: got %t, want %t", test.in, got, test.want) + } + } +} diff --git a/gopls/internal/vulncheck/vulntest/testdata/report.yaml b/gopls/internal/vulncheck/vulntest/testdata/report.yaml new file mode 100644 index 00000000000..48384b543b2 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/testdata/report.yaml @@ -0,0 +1,15 @@ +modules: + - module: github.com/gin-gonic/gin + versions: + - fixed: 1.6.0 + packages: + - package: github.com/gin-gonic/gin + symbols: + - defaultLogFormatter +description: | + The default Formatter for the Logger middleware (LoggerConfig.Formatter), + which is included in the Default engine, allows attackers to inject arbitrary + log entries by manipulating the request path. +references: + - fix: https://github.com/gin-gonic/gin/pull/1234 + - fix: https://github.com/gin-gonic/gin/commit/abcdefg From dc3cf95c8ac52dd8b765f37a431871194a78329f Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 4 Oct 2022 20:57:09 -0400 Subject: [PATCH 304/723] gopls/internal/vulncheck: use vulntest for test database creation Change-Id: Ie224452dfa512647e4f829e7f3be48db129cf91b Reviewed-on: https://go-review.googlesource.com/c/tools/+/438741 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller Run-TryBot: Hyang-Ah Hana Kim --- gopls/internal/vulncheck/command_test.go | 128 ++++++++++------------- 1 file changed, 54 insertions(+), 74 deletions(-) diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index 9b8678dc1e0..a9ab2c0a731 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -24,13 +24,22 @@ import ( "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/vuln/client" - "golang.org/x/vuln/osv" + "golang.org/x/tools/gopls/internal/vulncheck/vulntest" ) func TestCmd_Run(t *testing.T) { runTest(t, workspace1, proxy1, func(ctx context.Context, snapshot source.Snapshot) { - cmd := &cmd{Client: testClient1} + db, err := vulntest.NewDatabase(ctx, []byte(vulnsData)) + if err != nil { + t.Fatal(err) + } + defer db.Clean() + cli, err := vulntest.NewClient(db) + if err != nil { + t.Fatal(err) + } + + cmd := &cmd{Client: cli} cfg := packagesCfg(ctx, snapshot) result, err := cmd.Run(ctx, cfg, "./...") if err != nil { @@ -55,6 +64,7 @@ func TestCmd_Run(t *testing.T) { { Vuln: Vuln{ ID: "GO-2022-01", + Details: "Something.\n", Symbol: "VulnData.Vuln1", PkgPath: "golang.org/amod/avuln", ModPath: "golang.org/amod", @@ -92,7 +102,7 @@ func TestCmd_Run(t *testing.T) { { Vuln: Vuln{ ID: "GO-2022-03", - Details: "unaffecting vulnerability", + Details: "unaffecting vulnerability.\n", ModPath: "golang.org/amod", URL: "https://pkg.go.dev/vuln/GO-2022-03", FixedVersion: "v1.0.4", @@ -224,76 +234,46 @@ func Vuln() { } ` -// testClient contains the following test vulnerabilities -// -// golang.org/amod/avuln.{VulnData.Vuln1, vulnData.Vuln2} -// golang.org/bmod/bvuln.{Vuln} -var testClient1 = &mockClient{ - ret: map[string][]*osv.Entry{ - "golang.org/amod": { - { - ID: "GO-2022-01", - References: []osv.Reference{ - { - Type: "href", - URL: "pkg.go.dev/vuln/GO-2022-01", - }, - }, - Affected: []osv.Affected{{ - Package: osv.Package{Name: "golang.org/amod"}, - Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.0.4"}, {Introduced: "1.1.2"}}}}, - EcosystemSpecific: osv.EcosystemSpecific{ - Imports: []osv.EcosystemSpecificImport{{ - Path: "golang.org/amod/avuln", - Symbols: []string{"VulnData.Vuln1", "VulnData.Vuln2"}}}, - }, - }}, - }, - { - ID: "GO-2022-03", - Details: "unaffecting vulnerability", - References: []osv.Reference{ - { - Type: "href", - URL: "pkg.go.dev/vuln/GO-2022-01", - }, - }, - Affected: []osv.Affected{{ - Package: osv.Package{Name: "golang.org/amod"}, - Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.0.4"}, {Introduced: "1.1.2"}}}}, - EcosystemSpecific: osv.EcosystemSpecific{ - Imports: []osv.EcosystemSpecificImport{{ - Path: "golang.org/amod/avuln", - Symbols: []string{"nonExisting"}}}, - }, - }}, - }, - }, - "golang.org/bmod": { - { - ID: "GO-2022-02", - Affected: []osv.Affected{{ - Package: osv.Package{Name: "golang.org/bmod"}, - Ranges: osv.Affects{{Type: osv.TypeSemver}}, - EcosystemSpecific: osv.EcosystemSpecific{ - Imports: []osv.EcosystemSpecificImport{{ - Path: "golang.org/bmod/bvuln", - Symbols: []string{"Vuln"}}}, - }, - }}, - }, - }, - }, -} - -type mockClient struct { - client.Client - ret map[string][]*osv.Entry -} - -func (mc *mockClient) GetByModule(ctx context.Context, a string) ([]*osv.Entry, error) { - return mc.ret[a], nil -} +const vulnsData = ` +-- GO-2022-01.yaml -- +modules: + - module: golang.org/amod + versions: + - introduced: 1.0.0 + - fixed: 1.0.4 + - introduced: 1.1.2 + packages: + - package: golang.org/amod/avuln + symbols: + - VulnData.Vuln1 + - VulnData.Vuln2 +description: | + Something. +references: + - href: pkg.go.dev/vuln/GO-2022-01 + +-- GO-2022-03.yaml -- +modules: + - module: golang.org/amod + versions: + - introduced: 1.0.0 + - fixed: 1.0.4 + - introduced: 1.1.2 + packages: + - package: golang.org/amod/avuln + symbols: + - nonExisting +description: | + unaffecting vulnerability. + +-- GO-2022-02.yaml -- +modules: + - module: golang.org/bmod + packages: + - package: golang.org/bmod/bvuln + symbols: + - Vuln +` func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Context, source.Snapshot)) { ws, err := fake.NewSandbox(&fake.SandboxConfig{ From ede3ab263ce8306a8a9cc2d169de615c077c29a5 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 5 Oct 2022 17:45:44 -0400 Subject: [PATCH 305/723] gopls/internal/lsp/protocol: simplify OffsetRange, Position These functions are now both expressed in terms of OffsetPosition, which converts a file offset to a protocol (UTF-16) position. No span.Point intermediaries are allocated. Also, note some TODOs for further simplification. Change-Id: I7e95c1b3ab16e4acfe4e461ee0aaa260efb6eec5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439276 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley --- gopls/internal/lsp/protocol/span.go | 93 ++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index f24a28e1124..cbf57a9ec51 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -7,8 +7,10 @@ package protocol import ( + "bytes" "fmt" "go/token" + "unicode/utf8" "golang.org/x/tools/internal/span" ) @@ -19,6 +21,16 @@ type ColumnMapper struct { URI span.URI TokFile *token.File Content []byte + + // File content is only really needed for UTF-16 column + // computation, which could be be achieved more compactly. + // For example, one could record only the lines for which + // UTF-16 columns differ from the UTF-8 ones, or only the + // indices of the non-ASCII characters. + // + // TODO(adonovan): consider not retaining the entire file + // content, or at least not exposing the fact that we + // currently retain it. } // NewColumnMapper creates a new column mapper for the given uri and content. @@ -72,44 +84,81 @@ func (m *ColumnMapper) Range(s span.Span) (Range, error) { // OffsetRange returns a Range for the byte-offset interval Content[start:end], func (m *ColumnMapper) OffsetRange(start, end int) (Range, error) { - // TODO(adonovan): this can surely be simplified by expressing - // it terms of more primitive operations. - - // We use span.ToPosition for its "line+1 at EOF" workaround. - startLine, startCol, err := span.ToPosition(m.TokFile, start) - if err != nil { - return Range{}, fmt.Errorf("start line/col: %v", err) - } - startPoint := span.NewPoint(startLine, startCol, start) - startPosition, err := m.Position(startPoint) + startPosition, err := m.OffsetPosition(start) if err != nil { - return Range{}, fmt.Errorf("start position: %v", err) + return Range{}, fmt.Errorf("start: %v", err) } - endLine, endCol, err := span.ToPosition(m.TokFile, end) + endPosition, err := m.OffsetPosition(end) if err != nil { - return Range{}, fmt.Errorf("end line/col: %v", err) - } - endPoint := span.NewPoint(endLine, endCol, end) - endPosition, err := m.Position(endPoint) - if err != nil { - return Range{}, fmt.Errorf("end position: %v", err) + return Range{}, fmt.Errorf("end: %v", err) } return Range{Start: startPosition, End: endPosition}, nil } +// Position returns the protocol position for the specified point, +// which must have a byte offset. func (m *ColumnMapper) Position(p span.Point) (Position, error) { - chr, err := span.ToUTF16Column(p, m.Content) + if !p.HasOffset() { + return Position{}, fmt.Errorf("point is missing offset") + } + return m.OffsetPosition(p.Offset()) +} + +// OffsetPosition returns the protocol position of the specified +// offset within m.Content. +func (m *ColumnMapper) OffsetPosition(offset int) (Position, error) { + // We use span.ToPosition for its "line+1 at EOF" workaround. + // TODO(adonovan): ToPosition honors //line directives. It probably shouldn't. + line, _, err := span.ToPosition(m.TokFile, offset) if err != nil { - return Position{}, err + return Position{}, fmt.Errorf("OffsetPosition: %v", err) + } + // If that workaround executed, skip the usual column computation. + char := 0 + if offset != m.TokFile.Size() { + char = m.utf16Column(offset) } return Position{ - Line: uint32(p.Line() - 1), - Character: uint32(chr - 1), + Line: uint32(line - 1), + Character: uint32(char), }, nil } +// utf16Column returns the zero-based column index of the +// specified file offset, measured in UTF-16 codes. +// Precondition: 0 <= offset <= len(m.Content). +func (m *ColumnMapper) utf16Column(offset int) int { + s := m.Content[:offset] + if i := bytes.LastIndex(s, []byte("\n")); i >= 0 { + s = s[i+1:] + } + // s is the prefix of the line before offset. + return utf16len(s) +} + +// utf16len returns the number of codes in the UTF-16 transcoding of s. +func utf16len(s []byte) int { + var n int + for len(s) > 0 { + n++ + + // Fast path for ASCII. + if s[0] < 0x80 { + s = s[1:] + continue + } + + r, size := utf8.DecodeRune(s) + if r >= 0x10000 { + n++ // surrogate pair + } + s = s[size:] + } + return n +} + func (m *ColumnMapper) Span(l Location) (span.Span, error) { return m.RangeSpan(l.Range) } From bd8c28ff5c76b957a23f93a67337acc0c5a4f67e Mon Sep 17 00:00:00 2001 From: cui fliter Date: Fri, 16 Sep 2022 01:55:50 +0000 Subject: [PATCH 306/723] internal/diff/lcs: fix shell format error Change-Id: Id333dd169c7b7a54823bd077ce677fc74b4eb057 GitHub-Last-Rev: f16abccaad199cf229fb2e6dbf126441b4e573b5 GitHub-Pull-Request: golang/tools#397 Reviewed-on: https://go-review.googlesource.com/c/tools/+/431136 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Reviewed-by: Bryan Mills gopls-CI: kokoro Auto-Submit: Bryan Mills --- internal/diff/lcs/git.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/diff/lcs/git.sh b/internal/diff/lcs/git.sh index caa4c424198..6856f843958 100644 --- a/internal/diff/lcs/git.sh +++ b/internal/diff/lcs/git.sh @@ -1,4 +1,3 @@ - #!/bin/bash # # Copyright 2022 The Go Authors. All rights reserved. From c4f49e40d0ff55a0ffcc7539fd7c0453a0f70593 Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 4 Oct 2022 17:09:29 -0700 Subject: [PATCH 307/723] go/analysis/passes/assign: fix map assignment Eliminate false positives on map self-assignments. These have side-effects. Fixes golang/go#54840 Change-Id: I1b419e142a9681c68ea6fcddf8b368af903b1b54 Reviewed-on: https://go-review.googlesource.com/c/tools/+/438796 gopls-CI: kokoro Reviewed-by: Zvonimir Pavlinovic Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Tim King --- go/analysis/passes/assign/assign.go | 15 +++++++++- go/analysis/passes/assign/testdata/src/a/a.go | 28 +++++++++++++++++++ .../passes/assign/testdata/src/a/a.go.golden | 28 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/go/analysis/passes/assign/assign.go b/go/analysis/passes/assign/assign.go index 3586638efc0..89146b73346 100644 --- a/go/analysis/passes/assign/assign.go +++ b/go/analysis/passes/assign/assign.go @@ -12,6 +12,7 @@ import ( "fmt" "go/ast" "go/token" + "go/types" "reflect" "golang.org/x/tools/go/analysis" @@ -51,7 +52,8 @@ func run(pass *analysis.Pass) (interface{}, error) { for i, lhs := range stmt.Lhs { rhs := stmt.Rhs[i] if analysisutil.HasSideEffects(pass.TypesInfo, lhs) || - analysisutil.HasSideEffects(pass.TypesInfo, rhs) { + analysisutil.HasSideEffects(pass.TypesInfo, rhs) || + isMapIndex(pass.TypesInfo, lhs) { continue // expressions may not be equal } if reflect.TypeOf(lhs) != reflect.TypeOf(rhs) { @@ -74,3 +76,14 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } + +// isMapIndex returns true if e is a map index expression. +func isMapIndex(info *types.Info, e ast.Expr) bool { + if idx, ok := analysisutil.Unparen(e).(*ast.IndexExpr); ok { + if typ := info.Types[idx.X].Type; typ != nil { + _, ok := typ.Underlying().(*types.Map) + return ok + } + } + return false +} diff --git a/go/analysis/passes/assign/testdata/src/a/a.go b/go/analysis/passes/assign/testdata/src/a/a.go index eaec634d181..f9663120b4a 100644 --- a/go/analysis/passes/assign/testdata/src/a/a.go +++ b/go/analysis/passes/assign/testdata/src/a/a.go @@ -29,3 +29,31 @@ func (s *ST) SetX(x int, ch chan int) { } func num() int { return 2 } + +func Index() { + s := []int{1} + s[0] = s[0] // want "self-assignment" + + var a [5]int + a[0] = a[0] // want "self-assignment" + + pa := &[2]int{1, 2} + pa[1] = pa[1] // want "self-assignment" + + var pss *struct { // report self assignment despite nil dereference + s []int + } + pss.s[0] = pss.s[0] // want "self-assignment" + + m := map[int]string{1: "a"} + m[0] = m[0] // bail on map self-assignments due to side effects + m[1] = m[1] // not modeling what elements must be in the map + (m[2]) = (m[2]) // even with parens + type Map map[string]bool + named := make(Map) + named["s"] = named["s"] // even on named maps. + var psm *struct { + m map[string]int + } + psm.m["key"] = psm.m["key"] // handles dereferences +} diff --git a/go/analysis/passes/assign/testdata/src/a/a.go.golden b/go/analysis/passes/assign/testdata/src/a/a.go.golden index 6c91d3666cc..f45b7f208e2 100644 --- a/go/analysis/passes/assign/testdata/src/a/a.go.golden +++ b/go/analysis/passes/assign/testdata/src/a/a.go.golden @@ -29,3 +29,31 @@ func (s *ST) SetX(x int, ch chan int) { } func num() int { return 2 } + +func Index() { + s := []int{1} + // want "self-assignment" + + var a [5]int + // want "self-assignment" + + pa := &[2]int{1, 2} + // want "self-assignment" + + var pss *struct { // report self assignment despite nil dereference + s []int + } + // want "self-assignment" + + m := map[int]string{1: "a"} + m[0] = m[0] // bail on map self-assignments due to side effects + m[1] = m[1] // not modeling what elements must be in the map + (m[2]) = (m[2]) // even with parens + type Map map[string]bool + named := make(Map) + named["s"] = named["s"] // even on named maps. + var psm *struct { + m map[string]int + } + psm.m["key"] = psm.m["key"] // handles dereferences +} From 15525299aee7687cb1b12f5d9a48fc2718cbc4d2 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 5 Oct 2022 15:44:25 -0400 Subject: [PATCH 308/723] gopls/internal/regtest/misc: use vulntest for vuln_test Make the test run only with go1.18+. Change-Id: I8a2645929070bc1a63401ee942de1e88b5c083fa Reviewed-on: https://go-review.googlesource.com/c/tools/+/439275 Reviewed-by: Jamal Carvalho gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot --- gopls/internal/regtest/misc/vuln_test.go | 82 +++++++++++++++++++++--- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 5ba545454e7..e1b8c270620 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -2,18 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.18 +// +build go1.18 + package misc import ( - "os" - "path/filepath" + "context" "testing" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/vulncheck/vulntest" "golang.org/x/tools/internal/testenv" ) @@ -47,6 +49,62 @@ package foo }) } +const vulnsData = ` +-- GO-2022-01.yaml -- +modules: + - module: golang.org/amod + versions: + - introduced: 1.0.0 + - fixed: 1.0.4 + - introduced: 1.1.2 + packages: + - package: golang.org/amod/avuln + symbols: + - VulnData.Vuln1 + - VulnData.Vuln2 +description: > + vuln in amod +references: + - href: pkg.go.dev/vuln/GO-2022-01 +-- GO-2022-03.yaml -- +modules: + - module: golang.org/amod + versions: + - introduced: 1.0.0 + - fixed: 1.0.6 + packages: + - package: golang.org/amod/avuln + symbols: + - nonExisting +description: > + unaffecting vulnerability +-- GO-2022-02.yaml -- +modules: + - module: golang.org/bmod + packages: + - package: golang.org/bmod/bvuln + symbols: + - Vuln +description: | + vuln in bmod + + This is a long description + of this vulnerability. +references: + - href: pkg.go.dev/vuln/GO-2022-03 +-- STD.yaml -- +modules: + - module: stdlib + versions: + - introduced: 1.18.0 + packages: + - package: archive/zip + symbols: + - OpenReader +references: + - href: pkg.go.dev/vuln/STD +` + func TestRunVulncheckExpStd(t *testing.T) { testenv.NeedsGo1Point(t, 18) const files = ` @@ -68,12 +126,15 @@ func main() { } ` - cwd, _ := os.Getwd() - uri := span.URIFromPath(filepath.Join(cwd, "testdata", "vulndb")) + db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) + if err != nil { + t.Fatal(err) + } + defer db.Clean() WithOptions( EnvVars{ // Let the analyzer read vulnerabilities data from the testdata/vulndb. - "GOVULNDB": string(uri), + "GOVULNDB": db.URI(), // When fetchinging stdlib package vulnerability info, // behave as if our go version is go1.18 for this testing. // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). @@ -226,13 +287,16 @@ func Vuln() { func TestRunVulncheckExp(t *testing.T) { testenv.NeedsGo1Point(t, 18) - cwd, _ := os.Getwd() - uri := span.URIFromPath(filepath.Join(cwd, "testdata", "vulndb")) + db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) + if err != nil { + t.Fatal(err) + } + defer db.Clean() WithOptions( ProxyFiles(proxy1), EnvVars{ // Let the analyzer read vulnerabilities data from the testdata/vulndb. - "GOVULNDB": string(uri), + "GOVULNDB": db.URI(), // When fetching stdlib package vulnerability info, // behave as if our go version is go1.18 for this testing. // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). From c68255586f50c8e449bbb63d56219427cd5f8227 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 6 Oct 2022 13:38:22 -0400 Subject: [PATCH 309/723] gopls/internal/regtest/misc: delete testdata Change-Id: I45b2b80a81e8604765944e1d91c3e9f392b3e899 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439716 gopls-CI: kokoro Reviewed-by: Jamal Carvalho Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot --- .../misc/testdata/vulndb/golang.org/amod.json | 34 ------------------- .../misc/testdata/vulndb/golang.org/bmod.json | 18 ---------- .../testdata/vulndb/golang.org/x/crypto.json | 1 - .../testdata/vulndb/golang.org/x/text.json | 1 - .../regtest/misc/testdata/vulndb/stdlib.json | 1 - 5 files changed, 55 deletions(-) delete mode 100644 gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json delete mode 100644 gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json delete mode 100644 gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json delete mode 100644 gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json delete mode 100644 gopls/internal/regtest/misc/testdata/vulndb/stdlib.json diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json deleted file mode 100644 index 59808ed5fff..00000000000 --- a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/amod.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "id":"GO-2022-01", - "details": "vuln in amod", - "affected":[ - { - "package":{"name":"golang.org/amod","ecosystem":"Go"}, - "ranges":[{"type":"SEMVER","events":[{"introduced":"1.0.0"},{"fixed":"1.0.4"}, {"introduced": "1.1.2"}]}], - "ecosystem_specific":{ - "imports":[{"path":"golang.org/amod/avuln","symbols":["VulnData.Vuln1", "VulnData.Vuln2"]}] - } - } - ], - "references":[ - {"type":"href","url":"pkg.go.dev/vuln/GO-2022-01"} - ] - }, - { - "id":"GO-2022-03", - "details": "unaffecting vulnerability", - "affected":[ - { - "package":{"name":"golang.org/amod","ecosystem":"Go"}, - "ranges":[{"type":"SEMVER","events":[{"introduced":"1.0.0"},{"fixed":"1.0.6"}]}], - "ecosystem_specific":{ - "imports":[{"path":"golang.org/amod/avuln","symbols":["nonExisting"]}] - } - } - ], - "references":[ - {"type":"href","url":"pkg.go.dev/vuln/GO-2022-03"} - ] - } -] \ No newline at end of file diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json deleted file mode 100644 index 5e69cd9fdc6..00000000000 --- a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/bmod.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "id":"GO-2022-99", - "details": "vuln in bmod\n\nThis is a long description\nof this vulnerability.\n", - "affected":[ - { - "package":{"name":"golang.org/bmod","ecosystem":"Go"}, - "ranges":[{"type":"SEMVER"}], - "ecosystem_specific":{ - "imports":[{"path":"golang.org/bmod/bvuln","symbols":["Vuln"]}] - } - } - ], - "references":[ - {"type":"href","url":"pkg.go.dev/vuln/GO-2022-03"} - ] - } -] diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json deleted file mode 100644 index 7af60bb6fb5..00000000000 --- a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/crypto.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"GO-2020-0012","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2020-9283","GHSA-ffhg-7mh4-33c4"],"details":"An attacker can craft an ssh-ed25519 or sk-ssh-ed25519@openssh.com public\nkey, such that the library will panic when trying to verify a signature\nwith it. If verifying signatures using user supplied public keys, this\nmay be used as a denial of service vector.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20200220183623-bac4c82f6975"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0012"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/ssh","symbols":["NewPublicKey","ed25519PublicKey.Verify","parseED25519","parseSKEd25519","skEd25519PublicKey.Verify"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/220357"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/bac4c82f69751a6dd76e702d54b3ceb88adab236"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/3L45YRc91SY"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2020-9283"},{"type":"WEB","url":"https://github.com/advisories/GHSA-ffhg-7mh4-33c4"}]},{"id":"GO-2020-0013","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2017-3204"],"details":"By default host key verification is disabled which allows for\nman-in-the-middle attacks against SSH clients if\nClientConfig.HostKeyCallback is not set.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20170330155735-e4e2799dd7aa"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0013"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/ssh","symbols":["NewClientConn"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/340830"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/e4e2799dd7aab89f583e1d898300d96367750991"},{"type":"WEB","url":"https://go.dev/issue/19767"},{"type":"WEB","url":"https://bridge.grumpy-troll.org/2017/04/golang-ssh-security/"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2017-3204"}]},{"id":"GO-2021-0227","published":"2022-02-17T17:35:32Z","modified":"2022-02-17T17:35:32Z","aliases":["CVE-2020-29652"],"details":"Clients can cause a panic in SSH servers. An attacker can craft\nan authentication request message for the “gssapi-with-mic” method\nwhich will cause NewServerConn to panic via a nil pointer dereference\nif ServerConfig.GSSAPIWithMICConfig is nil.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20201216223049-8b5274cf687f"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0227"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/ssh","symbols":["connection.serverAuthenticate"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/278852"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/8b5274cf687fd9316b4108863654cc57385531e8"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/ouZIlBimOsE?pli=1"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2020-29652"}]},{"id":"GO-2021-0356","published":"2022-04-25T20:38:40Z","modified":"2022-08-18T20:22:13Z","aliases":["CVE-2022-27191","GHSA-8c26-wmh5-6g9v"],"details":"Attackers can cause a crash in SSH servers when the server has been\nconfigured by passing a Signer to ServerConfig.AddHostKey such that\n 1) the Signer passed to AddHostKey does not implement AlgorithmSigner, and\n 2) the Signer passed to AddHostKey returns a key of type “ssh-rsa” from its\n PublicKey method.\n\nServers that only use Signer implementations provided by the ssh package are\nunaffected.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20220314234659-1baeb1ce4c0b"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0356"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/ssh","symbols":["ServerConfig.AddHostKey","ServerConfig.AddHostKey"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/392355"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/1baeb1ce4c0b006eff0f294c47cb7617598dfb3d"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/-cp44ypCT5s"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2022-27191"},{"type":"WEB","url":"https://github.com/advisories/GHSA-8c26-wmh5-6g9v"}]},{"id":"GO-2022-0209","published":"2022-07-01T20:15:25Z","modified":"2022-08-18T20:22:13Z","aliases":["CVE-2019-11840"],"details":"XORKeyStream generates incorrect and insecure output for very\nlarge inputs.\n\nIf more than 256 GiB of keystream is generated, or if the counter\notherwise grows greater than 32 bits, the amd64 implementation will\nfirst generate incorrect output, and then cycle back to previously\ngenerated keystream. Repeated keystream bytes can lead to loss of\nconfidentiality in encryption applications, or to predictability\nin CSPRNG applications.\n\nThe issue might affect uses of golang.org/x/crypto/nacl with extremely\nlarge messages.\n\nArchitectures other than amd64 and uses that generate less than 256 GiB\nof keystream for a single salsa20.XORKeyStream invocation are unaffected.\n","affected":[{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20190320223903-b7391e95e576"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2022-0209"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/salsa20/salsa","goarch":["amd64"],"symbols":["XORKeyStream"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/168406"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/b7391e95e576cacdcdd422573063bc057239113d"},{"type":"WEB","url":"https://go.dev/issue/30965"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/tjyNcJxb2vQ/m/n0NRBziSCAAJ"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2019-11840"}]},{"id":"GO-2022-0229","published":"2022-07-06T18:23:48Z","modified":"2022-08-18T20:22:13Z","aliases":["CVE-2020-7919","GHSA-cjjc-xp8v-855w"],"details":"On 32-bit architectures, a malformed input to crypto/x509 or\nthe ASN.1 parsing functions of golang.org/x/crypto/cryptobyte\ncan lead to a panic.\n\nThe malformed certificate can be delivered via a crypto/tls\nconnection to a client, or to a server that accepts client\ncertificates. net/http clients can be made to crash by an HTTPS\nserver, while net/http servers that accept client certificates\nwill recover the panic and are unaffected.\n","affected":[{"package":{"name":"stdlib","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.12.16"},{"introduced":"1.13.0"},{"fixed":"1.13.7"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2022-0229"},"ecosystem_specific":{"imports":[{"path":"crypto/x509"}]}},{"package":{"name":"golang.org/x/crypto","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20200124225646-8b5121be2f68"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2022-0229"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/crypto/cryptobyte"}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/216680"},{"type":"FIX","url":"https://go.googlesource.com/go/+/b13ce14c4a6aa59b7b041ad2b6eed2d23e15b574"},{"type":"WEB","url":"https://go.dev/cl/216677"},{"type":"WEB","url":"https://go.dev/issue/36837"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/Hsw4mHYc470"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2020-7919"},{"type":"WEB","url":"https://github.com/advisories/GHSA-cjjc-xp8v-855w"}]}] \ No newline at end of file diff --git a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json b/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json deleted file mode 100644 index e9d55a39c9f..00000000000 --- a/gopls/internal/regtest/misc/testdata/vulndb/golang.org/x/text.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"GO-2020-0015","published":"2021-04-14T20:04:52Z","modified":"2021-06-07T12:00:00Z","aliases":["CVE-2020-14040","GHSA-5rcv-m4m3-hfh7"],"details":"An attacker could provide a single byte to a UTF16 decoder instantiated with\nUseBOM or ExpectBOM to trigger an infinite loop if the String function on\nthe Decoder is called, or the Decoder is passed to transform.String.\nIf used to parse user supplied input, this may be used as a denial of service\nvector.\n","affected":[{"package":{"name":"golang.org/x/text","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.3"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0015"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/text/encoding/unicode","symbols":["bomOverride.Transform","utf16Decoder.Transform"]},{"path":"golang.org/x/text/transform","symbols":["Transform"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/238238"},{"type":"FIX","url":"https://go.googlesource.com/text/+/23ae387dee1f90d29a23c0e87ee0b46038fbed0e"},{"type":"WEB","url":"https://go.dev/issue/39491"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/bXVeAmGOqz0"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2020-14040"},{"type":"WEB","url":"https://github.com/advisories/GHSA-5rcv-m4m3-hfh7"}]},{"id":"GO-2021-0113","published":"2021-10-06T17:51:21Z","modified":"2021-10-06T17:51:21Z","aliases":["CVE-2021-38561"],"details":"Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n","affected":[{"package":{"name":"golang.org/x/text","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.7"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0113"},"ecosystem_specific":{"imports":[{"path":"golang.org/x/text/language","symbols":["MatchStrings","MustParse","Parse","ParseAcceptLanguage"]}]}}],"references":[{"type":"FIX","url":"https://go.dev/cl/340830"},{"type":"FIX","url":"https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f"},{"type":"WEB","url":"https://nvd.nist.gov/vuln/detail/CVE-2021-38561"}]}] \ No newline at end of file diff --git a/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json b/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json deleted file mode 100644 index 756727b1a7d..00000000000 --- a/gopls/internal/regtest/misc/testdata/vulndb/stdlib.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"STD","affected":[{"package":{"name":"stdlib"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.18.0"}]}],"ecosystem_specific":{"imports":[{"path":"archive/zip","symbols":["OpenReader"]}]}}]}] From 778f9457c06f914027799e97a008de83beaa2943 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 6 Oct 2022 13:18:48 -0400 Subject: [PATCH 310/723] go/analysis: break dependency on span package The analysis driver uses a hack to reconstruct type errors (which contain a token.Pos) from packages.Errors (which contain file:line:col strings). This hack will be obviated by https://go.dev/cl/425095 but in the meantime it creates a dependency on the span package, which is really part of gopls. This change replaces the span.Parse function with simple calls to the standard library to extract the line and column. (Dependency aside, span.Parse was always overkill for this task, but perhaps packages.Error should have a File,Line,Col accessor.) Change-Id: I55108337f8a753523db68fe9e2aea15bb059cf15 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439755 gopls-CI: kokoro Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- go/analysis/internal/checker/checker.go | 46 ++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index c4eae378871..2972e44155a 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -26,6 +26,7 @@ import ( "runtime/pprof" "runtime/trace" "sort" + "strconv" "strings" "sync" "time" @@ -34,7 +35,6 @@ import ( "golang.org/x/tools/go/analysis/internal/analysisflags" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/span" ) var ( @@ -722,16 +722,52 @@ func (act *action) execOnce() { // Get any type errors that are attributed to the pkg. // This is necessary to test analyzers that provide // suggested fixes for compiler/type errors. + // TODO(adonovan): eliminate this hack; + // see https://github.com/golang/go/issues/54619. for _, err := range act.pkg.Errors { if err.Kind != packages.TypeError { continue } - // err.Pos is a string of form: "file:line:col" or "file:line" or "" or "-" - spn := span.Parse(err.Pos) + + // Parse err.Pos, a string of form: "file:line:col" or "file:line" or "" or "-" + // The filename may have a single ASCII letter Windows drive prefix such as "C:" + var file string + var line, col int + var convErr error + words := strings.Split(err.Pos, ":") + if runtime.GOOS == "windows" && + len(words) > 2 && + len(words[0]) == 1 && + ('A' <= words[0][0] && words[0][0] <= 'Z' || + 'a' <= words[0][0] && words[0][0] <= 'z') { + words[1] = words[0] + ":" + words[1] + words = words[1:] + } + switch len(words) { + case 2: + // file:line + file = words[0] + line, convErr = strconv.Atoi(words[1]) + case 3: + // file:line:col + file = words[0] + line, convErr = strconv.Atoi(words[1]) + if convErr == nil { + col, convErr = strconv.Atoi(words[2]) + } + default: + continue + } + if convErr != nil { + continue + } + // Extract the token positions from the error string. - line, col, offset := spn.Start().Line(), spn.Start().Column(), -1 + // (This is guesswork: Fset may contain all manner + // of stale files with the same name.) + offset := -1 act.pkg.Fset.Iterate(func(f *token.File) bool { - if f.Name() != spn.URI().Filename() { + if f.Name() != file { return true } offset = int(f.LineStart(line)) + col - 1 From 02bef08ac854e6831ece3f9c722e5084f0cc6ec1 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 6 Oct 2022 11:23:01 -0400 Subject: [PATCH 311/723] go/packages/packagestest: break dependency on gopls' span package The span package is properly part of gopls, and we'd like to move it into that module. However, the packagestest package unfortunately depends on span.Span as an alternative notation for Ranges. This change decouples span.Span from packagestest.Range using a new (unexported for now) rangeSetter interface, which Span implements. Neither package depends on the other. Technically this is a breaking API change: all the Range methods have gone away, as have the Span, URI, and Point types and their methods, which were accessible via Range.Span(). However, clients would not be able to access these internal types, and I think it is highly unlikely that anyone depends on it. Nonethless this is a cautionary tale about the risks from one innocuous-looking type alias declaration. Change-Id: I8acb03f4acb1f798f304b03648445e37a44f9c45 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439715 gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- go/packages/packagestest/expect.go | 105 ++++++++++++++++-------- go/packages/packagestest/expect_test.go | 3 +- go/packages/packagestest/export.go | 3 +- internal/span/span.go | 10 +++ 4 files changed, 85 insertions(+), 36 deletions(-) diff --git a/go/packages/packagestest/expect.go b/go/packages/packagestest/expect.go index 841099c0cdc..92c20a64a8d 100644 --- a/go/packages/packagestest/expect.go +++ b/go/packages/packagestest/expect.go @@ -16,7 +16,6 @@ import ( "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/span" ) const ( @@ -124,14 +123,31 @@ func (e *Exported) Expect(methods map[string]interface{}) error { return nil } -// Range is a type alias for span.Range for backwards compatibility, prefer -// using span.Range directly. -type Range = span.Range +// A Range represents an interval within a source file in go/token notation. +type Range struct { + TokFile *token.File // non-nil + Start, End token.Pos // both valid and within range of TokFile +} + +// A rangeSetter abstracts a variable that can be set from a Range value. +// +// The parameter conversion machinery will automatically construct a +// variable of type T and call the SetRange method on its address if +// *T implements rangeSetter. This allows alternative notations of +// source ranges to interoperate transparently with this package. +// +// This type intentionally does not mention Range itself, to avoid a +// dependency from the application's range type upon this package. +// +// Currently this is a secret back door for use only by gopls. +type rangeSetter interface { + SetRange(file *token.File, start, end token.Pos) +} // Mark adds a new marker to the known set. func (e *Exported) Mark(name string, r Range) { if e.markers == nil { - e.markers = make(map[string]span.Range) + e.markers = make(map[string]Range) } e.markers[name] = r } @@ -221,22 +237,22 @@ func (e *Exported) getMarkers() error { return nil } // set markers early so that we don't call getMarkers again from Expect - e.markers = make(map[string]span.Range) + e.markers = make(map[string]Range) return e.Expect(map[string]interface{}{ markMethod: e.Mark, }) } var ( - noteType = reflect.TypeOf((*expect.Note)(nil)) - identifierType = reflect.TypeOf(expect.Identifier("")) - posType = reflect.TypeOf(token.Pos(0)) - positionType = reflect.TypeOf(token.Position{}) - rangeType = reflect.TypeOf(span.Range{}) - spanType = reflect.TypeOf(span.Span{}) - fsetType = reflect.TypeOf((*token.FileSet)(nil)) - regexType = reflect.TypeOf((*regexp.Regexp)(nil)) - exportedType = reflect.TypeOf((*Exported)(nil)) + noteType = reflect.TypeOf((*expect.Note)(nil)) + identifierType = reflect.TypeOf(expect.Identifier("")) + posType = reflect.TypeOf(token.Pos(0)) + positionType = reflect.TypeOf(token.Position{}) + rangeType = reflect.TypeOf(Range{}) + rangeSetterType = reflect.TypeOf((*rangeSetter)(nil)).Elem() + fsetType = reflect.TypeOf((*token.FileSet)(nil)) + regexType = reflect.TypeOf((*regexp.Regexp)(nil)) + exportedType = reflect.TypeOf((*Exported)(nil)) ) // converter converts from a marker's argument parsed from the comment to @@ -295,17 +311,16 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } return reflect.ValueOf(r), remains, nil }, nil - case pt == spanType: + case reflect.PtrTo(pt).AssignableTo(rangeSetterType): + // (*pt).SetRange method exists: call it. return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err } - spn, err := r.Span() - if err != nil { - return reflect.Value{}, nil, err - } - return reflect.ValueOf(spn), remains, nil + v := reflect.New(pt) + v.Interface().(rangeSetter).SetRange(r.TokFile, r.Start, r.End) + return v.Elem(), remains, nil }, nil case pt == identifierType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { @@ -408,10 +423,10 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } } -func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (span.Range, []interface{}, error) { +func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (Range, []interface{}, error) { tokFile := e.ExpectFileSet.File(n.Pos) if len(args) < 1 { - return span.Range{}, nil, fmt.Errorf("missing argument") + return Range{}, nil, fmt.Errorf("missing argument") } arg := args[0] args = args[1:] @@ -422,34 +437,60 @@ func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (span.Rang case eofIdentifier: // end of file identifier eof := tokFile.Pos(tokFile.Size()) - return span.NewRange(tokFile, eof, eof), args, nil + return newRange(tokFile, eof, eof), args, nil default: // look up an marker by name mark, ok := e.markers[string(arg)] if !ok { - return span.Range{}, nil, fmt.Errorf("cannot find marker %v", arg) + return Range{}, nil, fmt.Errorf("cannot find marker %v", arg) } return mark, args, nil } case string: start, end, err := expect.MatchBefore(e.ExpectFileSet, e.FileContents, n.Pos, arg) if err != nil { - return span.Range{}, nil, err + return Range{}, nil, err } if !start.IsValid() { - return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) + return Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) } - return span.NewRange(tokFile, start, end), args, nil + return newRange(tokFile, start, end), args, nil case *regexp.Regexp: start, end, err := expect.MatchBefore(e.ExpectFileSet, e.FileContents, n.Pos, arg) if err != nil { - return span.Range{}, nil, err + return Range{}, nil, err } if !start.IsValid() { - return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) + return Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) } - return span.NewRange(tokFile, start, end), args, nil + return newRange(tokFile, start, end), args, nil default: - return span.Range{}, nil, fmt.Errorf("cannot convert %v to pos", arg) + return Range{}, nil, fmt.Errorf("cannot convert %v to pos", arg) + } +} + +// newRange creates a new Range from a token.File and two valid positions within it. +func newRange(file *token.File, start, end token.Pos) Range { + fileBase := file.Base() + fileEnd := fileBase + file.Size() + if !start.IsValid() { + panic("invalid start token.Pos") + } + if !end.IsValid() { + panic("invalid end token.Pos") + } + if int(start) < fileBase || int(start) > fileEnd { + panic(fmt.Sprintf("invalid start: %d not in [%d, %d]", start, fileBase, fileEnd)) + } + if int(end) < fileBase || int(end) > fileEnd { + panic(fmt.Sprintf("invalid end: %d not in [%d, %d]", end, fileBase, fileEnd)) + } + if start > end { + panic("invalid start: greater than end") + } + return Range{ + TokFile: file, + Start: start, + End: end, } } diff --git a/go/packages/packagestest/expect_test.go b/go/packages/packagestest/expect_test.go index 2587f580b06..46d96d61fb9 100644 --- a/go/packages/packagestest/expect_test.go +++ b/go/packages/packagestest/expect_test.go @@ -10,7 +10,6 @@ import ( "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages/packagestest" - "golang.org/x/tools/internal/span" ) func TestExpect(t *testing.T) { @@ -43,7 +42,7 @@ func TestExpect(t *testing.T) { } }, "directNote": func(n *expect.Note) {}, - "range": func(r span.Range) { + "range": func(r packagestest.Range) { if r.Start == token.NoPos || r.Start == 0 { t.Errorf("Range had no valid starting position") } diff --git a/go/packages/packagestest/export.go b/go/packages/packagestest/export.go index 894dcdd445d..b687a44fb4f 100644 --- a/go/packages/packagestest/export.go +++ b/go/packages/packagestest/export.go @@ -79,7 +79,6 @@ import ( "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -129,7 +128,7 @@ type Exported struct { primary string // the first non GOROOT module that was exported written map[string]map[string]string // the full set of exported files notes []*expect.Note // The list of expectations extracted from go source files - markers map[string]span.Range // The set of markers extracted from go source files + markers map[string]Range // The set of markers extracted from go source files } // Exporter implementations are responsible for converting from the generic description of some diff --git a/internal/span/span.go b/internal/span/span.go index a714cca8447..5b8d31ef03c 100644 --- a/internal/span/span.go +++ b/internal/span/span.go @@ -275,3 +275,13 @@ func (p *point) updateOffset(tf *token.File) error { p.Offset = offset return nil } + +// SetRange implements packagestest.rangeSetter, allowing +// gopls' test suites to use Spans instead of Range in parameters. +func (span *Span) SetRange(file *token.File, start, end token.Pos) { + point := func(pos token.Pos) Point { + posn := file.Position(pos) + return NewPoint(posn.Line, posn.Column, posn.Offset) + } + *span = New(URIFromPath(file.Name()), point(start), point(end)) +} From 60ddccae8574e69e18ab4c19979ee15bfdf95af6 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 5 Oct 2022 18:58:00 -0400 Subject: [PATCH 312/723] internal/diff: Apply: validate inputs Apply now checks that its edits are valid (not out of bounds or overlapping), and reports an error if not. It also sorts them, if necessary, using (start, end) as the key, to ensure that insertions (end=start) are ordered before deletions at the same point (but without changing the relative order of insertions). Two other implementations of the diff.Apply algorithm have been eliminated. (One of them failed to sort edits, requiring the protocol sender to do so; that burden is now gone.) Change-Id: Ia76e485e6869db4a165835c3312fd14bc7d43db2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439278 Auto-Submit: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Alan Donovan --- go/analysis/analysistest/analysistest.go | 12 ++- gopls/internal/hooks/diff.go | 4 +- gopls/internal/lsp/cmd/format.go | 3 +- gopls/internal/lsp/cmd/imports.go | 4 +- gopls/internal/lsp/cmd/rename.go | 4 +- gopls/internal/lsp/cmd/suggested_fix.go | 3 +- gopls/internal/lsp/fake/edit.go | 115 +++++++++-------------- gopls/internal/lsp/fake/edit_test.go | 4 +- gopls/internal/lsp/fake/editor.go | 4 +- gopls/internal/lsp/lsp_test.go | 24 +---- gopls/internal/lsp/source/format.go | 15 +++ gopls/internal/lsp/source/rename.go | 1 - gopls/internal/lsp/source/source_test.go | 34 +------ internal/diff/diff.go | 94 +++++++++++------- internal/diff/diff_test.go | 42 +++++++-- internal/diff/difftest/difftest.go | 5 +- 16 files changed, 184 insertions(+), 184 deletions(-) diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index bc25b9f2b78..14140be14b0 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -183,7 +183,11 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns for _, vf := range ar.Files { if vf.Name == sf { found = true - out := diff.Apply(string(orig), edits) + out, err := diff.Apply(string(orig), edits) + if err != nil { + t.Errorf("%s: error applying fixes: %v", file.Name(), err) + continue + } // the file may contain multiple trailing // newlines if the user places empty lines // between files in the archive. normalize @@ -213,7 +217,11 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns catchallEdits = append(catchallEdits, edits...) } - out := diff.Apply(string(orig), catchallEdits) + out, err := diff.Apply(string(orig), catchallEdits) + if err != nil { + t.Errorf("%s: error applying fixes: %v", file.Name(), err) + continue + } want := string(ar.Comment) formatted, err := format.Source([]byte(out)) diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go index 3aa1f0b7806..cac136ae192 100644 --- a/gopls/internal/hooks/diff.go +++ b/gopls/internal/hooks/diff.go @@ -162,8 +162,8 @@ func BothDiffs(before, after string) (edits []diff.Edit) { newedits := diff.Strings(before, after) stat.Newedits = len(newedits) stat.Newtime = time.Now().Sub(now) - got := diff.Apply(before, newedits) - if got != after { + got, err := diff.Apply(before, newedits) + if err != nil || got != after { stat.Msg += "FAIL" disaster(before, after) stat.save() diff --git a/gopls/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go index 17e3e6a5011..56e39053c10 100644 --- a/gopls/internal/lsp/cmd/format.go +++ b/gopls/internal/lsp/cmd/format.go @@ -76,11 +76,10 @@ func (c *format) Run(ctx context.Context, args ...string) error { if err != nil { return fmt.Errorf("%v: %v", spn, err) } - sedits, err := source.FromProtocolEdits(file.mapper, edits) + formatted, sedits, err := source.ApplyProtocolEdits(file.mapper, edits) if err != nil { return fmt.Errorf("%v: %v", spn, err) } - formatted := diff.Apply(string(file.mapper.Content), sedits) printIt := true if c.List { printIt = false diff --git a/gopls/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go index 7fd548381fe..4ee6fce4c96 100644 --- a/gopls/internal/lsp/cmd/imports.go +++ b/gopls/internal/lsp/cmd/imports.go @@ -81,12 +81,10 @@ func (t *imports) Run(ctx context.Context, args ...string) error { } } } - sedits, err := source.FromProtocolEdits(file.mapper, edits) + newContent, sedits, err := source.ApplyProtocolEdits(file.mapper, edits) if err != nil { return fmt.Errorf("%v: %v", edits, err) } - newContent := diff.Apply(string(file.mapper.Content), sedits) - filename := file.uri.Filename() switch { case t.Write: diff --git a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go index e0ffa663bfe..58fb07d7f15 100644 --- a/gopls/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -95,12 +95,10 @@ func (r *rename) Run(ctx context.Context, args ...string) error { cmdFile := conn.AddFile(ctx, uri) filename := cmdFile.uri.Filename() - // convert LSP-style edits to []diff.TextEdit cuz Spans are handy - renameEdits, err := source.FromProtocolEdits(cmdFile.mapper, edits[uri]) + newContent, renameEdits, err := source.ApplyProtocolEdits(cmdFile.mapper, edits[uri]) if err != nil { return fmt.Errorf("%v: %v", edits, err) } - newContent := diff.Apply(string(cmdFile.mapper.Content), renameEdits) switch { case r.Write: diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go index b6022a7ee55..9c103deff8a 100644 --- a/gopls/internal/lsp/cmd/suggested_fix.go +++ b/gopls/internal/lsp/cmd/suggested_fix.go @@ -142,11 +142,10 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { } } - sedits, err := source.FromProtocolEdits(file.mapper, edits) + newContent, sedits, err := source.ApplyProtocolEdits(file.mapper, edits) if err != nil { return fmt.Errorf("%v: %v", edits, err) } - newContent := diff.Apply(string(file.mapper.Content), sedits) filename := file.uri.Filename() switch { diff --git a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go index bb5fb80900b..3eb13ea2f4c 100644 --- a/gopls/internal/lsp/fake/edit.go +++ b/gopls/internal/lsp/fake/edit.go @@ -6,14 +6,16 @@ package fake import ( "fmt" - "sort" "strings" + "unicode/utf8" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/diff" ) -// Pos represents a position in a text buffer. Both Line and Column are -// 0-indexed. +// Pos represents a position in a text buffer. +// Both Line and Column are 0-indexed. +// Column counts runes. type Pos struct { Line, Column int } @@ -105,78 +107,51 @@ func inText(p Pos, content []string) bool { return true } -// editContent implements a simplistic, inefficient algorithm for applying text -// edits to our buffer representation. It returns an error if the edit is -// invalid for the current content. -// -// TODO(rfindley): this function does not handle non-ascii text correctly. -// TODO(rfindley): replace this with diff.Apply: we should not be -// maintaining an additional representation of edits. -func editContent(content []string, edits []Edit) ([]string, error) { - newEdits := make([]Edit, len(edits)) - copy(newEdits, edits) - sort.SliceStable(newEdits, func(i, j int) bool { - ei := newEdits[i] - ej := newEdits[j] - - // Sort by edit start position followed by end position. Given an edit - // 3:1-3:1 followed by an edit 3:1-3:15, we must process the empty edit - // first. - if cmp := comparePos(ei.Start, ej.Start); cmp != 0 { - return cmp < 0 - } - - return comparePos(ei.End, ej.End) < 0 - }) - - // Validate edits. - for _, edit := range newEdits { - if edit.End.Line < edit.Start.Line || (edit.End.Line == edit.Start.Line && edit.End.Column < edit.Start.Column) { - return nil, fmt.Errorf("invalid edit: end %v before start %v", edit.End, edit.Start) - } - if !inText(edit.Start, content) { - return nil, fmt.Errorf("start position %v is out of bounds", edit.Start) - } - if !inText(edit.End, content) { - return nil, fmt.Errorf("end position %v is out of bounds", edit.End) +// applyEdits applies the edits to a file with the specified lines, +// and returns a new slice containing the lines of the patched file. +// It is a wrapper around diff.Apply; see that function for preconditions. +func applyEdits(lines []string, edits []Edit) ([]string, error) { + src := strings.Join(lines, "\n") + + // Build a table of byte offset of start of each line. + lineOffset := make([]int, len(lines)+1) + offset := 0 + for i, line := range lines { + lineOffset[i] = offset + offset += len(line) + len("\n") + } + lineOffset[len(lines)] = offset // EOF + + var badCol error + posToOffset := func(pos Pos) int { + offset := lineOffset[pos.Line] + // Convert pos.Column (runes) to a UTF-8 byte offset. + if pos.Line < len(lines) { + for i := 0; i < pos.Column; i++ { + r, sz := utf8.DecodeRuneInString(src[offset:]) + if r == '\n' && badCol == nil { + badCol = fmt.Errorf("bad column") + } + offset += sz + } } + return offset } - var ( - b strings.Builder - line, column int - ) - advance := func(toLine, toColumn int) { - for ; line < toLine; line++ { - b.WriteString(string([]rune(content[line])[column:]) + "\n") - column = 0 + // Convert fake.Edits to diff.Edits + diffEdits := make([]diff.Edit, len(edits)) + for i, edit := range edits { + diffEdits[i] = diff.Edit{ + Start: posToOffset(edit.Start), + End: posToOffset(edit.End), + New: edit.Text, } - b.WriteString(string([]rune(content[line])[column:toColumn])) - column = toColumn } - for _, edit := range newEdits { - advance(edit.Start.Line, edit.Start.Column) - b.WriteString(edit.Text) - line = edit.End.Line - column = edit.End.Column - } - advance(len(content)-1, len([]rune(content[len(content)-1]))) - return strings.Split(b.String(), "\n"), nil -} -// comparePos returns -1 if left < right, 0 if left == right, and 1 if left > right. -func comparePos(left, right Pos) int { - if left.Line < right.Line { - return -1 - } - if left.Line > right.Line { - return 1 + patched, err := diff.Apply(src, diffEdits) + if err != nil { + return nil, err } - if left.Column < right.Column { - return -1 - } - if left.Column > right.Column { - return 1 - } - return 0 + + return strings.Split(patched, "\n"), badCol } diff --git a/gopls/internal/lsp/fake/edit_test.go b/gopls/internal/lsp/fake/edit_test.go index 4fa23bdb74a..f87d9210336 100644 --- a/gopls/internal/lsp/fake/edit_test.go +++ b/gopls/internal/lsp/fake/edit_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -func TestApplyEdit(t *testing.T) { +func TestApplyEdits(t *testing.T) { tests := []struct { label string content string @@ -82,7 +82,7 @@ func TestApplyEdit(t *testing.T) { test := test t.Run(test.label, func(t *testing.T) { lines := strings.Split(test.content, "\n") - newLines, err := editContent(lines, test.edits) + newLines, err := applyEdits(lines, test.edits) if (err != nil) != test.wantErr { t.Errorf("got err %v, want error: %t", err, test.wantErr) } diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 17bec286b7d..e65db938ad7 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -710,9 +710,7 @@ func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit if !ok { return fmt.Errorf("unknown buffer %q", path) } - content := make([]string, len(buf.lines)) - copy(content, buf.lines) - content, err := editContent(content, edits) + content, err := applyEdits(buf.lines, edits) if err != nil { return err } diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 78032f04368..4cd009d2f0f 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -24,7 +24,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" @@ -409,11 +408,10 @@ func (r *runner) Format(t *testing.T, spn span.Span) { if err != nil { t.Fatal(err) } - sedits, err := source.FromProtocolEdits(m, edits) + got, _, err := source.ApplyProtocolEdits(m, edits) if err != nil { t.Error(err) } - got := diff.Apply(string(m.Content), sedits) if diff := compare.Text(gofmted, got); diff != "" { t.Errorf("format failed for %s (-want +got):\n%s", filename, diff) } @@ -975,11 +973,10 @@ func (r *runner) InlayHints(t *testing.T, spn span.Span) { if err != nil { t.Fatal(err) } - sedits, err := source.FromProtocolEdits(m, edits) + got, _, err := source.ApplyProtocolEdits(m, edits) if err != nil { t.Error(err) } - got := diff.Apply(string(m.Content), sedits) withinlayHints := string(r.data.Golden(t, "inlayHint", filename, func() ([]byte, error) { return []byte(got), nil @@ -1115,29 +1112,16 @@ func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[sp return nil, err } } - res[uri] = string(m.Content) - sedits, err := source.FromProtocolEdits(m, docEdits.TextDocumentEdit.Edits) + patched, _, err := source.ApplyProtocolEdits(m, docEdits.TextDocumentEdit.Edits) if err != nil { return nil, err } - res[uri] = applyEdits(res[uri], sedits) + res[uri] = patched } } return res, nil } -func applyEdits(contents string, edits []diff.Edit) string { - res := contents - - // Apply the edits from the end of the file forward - // to preserve the offsets - for i := len(edits) - 1; i >= 0; i-- { - edit := edits[i] - res = res[:edit.Start] + edit.New + res[edit.End:] - } - return res -} - func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { params := &protocol.DocumentSymbolParams{ TextDocument: protocol.TextDocumentIdentifier{ diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index dc7445a025c..c6e70efce6f 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -337,6 +337,8 @@ func protocolEditsFromSource(src []byte, edits []diff.Edit, tf *token.File) ([]p return result, nil } +// ToProtocolEdits converts diff.Edits to LSP TextEdits. +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.Edit) ([]protocol.TextEdit, error) { result := make([]protocol.TextEdit, len(edits)) for i, edit := range edits { @@ -352,6 +354,8 @@ func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.Edit) ([]protocol.Te return result, nil } +// ToProtocolEdits converts LSP TextEdits to diff.Edits. +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.Edit, error) { if edits == nil { return nil, nil @@ -370,3 +374,14 @@ func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]d } return result, nil } + +// ApplyProtocolEdits applies the patch (edits) to m.Content and returns the result. +// It also returns the edits converted to diff-package form. +func ApplyProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) (string, []diff.Edit, error) { + diffEdits, err := FromProtocolEdits(m, edits) + if err != nil { + return "", nil, err + } + out, err := diff.Apply(string(m.Content), diffEdits) + return out, diffEdits, err +} diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 842b1a9850b..c5af1ca896e 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -446,7 +446,6 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO return nil, err } m := protocol.NewColumnMapper(uri, data) - diff.SortEdits(edits) protocolEdits, err := ToProtocolEdits(m, edits) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 0a4e70cb6f5..d81bdb7aead 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -22,7 +22,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" @@ -477,19 +476,14 @@ func (r *runner) Format(t *testing.T, spn span.Span) { } return } - data, err := fh.Read() - if err != nil { - t.Fatal(err) - } m, err := r.data.Mapper(spn.URI()) if err != nil { t.Fatal(err) } - diffEdits, err := source.FromProtocolEdits(m, edits) + got, _, err := source.ApplyProtocolEdits(m, edits) if err != nil { t.Error(err) } - got := diff.Apply(string(data), diffEdits) if gofmted != got { t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", spn.URI().Filename(), gofmted, got) } @@ -508,19 +502,14 @@ func (r *runner) Import(t *testing.T, spn span.Span) { if err != nil { t.Error(err) } - data, err := fh.Read() - if err != nil { - t.Fatal(err) - } m, err := r.data.Mapper(fh.URI()) if err != nil { t.Fatal(err) } - diffEdits, err := source.FromProtocolEdits(m, edits) + got, _, err := source.ApplyProtocolEdits(m, edits) if err != nil { t.Error(err) } - got := diff.Apply(string(data), diffEdits) want := string(r.data.Golden(t, "goimports", spn.URI().Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -781,19 +770,14 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { if err != nil { t.Fatal(err) } - data, err := fh.Read() - if err != nil { - t.Fatal(err) - } m, err := r.data.Mapper(fh.URI()) if err != nil { t.Fatal(err) } - diffEdits, err := source.FromProtocolEdits(m, edits) + contents, _, err := source.ApplyProtocolEdits(m, edits) if err != nil { t.Fatal(err) } - contents := applyEdits(string(data), diffEdits) if len(changes) > 1 { filename := filepath.Base(editURI.Filename()) contents = fmt.Sprintf("%s:\n%s", filename, contents) @@ -821,18 +805,6 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { } } -func applyEdits(contents string, edits []diff.Edit) string { - res := contents - - // Apply the edits from the end of the file forward - // to preserve the offsets - for i := len(edits) - 1; i >= 0; i-- { - edit := edits[i] - res = res[:edit.Start] + edit.New + res[edit.End:] - } - return res -} - func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) { // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go } diff --git a/internal/diff/diff.go b/internal/diff/diff.go index e7f8469d13d..a75026dcaa1 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -6,6 +6,7 @@ package diff import ( + "fmt" "sort" "strings" ) @@ -22,45 +23,70 @@ type Edit struct { New string // the replacement } -// SortEdits orders edits by their start offset. The sort is stable -// so that edits with the same start offset will not be reordered. -func SortEdits(edits []Edit) { - sort.SliceStable(edits, func(i int, j int) bool { - return edits[i].Start < edits[j].Start - }) -} - -// Apply applies a sequence of edits to the src buffer and -// returns the result. It may panic or produce garbage if the edits -// are overlapping, out of bounds of src, or out of order. +// Apply applies a sequence of edits to the src buffer and returns the +// result. Edits are applied in order of start offset; edits with the +// same start offset are applied in they order they were provided. // -// TODO(adonovan): this function must not panic if the edits aren't -// consistent with src, or with each other---especially when fed -// information from an untrusted source. It should probably be -// defensive against bad input and report an error in any of the above -// situations. -func Apply(src string, edits []Edit) string { - SortEdits(edits) // TODO(adonovan): move to caller? What's the contract? Don't mutate arguments. - - var out strings.Builder - // TODO(adonovan): opt: preallocate correct final size - // by scanning the list of edits. (This can be done - // in the same pass as detecting inconsistent edits.) - last := 0 +// Apply returns an error if any edit is out of bounds, +// or if any pair of edits is overlapping. +func Apply(src string, edits []Edit) (string, error) { + if !sort.IsSorted(editsSort(edits)) { + edits = append([]Edit(nil), edits...) + sortEdits(edits) + } + + // Check validity of edits and compute final size. + size := len(src) + lastEnd := 0 + for _, edit := range edits { + if !(0 <= edit.Start && edit.Start <= edit.End && edit.End <= len(src)) { + return "", fmt.Errorf("diff has out-of-bounds edits") + } + if edit.Start < lastEnd { + return "", fmt.Errorf("diff has overlapping edits") + } + size += len(edit.New) + edit.Start - edit.End + lastEnd = edit.End + } + + // Apply edits. + out := make([]byte, 0, size) + lastEnd = 0 for _, edit := range edits { - start := edit.Start - if start > last { - out.WriteString(src[last:start]) - last = start + if lastEnd < edit.Start { + out = append(out, src[lastEnd:edit.Start]...) } - out.WriteString(edit.New) - last = edit.End + out = append(out, edit.New...) + lastEnd = edit.End + } + out = append(out, src[lastEnd:]...) + + if len(out) != size { + panic("wrong size") } - if last < len(src) { - out.WriteString(src[last:]) + + return string(out), nil +} + +// sortEdits orders edits by (start, end) offset. +// This ordering puts insertions (end=start) before deletions +// (end>start) at the same point, but uses a stable sort to preserve +// the order of multiple insertions at the same point. +// (Apply detects multiple deletions at the same point as an error.) +func sortEdits(edits editsSort) { + sort.Stable(edits) +} + +type editsSort []Edit + +func (a editsSort) Len() int { return len(a) } +func (a editsSort) Less(i, j int) bool { + if cmp := a[i].Start - a[j].Start; cmp != 0 { + return cmp < 0 } - return out.String() + return a[i].End < a[j].End } +func (a editsSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } // LineEdits expands and merges a sequence of edits so that each // resulting edit replaces one or more complete lines. @@ -71,7 +97,7 @@ func Apply(src string, edits []Edit) string { // We could hide this from the API so that we can enforce // the precondition... but it seems like a reasonable feature. func LineEdits(src string, edits []Edit) []Edit { - SortEdits(edits) // TODO(adonovan): is this necessary? Move burden to caller? + sortEdits(edits) // TODO(adonovan): is this necessary? Move burden to caller? // Do all edits begin and end at the start of a line? // TODO(adonovan): opt: is this fast path necessary? diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index 8b8b6489e06..d4c3746184d 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -18,11 +18,19 @@ import ( func TestApply(t *testing.T) { for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { - if got := diff.Apply(tc.In, tc.Edits); got != tc.Out { + got, err := diff.Apply(tc.In, tc.Edits) + if err != nil { + t.Fatalf("Apply(Edits) failed: %v", err) + } + if got != tc.Out { t.Errorf("Apply(Edits): got %q, want %q", got, tc.Out) } if tc.LineEdits != nil { - if got := diff.Apply(tc.In, tc.LineEdits); got != tc.Out { + got, err := diff.Apply(tc.In, tc.LineEdits) + if err != nil { + t.Fatalf("Apply(LineEdits) failed: %v", err) + } + if got != tc.Out { t.Errorf("Apply(LineEdits): got %q, want %q", got, tc.Out) } } @@ -33,7 +41,10 @@ func TestApply(t *testing.T) { func TestNEdits(t *testing.T) { for _, tc := range difftest.TestCases { edits := diff.Strings(tc.In, tc.Out) - got := diff.Apply(tc.In, edits) + got, err := diff.Apply(tc.In, edits) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } if got != tc.Out { t.Fatalf("%s: got %q wanted %q", tc.Name, got, tc.Out) } @@ -49,7 +60,10 @@ func TestNRandom(t *testing.T) { a := randstr("abω", 16) b := randstr("abωc", 16) edits := diff.Strings(a, b) - got := diff.Apply(a, edits) + got, err := diff.Apply(a, edits) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } if got != b { t.Fatalf("%d: got %q, wanted %q, starting with %q", i, got, b, a) } @@ -63,7 +77,10 @@ func FuzzRoundTrip(f *testing.F) { return // inputs must be text } edits := diff.Strings(a, b) - got := diff.Apply(a, edits) + got, err := diff.Apply(a, edits) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } if got != b { t.Fatalf("applying diff(%q, %q) gives %q; edits=%v", a, b, got, edits) } @@ -90,7 +107,10 @@ func TestNLinesRandom(t *testing.T) { } a, b := strings.SplitAfter(x, "\n"), strings.SplitAfter(y, "\n") edits := diff.Lines(a, b) - got := diff.Apply(x, edits) + got, err := diff.Apply(x, edits) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } if got != y { t.Fatalf("%d: got\n%q, wanted\n%q, starting with %q", i, got, y, a) } @@ -134,7 +154,10 @@ func TestRegressionOld001(t *testing.T) { b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" diffs := diff.Strings(a, b) - got := diff.Apply(a, diffs) + got, err := diff.Apply(a, diffs) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } if got != b { i := 0 for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ { @@ -148,7 +171,10 @@ func TestRegressionOld002(t *testing.T) { a := "n\"\n)\n" b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n" diffs := diff.Strings(a, b) - got := diff.Apply(a, diffs) + got, err := diff.Apply(a, diffs) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } if got != b { i := 0 for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ { diff --git a/internal/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go index 998a90f109f..5c0a7413670 100644 --- a/internal/diff/difftest/difftest.go +++ b/internal/diff/difftest/difftest.go @@ -245,7 +245,10 @@ func DiffTest(t *testing.T, compute func(before, after string) []diff.Edit) { for _, test := range TestCases { t.Run(test.Name, func(t *testing.T) { edits := compute(test.In, test.Out) - got := diff.Apply(test.In, edits) + got, err := diff.Apply(test.In, edits) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } unified := diff.Unified(FileA, FileB, test.In, edits) if got != test.Out { t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out) From f2c4579afe5fead8846586e49d2cd2ad63518191 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 6 Oct 2022 16:11:42 -0400 Subject: [PATCH 313/723] internal/diff: avoid unnecessary allocations Previously, a diff of non-ASCII strings would incur three complete copies of the buffers; this changes reduces it to one. Also: - add diff.Bytes function, to avoid unnecessary conversions. - remove unused diff.Lines and LineEdits functions from API. - remove TODO to use []bytes everywhere. We tried it in CL 439277 and didn't like it. - Document that the diff is textual, even when given []byte. Change-Id: I2da3257cc3d12c569218a2d7ce182452e8647a96 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439835 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/cache/parse.go | 2 +- internal/diff/diff.go | 20 +---- internal/diff/diff_test.go | 30 ------- internal/diff/export_test.go | 9 ++ internal/diff/ndiff.go | 143 ++++++++++++++---------------- internal/diff/unified.go | 4 +- 6 files changed, 83 insertions(+), 125 deletions(-) create mode 100644 internal/diff/export_test.go diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index ec3e563cf0d..1a096c42bbf 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -157,7 +157,7 @@ func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, // it is likely we got stuck in a loop somehow. Log out a diff // of the last changes we made to aid in debugging. if i == 9 { - edits := diff.Strings(string(src), string(newSrc)) + edits := diff.Bytes(src, newSrc) unified := diff.Unified("before", "after", string(src), edits) event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name())) } diff --git a/internal/diff/diff.go b/internal/diff/diff.go index a75026dcaa1..6cb39da73e2 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.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 diff computes differences between files or strings. +// Package diff computes differences between text files or strings. package diff import ( @@ -11,13 +11,7 @@ import ( "strings" ) -// TODO(adonovan): switch to []byte throughout. -// But make clear that the operation is defined on runes, not bytes. -// Also: -// - delete LineEdits? (used only by Unified and test) -// - delete Lines (unused except by its test) - -// An Edit describes the replacement of a portion of a file. +// An Edit describes the replacement of a portion of a text file. type Edit struct { Start, End int // byte offsets of the region to replace New string // the replacement @@ -88,15 +82,9 @@ func (a editsSort) Less(i, j int) bool { } func (a editsSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -// LineEdits expands and merges a sequence of edits so that each +// lineEdits expands and merges a sequence of edits so that each // resulting edit replaces one or more complete lines. -// -// It may panic or produce garbage if the edits -// are overlapping, out of bounds of src, or out of order. -// TODO(adonovan): see consistency note at Apply. -// We could hide this from the API so that we can enforce -// the precondition... but it seems like a reasonable feature. -func LineEdits(src string, edits []Edit) []Edit { +func lineEdits(src string, edits []Edit) []Edit { sortEdits(edits) // TODO(adonovan): is this necessary? Move burden to caller? // Do all edits begin and end at the start of a line? diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index d4c3746184d..34754077522 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -87,36 +87,6 @@ func FuzzRoundTrip(f *testing.F) { }) } -func TestNLinesRandom(t *testing.T) { - rand.Seed(2) - for i := 0; i < 1000; i++ { - x := randlines("abω", 4) // avg line length is 6, want a change every 3rd line or so - v := []rune(x) - for i := 0; i < len(v); i++ { - if rand.Float64() < .05 { - v[i] = 'N' - } - } - y := string(v) - // occasionally remove the trailing \n - if rand.Float64() < .1 { - x = x[:len(x)-1] - } - if rand.Float64() < .1 { - y = y[:len(y)-1] - } - a, b := strings.SplitAfter(x, "\n"), strings.SplitAfter(y, "\n") - edits := diff.Lines(a, b) - got, err := diff.Apply(x, edits) - if err != nil { - t.Fatalf("Apply failed: %v", err) - } - if got != y { - t.Fatalf("%d: got\n%q, wanted\n%q, starting with %q", i, got, y, a) - } - } -} - func TestLineEdits(t *testing.T) { for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { diff --git a/internal/diff/export_test.go b/internal/diff/export_test.go new file mode 100644 index 00000000000..eedf0dd77ba --- /dev/null +++ b/internal/diff/export_test.go @@ -0,0 +1,9 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diff + +// This file exports some private declarations to tests. + +var LineEdits = lineEdits diff --git a/internal/diff/ndiff.go b/internal/diff/ndiff.go index e76d2db775c..722b50c986b 100644 --- a/internal/diff/ndiff.go +++ b/internal/diff/ndiff.go @@ -5,111 +5,104 @@ package diff import ( - "strings" + "bytes" "unicode/utf8" "golang.org/x/tools/internal/diff/lcs" ) -// maxDiffs is a limit on how deeply the lcs algorithm should search -// the value is just a guess -const maxDiffs = 30 - // Strings computes the differences between two strings. -// (Both it and the diff in the myers package have type ComputeEdits, which -// is why the arguments are strings, not []bytes.) -// TODO(adonovan): opt: consider switching everything to []bytes, if -// that's the more common type in practice. Or provide both flavors? +// The resulting edits respect rune boundaries. func Strings(before, after string) []Edit { if before == after { - // very frequently true - return nil + return nil // common case } - // The diffs returned by the lcs package use indexes into - // whatever slice was passed in. Edits use byte offsets, so - // rune or line offsets need to be converted. - // TODO(adonovan): opt: eliminate all the unnecessary allocations. - var diffs []lcs.Diff - if !isASCII(before) || !isASCII(after) { - diffs, _ = lcs.Compute([]rune(before), []rune(after), maxDiffs/2) - diffs = runeOffsets(diffs, []rune(before)) - } else { - // Common case: pure ASCII. Avoid expansion to []rune slice. - diffs, _ = lcs.Compute([]byte(before), []byte(after), maxDiffs/2) + + if stringIsASCII(before) && stringIsASCII(after) { + return diffASCII([]byte(before), []byte(after)) } - return convertDiffs(diffs) + return diffRunes([]rune(before), []rune(after)) } -// Lines computes the differences between two list of lines. -// TODO(adonovan): unused except by its test. Do we actually need it? -func Lines(before, after []string) []Edit { - diffs, _ := lcs.Compute(before, after, maxDiffs/2) - diffs = lineOffsets(diffs, before) - return convertDiffs(diffs) - // the code is not coping with possible missing \ns at the ends -} +// Bytes computes the differences between two byte slices. +// The resulting edits respect rune boundaries. +func Bytes(before, after []byte) []Edit { + if bytes.Equal(before, after) { + return nil // common case + } -func convertDiffs(diffs []lcs.Diff) []Edit { - ans := make([]Edit, len(diffs)) - for i, d := range diffs { - ans[i] = Edit{d.Start, d.End, d.Text} + if bytesIsASCII(before) && bytesIsASCII(after) { + return diffASCII(before, after) } - return ans + return diffRunes(runes(before), runes(after)) } -// convert diffs with rune offsets into diffs with byte offsets -func runeOffsets(diffs []lcs.Diff, src []rune) []lcs.Diff { - var idx int - var tmp strings.Builder // string because []byte([]rune) is illegal +func diffASCII(before, after []byte) []Edit { + diffs, _ := lcs.Compute(before, after, maxDiffs/2) + + // Convert from LCS diffs. + res := make([]Edit, len(diffs)) for i, d := range diffs { - tmp.WriteString(string(src[idx:d.Start])) - v := tmp.Len() - tmp.WriteString(string(src[d.Start:d.End])) - d.Start = v - idx = d.End - d.End = tmp.Len() - diffs[i] = d + res[i] = Edit{d.Start, d.End, d.Text} } - return diffs + return res } -// convert diffs with line offsets into diffs with byte offsets -func lineOffsets(diffs []lcs.Diff, src []string) []lcs.Diff { - var idx int - var tmp strings.Builder // bytes/ +func diffRunes(before, after []rune) []Edit { + diffs, _ := lcs.Compute(before, after, maxDiffs/2) + + // The diffs returned by the lcs package use indexes + // into whatever slice was passed in. + // Convert rune offsets to byte offsets. + res := make([]Edit, len(diffs)) + lastEnd := 0 + utf8Len := 0 for i, d := range diffs { - tmp.WriteString(strJoin(src[idx:d.Start])) - v := tmp.Len() - tmp.WriteString(strJoin(src[d.Start:d.End])) - d.Start = v - idx = d.End - d.End = tmp.Len() - diffs[i] = d + utf8Len += runesLen(before[lastEnd:d.Start]) // text between edits + start := utf8Len + utf8Len += runesLen(before[d.Start:d.End]) // text deleted by this edit + res[i] = Edit{start, utf8Len, d.Text} + lastEnd = d.End } - return diffs + return res } -// join lines. (strings.Join doesn't add a trailing separator) -func strJoin(elems []string) string { - if len(elems) == 0 { - return "" - } - n := 0 - for i := 0; i < len(elems); i++ { - n += len(elems[i]) +// maxDiffs is a limit on how deeply the lcs algorithm should search +// the value is just a guess +const maxDiffs = 30 + +// runes is like []rune(string(bytes)) without the duplicate allocation. +func runes(bytes []byte) []rune { + n := utf8.RuneCount(bytes) + runes := make([]rune, n) + for i := 0; i < n; i++ { + r, sz := utf8.DecodeRune(bytes) + bytes = bytes[sz:] + runes[i] = r } + return runes +} - var b strings.Builder - b.Grow(n) - for _, s := range elems { - b.WriteString(s) - //b.WriteByte('\n') +// runesLen returns the length in bytes of the UTF-8 encoding of runes. +func runesLen(runes []rune) (len int) { + for _, r := range runes { + len += utf8.RuneLen(r) } - return b.String() + return len } // isASCII reports whether s contains only ASCII. -func isASCII(s string) bool { +// TODO(adonovan): combine when x/tools allows generics. +func stringIsASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] >= utf8.RuneSelf { + return false + } + } + return true +} + +func bytesIsASCII(s []byte) bool { for i := 0; i < len(s); i++ { if s[i] >= utf8.RuneSelf { return false diff --git a/internal/diff/unified.go b/internal/diff/unified.go index f8618328f0c..d71b500f067 100644 --- a/internal/diff/unified.go +++ b/internal/diff/unified.go @@ -9,8 +9,6 @@ import ( "strings" ) -// TODO(adonovan): API: hide all but func Unified. - // Unified applies the edits to content and presents a unified diff. // The old and new labels are the names of the content and result files. func Unified(oldLabel, newLabel string, content string, edits []Edit) string { @@ -92,7 +90,7 @@ func toUnified(fromName, toName string, content string, edits []Edit) unified { if len(edits) == 0 { return u } - edits = LineEdits(content, edits) // expand to whole lines + edits = lineEdits(content, edits) // expand to whole lines lines := splitLines(content) var h *hunk last := 0 From 26a95e690155cd43f720b6ac25fd5413a0c921b9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 7 Oct 2022 10:40:32 -0400 Subject: [PATCH 314/723] gopls/internal/span: move internal/span into gopls Spans are logically part of gopls, but could not be moved into the gopls module because of a number of depenencies from packagestest, analysis, and internal/diff. Those edges are now broken. Change-Id: Icba5ebec6b27974f832a1186120a4b87d5f87103 Reviewed-on: https://go-review.googlesource.com/c/tools/+/440176 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/doc/design/integrating.md | 4 ++-- gopls/internal/lsp/analysis/fillstruct/fillstruct.go | 2 +- gopls/internal/lsp/analysis/undeclaredname/undeclared.go | 2 +- gopls/internal/lsp/cache/analysis.go | 2 +- gopls/internal/lsp/cache/cache.go | 6 +++--- gopls/internal/lsp/cache/check.go | 2 +- gopls/internal/lsp/cache/errors.go | 6 +++--- gopls/internal/lsp/cache/graph.go | 2 +- gopls/internal/lsp/cache/load.go | 2 +- gopls/internal/lsp/cache/maps.go | 2 +- gopls/internal/lsp/cache/metadata.go | 2 +- gopls/internal/lsp/cache/mod.go | 8 ++++---- gopls/internal/lsp/cache/mod_tidy.go | 2 +- gopls/internal/lsp/cache/pkg.go | 2 +- gopls/internal/lsp/cache/session.go | 2 +- gopls/internal/lsp/cache/snapshot.go | 2 +- gopls/internal/lsp/cache/view.go | 2 +- gopls/internal/lsp/cache/view_test.go | 2 +- gopls/internal/lsp/cache/workspace.go | 4 ++-- gopls/internal/lsp/cache/workspace_test.go | 2 +- gopls/internal/lsp/cmd/call_hierarchy.go | 2 +- gopls/internal/lsp/cmd/check.go | 2 +- gopls/internal/lsp/cmd/cmd.go | 2 +- gopls/internal/lsp/cmd/definition.go | 2 +- gopls/internal/lsp/cmd/folding_range.go | 2 +- gopls/internal/lsp/cmd/format.go | 2 +- gopls/internal/lsp/cmd/highlight.go | 2 +- gopls/internal/lsp/cmd/implementation.go | 2 +- gopls/internal/lsp/cmd/imports.go | 2 +- gopls/internal/lsp/cmd/links.go | 2 +- gopls/internal/lsp/cmd/prepare_rename.go | 2 +- gopls/internal/lsp/cmd/references.go | 2 +- gopls/internal/lsp/cmd/rename.go | 2 +- gopls/internal/lsp/cmd/semantictokens.go | 2 +- gopls/internal/lsp/cmd/signature.go | 2 +- gopls/internal/lsp/cmd/suggested_fix.go | 2 +- gopls/internal/lsp/cmd/symbols.go | 2 +- gopls/internal/lsp/cmd/test/call_hierarchy.go | 2 +- gopls/internal/lsp/cmd/test/check.go | 2 +- gopls/internal/lsp/cmd/test/cmdtest.go | 2 +- gopls/internal/lsp/cmd/test/definition.go | 2 +- gopls/internal/lsp/cmd/test/folding_range.go | 2 +- gopls/internal/lsp/cmd/test/format.go | 2 +- gopls/internal/lsp/cmd/test/highlight.go | 2 +- gopls/internal/lsp/cmd/test/implementation.go | 2 +- gopls/internal/lsp/cmd/test/imports.go | 2 +- gopls/internal/lsp/cmd/test/links.go | 2 +- gopls/internal/lsp/cmd/test/prepare_rename.go | 2 +- gopls/internal/lsp/cmd/test/references.go | 2 +- gopls/internal/lsp/cmd/test/rename.go | 2 +- gopls/internal/lsp/cmd/test/semanticdriver.go | 2 +- gopls/internal/lsp/cmd/test/signature.go | 2 +- gopls/internal/lsp/cmd/test/suggested_fix.go | 2 +- gopls/internal/lsp/cmd/test/symbols.go | 2 +- gopls/internal/lsp/cmd/test/workspace_symbol.go | 2 +- gopls/internal/lsp/code_action.go | 2 +- gopls/internal/lsp/command.go | 2 +- gopls/internal/lsp/completion_test.go | 2 +- gopls/internal/lsp/diagnostics.go | 2 +- gopls/internal/lsp/fake/editor.go | 2 +- gopls/internal/lsp/fake/workdir.go | 2 +- gopls/internal/lsp/general.go | 2 +- gopls/internal/lsp/link.go | 6 +++--- gopls/internal/lsp/lsp_test.go | 2 +- gopls/internal/lsp/mod/mod_test.go | 2 +- gopls/internal/lsp/protocol/span.go | 2 +- gopls/internal/lsp/rename.go | 2 +- gopls/internal/lsp/server.go | 2 +- gopls/internal/lsp/source/call_hierarchy.go | 4 ++-- gopls/internal/lsp/source/code_lens.go | 2 +- gopls/internal/lsp/source/completion/completion.go | 8 ++++---- gopls/internal/lsp/source/completion/definition.go | 2 +- gopls/internal/lsp/source/completion/format.go | 2 +- gopls/internal/lsp/source/completion/package.go | 6 +++--- gopls/internal/lsp/source/diagnostics.go | 2 +- gopls/internal/lsp/source/extract.go | 2 +- gopls/internal/lsp/source/fix.go | 4 ++-- gopls/internal/lsp/source/gc_annotations.go | 4 ++-- gopls/internal/lsp/source/identifier.go | 2 +- gopls/internal/lsp/source/implementation.go | 2 +- gopls/internal/lsp/source/references.go | 2 +- gopls/internal/lsp/source/rename.go | 2 +- gopls/internal/lsp/source/source_test.go | 2 +- gopls/internal/lsp/source/stub.go | 2 +- gopls/internal/lsp/source/util.go | 2 +- gopls/internal/lsp/source/util_test.go | 2 +- gopls/internal/lsp/source/view.go | 2 +- gopls/internal/lsp/source/workspace_symbol.go | 4 ++-- gopls/internal/lsp/template/implementations.go | 2 +- gopls/internal/lsp/template/parse.go | 2 +- gopls/internal/lsp/tests/tests.go | 2 +- gopls/internal/lsp/tests/util.go | 2 +- gopls/internal/lsp/text_synchronization.go | 6 +++--- gopls/internal/lsp/work/diagnostics.go | 2 +- gopls/internal/lsp/workspace.go | 2 +- {internal => gopls/internal}/span/parse.go | 0 {internal => gopls/internal}/span/span.go | 0 {internal => gopls/internal}/span/span_test.go | 2 +- {internal => gopls/internal}/span/token.go | 0 {internal => gopls/internal}/span/token_test.go | 2 +- {internal => gopls/internal}/span/uri.go | 0 {internal => gopls/internal}/span/uri_test.go | 2 +- {internal => gopls/internal}/span/uri_windows_test.go | 2 +- {internal => gopls/internal}/span/utf16.go | 0 {internal => gopls/internal}/span/utf16_test.go | 2 +- gopls/internal/vulncheck/vulntest/db.go | 2 +- gopls/test/debug/debug_test.go | 2 +- internal/diff/diff_test.go | 4 ++-- internal/diff/lcs/old_test.go | 4 ++-- 109 files changed, 128 insertions(+), 128 deletions(-) rename {internal => gopls/internal}/span/parse.go (100%) rename {internal => gopls/internal}/span/span.go (100%) rename {internal => gopls/internal}/span/span_test.go (97%) rename {internal => gopls/internal}/span/token.go (100%) rename {internal => gopls/internal}/span/token_test.go (97%) rename {internal => gopls/internal}/span/uri.go (100%) rename {internal => gopls/internal}/span/uri_test.go (98%) rename {internal => gopls/internal}/span/uri_windows_test.go (98%) rename {internal => gopls/internal}/span/utf16.go (100%) rename {internal => gopls/internal}/span/utf16_test.go (99%) diff --git a/gopls/doc/design/integrating.md b/gopls/doc/design/integrating.md index 7937ba844ca..ba2cc07aa71 100644 --- a/gopls/doc/design/integrating.md +++ b/gopls/doc/design/integrating.md @@ -20,7 +20,7 @@ Many LSP requests pass position or range information. This is described in the [ This means that integrators will need to calculate UTF-16 based column offsets. -[`golang.org/x/tools/internal/span`] has the code to do this in go. +[`golang.org/x/tools/gopls/internal/span`] has the code to do this in go. [#31080] tracks making `span` and other useful packages non-internal. ## Edits @@ -63,7 +63,7 @@ This is currently being added to gopls by a community member, and tracked in [#3 [InitializeResult]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#InitializeResult [ServerCapabilities]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#ServerCapabilities -[`golang.org/x/tools/internal/span`]: https://pkg.go.dev/golang.org/x/tools/internal/span#NewPoint +[`golang.org/x/tools/gopls/internal/span`]: https://pkg.go.dev/golang.org/x/tools/internal/span#NewPoint [LSP specification]: https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/ [lsp-response]: https://github.com/Microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md#response-message diff --git a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go index 931b2190060..faf5ba5a9a6 100644 --- a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -26,9 +26,9 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/fuzzy" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go index eaecc8f0e12..fd8d35eb1b9 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -18,8 +18,8 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/span" ) const Doc = `suggested fixes for "undeclared name: <>" diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index ffd14059403..95bac5b49eb 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -16,12 +16,12 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/analysis" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" ) func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) { diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go index 5056111ff75..0eb00f23201 100644 --- a/gopls/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -20,12 +20,12 @@ import ( "sync/atomic" "time" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" ) // New Creates a new cache for gopls operation results, using the given file diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 7058394f873..0b37135fc00 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -23,12 +23,12 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" ) diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index d01bf7f92e0..1c38e205e99 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -15,12 +15,12 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/typesinternal" ) diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index f90ee960881..a801c83a9ab 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -8,7 +8,7 @@ import ( "sort" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // A metadataGraph is an immutable and transitively closed import diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 287c310a1f8..a77afaaac6a 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -19,11 +19,11 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/packagesinternal" - "golang.org/x/tools/internal/span" ) var loadID uint64 // atomic identifier for loads diff --git a/gopls/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go index d3fe4e45e5f..edb8d168f24 100644 --- a/gopls/internal/lsp/cache/maps.go +++ b/gopls/internal/lsp/cache/maps.go @@ -6,8 +6,8 @@ package cache import ( "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/persistent" - "golang.org/x/tools/internal/span" ) // TODO(euroelessar): Use generics once support for go1.17 is dropped. diff --git a/gopls/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go index 7778996061b..66c679b18d8 100644 --- a/gopls/internal/lsp/cache/metadata.go +++ b/gopls/internal/lsp/cache/metadata.go @@ -8,8 +8,8 @@ import ( "go/types" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/packagesinternal" - "golang.org/x/tools/internal/span" ) // Declare explicit types for package paths, names, and IDs to ensure that we diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index 50fa0a4dd1c..f8404e7a9eb 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -14,14 +14,14 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/mod/module" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" ) // ParseMod parses a go.mod file, using a cache. It may return partial results and an error. diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index e471a9bf6ab..63346dc548e 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -19,11 +19,11 @@ import ( "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" ) // ModTidy returns the go.mod file that would be obtained by running diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index b1ee50e2faa..76e29eef027 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -12,7 +12,7 @@ import ( "golang.org/x/mod/module" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // pkg contains the type information needed by the source package. diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 8a45ae24af5..cb18834d1cd 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -16,11 +16,11 @@ import ( "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/persistent" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index fb4e78bfde7..8027b40a0ab 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -32,6 +32,7 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" @@ -39,7 +40,6 @@ import ( "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/persistent" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typesinternal" ) diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index af4ab9afbbd..7d3cba3edae 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -27,11 +27,11 @@ import ( "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go index f3851e9131d..99daff13b01 100644 --- a/gopls/internal/lsp/cache/view_test.go +++ b/gopls/internal/lsp/cache/view_test.go @@ -12,7 +12,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func TestCaseInsensitiveFilesystem(t *testing.T) { diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go index e066c7e56ec..2020718ea7d 100644 --- a/gopls/internal/lsp/cache/workspace.go +++ b/gopls/internal/lsp/cache/workspace.go @@ -15,9 +15,9 @@ import ( "sync" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/xcontext" ) diff --git a/gopls/internal/lsp/cache/workspace_test.go b/gopls/internal/lsp/cache/workspace_test.go index e6047a92629..37e8f2cc46d 100644 --- a/gopls/internal/lsp/cache/workspace_test.go +++ b/gopls/internal/lsp/cache/workspace_test.go @@ -14,7 +14,7 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // osFileSource is a fileSource that just reads from the operating system. diff --git a/gopls/internal/lsp/cmd/call_hierarchy.go b/gopls/internal/lsp/cmd/call_hierarchy.go index 643b2f7ff86..7736eecbe59 100644 --- a/gopls/internal/lsp/cmd/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/call_hierarchy.go @@ -11,7 +11,7 @@ import ( "strings" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/check.go b/gopls/internal/lsp/cmd/check.go index 9a136699270..5e4fe39eb36 100644 --- a/gopls/internal/lsp/cmd/check.go +++ b/gopls/internal/lsp/cmd/check.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // check implements the check verb for gopls. diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index 824382de96e..38accb6b9bb 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -28,8 +28,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/lsprpc" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" "golang.org/x/tools/internal/xcontext" ) diff --git a/gopls/internal/lsp/cmd/definition.go b/gopls/internal/lsp/cmd/definition.go index 3c4474a75ae..9096e17153e 100644 --- a/gopls/internal/lsp/cmd/definition.go +++ b/gopls/internal/lsp/cmd/definition.go @@ -14,7 +14,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/folding_range.go b/gopls/internal/lsp/cmd/folding_range.go index ee407150dd4..17cead91f00 100644 --- a/gopls/internal/lsp/cmd/folding_range.go +++ b/gopls/internal/lsp/cmd/folding_range.go @@ -10,7 +10,7 @@ import ( "fmt" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go index 56e39053c10..2f3b6828d61 100644 --- a/gopls/internal/lsp/cmd/format.go +++ b/gopls/internal/lsp/cmd/format.go @@ -12,8 +12,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" ) // format implements the format verb for gopls. diff --git a/gopls/internal/lsp/cmd/highlight.go b/gopls/internal/lsp/cmd/highlight.go index ace8125fd81..dcbd1097a06 100644 --- a/gopls/internal/lsp/cmd/highlight.go +++ b/gopls/internal/lsp/cmd/highlight.go @@ -11,7 +11,7 @@ import ( "sort" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/implementation.go b/gopls/internal/lsp/cmd/implementation.go index 073a61a75b3..259b72572b4 100644 --- a/gopls/internal/lsp/cmd/implementation.go +++ b/gopls/internal/lsp/cmd/implementation.go @@ -11,7 +11,7 @@ import ( "sort" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go index 4ee6fce4c96..c041eeee6dc 100644 --- a/gopls/internal/lsp/cmd/imports.go +++ b/gopls/internal/lsp/cmd/imports.go @@ -12,8 +12,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/links.go b/gopls/internal/lsp/cmd/links.go index d63ac218ecb..aec36da910c 100644 --- a/gopls/internal/lsp/cmd/links.go +++ b/gopls/internal/lsp/cmd/links.go @@ -12,7 +12,7 @@ import ( "os" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/prepare_rename.go b/gopls/internal/lsp/cmd/prepare_rename.go index 7dfac9b2545..434904b6920 100644 --- a/gopls/internal/lsp/cmd/prepare_rename.go +++ b/gopls/internal/lsp/cmd/prepare_rename.go @@ -11,7 +11,7 @@ import ( "fmt" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/references.go b/gopls/internal/lsp/cmd/references.go index 13c30bdf13f..bbebc9f917d 100644 --- a/gopls/internal/lsp/cmd/references.go +++ b/gopls/internal/lsp/cmd/references.go @@ -11,7 +11,7 @@ import ( "sort" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go index 58fb07d7f15..ceca7347c59 100644 --- a/gopls/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -15,8 +15,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/semantictokens.go b/gopls/internal/lsp/cmd/semantictokens.go index 547b0194f21..f90d49ccf34 100644 --- a/gopls/internal/lsp/cmd/semantictokens.go +++ b/gopls/internal/lsp/cmd/semantictokens.go @@ -19,7 +19,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // generate semantic tokens and interpolate them in the file diff --git a/gopls/internal/lsp/cmd/signature.go b/gopls/internal/lsp/cmd/signature.go index 81ec4feabaf..657d77235f0 100644 --- a/gopls/internal/lsp/cmd/signature.go +++ b/gopls/internal/lsp/cmd/signature.go @@ -10,7 +10,7 @@ import ( "fmt" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go index 9c103deff8a..6d42a670643 100644 --- a/gopls/internal/lsp/cmd/suggested_fix.go +++ b/gopls/internal/lsp/cmd/suggested_fix.go @@ -12,8 +12,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/symbols.go b/gopls/internal/lsp/cmd/symbols.go index 0d6e4dbe08d..3ecdff8011c 100644 --- a/gopls/internal/lsp/cmd/symbols.go +++ b/gopls/internal/lsp/cmd/symbols.go @@ -12,7 +12,7 @@ import ( "sort" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/test/call_hierarchy.go b/gopls/internal/lsp/cmd/test/call_hierarchy.go index be4ebe8e339..bb8d306224a 100644 --- a/gopls/internal/lsp/cmd/test/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/test/call_hierarchy.go @@ -12,7 +12,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) { diff --git a/gopls/internal/lsp/cmd/test/check.go b/gopls/internal/lsp/cmd/test/check.go index 819863dadaf..ea9747cae79 100644 --- a/gopls/internal/lsp/cmd/test/check.go +++ b/gopls/internal/lsp/cmd/test/check.go @@ -12,7 +12,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // Diagnostics runs the gopls command on a single file, parses its diff --git a/gopls/internal/lsp/cmd/test/cmdtest.go b/gopls/internal/lsp/cmd/test/cmdtest.go index e2a3e546e23..167631a3cab 100644 --- a/gopls/internal/lsp/cmd/test/cmdtest.go +++ b/gopls/internal/lsp/cmd/test/cmdtest.go @@ -23,8 +23,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/lsp/cmd/test/definition.go b/gopls/internal/lsp/cmd/test/definition.go index 6241867fa20..ca84e80ebe2 100644 --- a/gopls/internal/lsp/cmd/test/definition.go +++ b/gopls/internal/lsp/cmd/test/definition.go @@ -11,7 +11,7 @@ import ( "testing" "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) type godefMode int diff --git a/gopls/internal/lsp/cmd/test/folding_range.go b/gopls/internal/lsp/cmd/test/folding_range.go index ac41697a260..184c01a05bb 100644 --- a/gopls/internal/lsp/cmd/test/folding_range.go +++ b/gopls/internal/lsp/cmd/test/folding_range.go @@ -7,7 +7,7 @@ package cmdtest import ( "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { diff --git a/gopls/internal/lsp/cmd/test/format.go b/gopls/internal/lsp/cmd/test/format.go index 6523e665c62..368d535b20a 100644 --- a/gopls/internal/lsp/cmd/test/format.go +++ b/gopls/internal/lsp/cmd/test/format.go @@ -14,7 +14,7 @@ import ( exec "golang.org/x/sys/execabs" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/lsp/cmd/test/highlight.go b/gopls/internal/lsp/cmd/test/highlight.go index 99e8b2c3fc7..cd51b093c68 100644 --- a/gopls/internal/lsp/cmd/test/highlight.go +++ b/gopls/internal/lsp/cmd/test/highlight.go @@ -9,7 +9,7 @@ import ( "fmt" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Highlight(t *testing.T, spn span.Span, spans []span.Span) { diff --git a/gopls/internal/lsp/cmd/test/implementation.go b/gopls/internal/lsp/cmd/test/implementation.go index 189452466ce..e24584da99d 100644 --- a/gopls/internal/lsp/cmd/test/implementation.go +++ b/gopls/internal/lsp/cmd/test/implementation.go @@ -9,7 +9,7 @@ import ( "sort" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Implementation(t *testing.T, spn span.Span, imps []span.Span) { diff --git a/gopls/internal/lsp/cmd/test/imports.go b/gopls/internal/lsp/cmd/test/imports.go index eab46681e7d..5659291faa3 100644 --- a/gopls/internal/lsp/cmd/test/imports.go +++ b/gopls/internal/lsp/cmd/test/imports.go @@ -7,8 +7,8 @@ package cmdtest import ( "testing" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" ) func (r *runner) Import(t *testing.T, spn span.Span) { diff --git a/gopls/internal/lsp/cmd/test/links.go b/gopls/internal/lsp/cmd/test/links.go index 52d2a3185e2..a9616ee48a9 100644 --- a/gopls/internal/lsp/cmd/test/links.go +++ b/gopls/internal/lsp/cmd/test/links.go @@ -10,7 +10,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { diff --git a/gopls/internal/lsp/cmd/test/prepare_rename.go b/gopls/internal/lsp/cmd/test/prepare_rename.go index 4ae6d1ad857..c818c0197da 100644 --- a/gopls/internal/lsp/cmd/test/prepare_rename.go +++ b/gopls/internal/lsp/cmd/test/prepare_rename.go @@ -11,7 +11,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/cmd" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) { diff --git a/gopls/internal/lsp/cmd/test/references.go b/gopls/internal/lsp/cmd/test/references.go index 66d0d066286..85c9bc84a62 100644 --- a/gopls/internal/lsp/cmd/test/references.go +++ b/gopls/internal/lsp/cmd/test/references.go @@ -9,7 +9,7 @@ import ( "sort" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) References(t *testing.T, spn span.Span, itemList []span.Span) { diff --git a/gopls/internal/lsp/cmd/test/rename.go b/gopls/internal/lsp/cmd/test/rename.go index 4748c471501..0750d70281d 100644 --- a/gopls/internal/lsp/cmd/test/rename.go +++ b/gopls/internal/lsp/cmd/test/rename.go @@ -8,7 +8,7 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { diff --git a/gopls/internal/lsp/cmd/test/semanticdriver.go b/gopls/internal/lsp/cmd/test/semanticdriver.go index 24a1fbfdab0..069dd64f6e6 100644 --- a/gopls/internal/lsp/cmd/test/semanticdriver.go +++ b/gopls/internal/lsp/cmd/test/semanticdriver.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { diff --git a/gopls/internal/lsp/cmd/test/signature.go b/gopls/internal/lsp/cmd/test/signature.go index 273e6349121..40669e8d223 100644 --- a/gopls/internal/lsp/cmd/test/signature.go +++ b/gopls/internal/lsp/cmd/test/signature.go @@ -10,7 +10,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) { diff --git a/gopls/internal/lsp/cmd/test/suggested_fix.go b/gopls/internal/lsp/cmd/test/suggested_fix.go index 6d67f28807a..1e61fe9bcd5 100644 --- a/gopls/internal/lsp/cmd/test/suggested_fix.go +++ b/gopls/internal/lsp/cmd/test/suggested_fix.go @@ -10,7 +10,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) SuggestedFix(t *testing.T, spn span.Span, suggestedFixes []tests.SuggestedFix, expectedActions int) { diff --git a/gopls/internal/lsp/cmd/test/symbols.go b/gopls/internal/lsp/cmd/test/symbols.go index 3bd2fc02247..aaf3725d9c0 100644 --- a/gopls/internal/lsp/cmd/test/symbols.go +++ b/gopls/internal/lsp/cmd/test/symbols.go @@ -9,7 +9,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { diff --git a/gopls/internal/lsp/cmd/test/workspace_symbol.go b/gopls/internal/lsp/cmd/test/workspace_symbol.go index 4c19d1dd5ca..40c2c65d019 100644 --- a/gopls/internal/lsp/cmd/test/workspace_symbol.go +++ b/gopls/internal/lsp/cmd/test/workspace_symbol.go @@ -14,7 +14,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index e1f0dc23c99..1ad0cb417c1 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -14,10 +14,10 @@ import ( "golang.org/x/tools/gopls/internal/lsp/mod" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/span" ) func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index cb4c6982835..7f7380e2991 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -25,10 +25,10 @@ import ( "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index 98d92b982cd..23d69ed1269 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -11,7 +11,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 3efcaffcda4..f6f9f7935a3 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -21,9 +21,9 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/template" "golang.org/x/tools/gopls/internal/lsp/work" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index e65db938ad7..d9e6c701e65 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -19,9 +19,9 @@ import ( "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index 65302606092..3f17cb68f1a 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -18,7 +18,7 @@ import ( "time" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // FileEvent wraps the protocol.FileEvent so that it can be associated with a diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index 01f8242d0b0..eee946ed020 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -18,10 +18,10 @@ import ( "golang.org/x/tools/gopls/internal/lsp/debug" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/span" ) func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go index 456f4173048..929fe289f28 100644 --- a/gopls/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -17,11 +17,11 @@ import ( "sync" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" ) func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) (links []protocol.DocumentLink, err error) { diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 4cd009d2f0f..ce1aac49510 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -23,9 +23,9 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/lsp/mod/mod_test.go b/gopls/internal/lsp/mod/mod_test.go index 75c16660e5a..767ec44a7d8 100644 --- a/gopls/internal/lsp/mod/mod_test.go +++ b/gopls/internal/lsp/mod/mod_test.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index cbf57a9ec51..bfa2f8111e7 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -12,7 +12,7 @@ import ( "go/token" "unicode/utf8" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // A ColumnMapper maps between UTF-8 oriented positions (e.g. token.Pos, diff --git a/gopls/internal/lsp/rename.go b/gopls/internal/lsp/rename.go index 2d9ad69323f..e9bb2d40033 100644 --- a/gopls/internal/lsp/rename.go +++ b/gopls/internal/lsp/rename.go @@ -10,7 +10,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go index 0d257b26ebf..693afaedab5 100644 --- a/gopls/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -14,8 +14,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/span" ) const concurrentAnalyses = 1 diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index 298c712d2ea..1097d629072 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -14,10 +14,10 @@ import ( "path/filepath" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" ) // PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file. diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go index 6f1d720b37f..83ffcc28d29 100644 --- a/gopls/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -15,7 +15,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) type LensFunc func(context.Context, Snapshot, FileHandle) ([]protocol.CodeLens, error) diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 2ffd9bf5d60..1e5dd1cba09 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -23,13 +23,13 @@ import ( "unicode" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/fuzzy" + "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typeparams" ) diff --git a/gopls/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go index e61a9fa2620..fe41c55b3b8 100644 --- a/gopls/internal/lsp/source/completion/definition.go +++ b/gopls/internal/lsp/source/completion/definition.go @@ -15,7 +15,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // some definitions can be completed diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go index 4a76eefdaae..c2693dc12d4 100644 --- a/gopls/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -16,10 +16,10 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index 1f3649b428b..b7fad0fa513 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -18,12 +18,12 @@ import ( "strings" "unicode" - "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/fuzzy" ) // packageClauseCompletions offers completions for a package declaration when diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index faf49191e3e..c292f257989 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -8,7 +8,7 @@ import ( "context" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) type SuggestedFix struct { diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index 8e6f77d1e6b..488305ac0bd 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -19,9 +19,9 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/span" ) func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index bce2536976e..1ce6d758b39 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -14,9 +14,9 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" ) type ( diff --git a/gopls/internal/lsp/source/gc_annotations.go b/gopls/internal/lsp/source/gc_annotations.go index 111cd4fb8c0..ab0fd6035e6 100644 --- a/gopls/internal/lsp/source/gc_annotations.go +++ b/gopls/internal/lsp/source/gc_annotations.go @@ -14,9 +14,9 @@ import ( "path/filepath" "strings" - "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/gocommand" ) type Annotation string diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 91c7b6b30d8..7bf1e12b745 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -17,9 +17,9 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 3533c92d670..e2c60bb55e7 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -15,8 +15,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/span" ) func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) ([]protocol.Location, error) { diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 714a3cb7ade..9a10b38d161 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -16,9 +16,9 @@ import ( "strconv" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/span" ) // ReferenceInfo holds information about reference to an identifier in Go source. diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index c5af1ca896e..495791f9537 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -20,9 +20,9 @@ import ( "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/span" "golang.org/x/tools/refactor/satisfy" ) diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index d81bdb7aead..8c49306f5cc 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -21,9 +21,9 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source/completion" "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/fuzzy" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 3aab0b4e8aa..2d00ea5272c 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -20,7 +20,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 9939ca02a9c..b06992e4823 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -19,8 +19,8 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) diff --git a/gopls/internal/lsp/source/util_test.go b/gopls/internal/lsp/source/util_test.go index e0b2a29e3bb..60128e2344a 100644 --- a/gopls/internal/lsp/source/util_test.go +++ b/gopls/internal/lsp/source/util_test.go @@ -11,7 +11,7 @@ import ( "testing" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func TestMappedRangeAdjustment(t *testing.T) { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index fa9d6a901ec..e679839cfd8 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -23,9 +23,9 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/span" ) // Snapshot represents the current state for the given view. diff --git a/gopls/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go index 8fe23889c75..dabbeb3dd9f 100644 --- a/gopls/internal/lsp/source/workspace_symbol.go +++ b/gopls/internal/lsp/source/workspace_symbol.go @@ -16,10 +16,10 @@ import ( "strings" "unicode" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/fuzzy" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/span" ) // Symbol holds a precomputed symbol value. Note: we avoid using the diff --git a/gopls/internal/lsp/template/implementations.go b/gopls/internal/lsp/template/implementations.go index 3d14f6b4a9a..6c90b68cd5c 100644 --- a/gopls/internal/lsp/template/implementations.go +++ b/gopls/internal/lsp/template/implementations.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // line number (1-based) and message diff --git a/gopls/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go index 679bd4d16a5..06b7568094d 100644 --- a/gopls/internal/lsp/template/parse.go +++ b/gopls/internal/lsp/template/parse.go @@ -26,8 +26,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/span" ) var ( diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 2b8e8300af4..cab96e0e82c 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -32,7 +32,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/completion" "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/txtar" diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index ce5ab5b7924..2951f037dac 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -20,8 +20,8 @@ import ( "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/span" "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/span" ) // DiffLinks takes the links we got and checks if they are located within the source or a Note. diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go index c360e1e1eeb..9687528c645 100644 --- a/gopls/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -12,11 +12,11 @@ import ( "path/filepath" "time" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/xcontext" ) diff --git a/gopls/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go index 72c06e07f24..5d3a32897d2 100644 --- a/gopls/internal/lsp/work/diagnostics.go +++ b/gopls/internal/lsp/work/diagnostics.go @@ -13,9 +13,9 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/internal/span" ) func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { diff --git a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go index 79f144b8993..0e780d23680 100644 --- a/gopls/internal/lsp/workspace.go +++ b/gopls/internal/lsp/workspace.go @@ -10,7 +10,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (s *Server) didChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error { diff --git a/internal/span/parse.go b/gopls/internal/span/parse.go similarity index 100% rename from internal/span/parse.go rename to gopls/internal/span/parse.go diff --git a/internal/span/span.go b/gopls/internal/span/span.go similarity index 100% rename from internal/span/span.go rename to gopls/internal/span/span.go diff --git a/internal/span/span_test.go b/gopls/internal/span/span_test.go similarity index 97% rename from internal/span/span_test.go rename to gopls/internal/span/span_test.go index cff59c3d116..63c0752f959 100644 --- a/internal/span/span_test.go +++ b/gopls/internal/span/span_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) var ( diff --git a/internal/span/token.go b/gopls/internal/span/token.go similarity index 100% rename from internal/span/token.go rename to gopls/internal/span/token.go diff --git a/internal/span/token_test.go b/gopls/internal/span/token_test.go similarity index 97% rename from internal/span/token_test.go rename to gopls/internal/span/token_test.go index 1e0b53e1244..997c8fb53ef 100644 --- a/internal/span/token_test.go +++ b/gopls/internal/span/token_test.go @@ -10,7 +10,7 @@ import ( "path" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) var testdata = []struct { diff --git a/internal/span/uri.go b/gopls/internal/span/uri.go similarity index 100% rename from internal/span/uri.go rename to gopls/internal/span/uri.go diff --git a/internal/span/uri_test.go b/gopls/internal/span/uri_test.go similarity index 98% rename from internal/span/uri_test.go rename to gopls/internal/span/uri_test.go index bcbad87128e..e9904378504 100644 --- a/internal/span/uri_test.go +++ b/gopls/internal/span/uri_test.go @@ -10,7 +10,7 @@ package span_test import ( "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // TestURI tests the conversion between URIs and filenames. The test cases diff --git a/internal/span/uri_windows_test.go b/gopls/internal/span/uri_windows_test.go similarity index 98% rename from internal/span/uri_windows_test.go rename to gopls/internal/span/uri_windows_test.go index e50b58f1bb2..3891e0d3e77 100644 --- a/internal/span/uri_windows_test.go +++ b/gopls/internal/span/uri_windows_test.go @@ -10,7 +10,7 @@ package span_test import ( "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // TestURI tests the conversion between URIs and filenames. The test cases diff --git a/internal/span/utf16.go b/gopls/internal/span/utf16.go similarity index 100% rename from internal/span/utf16.go rename to gopls/internal/span/utf16.go diff --git a/internal/span/utf16_test.go b/gopls/internal/span/utf16_test.go similarity index 99% rename from internal/span/utf16_test.go rename to gopls/internal/span/utf16_test.go index 1eae7975bb4..5f75095dcf4 100644 --- a/internal/span/utf16_test.go +++ b/gopls/internal/span/utf16_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // The funny character below is 4 bytes long in UTF-8; two UTF-16 code points diff --git a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/vulntest/db.go index 674e920b5f2..511a47e1ba9 100644 --- a/gopls/internal/vulncheck/vulntest/db.go +++ b/gopls/internal/vulncheck/vulntest/db.go @@ -20,7 +20,7 @@ import ( "strings" "time" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/txtar" "golang.org/x/vuln/client" "golang.org/x/vuln/osv" diff --git a/gopls/test/debug/debug_test.go b/gopls/test/debug/debug_test.go index 9c6c0d6246e..9d5d6f0f12b 100644 --- a/gopls/test/debug/debug_test.go +++ b/gopls/test/debug/debug_test.go @@ -24,7 +24,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/debug" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) type tdata struct { diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index 34754077522..e23cb1d5305 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -120,9 +120,9 @@ func TestUnified(t *testing.T) { } func TestRegressionOld001(t *testing.T) { - a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n" - b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n" diffs := diff.Strings(a, b) got, err := diff.Apply(a, diffs) if err != nil { diff --git a/internal/diff/lcs/old_test.go b/internal/diff/lcs/old_test.go index 828a9d90e22..1d6047694f3 100644 --- a/internal/diff/lcs/old_test.go +++ b/internal/diff/lcs/old_test.go @@ -97,9 +97,9 @@ func TestSpecialOld(t *testing.T) { // needs lcs.fix } func TestRegressionOld001(t *testing.T) { - a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n" - b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/internal/span\"\n)\n" + b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n" for i := 1; i < len(b); i++ { diffs, lcs := Compute([]byte(a), []byte(b), int(i)) // 14 from gopls if !lcs.valid() { From a410e98a82e107495b06cf1d1526d07de5b22be9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 7 Oct 2022 08:26:53 -0400 Subject: [PATCH 315/723] internal/diff: ToUnified may fail LineEdits has similar consistency preconditions to ApplyEdits. Previously they were assumed, and bad input would create bad output or crashes; now it uses the same validation logic as ApplyEdits. Since it reports an error, computation of a unified diff can also fail if the edits are inconsistent. The ToUnified([]Edit) function now returns an error. For convenience we also provide a wrapper (Unified) that cannot fail since it calls Strings and ToUnified consistently. LineEdits itself is now private. Change-Id: I3780827f501d7d5c9665ec8be5656331c0dcda8e Reviewed-on: https://go-review.googlesource.com/c/tools/+/440175 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan --- go/analysis/analysistest/analysistest.go | 12 ++--- gopls/api-diff/api_diff.go | 8 ++-- gopls/internal/lsp/cache/parse.go | 3 +- gopls/internal/lsp/cmd/format.go | 7 ++- gopls/internal/lsp/cmd/imports.go | 7 ++- gopls/internal/lsp/cmd/rename.go | 7 ++- gopls/internal/lsp/cmd/suggested_fix.go | 5 +- gopls/internal/lsp/cmd/test/imports.go | 4 +- gopls/internal/lsp/lsp_test.go | 6 +-- gopls/internal/lsp/source/source_test.go | 6 +-- gopls/internal/lsp/tests/compare/text.go | 10 ++-- gopls/internal/lsp/tests/util.go | 20 ++++---- internal/diff/diff.go | 60 +++++++++++++++--------- internal/diff/diff_test.go | 22 +++++---- internal/diff/difftest/difftest.go | 5 +- internal/diff/unified.go | 37 ++++++++++++--- 16 files changed, 131 insertions(+), 88 deletions(-) diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index 14140be14b0..abef7b44729 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -198,9 +198,9 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns t.Errorf("%s: error formatting edited source: %v\n%s", file.Name(), err, out) continue } - if want != string(formatted) { - edits := diff.Strings(want, string(formatted)) - t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.Unified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, edits)) + if got := string(formatted); got != want { + unified := diff.Unified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, got) + t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), unified) } break } @@ -229,9 +229,9 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns t.Errorf("%s: error formatting resulting source: %v\n%s", file.Name(), err, out) continue } - if want != string(formatted) { - edits := diff.Strings(want, string(formatted)) - t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.Unified(file.Name()+".golden", "actual", want, edits)) + if got := string(formatted); got != want { + unified := diff.Unified(file.Name()+".golden", "actual", want, got) + t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), unified) } } } diff --git a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go index 0fabae11a02..f39feaa6107 100644 --- a/gopls/api-diff/api_diff.go +++ b/gopls/api-diff/api_diff.go @@ -256,12 +256,10 @@ func formatBlock(str string) string { } func diffStr(before, after string) string { - // Add newlines to avoid newline messages in diff. if before == after { return "" } - before += "\n" - after += "\n" - edits := diffpkg.Strings(before, after) - return fmt.Sprintf("%q", diffpkg.Unified("previous", "current", before, edits)) + // Add newlines to avoid newline messages in diff. + unified := diffpkg.Unified("previous", "current", before+"\n", after+"\n") + return fmt.Sprintf("%q", unified) } diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index 1a096c42bbf..5bac8713153 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -157,8 +157,7 @@ func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, // it is likely we got stuck in a loop somehow. Log out a diff // of the last changes we made to aid in debugging. if i == 9 { - edits := diff.Bytes(src, newSrc) - unified := diff.Unified("before", "after", string(src), edits) + unified := diff.Unified("before", "after", string(src), string(newSrc)) event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name())) } diff --git a/gopls/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go index 2f3b6828d61..1ad9614b476 100644 --- a/gopls/internal/lsp/cmd/format.go +++ b/gopls/internal/lsp/cmd/format.go @@ -95,8 +95,11 @@ func (c *format) Run(ctx context.Context, args ...string) error { } if c.Diff { printIt = false - u := diff.Unified(filename+".orig", filename, string(file.mapper.Content), sedits) - fmt.Print(u) + unified, err := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + if err != nil { + return err + } + fmt.Print(unified) } if printIt { fmt.Print(formatted) diff --git a/gopls/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go index c041eeee6dc..5b741739421 100644 --- a/gopls/internal/lsp/cmd/imports.go +++ b/gopls/internal/lsp/cmd/imports.go @@ -92,8 +92,11 @@ func (t *imports) Run(ctx context.Context, args ...string) error { ioutil.WriteFile(filename, []byte(newContent), 0644) } case t.Diff: - diffs := diff.Unified(filename+".orig", filename, string(file.mapper.Content), sedits) - fmt.Print(diffs) + unified, err := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + if err != nil { + return err + } + fmt.Print(unified) default: fmt.Print(string(newContent)) } diff --git a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go index ceca7347c59..48c67e3d30d 100644 --- a/gopls/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -110,8 +110,11 @@ func (r *rename) Run(ctx context.Context, args ...string) error { } ioutil.WriteFile(filename, []byte(newContent), 0644) case r.Diff: - diffs := diff.Unified(filename+".orig", filename, string(cmdFile.mapper.Content), renameEdits) - fmt.Print(diffs) + unified, err := diff.ToUnified(filename+".orig", filename, string(cmdFile.mapper.Content), renameEdits) + if err != nil { + return err + } + fmt.Print(unified) default: if len(orderedURIs) > 1 { fmt.Printf("%s:\n", filepath.Base(filename)) diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go index 6d42a670643..082e0481007 100644 --- a/gopls/internal/lsp/cmd/suggested_fix.go +++ b/gopls/internal/lsp/cmd/suggested_fix.go @@ -154,7 +154,10 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { ioutil.WriteFile(filename, []byte(newContent), 0644) } case s.Diff: - diffs := diff.Unified(filename+".orig", filename, string(file.mapper.Content), sedits) + diffs, err := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + if err != nil { + return err + } fmt.Print(diffs) default: fmt.Print(string(newContent)) diff --git a/gopls/internal/lsp/cmd/test/imports.go b/gopls/internal/lsp/cmd/test/imports.go index 5659291faa3..d26c88664e2 100644 --- a/gopls/internal/lsp/cmd/test/imports.go +++ b/gopls/internal/lsp/cmd/test/imports.go @@ -19,7 +19,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) { return []byte(got), nil })) if want != got { - edits := diff.Strings(want, got) - t.Errorf("imports failed for %s, expected:\n%s", filename, diff.Unified("want", "got", want, edits)) + unified := diff.Unified("want", "got", want, got) + t.Errorf("imports failed for %s, expected:\n%s", filename, unified) } } diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index ce1aac49510..3969e88216c 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -1223,11 +1223,7 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa if got == nil { t.Fatalf("expected %v, got nil", want) } - diff, err := tests.DiffSignatures(spn, want, got) - if err != nil { - t.Fatal(err) - } - if diff != "" { + if diff := tests.DiffSignatures(spn, want, got); diff != "" { t.Error(diff) } } diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 8c49306f5cc..d7dc77c6a1c 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -844,11 +844,7 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa Signatures: []protocol.SignatureInformation{*gotSignature}, ActiveParameter: uint32(gotActiveParameter), } - diff, err := tests.DiffSignatures(spn, want, got) - if err != nil { - t.Fatal(err) - } - if diff != "" { + if diff := tests.DiffSignatures(spn, want, got); diff != "" { t.Error(diff) } } diff --git a/gopls/internal/lsp/tests/compare/text.go b/gopls/internal/lsp/tests/compare/text.go index 0563fcd06f3..9521496feec 100644 --- a/gopls/internal/lsp/tests/compare/text.go +++ b/gopls/internal/lsp/tests/compare/text.go @@ -19,19 +19,15 @@ func Text(want, got string) string { } // Add newlines to avoid verbose newline messages ("No newline at end of file"). - want += "\n" - got += "\n" - - edits := diff.Strings(want, got) - diff := diff.Unified("want", "got", want, edits) + unified := diff.Unified("want", "got", want+"\n", got+"\n") // Defensively assert that we get an actual diff, so that we guarantee the // invariant that we return "" if and only if want == got. // // This is probably unnecessary, but convenient. - if diff == "" { + if unified == "" { panic("empty diff for non-identical input") } - return diff + return unified } diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index 2951f037dac..a90093a01cf 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -20,8 +20,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/completion" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/diff" ) // DiffLinks takes the links we got and checks if they are located within the source or a Note. @@ -213,25 +213,23 @@ func summarizeCodeLens(i int, uri span.URI, want, got []protocol.CodeLens, reaso return msg.String() } -func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) (string, error) { +func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) string { decorate := func(f string, args ...interface{}) string { return fmt.Sprintf("invalid signature at %s: %s", spn, fmt.Sprintf(f, args...)) } if len(got.Signatures) != 1 { - return decorate("wanted 1 signature, got %d", len(got.Signatures)), nil + return decorate("wanted 1 signature, got %d", len(got.Signatures)) } if got.ActiveSignature != 0 { - return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)), nil + return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)) } if want.ActiveParameter != got.ActiveParameter { - return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)), nil + return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)) } g := got.Signatures[0] w := want.Signatures[0] - if NormalizeAny(w.Label) != NormalizeAny(g.Label) { - wLabel := w.Label + "\n" - edits := diff.Strings(wLabel, g.Label+"\n") - return decorate("mismatched labels:\n%q", diff.Unified("want", "got", wLabel, edits)), nil + if diff := compare.Text(NormalizeAny(w.Label), NormalizeAny(g.Label)); diff != "" { + return decorate("mismatched labels:\n%s", diff) } var paramParts []string for _, p := range g.Parameters { @@ -239,9 +237,9 @@ func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) (string, e } paramsStr := strings.Join(paramParts, ", ") if !strings.Contains(g.Label, paramsStr) { - return decorate("expected signature %q to contain params %q", g.Label, paramsStr), nil + return decorate("expected signature %q to contain params %q", g.Label, paramsStr) } - return "", nil + return "" } // NormalizeAny replaces occurrences of interface{} in input with any. diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 6cb39da73e2..794e6b793df 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -24,28 +24,14 @@ type Edit struct { // Apply returns an error if any edit is out of bounds, // or if any pair of edits is overlapping. func Apply(src string, edits []Edit) (string, error) { - if !sort.IsSorted(editsSort(edits)) { - edits = append([]Edit(nil), edits...) - sortEdits(edits) - } - - // Check validity of edits and compute final size. - size := len(src) - lastEnd := 0 - for _, edit := range edits { - if !(0 <= edit.Start && edit.Start <= edit.End && edit.End <= len(src)) { - return "", fmt.Errorf("diff has out-of-bounds edits") - } - if edit.Start < lastEnd { - return "", fmt.Errorf("diff has overlapping edits") - } - size += len(edit.New) + edit.Start - edit.End - lastEnd = edit.End + edits, size, err := validate(src, edits) + if err != nil { + return "", err } // Apply edits. out := make([]byte, 0, size) - lastEnd = 0 + lastEnd := 0 for _, edit := range edits { if lastEnd < edit.Start { out = append(out, src[lastEnd:edit.Start]...) @@ -62,6 +48,32 @@ func Apply(src string, edits []Edit) (string, error) { return string(out), nil } +// validate checks that edits are consistent with src, +// and returns the size of the patched output. +// It may return a different slice. +func validate(src string, edits []Edit) ([]Edit, int, error) { + if !sort.IsSorted(editsSort(edits)) { + edits = append([]Edit(nil), edits...) + sortEdits(edits) + } + + // Check validity of edits and compute final size. + size := len(src) + lastEnd := 0 + for _, edit := range edits { + if !(0 <= edit.Start && edit.Start <= edit.End && edit.End <= len(src)) { + return nil, 0, fmt.Errorf("diff has out-of-bounds edits") + } + if edit.Start < lastEnd { + return nil, 0, fmt.Errorf("diff has overlapping edits") + } + size += len(edit.New) + edit.Start - edit.End + lastEnd = edit.End + } + + return edits, size, nil +} + // sortEdits orders edits by (start, end) offset. // This ordering puts insertions (end=start) before deletions // (end>start) at the same point, but uses a stable sort to preserve @@ -84,8 +96,12 @@ func (a editsSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } // lineEdits expands and merges a sequence of edits so that each // resulting edit replaces one or more complete lines. -func lineEdits(src string, edits []Edit) []Edit { - sortEdits(edits) // TODO(adonovan): is this necessary? Move burden to caller? +// See ApplyEdits for preconditions. +func lineEdits(src string, edits []Edit) ([]Edit, error) { + edits, _, err := validate(src, edits) + if err != nil { + return nil, err + } // Do all edits begin and end at the start of a line? // TODO(adonovan): opt: is this fast path necessary? @@ -97,7 +113,7 @@ func lineEdits(src string, edits []Edit) []Edit { goto expand } } - return edits // aligned + return edits, nil // aligned expand: expanded := make([]Edit, 0, len(edits)) // a guess @@ -116,7 +132,7 @@ expand: prev = edit } } - return append(expanded, expandEdit(prev, src)) // flush final edit + return append(expanded, expandEdit(prev, src)), nil // flush final edit } // expandEdit returns edit expanded to complete whole lines. diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index e23cb1d5305..7bb41a27bfa 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -95,22 +95,32 @@ func TestLineEdits(t *testing.T) { if edits == nil { edits = tc.Edits } - if got := diff.LineEdits(tc.In, tc.Edits); diffEdits(got, edits) { + got, err := diff.LineEdits(tc.In, tc.Edits) + if err != nil { + t.Fatalf("LineEdits: %v", err) + } + if !reflect.DeepEqual(got, edits) { t.Errorf("LineEdits got %q, want %q", got, edits) } }) } } -func TestUnified(t *testing.T) { +func TestToUnified(t *testing.T) { for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { - unified := diff.Unified(difftest.FileA, difftest.FileB, tc.In, tc.Edits) + unified, err := diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.Edits) + if err != nil { + t.Fatal(err) + } if unified != tc.Unified { t.Errorf("Unified(Edits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) } if tc.LineEdits != nil { - unified := diff.Unified(difftest.FileA, difftest.FileB, tc.In, tc.LineEdits) + unified, err := diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.LineEdits) + if err != nil { + t.Fatal(err) + } if unified != tc.Unified { t.Errorf("Unified(LineEdits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) } @@ -154,10 +164,6 @@ func TestRegressionOld002(t *testing.T) { } } -func diffEdits(got, want []diff.Edit) bool { - return !reflect.DeepEqual(got, want) -} - // return a random string of length n made of characters from s func randstr(s string, n int) string { src := []rune(s) diff --git a/internal/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go index 5c0a7413670..cd026bee490 100644 --- a/internal/diff/difftest/difftest.go +++ b/internal/diff/difftest/difftest.go @@ -249,7 +249,10 @@ func DiffTest(t *testing.T, compute func(before, after string) []diff.Edit) { if err != nil { t.Fatalf("Apply failed: %v", err) } - unified := diff.Unified(FileA, FileB, test.In, edits) + unified, err := diff.ToUnified(FileA, FileB, test.In, edits) + if err != nil { + t.Fatalf("ToUnified: %v", err) + } if got != test.Out { t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out) } diff --git a/internal/diff/unified.go b/internal/diff/unified.go index d71b500f067..fa376f17872 100644 --- a/internal/diff/unified.go +++ b/internal/diff/unified.go @@ -6,13 +6,32 @@ package diff import ( "fmt" + "log" "strings" ) -// Unified applies the edits to content and presents a unified diff. +// Unified returns a unified diff of the old and new strings. +// The old and new labels are the names of the old and new files. +// If the strings are equal, it returns the empty string. +func Unified(oldLabel, newLabel, old, new string) string { + edits := Strings(old, new) + unified, err := ToUnified(oldLabel, newLabel, old, edits) + if err != nil { + // Can't happen: edits are consistent. + log.Fatalf("internal error in diff.Unified: %v", err) + } + return unified +} + +// ToUnified applies the edits to content and returns a unified diff. // The old and new labels are the names of the content and result files. -func Unified(oldLabel, newLabel string, content string, edits []Edit) string { - return toUnified(oldLabel, newLabel, content, edits).String() +// It returns an error if the edits are inconsistent; see ApplyEdits. +func ToUnified(oldLabel, newLabel, content string, edits []Edit) (string, error) { + u, err := toUnified(oldLabel, newLabel, content, edits) + if err != nil { + return "", err + } + return u.String(), nil } // unified represents a set of edits as a unified diff. @@ -82,15 +101,19 @@ const ( // toUnified takes a file contents and a sequence of edits, and calculates // a unified diff that represents those edits. -func toUnified(fromName, toName string, content string, edits []Edit) unified { +func toUnified(fromName, toName string, content string, edits []Edit) (unified, error) { u := unified{ From: fromName, To: toName, } if len(edits) == 0 { - return u + return u, nil + } + var err error + edits, err = lineEdits(content, edits) // expand to whole lines + if err != nil { + return u, err } - edits = lineEdits(content, edits) // expand to whole lines lines := splitLines(content) var h *hunk last := 0 @@ -144,7 +167,7 @@ func toUnified(fromName, toName string, content string, edits []Edit) unified { addEqualLines(h, lines, last, last+edge) u.Hunks = append(u.Hunks, h) } - return u + return u, nil } func splitLines(text string) []string { From 709f10829c8f252180216567737887d1ece8cd61 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 7 Oct 2022 11:10:24 -0400 Subject: [PATCH 316/723] gopls/internal/lsp/cache: suppress crashes in fact_purity analyzer It has a known bug, and the purpose of the PanicOnBugs is to find unknown bugs. Updates golang/go#54762 Updates golang/go#56035 Change-Id: Id9fdfff478ef52b1d864acee366991808baeb574 Reviewed-on: https://go-review.googlesource.com/c/tools/+/440178 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/cache/analysis.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 95bac5b49eb..c0ee811284d 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -365,8 +365,10 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a if r := recover(); r != nil { // An Analyzer crashed. This is often merely a symptom // of a problem in package loading. - if bug.PanicOnBugs { + if bug.PanicOnBugs && analyzer.Name != "fact_purity" { // During testing, crash. See issues 54762, 56035. + // But ignore analyzers with known crash bugs: + // - fact_purity (dominikh/go-tools#1327) debug.SetTraceback("all") // show all goroutines panic(r) } else { From 20c1ee70e43bf43961e2ab670291f649bb66e6b0 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 28 Sep 2022 13:10:58 -0400 Subject: [PATCH 317/723] gopls/internal/lsp: warn about Go versions that are too old Add a showMessage notification when the Go version in PATH is too old. Also delete the unused View.Rebuild method. Updates golang/go#50825 Change-Id: I279a04f021a0f8ddb09fcfe299fbab8d10e8c022 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439836 Run-TryBot: Robert Findley Auto-Submit: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/cache/view.go | 13 +--- gopls/internal/lsp/general.go | 29 +++++++ gopls/internal/lsp/regtest/expectation.go | 14 ++-- gopls/internal/lsp/source/view.go | 8 +- gopls/internal/lsp/workspace.go | 3 + .../regtest/diagnostics/diagnostics_test.go | 11 ++- .../regtest/workspace/workspace_test.go | 77 +++++++++++++++++++ 7 files changed, 134 insertions(+), 21 deletions(-) diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 7d3cba3edae..6244d909c4a 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -317,15 +317,6 @@ func (v *View) SetOptions(ctx context.Context, options *source.Options) (source. return newView, err } -func (v *View) Rebuild(ctx context.Context) (source.Snapshot, func(), error) { - newView, err := v.session.updateView(ctx, v, v.Options()) - if err != nil { - return nil, func() {}, err - } - snapshot, release := newView.Snapshot(ctx) - return snapshot, release, nil -} - func (s *snapshot) WriteEnv(ctx context.Context, w io.Writer) error { s.view.optionsMu.Lock() env := s.view.options.EnvSlice() @@ -1057,6 +1048,10 @@ func (v *View) SetVulnerabilities(modfile span.URI, vulns []command.Vuln) { v.vulns[modfile] = vulns } +func (v *View) GoVersion() int { + return v.workspaceInformation.goversion +} + // Copied from // https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/str/path.go;l=58;drc=2910c5b4a01a573ebc97744890a07c1a3122c67a func globsMatchPath(globs, target string) bool { diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index eee946ed020..839af1fec73 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -202,6 +202,7 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa return err } s.pendingFolders = nil + s.checkViewGoVersions() var registrations []protocol.Registration if options.ConfigurationSupported && options.DynamicConfigurationSupported { @@ -223,6 +224,34 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa return nil } +// OldestSupportedGoVersion is the last X in Go 1.X that we support. +// +// Mutable for testing, since we won't otherwise run CI on unsupported Go +// versions. +var OldestSupportedGoVersion = 16 + +// checkViewGoVersions checks whether any Go version used by a view is too old, +// raising a showMessage notification if so. +// +// It should be called after views change. +func (s *Server) checkViewGoVersions() { + oldestVersion := -1 + for _, view := range s.session.Views() { + viewVersion := view.GoVersion() + if oldestVersion == -1 || viewVersion < oldestVersion { + oldestVersion = viewVersion + } + } + + if oldestVersion >= 0 && oldestVersion < OldestSupportedGoVersion { + msg := fmt.Sprintf("Found Go version 1.%d, which is unsupported. Please upgrade to Go 1.%d or later.", oldestVersion, OldestSupportedGoVersion) + s.eventuallyShowMessage(context.Background(), &protocol.ShowMessageParams{ + Type: protocol.Error, + Message: msg, + }) + } +} + func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) error { originalViews := len(s.session.Views()) viewErrors := make(map[span.URI]error) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index eddd8136fd9..335a46fc589 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -162,17 +162,19 @@ func NoOutstandingWork() SimpleExpectation { } } -// NoShowMessage asserts that the editor has not received a ShowMessage. -func NoShowMessage() SimpleExpectation { +// NoShownMessage asserts that the editor has not received a ShowMessage. +func NoShownMessage(subString string) SimpleExpectation { check := func(s State) Verdict { - if len(s.showMessage) == 0 { - return Met + for _, m := range s.showMessage { + if strings.Contains(m.Message, subString) { + return Unmeetable + } } - return Unmeetable + return Met } return SimpleExpectation{ check: check, - description: "no ShowMessage received", + description: fmt.Sprintf("no ShowMessage received containing %q", subString), } } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index e679839cfd8..bf152805966 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -253,11 +253,6 @@ type View interface { // no longer needed. Snapshot(ctx context.Context) (Snapshot, func()) - // Rebuild rebuilds the current view, replacing the original - // view in its session. It returns a Snapshot and a release - // function that must be called when the Snapshot is no longer needed. - Rebuild(ctx context.Context) (Snapshot, func(), error) - // IsGoPrivatePath reports whether target is a private import path, as identified // by the GOPRIVATE environment variable. IsGoPrivatePath(path string) bool @@ -284,6 +279,9 @@ type View interface { // FileKind returns the type of a file FileKind(FileHandle) FileKind + + // GoVersion returns the configured Go version for this view. + GoVersion() int } // A FileSource maps uris to FileHandles. This abstraction exists both for diff --git a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go index 0e780d23680..4f9948ec9d3 100644 --- a/gopls/internal/lsp/workspace.go +++ b/gopls/internal/lsp/workspace.go @@ -69,6 +69,9 @@ func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChan }() } + // An options change may have affected the detected Go version. + s.checkViewGoVersions() + registration := semanticTokenRegistration(options.SemanticTypes, options.SemanticMods) // Update any session-specific registrations or unregistrations. if !semanticTokensRegistered && options.SemanticTokens { diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index f12bdbae759..72fe3cae381 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1356,6 +1356,10 @@ func _() { } func TestEnableAllExperiments(t *testing.T) { + // Before the oldest supported Go version, gopls sends a warning to upgrade + // Go, which fails the expectation below. + testenv.NeedsGo1Point(t, lsp.OldestSupportedGoVersion) + const mod = ` -- go.mod -- module mod.com @@ -1374,7 +1378,12 @@ func b(c bytes.Buffer) { Settings{"allExperiments": true}, ).Run(t, mod, func(t *testing.T, env *Env) { // Confirm that the setting doesn't cause any warnings. - env.Await(NoShowMessage()) + env.Await( + OnceMet( + InitialWorkspaceLoad, + NoShownMessage(""), // empty substring to match any message + ), + ) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 5d3053c2851..4738c0f489a 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -5,15 +5,18 @@ package workspace import ( + "context" "fmt" "path/filepath" "strings" "testing" "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/gopls/internal/lsp" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/testenv" . "golang.org/x/tools/gopls/internal/lsp/regtest" @@ -1234,3 +1237,77 @@ import ( ) }) } + +// Test that we don't get a version warning when the Go version in PATH is +// supported. +func TestOldGoNotification_SupportedVersion(t *testing.T) { + v := goVersion(t) + if v < lsp.OldestSupportedGoVersion { + t.Skipf("go version 1.%d is unsupported", v) + } + + Run(t, "", func(t *testing.T, env *Env) { + env.Await( + OnceMet( + InitialWorkspaceLoad, + NoShownMessage("upgrade"), + ), + ) + }) +} + +// Test that we do get a version warning when the Go version in PATH is +// unsupported, though this test may never execute if we stop running CI at +// legacy Go versions (see also TestOldGoNotification_Fake) +func TestOldGoNotification_UnsupportedVersion(t *testing.T) { + v := goVersion(t) + if v >= lsp.OldestSupportedGoVersion { + t.Skipf("go version 1.%d is supported", v) + } + + Run(t, "", func(t *testing.T, env *Env) { + env.Await( + OnceMet( + InitialWorkspaceLoad, + ShownMessage("Please upgrade"), + ), + ) + }) +} + +func TestOldGoNotification_Fake(t *testing.T) { + // Get the Go version from path, and make sure it's unsupported. + // + // In the future we'll stop running CI on legacy Go versions. By mutating the + // oldest supported Go version here, we can at least ensure that the + // ShowMessage pop-up works. + ctx := context.Background() + goversion, err := gocommand.GoVersion(ctx, gocommand.Invocation{}, &gocommand.Runner{}) + if err != nil { + t.Fatal(err) + } + defer func(v int) { + lsp.OldestSupportedGoVersion = v + }(lsp.OldestSupportedGoVersion) + lsp.OldestSupportedGoVersion = goversion + 1 + + Run(t, "", func(t *testing.T, env *Env) { + env.Await( + OnceMet( + InitialWorkspaceLoad, + ShownMessage("Please upgrade"), + ), + ) + }) +} + +// goVersion returns the version of the Go command in PATH. +func goVersion(t *testing.T) int { + t.Helper() + ctx := context.Background() + goversion, err := gocommand.GoVersion(ctx, gocommand.Invocation{}, &gocommand.Runner{}) + if err != nil { + t.Fatal(err) + } + return goversion +} From 8b1d1ec39ce51ddb43a0bd32b6ed1fba985b8a6c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 7 Oct 2022 13:31:37 -0400 Subject: [PATCH 318/723] gopls/internal/lsp/cache: suppress panics during analysis We now have a plausible model for the cause of the analysis crashes and are disabling this check until we have submitted a plausible fix. (Specifically: actionHandle.promise may be a stale promise for an older package with the same ID, since snapshot.actions uses the package ID as a key. This causes us to re-use stale inspectors, whose syntax nodes won't be found in TypesInfo.) Updates golang/go#54762 Updates golang/go#56035 Change-Id: I1a7cc1b02b8c322692b1a6bee03f6cd858c08ea0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/440179 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Bryan Mills Auto-Submit: Alan Donovan --- gopls/internal/lsp/cache/analysis.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index c0ee811284d..944485c3cd8 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -365,7 +365,11 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a if r := recover(); r != nil { // An Analyzer crashed. This is often merely a symptom // of a problem in package loading. - if bug.PanicOnBugs && analyzer.Name != "fact_purity" { + // Now that we have a theory of these crashes, + // we disable the check to stop flakes from being a nuisance. + // TODO(adonovan): re-enable it when plausibly fixed. + const strict = false + if strict && bug.PanicOnBugs && analyzer.Name != "fact_purity" { // During testing, crash. See issues 54762, 56035. // But ignore analyzers with known crash bugs: // - fact_purity (dominikh/go-tools#1327) From 350f1e2c0a06ff80ae6211c472800ddef8a7333a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 7 Oct 2022 14:53:49 -0400 Subject: [PATCH 319/723] gopls/internal/lsp/fake: retry ephemeral errors when renaming on windows Investigation of renaming flakes revealed that renaming is known to be flaky on windows, and the go command has a robustio package that works around known flakes for certain IO operations on darwin and windows. Rather than duplicate this logic piecemeal, copy the entire robustio package to the gopls module, along with a script to sync it from GOROOT using go generate. Use this new package to de-flake renaming, and replace an existing workaround. The copy script got a little out of hand at the point where I needed to add +build constraints. Nevertheless, I've decided to keep it with the caveat that it may be removed if it proves too difficult to maintain. As is, it at least serves as documentation for how the sync was done. For golang/go#56040 For golang/go#56039 For golang/go#56038 For golang/go#55324 Change-Id: Ifeda408ac44a2866e84015a2a38ae340dc0a88bb Reviewed-on: https://go-review.googlesource.com/c/tools/+/440181 Run-TryBot: Robert Findley Reviewed-by: Bryan Mills TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/fake/editor.go | 5 +- gopls/internal/lsp/fake/sandbox.go | 32 +----- gopls/internal/lsp/fake/workdir.go | 9 +- gopls/internal/lsp/fake/workdir_windows.go | 21 +--- gopls/internal/robustio/copyfiles.go | 117 ++++++++++++++++++++ gopls/internal/robustio/gopls.go | 16 +++ gopls/internal/robustio/robustio.go | 53 +++++++++ gopls/internal/robustio/robustio_darwin.go | 21 ++++ gopls/internal/robustio/robustio_flaky.go | 93 ++++++++++++++++ gopls/internal/robustio/robustio_other.go | 29 +++++ gopls/internal/robustio/robustio_windows.go | 26 +++++ 11 files changed, 366 insertions(+), 56 deletions(-) create mode 100644 gopls/internal/robustio/copyfiles.go create mode 100644 gopls/internal/robustio/gopls.go create mode 100644 gopls/internal/robustio/robustio.go create mode 100644 gopls/internal/robustio/robustio_darwin.go create mode 100644 gopls/internal/robustio/robustio_flaky.go create mode 100644 gopls/internal/robustio/robustio_other.go create mode 100644 gopls/internal/robustio/robustio_windows.go diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index d9e6c701e65..8d1362a50fb 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -1203,7 +1203,10 @@ func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error } // Finally, perform the renaming on disk. - return e.sandbox.Workdir.RenameFile(ctx, oldPath, newPath) + if err := e.sandbox.Workdir.RenameFile(ctx, oldPath, newPath); err != nil { + return fmt.Errorf("renaming sandbox file: %w", err) + } + return nil } // renameBuffers renames in-memory buffers affected by the renaming of diff --git a/gopls/internal/lsp/fake/sandbox.go b/gopls/internal/lsp/fake/sandbox.go index e0e113f4914..206a3de1ba6 100644 --- a/gopls/internal/lsp/fake/sandbox.go +++ b/gopls/internal/lsp/fake/sandbox.go @@ -9,12 +9,11 @@ import ( "errors" "fmt" "io/ioutil" - "math/rand" "os" "path/filepath" "strings" - "time" + "golang.org/x/tools/gopls/internal/robustio" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" @@ -289,36 +288,9 @@ func (sb *Sandbox) Close() error { if sb.gopath != "" { goCleanErr = sb.RunGoCommand(context.Background(), "", "clean", []string{"-modcache"}, false) } - err := removeAll(sb.rootdir) + err := robustio.RemoveAll(sb.rootdir) if err != nil || goCleanErr != nil { return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err) } return nil } - -// removeAll is copied from GOROOT/src/testing/testing.go -// -// removeAll is like os.RemoveAll, but retries Windows "Access is denied." -// errors up to an arbitrary timeout. -// -// See https://go.dev/issue/50051 for additional context. -func removeAll(path string) error { - const arbitraryTimeout = 2 * time.Second - var ( - start time.Time - nextSleep = 1 * time.Millisecond - ) - for { - err := os.RemoveAll(path) - if !isWindowsRetryable(err) { - return err - } - if start.IsZero() { - start = time.Now() - } else if d := time.Since(start) + nextSleep; d >= arbitraryTimeout { - return err - } - time.Sleep(nextSleep) - nextSleep += time.Duration(rand.Int63n(int64(nextSleep))) - } -} diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index 3f17cb68f1a..f194e263fdd 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -18,6 +18,7 @@ import ( "time" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/robustio" "golang.org/x/tools/gopls/internal/span" ) @@ -62,6 +63,8 @@ func WriteFileData(path string, content []byte, rel RelativeTo) error { for { err := ioutil.WriteFile(fp, []byte(content), 0644) if err != nil { + // This lock file violation is not handled by the robustio package, as it + // indicates a real race condition that could be avoided. if isWindowsErrLockViolation(err) { time.Sleep(backoff) backoff *= 2 @@ -77,10 +80,6 @@ func WriteFileData(path string, content []byte, rel RelativeTo) error { // on Windows. var isWindowsErrLockViolation = func(err error) bool { return false } -// isWindowsRetryable reports whether err is a Windows error code -// that may be fixed by retrying a failed filesystem operation. -var isWindowsRetryable = func(err error) bool { return false } - // Workdir is a temporary working directory for tests. It exposes file // operations in terms of relative paths, and fakes file watching by triggering // events on file operations. @@ -334,7 +333,7 @@ func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error oldAbs := w.AbsPath(oldPath) newAbs := w.AbsPath(newPath) - if err := os.Rename(oldAbs, newAbs); err != nil { + if err := robustio.Rename(oldAbs, newAbs); err != nil { return err } diff --git a/gopls/internal/lsp/fake/workdir_windows.go b/gopls/internal/lsp/fake/workdir_windows.go index fc5ad1a89af..4d4f0152764 100644 --- a/gopls/internal/lsp/fake/workdir_windows.go +++ b/gopls/internal/lsp/fake/workdir_windows.go @@ -12,29 +12,10 @@ import ( func init() { // constants copied from GOROOT/src/internal/syscall/windows/syscall_windows.go const ( - ERROR_SHARING_VIOLATION syscall.Errno = 32 - ERROR_LOCK_VIOLATION syscall.Errno = 33 + ERROR_LOCK_VIOLATION syscall.Errno = 33 ) isWindowsErrLockViolation = func(err error) bool { return errors.Is(err, ERROR_LOCK_VIOLATION) } - - // Copied from GOROOT/src/testing/testing_windows.go - isWindowsRetryable = func(err error) bool { - for { - unwrapped := errors.Unwrap(err) - if unwrapped == nil { - break - } - err = unwrapped - } - if err == syscall.ERROR_ACCESS_DENIED { - return true // Observed in https://go.dev/issue/50051. - } - if err == ERROR_SHARING_VIOLATION { - return true // Observed in https://go.dev/issue/51442. - } - return false - } } diff --git a/gopls/internal/robustio/copyfiles.go b/gopls/internal/robustio/copyfiles.go new file mode 100644 index 00000000000..0b61a313e39 --- /dev/null +++ b/gopls/internal/robustio/copyfiles.go @@ -0,0 +1,117 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore +// +build ignore + +// The copyfiles script copies the contents of the internal cmd/go robustio +// package to the current directory, with adjustments to make it build. +// +// NOTE: In retrospect this script got out of hand, as we have to perform +// various operations on the package to get it to build at old Go versions. If +// in the future it proves to be flaky, delete it and just copy code manually. +package main + +import ( + "bytes" + "go/build/constraint" + "go/scanner" + "go/token" + "log" + "os" + "path/filepath" + "runtime" + "strings" +) + +func main() { + dir := filepath.Join(runtime.GOROOT(), "src", "cmd", "go", "internal", "robustio") + + entries, err := os.ReadDir(dir) + if err != nil { + log.Fatal("reading the robustio dir: %v", err) + } + + // Collect file content so that we can validate before copying. + fileContent := make(map[string][]byte) + windowsImport := []byte("\t\"internal/syscall/windows\"\n") + foundWindowsImport := false + for _, entry := range entries { + if strings.HasSuffix(entry.Name(), ".go") { + pth := filepath.Join(dir, entry.Name()) + content, err := os.ReadFile(pth) + if err != nil { + log.Fatalf("reading %q: %v", entry.Name(), err) + } + + // Replace the use of internal/syscall/windows.ERROR_SHARING_VIOLATION + // with a local constant. + if entry.Name() == "robustio_windows.go" && bytes.Contains(content, windowsImport) { + foundWindowsImport = true + content = bytes.Replace(content, windowsImport, nil, 1) + content = bytes.Replace(content, []byte("windows.ERROR_SHARING_VIOLATION"), []byte("ERROR_SHARING_VIOLATION"), -1) + } + + // Replace os.ReadFile with ioutil.ReadFile (for 1.15 and older). We + // attempt to match calls (via the '('), to avoid matching mentions of + // os.ReadFile in comments. + // + // TODO(rfindley): once we (shortly!) no longer support 1.15, remove + // this and break the build. + if bytes.Contains(content, []byte("os.ReadFile(")) { + content = bytes.Replace(content, []byte("\"os\""), []byte("\"io/ioutil\"\n\t\"os\""), 1) + content = bytes.Replace(content, []byte("os.ReadFile("), []byte("ioutil.ReadFile("), -1) + } + + // Add +build constraints, for 1.16. + content = addPlusBuildConstraints(content) + + fileContent[entry.Name()] = content + } + } + + if !foundWindowsImport { + log.Fatal("missing expected import of internal/syscall/windows in robustio_windows.go") + } + + for name, content := range fileContent { + if err := os.WriteFile(name, content, 0644); err != nil { + log.Fatalf("writing %q: %v", name, err) + } + } +} + +// addPlusBuildConstraints splices in +build constraints for go:build +// constraints encountered in the source. +// +// Gopls still builds at Go 1.16, which requires +build constraints. +func addPlusBuildConstraints(src []byte) []byte { + var s scanner.Scanner + fset := token.NewFileSet() + file := fset.AddFile("", fset.Base(), len(src)) + s.Init(file, src, nil /* no error handler */, scanner.ScanComments) + + result := make([]byte, 0, len(src)) + lastInsertion := 0 + for { + pos, tok, lit := s.Scan() + if tok == token.EOF { + break + } + if tok == token.COMMENT { + if c, err := constraint.Parse(lit); err == nil { + plusBuild, err := constraint.PlusBuildLines(c) + if err != nil { + log.Fatal("computing +build constraint for %q: %v", lit, err) + } + insertAt := file.Offset(pos) + len(lit) + result = append(result, src[lastInsertion:insertAt]...) + result = append(result, []byte("\n"+strings.Join(plusBuild, "\n"))...) + lastInsertion = insertAt + } + } + } + result = append(result, src[lastInsertion:]...) + return result +} diff --git a/gopls/internal/robustio/gopls.go b/gopls/internal/robustio/gopls.go new file mode 100644 index 00000000000..949f2781619 --- /dev/null +++ b/gopls/internal/robustio/gopls.go @@ -0,0 +1,16 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package robustio + +import "syscall" + +// The robustio package is copied from cmd/go/internal/robustio, a package used +// by the go command to retry known flaky operations on certain operating systems. + +//go:generate go run copyfiles.go + +// Since the gopls module cannot access internal/syscall/windows, copy a +// necessary constant. +const ERROR_SHARING_VIOLATION syscall.Errno = 32 diff --git a/gopls/internal/robustio/robustio.go b/gopls/internal/robustio/robustio.go new file mode 100644 index 00000000000..15b33773cf5 --- /dev/null +++ b/gopls/internal/robustio/robustio.go @@ -0,0 +1,53 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package robustio wraps I/O functions that are prone to failure on Windows, +// transparently retrying errors up to an arbitrary timeout. +// +// Errors are classified heuristically and retries are bounded, so the functions +// in this package do not completely eliminate spurious errors. However, they do +// significantly reduce the rate of failure in practice. +// +// If so, the error will likely wrap one of: +// The functions in this package do not completely eliminate spurious errors, +// but substantially reduce their rate of occurrence in practice. +package robustio + +// Rename is like os.Rename, but on Windows retries errors that may occur if the +// file is concurrently read or overwritten. +// +// (See golang.org/issue/31247 and golang.org/issue/32188.) +func Rename(oldpath, newpath string) error { + return rename(oldpath, newpath) +} + +// ReadFile is like os.ReadFile, but on Windows retries errors that may +// occur if the file is concurrently replaced. +// +// (See golang.org/issue/31247 and golang.org/issue/32188.) +func ReadFile(filename string) ([]byte, error) { + return readFile(filename) +} + +// RemoveAll is like os.RemoveAll, but on Windows retries errors that may occur +// if an executable file in the directory has recently been executed. +// +// (See golang.org/issue/19491.) +func RemoveAll(path string) error { + return removeAll(path) +} + +// IsEphemeralError reports whether err is one of the errors that the functions +// in this package attempt to mitigate. +// +// Errors considered ephemeral include: +// - syscall.ERROR_ACCESS_DENIED +// - syscall.ERROR_FILE_NOT_FOUND +// - internal/syscall/windows.ERROR_SHARING_VIOLATION +// +// This set may be expanded in the future; programs must not rely on the +// non-ephemerality of any given error. +func IsEphemeralError(err error) bool { + return isEphemeralError(err) +} diff --git a/gopls/internal/robustio/robustio_darwin.go b/gopls/internal/robustio/robustio_darwin.go new file mode 100644 index 00000000000..99fd8ebc2ff --- /dev/null +++ b/gopls/internal/robustio/robustio_darwin.go @@ -0,0 +1,21 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package robustio + +import ( + "errors" + "syscall" +) + +const errFileNotFound = syscall.ENOENT + +// isEphemeralError returns true if err may be resolved by waiting. +func isEphemeralError(err error) bool { + var errno syscall.Errno + if errors.As(err, &errno) { + return errno == errFileNotFound + } + return false +} diff --git a/gopls/internal/robustio/robustio_flaky.go b/gopls/internal/robustio/robustio_flaky.go new file mode 100644 index 00000000000..c6f99724468 --- /dev/null +++ b/gopls/internal/robustio/robustio_flaky.go @@ -0,0 +1,93 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows || darwin +// +build windows darwin + +package robustio + +import ( + "errors" + "io/ioutil" + "math/rand" + "os" + "syscall" + "time" +) + +const arbitraryTimeout = 2000 * time.Millisecond + +// retry retries ephemeral errors from f up to an arbitrary timeout +// to work around filesystem flakiness on Windows and Darwin. +func retry(f func() (err error, mayRetry bool)) error { + var ( + bestErr error + lowestErrno syscall.Errno + start time.Time + nextSleep time.Duration = 1 * time.Millisecond + ) + for { + err, mayRetry := f() + if err == nil || !mayRetry { + return err + } + + var errno syscall.Errno + if errors.As(err, &errno) && (lowestErrno == 0 || errno < lowestErrno) { + bestErr = err + lowestErrno = errno + } else if bestErr == nil { + bestErr = err + } + + if start.IsZero() { + start = time.Now() + } else if d := time.Since(start) + nextSleep; d >= arbitraryTimeout { + break + } + time.Sleep(nextSleep) + nextSleep += time.Duration(rand.Int63n(int64(nextSleep))) + } + + return bestErr +} + +// rename is like os.Rename, but retries ephemeral errors. +// +// On Windows it wraps os.Rename, which (as of 2019-06-04) uses MoveFileEx with +// MOVEFILE_REPLACE_EXISTING. +// +// Windows also provides a different system call, ReplaceFile, +// that provides similar semantics, but perhaps preserves more metadata. (The +// documentation on the differences between the two is very sparse.) +// +// Empirical error rates with MoveFileEx are lower under modest concurrency, so +// for now we're sticking with what the os package already provides. +func rename(oldpath, newpath string) (err error) { + return retry(func() (err error, mayRetry bool) { + err = os.Rename(oldpath, newpath) + return err, isEphemeralError(err) + }) +} + +// readFile is like os.ReadFile, but retries ephemeral errors. +func readFile(filename string) ([]byte, error) { + var b []byte + err := retry(func() (err error, mayRetry bool) { + b, err = ioutil.ReadFile(filename) + + // Unlike in rename, we do not retry errFileNotFound here: it can occur + // as a spurious error, but the file may also genuinely not exist, so the + // increase in robustness is probably not worth the extra latency. + return err, isEphemeralError(err) && !errors.Is(err, errFileNotFound) + }) + return b, err +} + +func removeAll(path string) error { + return retry(func() (err error, mayRetry bool) { + err = os.RemoveAll(path) + return err, isEphemeralError(err) + }) +} diff --git a/gopls/internal/robustio/robustio_other.go b/gopls/internal/robustio/robustio_other.go new file mode 100644 index 00000000000..c11dbf9f14b --- /dev/null +++ b/gopls/internal/robustio/robustio_other.go @@ -0,0 +1,29 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !windows && !darwin +// +build !windows,!darwin + +package robustio + +import ( + "io/ioutil" + "os" +) + +func rename(oldpath, newpath string) error { + return os.Rename(oldpath, newpath) +} + +func readFile(filename string) ([]byte, error) { + return ioutil.ReadFile(filename) +} + +func removeAll(path string) error { + return os.RemoveAll(path) +} + +func isEphemeralError(err error) bool { + return false +} diff --git a/gopls/internal/robustio/robustio_windows.go b/gopls/internal/robustio/robustio_windows.go new file mode 100644 index 00000000000..d16e976aad6 --- /dev/null +++ b/gopls/internal/robustio/robustio_windows.go @@ -0,0 +1,26 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package robustio + +import ( + "errors" + "syscall" +) + +const errFileNotFound = syscall.ERROR_FILE_NOT_FOUND + +// isEphemeralError returns true if err may be resolved by waiting. +func isEphemeralError(err error) bool { + var errno syscall.Errno + if errors.As(err, &errno) { + switch errno { + case syscall.ERROR_ACCESS_DENIED, + syscall.ERROR_FILE_NOT_FOUND, + ERROR_SHARING_VIOLATION: + return true + } + } + return false +} From f90d8ad46c2df369bcdbe66257bdcd940c98938b Mon Sep 17 00:00:00 2001 From: cui fliter Date: Mon, 10 Oct 2022 20:58:54 +0800 Subject: [PATCH 320/723] all: fix a few function names on comments Change-Id: I91eec68ebd9394685a6586c8d0cf01ecf3fd82e2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/441775 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Auto-Submit: Robert Findley gopls-CI: kokoro Reviewed-by: Robert Findley Reviewed-by: Hyang-Ah Hana Kim --- cmd/guru/guru.go | 2 +- go/buildutil/util.go | 2 +- go/ssa/dom.go | 2 +- gopls/internal/lsp/cache/load.go | 2 +- gopls/internal/lsp/lsppos/lsppos.go | 2 +- gopls/internal/lsp/lsppos/token.go | 2 +- gopls/internal/lsp/lsprpc/autostart_default.go | 2 +- gopls/internal/lsp/lsprpc/autostart_posix.go | 2 +- gopls/internal/lsp/source/extract.go | 4 ++-- gopls/internal/lsp/source/format.go | 2 +- gopls/internal/lsp/source/rename.go | 2 +- gopls/internal/span/uri.go | 2 +- internal/diff/ndiff.go | 2 +- present/args.go | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cmd/guru/guru.go b/cmd/guru/guru.go index fdb13f92932..18bb0840a1e 100644 --- a/cmd/guru/guru.go +++ b/cmd/guru/guru.go @@ -207,7 +207,7 @@ func pkgContainsFile(bp *build.Package, filename string) byte { return 0 // not found } -// ParseQueryPos parses the source query position pos and returns the +// parseQueryPos parses the source query position pos and returns the // AST node of the loaded program lprog that it identifies. // If needExact, it must identify a single AST subtree; // this is appropriate for queries that allow fairly arbitrary syntax, diff --git a/go/buildutil/util.go b/go/buildutil/util.go index d771b18e32d..bee6390de4c 100644 --- a/go/buildutil/util.go +++ b/go/buildutil/util.go @@ -80,7 +80,7 @@ func ContainingPackage(ctxt *build.Context, dir, filename string) (*build.Packag // (go/build.Context defines these as methods, but does not export them.) -// hasSubdir calls ctxt.HasSubdir (if not nil) or else uses +// HasSubdir calls ctxt.HasSubdir (if not nil) or else uses // the local file system to answer the question. func HasSubdir(ctxt *build.Context, root, dir string) (rel string, ok bool) { if f := ctxt.HasSubdir; f != nil { diff --git a/go/ssa/dom.go b/go/ssa/dom.go index ce2473cafce..66a2f5e6ed3 100644 --- a/go/ssa/dom.go +++ b/go/ssa/dom.go @@ -303,7 +303,7 @@ func sanityCheckDomTree(f *Function) { // Printing functions ---------------------------------------- -// printDomTree prints the dominator tree as text, using indentation. +// printDomTreeText prints the dominator tree as text, using indentation. func printDomTreeText(buf *bytes.Buffer, v *BasicBlock, indent int) { fmt.Fprintf(buf, "%*s%s\n", 4*indent, "", v) for _, child := range v.dom.children { diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index a77afaaac6a..37d42db1df4 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -616,7 +616,7 @@ func containsOpenFileLocked(s *snapshot, m *KnownMetadata) bool { return false } -// containsFileInWorkspace reports whether m contains any file inside the +// containsFileInWorkspaceLocked reports whether m contains any file inside the // workspace of the snapshot s. // // s.mu must be held while calling this function. diff --git a/gopls/internal/lsp/lsppos/lsppos.go b/gopls/internal/lsp/lsppos/lsppos.go index 7db25fea9e7..425a4f74aeb 100644 --- a/gopls/internal/lsp/lsppos/lsppos.go +++ b/gopls/internal/lsp/lsppos/lsppos.go @@ -118,7 +118,7 @@ func (m *Mapper) Range(start, end int) (protocol.Range, error) { return protocol.Range{Start: startPos, End: endPos}, nil } -// UTF16Len returns the UTF-16 length of the UTF-8 encoded content, were it to +// UTF16len returns the UTF-16 length of the UTF-8 encoded content, were it to // be re-encoded as UTF-16. func UTF16len(buf []byte) int { // This function copies buf, but microbenchmarks showed it to be faster than diff --git a/gopls/internal/lsp/lsppos/token.go b/gopls/internal/lsp/lsppos/token.go index c1b726fe24c..a42b5dad331 100644 --- a/gopls/internal/lsp/lsppos/token.go +++ b/gopls/internal/lsp/lsppos/token.go @@ -25,7 +25,7 @@ type TokenMapper struct { mapper *Mapper } -// NewMapper creates a new TokenMapper for the given content, using the +// NewTokenMapper creates a new TokenMapper for the given content, using the // provided file to compute offsets. func NewTokenMapper(content []byte, file *token.File) *TokenMapper { return &TokenMapper{ diff --git a/gopls/internal/lsp/lsprpc/autostart_default.go b/gopls/internal/lsp/lsprpc/autostart_default.go index 59a76dc2f9f..20b974728d9 100644 --- a/gopls/internal/lsp/lsprpc/autostart_default.go +++ b/gopls/internal/lsp/lsprpc/autostart_default.go @@ -24,7 +24,7 @@ func runRemote(cmd *exec.Cmd) error { return nil } -// autoNetworkAddress returns the default network and address for the +// autoNetworkAddressDefault returns the default network and address for the // automatically-started gopls remote. See autostart_posix.go for more // information. func autoNetworkAddressDefault(goplsPath, id string) (network string, address string) { diff --git a/gopls/internal/lsp/lsprpc/autostart_posix.go b/gopls/internal/lsp/lsprpc/autostart_posix.go index 948d44fcedf..90cc72ddf10 100644 --- a/gopls/internal/lsp/lsprpc/autostart_posix.go +++ b/gopls/internal/lsp/lsprpc/autostart_posix.go @@ -33,7 +33,7 @@ func daemonizePosix(cmd *exec.Cmd) { } } -// autoNetworkAddress resolves an id on the 'auto' pseduo-network to a +// autoNetworkAddressPosix resolves an id on the 'auto' pseduo-network to a // real network and address. On unix, this uses unix domain sockets. func autoNetworkAddressPosix(goplsPath, id string) (network string, address string) { // Especially when doing local development or testing, it's important that diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index 488305ac0bd..7f29d45ba2f 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -651,7 +651,7 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file }, nil } -// adjustRangeForCommentsAndWhitespace adjusts the given range to exclude unnecessary leading or +// adjustRangeForCommentsAndWhiteSpace adjusts the given range to exclude unnecessary leading or // trailing whitespace characters from selection as well as leading or trailing comments. // In the following example, each line of the if statement is indented once. There are also two // extra spaces after the sclosing bracket before the line break and a comment. @@ -1119,7 +1119,7 @@ func varOverridden(info *types.Info, firstUse *ast.Ident, obj types.Object, isFr return isOverriden } -// parseExtraction generates an AST file from the given text. We then return the portion of the +// parseBlockStmt generates an AST file from the given text. We then return the portion of the // file that represents the text. func parseBlockStmt(fset *token.FileSet, src []byte) (*ast.BlockStmt, error) { text := "package main\nfunc _() { " + string(src) + " }" diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index c6e70efce6f..2c9574a5f57 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -354,7 +354,7 @@ func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.Edit) ([]protocol.Te return result, nil } -// ToProtocolEdits converts LSP TextEdits to diff.Edits. +// FromProtocolEdits converts LSP TextEdits to diff.Edits. // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.Edit, error) { if edits == nil { diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 495791f9537..c926dedcf6b 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -276,7 +276,7 @@ func computeImportRenamingEdits(ctx context.Context, s Snapshot, renamingPkg Pac return result, nil } -// computeImportRenamingEdits computes all edits to files within the renming packages. +// computePackageNameRenamingEdits computes all edits to files within the renming packages. func computePackageNameRenamingEdits(renamingPkg Package, newName string) (map[span.URI][]protocol.TextEdit, error) { result := make(map[span.URI][]protocol.TextEdit) // Rename internal references to the package in the renaming package. diff --git a/gopls/internal/span/uri.go b/gopls/internal/span/uri.go index ce874e70318..adcc54a7797 100644 --- a/gopls/internal/span/uri.go +++ b/gopls/internal/span/uri.go @@ -185,7 +185,7 @@ func isWindowsDrivePath(path string) bool { return unicode.IsLetter(rune(path[0])) && path[1] == ':' } -// isWindowsDriveURI returns true if the file URI is of the format used by +// isWindowsDriveURIPath returns true if the file URI is of the format used by // Windows URIs. The url.Parse package does not specially handle Windows paths // (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). func isWindowsDriveURIPath(uri string) bool { diff --git a/internal/diff/ndiff.go b/internal/diff/ndiff.go index 722b50c986b..4dd83237af6 100644 --- a/internal/diff/ndiff.go +++ b/internal/diff/ndiff.go @@ -91,7 +91,7 @@ func runesLen(runes []rune) (len int) { return len } -// isASCII reports whether s contains only ASCII. +// stringIsASCII reports whether s contains only ASCII. // TODO(adonovan): combine when x/tools allows generics. func stringIsASCII(s string) bool { for i := 0; i < len(s); i++ { diff --git a/present/args.go b/present/args.go index d63196e028c..b4f7503b6da 100644 --- a/present/args.go +++ b/present/args.go @@ -18,7 +18,7 @@ import ( // regular expressions. That is the only change to the code from codewalk.go. // See http://9p.io/sys/doc/sam/sam.html Table II for details on the syntax. -// addrToByte evaluates the given address starting at offset start in data. +// addrToByteRange evaluates the given address starting at offset start in data. // It returns the lo and hi byte offset of the matched region within data. func addrToByteRange(addr string, start int, data []byte) (lo, hi int, err error) { if addr == "" { From 89b433532419d291bd3262c8d99f0b29dba4ca2a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 7 Oct 2022 13:43:27 -0400 Subject: [PATCH 321/723] gopls/internal/lsp: merge notifications about deprecated settings VS Code suppresses notifications if we send too many, but we don't want users to miss warnings about deprecated settings. Merge them all into a single message body. Also fix a race in a test that added in the preceding CL: the old go warnings may race with the initial workspace load. For golang/vscode-go#2487 Change-Id: I69d61a17e0e6e888fa04fa1edce864e28a8d650e Reviewed-on: https://go-review.googlesource.com/c/tools/+/440180 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/general.go | 46 +++++++++++++------ gopls/internal/lsp/source/options.go | 16 +++---- .../regtest/workspace/workspace_test.go | 14 +++--- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index 839af1fec73..da4e4bfb89c 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -13,6 +13,8 @@ import ( "os" "path" "path/filepath" + "sort" + "strings" "sync" "golang.org/x/tools/gopls/internal/lsp/debug" @@ -459,28 +461,46 @@ func (s *Server) eventuallyShowMessage(ctx context.Context, msg *protocol.ShowMe } func (s *Server) handleOptionResults(ctx context.Context, results source.OptionResults) error { + var warnings, errors []string for _, result := range results { - var msg *protocol.ShowMessageParams switch result.Error.(type) { case nil: // nothing to do case *source.SoftError: - msg = &protocol.ShowMessageParams{ - Type: protocol.Warning, - Message: result.Error.Error(), - } + warnings = append(warnings, result.Error.Error()) default: - msg = &protocol.ShowMessageParams{ - Type: protocol.Error, - Message: result.Error.Error(), - } + errors = append(errors, result.Error.Error()) } - if msg != nil { - if err := s.eventuallyShowMessage(ctx, msg); err != nil { - return err - } + } + + // Sort messages, but put errors first. + // + // Having stable content for the message allows clients to de-duplicate. This + // matters because we may send duplicate warnings for clients that support + // dynamic configuration: one for the initial settings, and then more for the + // individual view settings. + var msgs []string + msgType := protocol.Warning + if len(errors) > 0 { + msgType = protocol.Error + sort.Strings(errors) + msgs = append(msgs, errors...) + } + if len(warnings) > 0 { + sort.Strings(warnings) + msgs = append(msgs, warnings...) + } + + if len(msgs) > 0 { + // Settings + combined := "Invalid settings: " + strings.Join(msgs, "; ") + params := &protocol.ShowMessageParams{ + Type: msgType, + Message: combined, } + return s.eventuallyShowMessage(ctx, params) } + return nil } diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 04a614c973f..eef510181f5 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -1052,10 +1052,10 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.ExperimentalPostfixCompletions) case "experimentalWorkspaceModule": - const msg = "The experimentalWorkspaceModule feature has been replaced by go workspaces, " + - "and will be removed in a future version of gopls (https://go.dev/issue/55331). " + - "Please see https://github.com/golang/tools/blob/master/gopls/doc/workspace.md " + - "for information on setting up multi-module workspaces using go.work files." + const msg = "experimentalWorkspaceModule has been replaced by go workspaces, " + + "and will be removed in a future version of gopls (https://go.dev/issue/55331) -- " + + "see https://github.com/golang/tools/blob/master/gopls/doc/workspace.md " + + "for information on setting up multi-module workspaces using go.work files" result.softErrorf(msg) result.setBool(&o.ExperimentalWorkspaceModule) @@ -1084,8 +1084,8 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setDuration(&o.DiagnosticsDelay) case "experimentalWatchedFileDelay": - const msg = "The experimentalWatchedFileDelay setting is deprecated, and will " + - "be removed in a future version of gopls (https://go.dev/issue/55332)." + const msg = "experimentalWatchedFileDelay is deprecated, and will " + + "be removed in a future version of gopls (https://go.dev/issue/55332)" result.softErrorf(msg) result.setDuration(&o.ExperimentalWatchedFileDelay) @@ -1099,8 +1099,8 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.AllowImplicitNetworkAccess) case "experimentalUseInvalidMetadata": - const msg = "The experimentalUseInvalidMetadata setting is deprecated, and will be removed" + - "in a future version of gopls (https://go.dev/issue/55333)." + const msg = "experimentalUseInvalidMetadata is deprecated, and will be removed " + + "in a future version of gopls (https://go.dev/issue/55333)" result.softErrorf(msg) result.setBool(&o.ExperimentalUseInvalidMetadata) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 4738c0f489a..a271bb0fa7a 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1267,10 +1267,9 @@ func TestOldGoNotification_UnsupportedVersion(t *testing.T) { Run(t, "", func(t *testing.T, env *Env) { env.Await( - OnceMet( - InitialWorkspaceLoad, - ShownMessage("Please upgrade"), - ), + // Note: cannot use OnceMet(InitialWorkspaceLoad, ...) here, as the + // upgrade message may race with the IWL. + ShownMessage("Please upgrade"), ) }) } @@ -1293,10 +1292,9 @@ func TestOldGoNotification_Fake(t *testing.T) { Run(t, "", func(t *testing.T, env *Env) { env.Await( - OnceMet( - InitialWorkspaceLoad, - ShownMessage("Please upgrade"), - ), + // Note: cannot use OnceMet(InitialWorkspaceLoad, ...) here, as the + // upgrade message may race with the IWL. + ShownMessage("Please upgrade"), ) }) } From fa6bd3b49c3c24e456fada0e9d8542f244bcd4ae Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Mon, 10 Oct 2022 11:59:30 -0400 Subject: [PATCH 322/723] gopls: include informational vulnerability diagnostics Unaffecting vulnerabilities that appear should be shown as informational diagnostics. These do not have current version. Change-Id: I5dc8d111fd9de8388195627c8f050a2660426abb Reviewed-on: https://go-review.googlesource.com/c/tools/+/441875 Reviewed-by: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Suzy Mueller --- gopls/internal/lsp/mod/diagnostics.go | 2 +- gopls/internal/regtest/misc/vuln_test.go | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 61cf594f65d..256abd13cfa 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -190,7 +190,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, for _, v := range vulnList { // Only show the diagnostic if the vulnerability was calculated // for the module at the current version. - if semver.Compare(req.Mod.Version, v.CurrentVersion) != 0 { + if semver.IsValid(v.CurrentVersion) && semver.Compare(req.Mod.Version, v.CurrentVersion) != 0 { continue } diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index e1b8c270620..2fb9e31a3ce 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -9,6 +9,7 @@ package misc import ( "context" + "strings" "testing" "golang.org/x/tools/gopls/internal/lsp/command" @@ -321,12 +322,19 @@ func TestRunVulncheckExp(t *testing.T) { ShownMessage("Found"), OnceMet( env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "golang.org/amod has a known vulnerability: vuln in amod"), + env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "golang.org/amod has a known vulnerability: unaffecting vulnerability"), env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/bmod`, "golang.org/bmod has a known vulnerability: vuln in bmod\n\nThis is a long description of this vulnerability."), ReadDiagnostics("go.mod", d), ), ) - env.ApplyQuickFixes("go.mod", d.Diagnostics) + var toFix []protocol.Diagnostic + for _, diag := range d.Diagnostics { + if strings.Contains(diag.Message, "vuln in ") { + toFix = append(toFix, diag) + } + } + env.ApplyQuickFixes("go.mod", toFix) env.Await(env.DoneWithChangeWatchedFiles()) wantGoMod := `module golang.org/entry From 19cfa79754573bbd658c920d7e6a1bd98df3c672 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 1 Sep 2022 08:53:03 -0400 Subject: [PATCH 323/723] internal/lsp/source: switch the default diff algorithm to "checked" We've been using this setting in experimental for some time without issue, so switch it to default. For golang/go#52967 Change-Id: Ib4d786e689d4b0f009195cc86d7dd5d8269cf424 Reviewed-on: https://go-review.googlesource.com/c/tools/+/427534 Reviewed-by: Peter Weinberger Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/source/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index eef510181f5..00c9b7b59ce 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -165,6 +165,7 @@ func DefaultOptions() *Options { CompletionDocumentation: true, DeepCompletion: true, ChattyDiagnostics: true, + NewDiff: "both", }, Hooks: Hooks{ // TODO(adonovan): switch to new diff.Strings implementation. @@ -822,7 +823,6 @@ func (o *Options) EnableAllExperiments() { o.SemanticTokens = true o.ExperimentalUseInvalidMetadata = true o.ExperimentalWatchedFileDelay = 50 * time.Millisecond - o.NewDiff = "checked" } func (o *Options) enableAllExperimentMaps() { From 150b5f8bb6f8aa17a8e8458632678166ed66184b Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 4 Oct 2022 15:41:42 -0400 Subject: [PATCH 324/723] internal/imports: read entire API dir in mkstdlib.go Read the entire API directory, so that we don't need to edit mkstdlib.go when regenerating zstdlib.go for new go versions. Joint with Dylan Le. Co-Authored-By: dle8@u.rochester.edu. Change-Id: If5c71bfcfd2104f923df10cf21d16b5280fed025 Reviewed-on: https://go-review.googlesource.com/c/tools/+/438504 Reviewed-by: Dylan Le gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- internal/imports/mkstdlib.go | 47 ++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/internal/imports/mkstdlib.go b/internal/imports/mkstdlib.go index b1670227a24..072e4e1d02c 100644 --- a/internal/imports/mkstdlib.go +++ b/internal/imports/mkstdlib.go @@ -23,6 +23,7 @@ import ( "regexp" "runtime" "sort" + "strings" exec "golang.org/x/sys/execabs" ) @@ -51,32 +52,7 @@ func main() { outf("// Code generated by mkstdlib.go. DO NOT EDIT.\n\n") outf("package imports\n") outf("var stdlib = map[string][]string{\n") - f := io.MultiReader( - mustOpen(api("go1.txt")), - mustOpen(api("go1.1.txt")), - mustOpen(api("go1.2.txt")), - mustOpen(api("go1.3.txt")), - mustOpen(api("go1.4.txt")), - mustOpen(api("go1.5.txt")), - mustOpen(api("go1.6.txt")), - mustOpen(api("go1.7.txt")), - mustOpen(api("go1.8.txt")), - mustOpen(api("go1.9.txt")), - mustOpen(api("go1.10.txt")), - mustOpen(api("go1.11.txt")), - mustOpen(api("go1.12.txt")), - mustOpen(api("go1.13.txt")), - mustOpen(api("go1.14.txt")), - mustOpen(api("go1.15.txt")), - mustOpen(api("go1.16.txt")), - mustOpen(api("go1.17.txt")), - mustOpen(api("go1.18.txt")), - mustOpen(api("go1.19.txt")), - - // The API of the syscall/js package needs to be computed explicitly, - // because it's not included in the GOROOT/api/go1.*.txt files at this time. - syscallJSAPI(), - ) + f := readAPI() sc := bufio.NewScanner(f) pkgs := map[string]map[string]bool{ @@ -124,6 +100,25 @@ func main() { } } +// readAPI opens an io.Reader that reads all stdlib API content. +func readAPI() io.Reader { + entries, err := os.ReadDir(filepath.Join(runtime.GOROOT(), "api")) + if err != nil { + log.Fatal(err) + } + var readers []io.Reader + for _, entry := range entries { + name := entry.Name() + if strings.HasPrefix(name, "go") && strings.HasSuffix(name, ".txt") { + readers = append(readers, mustOpen(api(name))) + } + } + // The API of the syscall/js package needs to be computed explicitly, + // because it's not included in the GOROOT/api/go1.*.txt files at this time. + readers = append(readers, syscallJSAPI()) + return io.MultiReader(readers...) +} + // syscallJSAPI returns the API of the syscall/js package. // It's computed from the contents of $(go env GOROOT)/src/syscall/js. func syscallJSAPI() io.Reader { From 2a41b2554b2825d110e864b1abd11e7a7de2c92e Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 10 Oct 2022 15:19:35 -0400 Subject: [PATCH 325/723] internal/robustio: fix log.Fatal calls that should be log.Fatalf Change-Id: I810160ebabfdd6062f5cdd902ceda77b5912a6fa Reviewed-on: https://go-review.googlesource.com/c/tools/+/441876 Reviewed-by: Peter Weinberger TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Auto-Submit: Robert Findley --- gopls/internal/robustio/copyfiles.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/internal/robustio/copyfiles.go b/gopls/internal/robustio/copyfiles.go index 0b61a313e39..6e9f4b3875f 100644 --- a/gopls/internal/robustio/copyfiles.go +++ b/gopls/internal/robustio/copyfiles.go @@ -30,7 +30,7 @@ func main() { entries, err := os.ReadDir(dir) if err != nil { - log.Fatal("reading the robustio dir: %v", err) + log.Fatalf("reading the robustio dir: %v", err) } // Collect file content so that we can validate before copying. @@ -103,7 +103,7 @@ func addPlusBuildConstraints(src []byte) []byte { if c, err := constraint.Parse(lit); err == nil { plusBuild, err := constraint.PlusBuildLines(c) if err != nil { - log.Fatal("computing +build constraint for %q: %v", lit, err) + log.Fatalf("computing +build constraint for %q: %v", lit, err) } insertAt := file.Offset(pos) + len(lit) result = append(result, src[lastInsertion:insertAt]...) From 906c733cc2d5f823ccd70dd604aab8e2cc3c25d0 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 4 Oct 2022 15:01:17 -0400 Subject: [PATCH 326/723] gopls/internal/lsp/source: find references in test packages When finding references or implementations, we must include objects defined in intermediate test variants. However, as we have seen we should be careful to avoid accidentally type-checking intermediate test variants in ParseFull, which is not their workspace parse mode. Therefore eliminate the problematic TypecheckAll type-check mode in favor of special handling in this one case where it is necessary. Along the way: - Simplify the mapping of protocol position->offset. This should not require getting a package, or even parsing a file. For isolation, just use the NewColumnMapper constructor, even though it may technically result in building a token.File multiple times. - Update package renaming logic to use TypecheckWorkspace, since we only need to rename within the workspace. - Add regtest support for Implementations requests. Fixes golang/go#43144 Change-Id: I41f684ad766f5af805abbd7c5ee0a010ff9b9b8c Reviewed-on: https://go-review.googlesource.com/c/tools/+/438755 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/load.go | 1 + gopls/internal/lsp/cache/snapshot.go | 25 ++-- gopls/internal/lsp/fake/editor.go | 27 +++- gopls/internal/lsp/regtest/wrappers.go | 13 +- gopls/internal/lsp/source/implementation.go | 65 ++++++---- gopls/internal/lsp/source/references.go | 2 +- gopls/internal/lsp/source/rename.go | 4 +- gopls/internal/lsp/source/view.go | 7 +- .../internal/regtest/misc/references_test.go | 119 ++++++++++++++++++ 9 files changed, 208 insertions(+), 55 deletions(-) diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 37d42db1df4..2550dfba4be 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -655,6 +655,7 @@ func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[Packag if !m.Valid { continue } + if !containsPackageLocked(s, m.Metadata) { continue } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 8027b40a0ab..d7e39b31fb5 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -698,27 +698,16 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant() && !withIntermediateTestVariants { continue } - var parseModes []source.ParseMode - switch mode { - case source.TypecheckAll: - if s.workspaceParseMode(id) == source.ParseFull { - parseModes = []source.ParseMode{source.ParseFull} - } else { - parseModes = []source.ParseMode{source.ParseExported, source.ParseFull} - } - case source.TypecheckFull: - parseModes = []source.ParseMode{source.ParseFull} - case source.TypecheckWorkspace: - parseModes = []source.ParseMode{s.workspaceParseMode(id)} + parseMode := source.ParseFull + if mode == source.TypecheckWorkspace { + parseMode = s.workspaceParseMode(id) } - for _, parseMode := range parseModes { - ph, err := s.buildPackageHandle(ctx, id, parseMode) - if err != nil { - return nil, err - } - phs = append(phs, ph) + ph, err := s.buildPackageHandle(ctx, id, parseMode) + if err != nil { + return nil, err } + phs = append(phs, ph) } return phs, nil } diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 8d1362a50fb..b9f62f3028d 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -1137,7 +1137,8 @@ func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHi return hints, nil } -// References executes a reference request on the server. +// References returns references to the object at (path, pos), as returned by +// the connected LSP server. If no server is connected, it returns (nil, nil). func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) { if e.Server == nil { return nil, nil @@ -1164,6 +1165,8 @@ func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protoc return locations, nil } +// Rename performs a rename of the object at (path, pos) to newName, using the +// connected LSP server. If no server is connected, it returns nil. func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName string) error { if e.Server == nil { return nil @@ -1185,6 +1188,28 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin return nil } +// Implementations returns implementations for the object at (path, pos), as +// returned by the connected LSP server. If no server is connected, it returns +// (nil, nil). +func (e *Editor) Implementations(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) { + if e.Server == nil { + return nil, nil + } + e.mu.Lock() + _, ok := e.buffers[path] + e.mu.Unlock() + if !ok { + return nil, fmt.Errorf("buffer %q is not open", path) + } + params := &protocol.ImplementationParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: e.TextDocumentIdentifier(path), + Position: pos.ToProtocolPosition(), + }, + } + return e.Server.Implementation(ctx, params) +} + func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error { closed, opened, err := e.renameBuffers(ctx, oldPath, newPath) if err != nil { diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index 0f7cc9a1a66..63b59917c10 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -389,8 +389,7 @@ func (e *Env) WorkspaceSymbol(sym string) []protocol.SymbolInformation { return ans } -// References calls textDocument/references for the given path at the given -// position. +// References wraps Editor.References, calling t.Fatal on any error. func (e *Env) References(path string, pos fake.Pos) []protocol.Location { e.T.Helper() locations, err := e.Editor.References(e.Ctx, path, pos) @@ -408,6 +407,16 @@ func (e *Env) Rename(path string, pos fake.Pos, newName string) { } } +// Implementations wraps Editor.Implementations, calling t.Fatal on any error. +func (e *Env) Implementations(path string, pos fake.Pos) []protocol.Location { + e.T.Helper() + locations, err := e.Editor.Implementations(e.Ctx, path, pos) + if err != nil { + e.T.Fatal(err) + } + return locations +} + // RenameFile wraps Editor.RenameFile, calling t.Fatal on any error. func (e *Env) RenameFile(oldPath, newPath string) { e.T.Helper() diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index e2c60bb55e7..2da488dd67a 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -208,36 +208,31 @@ var ( errNoObjectFound = errors.New("no object found") ) -// qualifiedObjsAtProtocolPos returns info for all the type.Objects -// referenced at the given position. An object will be returned for -// every package that the file belongs to, in every typechecking mode -// applicable. +// qualifiedObjsAtProtocolPos returns info for all the types.Objects referenced +// at the given position, for the following selection of packages: +// +// 1. all packages (including all test variants), in their workspace parse mode +// 2. if not included above, at least one package containing uri in full parse mode +// +// Finding objects in (1) ensures that we locate references within all +// workspace packages, including in x_test packages. Including (2) ensures that +// we find local references in the current package, for non-workspace packages +// that may be open. func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, pp protocol.Position) ([]qualifiedObject, error) { - offset, err := protocolPositionToOffset(ctx, s, uri, pp) + fh, err := s.GetFile(ctx, uri) if err != nil { return nil, err } - return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{}) -} - -func protocolPositionToOffset(ctx context.Context, s Snapshot, uri span.URI, pp protocol.Position) (int, error) { - pkgs, err := s.PackagesForFile(ctx, uri, TypecheckAll, false) + content, err := fh.Read() if err != nil { - return 0, err - } - if len(pkgs) == 0 { - return 0, errNoObjectFound - } - pkg := pkgs[0] - pgf, err := pkg.File(uri) - if err != nil { - return 0, err + return nil, err } - pos, err := pgf.Mapper.Pos(pp) + m := protocol.NewColumnMapper(uri, content) + offset, err := m.Offset(pp) if err != nil { - return 0, err + return nil, err } - return safetoken.Offset(pgf.Tok, pos) + return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{}) } // A positionKey identifies a byte offset within a file (URI). @@ -251,8 +246,8 @@ type positionKey struct { offset int } -// qualifiedObjsAtLocation finds all objects referenced at offset in uri, across -// all packages in the snapshot. +// qualifiedObjsAtLocation finds all objects referenced at offset in uri, +// across all packages in the snapshot. func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, seen map[positionKey]bool) ([]qualifiedObject, error) { if seen[key] { return nil, nil @@ -268,11 +263,31 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s // try to be comprehensive in case we ever support variations on build // constraints. - pkgs, err := s.PackagesForFile(ctx, key.uri, TypecheckAll, false) + pkgs, err := s.PackagesForFile(ctx, key.uri, TypecheckWorkspace, true) if err != nil { return nil, err } + // In order to allow basic references/rename/implementations to function when + // non-workspace packages are open, ensure that we have at least one fully + // parsed package for the current file. This allows us to find references + // inside the open package. Use WidestPackage to capture references in test + // files. + hasFullPackage := false + for _, pkg := range pkgs { + if pkg.ParseMode() == ParseFull { + hasFullPackage = true + break + } + } + if !hasFullPackage { + pkg, err := s.PackageForFile(ctx, key.uri, TypecheckFull, WidestPackage) + if err != nil { + return nil, err + } + pkgs = append(pkgs, pkg) + } + // report objects in the order we encounter them. This ensures that the first // result is at the cursor... var qualifiedObjs []qualifiedObject diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 9a10b38d161..de6687ad49f 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -62,7 +62,7 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit } if inPackageName { - renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckAll, NarrowestPackage) + renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckWorkspace, NarrowestPackage) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index c926dedcf6b..2f0ad56ec66 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -76,7 +76,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot err := errors.New("can't rename package: LSP client does not support file renaming") return nil, err, err } - renamingPkg, err := snapshot.PackageForFile(ctx, f.URI(), TypecheckAll, NarrowestPackage) + renamingPkg, err := snapshot.PackageForFile(ctx, f.URI(), TypecheckWorkspace, NarrowestPackage) if err != nil { return nil, err, err } @@ -169,7 +169,7 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, // TODO(rfindley): but is this correct? What about x_test packages that // import the renaming package? const includeTestVariants = false - pkgs, err := s.PackagesForFile(ctx, f.URI(), TypecheckAll, includeTestVariants) + pkgs, err := s.PackagesForFile(ctx, f.URI(), TypecheckWorkspace, includeTestVariants) if err != nil { return nil, true, err } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index bf152805966..cd0c8417f47 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -437,16 +437,11 @@ var AllParseModes = []ParseMode{ParseHeader, ParseExported, ParseFull} type TypecheckMode int const ( - // Invalid default value. - TypecheckUnknown TypecheckMode = iota // TypecheckFull means to use ParseFull. - TypecheckFull + TypecheckFull TypecheckMode = iota // TypecheckWorkspace means to use ParseFull for workspace packages, and // ParseExported for others. TypecheckWorkspace - // TypecheckAll means ParseFull for workspace packages, and both Full and - // Exported for others. Only valid for some functions. - TypecheckAll ) type VersionedFileHandle interface { diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index 058aad3e60b..2cd4359d313 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -6,9 +6,12 @@ package misc import ( "fmt" + "sort" "strings" "testing" + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" ) @@ -169,3 +172,119 @@ func main() { } }) } + +// Test for golang/go#43144. +// +// Verify that we search for references and implementations in intermediate +// test variants. +func TestReferencesInTestVariants(t *testing.T) { + const files = ` +-- go.mod -- +module foo.mod + +go 1.12 +-- foo/foo.go -- +package foo + +import "foo.mod/bar" + +const Foo = 42 + +type T int +type Interface interface{ M() } + +func _() { + _ = bar.Blah +} + +-- bar/bar.go -- +package bar + +var Blah = 123 + +-- bar/bar_test.go -- +package bar + +type Mer struct{} +func (Mer) M() {} + +func TestBar() { + _ = Blah +} +-- bar/bar_x_test.go -- +package bar_test + +import ( + "foo.mod/bar" + "foo.mod/foo" +) + +type Mer struct{} +func (Mer) M() {} + +func _() { + _ = bar.Blah + _ = foo.Foo +} +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo/foo.go") + + // Helper to map locations relative file paths. + fileLocations := func(locs []protocol.Location) []string { + var got []string + for _, loc := range locs { + got = append(got, env.Sandbox.Workdir.URIToPath(loc.URI)) + } + sort.Strings(got) + return got + } + + refTests := []struct { + re string + wantRefs []string + }{ + // Blah is referenced: + // - inside the foo.mod/bar (ordinary) package + // - inside the foo.mod/bar [foo.mod/bar.test] test variant package + // - from the foo.mod/bar_test [foo.mod/bar.test] x_test package + // - from the foo.mod/foo package + {"Blah", []string{"bar/bar.go", "bar/bar_test.go", "bar/bar_x_test.go", "foo/foo.go"}}, + + // Foo is referenced in bar_x_test.go via the intermediate test variant + // foo.mod/foo [foo.mod/bar.test]. + {"Foo", []string{"bar/bar_x_test.go", "foo/foo.go"}}, + } + + for _, test := range refTests { + pos := env.RegexpSearch("foo/foo.go", test.re) + refs := env.References("foo/foo.go", pos) + + got := fileLocations(refs) + if diff := cmp.Diff(test.wantRefs, got); diff != "" { + t.Errorf("References(%q) returned unexpected diff (-want +got):\n%s", test.re, diff) + } + } + + implTests := []struct { + re string + wantImpls []string + }{ + // Interface is implemented both in foo.mod/bar [foo.mod/bar.test] (which + // doesn't import foo), and in foo.mod/bar_test [foo.mod/bar.test], which + // imports the test variant of foo. + {"Interface", []string{"bar/bar_test.go", "bar/bar_x_test.go"}}, + } + + for _, test := range implTests { + pos := env.RegexpSearch("foo/foo.go", test.re) + refs := env.Implementations("foo/foo.go", pos) + + got := fileLocations(refs) + if diff := cmp.Diff(test.wantImpls, got); diff != "" { + t.Errorf("Implementations(%q) returned unexpected diff (-want +got):\n%s", test.re, diff) + } + } + }) +} From cd0288ff8539478585b7c3bdba0a33b9d6c63266 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 1 Aug 2022 14:17:38 -0400 Subject: [PATCH 327/723] internal/lsp/cache: invalidate analysis results on packages.Load Most invalidation happens in snapshot.clone, but to be safe we were also invalidating package data when new metadata is set via packages.Load. At the time, I wasn't sure why this was necessary. Now I understand: with experimentalUseInvalidMetadata it is possible that we re-compute invalidated data using stale metadata in between the initial clone and the subsequent reload. I noticed that it was also possible to have stale analysis results after the Load results arrive. Fix this by invalidating analysis results on Load, in addition to packages. Factor out invalidation into a new helper method. Since we believe this may fix analyzer panics, re-enable strict handling of analysis panics during tests. For golang/go#56035 For golang/go#54762 For golang/go#42857 Change-Id: I8c28e0700f8c16c58df4ecf60f6127b1c05d6dc5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/420538 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/analysis.go | 8 ++--- gopls/internal/lsp/cache/graph.go | 8 ++--- gopls/internal/lsp/cache/load.go | 19 ++++++----- gopls/internal/lsp/cache/snapshot.go | 50 +++++++++++++++++----------- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 944485c3cd8..d41116c3400 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -365,10 +365,10 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a if r := recover(); r != nil { // An Analyzer crashed. This is often merely a symptom // of a problem in package loading. - // Now that we have a theory of these crashes, - // we disable the check to stop flakes from being a nuisance. - // TODO(adonovan): re-enable it when plausibly fixed. - const strict = false + // + // We believe that CL 420538 may have fixed these crashes, so enable + // strict checks in tests. + const strict = true if strict && bug.PanicOnBugs && analyzer.Name != "fact_purity" { // During testing, crash. See issues 54762, 56035. // But ignore analyzers with known crash bugs: diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index a801c83a9ab..3d530c45ab1 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -134,12 +134,12 @@ func (g *metadataGraph) build() { // // If includeInvalid is false, the algorithm ignores packages with invalid // metadata (including those in the given list of ids). -func (g *metadataGraph) reverseTransitiveClosure(includeInvalid bool, ids ...PackageID) map[PackageID]struct{} { - seen := make(map[PackageID]struct{}) +func (g *metadataGraph) reverseTransitiveClosure(includeInvalid bool, ids ...PackageID) map[PackageID]bool { + seen := make(map[PackageID]bool) var visitAll func([]PackageID) visitAll = func(ids []PackageID) { for _, id := range ids { - if _, ok := seen[id]; ok { + if seen[id] { continue } m := g.metadata[id] @@ -147,7 +147,7 @@ func (g *metadataGraph) reverseTransitiveClosure(includeInvalid bool, ids ...Pac if m == nil || !(m.Valid || includeInvalid) { continue } - seen[id] = struct{}{} + seen[id] = true visitAll(g.importedBy[id]) } } diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 2550dfba4be..3ef5301f83c 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -228,16 +228,17 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf s.meta = s.meta.Clone(updates) s.resetIsActivePackageLocked() - // Invalidate any packages we may have associated with this metadata. + // Invalidate any packages and analysis results we may have associated with + // this metadata. // - // TODO(rfindley): this should not be necessary, as we should have already - // invalidated in snapshot.clone. - for id := range invalidatedPackages { - for _, mode := range source.AllParseModes { - key := packageKey{mode, id} - s.packages.Delete(key) - } - } + // Generally speaking we should have already invalidated these results in + // snapshot.clone, but with experimentalUseInvalidMetadata is may be possible + // that we have re-computed stale results before the reload completes. In + // this case, we must re-invalidate here. + // + // TODO(golang/go#54180): if we decide to make experimentalUseInvalidMetadata + // obsolete, we should avoid this invalidation. + s.invalidatePackagesLocked(invalidatedPackages) s.workspacePackages = computeWorkspacePackagesLocked(s, s.meta) s.dumpWorkspace("load") diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index d7e39b31fb5..1587a5d178d 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1856,26 +1856,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC addRevDeps(id, invalidateMetadata) } - // Delete invalidated package type information. - for id := range idsToInvalidate { - for _, mode := range source.AllParseModes { - key := packageKey{mode, id} - result.packages.Delete(key) - } - } - - // Copy actions. - // TODO(adonovan): opt: avoid iteration over s.actions. - var actionsToDelete []actionKey - s.actions.Range(func(k, _ interface{}) { - key := k.(actionKey) - if _, ok := idsToInvalidate[key.pkgid]; ok { - actionsToDelete = append(actionsToDelete, key) - } - }) - for _, key := range actionsToDelete { - result.actions.Delete(key) - } + result.invalidatePackagesLocked(idsToInvalidate) // If a file has been deleted, we must delete metadata for all packages // containing that file. @@ -2050,6 +2031,35 @@ func invalidatedPackageIDs(uri span.URI, known map[span.URI][]PackageID, package return invalidated } +// invalidatePackagesLocked deletes data associated with the given package IDs. +// +// Note: all keys in the ids map are invalidated, regardless of the +// corresponding value. +// +// s.mu must be held while calling this function. +func (s *snapshot) invalidatePackagesLocked(ids map[PackageID]bool) { + // Delete invalidated package type information. + for id := range ids { + for _, mode := range source.AllParseModes { + key := packageKey{mode, id} + s.packages.Delete(key) + } + } + + // Copy actions. + // TODO(adonovan): opt: avoid iteration over s.actions. + var actionsToDelete []actionKey + s.actions.Range(func(k, _ interface{}) { + key := k.(actionKey) + if _, ok := ids[key.pkgid]; ok { + actionsToDelete = append(actionsToDelete, key) + } + }) + for _, key := range actionsToDelete { + s.actions.Delete(key) + } +} + // fileWasSaved reports whether the FileHandle passed in has been saved. It // accomplishes this by checking to see if the original and current FileHandles // are both overlays, and if the current FileHandle is saved while the original From 49b340b35229f750d46cdc3b2025e5f4d3776bd0 Mon Sep 17 00:00:00 2001 From: Michael Matloob Date: Mon, 10 Oct 2022 15:56:37 -0400 Subject: [PATCH 328/723] go/analysis: update tests for different go list error behavior golang.org/cl/437298 updates go list to not exit with a non-zero error code in some cases and instead place the error on the Package struct. Also, as a cleanup, some places where exit status 2 was returned will return exit status 1 (bringing it in line with other errors returned by the go command). TestEncodeDecode in facts_test.go has been updated to fix a missing function body that is not relevant to the test, but that was causing an error because the Package struct now has an error on it. TestRunDespiteErrors in checker_test.go has been updated to reflect that in some cases an analysis with RunDespiteErrors will fail to run because a build error returned by go list when it's run to get export data is not recognized as being a parse/typechecker error (the kind of error allowed by TestRunDespiteError). TestIntegration in unitchecker_test.go has been updated to reflect that go vet running unitchecker will now fail with exit status 1 instead of 2 (so it just checks for a zero or non-zero status). For golang/go#25842 Change-Id: Idbbd19b5de661e6e5f49e0475c5bc918d8e33803 Reviewed-on: https://go-review.googlesource.com/c/tools/+/441879 Reviewed-by: Alan Donovan Reviewed-by: Michael Matloob TryBot-Result: Gopher Robot Run-TryBot: Michael Matloob gopls-CI: kokoro --- go/analysis/internal/checker/checker_test.go | 31 +++++++++++++++++++- go/analysis/internal/facts/facts_test.go | 2 +- go/analysis/unitchecker/unitchecker_test.go | 28 ++++++++++-------- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go index 383a8e1dd50..d91425ee4ab 100644 --- a/go/analysis/internal/checker/checker_test.go +++ b/go/analysis/internal/checker/checker_test.go @@ -128,6 +128,18 @@ func Foo(s string) int { RunDespiteErrors: true, } + // A no-op analyzer that should finish regardless of + // parse or type errors in the code. + noopWithFact := &analysis.Analyzer{ + Name: "noopfact", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: func(pass *analysis.Pass) (interface{}, error) { + return nil, nil + }, + RunDespiteErrors: true, + FactTypes: []analysis.Fact{&EmptyFact{}}, + } + for _, test := range []struct { name string pattern []string @@ -136,7 +148,17 @@ func Foo(s string) int { }{ // parse/type errors {name: "skip-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{analyzer}, code: 1}, - {name: "despite-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noop}, code: 0}, + // RunDespiteErrors allows a driver to run an Analyzer even after parse/type errors. + // + // The noop analyzer doesn't use facts, so the driver loads only the root + // package from source. For the rest, it asks 'go list' for export data, + // which fails because the compiler encounters the type error. Since the + // errors come from 'go list', the driver doesn't run the analyzer. + {name: "despite-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noop}, code: 1}, + // The noopfact analyzer does use facts, so the driver loads source for + // all dependencies, does type checking itself, recognizes the error as a + // type error, and runs the analyzer. + {name: "despite-error-fact", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 0}, // combination of parse/type errors and no errors {name: "despite-error-and-no-error", pattern: []string{"file=" + path, "sort"}, analyzers: []*analysis.Analyzer{analyzer, noop}, code: 1}, // non-existing package error @@ -150,6 +172,9 @@ func Foo(s string) int { // no errors {name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{analyzer, noop}, code: 0}, } { + if test.name == "despite-error" { // TODO(matloob): once CL 437298 is submitted, add the condition testenv.Go1Point() < 20 + continue + } if got := checker.Run(test.pattern, test.analyzers); got != test.code { t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code) } @@ -157,3 +182,7 @@ func Foo(s string) int { defer cleanup() } + +type EmptyFact struct{} + +func (f *EmptyFact) AFact() {} diff --git a/go/analysis/internal/facts/facts_test.go b/go/analysis/internal/facts/facts_test.go index a55e30d7a31..c8379c58aa8 100644 --- a/go/analysis/internal/facts/facts_test.go +++ b/go/analysis/internal/facts/facts_test.go @@ -148,7 +148,7 @@ func TestEncodeDecode(t *testing.T) { type N4[T a.T4|int8] func() T type N5[T interface{Bar() a.T5} ] func() T - type t5 struct{}; func (t5) Bar() a.T5 + type t5 struct{}; func (t5) Bar() a.T5 { return 0 } var G1 N1[a.T1] var G2 func() N2[a.T2] diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go index a2393b7265f..197abd9a168 100644 --- a/go/analysis/unitchecker/unitchecker_test.go +++ b/go/analysis/unitchecker/unitchecker_test.go @@ -148,17 +148,17 @@ func _() { \} ` for _, test := range []struct { - args string - wantOut string - wantExit int + args string + wantOut string + wantExitError bool }{ - {args: "golang.org/fake/a", wantOut: wantA, wantExit: 2}, - {args: "golang.org/fake/b", wantOut: wantB, wantExit: 2}, - {args: "golang.org/fake/c", wantOut: wantC, wantExit: 2}, - {args: "golang.org/fake/a golang.org/fake/b", wantOut: wantA + wantB, wantExit: 2}, - {args: "-json golang.org/fake/a", wantOut: wantAJSON, wantExit: 0}, - {args: "-json golang.org/fake/c", wantOut: wantCJSON, wantExit: 0}, - {args: "-c=0 golang.org/fake/a", wantOut: wantA + "4 MyFunc123\\(\\)\n", wantExit: 2}, + {args: "golang.org/fake/a", wantOut: wantA, wantExitError: true}, + {args: "golang.org/fake/b", wantOut: wantB, wantExitError: true}, + {args: "golang.org/fake/c", wantOut: wantC, wantExitError: true}, + {args: "golang.org/fake/a golang.org/fake/b", wantOut: wantA + wantB, wantExitError: true}, + {args: "-json golang.org/fake/a", wantOut: wantAJSON, wantExitError: false}, + {args: "-json golang.org/fake/c", wantOut: wantCJSON, wantExitError: false}, + {args: "-c=0 golang.org/fake/a", wantOut: wantA + "4 MyFunc123\\(\\)\n", wantExitError: true}, } { cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "-findcall.name=MyFunc123") cmd.Args = append(cmd.Args, strings.Fields(test.args)...) @@ -170,8 +170,12 @@ func _() { if exitErr, ok := err.(*exec.ExitError); ok { exitcode = exitErr.ExitCode() } - if exitcode != test.wantExit { - t.Errorf("%s: got exit code %d, want %d", test.args, exitcode, test.wantExit) + if (exitcode != 0) != test.wantExitError { + want := "zero" + if test.wantExitError { + want = "nonzero" + } + t.Errorf("%s: got exit code %d, want %s", test.args, exitcode, want) } matched, err := regexp.Match(test.wantOut, out) From 29429f53af821b6b3578cb28cf4eb317659ab6ed Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 11 Oct 2022 13:52:53 -0400 Subject: [PATCH 329/723] gopls/internal/lsp/source: sort protocol edits The LSP protocol doesn't require edits to be sorted, but some clients such as govim were relying on it, as we learned when I recently removed the sort. This change restores the sort behavior. See also govim/govim#1171 Change-Id: I589b6cfde933ea6907859bf40acd33c1a7fcde02 Reviewed-on: https://go-review.googlesource.com/c/tools/+/442256 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan --- gopls/internal/lsp/source/format.go | 6 ++++++ internal/diff/diff.go | 12 ++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index 2c9574a5f57..b713893fbab 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -340,6 +340,12 @@ func protocolEditsFromSource(src []byte, edits []diff.Edit, tf *token.File) ([]p // ToProtocolEdits converts diff.Edits to LSP TextEdits. // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.Edit) ([]protocol.TextEdit, error) { + // LSP doesn't require TextEditArray to be sorted: + // this is the receiver's concern. But govim, and perhaps + // other clients have historically relied on the order. + edits = append([]diff.Edit(nil), edits...) + diff.SortEdits(edits) + result := make([]protocol.TextEdit, len(edits)) for i, edit := range edits { rng, err := m.OffsetRange(edit.Start, edit.End) diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 794e6b793df..7b08ad57c82 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -54,7 +54,7 @@ func Apply(src string, edits []Edit) (string, error) { func validate(src string, edits []Edit) ([]Edit, int, error) { if !sort.IsSorted(editsSort(edits)) { edits = append([]Edit(nil), edits...) - sortEdits(edits) + SortEdits(edits) } // Check validity of edits and compute final size. @@ -74,13 +74,13 @@ func validate(src string, edits []Edit) ([]Edit, int, error) { return edits, size, nil } -// sortEdits orders edits by (start, end) offset. -// This ordering puts insertions (end=start) before deletions -// (end>start) at the same point, but uses a stable sort to preserve +// SortEdits orders a slice of Edits by (start, end) offset. +// This ordering puts insertions (end = start) before deletions +// (end > start) at the same point, but uses a stable sort to preserve // the order of multiple insertions at the same point. // (Apply detects multiple deletions at the same point as an error.) -func sortEdits(edits editsSort) { - sort.Stable(edits) +func SortEdits(edits []Edit) { + sort.Stable(editsSort(edits)) } type editsSort []Edit From ab79327e0571fe08aa15c08e5f3f38e548f7b520 Mon Sep 17 00:00:00 2001 From: Tim King Date: Mon, 10 Oct 2022 13:45:43 -0700 Subject: [PATCH 330/723] cmd/ssadump: disable run mode with runtime package Changes `ssadump -run` to ensure that the package runtime is not imported (changed from must be imported). For several years, the runtime package has used unsafe constructs x/tools/go/ssa/interp cannot interpret. This must have been failing a similar amount of time. This is unfortunate, but is unlikely to be addressed soon. For golang/go#43163 Change-Id: I9e2aee640ff7b1123e591e6c49cac9967c5e8da8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/441817 Run-TryBot: Tim King gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- cmd/ssadump/main.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/ssadump/main.go b/cmd/ssadump/main.go index 138e7f69ff2..cfb9122b24d 100644 --- a/cmd/ssadump/main.go +++ b/cmd/ssadump/main.go @@ -157,12 +157,15 @@ func doMain() error { // Build SSA for all packages. prog.Build() - // The interpreter needs the runtime package. - // It is a limitation of go/packages that - // we cannot add "runtime" to its initial set, - // we can only check that it is present. - if prog.ImportedPackage("runtime") == nil { - return fmt.Errorf("-run: program does not depend on runtime") + // Earlier versions of the interpreter needed the runtime + // package; however, interp cannot handle unsafe constructs + // used during runtime's package initialization at the moment. + // The key construct blocking support is: + // *((*T)(unsafe.Pointer(p))) + // Unfortunately, this means only trivial programs can be + // interpreted by ssadump. + if prog.ImportedPackage("runtime") != nil { + return fmt.Errorf("-run: program depends on runtime package (interpreter can run only trivial programs)") } if runtime.GOARCH != build.Default.GOARCH { From 19a5504ffc849045535bf26b02eb603559f42d5d Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Mon, 26 Sep 2022 13:59:36 -0400 Subject: [PATCH 331/723] gopls/internal/lsp: use the golang.org/x/vuln/exp/govulncheck Change-Id: Ifac1c9398a7d0923fa84c175ce8eea40e41a93f6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/435362 Reviewed-by: Suzy Mueller Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/go.mod | 2 +- gopls/go.sum | 4 ++ gopls/internal/govulncheck/summary.go | 46 ++++++++++++++++++ gopls/internal/lsp/cache/session.go | 4 +- gopls/internal/lsp/cache/view.go | 10 ++-- gopls/internal/lsp/cmd/usage/vulncheck.hlp | 2 + gopls/internal/lsp/cmd/vulncheck.go | 22 +++++---- gopls/internal/lsp/command.go | 23 +++++---- gopls/internal/lsp/command/interface.go | 1 + gopls/internal/lsp/mod/diagnostics.go | 55 ++++++++++++++++++---- gopls/internal/lsp/source/view.go | 6 +-- gopls/internal/regtest/misc/vuln_test.go | 9 ++-- gopls/internal/vulncheck/command.go | 24 ++++++++++ gopls/internal/vulncheck/vulncheck.go | 2 + 14 files changed, 162 insertions(+), 48 deletions(-) create mode 100644 gopls/internal/govulncheck/summary.go diff --git a/gopls/go.mod b/gopls/go.mod index e42749e71b8..9a16f38c1e6 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -12,7 +12,7 @@ require ( golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 golang.org/x/text v0.3.7 golang.org/x/tools v0.1.13-0.20220928184430-f80e98464e27 - golang.org/x/vuln v0.0.0-20221004232641-2aa0553d353b + golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.3.3 mvdan.cc/gofumpt v0.3.1 diff --git a/gopls/go.sum b/gopls/go.sum index 9ced54e9681..e4293b40ac6 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -71,6 +71,10 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/vuln v0.0.0-20221004232641-2aa0553d353b h1:8Tu9pgIV7kt8ulNtzidzpLl9E9l1i+U4QLdKG0ZzHyE= golang.org/x/vuln v0.0.0-20221004232641-2aa0553d353b/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68= +golang.org/x/vuln v0.0.0-20221006005703-27389ae96df4 h1:rj0uNKXz70TlwVjkDL/rF4qGHp0RzIXzDg7d7b0pnQo= +golang.org/x/vuln v0.0.0-20221006005703-27389ae96df4/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68= +golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 h1:KaYZQUtEEaV8aVADIHAuYBTjo77aUcCvC7KTGKM3J1I= +golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gopls/internal/govulncheck/summary.go b/gopls/internal/govulncheck/summary.go new file mode 100644 index 00000000000..e389b89a4ad --- /dev/null +++ b/gopls/internal/govulncheck/summary.go @@ -0,0 +1,46 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package govulncheck + +import "golang.org/x/vuln/osv" + +// TODO(hyangah): Find a better package for these types +// unless golang.org/x/vuln/exp/govulncheck starts to export these. + +// Summary is the govulncheck result. +type Summary struct { + // Vulnerabilities affecting the analysis target binary or source code. + Affecting []Vuln + // Vulnerabilities that may be imported but the vulnerable symbols are + // not called. For binary analysis, this will be always empty. + NonAffecting []Vuln +} + +// Vuln represents a vulnerability relevant to a (module, package). +type Vuln struct { + OSV *osv.Entry + PkgPath string // Package path. + ModPath string // Module path. + FoundIn string // @ if we know when it was introduced. Empty otherwise. + FixedIn string // @ if fix is available. Empty otherwise. + // Trace contains a call stack for each affecting symbol. + // For vulnerabilities found from binary analysis, and vulnerabilities + // that are reported as Unaffecting ones, this will be always empty. + Trace []Trace +} + +// Trace represents a sample trace for a vulnerable symbol. +type Trace struct { + Symbol string // Name of the detected vulnerable function or method. + Desc string // One-line description of the callstack. + Stack []StackEntry // Call stack. + Seen int // Number of similar call stacks. +} + +// StackEntry represents a call stack entry. +type StackEntry struct { + FuncName string // Function name is the function name, adjusted to remove pointer annotation. + CallSite string // Position of the call/reference site. It is one of the formats token.Pos.String() returns or empty if unknown. +} diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index cb18834d1cd..57bba47df80 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -13,7 +13,7 @@ import ( "sync" "sync/atomic" - "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" @@ -239,7 +239,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, name: name, folder: folder, moduleUpgrades: map[span.URI]map[string]string{}, - vulns: map[span.URI][]command.Vuln{}, + vulns: map[span.URI][]govulncheck.Vuln{}, filesByURI: map[span.URI]*fileBase{}, filesByBase: map[string][]*fileBase{}, rootURI: root, diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 6244d909c4a..bdf22817576 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -24,7 +24,7 @@ import ( "golang.org/x/mod/semver" exec "golang.org/x/sys/execabs" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" @@ -61,7 +61,7 @@ type View struct { // Each modfile has a map of module name to upgrade version. moduleUpgrades map[span.URI]map[string]string - vulns map[span.URI][]command.Vuln + vulns map[span.URI][]govulncheck.Vuln // keep track of files by uri and by basename, a single file may be mapped // to multiple uris, and the same basename may map to multiple files @@ -1032,16 +1032,16 @@ func (v *View) ClearModuleUpgrades(modfile span.URI) { delete(v.moduleUpgrades, modfile) } -func (v *View) Vulnerabilities(modfile span.URI) []command.Vuln { +func (v *View) Vulnerabilities(modfile span.URI) []govulncheck.Vuln { v.mu.Lock() defer v.mu.Unlock() - vulns := make([]command.Vuln, len(v.vulns[modfile])) + vulns := make([]govulncheck.Vuln, len(v.vulns[modfile])) copy(vulns, v.vulns[modfile]) return vulns } -func (v *View) SetVulnerabilities(modfile span.URI, vulns []command.Vuln) { +func (v *View) SetVulnerabilities(modfile span.URI, vulns []govulncheck.Vuln) { v.mu.Lock() defer v.mu.Unlock() diff --git a/gopls/internal/lsp/cmd/usage/vulncheck.hlp b/gopls/internal/lsp/cmd/usage/vulncheck.hlp index 4cdb8f3b640..4fbe573e22a 100644 --- a/gopls/internal/lsp/cmd/usage/vulncheck.hlp +++ b/gopls/internal/lsp/cmd/usage/vulncheck.hlp @@ -13,3 +13,5 @@ Usage: -config If true, the command reads a JSON-encoded package load configuration from stdin + -summary + If true, outputs a JSON-encoded govulnchecklib.Summary JSON diff --git a/gopls/internal/lsp/cmd/vulncheck.go b/gopls/internal/lsp/cmd/vulncheck.go index 3ae2175a962..93b9c106aef 100644 --- a/gopls/internal/lsp/cmd/vulncheck.go +++ b/gopls/internal/lsp/cmd/vulncheck.go @@ -18,8 +18,9 @@ import ( // vulncheck implements the vulncheck command. type vulncheck struct { - Config bool `flag:"config" help:"If true, the command reads a JSON-encoded package load configuration from stdin"` - app *Application + Config bool `flag:"config" help:"If true, the command reads a JSON-encoded package load configuration from stdin"` + AsSummary bool `flag:"summary" help:"If true, outputs a JSON-encoded govulnchecklib.Summary JSON"` + app *Application } type pkgLoadConfig struct { @@ -58,6 +59,7 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { return fmt.Errorf("vulncheck command is available only in gopls compiled with go1.18 or newer") } + // TODO(hyangah): what's wrong with allowing multiple targets? if len(args) > 1 { return tool.CommandLineErrorf("vulncheck accepts at most one package pattern") } @@ -65,25 +67,27 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { if len(args) == 1 { pattern = args[0] } + var cfg pkgLoadConfig if v.Config { if err := json.NewDecoder(os.Stdin).Decode(&cfg); err != nil { return tool.CommandLineErrorf("failed to parse cfg: %v", err) } } - - if vulnchecklib.Govulncheck == nil { - return fmt.Errorf("vulncheck feature is not available") - } - - loadCfg := &packages.Config{ + loadCfg := packages.Config{ Context: ctx, Tests: cfg.Tests, BuildFlags: cfg.BuildFlags, // inherit the current process's cwd and env. } - res, err := vulnchecklib.Govulncheck(ctx, loadCfg, pattern) + if v.AsSummary { + // vulnchecklib.Main calls os.Exit and never returns. + vulnchecklib.Main(loadCfg, args...) + return nil + } + // TODO(hyangah): delete. + res, err := vulnchecklib.Govulncheck(ctx, &loadCfg, pattern) if err != nil { return fmt.Errorf("vulncheck failed: %v", err) } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 7f7380e2991..e231b4bba4e 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -20,6 +20,7 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/debug" "golang.org/x/tools/gopls/internal/lsp/progress" @@ -849,7 +850,7 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc return errors.New("vulncheck feature is not available") } - cmd := exec.CommandContext(ctx, os.Args[0], "vulncheck", "-config", args.Pattern) + cmd := exec.CommandContext(ctx, os.Args[0], "vulncheck", "-summary", "-config", args.Pattern) cmd.Dir = filepath.Dir(args.URI.SpanURI().Filename()) var viewEnv []string @@ -879,28 +880,26 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc return fmt.Errorf("failed to run govulncheck: %v", err) } - var vulns command.VulncheckResult - if err := json.Unmarshal(stdout, &vulns); err != nil { + var summary govulncheck.Summary + if err := json.Unmarshal(stdout, &summary); err != nil { // TODO: for easy debugging, log the failed stdout somewhere? return fmt.Errorf("failed to parse govulncheck output: %v", err) } - deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), vulns.Vuln) + vulns := append(summary.Affecting, summary.NonAffecting...) + deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), vulns) c.s.diagnoseSnapshot(deps.snapshot, nil, false) - set := make(map[string]bool) - for _, v := range vulns.Vuln { - if len(v.CallStackSummaries) > 0 { - set[v.ID] = true - } - } - if len(set) == 0 { + if len(summary.Affecting) == 0 { return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ Type: protocol.Info, Message: "No vulnerabilities found", }) } - + set := make(map[string]bool) + for _, v := range summary.Affecting { + set[v.OSV.ID] = true + } list := make([]string, 0, len(set)) for k := range set { list = append(list, k) diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index fc5057580f5..23b9f655046 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -336,6 +336,7 @@ type StackEntry struct { } // Vuln models an osv.Entry and representative call stacks. +// TODO: deprecate type Vuln struct { // ID is the vulnerability ID (osv.Entry.ID). // https://ossf.github.io/osv-schema/#id-modified-fields diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 256abd13cfa..546c84cb07a 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -10,13 +10,16 @@ import ( "bytes" "context" "fmt" + "strings" "golang.org/x/mod/semver" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" + "golang.org/x/vuln/osv" ) // Diagnostics returns diagnostics for the modules in the workspace. @@ -158,6 +161,21 @@ func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh sou return upgradeDiagnostics, nil } +func pkgVersion(pkgVersion string) (pkg, ver string) { + if pkgVersion == "" { + return "", "" + } + at := strings.Index(pkgVersion, "@") + switch { + case at < 0: + return pkgVersion, "" + case at == 0: + return "", pkgVersion[1:] + default: + return pkgVersion[:at], pkgVersion[at+1:] + } +} + // ModVulnerabilityDiagnostics adds diagnostics for vulnerabilities in individual modules // if the vulnerability is recorded in the view. func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (vulnDiagnostics []*source.Diagnostic, err error) { @@ -173,7 +191,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, vs := snapshot.View().Vulnerabilities(fh.URI()) // TODO(suzmue): should we just store the vulnerabilities like this? - vulns := make(map[string][]command.Vuln) + vulns := make(map[string][]govulncheck.Vuln) for _, v := range vs { vulns[v.ModPath] = append(vulns[v.ModPath], v) } @@ -190,14 +208,14 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, for _, v := range vulnList { // Only show the diagnostic if the vulnerability was calculated // for the module at the current version. - if semver.IsValid(v.CurrentVersion) && semver.Compare(req.Mod.Version, v.CurrentVersion) != 0 { + if semver.IsValid(v.FoundIn) && semver.Compare(req.Mod.Version, v.FoundIn) != 0 { continue } - // Upgrade to the exact version we offer the user, not the most recent. // TODO(suzmue): Add an upgrade for module@latest. + // TODO(hakim): Produce fixes only for affecting vulnerabilities (if len(v.Trace) > 0) var fixes []source.SuggestedFix - if fixedVersion := v.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { + if fixedVersion := v.FixedIn; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { title := fmt.Sprintf("Upgrade to %v", fixedVersion) cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ URI: protocol.URIFromSpanURI(fh.URI()), @@ -211,7 +229,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, } severity := protocol.SeverityInformation - if len(v.CallStacks) > 0 { + if len(v.Trace) > 0 { severity = protocol.SeverityWarning } @@ -220,9 +238,9 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, Range: rng, Severity: severity, Source: source.Vulncheck, - Code: v.ID, - CodeHref: v.URL, - Message: formatMessage(&v), + Code: v.OSV.ID, + CodeHref: href(v.OSV), + Message: formatMessage(v), SuggestedFixes: fixes, }) } @@ -232,8 +250,8 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return vulnDiagnostics, nil } -func formatMessage(v *command.Vuln) string { - details := []byte(v.Details) +func formatMessage(v govulncheck.Vuln) string { + details := []byte(v.OSV.Details) // Remove any new lines that are not preceded or followed by a new line. for i, r := range details { if r == '\n' && i > 0 && details[i-1] != '\n' && i+1 < len(details) && details[i+1] != '\n' { @@ -242,3 +260,20 @@ func formatMessage(v *command.Vuln) string { } return fmt.Sprintf("%s has a known vulnerability: %s", v.ModPath, string(bytes.TrimSpace(details))) } + +// href returns a URL embedded in the entry if any. +// If no suitable URL is found, it returns a default entry in +// pkg.go.dev/vuln. +func href(vuln *osv.Entry) string { + for _, affected := range vuln.Affected { + if url := affected.DatabaseSpecific.URL; url != "" { + return url + } + } + for _, r := range vuln.References { + if r.Type == "WEB" { + return r.URL + } + } + return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vuln.ID) +} diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index cd0c8417f47..e986d1261f8 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -21,7 +21,7 @@ import ( "golang.org/x/mod/module" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/gocommand" @@ -271,11 +271,11 @@ type View interface { // Vulnerabilites returns known vulnerabilities for the given modfile. // TODO(suzmue): replace command.Vuln with a different type, maybe // https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary? - Vulnerabilities(modfile span.URI) []command.Vuln + Vulnerabilities(modfile span.URI) []govulncheck.Vuln // SetVulnerabilities resets the list of vulnerabilites that exists for the given modules // required by modfile. - SetVulnerabilities(modfile span.URI, vulnerabilities []command.Vuln) + SetVulnerabilities(modfile span.URI, vulnerabilities []govulncheck.Vuln) // FileKind returns the type of a file FileKind(FileHandle) FileKind diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 2fb9e31a3ce..1b55a6cabe0 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -16,6 +16,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/gopls/internal/vulncheck/vulntest" "golang.org/x/tools/internal/testenv" ) @@ -139,9 +140,7 @@ func main() { // When fetchinging stdlib package vulnerability info, // behave as if our go version is go1.18 for this testing. // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). - // See gopls/internal/vulncheck.goVersion - // which follows the convention used in golang.org/x/vuln/cmd/govulncheck. - "GOVERSION": "go1.18", + vulncheck.GoVersionForVulnTest: "go1.18", "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. }, Settings{ @@ -301,9 +300,7 @@ func TestRunVulncheckExp(t *testing.T) { // When fetching stdlib package vulnerability info, // behave as if our go version is go1.18 for this testing. // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). - // See gopls/internal/vulncheck.goVersion - // which follows the convention used in golang.org/x/vuln/cmd/govulncheck. - "GOVERSION": "go1.18", + vulncheck.GoVersionForVulnTest: "go1.18", "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. }, Settings{ diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 641c9ddeb67..e607eb415e6 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -19,6 +19,7 @@ import ( gvc "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/vuln/client" + gvcapi "golang.org/x/vuln/exp/govulncheck" "golang.org/x/vuln/osv" "golang.org/x/vuln/vulncheck" ) @@ -208,3 +209,26 @@ func trimPosPrefix(summary string) string { } return after } + +// GoVersionForVulnTest is an internal environment variable used in gopls +// testing to examine govulncheck behavior with a go version different +// than what `go version` returns in the system. +const GoVersionForVulnTest = "_GOPLS_TEST_VULNCHECK_GOVERSION" + +func init() { + Main = func(cfg packages.Config, patterns ...string) { + // never return + err := gvcapi.Main(gvcapi.Config{ + AnalysisType: "source", + OutputType: "summary", + Patterns: patterns, + SourceLoadConfig: &cfg, + GoVersion: os.Getenv(GoVersionForVulnTest), + }) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(0) + } +} diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go index d452045a5ef..7167ec1c266 100644 --- a/gopls/internal/vulncheck/vulncheck.go +++ b/gopls/internal/vulncheck/vulncheck.go @@ -18,3 +18,5 @@ import ( // Govulncheck runs the in-process govulncheck implementation. // With go1.18+, this is swapped with the real implementation. var Govulncheck func(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) = nil + +var Main func(cfg packages.Config, patterns ...string) = nil From b20ae4bcf794ac40a5e279e3db0a07ba44bcdcaf Mon Sep 17 00:00:00 2001 From: Alan Yee Date: Wed, 12 Oct 2022 02:11:58 +0000 Subject: [PATCH 332/723] README: format install command Separate command into a separate line in order to make it easier to copy and paste into the terminal Change-Id: I2e446475201c40ddb5054fe403ff23ed79b0742e GitHub-Last-Rev: bbd650aa12415725168b59ec934aafeeaf0f21aa GitHub-Pull-Request: golang/tools#346 Reviewed-on: https://go-review.googlesource.com/c/tools/+/361394 Auto-Submit: Bryan Mills Reviewed-by: Robert Findley gopls-CI: kokoro Reviewed-by: Cherry Mui Reviewed-by: Bryan Mills Run-TryBot: Bryan Mills --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cd8f0ac6e9..d9d7edd7332 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ Selected commands: - `cmd/toolstash` is a utility to simplify working with multiple versions of the Go toolchain. These commands may be fetched with a command such as -`go install golang.org/x/tools/cmd/goimports@latest`. +``` +go install golang.org/x/tools/cmd/goimports@latest +``` Selected packages: From d3752388a24ff16883172a850223be941fe96af3 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Thu, 29 Sep 2022 17:04:11 -0400 Subject: [PATCH 333/723] gopls: dedup upgrade code actions for vulncheck Make sure we aren't sending multiple code actions that do the same thing. This also adds a upgrade to latest code action. Change-Id: Ic9cecd0a9410648673d4afe63da5a940960a4afc Reviewed-on: https://go-review.googlesource.com/c/tools/+/436776 Reviewed-by: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Suzy Mueller --- gopls/internal/lsp/code_action.go | 22 +++- gopls/internal/lsp/mod/diagnostics.go | 71 ++++++++++-- gopls/internal/regtest/misc/vuln_test.go | 131 +++++++++++++++++++++-- 3 files changed, 201 insertions(+), 23 deletions(-) diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 1ad0cb417c1..d19cafc0704 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -81,16 +81,30 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara if err != nil { return nil, err } - vdiags, err := mod.ModVulnerabilityDiagnostics(ctx, snapshot, fh) + quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, append(diags, udiags...)) if err != nil { return nil, err } - // TODO(suzmue): Consider deduping upgrades from ModUpgradeDiagnostics and ModVulnerabilityDiagnostics. - quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, append(append(diags, udiags...), vdiags...)) + codeActions = append(codeActions, quickFixes...) + + vdiags, err := mod.ModVulnerabilityDiagnostics(ctx, snapshot, fh) if err != nil { return nil, err } - codeActions = append(codeActions, quickFixes...) + // Group vulnerabilities by location and then limit which code actions we return + // for each location. + m := make(map[protocol.Range][]*source.Diagnostic) + for _, v := range vdiags { + m[v.Range] = append(m[v.Range], v) + } + for _, sdiags := range m { + quickFixes, err = codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, sdiags) + if err != nil { + return nil, err + } + quickFixes = mod.SelectUpgradeCodeActions(quickFixes) + codeActions = append(codeActions, quickFixes...) + } } case source.Go: // Don't suggest fixes for generated files, since they are generally diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 546c84cb07a..d79442468c6 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -10,8 +10,10 @@ import ( "bytes" "context" "fmt" + "sort" "strings" + "golang.org/x/mod/modfile" "golang.org/x/mod/semver" "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/command" @@ -139,7 +141,7 @@ func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh sou return nil, err } // Upgrade to the exact version we offer the user, not the most recent. - title := fmt.Sprintf("Upgrade to %v", ver) + title := fmt.Sprintf("%s%v", upgradeCodeActionPrefix, ver) cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ URI: protocol.URIFromSpanURI(fh.URI()), AddRequire: false, @@ -176,6 +178,8 @@ func pkgVersion(pkgVersion string) (pkg, ver string) { } } +const upgradeCodeActionPrefix = "Upgrade to " + // ModVulnerabilityDiagnostics adds diagnostics for vulnerabilities in individual modules // if the vulnerability is recorded in the view. func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (vulnDiagnostics []*source.Diagnostic, err error) { @@ -212,20 +216,24 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, continue } // Upgrade to the exact version we offer the user, not the most recent. - // TODO(suzmue): Add an upgrade for module@latest. // TODO(hakim): Produce fixes only for affecting vulnerabilities (if len(v.Trace) > 0) var fixes []source.SuggestedFix if fixedVersion := v.FixedIn; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { - title := fmt.Sprintf("Upgrade to %v", fixedVersion) - cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ - URI: protocol.URIFromSpanURI(fh.URI()), - AddRequire: false, - GoCmdArgs: []string{req.Mod.Path + "@" + fixedVersion}, - }) + cmd, err := getUpgradeCodeAction(fh, req, fixedVersion) if err != nil { return nil, err } - fixes = append(fixes, source.SuggestedFixFromCommand(cmd, protocol.QuickFix)) + // Add an upgrade for module@latest. + // TODO(suzmue): verify if latest is the same as fixedVersion. + latest, err := getUpgradeCodeAction(fh, req, "latest") + if err != nil { + return nil, err + } + + fixes = []source.SuggestedFix{ + source.SuggestedFixFromCommand(cmd, protocol.QuickFix), + source.SuggestedFixFromCommand(latest, protocol.QuickFix), + } } severity := protocol.SeverityInformation @@ -277,3 +285,48 @@ func href(vuln *osv.Entry) string { } return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vuln.ID) } + +func getUpgradeCodeAction(fh source.FileHandle, req *modfile.Require, version string) (protocol.Command, error) { + cmd, err := command.NewUpgradeDependencyCommand(upgradeTitle(version), command.DependencyArgs{ + URI: protocol.URIFromSpanURI(fh.URI()), + AddRequire: false, + GoCmdArgs: []string{req.Mod.Path + "@" + version}, + }) + if err != nil { + return protocol.Command{}, err + } + return cmd, nil +} + +func upgradeTitle(fixedVersion string) string { + title := fmt.Sprintf("%s%v", upgradeCodeActionPrefix, fixedVersion) + return title +} + +// SelectUpgradeCodeActions takes a list of upgrade code actions for a +// required module and returns a more selective list of upgrade code actions, +// where the code actions have been deduped. +func SelectUpgradeCodeActions(actions []protocol.CodeAction) []protocol.CodeAction { + // TODO(suzmue): we can further limit the code actions to only return the most + // recent version that will fix all the vulnerabilities. + + set := make(map[string]protocol.CodeAction) + for _, action := range actions { + set[action.Command.Title] = action + } + var result []protocol.CodeAction + for _, action := range set { + result = append(result, action) + } + // Sort results by version number, latest first. + // There should be no duplicates at this point. + sort.Slice(result, func(i, j int) bool { + vi, vj := getUpgradeVersion(result[i]), getUpgradeVersion(result[j]) + return vi == "latest" || (vj != "latest" && semver.Compare(vi, vj) > 0) + }) + return result +} + +func getUpgradeVersion(p protocol.CodeAction) string { + return strings.TrimPrefix(p.Title, upgradeCodeActionPrefix) +} diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 1b55a6cabe0..ecebfb52202 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -9,7 +9,6 @@ package misc import ( "context" - "strings" "testing" "golang.org/x/tools/gopls/internal/lsp/command" @@ -310,7 +309,6 @@ func TestRunVulncheckExp(t *testing.T) { }, ).Run(t, workspace1, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - env.ExecuteCodeLensCommand("go.mod", command.Tidy) env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp) d := &protocol.PublishDiagnosticsParams{} @@ -318,20 +316,117 @@ func TestRunVulncheckExp(t *testing.T) { CompletedWork("govulncheck", 1, true), ShownMessage("Found"), OnceMet( - env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "golang.org/amod has a known vulnerability: vuln in amod"), - env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/amod`, "golang.org/amod has a known vulnerability: unaffecting vulnerability"), - env.DiagnosticAtRegexpWithMessage("go.mod", `golang.org/bmod`, "golang.org/bmod has a known vulnerability: vuln in bmod\n\nThis is a long description of this vulnerability."), + env.DiagnosticAtRegexp("go.mod", `golang.org/amod`), ReadDiagnostics("go.mod", d), ), ) - var toFix []protocol.Diagnostic - for _, diag := range d.Diagnostics { - if strings.Contains(diag.Message, "vuln in ") { - toFix = append(toFix, diag) + type diagnostic struct { + msg string + severity protocol.DiagnosticSeverity + // codeActions is a list titles of code actions that we get with this + // diagnostics as the context. + codeActions []string + } + // wantDiagnostics maps a module path in the require + // section of a go.mod to diagnostics that will be returned + // when running vulncheck. + wantDiagnostics := map[string]struct { + // applyAction is the title of the code action to run for this module. + // If empty, no code actions will be executed. + applyAction string + // diagnostics is the list of diagnostics we expect at the require line for + // the module path. + diagnostics []diagnostic + // codeActions is a list titles of code actions that we get with context + // diagnostics. + codeActions []string + }{ + "golang.org/amod": { + applyAction: "Upgrade to v1.0.4", + diagnostics: []diagnostic{ + { + msg: "golang.org/amod has a known vulnerability: vuln in amod", + severity: protocol.SeverityWarning, + codeActions: []string{ + "Upgrade to latest", + "Upgrade to v1.0.4", + }, + }, + { + msg: "golang.org/amod has a known vulnerability: unaffecting vulnerability", + severity: protocol.SeverityInformation, + codeActions: []string{ + "Upgrade to latest", + "Upgrade to v1.0.6", + }, + }, + }, + codeActions: []string{ + "Upgrade to latest", + "Upgrade to v1.0.6", + "Upgrade to v1.0.4", + }, + }, + "golang.org/bmod": { + diagnostics: []diagnostic{ + { + msg: "golang.org/bmod has a known vulnerability: vuln in bmod\n\nThis is a long description of this vulnerability.", + severity: protocol.SeverityWarning, + }, + }, + }, + } + + for mod, want := range wantDiagnostics { + pos := env.RegexpSearch("go.mod", mod) + var modPathDiagnostics []protocol.Diagnostic + for _, w := range want.diagnostics { + // Find the diagnostics at pos. + var diag *protocol.Diagnostic + for _, g := range d.Diagnostics { + g := g + if g.Range.Start == pos.ToProtocolPosition() && w.msg == g.Message { + modPathDiagnostics = append(modPathDiagnostics, g) + diag = &g + break + } + } + if diag == nil { + t.Errorf("no diagnostic at %q matching %q found\n", mod, w.msg) + continue + } + if diag.Severity != w.severity { + t.Errorf("incorrect severity for %q, expected %s got %s\n", w.msg, w.severity, diag.Severity) + } + + gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag}) + if !sameCodeActions(gotActions, w.codeActions) { + t.Errorf("code actions for %q do not match, expected %v, got %v\n", w.msg, w.codeActions, gotActions) + continue + } + } + + // Check that the actions we get when including all diagnostics at a location return the same result + gotActions := env.CodeAction("go.mod", modPathDiagnostics) + if !sameCodeActions(gotActions, want.codeActions) { + t.Errorf("code actions for %q do not match, expected %v, got %v\n", mod, want.codeActions, gotActions) + continue + } + + // Apply the code action matching applyAction. + if want.applyAction == "" { + continue } + for _, action := range gotActions { + if action.Title == want.applyAction { + env.ApplyCodeAction(action) + break + } + } + } - env.ApplyQuickFixes("go.mod", toFix) + env.Await(env.DoneWithChangeWatchedFiles()) wantGoMod := `module golang.org/entry @@ -349,3 +444,19 @@ require ( } }) } + +func sameCodeActions(gotActions []protocol.CodeAction, want []string) bool { + gotTitles := make([]string, len(gotActions)) + for i, ca := range gotActions { + gotTitles[i] = ca.Title + } + if len(gotTitles) != len(want) { + return false + } + for i := range want { + if gotTitles[i] != want[i] { + return false + } + } + return true +} From 3beecff0f65ddefa5442beabf188a6c8411dcf82 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 10 Oct 2022 22:49:16 -0400 Subject: [PATCH 334/723] gopls/internal/span: some cleanups This change eliminates these redundant helper functions - cache.rangeFromPositions - source.LineToRange - source.ByteOffsetsToRange and makes various other simplifications and documentation improvements. Change-Id: Ic820ab560d71b88bde00b005e3a919334a5d1856 Reviewed-on: https://go-review.googlesource.com/c/tools/+/442015 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/check.go | 2 +- gopls/internal/lsp/cache/load.go | 4 +- gopls/internal/lsp/cache/mod.go | 4 +- gopls/internal/lsp/cache/mod_tidy.go | 24 ++------ gopls/internal/lsp/diagnostics.go | 6 +- gopls/internal/lsp/lsp_test.go | 24 +++----- gopls/internal/lsp/mod/code_lens.go | 4 +- gopls/internal/lsp/mod/diagnostics.go | 4 +- gopls/internal/lsp/mod/hover.go | 4 +- gopls/internal/lsp/protocol/span.go | 65 +++++++++++++++++----- gopls/internal/lsp/source/fix.go | 12 ++-- gopls/internal/lsp/source/util.go | 24 +------- gopls/internal/lsp/text_synchronization.go | 6 +- gopls/internal/lsp/work/diagnostics.go | 2 +- gopls/internal/lsp/work/hover.go | 4 +- gopls/internal/span/span.go | 3 +- gopls/internal/span/utf16.go | 3 + 17 files changed, 95 insertions(+), 100 deletions(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 0b37135fc00..a186a816853 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -729,7 +729,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost if reference == nil { continue } - rng, err := rangeFromPositions(pm.Mapper, reference.Start, reference.End) + rng, err := pm.Mapper.OffsetRange(reference.Start.Byte, reference.End.Byte) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 3ef5301f83c..c33561fe2b4 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -392,8 +392,8 @@ func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, fi } case source.Mod: if pmf, err := s.ParseMod(ctx, fh); err == nil { - if pmf.File.Module != nil && pmf.File.Module.Syntax != nil { - rng, _ = rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End) + if mod := pmf.File.Module; mod != nil && mod.Syntax != nil { + rng, _ = pmf.Mapper.OffsetRange(mod.Syntax.Start.Byte, mod.Syntax.End.Byte) } } } diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index f8404e7a9eb..97bdb2f019a 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -79,7 +79,7 @@ func parseModImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedModu return nil, fmt.Errorf("unexpected parse error type %v", parseErr) } for _, mfErr := range mfErrList { - rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos) + rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) if err != nil { return nil, err } @@ -155,7 +155,7 @@ func parseWorkImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedWor return nil, fmt.Errorf("unexpected parse error type %v", parseErr) } for _, mfErr := range mfErrList { - rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos) + rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index 63346dc548e..8e4e060904a 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -287,7 +287,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.Pars // unusedDiagnostic returns a source.Diagnostic for an unused require. func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) { - rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End) + rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) if err != nil { return nil, err } @@ -313,7 +313,7 @@ func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagno // directnessDiagnostic extracts errors when a dependency is labeled indirect when // it should be direct and vice versa. func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits source.DiffFunction) (*source.Diagnostic, error) { - rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End) + rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) if err != nil { return nil, err } @@ -325,8 +325,8 @@ func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, comput if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 { end := comments.Suffix[0].Start end.LineRune += len(comments.Suffix[0].Token) - end.Byte += len([]byte(comments.Suffix[0].Token)) - rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end) + end.Byte += len(comments.Suffix[0].Token) + rng, err = m.OffsetRange(comments.Suffix[0].Start.Byte, end.Byte) if err != nil { return nil, err } @@ -359,7 +359,7 @@ func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*so if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil { start, end := pm.File.Module.Syntax.Span() var err error - rng, err = rangeFromPositions(pm.Mapper, start, end) + rng, err = pm.Mapper.OffsetRange(start.Byte, end.Byte) if err != nil { return nil, err } @@ -429,11 +429,7 @@ func missingModuleForImport(file *token.File, m *protocol.ColumnMapper, imp *ast if req.Syntax == nil { return nil, fmt.Errorf("no syntax for %v", req) } - spn, err := span.NewRange(file, imp.Path.Pos(), imp.Path.End()).Span() - if err != nil { - return nil, err - } - rng, err := m.Range(spn) + rng, err := m.PosRange(imp.Path.Pos(), imp.Path.End()) if err != nil { return nil, err } @@ -447,14 +443,6 @@ func missingModuleForImport(file *token.File, m *protocol.ColumnMapper, imp *ast }, nil } -func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) { - spn, err := spanFromPositions(m, s, e) - if err != nil { - return protocol.Range{}, err - } - return m.Range(spn) -} - func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) { toPoint := func(offset int) (span.Point, error) { l, c, err := span.ToPosition(m.TokFile, offset) diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index f6f9f7935a3..5a47626b6e7 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -545,11 +545,7 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps if !pgf.File.Name.Pos().IsValid() { return nil } - spn, err := span.NewRange(pgf.Tok, pgf.File.Name.Pos(), pgf.File.Name.End()).Span() - if err != nil { - return nil - } - rng, err := pgf.Mapper.Range(spn) + rng, err := pgf.Mapper.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) if err != nil { return nil } diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 3969e88216c..d966d94397d 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -360,26 +360,18 @@ func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.Fol res := contents // Apply the edits from the end of the file forward // to preserve the offsets + // TODO(adonovan): factor to use diff.ApplyEdits, which validates the input. for i := len(ranges) - 1; i >= 0; i-- { - fRange := ranges[i] - spn, err := m.RangeSpan(protocol.Range{ - Start: protocol.Position{ - Line: fRange.StartLine, - Character: fRange.StartCharacter, - }, - End: protocol.Position{ - Line: fRange.EndLine, - Character: fRange.EndCharacter, - }, - }) + r := ranges[i] + start, err := m.Point(protocol.Position{r.StartLine, r.StartCharacter}) if err != nil { return "", err } - start := spn.Start().Offset() - end := spn.End().Offset() - - tmp := res[0:start] + foldedText - res = tmp + res[end:] + end, err := m.Point(protocol.Position{r.EndLine, r.EndCharacter}) + if err != nil { + return "", err + } + res = res[:start.Offset()] + foldedText + res[end.Offset():] } return res, nil } diff --git a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go index daafee0cb52..01d75d92a20 100644 --- a/gopls/internal/lsp/mod/code_lens.go +++ b/gopls/internal/lsp/mod/code_lens.go @@ -134,7 +134,7 @@ func moduleStmtRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Ra return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI()) } syntax := pm.File.Module.Syntax - return source.LineToRange(pm.Mapper, fh.URI(), syntax.Start, syntax.End) + return pm.Mapper.OffsetRange(syntax.Start.Byte, syntax.End.Byte) } // firstRequireRange returns the range for the first "require" in the given @@ -155,7 +155,7 @@ func firstRequireRange(fh source.FileHandle, pm *source.ParsedModule) (protocol. if start.Byte == 0 || firstRequire.Start.Byte < start.Byte { start, end = firstRequire.Start, firstRequire.End } - return source.LineToRange(pm.Mapper, fh.URI(), start, end) + return pm.Mapper.OffsetRange(start.Byte, end.Byte) } func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index d79442468c6..4b4f421ece1 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -136,7 +136,7 @@ func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh sou if !ok || req.Mod.Version == ver { continue } - rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End) + rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) if err != nil { return nil, err } @@ -205,7 +205,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, if !ok { continue } - rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End) + rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 0f3dfc478de..29812749df3 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -11,9 +11,9 @@ import ( "strings" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" ) func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { @@ -78,7 +78,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Get the range to highlight for the hover. - rng, err := source.ByteOffsetsToRange(pm.Mapper, fh.URI(), startPos, endPos) + rng, err := pm.Mapper.OffsetRange(startPos, endPos) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index bfa2f8111e7..2d87c081274 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -4,6 +4,34 @@ // this file contains protocol<->span converters +// Here's a handy guide for your tour of the location zoo: +// +// Imports: source --> lsppos --> protocol --> span --> token +// +// source.MappedRange = (span.Range, protocol.ColumnMapper) +// +// lsppos.TokenMapper = (token.File, lsppos.Mapper) +// lsppos.Mapper = (line offset table, content) +// +// protocol.ColumnMapper = (URI, token.File, content) +// protocol.Location = (URI, protocol.Range) +// protocol.Range = (start, end Position) +// protocol.Position = (line, char uint32) 0-based UTF-16 +// +// span.Point = (line?, col?, offset?) 1-based UTF-8 +// span.Span = (uri URI, start, end span.Point) +// span.Range = (file token.File, start, end token.Pos) +// +// token.Pos +// token.FileSet +// offset int +// +// TODO(adonovan): simplify this picture. Eliminate the optionality of +// span.{Span,Point}'s position and offset fields: work internally in +// terms of offsets (like span.Range), and require a mapper to convert +// them to protocol (UTF-16) line/col form. Stop honoring //line +// directives. + package protocol import ( @@ -12,6 +40,7 @@ import ( "go/token" "unicode/utf8" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" ) @@ -67,7 +96,7 @@ func (m *ColumnMapper) Range(s span.Span) (Range, error) { if span.CompareURI(m.URI, s.URI()) != 0 { return Range{}, fmt.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI()) } - s, err := s.WithAll(m.TokFile) + s, err := s.WithOffset(m.TokFile) if err != nil { return Range{}, err } @@ -97,6 +126,19 @@ func (m *ColumnMapper) OffsetRange(start, end int) (Range, error) { return Range{Start: startPosition, End: endPosition}, nil } +// PosRange returns a protocol Range for the token.Pos interval Content[start:end]. +func (m *ColumnMapper) PosRange(start, end token.Pos) (Range, error) { + startOffset, err := safetoken.Offset(m.TokFile, start) + if err != nil { + return Range{}, fmt.Errorf("start: %v", err) + } + endOffset, err := safetoken.Offset(m.TokFile, end) + if err != nil { + return Range{}, fmt.Errorf("end: %v", err) + } + return m.OffsetRange(startOffset, endOffset) +} + // Position returns the protocol position for the specified point, // which must have a byte offset. func (m *ColumnMapper) Position(p span.Point) (Position, error) { @@ -163,6 +205,8 @@ func (m *ColumnMapper) Span(l Location) (span.Span, error) { return m.RangeSpan(l.Range) } +// RangeSpan converts a UTF-16 range to a Span with both the +// position (line/col) and offset fields populated. func (m *ColumnMapper) RangeSpan(r Range) (span.Span, error) { start, err := m.Point(r.Start) if err != nil { @@ -183,22 +227,13 @@ func (m *ColumnMapper) RangeToSpanRange(r Range) (span.Range, error) { return spn.Range(m.TokFile) } -// Pos returns the token.Pos of p within the mapped file. +// Pos returns the token.Pos of protocol position p within the mapped file. func (m *ColumnMapper) Pos(p Position) (token.Pos, error) { start, err := m.Point(p) if err != nil { return token.NoPos, err } - // TODO: refactor the span package to avoid creating this unnecessary end position. - spn, err := span.New(m.URI, start, start).WithAll(m.TokFile) - if err != nil { - return token.NoPos, err - } - rng, err := spn.Range(m.TokFile) - if err != nil { - return token.NoPos, err - } - return rng.Start, nil + return safetoken.Pos(m.TokFile, start.Offset()) } // Offset returns the utf-8 byte offset of p within the mapped file. @@ -210,10 +245,12 @@ func (m *ColumnMapper) Offset(p Position) (int, error) { return start.Offset(), nil } -// Point returns a span.Point for p within the mapped file. The resulting point -// always has an Offset. +// Point returns a span.Point for the protocol position p within the mapped file. +// The resulting point has a valid Position and Offset. func (m *ColumnMapper) Point(p Position) (span.Point, error) { line := int(p.Line) + 1 + + // Find byte offset of start of containing line. offset, err := span.ToOffset(m.TokFile, line, 1) if err != nil { return span.Point{}, err diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 1ce6d758b39..a4c6676cf05 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -93,15 +93,11 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi if !end.IsValid() { end = edit.Pos } - spn, err := span.NewRange(tokFile, edit.Pos, end).Span() + fh, err := snapshot.GetVersionedFile(ctx, span.URIFromPath(tokFile.Name())) if err != nil { return nil, err } - fh, err := snapshot.GetVersionedFile(ctx, spn.URI()) - if err != nil { - return nil, err - } - te, ok := editsPerFile[spn.URI()] + te, ok := editsPerFile[fh.URI()] if !ok { te = &protocol.TextDocumentEdit{ TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ @@ -111,13 +107,13 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi }, }, } - editsPerFile[spn.URI()] = te + editsPerFile[fh.URI()] = te } _, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return nil, err } - rng, err := pgf.Mapper.Range(spn) + rng, err := pgf.Mapper.PosRange(edit.Pos, end) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index b06992e4823..c933fbadd00 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -17,7 +17,6 @@ import ( "strconv" "strings" - "golang.org/x/mod/modfile" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" @@ -26,6 +25,9 @@ import ( // MappedRange provides mapped protocol.Range for a span.Range, accounting for // UTF-16 code points. +// +// TOOD(adonovan): stop treating //line directives specially, and +// eliminate this type. All callers need either m, or a protocol.Range. type MappedRange struct { spanRange span.Range // the range in the compiled source (package.CompiledGoFiles) m *protocol.ColumnMapper // a mapper of the edited source (package.GoFiles) @@ -555,26 +557,6 @@ func IsCommandLineArguments(s string) bool { return strings.Contains(s, "command-line-arguments") } -// LineToRange creates a Range spanning start and end. -func LineToRange(m *protocol.ColumnMapper, uri span.URI, start, end modfile.Position) (protocol.Range, error) { - return ByteOffsetsToRange(m, uri, start.Byte, end.Byte) -} - -// ByteOffsetsToRange creates a range spanning start and end. -func ByteOffsetsToRange(m *protocol.ColumnMapper, uri span.URI, start, end int) (protocol.Range, error) { - line, col, err := span.ToPosition(m.TokFile, start) - if err != nil { - return protocol.Range{}, err - } - s := span.NewPoint(line, col, start) - line, col, err = span.ToPosition(m.TokFile, end) - if err != nil { - return protocol.Range{}, err - } - e := span.NewPoint(line, col, end) - return m.Range(span.New(uri, s, e)) -} - // RecvIdent returns the type identifier of a method receiver. // e.g. A for all of A, *A, A[T], *A[T], etc. func RecvIdent(recv *ast.FieldList) *ast.Ident { diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go index 9687528c645..63bc0e8e561 100644 --- a/gopls/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -356,6 +356,9 @@ func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, chan return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err) } for _, change := range changes { + // TODO(adonovan): refactor to use diff.Apply, which is robust w.r.t. + // out-of-order or overlapping changes---and much more efficient. + // Make sure to update column mapper along with the content. m := protocol.NewColumnMapper(uri, content) if change.Range == nil { @@ -365,9 +368,6 @@ func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, chan if err != nil { return nil, err } - if !spn.HasOffset() { - return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) - } start, end := spn.Start().Offset(), spn.End().Offset() if end < start { return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) diff --git a/gopls/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go index 5d3a32897d2..0d0f4eb18d9 100644 --- a/gopls/internal/lsp/work/diagnostics.go +++ b/gopls/internal/lsp/work/diagnostics.go @@ -59,7 +59,7 @@ func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh source // Add diagnostic if a directory does not contain a module. var diagnostics []*source.Diagnostic for _, use := range pw.File.Use { - rng, err := source.LineToRange(pw.Mapper, fh.URI(), use.Syntax.Start, use.Syntax.End) + rng, err := pw.Mapper.OffsetRange(use.Syntax.Start.Byte, use.Syntax.End.Byte) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/work/hover.go b/gopls/internal/lsp/work/hover.go index ea4c1e58d9f..641028b16e6 100644 --- a/gopls/internal/lsp/work/hover.go +++ b/gopls/internal/lsp/work/hover.go @@ -11,9 +11,9 @@ import ( "go/token" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" ) func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { @@ -56,7 +56,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, mod := pm.File.Module.Mod // Get the range to highlight for the hover. - rng, err := source.ByteOffsetsToRange(pw.Mapper, fh.URI(), pathStart, pathEnd) + rng, err := pw.Mapper.OffsetRange(pathStart, pathEnd) if err != nil { return nil, err } diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go index 5b8d31ef03c..333048ae8c6 100644 --- a/gopls/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -209,7 +209,8 @@ func (s Span) Format(f fmt.State, c rune) { } } -func (s Span) WithPosition(tf *token.File) (Span, error) { +// (Currently unused, but we gain little yet by deleting it.) +func (s Span) withPosition(tf *token.File) (Span, error) { if err := s.update(tf, true, false); err != nil { return Span{}, err } diff --git a/gopls/internal/span/utf16.go b/gopls/internal/span/utf16.go index f4c93a6ead3..0f8e1bcacdf 100644 --- a/gopls/internal/span/utf16.go +++ b/gopls/internal/span/utf16.go @@ -13,6 +13,9 @@ import ( // supplied file contents. // This is used to convert from the native (always in bytes) column // representation and the utf16 counts used by some editors. +// +// TODO(adonovan): this function is unused except by its test. Delete, +// or consolidate with (*protocol.ColumnMapper).utf16Column. func ToUTF16Column(p Point, content []byte) (int, error) { if !p.HasPosition() { return -1, fmt.Errorf("ToUTF16Column: point is missing position") From b2533141c6916af4859083b52e19d2cb17bf6a56 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 10 Oct 2022 13:50:45 -0400 Subject: [PATCH 335/723] gopls/internal/lsp/cache: add support for loading standalone main files Add support in gopls for working on "standalone main files", which are Go source files that should be treated as standalone packages. Standalone files are identified by a specific build tag, which may be configured via the new standaloneTags setting. For example, it is common to use the directive "//go:build ignore" to colocate standalone files with other package files. Specifically, - add a new loadScope interface for use in snapshot.load, to add a bit of type safety - add a new standaloneTags setting to allow configuring the set of build constraints that define standalone main files - add an isStandaloneFile function that detects standalone files based on build constraints - implement the loading of standalone files, by querying go/packages for the standalone file path - rewrite getOrLoadIDsForURI, which had inconsistent behavior with respect to error handling and the experimentalUseInvalidMetadata setting - update the WorkspaceSymbols handler to properly format command-line-arguments packages - add regression tests for LSP behavior with standalone files, and for dynamic configuration of standalone files Fixes golang/go#49657 Change-Id: I7b79257a984a87b67e476c32dec3c122f9bbc636 Reviewed-on: https://go-review.googlesource.com/c/tools/+/441877 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/doc/settings.md | 23 ++ gopls/internal/lsp/cache/load.go | 30 ++- gopls/internal/lsp/cache/metadata.go | 5 + gopls/internal/lsp/cache/pkg.go | 19 +- gopls/internal/lsp/cache/snapshot.go | 119 ++++++--- gopls/internal/lsp/cache/standalone_go115.go | 14 + gopls/internal/lsp/cache/standalone_go116.go | 52 ++++ .../lsp/cache/standalone_go116_test.go | 90 +++++++ gopls/internal/lsp/cache/view.go | 7 +- gopls/internal/lsp/diagnostics.go | 2 +- gopls/internal/lsp/source/api_json.go | 7 + gopls/internal/lsp/source/options.go | 55 ++++ gopls/internal/lsp/source/view.go | 9 +- gopls/internal/lsp/source/workspace_symbol.go | 6 + .../regtest/diagnostics/diagnostics_test.go | 8 +- .../regtest/misc/workspace_symbol_test.go | 11 +- .../regtest/workspace/metadata_test.go | 17 +- .../regtest/workspace/standalone_test.go | 249 ++++++++++++++++++ 18 files changed, 656 insertions(+), 67 deletions(-) create mode 100644 gopls/internal/lsp/cache/standalone_go115.go create mode 100644 gopls/internal/lsp/cache/standalone_go116.go create mode 100644 gopls/internal/lsp/cache/standalone_go116_test.go create mode 100644 gopls/internal/regtest/workspace/standalone_test.go diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 01b5d1a4da6..34fb8d01ce8 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -174,6 +174,29 @@ version of gopls (https://go.dev/issue/55333). Default: `false`. +#### **standaloneTags** *[]string* + +standaloneTags specifies a set of build constraints that identify +individual Go source files that make up the entire main package of an +executable. + +A common example of standalone main files is the convention of using the +directive `//go:build ignore` to denote files that are not intended to be +included in any package, for example because they are invoked directly by +the developer using `go run`. + +Gopls considers a file to be a standalone main file if and only if it has +package name "main" and has a build directive of the exact form +"//go:build tag" or "// +build tag", where tag is among the list of tags +configured by this setting. Notably, if the build constraint is more +complicated than a simple tag (such as the composite constraint +`//go:build tag && go1.18`), the file is not considered to be a standalone +main file. + +This setting is only supported when gopls is built with Go 1.16 or later. + +Default: `["ignore"]`. + ### Formatting #### **local** *string* diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index c33561fe2b4..e930eb4f4d5 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -7,6 +7,7 @@ package cache import ( "bytes" "context" + "errors" "fmt" "io/ioutil" "os" @@ -28,12 +29,15 @@ import ( var loadID uint64 // atomic identifier for loads +// errNoPackages indicates that a load query matched no packages. +var errNoPackages = errors.New("no packages returned") + // load calls packages.Load for the given scopes, updating package metadata, // import graph, and mapped files with the result. // // The resulting error may wrap the moduleErrorMap error type, representing // errors associated with specific modules. -func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) (err error) { +func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadScope) (err error) { id := atomic.AddUint64(&loadID, 1) eventName := fmt.Sprintf("go/packages.Load #%d", id) // unique name for logging @@ -45,7 +49,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf moduleQueries := make(map[string]string) for _, scope := range scopes { switch scope := scope.(type) { - case PackagePath: + case packageLoadScope: if source.IsCommandLineArguments(string(scope)) { panic("attempted to load command-line-arguments") } @@ -53,14 +57,24 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf // partial workspace load. In those cases, the paths came back from // go list and should already be GOPATH-vendorized when appropriate. query = append(query, string(scope)) - case fileURI: + + case fileLoadScope: uri := span.URI(scope) - // Don't try to load a file that doesn't exist. fh := s.FindFile(uri) if fh == nil || s.View().FileKind(fh) != source.Go { + // Don't try to load a file that doesn't exist, or isn't a go file. continue } - query = append(query, fmt.Sprintf("file=%s", uri.Filename())) + contents, err := fh.Read() + if err != nil { + continue + } + if isStandaloneFile(contents, s.view.Options().StandaloneTags) { + query = append(query, uri.Filename()) + } else { + query = append(query, fmt.Sprintf("file=%s", uri.Filename())) + } + case moduleLoadScope: switch scope { case "std", "cmd": @@ -70,6 +84,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf query = append(query, modQuery) moduleQueries[modQuery] = string(scope) } + case viewLoadScope: // If we are outside of GOPATH, a module, or some other known // build system, don't load subdirectories. @@ -78,6 +93,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf } else { query = append(query, "./...") } + default: panic(fmt.Sprintf("unknown scope type %T", scope)) } @@ -136,9 +152,9 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf if len(pkgs) == 0 { if err == nil { - err = fmt.Errorf("no packages returned") + err = errNoPackages } - return fmt.Errorf("%v: %w", err, source.PackagesLoadError) + return fmt.Errorf("packages.Load error: %w", err) } moduleErrs := make(map[string][]packages.Error) // module path -> errors diff --git a/gopls/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go index 66c679b18d8..2fa87eb1fc2 100644 --- a/gopls/internal/lsp/cache/metadata.go +++ b/gopls/internal/lsp/cache/metadata.go @@ -41,6 +41,11 @@ type Metadata struct { Config *packages.Config } +// PackageID implements the source.Metadata interface. +func (m *Metadata) PackageID() string { + return string(m.ID) +} + // Name implements the source.Metadata interface. func (m *Metadata) PackageName() string { return string(m.Name) diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 76e29eef027..44fe855c0d3 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -32,13 +32,24 @@ type pkg struct { hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors } -// Declare explicit types for files and directories to distinguish between the two. +// A loadScope defines a package loading scope for use with go/packages. +type loadScope interface { + aScope() +} + type ( - fileURI span.URI - moduleLoadScope string - viewLoadScope span.URI + fileLoadScope span.URI // load packages containing a file (including command-line-arguments) + packageLoadScope string // load a specific package + moduleLoadScope string // load packages in a specific module + viewLoadScope span.URI // load the workspace ) +// Implement the loadScope interface. +func (fileLoadScope) aScope() {} +func (packageLoadScope) aScope() {} +func (moduleLoadScope) aScope() {} +func (viewLoadScope) aScope() {} + func (p *pkg) ID() string { return string(p.m.ID) } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 1587a5d178d..f070fe3c1cc 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -712,51 +712,94 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode return phs, nil } +// getOrLoadIDsForURI returns package IDs associated with the file uri. If no +// such packages exist or if they are known to be stale, it reloads the file. +// +// If experimentalUseInvalidMetadata is set, this function may return package +// IDs with invalid metadata. func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]PackageID, error) { + useInvalidMetadata := s.useInvalidMetadata() + s.mu.Lock() + + // Start with the set of package associations derived from the last load. ids := s.meta.ids[uri] - reload := len(ids) == 0 + + hasValidID := false // whether we have any valid package metadata containing uri + shouldLoad := false // whether any packages containing uri are marked 'shouldLoad' for _, id := range ids { - // If the file is part of a package that needs reloading, reload it now to - // improve our responsiveness. + // TODO(rfindley): remove the defensiveness here. s.meta.metadata[id] must + // exist. + if m, ok := s.meta.metadata[id]; ok && m.Valid { + hasValidID = true + } if len(s.shouldLoad[id]) > 0 { - reload = true - break + shouldLoad = true } - // TODO(golang/go#36918): Previously, we would reload any package with - // missing dependencies. This is expensive and results in too many - // calls to packages.Load. Determine what we should do instead. } + + // Check if uri is known to be unloadable. + // + // TODO(rfindley): shouldn't we also mark uri as unloadable if the load below + // fails? Otherwise we endlessly load files with no packages. + _, unloadable := s.unloadableFiles[uri] + s.mu.Unlock() - if reload { - scope := fileURI(uri) + // Special case: if experimentalUseInvalidMetadata is set and we have any + // ids, just return them. + // + // This is arguably wrong: if the metadata is invalid we should try reloading + // it. However, this was the pre-existing behavior, and + // experimentalUseInvalidMetadata will be removed in a future release. + if !shouldLoad && useInvalidMetadata && len(ids) > 0 { + return ids, nil + } + + // Reload if loading is likely to improve the package associations for uri: + // - uri is not contained in any valid packages + // - ...or one of the packages containing uri is marked 'shouldLoad' + // - ...but uri is not unloadable + if (shouldLoad || !hasValidID) && !unloadable { + scope := fileLoadScope(uri) err := s.load(ctx, false, scope) - // As in reloadWorkspace, we must clear scopes after loading. + // Guard against failed loads due to context cancellation. // - // TODO(rfindley): simply call reloadWorkspace here, first, to avoid this - // duplication. - if !errors.Is(err, context.Canceled) { - s.clearShouldLoad(scope) + // Return the context error here as the current operation is no longer + // valid. + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr } - // TODO(rfindley): this doesn't look right. If we don't reload, we use - // invalid metadata anyway, but if we DO reload and it fails, we don't? - if !s.useInvalidMetadata() && err != nil { - return nil, err - } + // We must clear scopes after loading. + // + // TODO(rfindley): unlike reloadWorkspace, this is simply marking loaded + // packages as loaded. We could do this from snapshot.load and avoid + // raciness. + s.clearShouldLoad(scope) - s.mu.Lock() - ids = s.meta.ids[uri] - s.mu.Unlock() + // Don't return an error here, as we may still return stale IDs. + // Furthermore, the result of getOrLoadIDsForURI should be consistent upon + // subsequent calls, even if the file is marked as unloadable. + if err != nil && !errors.Is(err, errNoPackages) { + event.Error(ctx, "getOrLoadIDsForURI", err) + } + } - // We've tried to reload and there are still no known IDs for the URI. - // Return the load error, if there was one. - if len(ids) == 0 { - return nil, err + s.mu.Lock() + ids = s.meta.ids[uri] + if !useInvalidMetadata { + var validIDs []PackageID + for _, id := range ids { + // TODO(rfindley): remove the defensiveness here as well. + if m, ok := s.meta.metadata[id]; ok && m.Valid { + validIDs = append(validIDs, id) + } } + ids = validIDs } + s.mu.Unlock() return ids, nil } @@ -1206,17 +1249,18 @@ func (s *snapshot) getMetadata(id PackageID) *KnownMetadata { // clearShouldLoad clears package IDs that no longer need to be reloaded after // scopes has been loaded. -func (s *snapshot) clearShouldLoad(scopes ...interface{}) { +func (s *snapshot) clearShouldLoad(scopes ...loadScope) { s.mu.Lock() defer s.mu.Unlock() for _, scope := range scopes { switch scope := scope.(type) { - case PackagePath: + case packageLoadScope: + scopePath := PackagePath(scope) var toDelete []PackageID for id, pkgPaths := range s.shouldLoad { for _, pkgPath := range pkgPaths { - if pkgPath == scope { + if pkgPath == scopePath { toDelete = append(toDelete, id) } } @@ -1224,7 +1268,7 @@ func (s *snapshot) clearShouldLoad(scopes ...interface{}) { for _, id := range toDelete { delete(s.shouldLoad, id) } - case fileURI: + case fileLoadScope: uri := span.URI(scope) ids := s.meta.ids[uri] for _, id := range ids { @@ -1481,7 +1525,7 @@ func (s *snapshot) AwaitInitialized(ctx context.Context) { // reloadWorkspace reloads the metadata for all invalidated workspace packages. func (s *snapshot) reloadWorkspace(ctx context.Context) error { - var scopes []interface{} + var scopes []loadScope var seen map[PackagePath]bool s.mu.Lock() for _, pkgPaths := range s.shouldLoad { @@ -1493,7 +1537,7 @@ func (s *snapshot) reloadWorkspace(ctx context.Context) error { continue } seen[pkgPath] = true - scopes = append(scopes, pkgPath) + scopes = append(scopes, packageLoadScope(pkgPath)) } } s.mu.Unlock() @@ -1505,7 +1549,7 @@ func (s *snapshot) reloadWorkspace(ctx context.Context) error { // If the view's build configuration is invalid, we cannot reload by // package path. Just reload the directory instead. if !s.ValidBuildConfiguration() { - scopes = []interface{}{viewLoadScope("LOAD_INVALID_VIEW")} + scopes = []loadScope{viewLoadScope("LOAD_INVALID_VIEW")} } err := s.load(ctx, false, scopes...) @@ -1527,7 +1571,7 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { files := s.orphanedFiles() // Files without a valid package declaration can't be loaded. Don't try. - var scopes []interface{} + var scopes []loadScope for _, file := range files { pgf, err := s.ParseGo(ctx, file, source.ParseHeader) if err != nil { @@ -1536,7 +1580,8 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { if !pgf.File.Package.IsValid() { continue } - scopes = append(scopes, fileURI(file.URI())) + + scopes = append(scopes, fileLoadScope(file.URI())) } if len(scopes) == 0 { @@ -1560,7 +1605,7 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { event.Error(ctx, "reloadOrphanedFiles: failed to load", err, tag.Query.Of(scopes)) s.mu.Lock() for _, scope := range scopes { - uri := span.URI(scope.(fileURI)) + uri := span.URI(scope.(fileLoadScope)) if s.noValidMetadataForURILocked(uri) { s.unloadableFiles[uri] = struct{}{} } diff --git a/gopls/internal/lsp/cache/standalone_go115.go b/gopls/internal/lsp/cache/standalone_go115.go new file mode 100644 index 00000000000..79569ae10ec --- /dev/null +++ b/gopls/internal/lsp/cache/standalone_go115.go @@ -0,0 +1,14 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.16 +// +build !go1.16 + +package cache + +// isStandaloneFile returns false, as the 'standaloneTags' setting is +// unsupported on Go 1.15 and earlier. +func isStandaloneFile(src []byte, standaloneTags []string) bool { + return false +} diff --git a/gopls/internal/lsp/cache/standalone_go116.go b/gopls/internal/lsp/cache/standalone_go116.go new file mode 100644 index 00000000000..39e8864bb84 --- /dev/null +++ b/gopls/internal/lsp/cache/standalone_go116.go @@ -0,0 +1,52 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.16 +// +build go1.16 + +package cache + +import ( + "fmt" + "go/build/constraint" + "go/parser" + "go/token" +) + +// isStandaloneFile reports whether a file with the given contents should be +// considered a 'standalone main file', meaning a package that consists of only +// a single file. +func isStandaloneFile(src []byte, standaloneTags []string) bool { + f, err := parser.ParseFile(token.NewFileSet(), "", src, parser.PackageClauseOnly|parser.ParseComments) + if err != nil { + return false + } + + if f.Name == nil || f.Name.Name != "main" { + return false + } + + for _, cg := range f.Comments { + // Even with PackageClauseOnly the parser consumes the semicolon following + // the package clause, so we must guard against comments that come after + // the package name. + if cg.Pos() > f.Name.Pos() { + continue + } + for _, comment := range cg.List { + fmt.Println(comment.Text) + if c, err := constraint.Parse(comment.Text); err == nil { + if tag, ok := c.(*constraint.TagExpr); ok { + for _, t := range standaloneTags { + if t == tag.Tag { + return true + } + } + } + } + } + } + + return false +} diff --git a/gopls/internal/lsp/cache/standalone_go116_test.go b/gopls/internal/lsp/cache/standalone_go116_test.go new file mode 100644 index 00000000000..5eb7ff00f1d --- /dev/null +++ b/gopls/internal/lsp/cache/standalone_go116_test.go @@ -0,0 +1,90 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.16 +// +build go1.16 + +package cache + +import ( + "testing" +) + +func TestIsStandaloneFile(t *testing.T) { + tests := []struct { + desc string + contents string + standaloneTags []string + want bool + }{ + { + "new syntax", + "//go:build ignore\n\npackage main\n", + []string{"ignore"}, + true, + }, + { + "legacy syntax", + "// +build ignore\n\npackage main\n", + []string{"ignore"}, + true, + }, + { + "invalid tag", + "// +build ignore\n\npackage main\n", + []string{"script"}, + false, + }, + { + "non-main package", + "//go:build ignore\n\npackage p\n", + []string{"ignore"}, + false, + }, + { + "alternate tag", + "// +build script\n\npackage main\n", + []string{"script"}, + true, + }, + { + "both syntax", + "//go:build ignore\n// +build ignore\n\npackage main\n", + []string{"ignore"}, + true, + }, + { + "after comments", + "// A non-directive comment\n//go:build ignore\n\npackage main\n", + []string{"ignore"}, + true, + }, + { + "after package decl", + "package main //go:build ignore\n", + []string{"ignore"}, + false, + }, + { + "on line after package decl", + "package main\n\n//go:build ignore\n", + []string{"ignore"}, + false, + }, + { + "combined with other expressions", + "\n\n//go:build ignore || darwin\n\npackage main\n", + []string{"ignore"}, + false, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + if got := isStandaloneFile([]byte(test.contents), test.standaloneTags); got != test.want { + t.Errorf("isStandaloneFile(%q, %v) = %t, want %t", test.contents, test.standaloneTags, got, test.want) + } + }) + } +} diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index bdf22817576..dec2cb0808b 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -291,6 +291,9 @@ func minorOptionsChange(a, b *source.Options) bool { if !reflect.DeepEqual(a.DirectoryFilters, b.DirectoryFilters) { return false } + if !reflect.DeepEqual(a.StandaloneTags, b.StandaloneTags) { + return false + } if a.MemoryMode != b.MemoryMode { return false } @@ -665,7 +668,7 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { // Collect module paths to load by parsing go.mod files. If a module fails to // parse, capture the parsing failure as a critical diagnostic. - var scopes []interface{} // scopes to load + var scopes []loadScope // scopes to load var modDiagnostics []*source.Diagnostic // diagnostics for broken go.mod files addError := func(uri span.URI, err error) { modDiagnostics = append(modDiagnostics, &source.Diagnostic{ @@ -709,7 +712,7 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { // since it provides fake definitions (and documentation) // for types like int that are used everywhere. if len(scopes) > 0 { - scopes = append(scopes, PackagePath("builtin")) + scopes = append(scopes, packageLoadScope("builtin")) } err := s.load(ctx, true, scopes...) diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 5a47626b6e7..558556ec501 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -535,7 +535,7 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps return nil } pkgs, err := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace, false) - if len(pkgs) > 0 || err == nil { + if len(pkgs) > 0 { return nil } pgf, err := snapshot.ParseGo(ctx, fh, source.ParseHeader) diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index e40ecc2a16d..2b7362cb124 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -96,6 +96,13 @@ var GeneratedAPIJSON = &APIJSON{ Status: "experimental", Hierarchy: "build", }, + { + Name: "standaloneTags", + Type: "[]string", + Doc: "standaloneTags specifies a set of build constraints that identify\nindividual Go source files that make up the entire main package of an\nexecutable.\n\nA common example of standalone main files is the convention of using the\ndirective `//go:build ignore` to denote files that are not intended to be\nincluded in any package, for example because they are invoked directly by\nthe developer using `go run`.\n\nGopls considers a file to be a standalone main file if and only if it has\npackage name \"main\" and has a build directive of the exact form\n\"//go:build tag\" or \"// +build tag\", where tag is among the list of tags\nconfigured by this setting. Notably, if the build constraint is more\ncomplicated than a simple tag (such as the composite constraint\n`//go:build tag && go1.18`), the file is not considered to be a standalone\nmain file.\n\nThis setting is only supported when gopls is built with Go 1.16 or later.\n", + Default: "[\"ignore\"]", + Hierarchy: "build", + }, { Name: "hoverKind", Type: "enum", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 00c9b7b59ce..fe86ff280ed 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -120,6 +120,7 @@ func DefaultOptions() *Options { MemoryMode: ModeNormal, DirectoryFilters: []string{"-**/node_modules"}, TemplateExtensions: []string{}, + StandaloneTags: []string{"ignore"}, }, UIOptions: UIOptions{ DiagnosticOptions: DiagnosticOptions{ @@ -298,6 +299,26 @@ type BuildOptions struct { // Deprecated: this setting is deprecated and will be removed in a future // version of gopls (https://go.dev/issue/55333). ExperimentalUseInvalidMetadata bool `status:"experimental"` + + // StandaloneTags specifies a set of build constraints that identify + // individual Go source files that make up the entire main package of an + // executable. + // + // A common example of standalone main files is the convention of using the + // directive `//go:build ignore` to denote files that are not intended to be + // included in any package, for example because they are invoked directly by + // the developer using `go run`. + // + // Gopls considers a file to be a standalone main file if and only if it has + // package name "main" and has a build directive of the exact form + // "//go:build tag" or "// +build tag", where tag is among the list of tags + // configured by this setting. Notably, if the build constraint is more + // complicated than a simple tag (such as the composite constraint + // `//go:build tag && go1.18`), the file is not considered to be a standalone + // main file. + // + // This setting is only supported when gopls is built with Go 1.16 or later. + StandaloneTags []string } type UIOptions struct { @@ -760,6 +781,8 @@ func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) { } func (o *Options) Clone() *Options { + // TODO(rfindley): has this function gone stale? It appears that there are + // settings that are incorrectly cloned here (such as TemplateExtensions). result := &Options{ ClientOptions: o.ClientOptions, InternalOptions: o.InternalOptions, @@ -793,6 +816,7 @@ func (o *Options) Clone() *Options { result.SetEnvSlice(o.EnvSlice()) result.BuildFlags = copySlice(o.BuildFlags) result.DirectoryFilters = copySlice(o.DirectoryFilters) + result.StandaloneTags = copySlice(o.StandaloneTags) copyAnalyzerMap := func(src map[string]*Analyzer) map[string]*Analyzer { dst := make(map[string]*Analyzer) @@ -887,6 +911,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) } case "buildFlags": + // TODO(rfindley): use asStringSlice. iflags, ok := value.([]interface{}) if !ok { result.parseErrorf("invalid type %T, expect list", value) @@ -897,7 +922,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) flags = append(flags, fmt.Sprintf("%s", flag)) } o.BuildFlags = flags + case "directoryFilters": + // TODO(rfindley): use asStringSlice. ifilters, ok := value.([]interface{}) if !ok { result.parseErrorf("invalid type %T, expect list", value) @@ -913,6 +940,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/")) } o.DirectoryFilters = filters + case "memoryMode": if s, ok := result.asOneOf( string(ModeNormal), @@ -1104,6 +1132,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.softErrorf(msg) result.setBool(&o.ExperimentalUseInvalidMetadata) + case "standaloneTags": + result.setStringSlice(&o.StandaloneTags) + case "allExperiments": // This setting should be handled before all of the other options are // processed, so do nothing here. @@ -1294,6 +1325,24 @@ func (r *OptionResult) asString() (string, bool) { return b, true } +func (r *OptionResult) asStringSlice() ([]string, bool) { + iList, ok := r.Value.([]interface{}) + if !ok { + r.parseErrorf("invalid type %T, expect list", r.Value) + return nil, false + } + var list []string + for _, elem := range iList { + s, ok := elem.(string) + if !ok { + r.parseErrorf("invalid element type %T, expect string", elem) + return nil, false + } + list = append(list, s) + } + return list, true +} + func (r *OptionResult) asOneOf(options ...string) (string, bool) { s, ok := r.asString() if !ok { @@ -1322,6 +1371,12 @@ func (r *OptionResult) setString(s *string) { } } +func (r *OptionResult) setStringSlice(s *[]string) { + if v, ok := r.asStringSlice(); ok { + *s = v + } +} + // EnabledAnalyzers returns all of the analyzers enabled for the given // snapshot. func EnabledAnalyzers(snapshot Snapshot) (analyzers []*Analyzer) { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index e986d1261f8..d242cf4026e 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -331,7 +331,12 @@ type TidiedModule struct { } // Metadata represents package metadata retrieved from go/packages. +// +// TODO(rfindley): move the strongly typed strings from the cache package here. type Metadata interface { + // PackageID is the unique package id. + PackageID() string + // PackageName is the package name. PackageName() string @@ -653,10 +658,6 @@ func AnalyzerErrorKind(name string) DiagnosticSource { return DiagnosticSource(name) } -var ( - PackagesLoadError = errors.New("packages.Load error") -) - // WorkspaceModuleVersion is the nonexistent pseudoversion suffix used in the // construction of the workspace module. It is exported so that we can make // sure not to show this version to end users in error messages, to avoid diff --git a/gopls/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go index dabbeb3dd9f..bd1e7b12adc 100644 --- a/gopls/internal/lsp/source/workspace_symbol.go +++ b/gopls/internal/lsp/source/workspace_symbol.go @@ -98,6 +98,12 @@ func fullyQualifiedSymbolMatch(space []string, name string, pkg Metadata, matche } func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { + if IsCommandLineArguments(pkg.PackageID()) { + // command-line-arguments packages have a non-sensical package path, so + // just use their package name. + return packageSymbolMatch(space, name, pkg, matcher) + } + var score float64 endsInPkgName := strings.HasSuffix(pkg.PackagePath(), pkg.PackageName()) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 72fe3cae381..f0082073326 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1334,8 +1334,8 @@ package a func main() { var x int } --- a/a_ignore.go -- -// +build ignore +-- a/a_exclude.go -- +// +build exclude package a @@ -1348,9 +1348,9 @@ func _() { env.Await( env.DiagnosticAtRegexp("a/a.go", "x"), ) - env.OpenFile("a/a_ignore.go") + env.OpenFile("a/a_exclude.go") env.Await( - DiagnosticAt("a/a_ignore.go", 2, 8), + DiagnosticAt("a/a_exclude.go", 2, 8), ) }) } diff --git a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go index 4ba31354ed9..d1fc8646cef 100644 --- a/gopls/internal/regtest/misc/workspace_symbol_test.go +++ b/gopls/internal/regtest/misc/workspace_symbol_test.go @@ -26,13 +26,14 @@ go 1.17 package p const C1 = "a.go" --- ignore.go -- +-- exclude.go -- -// +build ignore +//go:build exclude +// +build exclude -package ignore +package exclude -const C2 = "ignore.go" +const C2 = "exclude.go" ` Run(t, files, func(t *testing.T, env *Env) { @@ -44,7 +45,7 @@ const C2 = "ignore.go" // Opening up an ignored file will result in an overlay with missing // metadata, but this shouldn't break workspace symbols requests. - env.OpenFile("ignore.go") + env.OpenFile("exclude.go") syms = env.WorkspaceSymbol("C") if got, want := len(syms), 1; got != want { t.Errorf("got %d symbols, want %d", got, want) diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index 0356ebcb55d..c5598c93f76 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -45,7 +45,7 @@ const C = 42 // Test that moving ignoring a file via build constraints causes diagnostics to // be resolved. func TestIgnoreFile(t *testing.T) { - testenv.NeedsGo1Point(t, 16) // needs native overlays + testenv.NeedsGo1Point(t, 17) // needs native overlays and support for go:build directives const src = ` -- go.mod -- @@ -81,8 +81,9 @@ func main() {} env.DiagnosticAtRegexp("bar.go", "func (main)"), ), ) + // Ignore bar.go. This should resolve diagnostics. - env.RegexpReplace("bar.go", "package main", "// +build ignore\n\npackage main") + env.RegexpReplace("bar.go", "package main", "//go:build ignore\n\npackage main") // To make this test pass with experimentalUseInvalidMetadata, we could make // an arbitrary edit that invalidates the snapshot, at which point the @@ -95,8 +96,18 @@ func main() {} OnceMet( env.DoneWithChange(), EmptyDiagnostics("foo.go"), + EmptyDiagnostics("bar.go"), + ), + ) + + // If instead of 'ignore' (which gopls treats as a standalone package) we + // used a different build tag, we should get a warning about having no + // packages for bar.go + env.RegexpReplace("bar.go", "ignore", "excluded") + env.Await( + OnceMet( + env.DoneWithChange(), env.DiagnosticAtRegexpWithMessage("bar.go", "package (main)", "No packages"), - env.NoDiagnosticAtRegexp("bar.go", "func (main)"), ), ) }) diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go new file mode 100644 index 00000000000..1e51b31fb78 --- /dev/null +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -0,0 +1,249 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package workspace + +import ( + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" +) + +func TestStandaloneFiles(t *testing.T) { + testenv.NeedsGo1Point(t, 16) // Standalone files are only supported at Go 1.16 and later. + + const files = ` +-- go.mod -- +module mod.test + +go 1.16 +-- lib/lib.go -- +package lib + +const C = 0 + +type I interface { + M() +} +-- lib/ignore.go -- +//go:build ignore +// +build ignore + +package main + +import ( + "mod.test/lib" +) + +const C = 1 + +type Mer struct{} +func (Mer) M() + +func main() { + println(lib.C + C) +} +` + WithOptions( + // On Go 1.17 and earlier, this test fails with + // experimentalWorkspaceModule. Not investigated, as + // experimentalWorkspaceModule will be removed. + Modes(Default), + ).Run(t, files, func(t *testing.T, env *Env) { + // Initially, gopls should not know about the standalone file as it hasn't + // been opened. Therefore, we should only find one symbol 'C'. + syms := env.WorkspaceSymbol("C") + if got, want := len(syms), 1; got != want { + t.Errorf("got %d symbols, want %d", got, want) + } + + // Similarly, we should only find one reference to "C", and no + // implementations of I. + checkLocations := func(method string, gotLocations []protocol.Location, wantFiles ...string) { + var gotFiles []string + for _, l := range gotLocations { + gotFiles = append(gotFiles, env.Sandbox.Workdir.URIToPath(l.URI)) + } + sort.Strings(gotFiles) + sort.Strings(wantFiles) + if diff := cmp.Diff(wantFiles, gotFiles); diff != "" { + t.Errorf("%s(...): unexpected locations (-want +got):\n%s", method, diff) + } + } + + env.OpenFile("lib/lib.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + NoOutstandingDiagnostics(), + ), + ) + + // Replacing C with D should not cause any workspace diagnostics, since we + // haven't yet opened the standalone file. + env.RegexpReplace("lib/lib.go", "C", "D") + env.Await( + OnceMet( + env.DoneWithChange(), + NoOutstandingDiagnostics(), + ), + ) + env.RegexpReplace("lib/lib.go", "D", "C") + env.Await( + OnceMet( + env.DoneWithChange(), + NoOutstandingDiagnostics(), + ), + ) + + refs := env.References("lib/lib.go", env.RegexpSearch("lib/lib.go", "C")) + checkLocations("References", refs, "lib/lib.go") + + impls := env.Implementations("lib/lib.go", env.RegexpSearch("lib/lib.go", "I")) + checkLocations("Implementations", impls) // no implementations + + // Opening the standalone file should not result in any diagnostics. + env.OpenFile("lib/ignore.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + NoOutstandingDiagnostics(), + ), + ) + + // Having opened the standalone file, we should find its symbols in the + // workspace. + syms = env.WorkspaceSymbol("C") + if got, want := len(syms), 2; got != want { + t.Fatalf("got %d symbols, want %d", got, want) + } + + foundMainC := false + var symNames []string + for _, sym := range syms { + symNames = append(symNames, sym.Name) + if sym.Name == "main.C" { + foundMainC = true + } + } + if !foundMainC { + t.Errorf("WorkspaceSymbol(\"C\") = %v, want containing main.C", symNames) + } + + // We should resolve workspace definitions in the standalone file. + file, _ := env.GoToDefinition("lib/ignore.go", env.RegexpSearch("lib/ignore.go", "lib.(C)")) + if got, want := file, "lib/lib.go"; got != want { + t.Errorf("GoToDefinition(lib.C) = %v, want %v", got, want) + } + + // ...as well as intra-file definitions + file, pos := env.GoToDefinition("lib/ignore.go", env.RegexpSearch("lib/ignore.go", "\\+ (C)")) + if got, want := file, "lib/ignore.go"; got != want { + t.Errorf("GoToDefinition(C) = %v, want %v", got, want) + } + wantPos := env.RegexpSearch("lib/ignore.go", "const (C)") + if pos != wantPos { + t.Errorf("GoToDefinition(C) = %v, want %v", pos, wantPos) + } + + // Renaming "lib.C" to "lib.D" should cause a diagnostic in the standalone + // file. + env.RegexpReplace("lib/lib.go", "C", "D") + env.Await( + OnceMet( + env.DoneWithChange(), + env.DiagnosticAtRegexp("lib/ignore.go", "lib.(C)"), + ), + ) + + // Undoing the replacement should fix diagnostics + env.RegexpReplace("lib/lib.go", "D", "C") + env.Await( + OnceMet( + env.DoneWithChange(), + NoOutstandingDiagnostics(), + ), + ) + + // Now that our workspace has no errors, we should be able to find + // references and rename. + refs = env.References("lib/lib.go", env.RegexpSearch("lib/lib.go", "C")) + checkLocations("References", refs, "lib/lib.go", "lib/ignore.go") + + impls = env.Implementations("lib/lib.go", env.RegexpSearch("lib/lib.go", "I")) + checkLocations("Implementations", impls, "lib/ignore.go") + + // Renaming should rename in the standalone package. + env.Rename("lib/lib.go", env.RegexpSearch("lib/lib.go", "C"), "D") + env.RegexpSearch("lib/ignore.go", "lib.D") + }) +} + +func TestStandaloneFiles_Configuration(t *testing.T) { + testenv.NeedsGo1Point(t, 16) // Standalone files are only supported at Go 1.16 and later. + + const files = ` +-- go.mod -- +module mod.test + +go 1.18 +-- lib.go -- +package lib // without this package, files are loaded as command-line-arguments +-- ignore.go -- +//go:build ignore +// +build ignore + +package main + +// An arbitrary comment. + +func main() {} +-- standalone.go -- +//go:build standalone +// +build standalone + +package main + +func main() {} +` + + WithOptions( + Settings{ + "standaloneTags": []string{"standalone"}, + }, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("ignore.go") + env.OpenFile("standalone.go") + + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("ignore.go", "package (main)"), + EmptyOrNoDiagnostics("standalone.go"), + ), + ) + + cfg := env.Editor.Config() + cfg.Settings = map[string]interface{}{ + "standaloneTags": []string{"ignore"}, + } + env.ChangeConfiguration(cfg) + + // TODO(golang/go#56158): gopls does not purge previously published + // diagnostice when configuration changes. + env.RegexpReplace("ignore.go", "arbitrary", "meaningless") + + env.Await( + OnceMet( + env.DoneWithChange(), + EmptyOrNoDiagnostics("ignore.go"), + env.DiagnosticAtRegexp("standalone.go", "package (main)"), + ), + ) + }) +} From f87c1ed972365640140266649731db08360c098e Mon Sep 17 00:00:00 2001 From: Charlie Vieth Date: Sat, 12 Mar 2022 17:51:45 -0500 Subject: [PATCH 336/723] internal/fastwalk: improve Darwin performance by ~3x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On darwin/cgo use readdir_r instead of syscall.ReadDirent() since the later is simulated and slow (see: golang/go#30933). Unlike CL 392094, this uses CGO instead of "//go:linkname" and assembly. goos: darwin goarch: arm64 FastWalk-10 68.9ms ±11% 20.7ms ± 2% -69.89% (p=0.008 n=5+5) name old alloc/op new alloc/op delta FastWalk-10 1.49MB ± 0% 1.51MB ± 0% +1.06% (p=0.008 n=5+5) name old allocs/op new allocs/op delta FastWalk-10 32.2k ± 0% 30.7k ± 0% -4.61% (p=0.016 n=5+4) Fixes golang/go#51356 Change-Id: Ia3afd06c8f14bd2036b2a1ea6e3cafbef81d3530 Reviewed-on: https://go-review.googlesource.com/c/tools/+/436780 TryBot-Result: Gopher Robot Reviewed-by: Heschi Kreinick Reviewed-by: Robert Findley Run-TryBot: Robert Findley Auto-Submit: Heschi Kreinick gopls-CI: kokoro --- internal/fastwalk/fastwalk_darwin.go | 119 ++++++++++++++++++ internal/fastwalk/fastwalk_dirent_ino.go | 4 +- .../fastwalk/fastwalk_dirent_namlen_bsd.go | 4 +- internal/fastwalk/fastwalk_unix.go | 4 +- 4 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 internal/fastwalk/fastwalk_darwin.go diff --git a/internal/fastwalk/fastwalk_darwin.go b/internal/fastwalk/fastwalk_darwin.go new file mode 100644 index 00000000000..0ca55e0d56f --- /dev/null +++ b/internal/fastwalk/fastwalk_darwin.go @@ -0,0 +1,119 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && cgo +// +build darwin,cgo + +package fastwalk + +/* +#include + +// fastwalk_readdir_r wraps readdir_r so that we don't have to pass a dirent** +// result pointer which triggers CGO's "Go pointer to Go pointer" check unless +// we allocat the result dirent* with malloc. +// +// fastwalk_readdir_r returns 0 on success, -1 upon reaching the end of the +// directory, or a positive error number to indicate failure. +static int fastwalk_readdir_r(DIR *fd, struct dirent *entry) { + struct dirent *result; + int ret = readdir_r(fd, entry, &result); + if (ret == 0 && result == NULL) { + ret = -1; // EOF + } + return ret; +} +*/ +import "C" + +import ( + "os" + "syscall" + "unsafe" +) + +func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error { + fd, err := openDir(dirName) + if err != nil { + return &os.PathError{Op: "opendir", Path: dirName, Err: err} + } + defer C.closedir(fd) + + skipFiles := false + var dirent syscall.Dirent + for { + ret := int(C.fastwalk_readdir_r(fd, (*C.struct_dirent)(unsafe.Pointer(&dirent)))) + if ret != 0 { + if ret == -1 { + break // EOF + } + if ret == int(syscall.EINTR) { + continue + } + return &os.PathError{Op: "readdir", Path: dirName, Err: syscall.Errno(ret)} + } + if dirent.Ino == 0 { + continue + } + typ := dtToType(dirent.Type) + if skipFiles && typ.IsRegular() { + continue + } + name := (*[len(syscall.Dirent{}.Name)]byte)(unsafe.Pointer(&dirent.Name))[:] + name = name[:dirent.Namlen] + for i, c := range name { + if c == 0 { + name = name[:i] + break + } + } + // Check for useless names before allocating a string. + if string(name) == "." || string(name) == ".." { + continue + } + if err := fn(dirName, string(name), typ); err != nil { + if err != ErrSkipFiles { + return err + } + skipFiles = true + } + } + + return nil +} + +func dtToType(typ uint8) os.FileMode { + switch typ { + case syscall.DT_BLK: + return os.ModeDevice + case syscall.DT_CHR: + return os.ModeDevice | os.ModeCharDevice + case syscall.DT_DIR: + return os.ModeDir + case syscall.DT_FIFO: + return os.ModeNamedPipe + case syscall.DT_LNK: + return os.ModeSymlink + case syscall.DT_REG: + return 0 + case syscall.DT_SOCK: + return os.ModeSocket + } + return ^os.FileMode(0) +} + +// openDir wraps opendir(3) and handles any EINTR errors. The returned *DIR +// needs to be closed with closedir(3). +func openDir(path string) (*C.DIR, error) { + name, err := syscall.BytePtrFromString(path) + if err != nil { + return nil, err + } + for { + fd, err := C.opendir((*C.char)(unsafe.Pointer(name))) + if err != syscall.EINTR { + return fd, err + } + } +} diff --git a/internal/fastwalk/fastwalk_dirent_ino.go b/internal/fastwalk/fastwalk_dirent_ino.go index 041775bf7c1..d3922890b0b 100644 --- a/internal/fastwalk/fastwalk_dirent_ino.go +++ b/internal/fastwalk/fastwalk_dirent_ino.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (linux || darwin) && !appengine -// +build linux darwin +//go:build (linux || (darwin && !cgo)) && !appengine +// +build linux darwin,!cgo // +build !appengine package fastwalk diff --git a/internal/fastwalk/fastwalk_dirent_namlen_bsd.go b/internal/fastwalk/fastwalk_dirent_namlen_bsd.go index d5c9c321ed2..38a4db6af3a 100644 --- a/internal/fastwalk/fastwalk_dirent_namlen_bsd.go +++ b/internal/fastwalk/fastwalk_dirent_namlen_bsd.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build darwin || freebsd || openbsd || netbsd -// +build darwin freebsd openbsd netbsd +//go:build (darwin && !cgo) || freebsd || openbsd || netbsd +// +build darwin,!cgo freebsd openbsd netbsd package fastwalk diff --git a/internal/fastwalk/fastwalk_unix.go b/internal/fastwalk/fastwalk_unix.go index 58bd87841e1..f12f1a734cc 100644 --- a/internal/fastwalk/fastwalk_unix.go +++ b/internal/fastwalk/fastwalk_unix.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (linux || darwin || freebsd || openbsd || netbsd) && !appengine -// +build linux darwin freebsd openbsd netbsd +//go:build (linux || freebsd || openbsd || netbsd || (darwin && !cgo)) && !appengine +// +build linux freebsd openbsd netbsd darwin,!cgo // +build !appengine package fastwalk From ffb862b5444512cfc8c66d91cbf7d22d301f0bfd Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 13 Oct 2022 13:28:18 -0400 Subject: [PATCH 337/723] gopls/internal/lsp/cache: remove stray print statement A stray print statement was accidentally left in CL 441877. Remove it. Change-Id: I44e4408059f30d35ad8c84b070aea3e197762d1c Reviewed-on: https://go-review.googlesource.com/c/tools/+/442782 gopls-CI: kokoro Auto-Submit: Robert Findley Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/standalone_go116.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/gopls/internal/lsp/cache/standalone_go116.go b/gopls/internal/lsp/cache/standalone_go116.go index 39e8864bb84..2f72d5f5495 100644 --- a/gopls/internal/lsp/cache/standalone_go116.go +++ b/gopls/internal/lsp/cache/standalone_go116.go @@ -8,7 +8,6 @@ package cache import ( - "fmt" "go/build/constraint" "go/parser" "go/token" @@ -35,7 +34,6 @@ func isStandaloneFile(src []byte, standaloneTags []string) bool { continue } for _, comment := range cg.List { - fmt.Println(comment.Text) if c, err := constraint.Parse(comment.Text); err == nil { if tag, ok := c.(*constraint.TagExpr); ok { for _, t := range standaloneTags { From 9ffaf69c2c99465e12712e54df2f38e28a3e32a8 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 12 Oct 2022 16:07:31 -0400 Subject: [PATCH 338/723] gopls/internal/lsp/cache: minor analysis cleanups This change moves the *pkg out of the actionHandle into the actionResult. (In future we may not need to load the package before creating the handle.) The actionResult retains only the exported type information, to avoid keeping the syntax trees (etc) live. Also, perform object fact filtering once after the analysis pass, instead of every time it is imported by another analysis. Also, filter out unexported constants. Also, log internal errors even when we don't bug.Reportf, since errors merely returned by analysis are usually ignored, making it harder to understand the analysis crashes. Change-Id: I7257e8da5dc269090bff2817409948e46dcf7176 Reviewed-on: https://go-review.googlesource.com/c/tools/+/442556 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/cache/analysis.go | 54 +++++++++++++++++----------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index d41116c3400..0d5b98ce613 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -73,14 +73,14 @@ type actionHandleKey source.Hash // package (as different analyzers are applied, either in sequence or // parallel), and across packages (as dependencies are analyzed). type actionHandle struct { + key actionKey // just for String() promise *memoize.Promise // [actionResult] - - analyzer *analysis.Analyzer - pkg *pkg } // actionData is the successful result of analyzing a package. type actionData struct { + analyzer *analysis.Analyzer + pkgTypes *types.Package // types only; don't keep syntax live diagnostics []*source.Diagnostic result interface{} objectFacts map[objectFactKey]analysis.Fact @@ -168,9 +168,8 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A }) ah := &actionHandle{ - analyzer: a, - pkg: pkg, - promise: promise, + key: key, + promise: promise, } s.mu.Lock() @@ -191,8 +190,12 @@ func buildActionKey(a *analysis.Analyzer, ph *packageHandle) actionHandleKey { return actionHandleKey(source.Hashf("%p%s", a, ph.key[:])) } +func (key actionKey) String() string { + return fmt.Sprintf("%s@%s", key.analyzer, key.pkgid) +} + func (act *actionHandle) String() string { - return fmt.Sprintf("%s@%s", act.analyzer, act.pkg.PkgPath()) + return act.key.String() } // actionImpl runs the analysis for action node (analyzer, pkg), @@ -222,29 +225,20 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a mu.Lock() defer mu.Unlock() - if dep.pkg == pkg { + if data.pkgTypes == pkg.types { // Same package, different analysis (horizontal edge): // in-memory outputs of prerequisite analyzers // become inputs to this analysis pass. - inputs[dep.analyzer] = data.result + inputs[data.analyzer] = data.result - } else if dep.analyzer == analyzer { + } else if data.analyzer == analyzer { // Same analysis, different package (vertical edge): // serialized facts produced by prerequisite analysis // become available to this analysis pass. for key, fact := range data.objectFacts { - // Filter out facts related to objects - // that are irrelevant downstream - // (equivalently: not in the compiler export data). - if !exportedFrom(key.obj, dep.pkg.types) { - continue - } objectFacts[key] = fact } for key, fact := range data.packageFacts { - // TODO: filter out facts that belong to - // packages not mentioned in the export data - // to prevent side channels. packageFacts[key] = fact } @@ -268,7 +262,11 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a if source.IsCommandLineArguments(pkg.ID()) { errorf = fmt.Errorf // suppress reporting } - return errorf("internal error: unexpected analysis dependency %s@%s -> %s", analyzer.Name, pkg.ID(), dep) + err := errorf("internal error: unexpected analysis dependency %s@%s -> %s", analyzer.Name, pkg.ID(), dep) + // Log the event in any case, as the ultimate + // consumer of actionResult ignores errors. + event.Error(ctx, "analysis", err) + return err } return nil }) @@ -401,6 +399,16 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a panic(fmt.Sprintf("%s:%s: Pass.ExportPackageFact(%T) called after Run", analyzer.Name, pkg.PkgPath(), fact)) } + // Filter out facts related to objects that are irrelevant downstream + // (equivalently: not in the compiler export data). + for key := range objectFacts { + if !exportedFrom(key.obj, pkg.types) { + delete(objectFacts, key) + } + } + // TODO: filter out facts that belong to packages not + // mentioned in the export data to prevent side channels. + var diagnostics []*source.Diagnostic for _, diag := range rawDiagnostics { srcDiags, err := analysisDiagnosticDiagnostics(snapshot, pkg, analyzer, &diag) @@ -411,6 +419,8 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a diagnostics = append(diagnostics, srcDiags...) } return &actionData{ + analyzer: analyzer, + pkgTypes: pkg.types, diagnostics: diagnostics, result: result, objectFacts: objectFacts, @@ -434,8 +444,10 @@ func exportedFrom(obj types.Object, pkg *types.Package) bool { case *types.Var: return obj.Exported() && obj.Pkg() == pkg || obj.IsField() - case *types.TypeName, *types.Const: + case *types.TypeName: return true + case *types.Const: + return obj.Exported() && obj.Pkg() == pkg } return false // Nil, Builtin, Label, or PkgName } From 9b5e55b1a7e215a54c9784492d801104a8381a91 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 14 Oct 2022 09:27:47 -0400 Subject: [PATCH 339/723] gopls/internal/lsp/cache: disable strict analysis while we fix panics We now understand the longstanding race causing panics in gopls analysis (the cache key for analysis handles is not correct, and can lead to cache hits for analysis of stale packages). It will take a little while to redesign our analysis caching, the simplest solution being to hang analysis results off of the actual package they are analyzing. In the meantime, suppress the panic as before, to eliminate flakes. For golang/go#55201 For golang/go#56035 Change-Id: I96600f186f9f31b67dae10c0a95a14fa73630571 Reviewed-on: https://go-review.googlesource.com/c/tools/+/442794 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/analysis.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 0d5b98ce613..f54d0ed872a 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -258,7 +258,12 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a // discover this problem in regular packages. // For command-line-arguments we quietly abort the analysis // for now since we already know there is a bug. - errorf := bug.Errorf // report this discovery + + // Temporarily disable bug reporting due to golang/go#56035: using + // bug.Errorf causes too many test flakes. + // TODO(rfindley): switch back to bug.Errorf once panics are fixed. + errorf := fmt.Errorf + if source.IsCommandLineArguments(pkg.ID()) { errorf = fmt.Errorf // suppress reporting } @@ -364,9 +369,9 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a // An Analyzer crashed. This is often merely a symptom // of a problem in package loading. // - // We believe that CL 420538 may have fixed these crashes, so enable - // strict checks in tests. - const strict = true + // We are aware of a likely cause for these panics: the actionHandle + // cache key is not correct. In the meantime, disable strict mode. + const strict = false if strict && bug.PanicOnBugs && analyzer.Name != "fact_purity" { // During testing, crash. See issues 54762, 56035. // But ignore analyzers with known crash bugs: From b93a56f289b07a90bd39c92c8d470f9c21d2623d Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 14 Oct 2022 12:21:42 -0400 Subject: [PATCH 340/723] refactor/satisfy: fix visiting functions in the unsafe package Unlike other calls of qualified functions, the type of certain functions in the unsafe package (namely Slice, Add) is not constant, and cannot be determined by the types.Func being called. Update Finder.expr to pre-emptively handle calls to functions in the unsafe package, similarly to how it handles other builtins. Fixes golang/go#56227 Change-Id: I7af51c1ecacbdf35e39c8e7b8273ffe6f953427f Reviewed-on: https://go-review.googlesource.com/c/tools/+/443096 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cmd/test/rename.go | 9 ++++--- gopls/internal/regtest/misc/rename_test.go | 30 ++++++++++++++++++++++ refactor/satisfy/find.go | 22 +++++++++++++--- refactor/satisfy/find_test.go | 16 ++++++++++-- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/gopls/internal/lsp/cmd/test/rename.go b/gopls/internal/lsp/cmd/test/rename.go index 0750d70281d..a9eb31e3877 100644 --- a/gopls/internal/lsp/cmd/test/rename.go +++ b/gopls/internal/lsp/cmd/test/rename.go @@ -8,6 +8,7 @@ import ( "fmt" "testing" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/gopls/internal/span" ) @@ -17,13 +18,13 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { loc := fmt.Sprintf("%v", spn) got, err := r.NormalizeGoplsCmd(t, "rename", loc, newText) got += err - expect := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { + want := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { return []byte(got), nil })) - if expect != got { - t.Errorf("rename failed with %v %v\nexpected:\n%s\ngot:\n%s", loc, newText, expect, got) + if diff := compare.Text(want, got); diff != "" { + t.Errorf("rename failed with %v %v (-want +got):\n%s", loc, newText, diff) } // now check we can build a valid unified diff unified, _ := r.NormalizeGoplsCmd(t, "rename", "-d", loc, newText) - checkUnified(t, filename, expect, unified) + checkUnified(t, filename, want, unified) } diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 1f6ee1c2b85..aa0a47ad7b8 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -52,6 +52,36 @@ func main() { }) } +// Test case for golang/go#56227 +func TestRenameWithUnsafeSlice(t *testing.T) { + testenv.NeedsGo1Point(t, 17) // unsafe.Slice was added in Go 1.17 + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- p.go -- +package p + +import "unsafe" + +type T struct{} + +func (T) M() {} + +func _() { + x := [3]int{1, 2, 3} + ptr := unsafe.Pointer(&x) + _ = unsafe.Slice((*int)(ptr), 3) +} +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("p.go") + env.Rename("p.go", env.RegexpSearch("p.go", "M"), "N") // must not panic + }) +} + func TestPrepareRenameWithNoPackageDeclaration(t *testing.T) { testenv.NeedsGo1Point(t, 15) const files = ` diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go index aacb56bce8b..6b4d5284aec 100644 --- a/refactor/satisfy/find.go +++ b/refactor/satisfy/find.go @@ -198,7 +198,8 @@ func (f *Finder) call(sig *types.Signature, args []ast.Expr) { } } -func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Expr, T types.Type) types.Type { +// builtin visits the arguments of a builtin type with signature sig. +func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Expr) { switch obj.Name() { case "make", "new": // skip the type operand @@ -228,8 +229,6 @@ func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Ex // ordinary call f.call(sig, args) } - - return T } func (f *Finder) extract(tuple types.Type, i int) types.Type { @@ -439,12 +438,27 @@ func (f *Finder) expr(e ast.Expr) types.Type { f.assign(tvFun.Type, arg0) } else { // function call + + // unsafe call. Treat calls to functions in unsafe like ordinary calls, + // except that their signature cannot be determined by their func obj. + // Without this special handling, f.expr(e.Fun) would fail below. + if s, ok := unparen(e.Fun).(*ast.SelectorExpr); ok { + if obj, ok := f.info.Uses[s.Sel].(*types.Builtin); ok && obj.Pkg().Path() == "unsafe" { + sig := f.info.Types[e.Fun].Type.(*types.Signature) + f.call(sig, e.Args) + return tv.Type + } + } + + // builtin call if id, ok := unparen(e.Fun).(*ast.Ident); ok { if obj, ok := f.info.Uses[id].(*types.Builtin); ok { sig := f.info.Types[id].Type.(*types.Signature) - return f.builtin(obj, sig, e.Args, tv.Type) + f.builtin(obj, sig, e.Args) + return tv.Type } } + // ordinary call f.call(coreType(f.expr(e.Fun)).(*types.Signature), e.Args) } diff --git a/refactor/satisfy/find_test.go b/refactor/satisfy/find_test.go index 234bce905d3..35a1e87caf4 100644 --- a/refactor/satisfy/find_test.go +++ b/refactor/satisfy/find_test.go @@ -7,6 +7,7 @@ package satisfy_test import ( "fmt" "go/ast" + "go/importer" "go/parser" "go/token" "go/types" @@ -27,6 +28,8 @@ func TestGenericCoreOperations(t *testing.T) { const src = `package foo +import "unsafe" + type I interface { f() } type impl struct{} @@ -53,6 +56,7 @@ type R struct{impl} type S struct{impl} type T struct{impl} type U struct{impl} +type V struct{impl} type Generic[T any] struct{impl} func (Generic[T]) g(T) {} @@ -153,8 +157,13 @@ func _[T any]() { type Gen2[T any] struct{} func (f Gen2[T]) g(string) { global = f } // GI[string] <- Gen2[T] -var global GI[string] +var global GI[string] +func _() { + var x [3]V + // golang/go#56227: the finder should visit calls in the unsafe package. + _ = unsafe.Slice(&x[0], func() int { var _ I = x[0]; return 3 }()) // I <- V +} ` got := constraints(t, src) want := []string{ @@ -184,6 +193,7 @@ var global GI[string] "p.I <- p.S", "p.I <- p.T", "p.I <- p.U", + "p.I <- p.V", } if !reflect.DeepEqual(got, want) { t.Fatalf("found unexpected constraints: got %s, want %s", got, want) @@ -209,7 +219,9 @@ func constraints(t *testing.T, src string) []string { Selections: make(map[*ast.SelectorExpr]*types.Selection), } typeparams.InitInstanceInfo(info) - conf := types.Config{} + conf := types.Config{ + Importer: importer.Default(), + } if _, err := conf.Check("p", fset, files, info); err != nil { t.Fatal(err) // type error } From bc4e384f856e909b9ea0fcea24d662e254af38a3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 14 Oct 2022 18:49:47 -0400 Subject: [PATCH 341/723] gopls/internal/lsp/cache: fix crash in analysis Since we removed the 'recover' in analysis, which swept crash bugs under the rug, we've seen a large number of crashes on the builders in which an analyzer crashes while trying to use the result of another analyzer (e.g. the inspector) on the same package. Logging shows that the "same" package is in fact a stale version of the same package. The root cause is that the key for the analysis cache is the analyzer name paired with the "recipe" for the package. However, analysis is not content with an equivalent package: identity matters for ast.Nodes, types.Objects, etc. This change attemps to fix the problem by, in effect, using the identity (not the recipe) of the package as part of the key. It is materialized by adding a store of promises to the pkg, and then using the analysis name as the key within this store. Change-Id: I057807d2781492a685f27c815250c3445e7475f9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/443100 gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/analysis.go | 17 ++++++++++------- gopls/internal/lsp/cache/pkg.go | 3 +++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index f54d0ed872a..c75b8de36b4 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -66,8 +66,6 @@ type actionKey struct { analyzer *analysis.Analyzer } -type actionHandleKey source.Hash - // An action represents one unit of analysis work: the application of // one analysis to one package. Actions form a DAG, both within a // package (as different analyzers are applied, either in sequence or @@ -162,7 +160,16 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } } - promise, release := s.store.Promise(buildActionKey(a, ph), func(ctx context.Context, arg interface{}) interface{} { + // The promises are kept in a store on the package, + // so the key need only include the analyzer name. + // + // (Since type-checking and analysis depend on the identity + // of packages--distinct packages produced by the same + // recipe are not fungible--we must in effect use the package + // itself as part of the key. Rather than actually use a pointer + // in the key, we get a simpler object graph if we shard the + // store by packages.) + promise, release := pkg.analyses.Promise(a.Name, func(ctx context.Context, arg interface{}) interface{} { res, err := actionImpl(ctx, arg.(*snapshot), deps, a, pkg) return actionResult{res, err} }) @@ -186,10 +193,6 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A return ah, nil } -func buildActionKey(a *analysis.Analyzer, ph *packageHandle) actionHandleKey { - return actionHandleKey(source.Hashf("%p%s", a, ph.key[:])) -} - func (key actionKey) String() string { return fmt.Sprintf("%s@%s", key.analyzer, key.pkgid) } diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 44fe855c0d3..0c9914d66fd 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -13,6 +13,7 @@ import ( "golang.org/x/mod/module" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/memoize" ) // pkg contains the type information needed by the source package. @@ -30,6 +31,8 @@ type pkg struct { typesInfo *types.Info typesSizes types.Sizes hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors + + analyses memoize.Store // maps analyzer.Name to Promise[actionResult] } // A loadScope defines a package loading scope for use with go/packages. From 0e222f5c6f028422c15de6dd525efbba33eb849e Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Mon, 28 Feb 2022 23:24:50 -0500 Subject: [PATCH 342/723] internal/jsonrpc2_v2: close the underlying connection if Wait is called instead of Close (*Server).run calls Wait on all of its connections before returning, but does not call Close explicitly. If Close is necessary to release resources (as is often the case for a net.Conn), the server ends up leaking resources. For golang/go#46047 Change-Id: I4ad048c4f5e5d3f14f992eee6388acd42398c26b Reviewed-on: https://go-review.googlesource.com/c/tools/+/388599 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Bryan Mills Auto-Submit: Bryan Mills Reviewed-by: Alan Donovan --- internal/jsonrpc2_v2/conn.go | 43 +++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 47668e209be..15d40b4a4c2 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "io" + "sync" "sync/atomic" "golang.org/x/tools/internal/event" @@ -45,8 +46,11 @@ type ConnectionOptions struct { // Connection is bidirectional; it does not have a designated server or client // end. type Connection struct { - seq int64 // must only be accessed using atomic operations - closer io.Closer + seq int64 // must only be accessed using atomic operations + + closeOnce sync.Once + closer io.Closer + writerBox chan Writer outgoingBox chan map[ID]chan<- *Response incomingBox chan map[ID]*incoming @@ -275,14 +279,13 @@ func (c *Connection) Wait() error { // This does not cancel in flight requests, but waits for them to gracefully complete. func (c *Connection) Close() error { // close the underlying stream - if err := c.closer.Close(); err != nil && !isClosingError(err) { - return err - } + c.closeOnce.Do(func() { + if err := c.closer.Close(); err != nil { + c.async.setError(err) + } + }) // and then wait for it to cause the connection to close - if err := c.Wait(); err != nil && !isClosingError(err) { - return err - } - return nil + return c.Wait() } // readIncoming collects inbound messages from the reader and delivers them, either responding @@ -295,7 +298,9 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue ch msg, n, err := reader.Read(ctx) if err != nil { // The stream failed, we cannot continue - c.async.setError(err) + if !isClosingError(err) { + c.async.setError(err) + } return } switch msg := msg.(type) { @@ -395,7 +400,23 @@ func (c *Connection) manageQueue(ctx context.Context, preempter Preempter, fromR } func (c *Connection) deliverMessages(ctx context.Context, handler Handler, fromQueue <-chan *incoming) { - defer c.async.done() + defer func() { + // Close the underlying ReadWriteCloser if not already closed. We're about + // to mark the Connection as done, so we'd better actually be done! 😅 + // + // TODO(bcmills): This is actually a bit premature, since we may have + // asynchronous handlers still in flight at this point, but it's at least no + // more premature than calling c.async.done at this point (which we were + // already doing). This will get a proper fix in https://go.dev/cl/388134. + c.closeOnce.Do(func() { + if err := c.closer.Close(); err != nil { + c.async.setError(err) + } + }) + + c.async.done() + }() + for entry := range fromQueue { // cancel any messages in the queue that we have a pending cancel for var result interface{} From fd32990e09f03db6fe358c6dc9bb99bff0d6082d Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 1 Mar 2022 17:34:15 -0500 Subject: [PATCH 343/723] internal/jsonrpc2_v2: error out in-flight client calls when the reader breaks Otherwise, the Await method on the corresponding AsyncCall will never unblock, leading to a deadlock (detected by the test changes in CL 388597). For golang/go#46047 For golang/go#46520 For golang/go#49387 Change-Id: I7954f80786059772ddd7f8c98d8752d56d074919 Reviewed-on: https://go-review.googlesource.com/c/tools/+/388775 Run-TryBot: Bryan Mills Reviewed-by: Ian Cottrell gopls-CI: kokoro Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot --- internal/jsonrpc2_v2/conn.go | 49 ++++++++++++++++++++++++++++-------- internal/jsonrpc2_v2/wire.go | 4 +++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 15d40b4a4c2..0215c67af7e 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -175,9 +175,25 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{} // are racing the response. // rchan is buffered in case the response arrives without a listener. result.response = make(chan *Response, 1) - pending := <-c.outgoingBox - pending[result.id] = result.response - c.outgoingBox <- pending + outgoing, ok := <-c.outgoingBox + if !ok { + // If the call failed due to (say) an I/O error or broken pipe, attribute it + // as such. (If the error is nil, then the connection must have been shut + // down cleanly.) + err := c.async.wait() + if err == nil { + err = ErrClientClosing + } + + resp, respErr := NewResponse(result.id, nil, err) + if respErr != nil { + panic(fmt.Errorf("unexpected error from NewResponse: %w", respErr)) + } + result.response <- resp + return result + } + outgoing[result.id] = result.response + c.outgoingBox <- outgoing // now we are ready to send if err := c.write(ctx, call); err != nil { // sending failed, we will never get a response, so deliver a fake one @@ -290,8 +306,19 @@ func (c *Connection) Close() error { // readIncoming collects inbound messages from the reader and delivers them, either responding // to outgoing calls or feeding requests to the queue. -func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue chan<- *incoming) { - defer close(toQueue) +func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue chan<- *incoming) (err error) { + defer func() { + // Retire any outgoing requests that were still in flight. + // With the Reader no longer being processed, they necessarily cannot receive a response. + outgoing := <-c.outgoingBox + close(c.outgoingBox) // Prevent new outgoing requests, which would deadlock. + for id, responseBox := range outgoing { + responseBox <- &Response{ID: id, Error: err} + } + + close(toQueue) + }() + for { // get the next message // no lock is needed, this is the only reader @@ -301,7 +328,7 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue ch if !isClosingError(err) { c.async.setError(err) } - return + return err } switch msg := msg.(type) { case *Request: @@ -340,12 +367,12 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue ch } func (c *Connection) incomingResponse(msg *Response) { - pending := <-c.outgoingBox - response, ok := pending[msg.ID] - if ok { - delete(pending, msg.ID) + var response chan<- *Response + if outgoing, ok := <-c.outgoingBox; ok { + response = outgoing[msg.ID] + delete(outgoing, msg.ID) + c.outgoingBox <- outgoing } - c.outgoingBox <- pending if response != nil { response <- msg } diff --git a/internal/jsonrpc2_v2/wire.go b/internal/jsonrpc2_v2/wire.go index 4da129ae6e2..c99e497e33f 100644 --- a/internal/jsonrpc2_v2/wire.go +++ b/internal/jsonrpc2_v2/wire.go @@ -33,6 +33,10 @@ var ( ErrServerOverloaded = NewError(-32000, "JSON RPC overloaded") // ErrUnknown should be used for all non coded errors. ErrUnknown = NewError(-32001, "JSON RPC unknown error") + // ErrServerClosing is returned for calls that arrive while the server is closing. + ErrServerClosing = NewError(-32002, "JSON RPC server is closing") + // ErrClientClosing is a dummy error returned for calls initiated while the client is closing. + ErrClientClosing = NewError(-32003, "JSON RPC client is closing") ) const wireVersion = "2.0" From 5935531253bb9575611f9d7dbca99a37c27cd09f Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Mon, 17 Oct 2022 10:53:56 -0400 Subject: [PATCH 344/723] =?UTF-8?q?internal/jsonrpc2=5Fv2:=20remove=20?= =?UTF-8?q?=E2=80=9CBox=E2=80=9D=20suffix=20from=20channel=20field=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the suffixes I end up a little lost in the “box” noise while I'm reading — the channel ops alone suffice to make the storage mechanism clear. (To me, the mechanism of storing a value in a 1-buffered channel is conceptually similar to storing it in a pointer, atomic.Pointer, or similar — and we don't generally name those with a suffix either.) For golang/go#46047. For golang/go#46520. For golang/go#49387. Change-Id: I7f58a9ac532f597fe49ed70606d89bd8cbe33b55 Reviewed-on: https://go-review.googlesource.com/c/tools/+/443355 TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills Auto-Submit: Bryan Mills Reviewed-by: Alan Donovan gopls-CI: kokoro --- internal/jsonrpc2_v2/conn.go | 82 +++++++++++++-------------- internal/jsonrpc2_v2/jsonrpc2_test.go | 14 ++--- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 0215c67af7e..f010d9a0552 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -51,17 +51,17 @@ type Connection struct { closeOnce sync.Once closer io.Closer - writerBox chan Writer - outgoingBox chan map[ID]chan<- *Response - incomingBox chan map[ID]*incoming - async *async + writer chan Writer + outgoing chan map[ID]chan<- *Response + incoming chan map[ID]*incoming + async *async } type AsyncCall struct { - id ID - response chan *Response // the channel a response will be delivered on - resultBox chan asyncResult - endSpan func() // close the tracing span when all processing for the message is complete + id ID + response chan *Response // the channel a response will be delivered on + result chan asyncResult + endSpan func() // close the tracing span when all processing for the message is complete } type asyncResult struct { @@ -87,11 +87,11 @@ func (o ConnectionOptions) Bind(context.Context, *Connection) (ConnectionOptions // This is used by the Dial and Serve functions to build the actual connection. func newConnection(ctx context.Context, rwc io.ReadWriteCloser, binder Binder) (*Connection, error) { c := &Connection{ - closer: rwc, - writerBox: make(chan Writer, 1), - outgoingBox: make(chan map[ID]chan<- *Response, 1), - incomingBox: make(chan map[ID]*incoming, 1), - async: newAsync(), + closer: rwc, + writer: make(chan Writer, 1), + outgoing: make(chan map[ID]chan<- *Response, 1), + incoming: make(chan map[ID]*incoming, 1), + async: newAsync(), } options, err := binder.Bind(ctx, c) @@ -107,8 +107,8 @@ func newConnection(ctx context.Context, rwc io.ReadWriteCloser, binder Binder) ( if options.Handler == nil { options.Handler = defaultHandler{} } - c.outgoingBox <- make(map[ID]chan<- *Response) - c.incomingBox <- make(map[ID]*incoming) + c.outgoing <- make(map[ID]chan<- *Response) + c.incoming <- make(map[ID]*incoming) // the goroutines started here will continue until the underlying stream is closed reader := options.Framer.Reader(rwc) readToQueue := make(chan *incoming) @@ -119,7 +119,7 @@ func newConnection(ctx context.Context, rwc io.ReadWriteCloser, binder Binder) ( // releaseing the writer must be the last thing we do in case any requests // are blocked waiting for the connection to be ready - c.writerBox <- options.Framer.Writer(rwc) + c.writer <- options.Framer.Writer(rwc) return c, nil } @@ -154,14 +154,14 @@ func (c *Connection) Notify(ctx context.Context, method string, params interface // If sending the call failed, the response will be ready and have the error in it. func (c *Connection) Call(ctx context.Context, method string, params interface{}) *AsyncCall { result := &AsyncCall{ - id: Int64ID(atomic.AddInt64(&c.seq, 1)), - resultBox: make(chan asyncResult, 1), + id: Int64ID(atomic.AddInt64(&c.seq, 1)), + result: make(chan asyncResult, 1), } // generate a new request identifier call, err := NewCall(result.id, method, params) if err != nil { //set the result to failed - result.resultBox <- asyncResult{err: fmt.Errorf("marshaling call parameters: %w", err)} + result.result <- asyncResult{err: fmt.Errorf("marshaling call parameters: %w", err)} return result } ctx, endSpan := event.Start(ctx, method, @@ -175,7 +175,7 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{} // are racing the response. // rchan is buffered in case the response arrives without a listener. result.response = make(chan *Response, 1) - outgoing, ok := <-c.outgoingBox + outgoing, ok := <-c.outgoing if !ok { // If the call failed due to (say) an I/O error or broken pipe, attribute it // as such. (If the error is nil, then the connection must have been shut @@ -193,7 +193,7 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{} return result } outgoing[result.id] = result.response - c.outgoingBox <- outgoing + c.outgoing <- outgoing // now we are ready to send if err := c.write(ctx, call); err != nil { // sending failed, we will never get a response, so deliver a fake one @@ -212,8 +212,8 @@ func (a *AsyncCall) ID() ID { return a.id } // returned, or a call that failed to send in the first place. func (a *AsyncCall) IsReady() bool { select { - case r := <-a.resultBox: - a.resultBox <- r + case r := <-a.result: + a.result <- r return true default: return false @@ -236,14 +236,14 @@ func (a *AsyncCall) Await(ctx context.Context, result interface{}) error { r.result = response.Result event.Label(ctx, tag.StatusCode.Of("OK")) } - case r = <-a.resultBox: + case r = <-a.result: // result already available case <-ctx.Done(): event.Label(ctx, tag.StatusCode.Of("CANCELLED")) return ctx.Err() } // refill the box for the next caller - a.resultBox <- r + a.result <- r // and unpack the result if r.err != nil { return r.err @@ -259,8 +259,8 @@ func (a *AsyncCall) Await(ctx context.Context, result interface{}) error { // Respond must be called exactly once for any message for which a handler // returns ErrAsyncResponse. It must not be called for any other message. func (c *Connection) Respond(id ID, result interface{}, rerr error) error { - pending := <-c.incomingBox - defer func() { c.incomingBox <- pending }() + pending := <-c.incoming + defer func() { c.incoming <- pending }() entry, found := pending[id] if !found { return nil @@ -277,8 +277,8 @@ func (c *Connection) Respond(id ID, result interface{}, rerr error) error { // not cause any messages that have not arrived yet with that ID to be // cancelled. func (c *Connection) Cancel(id ID) { - pending := <-c.incomingBox - defer func() { c.incomingBox <- pending }() + pending := <-c.incoming + defer func() { c.incoming <- pending }() if entry, found := pending[id]; found && entry.cancel != nil { entry.cancel() entry.cancel = nil @@ -310,10 +310,10 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue ch defer func() { // Retire any outgoing requests that were still in flight. // With the Reader no longer being processed, they necessarily cannot receive a response. - outgoing := <-c.outgoingBox - close(c.outgoingBox) // Prevent new outgoing requests, which would deadlock. - for id, responseBox := range outgoing { - responseBox <- &Response{ID: id, Error: err} + outgoing := <-c.outgoing + close(c.outgoing) // Prevent new outgoing requests, which would deadlock. + for id, response := range outgoing { + response <- &Response{ID: id, Error: err} } close(toQueue) @@ -352,9 +352,9 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue ch // if the request is a call, add it to the incoming map so it can be // cancelled by id if msg.IsCall() { - pending := <-c.incomingBox + pending := <-c.incoming pending[msg.ID] = entry - c.incomingBox <- pending + c.incoming <- pending } // send the message to the incoming queue toQueue <- entry @@ -368,10 +368,10 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue ch func (c *Connection) incomingResponse(msg *Response) { var response chan<- *Response - if outgoing, ok := <-c.outgoingBox; ok { + if outgoing, ok := <-c.outgoing; ok { response = outgoing[msg.ID] delete(outgoing, msg.ID) - c.outgoingBox <- outgoing + c.outgoing <- outgoing } if response != nil { response <- msg @@ -468,8 +468,8 @@ func (c *Connection) deliverMessages(ctx context.Context, handler Handler, fromQ func (c *Connection) reply(entry *incoming, result interface{}, rerr error) { if entry.request.IsCall() { // we have a call finishing, remove it from the incoming map - pending := <-c.incomingBox - defer func() { c.incomingBox <- pending }() + pending := <-c.incoming + defer func() { c.incoming <- pending }() delete(pending, entry.request.ID) } if err := c.respond(entry, result, rerr); err != nil { @@ -527,8 +527,8 @@ func (c *Connection) respond(entry *incoming, result interface{}, rerr error) er // write is used by all things that write outgoing messages, including replies. // it makes sure that writes are atomic func (c *Connection) write(ctx context.Context, msg Message) error { - writer := <-c.writerBox - defer func() { c.writerBox <- writer }() + writer := <-c.writer + defer func() { c.writer <- writer }() n, err := writer.Write(ctx, msg) event.Metric(ctx, tag.SentBytes.Of(n)) return err diff --git a/internal/jsonrpc2_v2/jsonrpc2_test.go b/internal/jsonrpc2_v2/jsonrpc2_test.go index 8e90c235f93..c63a6a982bc 100644 --- a/internal/jsonrpc2_v2/jsonrpc2_test.go +++ b/internal/jsonrpc2_v2/jsonrpc2_test.go @@ -77,7 +77,7 @@ type binder struct { type handler struct { conn *jsonrpc2.Connection accumulator int - waitersBox chan map[string]chan struct{} + waiters chan map[string]chan struct{} calls map[string]*jsonrpc2.AsyncCall } @@ -256,11 +256,11 @@ func verifyResults(t *testing.T, method string, results interface{}, expect inte func (b binder) Bind(ctx context.Context, conn *jsonrpc2.Connection) (jsonrpc2.ConnectionOptions, error) { h := &handler{ - conn: conn, - waitersBox: make(chan map[string]chan struct{}, 1), - calls: make(map[string]*jsonrpc2.AsyncCall), + conn: conn, + waiters: make(chan map[string]chan struct{}, 1), + calls: make(map[string]*jsonrpc2.AsyncCall), } - h.waitersBox <- make(map[string]chan struct{}) + h.waiters <- make(map[string]chan struct{}) if b.runTest != nil { go b.runTest(h) } @@ -272,8 +272,8 @@ func (b binder) Bind(ctx context.Context, conn *jsonrpc2.Connection) (jsonrpc2.C } func (h *handler) waiter(name string) chan struct{} { - waiters := <-h.waitersBox - defer func() { h.waitersBox <- waiters }() + waiters := <-h.waiters + defer func() { h.waiters <- waiters }() waiter, found := waiters[name] if !found { waiter = make(chan struct{}) From 371ef162f2d8dc59b777cf3705f6afe18d3d6b5c Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Mon, 28 Feb 2022 17:19:32 -0500 Subject: [PATCH 345/723] internal/jsonrpc2_v2: rework concurrency in idleListener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This eliminates a race between a successful Accept call and a concurrent Close call, which previously could have shut down the 'run' goroutine before Accept sent to the newConns channel, causing Accept to deadlock. In fact, it eliminates the long-running 'run' goroutine entirely (replacing it with a time.Timer), and in the process avoids leaking O(N) closed connections when only the very first one is long-lived. It also eliminates a potential double-Close bug: if the run method had called l.wrapped.Close due to an idle timeout, a subsequent call to Close would invoke l.wrapped.Close again. The io.Closer method explicitly documents doubled Close calls as undefined behavior, and many Closer implementations (especially test fakes) panic or deadlock in that case. It also eliminates a timer leak if the Listener rapidly oscillates between active and idle: previously the implementation used time.After, but it now uses an explicit time.Timer which can be stopped (and garbage-collected) when the listener becomes active. Idleness is now tracked based on the connection's Close method rather than Read: we have no guarantee in general that a caller will ever actually invoke Read (if, for example, they Close the connection as soon as it is dialed), but we can reasonably expect a caller to at least try to ensure that Close is always called. We now also verify, using a finalizer on a best-effort basis, that the Close method on each connection is called. We use the finalizer to verify the Close call — rather than to close the connection implicitly — because closing the connection in a finalizer would delay the start of the idle timer by an arbitrary and unbounded duration after the last connection is actually no longer in use. Fixes golang/go#46047. Fixes golang/go#51435. For golang/go#46520. For golang/go#49387. Change-Id: If173a3ed7a44aff14734b72c8340122e8d020f26 Reviewed-on: https://go-review.googlesource.com/c/tools/+/388597 Run-TryBot: Bryan Mills TryBot-Result: Gopher Robot Reviewed-by: Ian Cottrell Auto-Submit: Bryan Mills gopls-CI: kokoro --- internal/jsonrpc2_v2/serve.go | 248 ++++++++++++++++++++--------- internal/jsonrpc2_v2/serve_test.go | 190 ++++++++++++++++++---- internal/jsonrpc2_v2/wire.go | 8 + 3 files changed, 334 insertions(+), 112 deletions(-) diff --git a/internal/jsonrpc2_v2/serve.go b/internal/jsonrpc2_v2/serve.go index 646267b5573..5ffce681e91 100644 --- a/internal/jsonrpc2_v2/serve.go +++ b/internal/jsonrpc2_v2/serve.go @@ -7,6 +7,7 @@ package jsonrpc2 import ( "context" "errors" + "fmt" "io" "runtime" "strings" @@ -171,80 +172,121 @@ func isClosingError(err error) bool { } // NewIdleListener wraps a listener with an idle timeout. -// When there are no active connections for at least the timeout duration a -// call to accept will fail with ErrIdleTimeout. +// +// When there are no active connections for at least the timeout duration, +// calls to Accept will fail with ErrIdleTimeout. +// +// A connection is considered inactive as soon as its Close method is called. func NewIdleListener(timeout time.Duration, wrap Listener) Listener { l := &idleListener{ - timeout: timeout, - wrapped: wrap, - newConns: make(chan *idleCloser), - closed: make(chan struct{}), - wasTimeout: make(chan struct{}), + wrapped: wrap, + timeout: timeout, + active: make(chan int, 1), + timedOut: make(chan struct{}), + idleTimer: make(chan *time.Timer, 1), } - go l.run() + l.idleTimer <- time.AfterFunc(l.timeout, l.timerExpired) return l } type idleListener struct { - wrapped Listener - timeout time.Duration - newConns chan *idleCloser - closed chan struct{} - wasTimeout chan struct{} - closeOnce sync.Once -} + wrapped Listener + timeout time.Duration -type idleCloser struct { - wrapped io.ReadWriteCloser - closed chan struct{} - closeOnce sync.Once + // Only one of these channels is receivable at any given time. + active chan int // count of active connections; closed when Close is called if not timed out + timedOut chan struct{} // closed when the idle timer expires + idleTimer chan *time.Timer // holds the timer only when idle } -func (c *idleCloser) Read(p []byte) (int, error) { - n, err := c.wrapped.Read(p) - if err != nil && isClosingError(err) { - c.closeOnce.Do(func() { close(c.closed) }) +// Accept accepts an incoming connection. +// +// If an incoming connection is accepted concurrent to the listener being closed +// due to idleness, the new connection is immediately closed. +func (l *idleListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { + rwc, err := l.wrapped.Accept(ctx) + + if err != nil && !isClosingError(err) { + return nil, err } - return n, err -} -func (c *idleCloser) Write(p []byte) (int, error) { - // we do not close on write failure, we rely on the wrapped writer to do that - // if it is appropriate, which we will detect in the next read. - return c.wrapped.Write(p) -} + select { + case n, ok := <-l.active: + if err != nil { + if ok { + l.active <- n + } + return nil, err + } + if ok { + l.active <- n + 1 + } else { + // l.wrapped.Close Close has been called, but Accept returned a + // connection. This race can occur with concurrent Accept and Close calls + // with any net.Listener, and it is benign: since the listener was closed + // explicitly, it can't have also timed out. + } + return l.newConn(rwc), nil -func (c *idleCloser) Close() error { - // we rely on closing the wrapped stream to signal to the next read that we - // are closed, rather than triggering the closed signal directly - return c.wrapped.Close() -} + case <-l.timedOut: + if err == nil { + // Keeping the connection open would leave the listener simultaneously + // active and closed due to idleness, which would be contradictory and + // confusing. Close the connection and pretend that it never happened. + rwc.Close() + } + return nil, ErrIdleTimeout -func (l *idleListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { - rwc, err := l.wrapped.Accept(ctx) - if err != nil { - if isClosingError(err) { - // underlying listener was closed - l.closeOnce.Do(func() { close(l.closed) }) - // was it closed because of the idle timeout? - select { - case <-l.wasTimeout: - err = ErrIdleTimeout - default: - } + case timer := <-l.idleTimer: + if err != nil { + // The idle timer hasn't run yet, so err can't be ErrIdleTimeout. + // Leave the idle timer as it was and return whatever error we got. + l.idleTimer <- timer + return nil, err } - return nil, err - } - conn := &idleCloser{ - wrapped: rwc, - closed: make(chan struct{}), + + if !timer.Stop() { + // Failed to stop the timer — the timer goroutine is in the process of + // firing. Send the timer back to the timer goroutine so that it can + // safely close the timedOut channel, and then wait for the listener to + // actually be closed before we return ErrIdleTimeout. + l.idleTimer <- timer + rwc.Close() + <-l.timedOut + return nil, ErrIdleTimeout + } + + l.active <- 1 + return l.newConn(rwc), nil } - l.newConns <- conn - return conn, err } func (l *idleListener) Close() error { - defer l.closeOnce.Do(func() { close(l.closed) }) + select { + case _, ok := <-l.active: + if ok { + close(l.active) + } + + case <-l.timedOut: + // Already closed by the timer; take care not to double-close if the caller + // only explicitly invokes this Close method once, since the io.Closer + // interface explicitly leaves doubled Close calls undefined. + return ErrIdleTimeout + + case timer := <-l.idleTimer: + if !timer.Stop() { + // Couldn't stop the timer. It shouldn't take long to run, so just wait + // (so that the Listener is guaranteed to be closed before we return) + // and pretend that this call happened afterward. + // That way we won't leak any timers or goroutines when Close returns. + l.idleTimer <- timer + <-l.timedOut + return ErrIdleTimeout + } + close(l.active) + } + return l.wrapped.Close() } @@ -252,31 +294,83 @@ func (l *idleListener) Dialer() Dialer { return l.wrapped.Dialer() } -func (l *idleListener) run() { - var conns []*idleCloser - for { - var firstClosed chan struct{} // left at nil if there are no active conns - var timeout <-chan time.Time // left at nil if there are active conns - if len(conns) > 0 { - firstClosed = conns[0].closed +func (l *idleListener) timerExpired() { + select { + case n, ok := <-l.active: + if ok { + panic(fmt.Sprintf("jsonrpc2: idleListener idle timer fired with %d connections still active", n)) } else { - timeout = time.After(l.timeout) + panic("jsonrpc2: Close finished with idle timer still running") } - select { - case <-l.closed: - // the main listener closed, no need to keep going + + case <-l.timedOut: + panic("jsonrpc2: idleListener idle timer fired more than once") + + case <-l.idleTimer: + // The timer for this very call! + } + + // Close the Listener with all channels still blocked to ensure that this call + // to l.wrapped.Close doesn't race with the one in l.Close. + defer close(l.timedOut) + l.wrapped.Close() +} + +func (l *idleListener) connClosed() { + select { + case n, ok := <-l.active: + if !ok { + // l is already closed, so it can't close due to idleness, + // and we don't need to track the number of active connections any more. return - case conn := <-l.newConns: - // a new conn arrived, add it to the list - conns = append(conns, conn) - case <-timeout: - // we timed out, only happens when there are no active conns - // close the underlying listener, and allow the normal closing process to happen - close(l.wasTimeout) - l.wrapped.Close() - case <-firstClosed: - // a conn closed, remove it from the active list - conns = conns[:copy(conns, conns[1:])] } + n-- + if n == 0 { + l.idleTimer <- time.AfterFunc(l.timeout, l.timerExpired) + } else { + l.active <- n + } + + case <-l.timedOut: + panic("jsonrpc2: idleListener idle timer fired before last active connection was closed") + + case <-l.idleTimer: + panic("jsonrpc2: idleListener idle timer active before last active connection was closed") } } + +type idleListenerConn struct { + wrapped io.ReadWriteCloser + l *idleListener + closeOnce sync.Once +} + +func (l *idleListener) newConn(rwc io.ReadWriteCloser) *idleListenerConn { + c := &idleListenerConn{ + wrapped: rwc, + l: l, + } + + // A caller that forgets to call Close may disrupt the idleListener's + // accounting, even though the file descriptor for the underlying connection + // may eventually be garbage-collected anyway. + // + // Set a (best-effort) finalizer to verify that a Close call always occurs. + // (We will clear the finalizer explicitly in Close.) + runtime.SetFinalizer(c, func(c *idleListenerConn) { + panic("jsonrpc2: IdleListener connection became unreachable without a call to Close") + }) + + return c +} + +func (c *idleListenerConn) Read(p []byte) (int, error) { return c.wrapped.Read(p) } +func (c *idleListenerConn) Write(p []byte) (int, error) { return c.wrapped.Write(p) } + +func (c *idleListenerConn) Close() error { + defer c.closeOnce.Do(func() { + c.l.connClosed() + runtime.SetFinalizer(c, nil) + }) + return c.wrapped.Close() +} diff --git a/internal/jsonrpc2_v2/serve_test.go b/internal/jsonrpc2_v2/serve_test.go index 26cf6a58c4e..9ca513361b9 100644 --- a/internal/jsonrpc2_v2/serve_test.go +++ b/internal/jsonrpc2_v2/serve_test.go @@ -7,6 +7,8 @@ package jsonrpc2_test import ( "context" "errors" + "fmt" + "runtime/debug" "testing" "time" @@ -16,48 +18,117 @@ import ( func TestIdleTimeout(t *testing.T) { stacktest.NoLeak(t) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - listener, err := jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{}) - if err != nil { - t.Fatal(err) - } - listener = jsonrpc2.NewIdleListener(100*time.Millisecond, listener) - defer listener.Close() - server, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.ConnectionOptions{}) - if err != nil { - t.Fatal(err) - } + // Use a panicking time.AfterFunc instead of context.WithTimeout so that we + // get a goroutine dump on failure. We expect the test to take on the order of + // a few tens of milliseconds at most, so 10s should be several orders of + // magnitude of headroom. + timer := time.AfterFunc(10*time.Second, func() { + debug.SetTraceback("all") + panic("TestIdleTimeout deadlocked") + }) + defer timer.Stop() + ctx := context.Background() - connect := func() *jsonrpc2.Connection { - client, err := jsonrpc2.Dial(ctx, - listener.Dialer(), - jsonrpc2.ConnectionOptions{}) + try := func(d time.Duration) (longEnough bool) { + listener, err := jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{}) if err != nil { t.Fatal(err) } - return client - } - // Exercise some connection/disconnection patterns, and then assert that when - // our timer fires, the server exits. - conn1 := connect() - conn2 := connect() - if err := conn1.Close(); err != nil { - t.Fatalf("conn1.Close failed with error: %v", err) - } - if err := conn2.Close(); err != nil { - t.Fatalf("conn2.Close failed with error: %v", err) - } - conn3 := connect() - if err := conn3.Close(); err != nil { - t.Fatalf("conn3.Close failed with error: %v", err) - } - serverError := server.Wait() + idleStart := time.Now() + listener = jsonrpc2.NewIdleListener(d, listener) + defer listener.Close() + + server, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.ConnectionOptions{}) + if err != nil { + t.Fatal(err) + } + + // Exercise some connection/disconnection patterns, and then assert that when + // our timer fires, the server exits. + conn1, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + if err != nil { + if since := time.Since(idleStart); since < d { + t.Fatalf("conn1 failed to connect after %v: %v", since, err) + } + t.Log("jsonrpc2.Dial:", err) + return false // Took to long to dial, so the failure could have been due to the idle timeout. + } + // On the server side, Accept can race with the connection timing out. + // Send a call and wait for the response to ensure that the connection was + // actually fully accepted. + ac := conn1.Call(ctx, "ping", nil) + if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { + if since := time.Since(idleStart); since < d { + t.Fatalf("conn1 broken after %v: %v", since, err) + } + t.Log(`conn1.Call(ctx, "ping", nil):`, err) + conn1.Close() + return false + } + + conn2, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + if err != nil { + conn1.Close() + if since := time.Since(idleStart); since < d { + t.Fatalf("conn2 failed to connect while non-idle: %v", err) + } + t.Log("jsonrpc2.Dial:", err) + return false + } + + if err := conn1.Close(); err != nil { + t.Fatalf("conn1.Close failed with error: %v", err) + } + idleStart = time.Now() + if err := conn2.Close(); err != nil { + t.Fatalf("conn2.Close failed with error: %v", err) + } + + conn3, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + if err != nil { + if since := time.Since(idleStart); since < d { + t.Fatalf("conn3 failed to connect after %v: %v", since, err) + } + t.Log("jsonrpc2.Dial:", err) + return false // Took to long to dial, so the failure could have been due to the idle timeout. + } + + ac = conn3.Call(ctx, "ping", nil) + if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { + if since := time.Since(idleStart); since < d { + t.Fatalf("conn3 broken after %v: %v", since, err) + } + t.Log(`conn3.Call(ctx, "ping", nil):`, err) + conn3.Close() + return false + } + + idleStart = time.Now() + if err := conn3.Close(); err != nil { + t.Fatalf("conn3.Close failed with error: %v", err) + } + + serverError := server.Wait() - if !errors.Is(serverError, jsonrpc2.ErrIdleTimeout) { - t.Errorf("run() returned error %v, want %v", serverError, jsonrpc2.ErrIdleTimeout) + if !errors.Is(serverError, jsonrpc2.ErrIdleTimeout) { + t.Errorf("run() returned error %v, want %v", serverError, jsonrpc2.ErrIdleTimeout) + } + if since := time.Since(idleStart); since < d { + t.Errorf("server shut down after %v idle; want at least %v", since, d) + } + return true + } + + d := 1 * time.Millisecond + for { + t.Logf("testing with idle timout %v", d) + if !try(d) { + d *= 2 + continue + } + break } } @@ -142,3 +213,52 @@ func newFake(t *testing.T, ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2. server.Wait() }, nil } + +// TestIdleListenerAcceptCloseRace checks for the Accept/Close race fixed in CL 388597. +// +// (A bug in the idleListener implementation caused a successful Accept to block +// on sending to a background goroutine that could have already exited.) +func TestIdleListenerAcceptCloseRace(t *testing.T) { + ctx := context.Background() + + n := 10 + + // Each iteration of the loop appears to take around a millisecond, so to + // avoid spurious failures we'll set the watchdog for three orders of + // magnitude longer. When the bug was present, this reproduced the deadlock + // reliably on a Linux workstation when run with -count=100, which should be + // frequent enough to show up on the Go build dashboard if it regresses. + watchdog := time.Duration(n) * 1000 * time.Millisecond + timer := time.AfterFunc(watchdog, func() { + debug.SetTraceback("all") + panic(fmt.Sprintf("TestAcceptCloseRace deadlocked after %v", watchdog)) + }) + defer timer.Stop() + + for ; n > 0; n-- { + listener, err := jsonrpc2.NetPipeListener(ctx) + if err != nil { + t.Fatal(err) + } + listener = jsonrpc2.NewIdleListener(24*time.Hour, listener) + + done := make(chan struct{}) + go func() { + conn, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + listener.Close() + if err == nil { + conn.Close() + } + close(done) + }() + + // Accept may return a non-nil error if Close closes the underlying network + // connection before the wrapped Accept call unblocks. However, it must not + // deadlock! + c, err := listener.Accept(ctx) + if err == nil { + c.Close() + } + <-done + } +} diff --git a/internal/jsonrpc2_v2/wire.go b/internal/jsonrpc2_v2/wire.go index c99e497e33f..c8dc9ebf1bf 100644 --- a/internal/jsonrpc2_v2/wire.go +++ b/internal/jsonrpc2_v2/wire.go @@ -76,3 +76,11 @@ func NewError(code int64, message string) error { func (err *wireError) Error() string { return err.Message } + +func (err *wireError) Is(other error) bool { + w, ok := other.(*wireError) + if !ok { + return false + } + return err.Code == w.Code +} From b2efd4d155d2908456f148c650480be8135e1e9c Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 1 Mar 2022 15:17:42 -0500 Subject: [PATCH 346/723] internal/jsonrpc2_v2: eliminate most arbitrary timeouts in tests For golang/go#46047 For golang/go#49387 For golang/go#46520 Change-Id: Ib72863a024d74f45c70a6cb53482cb606510f7e4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/388598 Run-TryBot: Bryan Mills Reviewed-by: Ian Cottrell TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Bryan Mills --- internal/jsonrpc2_v2/jsonrpc2_test.go | 5 ----- internal/jsonrpc2_v2/serve_test.go | 5 ++--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/internal/jsonrpc2_v2/jsonrpc2_test.go b/internal/jsonrpc2_v2/jsonrpc2_test.go index c63a6a982bc..b2fa4963b74 100644 --- a/internal/jsonrpc2_v2/jsonrpc2_test.go +++ b/internal/jsonrpc2_v2/jsonrpc2_test.go @@ -11,7 +11,6 @@ import ( "path" "reflect" "testing" - "time" "golang.org/x/tools/internal/event/export/eventtest" jsonrpc2 "golang.org/x/tools/internal/jsonrpc2_v2" @@ -370,8 +369,6 @@ func (h *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{ return true, nil case <-ctx.Done(): return nil, ctx.Err() - case <-time.After(time.Second): - return nil, fmt.Errorf("wait for %q timed out", name) } case "fork": var name string @@ -385,8 +382,6 @@ func (h *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{ h.conn.Respond(req.ID, true, nil) case <-ctx.Done(): h.conn.Respond(req.ID, nil, ctx.Err()) - case <-time.After(time.Second): - h.conn.Respond(req.ID, nil, fmt.Errorf("wait for %q timed out", name)) } }() return nil, jsonrpc2.ErrAsyncResponse diff --git a/internal/jsonrpc2_v2/serve_test.go b/internal/jsonrpc2_v2/serve_test.go index 9ca513361b9..f0c27a8be56 100644 --- a/internal/jsonrpc2_v2/serve_test.go +++ b/internal/jsonrpc2_v2/serve_test.go @@ -28,6 +28,7 @@ func TestIdleTimeout(t *testing.T) { panic("TestIdleTimeout deadlocked") }) defer timer.Stop() + ctx := context.Background() try := func(d time.Duration) (longEnough bool) { @@ -149,8 +150,7 @@ func (fakeHandler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface func TestServe(t *testing.T) { stacktest.NoLeak(t) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + ctx := context.Background() tests := []struct { name string @@ -187,7 +187,6 @@ func TestServe(t *testing.T) { } func newFake(t *testing.T, ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2.Connection, func(), error) { - l = jsonrpc2.NewIdleListener(100*time.Millisecond, l) server, err := jsonrpc2.Serve(ctx, l, jsonrpc2.ConnectionOptions{ Handler: fakeHandler{}, }) From 4818d9eec9ede7da284bef811eb0a7835d9a41df Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 17 Oct 2022 17:08:50 +0000 Subject: [PATCH 347/723] Revert "gopls/internal/lsp/cache: disable strict analysis while we fix panics" This reverts commit 9b5e55b1a7e215a54c9784492d801104a8381a91. Reason for revert: Should have been fixed by https://go.dev/cl/443100 Change-Id: I44eeff6605697b745c3729ac1ec49b9c09114f71 Reviewed-on: https://go-review.googlesource.com/c/tools/+/443340 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/analysis.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index c75b8de36b4..6da77b59045 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -261,12 +261,7 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a // discover this problem in regular packages. // For command-line-arguments we quietly abort the analysis // for now since we already know there is a bug. - - // Temporarily disable bug reporting due to golang/go#56035: using - // bug.Errorf causes too many test flakes. - // TODO(rfindley): switch back to bug.Errorf once panics are fixed. - errorf := fmt.Errorf - + errorf := bug.Errorf // report this discovery if source.IsCommandLineArguments(pkg.ID()) { errorf = fmt.Errorf // suppress reporting } @@ -372,9 +367,9 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a // An Analyzer crashed. This is often merely a symptom // of a problem in package loading. // - // We are aware of a likely cause for these panics: the actionHandle - // cache key is not correct. In the meantime, disable strict mode. - const strict = false + // We believe that CL 420538 may have fixed these crashes, so enable + // strict checks in tests. + const strict = true if strict && bug.PanicOnBugs && analyzer.Name != "fact_purity" { // During testing, crash. See issues 54762, 56035. // But ignore analyzers with known crash bugs: From a9b653b41147f7b91117ee2c93e3072434f25fc5 Mon Sep 17 00:00:00 2001 From: Keith Randall Date: Mon, 17 Oct 2022 14:30:01 -0700 Subject: [PATCH 348/723] cmd/compilebench: use -a instead of -i to ensure dependencies are built -i has been deprecated since 1.15, and is now gone in the soon-to-be-1.20. Change-Id: Ib570f2bcfd0b1feb937990b72e4c9e43e1ada8cd Reviewed-on: https://go-review.googlesource.com/c/tools/+/443456 gopls-CI: kokoro Reviewed-by: David Chase TryBot-Result: Gopher Robot Reviewed-by: Keith Randall Run-TryBot: Keith Randall --- cmd/compilebench/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/compilebench/main.go b/cmd/compilebench/main.go index 28dc450e9e0..abdf28a2d95 100644 --- a/cmd/compilebench/main.go +++ b/cmd/compilebench/main.go @@ -336,9 +336,9 @@ func (compile) long() bool { return false } func (c compile) run(name string, count int) error { // Make sure dependencies needed by go tool compile are installed to GOROOT/pkg. - out, err := exec.Command(*flagGoCmd, "build", "-i", c.dir).CombinedOutput() + out, err := exec.Command(*flagGoCmd, "build", "-a", c.dir).CombinedOutput() if err != nil { - return fmt.Errorf("go build -i %s: %v\n%s", c.dir, err, out) + return fmt.Errorf("go build -a %s: %v\n%s", c.dir, err, out) } // Find dir and source file list. @@ -406,9 +406,9 @@ func (r link) run(name string, count int) error { } // Build dependencies. - out, err := exec.Command(*flagGoCmd, "build", "-i", "-o", "/dev/null", r.dir).CombinedOutput() + out, err := exec.Command(*flagGoCmd, "build", "-a", "-o", "/dev/null", r.dir).CombinedOutput() if err != nil { - return fmt.Errorf("go build -i %s: %v\n%s", r.dir, err, out) + return fmt.Errorf("go build -a %s: %v\n%s", r.dir, err, out) } // Build the main package. From bc2e3aeaba30b86fc421aefdfc78094b43700314 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Mon, 28 Feb 2022 11:24:36 -0500 Subject: [PATCH 349/723] internal/jsonrpc2_v2: add Func convenience wrappers for the Binder and Preempter interfaces Change-Id: Ib21dabb908c13d9bbf50f053a342a8644d3ee68b Reviewed-on: https://go-review.googlesource.com/c/tools/+/388596 Run-TryBot: Bryan Mills gopls-CI: kokoro Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- internal/jsonrpc2_v2/conn.go | 9 +++++++++ internal/jsonrpc2_v2/jsonrpc2.go | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index f010d9a0552..7995f404e58 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -28,6 +28,15 @@ type Binder interface { Bind(context.Context, *Connection) (ConnectionOptions, error) } +// A BinderFunc implements the Binder interface for a standalone Bind function. +type BinderFunc func(context.Context, *Connection) (ConnectionOptions, error) + +func (f BinderFunc) Bind(ctx context.Context, c *Connection) (ConnectionOptions, error) { + return f(ctx, c) +} + +var _ Binder = BinderFunc(nil) + // ConnectionOptions holds the options for new connections. type ConnectionOptions struct { // Framer allows control over the message framing and encoding. diff --git a/internal/jsonrpc2_v2/jsonrpc2.go b/internal/jsonrpc2_v2/jsonrpc2.go index e685584427a..e9164b0bc95 100644 --- a/internal/jsonrpc2_v2/jsonrpc2.go +++ b/internal/jsonrpc2_v2/jsonrpc2.go @@ -47,6 +47,15 @@ type Preempter interface { Preempt(ctx context.Context, req *Request) (result interface{}, err error) } +// A PreempterFunc implements the Preempter interface for a standalone Preempt function. +type PreempterFunc func(ctx context.Context, req *Request) (interface{}, error) + +func (f PreempterFunc) Preempt(ctx context.Context, req *Request) (interface{}, error) { + return f(ctx, req) +} + +var _ Preempter = PreempterFunc(nil) + // Handler handles messages on a connection. type Handler interface { // Handle is invoked sequentially for each incoming request that has not @@ -75,12 +84,15 @@ func (defaultHandler) Handle(context.Context, *Request) (interface{}, error) { return nil, ErrNotHandled } +// A HandlerFunc implements the Handler interface for a standalone Handle function. type HandlerFunc func(ctx context.Context, req *Request) (interface{}, error) func (f HandlerFunc) Handle(ctx context.Context, req *Request) (interface{}, error) { return f(ctx, req) } +var _ Handler = HandlerFunc(nil) + // async is a small helper for operations with an asynchronous result that you // can wait for. type async struct { From d67c3ada0900abbc8e2bf23bb224ebf294fa2026 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Tue, 18 Oct 2022 08:54:03 -0400 Subject: [PATCH 350/723] internal/imports: repair warnings from default analyzers Some of the analyzers that are on by default in gopls produce many distracting warnings. These are 'composites' and 'simplifycompositelit'. This CL changes code to remove the 160 or so warnings. An alternative would be to remove these from the default set of gopls analyzers. Change-Id: Id5724a5aef260939dedd21a37e28f3d05bfa023d Reviewed-on: https://go-review.googlesource.com/c/tools/+/443635 Reviewed-by: Heschi Kreinick Run-TryBot: Peter Weinberger gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/imports/fix.go | 4 +- internal/imports/mkstdlib.go | 7 +- internal/imports/mod.go | 12 +- internal/imports/zstdlib.go | 310 ++++++++++++++++++----------------- 4 files changed, 171 insertions(+), 162 deletions(-) diff --git a/internal/imports/fix.go b/internal/imports/fix.go index 45a492ef039..9b7b106fde1 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -1372,9 +1372,9 @@ func (r *gopathResolver) scan(ctx context.Context, callback *scanCallback) error return err } var roots []gopathwalk.Root - roots = append(roots, gopathwalk.Root{filepath.Join(goenv["GOROOT"], "src"), gopathwalk.RootGOROOT}) + roots = append(roots, gopathwalk.Root{Path: filepath.Join(goenv["GOROOT"], "src"), Type: gopathwalk.RootGOROOT}) for _, p := range filepath.SplitList(goenv["GOPATH"]) { - roots = append(roots, gopathwalk.Root{filepath.Join(p, "src"), gopathwalk.RootGOPATH}) + roots = append(roots, gopathwalk.Root{Path: filepath.Join(p, "src"), Type: gopathwalk.RootGOPATH}) } // The callback is not necessarily safe to use in the goroutine below. Process roots eagerly. roots = filterRoots(roots, callback.rootFound) diff --git a/internal/imports/mkstdlib.go b/internal/imports/mkstdlib.go index 072e4e1d02c..a73f024c1d0 100644 --- a/internal/imports/mkstdlib.go +++ b/internal/imports/mkstdlib.go @@ -49,6 +49,11 @@ func main() { outf := func(format string, args ...interface{}) { fmt.Fprintf(&buf, format, args...) } + outf(`// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +`) outf("// Code generated by mkstdlib.go. DO NOT EDIT.\n\n") outf("package imports\n") outf("var stdlib = map[string][]string{\n") @@ -77,7 +82,7 @@ func main() { } sort.Strings(paths) for _, path := range paths { - outf("\t%q: []string{\n", path) + outf("\t%q: {\n", path) pkg := pkgs[path] var syms []string for sym := range pkg { diff --git a/internal/imports/mod.go b/internal/imports/mod.go index dec388bc099..7d99d04ca8a 100644 --- a/internal/imports/mod.go +++ b/internal/imports/mod.go @@ -129,22 +129,22 @@ func (r *ModuleResolver) init() error { }) r.roots = []gopathwalk.Root{ - {filepath.Join(goenv["GOROOT"], "/src"), gopathwalk.RootGOROOT}, + {Path: filepath.Join(goenv["GOROOT"], "/src"), Type: gopathwalk.RootGOROOT}, } r.mainByDir = make(map[string]*gocommand.ModuleJSON) for _, main := range r.mains { - r.roots = append(r.roots, gopathwalk.Root{main.Dir, gopathwalk.RootCurrentModule}) + r.roots = append(r.roots, gopathwalk.Root{Path: main.Dir, Type: gopathwalk.RootCurrentModule}) r.mainByDir[main.Dir] = main } if vendorEnabled { - r.roots = append(r.roots, gopathwalk.Root{r.dummyVendorMod.Dir, gopathwalk.RootOther}) + r.roots = append(r.roots, gopathwalk.Root{Path: r.dummyVendorMod.Dir, Type: gopathwalk.RootOther}) } else { addDep := func(mod *gocommand.ModuleJSON) { if mod.Replace == nil { // This is redundant with the cache, but we'll skip it cheaply enough. - r.roots = append(r.roots, gopathwalk.Root{mod.Dir, gopathwalk.RootModuleCache}) + r.roots = append(r.roots, gopathwalk.Root{Path: mod.Dir, Type: gopathwalk.RootModuleCache}) } else { - r.roots = append(r.roots, gopathwalk.Root{mod.Dir, gopathwalk.RootOther}) + r.roots = append(r.roots, gopathwalk.Root{Path: mod.Dir, Type: gopathwalk.RootOther}) } } // Walk dependent modules before scanning the full mod cache, direct deps first. @@ -158,7 +158,7 @@ func (r *ModuleResolver) init() error { addDep(mod) } } - r.roots = append(r.roots, gopathwalk.Root{r.moduleCacheDir, gopathwalk.RootModuleCache}) + r.roots = append(r.roots, gopathwalk.Root{Path: r.moduleCacheDir, Type: gopathwalk.RootModuleCache}) } r.scannedRoots = map[gopathwalk.Root]bool{} diff --git a/internal/imports/zstdlib.go b/internal/imports/zstdlib.go index 116a149ba14..5db9b2d4c73 100644 --- a/internal/imports/zstdlib.go +++ b/internal/imports/zstdlib.go @@ -1,9 +1,13 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + // Code generated by mkstdlib.go. DO NOT EDIT. package imports var stdlib = map[string][]string{ - "archive/tar": []string{ + "archive/tar": { "ErrFieldTooLong", "ErrHeader", "ErrWriteAfterClose", @@ -34,7 +38,7 @@ var stdlib = map[string][]string{ "TypeXHeader", "Writer", }, - "archive/zip": []string{ + "archive/zip": { "Compressor", "Decompressor", "Deflate", @@ -54,7 +58,7 @@ var stdlib = map[string][]string{ "Store", "Writer", }, - "bufio": []string{ + "bufio": { "ErrAdvanceTooFar", "ErrBadReadCount", "ErrBufferFull", @@ -81,7 +85,7 @@ var stdlib = map[string][]string{ "SplitFunc", "Writer", }, - "bytes": []string{ + "bytes": { "Buffer", "Compare", "Contains", @@ -138,11 +142,11 @@ var stdlib = map[string][]string{ "TrimSpace", "TrimSuffix", }, - "compress/bzip2": []string{ + "compress/bzip2": { "NewReader", "StructuralError", }, - "compress/flate": []string{ + "compress/flate": { "BestCompression", "BestSpeed", "CorruptInputError", @@ -160,7 +164,7 @@ var stdlib = map[string][]string{ "WriteError", "Writer", }, - "compress/gzip": []string{ + "compress/gzip": { "BestCompression", "BestSpeed", "DefaultCompression", @@ -175,7 +179,7 @@ var stdlib = map[string][]string{ "Reader", "Writer", }, - "compress/lzw": []string{ + "compress/lzw": { "LSB", "MSB", "NewReader", @@ -184,7 +188,7 @@ var stdlib = map[string][]string{ "Reader", "Writer", }, - "compress/zlib": []string{ + "compress/zlib": { "BestCompression", "BestSpeed", "DefaultCompression", @@ -201,7 +205,7 @@ var stdlib = map[string][]string{ "Resetter", "Writer", }, - "container/heap": []string{ + "container/heap": { "Fix", "Init", "Interface", @@ -209,16 +213,16 @@ var stdlib = map[string][]string{ "Push", "Remove", }, - "container/list": []string{ + "container/list": { "Element", "List", "New", }, - "container/ring": []string{ + "container/ring": { "New", "Ring", }, - "context": []string{ + "context": { "Background", "CancelFunc", "Canceled", @@ -230,7 +234,7 @@ var stdlib = map[string][]string{ "WithTimeout", "WithValue", }, - "crypto": []string{ + "crypto": { "BLAKE2b_256", "BLAKE2b_384", "BLAKE2b_512", @@ -259,12 +263,12 @@ var stdlib = map[string][]string{ "Signer", "SignerOpts", }, - "crypto/aes": []string{ + "crypto/aes": { "BlockSize", "KeySizeError", "NewCipher", }, - "crypto/cipher": []string{ + "crypto/cipher": { "AEAD", "Block", "BlockMode", @@ -281,13 +285,13 @@ var stdlib = map[string][]string{ "StreamReader", "StreamWriter", }, - "crypto/des": []string{ + "crypto/des": { "BlockSize", "KeySizeError", "NewCipher", "NewTripleDESCipher", }, - "crypto/dsa": []string{ + "crypto/dsa": { "ErrInvalidPublicKey", "GenerateKey", "GenerateParameters", @@ -302,7 +306,7 @@ var stdlib = map[string][]string{ "Sign", "Verify", }, - "crypto/ecdsa": []string{ + "crypto/ecdsa": { "GenerateKey", "PrivateKey", "PublicKey", @@ -311,7 +315,7 @@ var stdlib = map[string][]string{ "Verify", "VerifyASN1", }, - "crypto/ed25519": []string{ + "crypto/ed25519": { "GenerateKey", "NewKeyFromSeed", "PrivateKey", @@ -323,7 +327,7 @@ var stdlib = map[string][]string{ "SignatureSize", "Verify", }, - "crypto/elliptic": []string{ + "crypto/elliptic": { "Curve", "CurveParams", "GenerateKey", @@ -336,28 +340,28 @@ var stdlib = map[string][]string{ "Unmarshal", "UnmarshalCompressed", }, - "crypto/hmac": []string{ + "crypto/hmac": { "Equal", "New", }, - "crypto/md5": []string{ + "crypto/md5": { "BlockSize", "New", "Size", "Sum", }, - "crypto/rand": []string{ + "crypto/rand": { "Int", "Prime", "Read", "Reader", }, - "crypto/rc4": []string{ + "crypto/rc4": { "Cipher", "KeySizeError", "NewCipher", }, - "crypto/rsa": []string{ + "crypto/rsa": { "CRTValue", "DecryptOAEP", "DecryptPKCS1v15", @@ -382,13 +386,13 @@ var stdlib = map[string][]string{ "VerifyPKCS1v15", "VerifyPSS", }, - "crypto/sha1": []string{ + "crypto/sha1": { "BlockSize", "New", "Size", "Sum", }, - "crypto/sha256": []string{ + "crypto/sha256": { "BlockSize", "New", "New224", @@ -397,7 +401,7 @@ var stdlib = map[string][]string{ "Sum224", "Sum256", }, - "crypto/sha512": []string{ + "crypto/sha512": { "BlockSize", "New", "New384", @@ -412,7 +416,7 @@ var stdlib = map[string][]string{ "Sum512_224", "Sum512_256", }, - "crypto/subtle": []string{ + "crypto/subtle": { "ConstantTimeByteEq", "ConstantTimeCompare", "ConstantTimeCopy", @@ -420,7 +424,7 @@ var stdlib = map[string][]string{ "ConstantTimeLessOrEq", "ConstantTimeSelect", }, - "crypto/tls": []string{ + "crypto/tls": { "Certificate", "CertificateRequestInfo", "CipherSuite", @@ -506,7 +510,7 @@ var stdlib = map[string][]string{ "X25519", "X509KeyPair", }, - "crypto/x509": []string{ + "crypto/x509": { "CANotAuthorizedForExtKeyUsage", "CANotAuthorizedForThisName", "CertPool", @@ -612,7 +616,7 @@ var stdlib = map[string][]string{ "UnknownSignatureAlgorithm", "VerifyOptions", }, - "crypto/x509/pkix": []string{ + "crypto/x509/pkix": { "AlgorithmIdentifier", "AttributeTypeAndValue", "AttributeTypeAndValueSET", @@ -624,7 +628,7 @@ var stdlib = map[string][]string{ "RevokedCertificate", "TBSCertificateList", }, - "database/sql": []string{ + "database/sql": { "ColumnType", "Conn", "DB", @@ -665,7 +669,7 @@ var stdlib = map[string][]string{ "Tx", "TxOptions", }, - "database/sql/driver": []string{ + "database/sql/driver": { "Bool", "ColumnConverter", "Conn", @@ -713,12 +717,12 @@ var stdlib = map[string][]string{ "ValueConverter", "Valuer", }, - "debug/buildinfo": []string{ + "debug/buildinfo": { "BuildInfo", "Read", "ReadFile", }, - "debug/dwarf": []string{ + "debug/dwarf": { "AddrType", "ArrayType", "Attr", @@ -969,7 +973,7 @@ var stdlib = map[string][]string{ "UnsupportedType", "VoidType", }, - "debug/elf": []string{ + "debug/elf": { "ARM_MAGIC_TRAMP_NUMBER", "COMPRESS_HIOS", "COMPRESS_HIPROC", @@ -2368,7 +2372,7 @@ var stdlib = map[string][]string{ "Type", "Version", }, - "debug/gosym": []string{ + "debug/gosym": { "DecodingError", "Func", "LineTable", @@ -2380,7 +2384,7 @@ var stdlib = map[string][]string{ "UnknownFileError", "UnknownLineError", }, - "debug/macho": []string{ + "debug/macho": { "ARM64_RELOC_ADDEND", "ARM64_RELOC_BRANCH26", "ARM64_RELOC_GOT_LOAD_PAGE21", @@ -2510,7 +2514,7 @@ var stdlib = map[string][]string{ "X86_64_RELOC_TLV", "X86_64_RELOC_UNSIGNED", }, - "debug/pe": []string{ + "debug/pe": { "COFFSymbol", "COFFSymbolAuxFormat5", "COFFSymbolSize", @@ -2623,7 +2627,7 @@ var stdlib = map[string][]string{ "StringTable", "Symbol", }, - "debug/plan9obj": []string{ + "debug/plan9obj": { "ErrNoSymbols", "File", "FileHeader", @@ -2637,16 +2641,16 @@ var stdlib = map[string][]string{ "SectionHeader", "Sym", }, - "embed": []string{ + "embed": { "FS", }, - "encoding": []string{ + "encoding": { "BinaryMarshaler", "BinaryUnmarshaler", "TextMarshaler", "TextUnmarshaler", }, - "encoding/ascii85": []string{ + "encoding/ascii85": { "CorruptInputError", "Decode", "Encode", @@ -2654,7 +2658,7 @@ var stdlib = map[string][]string{ "NewDecoder", "NewEncoder", }, - "encoding/asn1": []string{ + "encoding/asn1": { "BitString", "ClassApplication", "ClassContextSpecific", @@ -2692,7 +2696,7 @@ var stdlib = map[string][]string{ "Unmarshal", "UnmarshalWithParams", }, - "encoding/base32": []string{ + "encoding/base32": { "CorruptInputError", "Encoding", "HexEncoding", @@ -2703,7 +2707,7 @@ var stdlib = map[string][]string{ "StdEncoding", "StdPadding", }, - "encoding/base64": []string{ + "encoding/base64": { "CorruptInputError", "Encoding", "NewDecoder", @@ -2716,7 +2720,7 @@ var stdlib = map[string][]string{ "StdPadding", "URLEncoding", }, - "encoding/binary": []string{ + "encoding/binary": { "AppendByteOrder", "AppendUvarint", "AppendVarint", @@ -2736,7 +2740,7 @@ var stdlib = map[string][]string{ "Varint", "Write", }, - "encoding/csv": []string{ + "encoding/csv": { "ErrBareQuote", "ErrFieldCount", "ErrQuote", @@ -2747,7 +2751,7 @@ var stdlib = map[string][]string{ "Reader", "Writer", }, - "encoding/gob": []string{ + "encoding/gob": { "CommonType", "Decoder", "Encoder", @@ -2758,7 +2762,7 @@ var stdlib = map[string][]string{ "Register", "RegisterName", }, - "encoding/hex": []string{ + "encoding/hex": { "Decode", "DecodeString", "DecodedLen", @@ -2772,7 +2776,7 @@ var stdlib = map[string][]string{ "NewDecoder", "NewEncoder", }, - "encoding/json": []string{ + "encoding/json": { "Compact", "Decoder", "Delim", @@ -2799,13 +2803,13 @@ var stdlib = map[string][]string{ "UnsupportedValueError", "Valid", }, - "encoding/pem": []string{ + "encoding/pem": { "Block", "Decode", "Encode", "EncodeToMemory", }, - "encoding/xml": []string{ + "encoding/xml": { "Attr", "CharData", "Comment", @@ -2839,13 +2843,13 @@ var stdlib = map[string][]string{ "UnmarshalerAttr", "UnsupportedTypeError", }, - "errors": []string{ + "errors": { "As", "Is", "New", "Unwrap", }, - "expvar": []string{ + "expvar": { "Do", "Float", "Func", @@ -2862,7 +2866,7 @@ var stdlib = map[string][]string{ "String", "Var", }, - "flag": []string{ + "flag": { "Arg", "Args", "Bool", @@ -2907,7 +2911,7 @@ var stdlib = map[string][]string{ "Visit", "VisitAll", }, - "fmt": []string{ + "fmt": { "Append", "Appendf", "Appendln", @@ -2937,7 +2941,7 @@ var stdlib = map[string][]string{ "State", "Stringer", }, - "go/ast": []string{ + "go/ast": { "ArrayType", "AssignStmt", "Bad", @@ -3040,7 +3044,7 @@ var stdlib = map[string][]string{ "Visitor", "Walk", }, - "go/build": []string{ + "go/build": { "AllowBinary", "ArchChar", "Context", @@ -3057,7 +3061,7 @@ var stdlib = map[string][]string{ "Package", "ToolDir", }, - "go/build/constraint": []string{ + "go/build/constraint": { "AndExpr", "Expr", "IsGoBuild", @@ -3069,7 +3073,7 @@ var stdlib = map[string][]string{ "SyntaxError", "TagExpr", }, - "go/constant": []string{ + "go/constant": { "BinaryOp", "BitLen", "Bool", @@ -3110,7 +3114,7 @@ var stdlib = map[string][]string{ "Val", "Value", }, - "go/doc": []string{ + "go/doc": { "AllDecls", "AllMethods", "Example", @@ -3131,7 +3135,7 @@ var stdlib = map[string][]string{ "Type", "Value", }, - "go/doc/comment": []string{ + "go/doc/comment": { "Block", "Code", "DefaultLookupPackage", @@ -3149,17 +3153,17 @@ var stdlib = map[string][]string{ "Printer", "Text", }, - "go/format": []string{ + "go/format": { "Node", "Source", }, - "go/importer": []string{ + "go/importer": { "Default", "For", "ForCompiler", "Lookup", }, - "go/parser": []string{ + "go/parser": { "AllErrors", "DeclarationErrors", "ImportsOnly", @@ -3174,7 +3178,7 @@ var stdlib = map[string][]string{ "SpuriousErrors", "Trace", }, - "go/printer": []string{ + "go/printer": { "CommentedNode", "Config", "Fprint", @@ -3184,7 +3188,7 @@ var stdlib = map[string][]string{ "TabIndent", "UseSpaces", }, - "go/scanner": []string{ + "go/scanner": { "Error", "ErrorHandler", "ErrorList", @@ -3193,7 +3197,7 @@ var stdlib = map[string][]string{ "ScanComments", "Scanner", }, - "go/token": []string{ + "go/token": { "ADD", "ADD_ASSIGN", "AND", @@ -3291,7 +3295,7 @@ var stdlib = map[string][]string{ "XOR", "XOR_ASSIGN", }, - "go/types": []string{ + "go/types": { "ArgumentError", "Array", "AssertableTo", @@ -3442,17 +3446,17 @@ var stdlib = map[string][]string{ "WriteSignature", "WriteType", }, - "hash": []string{ + "hash": { "Hash", "Hash32", "Hash64", }, - "hash/adler32": []string{ + "hash/adler32": { "Checksum", "New", "Size", }, - "hash/crc32": []string{ + "hash/crc32": { "Castagnoli", "Checksum", "ChecksumIEEE", @@ -3466,7 +3470,7 @@ var stdlib = map[string][]string{ "Table", "Update", }, - "hash/crc64": []string{ + "hash/crc64": { "Checksum", "ECMA", "ISO", @@ -3476,7 +3480,7 @@ var stdlib = map[string][]string{ "Table", "Update", }, - "hash/fnv": []string{ + "hash/fnv": { "New128", "New128a", "New32", @@ -3484,18 +3488,18 @@ var stdlib = map[string][]string{ "New64", "New64a", }, - "hash/maphash": []string{ + "hash/maphash": { "Bytes", "Hash", "MakeSeed", "Seed", "String", }, - "html": []string{ + "html": { "EscapeString", "UnescapeString", }, - "html/template": []string{ + "html/template": { "CSS", "ErrAmbigContext", "ErrBadHTML", @@ -3533,7 +3537,7 @@ var stdlib = map[string][]string{ "URL", "URLQueryEscaper", }, - "image": []string{ + "image": { "Alpha", "Alpha16", "Black", @@ -3586,7 +3590,7 @@ var stdlib = map[string][]string{ "ZP", "ZR", }, - "image/color": []string{ + "image/color": { "Alpha", "Alpha16", "Alpha16Model", @@ -3622,11 +3626,11 @@ var stdlib = map[string][]string{ "YCbCrModel", "YCbCrToRGB", }, - "image/color/palette": []string{ + "image/color/palette": { "Plan9", "WebSafe", }, - "image/draw": []string{ + "image/draw": { "Draw", "DrawMask", "Drawer", @@ -3638,7 +3642,7 @@ var stdlib = map[string][]string{ "RGBA64Image", "Src", }, - "image/gif": []string{ + "image/gif": { "Decode", "DecodeAll", "DecodeConfig", @@ -3650,7 +3654,7 @@ var stdlib = map[string][]string{ "GIF", "Options", }, - "image/jpeg": []string{ + "image/jpeg": { "Decode", "DecodeConfig", "DefaultQuality", @@ -3660,7 +3664,7 @@ var stdlib = map[string][]string{ "Reader", "UnsupportedError", }, - "image/png": []string{ + "image/png": { "BestCompression", "BestSpeed", "CompressionLevel", @@ -3675,11 +3679,11 @@ var stdlib = map[string][]string{ "NoCompression", "UnsupportedError", }, - "index/suffixarray": []string{ + "index/suffixarray": { "Index", "New", }, - "io": []string{ + "io": { "ByteReader", "ByteScanner", "ByteWriter", @@ -3731,7 +3735,7 @@ var stdlib = map[string][]string{ "WriterAt", "WriterTo", }, - "io/fs": []string{ + "io/fs": { "DirEntry", "ErrClosed", "ErrExist", @@ -3775,7 +3779,7 @@ var stdlib = map[string][]string{ "WalkDir", "WalkDirFunc", }, - "io/ioutil": []string{ + "io/ioutil": { "Discard", "NopCloser", "ReadAll", @@ -3785,7 +3789,7 @@ var stdlib = map[string][]string{ "TempFile", "WriteFile", }, - "log": []string{ + "log": { "Default", "Fatal", "Fatalf", @@ -3814,7 +3818,7 @@ var stdlib = map[string][]string{ "SetPrefix", "Writer", }, - "log/syslog": []string{ + "log/syslog": { "Dial", "LOG_ALERT", "LOG_AUTH", @@ -3849,7 +3853,7 @@ var stdlib = map[string][]string{ "Priority", "Writer", }, - "math": []string{ + "math": { "Abs", "Acos", "Acosh", @@ -3948,7 +3952,7 @@ var stdlib = map[string][]string{ "Y1", "Yn", }, - "math/big": []string{ + "math/big": { "Above", "Accuracy", "AwayFromZero", @@ -3975,7 +3979,7 @@ var stdlib = map[string][]string{ "ToZero", "Word", }, - "math/bits": []string{ + "math/bits": { "Add", "Add32", "Add64", @@ -4027,7 +4031,7 @@ var stdlib = map[string][]string{ "TrailingZeros8", "UintSize", }, - "math/cmplx": []string{ + "math/cmplx": { "Abs", "Acos", "Acosh", @@ -4056,7 +4060,7 @@ var stdlib = map[string][]string{ "Tan", "Tanh", }, - "math/rand": []string{ + "math/rand": { "ExpFloat64", "Float32", "Float64", @@ -4081,7 +4085,7 @@ var stdlib = map[string][]string{ "Uint64", "Zipf", }, - "mime": []string{ + "mime": { "AddExtensionType", "BEncoding", "ErrInvalidMediaParameter", @@ -4093,7 +4097,7 @@ var stdlib = map[string][]string{ "WordDecoder", "WordEncoder", }, - "mime/multipart": []string{ + "mime/multipart": { "ErrMessageTooLarge", "File", "FileHeader", @@ -4104,13 +4108,13 @@ var stdlib = map[string][]string{ "Reader", "Writer", }, - "mime/quotedprintable": []string{ + "mime/quotedprintable": { "NewReader", "NewWriter", "Reader", "Writer", }, - "net": []string{ + "net": { "Addr", "AddrError", "Buffers", @@ -4212,7 +4216,7 @@ var stdlib = map[string][]string{ "UnixListener", "UnknownNetworkError", }, - "net/http": []string{ + "net/http": { "AllowQuerySemicolons", "CanonicalHeaderKey", "Client", @@ -4388,25 +4392,25 @@ var stdlib = map[string][]string{ "TrailerPrefix", "Transport", }, - "net/http/cgi": []string{ + "net/http/cgi": { "Handler", "Request", "RequestFromMap", "Serve", }, - "net/http/cookiejar": []string{ + "net/http/cookiejar": { "Jar", "New", "Options", "PublicSuffixList", }, - "net/http/fcgi": []string{ + "net/http/fcgi": { "ErrConnClosed", "ErrRequestAborted", "ProcessEnv", "Serve", }, - "net/http/httptest": []string{ + "net/http/httptest": { "DefaultRemoteAddr", "NewRecorder", "NewRequest", @@ -4416,7 +4420,7 @@ var stdlib = map[string][]string{ "ResponseRecorder", "Server", }, - "net/http/httptrace": []string{ + "net/http/httptrace": { "ClientTrace", "ContextClientTrace", "DNSDoneInfo", @@ -4425,7 +4429,7 @@ var stdlib = map[string][]string{ "WithClientTrace", "WroteRequestInfo", }, - "net/http/httputil": []string{ + "net/http/httputil": { "BufferPool", "ClientConn", "DumpRequest", @@ -4444,7 +4448,7 @@ var stdlib = map[string][]string{ "ReverseProxy", "ServerConn", }, - "net/http/pprof": []string{ + "net/http/pprof": { "Cmdline", "Handler", "Index", @@ -4452,7 +4456,7 @@ var stdlib = map[string][]string{ "Symbol", "Trace", }, - "net/mail": []string{ + "net/mail": { "Address", "AddressParser", "ErrHeaderNotPresent", @@ -4463,7 +4467,7 @@ var stdlib = map[string][]string{ "ParseDate", "ReadMessage", }, - "net/netip": []string{ + "net/netip": { "Addr", "AddrFrom16", "AddrFrom4", @@ -4482,7 +4486,7 @@ var stdlib = map[string][]string{ "Prefix", "PrefixFrom", }, - "net/rpc": []string{ + "net/rpc": { "Accept", "Call", "Client", @@ -4509,14 +4513,14 @@ var stdlib = map[string][]string{ "ServerCodec", "ServerError", }, - "net/rpc/jsonrpc": []string{ + "net/rpc/jsonrpc": { "Dial", "NewClient", "NewClientCodec", "NewServerCodec", "ServeConn", }, - "net/smtp": []string{ + "net/smtp": { "Auth", "CRAMMD5Auth", "Client", @@ -4526,7 +4530,7 @@ var stdlib = map[string][]string{ "SendMail", "ServerInfo", }, - "net/textproto": []string{ + "net/textproto": { "CanonicalMIMEHeaderKey", "Conn", "Dial", @@ -4542,7 +4546,7 @@ var stdlib = map[string][]string{ "TrimString", "Writer", }, - "net/url": []string{ + "net/url": { "Error", "EscapeError", "InvalidHostError", @@ -4560,7 +4564,7 @@ var stdlib = map[string][]string{ "Userinfo", "Values", }, - "os": []string{ + "os": { "Args", "Chdir", "Chmod", @@ -4676,7 +4680,7 @@ var stdlib = map[string][]string{ "UserHomeDir", "WriteFile", }, - "os/exec": []string{ + "os/exec": { "Cmd", "Command", "CommandContext", @@ -4686,7 +4690,7 @@ var stdlib = map[string][]string{ "ExitError", "LookPath", }, - "os/signal": []string{ + "os/signal": { "Ignore", "Ignored", "Notify", @@ -4694,7 +4698,7 @@ var stdlib = map[string][]string{ "Reset", "Stop", }, - "os/user": []string{ + "os/user": { "Current", "Group", "Lookup", @@ -4707,7 +4711,7 @@ var stdlib = map[string][]string{ "UnknownUserIdError", "User", }, - "path": []string{ + "path": { "Base", "Clean", "Dir", @@ -4718,7 +4722,7 @@ var stdlib = map[string][]string{ "Match", "Split", }, - "path/filepath": []string{ + "path/filepath": { "Abs", "Base", "Clean", @@ -4744,12 +4748,12 @@ var stdlib = map[string][]string{ "WalkDir", "WalkFunc", }, - "plugin": []string{ + "plugin": { "Open", "Plugin", "Symbol", }, - "reflect": []string{ + "reflect": { "Append", "AppendSlice", "Array", @@ -4824,7 +4828,7 @@ var stdlib = map[string][]string{ "VisibleFields", "Zero", }, - "regexp": []string{ + "regexp": { "Compile", "CompilePOSIX", "Match", @@ -4835,7 +4839,7 @@ var stdlib = map[string][]string{ "QuoteMeta", "Regexp", }, - "regexp/syntax": []string{ + "regexp/syntax": { "ClassNL", "Compile", "DotNL", @@ -4914,7 +4918,7 @@ var stdlib = map[string][]string{ "UnicodeGroups", "WasDollar", }, - "runtime": []string{ + "runtime": { "BlockProfile", "BlockProfileRecord", "Breakpoint", @@ -4962,11 +4966,11 @@ var stdlib = map[string][]string{ "UnlockOSThread", "Version", }, - "runtime/cgo": []string{ + "runtime/cgo": { "Handle", "NewHandle", }, - "runtime/debug": []string{ + "runtime/debug": { "BuildInfo", "BuildSetting", "FreeOSMemory", @@ -4985,7 +4989,7 @@ var stdlib = map[string][]string{ "Stack", "WriteHeapDump", }, - "runtime/metrics": []string{ + "runtime/metrics": { "All", "Description", "Float64Histogram", @@ -4998,7 +5002,7 @@ var stdlib = map[string][]string{ "Value", "ValueKind", }, - "runtime/pprof": []string{ + "runtime/pprof": { "Do", "ForLabels", "Label", @@ -5014,7 +5018,7 @@ var stdlib = map[string][]string{ "WithLabels", "WriteHeapProfile", }, - "runtime/trace": []string{ + "runtime/trace": { "IsEnabled", "Log", "Logf", @@ -5026,7 +5030,7 @@ var stdlib = map[string][]string{ "Task", "WithRegion", }, - "sort": []string{ + "sort": { "Find", "Float64Slice", "Float64s", @@ -5050,7 +5054,7 @@ var stdlib = map[string][]string{ "Strings", "StringsAreSorted", }, - "strconv": []string{ + "strconv": { "AppendBool", "AppendFloat", "AppendInt", @@ -5090,7 +5094,7 @@ var stdlib = map[string][]string{ "Unquote", "UnquoteChar", }, - "strings": []string{ + "strings": { "Builder", "Clone", "Compare", @@ -5144,7 +5148,7 @@ var stdlib = map[string][]string{ "TrimSpace", "TrimSuffix", }, - "sync": []string{ + "sync": { "Cond", "Locker", "Map", @@ -5155,7 +5159,7 @@ var stdlib = map[string][]string{ "RWMutex", "WaitGroup", }, - "sync/atomic": []string{ + "sync/atomic": { "AddInt32", "AddInt64", "AddUint32", @@ -5194,7 +5198,7 @@ var stdlib = map[string][]string{ "Uintptr", "Value", }, - "syscall": []string{ + "syscall": { "AF_ALG", "AF_APPLETALK", "AF_ARP", @@ -10344,7 +10348,7 @@ var stdlib = map[string][]string{ "XP1_UNI_RECV", "XP1_UNI_SEND", }, - "syscall/js": []string{ + "syscall/js": { "CopyBytesToGo", "CopyBytesToJS", "Error", @@ -10366,7 +10370,7 @@ var stdlib = map[string][]string{ "ValueError", "ValueOf", }, - "testing": []string{ + "testing": { "AllocsPerRun", "B", "Benchmark", @@ -10394,12 +10398,12 @@ var stdlib = map[string][]string{ "TB", "Verbose", }, - "testing/fstest": []string{ + "testing/fstest": { "MapFS", "MapFile", "TestFS", }, - "testing/iotest": []string{ + "testing/iotest": { "DataErrReader", "ErrReader", "ErrTimeout", @@ -10411,7 +10415,7 @@ var stdlib = map[string][]string{ "TimeoutReader", "TruncateWriter", }, - "testing/quick": []string{ + "testing/quick": { "Check", "CheckEqual", "CheckEqualError", @@ -10421,7 +10425,7 @@ var stdlib = map[string][]string{ "SetupError", "Value", }, - "text/scanner": []string{ + "text/scanner": { "Char", "Comment", "EOF", @@ -10444,7 +10448,7 @@ var stdlib = map[string][]string{ "String", "TokenString", }, - "text/tabwriter": []string{ + "text/tabwriter": { "AlignRight", "Debug", "DiscardEmptyColumns", @@ -10455,7 +10459,7 @@ var stdlib = map[string][]string{ "TabIndent", "Writer", }, - "text/template": []string{ + "text/template": { "ExecError", "FuncMap", "HTMLEscape", @@ -10473,7 +10477,7 @@ var stdlib = map[string][]string{ "Template", "URLQueryEscaper", }, - "text/template/parse": []string{ + "text/template/parse": { "ActionNode", "BoolNode", "BranchNode", @@ -10529,7 +10533,7 @@ var stdlib = map[string][]string{ "VariableNode", "WithNode", }, - "time": []string{ + "time": { "ANSIC", "After", "AfterFunc", @@ -10601,7 +10605,7 @@ var stdlib = map[string][]string{ "Wednesday", "Weekday", }, - "unicode": []string{ + "unicode": { "ASCII_Hex_Digit", "Adlam", "Ahom", @@ -10887,14 +10891,14 @@ var stdlib = map[string][]string{ "Zp", "Zs", }, - "unicode/utf16": []string{ + "unicode/utf16": { "Decode", "DecodeRune", "Encode", "EncodeRune", "IsSurrogate", }, - "unicode/utf8": []string{ + "unicode/utf8": { "AppendRune", "DecodeLastRune", "DecodeLastRuneInString", @@ -10915,7 +10919,7 @@ var stdlib = map[string][]string{ "ValidRune", "ValidString", }, - "unsafe": []string{ + "unsafe": { "Alignof", "ArbitraryType", "Offsetof", From 502b93c33e3e519598e1a93f25ea8e4304def4de Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 17 Oct 2022 11:36:38 -0400 Subject: [PATCH 351/723] gopls/internal/lsp: tolerate missing end position in RelatedInformation Fix the panic reported in #56270, allowing RelatedInformation.End to be missing. Fixes golang/go#56270 Change-Id: I5f2dc27cf149c324f39ddbb056862434fe38f730 Reviewed-on: https://go-review.googlesource.com/c/tools/+/443337 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley --- go/analysis/diagnostic.go | 2 +- gopls/internal/lsp/cache/errors.go | 6 ++- gopls/internal/lsp/regtest/env.go | 4 +- gopls/internal/lsp/source/options.go | 4 +- .../internal/regtest/misc/staticcheck_test.go | 37 +++++++++++++++++++ gopls/internal/span/token.go | 6 ++- 6 files changed, 51 insertions(+), 8 deletions(-) diff --git a/go/analysis/diagnostic.go b/go/analysis/diagnostic.go index cd462a0cb55..5cdcf46d2a1 100644 --- a/go/analysis/diagnostic.go +++ b/go/analysis/diagnostic.go @@ -37,7 +37,7 @@ type Diagnostic struct { // declaration. type RelatedInformation struct { Pos token.Pos - End token.Pos + End token.Pos // optional Message string } diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 1c38e205e99..bca68d50491 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -334,7 +334,11 @@ func relatedInformation(pkg *pkg, fset *token.FileSet, diag *analysis.Diagnostic if tokFile == nil { return nil, bug.Errorf("no file for %q diagnostic position", diag.Category) } - spn, err := span.NewRange(tokFile, related.Pos, related.End).Span() + end := related.End + if !end.IsValid() { + end = related.Pos + } + spn, err := span.NewRange(tokFile, related.Pos, end).Span() if err != nil { return nil, err } diff --git a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go index 23be64e9293..2374dab19ef 100644 --- a/gopls/internal/lsp/regtest/env.go +++ b/gopls/internal/lsp/regtest/env.go @@ -11,9 +11,9 @@ import ( "sync" "testing" - "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/jsonrpc2/servertest" ) // Env holds the building blocks of an editor testing environment, providing @@ -119,7 +119,7 @@ func (s State) String() string { for name, params := range s.diagnostics { fmt.Fprintf(&b, "\t%s (version %d):\n", name, int(params.Version)) for _, d := range params.Diagnostics { - fmt.Fprintf(&b, "\t\t(%d, %d): %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Message) + fmt.Fprintf(&b, "\t\t(%d, %d) [%s]: %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Source, d.Message) } } b.WriteString("\n") diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index fe86ff280ed..136ba23d86c 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -1306,8 +1306,8 @@ func (r *OptionResult) asBoolMap() map[string]bool { } m := make(map[string]bool) for a, enabled := range all { - if enabled, ok := enabled.(bool); ok { - m[a] = enabled + if e, ok := enabled.(bool); ok { + m[a] = e } else { r.parseErrorf("invalid type %T for map key %q", enabled, a) return m diff --git a/gopls/internal/regtest/misc/staticcheck_test.go b/gopls/internal/regtest/misc/staticcheck_test.go index 86766900b71..f2ba3ccd8a7 100644 --- a/gopls/internal/regtest/misc/staticcheck_test.go +++ b/gopls/internal/regtest/misc/staticcheck_test.go @@ -74,3 +74,40 @@ var FooErr error = errors.New("foo") ) }) } + +// Test for golang/go#56270: an analysis with related info should not panic if +// analysis.RelatedInformation.End is not set. +func TestStaticcheckRelatedInfo(t *testing.T) { + testenv.NeedsGo1Point(t, 17) // staticcheck is only supported at Go 1.17+ + const files = ` +-- go.mod -- +module mod.test + +go 1.18 +-- p.go -- +package p + +import ( + "fmt" +) + +func Foo(enabled interface{}) { + if enabled, ok := enabled.(bool); ok { + } else { + _ = fmt.Sprintf("invalid type %T", enabled) // enabled is always bool here + } +} +` + + WithOptions( + Settings{"staticcheck": true}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("p.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexpFromSource("p.go", ", (enabled)", "SA9008"), + ), + ) + }) +} diff --git a/gopls/internal/span/token.go b/gopls/internal/span/token.go index da4dc22febb..6bde300240f 100644 --- a/gopls/internal/span/token.go +++ b/gopls/internal/span/token.go @@ -19,7 +19,9 @@ type Range struct { Start, End token.Pos // both IsValid() } -// NewRange creates a new Range from a token.File and two valid positions within it. +// NewRange creates a new Range from a token.File and two positions within it. +// The given start position must be valid; if end is invalid, start is used as +// the end position. // // (If you only have a token.FileSet, use file = fset.File(start). But // most callers know exactly which token.File they're dealing with and @@ -33,7 +35,7 @@ func NewRange(file *token.File, start, end token.Pos) Range { panic("invalid start token.Pos") } if !end.IsValid() { - panic("invalid end token.Pos") + end = start } // TODO(adonovan): ideally we would make this stronger assertion: From b50d7ba6eeb13c2ded0ec2d8535be9170c065059 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 13 Oct 2022 11:06:38 -0400 Subject: [PATCH 352/723] gopls: minor cleanup of standalone package support Remove an unused err value, and update tests to exercise multiple standaloneTags. Change-Id: I88daace99111cba6f8bc74fad8aa0db844cb2654 Reviewed-on: https://go-review.googlesource.com/c/tools/+/442776 Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Auto-Submit: Robert Findley --- gopls/internal/lsp/cache/standalone_go116_test.go | 6 ++++++ gopls/internal/lsp/diagnostics.go | 2 +- gopls/internal/regtest/workspace/standalone_test.go | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/gopls/internal/lsp/cache/standalone_go116_test.go b/gopls/internal/lsp/cache/standalone_go116_test.go index 5eb7ff00f1d..9adf01e6cea 100644 --- a/gopls/internal/lsp/cache/standalone_go116_test.go +++ b/gopls/internal/lsp/cache/standalone_go116_test.go @@ -30,6 +30,12 @@ func TestIsStandaloneFile(t *testing.T) { []string{"ignore"}, true, }, + { + "multiple tags", + "//go:build ignore\n\npackage main\n", + []string{"exclude", "ignore"}, + true, + }, { "invalid tag", "// +build ignore\n\npackage main\n", diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 558556ec501..ff3e4b2ec24 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -534,7 +534,7 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps if snapshot.IsBuiltin(ctx, fh.URI()) { return nil } - pkgs, err := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace, false) + pkgs, _ := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace, false) if len(pkgs) > 0 { return nil } diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go index 1e51b31fb78..86e3441e52d 100644 --- a/gopls/internal/regtest/workspace/standalone_test.go +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -214,7 +214,7 @@ func main() {} WithOptions( Settings{ - "standaloneTags": []string{"standalone"}, + "standaloneTags": []string{"standalone", "script"}, }, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("ignore.go") From 9eda97bc2d7c2fa6ed3f23c9511a6cdeb174b4c8 Mon Sep 17 00:00:00 2001 From: Michael Matloob Date: Mon, 10 Oct 2022 18:10:03 -0400 Subject: [PATCH 353/723] go/analysis: enable a test that applies after go list behavior change The despite-error case in TestRunDespiteErrors only returns error code 1 after golang.org/cl/437298. This CL enables the test for Go 1.20+. Change-Id: Ib212026d595e7bf872aa2482e1d91723010e7018 Reviewed-on: https://go-review.googlesource.com/c/tools/+/441880 Run-TryBot: Michael Matloob Reviewed-by: David Chase TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- go/analysis/internal/checker/checker_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go index d91425ee4ab..f07963fa008 100644 --- a/go/analysis/internal/checker/checker_test.go +++ b/go/analysis/internal/checker/checker_test.go @@ -172,7 +172,8 @@ func Foo(s string) int { // no errors {name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{analyzer, noop}, code: 0}, } { - if test.name == "despite-error" { // TODO(matloob): once CL 437298 is submitted, add the condition testenv.Go1Point() < 20 + if test.name == "despite-error" && testenv.Go1Point() < 20 { + // The behavior in the comment on the despite-error test only occurs for Go 1.20+. continue } if got := checker.Run(test.pattern, test.analyzers); got != test.code { From 91311ab3b8fbb39d8d1df146082b6313ec6e2d55 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 18 Oct 2022 10:28:21 -0400 Subject: [PATCH 354/723] gopls/internal/lsp/cache: better import path hygiene An import path is not the same as a package path. For example, the declaration: import "example.com/foo" may load a package whose PkgPath() is "vendor/example.com/foo". There was a comment hinting at this in load.go. This change introduces the concept of ImportPath as a distinct type from PackagePath, and documents clearly throughout the relevant APIs which one is expected. Notes: - Metadata.PkgPath is consistently set from the PackagePath, not sometimes (when a root) from PackagePath and sometimes (when indirectly loaded) from an arbitrary ImportPath that resolves to it. I expect this means some packages will now (correctly) appear to be called "vendor/example.com/foo" in the user interface where previously the vendor prefix was omitted. - Metadata.Deps is gone. - Metadata.Imports is a new map from ImportPath to ID. - Metadata.MissingDeps is now a set of ImportPath. - the package loading key is now based on a hash of unique imports. (Duplicates would cancel out.) - The ImporterFunc no longer needs the guesswork of resolveImportPath, since 'go list' already told us the correct answer. - Package.GetImport is renamed DirectDep(packagePath) to avoid the suggestion that it accepts an ImportPath. - Package.ResolveImportPath is the analogous method for import paths. Most clients seem to want this. Change-Id: I4999709120fff4663ba8669358fe149f1626bb8e Reviewed-on: https://go-review.googlesource.com/c/tools/+/443636 Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/analysis.go | 4 +- gopls/internal/lsp/cache/check.go | 98 +++++++------------ gopls/internal/lsp/cache/graph.go | 23 ++++- gopls/internal/lsp/cache/load.go | 73 ++++++++++---- gopls/internal/lsp/cache/metadata.go | 11 ++- gopls/internal/lsp/cache/mod_tidy.go | 5 +- gopls/internal/lsp/cache/pkg.go | 44 +++++++-- gopls/internal/lsp/cache/snapshot.go | 10 +- gopls/internal/lsp/link.go | 4 +- gopls/internal/lsp/semantic.go | 25 ++--- .../lsp/source/completion/completion.go | 3 - .../lsp/source/completion/statements.go | 2 +- gopls/internal/lsp/source/hover.go | 16 +-- gopls/internal/lsp/source/identifier.go | 12 +-- gopls/internal/lsp/source/known_packages.go | 2 + gopls/internal/lsp/source/rename_check.go | 4 +- gopls/internal/lsp/source/stub.go | 4 +- gopls/internal/lsp/source/view.go | 23 +++-- 18 files changed, 213 insertions(+), 150 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 6da77b59045..fa2f7eaf08f 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -132,7 +132,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A } // Add a dependency on each required analyzer. - var deps []*actionHandle + var deps []*actionHandle // unordered for _, req := range a.Requires { // TODO(adonovan): opt: there's no need to repeat the package-handle // portion of the recursion here, since we have the pkg already. @@ -150,7 +150,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A // An analysis that consumes/produces facts // must run on the package's dependencies too. if len(a.FactTypes) > 0 { - for _, importID := range ph.m.Deps { + for _, importID := range ph.m.Imports { depActionHandle, err := s.actionHandle(ctx, importID, a) if err != nil { return nil, err diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index a186a816853..235eacd2d91 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -11,7 +11,6 @@ import ( "fmt" "go/ast" "go/types" - "path" "path/filepath" "regexp" "strings" @@ -99,9 +98,13 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // TODO(adonovan): use a promise cache to ensure that the key // for each package is computed by at most one thread, then do // the recursive key building of dependencies in parallel. - deps := make(map[PackagePath]*packageHandle) - depKeys := make([]packageHandleKey, len(m.Deps)) - for i, depID := range m.Deps { + deps := make(map[PackageID]*packageHandle) + var depKey source.Hash // XOR of all unique deps + for _, depID := range m.Imports { + depHandle, ok := deps[depID] + if ok { + continue // e.g. duplicate import + } depHandle, err := s.buildPackageHandle(ctx, depID, s.workspaceParseMode(depID)) // Don't use invalid metadata for dependencies if the top-level // metadata is valid. We only load top-level packages, so if the @@ -125,8 +128,9 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so continue } - deps[depHandle.m.PkgPath] = depHandle - depKeys[i] = depHandle.key + depKey.XORWith(source.Hash(depHandle.key)) + + deps[depID] = depHandle } // Read both lists of files of this package, in parallel. @@ -144,7 +148,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // All the file reading has now been done. // Create a handle for the result of type checking. experimentalKey := s.View().Options().ExperimentalPackageCacheKey - phKey := computePackageKey(m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) + phKey := computePackageKey(m.ID, compiledGoFiles, m, depKey, mode, experimentalKey) promise, release := s.store.Promise(phKey, func(ctx context.Context, arg interface{}) interface{} { pkg, err := typeCheckImpl(ctx, arg.(*snapshot), goFiles, compiledGoFiles, m.Metadata, mode, deps) @@ -225,8 +229,8 @@ func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { // computePackageKey returns a key representing the act of type checking // a package named id containing the specified files, metadata, and -// dependency hashes. -func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata, deps []packageHandleKey, mode source.ParseMode, experimentalKey bool) packageHandleKey { +// combined dependency hash. +func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata, depsKey source.Hash, mode source.ParseMode, experimentalKey bool) packageHandleKey { // TODO(adonovan): opt: no need to materalize the bytes; hash them directly. // Also, use field separators to avoid spurious collisions. b := bytes.NewBuffer(nil) @@ -243,9 +247,7 @@ func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata b.Write(hc[:]) } b.WriteByte(byte(mode)) - for _, dep := range deps { - b.Write(dep[:]) - } + b.Write(depsKey[:]) for _, file := range files { b.WriteString(file.FileIdentity().String()) } @@ -311,7 +313,7 @@ func (ph *packageHandle) cached() (*pkg, error) { // typeCheckImpl type checks the parsed source files in compiledGoFiles. // (The resulting pkg also holds the parsed but not type-checked goFiles.) // deps holds the future results of type-checking the direct dependencies. -func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle) (*pkg, error) { +func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle) (*pkg, error) { // Start type checking of direct dependencies, // in parallel and asynchronously. // As the type checker imports each of these @@ -446,15 +448,15 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) -func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { +func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { ctx, done := event.Start(ctx, "cache.typeCheck", tag.Package.Of(string(m.ID))) defer done() pkg := &pkg{ - m: m, - mode: mode, - imports: make(map[PackagePath]*pkg), - types: types.NewPackage(string(m.PkgPath), string(m.Name)), + m: m, + mode: mode, + depsByPkgPath: make(map[PackagePath]*pkg), + types: types.NewPackage(string(m.PkgPath), string(m.Name)), typesInfo: &types.Info{ Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object), @@ -522,23 +524,30 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil Error: func(e error) { pkg.typeErrors = append(pkg.typeErrors, e.(types.Error)) }, - Importer: importerFunc(func(pkgPath string) (*types.Package, error) { - // If the context was cancelled, we should abort. - if ctx.Err() != nil { - return nil, ctx.Err() + Importer: importerFunc(func(path string) (*types.Package, error) { + // While all of the import errors could be reported + // based on the metadata before we start type checking, + // reporting them via types.Importer places the errors + // at the correct source location. + id, ok := pkg.m.Imports[ImportPath(path)] + if !ok { + // If the import declaration is broken, + // go list may fail to report metadata about it. + // See TestFixImportDecl for an example. + return nil, fmt.Errorf("missing metadata for import of %q", path) } - dep := resolveImportPath(pkgPath, pkg, deps) - if dep == nil { - return nil, snapshot.missingPkgError(ctx, pkgPath) + dep, ok := deps[id] + if !ok { + return nil, snapshot.missingPkgError(ctx, path) } if !source.IsValidImport(string(m.PkgPath), string(dep.m.PkgPath)) { - return nil, fmt.Errorf("invalid use of internal package %s", pkgPath) + return nil, fmt.Errorf("invalid use of internal package %s", path) } depPkg, err := dep.await(ctx, snapshot) if err != nil { return nil, err } - pkg.imports[depPkg.m.PkgPath] = depPkg + pkg.depsByPkgPath[depPkg.m.PkgPath] = depPkg return depPkg.types, nil }), } @@ -840,41 +849,6 @@ func expandErrors(errs []types.Error, supportsRelatedInformation bool) []extende return result } -// resolveImportPath resolves an import path in pkg to a package from deps. -// It should produce the same results as resolveImportPath: -// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/load/pkg.go;drc=641918ee09cb44d282a30ee8b66f99a0b63eaef9;l=990. -func resolveImportPath(importPath string, pkg *pkg, deps map[PackagePath]*packageHandle) *packageHandle { - if dep := deps[PackagePath(importPath)]; dep != nil { - return dep - } - // We may be in GOPATH mode, in which case we need to check vendor dirs. - searchDir := path.Dir(pkg.PkgPath()) - for { - vdir := PackagePath(path.Join(searchDir, "vendor", importPath)) - if vdep := deps[vdir]; vdep != nil { - return vdep - } - - // Search until Dir doesn't take us anywhere new, e.g. "." or "/". - next := path.Dir(searchDir) - if searchDir == next { - break - } - searchDir = next - } - - // Vendor didn't work. Let's try minimal module compatibility mode. - // In MMC, the packagePath is the canonical (.../vN/...) path, which - // is hard to calculate. But the go command has already resolved the ID - // to the non-versioned path, and we can take advantage of that. - for _, dep := range deps { - if dep.ID() == importPath { - return dep - } - } - return nil -} - // An importFunc is an implementation of the single-method // types.Importer interface based on a function value. type importerFunc func(path string) (*types.Package, error) diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index 3d530c45ab1..047a55c7920 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -53,7 +53,7 @@ func (g *metadataGraph) build() { // Build the import graph. g.importedBy = make(map[PackageID][]PackageID) for id, m := range g.metadata { - for _, importID := range m.Deps { + for _, importID := range uniqueDeps(m.Imports) { g.importedBy[importID] = append(g.importedBy[importID], id) } } @@ -129,8 +129,27 @@ func (g *metadataGraph) build() { } } +// uniqueDeps returns a new sorted and duplicate-free slice containing the +// IDs of the package's direct dependencies. +func uniqueDeps(imports map[ImportPath]PackageID) []PackageID { + // TODO(adonovan): use generic maps.SortedUniqueValues(m.Imports) when available. + ids := make([]PackageID, 0, len(imports)) + for _, id := range imports { + ids = append(ids, id) + } + sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + // de-duplicate in place + out := ids[:0] + for _, id := range ids { + if len(out) == 0 || id != out[len(out)-1] { + out = append(out, id) + } + } + return out +} + // reverseTransitiveClosure calculates the set of packages that transitively -// reach an id in ids via their Deps. The result also includes given ids. +// import an id in ids. The result also includes given ids. // // If includeInvalid is false, the algorithm ignores packages with invalid // metadata (including those in the given list of ids). diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index e930eb4f4d5..83ea2cab41b 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -207,7 +207,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc if s.view.allFilesExcluded(pkg, filterer) { continue } - if err := buildMetadata(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, newMetadata, nil); err != nil { + if err := buildMetadata(ctx, pkg, cfg, query, newMetadata, nil); err != nil { return err } } @@ -476,7 +476,9 @@ func makeWorkspaceDir(ctx context.Context, workspace *workspace, fs source.FileS // buildMetadata populates the updates map with metadata updates to // apply, based on the given pkg. It recurs through pkg.Imports to ensure that // metadata exists for all dependencies. -func buildMetadata(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { +func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { + // Allow for multiple ad-hoc packages in the workspace (see #47584). + pkgPath := PackagePath(pkg.PkgPath) id := PackageID(pkg.ID) if source.IsCommandLineArguments(pkg.ID) { suffix := ":" + strings.Join(query, ",") @@ -540,33 +542,66 @@ func buildMetadata(ctx context.Context, pkgPath PackagePath, pkg *packages.Packa m.GoFiles = append(m.GoFiles, uri) } - for importPath, importPkg := range pkg.Imports { - // TODO(rfindley): in rare cases it is possible that the import package - // path is not the same as the package path of the import. That is to say - // (quoting adonovan): - // "The importPath string is the path by which one package is imported from - // another, but that needn't be the same as its internal name (sometimes - // called the "package path") used to prefix its linker symbols" - // - // We should not set this package path on the metadata of the dep. - importPkgPath := PackagePath(importPath) - importID := PackageID(importPkg.ID) + imports := make(map[ImportPath]PackageID) + for importPath, imported := range pkg.Imports { + importPath := ImportPath(importPath) + imports[importPath] = PackageID(imported.ID) - m.Deps = append(m.Deps, importID) + // It is not an invariant that importPath == imported.PkgPath. + // For example, package "net" imports "golang.org/x/net/dns/dnsmessage" + // which refers to the package whose ID and PkgPath are both + // "vendor/golang.org/x/net/dns/dnsmessage". Notice the ImportMap, + // which maps ImportPaths to PackagePaths: + // + // $ go list -json net vendor/golang.org/x/net/dns/dnsmessage + // { + // "ImportPath": "net", + // "Name": "net", + // "Imports": [ + // "C", + // "vendor/golang.org/x/net/dns/dnsmessage", + // "vendor/golang.org/x/net/route", + // ... + // ], + // "ImportMap": { + // "golang.org/x/net/dns/dnsmessage": "vendor/golang.org/x/net/dns/dnsmessage", + // "golang.org/x/net/route": "vendor/golang.org/x/net/route" + // }, + // ... + // } + // { + // "ImportPath": "vendor/golang.org/x/net/dns/dnsmessage", + // "Name": "dnsmessage", + // ... + // } + // + // (Beware that, for historical reasons, go list uses + // the JSON field "ImportPath" for the package's + // path--effectively the linker symbol prefix.) // Don't remember any imports with significant errors. - if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 { + // + // The len=0 condition is a heuristic check for imports of + // non-existent packages (for which go/packages will create + // an edge to a synthesized node). The heuristic is unsound + // because some valid packages have zero files, for example, + // a directory containing only the file p_test.go defines an + // empty package p. + // TODO(adonovan): clarify this. Perhaps go/packages should + // report which nodes were synthesized. + if importPath != "unsafe" && len(imported.CompiledGoFiles) == 0 { if m.MissingDeps == nil { - m.MissingDeps = make(map[PackagePath]struct{}) + m.MissingDeps = make(map[ImportPath]struct{}) } - m.MissingDeps[importPkgPath] = struct{}{} + m.MissingDeps[importPath] = struct{}{} continue } - if err := buildMetadata(ctx, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil { + + if err := buildMetadata(ctx, imported, cfg, query, updates, append(path, id)); err != nil { event.Error(ctx, "error in dependency", err) } } - sort.Slice(m.Deps, func(i, j int) bool { return m.Deps[i] < m.Deps[j] }) // for determinism + m.Imports = imports return nil } diff --git a/gopls/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go index 2fa87eb1fc2..5ac741a338f 100644 --- a/gopls/internal/lsp/cache/metadata.go +++ b/gopls/internal/lsp/cache/metadata.go @@ -17,9 +17,10 @@ import ( // it would result in confusing errors because package IDs often look like // package paths. type ( - PackageID string - PackagePath string - PackageName string + PackageID string // go list's unique identifier for a package (e.g. "vendor/example.com/foo [vendor/example.com/bar.test]") + PackagePath string // name used to prefix linker symbols (e.g. "vendor/example.com/foo") + PackageName string // identifier in 'package' declaration (e.g. "foo") + ImportPath string // path that appears in an import declaration (e.g. "example.com/foo") ) // Metadata holds package Metadata extracted from a call to packages.Load. @@ -32,8 +33,8 @@ type Metadata struct { ForTest PackagePath // package path under test, or "" TypesSizes types.Sizes Errors []packages.Error - Deps []PackageID // direct dependencies, in string order - MissingDeps map[PackagePath]struct{} + Imports map[ImportPath]PackageID // may contain duplicate IDs + MissingDeps map[ImportPath]struct{} Module *packages.Module depsErrors []*packagesinternal.PackageError diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index 8e4e060904a..cc604c1c86f 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -466,9 +466,8 @@ func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Sp // the set of strings that appear in import declarations within // GoFiles. Errors are ignored. // -// (We can't simply use ph.m.Metadata.Deps because it contains -// PackageIDs--not import paths--and is based on CompiledGoFiles, -// after cgo processing.) +// (We can't simply use Metadata.Imports because it is based on +// CompiledGoFiles, after cgo processing.) func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) map[string]bool { s.mu.Lock() // peekOrParse requires a locked snapshot (!) defer s.mu.Unlock() diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 0c9914d66fd..2f7389db18e 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -23,7 +23,7 @@ type pkg struct { goFiles []*source.ParsedGoFile compiledGoFiles []*source.ParsedGoFile diagnostics []*source.Diagnostic - imports map[PackagePath]*pkg + depsByPkgPath map[PackagePath]*pkg version *module.Version parseErrors []scanner.ErrorList typeErrors []types.Error @@ -111,27 +111,57 @@ func (p *pkg) ForTest() string { return string(p.m.ForTest) } -func (p *pkg) GetImport(pkgPath string) (source.Package, error) { - if imp := p.imports[PackagePath(pkgPath)]; imp != nil { +// DirectDep returns the directly imported dependency of this package, +// given its PackagePath. (If you have an ImportPath, e.g. a string +// from an import declaration, use ResolveImportPath instead. +// They may differ in case of vendoring.) +func (p *pkg) DirectDep(pkgPath string) (source.Package, error) { + if imp := p.depsByPkgPath[PackagePath(pkgPath)]; imp != nil { return imp, nil } // Don't return a nil pointer because that still satisfies the interface. return nil, fmt.Errorf("no imported package for %s", pkgPath) } +// ResolveImportPath returns the directly imported dependency of this package, +// given its ImportPath. See also DirectDep. +func (p *pkg) ResolveImportPath(importPath string) (source.Package, error) { + if id, ok := p.m.Imports[ImportPath(importPath)]; ok { + for _, imported := range p.depsByPkgPath { + if PackageID(imported.ID()) == id { + return imported, nil + } + } + } + return nil, fmt.Errorf("package does not import %s", importPath) +} + func (p *pkg) MissingDependencies() []string { // We don't invalidate metadata for import deletions, so check the package // imports via the *types.Package. Only use metadata if p.types is nil. if p.types == nil { var md []string - for i := range p.m.MissingDeps { - md = append(md, string(i)) + for importPath := range p.m.MissingDeps { + md = append(md, string(importPath)) } return md } + + // This looks wrong. + // + // rfindley says: it looks like this is intending to implement + // a heuristic "if go list couldn't resolve import paths to + // packages, then probably you're not in GOPATH or a module". + // This is used to determine if we need to show a warning diagnostic. + // It looks like this logic is implementing the heuristic that + // "even if the metadata has a MissingDep, if the types.Package + // doesn't need that dep anymore we shouldn't show the warning". + // But either we're outside of GOPATH/Module, or we're not... + // + // TODO(adonovan): figure out what it is trying to do. var md []string for _, pkg := range p.types.Imports() { - if _, ok := p.m.MissingDeps[PackagePath(pkg.Path())]; ok { + if _, ok := p.m.MissingDeps[ImportPath(pkg.Path())]; ok { md = append(md, pkg.Path()) } } @@ -140,7 +170,7 @@ func (p *pkg) MissingDependencies() []string { func (p *pkg) Imports() []source.Package { var result []source.Package - for _, imp := range p.imports { + for _, imp := range p.depsByPkgPath { result = append(result, imp) } return result diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index f070fe3c1cc..eda1ff7e137 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -892,7 +892,7 @@ func (s *snapshot) isActiveLocked(id PackageID) (active bool) { } // TODO(rfindley): it looks incorrect that we don't also check GoFiles here. // If a CGo file is open, we want to consider the package active. - for _, dep := range m.Deps { + for _, dep := range m.Imports { if s.isActiveLocked(dep) { return true } @@ -1209,14 +1209,14 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac if err != nil { return } - for importPath, newPkg := range cachedPkg.imports { - if oldPkg, ok := results[string(importPath)]; ok { + for pkgPath, newPkg := range cachedPkg.depsByPkgPath { + if oldPkg, ok := results[string(pkgPath)]; ok { // Using the same trick as NarrowestPackage, prefer non-variants. if len(newPkg.compiledGoFiles) < len(oldPkg.(*pkg).compiledGoFiles) { - results[string(importPath)] = newPkg + results[string(pkgPath)] = newPkg } } else { - results[string(importPath)] = newPkg + results[string(pkgPath)] = newPkg } } }) diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go index 929fe289f28..b26bfb33c80 100644 --- a/gopls/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -174,8 +174,8 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle return links, nil } -func moduleAtVersion(target string, pkg source.Package) (string, string, bool) { - impPkg, err := pkg.GetImport(target) +func moduleAtVersion(targetImportPath string, pkg source.Package) (string, string, bool) { + impPkg, err := pkg.ResolveImportPath(targetImportPath) if err != nil { return "", "", false } diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 641c2c477de..713c3ac4c0f 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -15,14 +15,15 @@ import ( "log" "path/filepath" "sort" + "strconv" "strings" "time" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/template" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" ) @@ -875,31 +876,25 @@ func (e *encoded) importSpec(d *ast.ImportSpec) { } return // don't mark anything for . or _ } - val := d.Path.Value - if len(val) < 2 || val[0] != '"' || val[len(val)-1] != '"' { - // avoid panics on imports without a properly quoted string + importPath, err := strconv.Unquote(d.Path.Value) + if err != nil { return } - nm := val[1 : len(val)-1] // remove surrounding "s // Import strings are implementation defined. Try to match with parse information. - x, err := e.pkg.GetImport(nm) + imported, err := e.pkg.ResolveImportPath(importPath) if err != nil { // unexpected, but impact is that maybe some import is not colored return } - // expect that nm is x.PkgPath and that x.Name() is a component of it - if x.PkgPath() != nm { - // don't know how or what to color (if this can happen at all) - return - } - // this is not a precise test: imagine "github.com/nasty/v/v2" - j := strings.LastIndex(nm, x.Name()) + // Check whether the original literal contains the package's declared name. + j := strings.LastIndex(d.Path.Value, imported.Name()) if j == -1 { // name doesn't show up, for whatever reason, so nothing to report return } - start := d.Path.Pos() + 1 + token.Pos(j) // skip the initial quote - e.token(start, len(x.Name()), tokNamespace, nil) + // Report virtual declaration at the position of the substring. + start := d.Path.Pos() + token.Pos(j) + e.token(start, len(imported.Name()), tokNamespace, nil) } // log unexpected state diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 1e5dd1cba09..7d98205d020 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -264,7 +264,6 @@ type compLitInfo struct { type importInfo struct { importPath string name string - pkg source.Package } type methodSetKey struct { @@ -1165,7 +1164,6 @@ func (c *completer) unimportedMembers(ctx context.Context, id *ast.Ident) error } imp := &importInfo{ importPath: path, - pkg: pkg, } if imports.ImportPathToAssumedName(path) != pkg.GetTypes().Name() { imp.name = pkg.GetTypes().Name() @@ -1517,7 +1515,6 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru } imp := &importInfo{ importPath: path, - pkg: pkg, } if imports.ImportPathToAssumedName(path) != pkg.GetTypes().Name() { imp.name = pkg.GetTypes().Name() diff --git a/gopls/internal/lsp/source/completion/statements.go b/gopls/internal/lsp/source/completion/statements.go index b43629c0394..1f80193f194 100644 --- a/gopls/internal/lsp/source/completion/statements.go +++ b/gopls/internal/lsp/source/completion/statements.go @@ -336,7 +336,7 @@ func getTestVar(enclosingFunc *funcInfo, pkg source.Package) string { if param.Name() == "_" { continue } - testingPkg, err := pkg.GetImport("testing") + testingPkg, err := pkg.DirectDep("testing") if err != nil { continue } diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 0dc8d8ab0f6..c758d688dd2 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -342,7 +342,7 @@ func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) // See golang/go#36998: don't link to modules matching GOPRIVATE. // - // The path returned by linkData is an import path. + // The path returned by linkData is a package path. if i.Snapshot.View().IsGoPrivatePath(h.LinkPath) { h.LinkPath = "" } else if mod, version, ok := moduleAtVersion(h.LinkPath, i); ok { @@ -352,11 +352,11 @@ func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) return h, nil } -// linkData returns the name, import path, and anchor to use in building links +// linkData returns the name, package path, and anchor to use in building links // to obj. // // If obj is not visible in documentation, the returned name will be empty. -func linkData(obj types.Object, enclosing *types.TypeName) (name, importPath, anchor string) { +func linkData(obj types.Object, enclosing *types.TypeName) (name, packagePath, anchor string) { // Package names simply link to the package. if obj, ok := obj.(*types.PkgName); ok { return obj.Name(), obj.Imported().Path(), "" @@ -430,7 +430,7 @@ func linkData(obj types.Object, enclosing *types.TypeName) (name, importPath, an return "", "", "" } - importPath = obj.Pkg().Path() + packagePath = obj.Pkg().Path() if recv != nil { anchor = fmt.Sprintf("%s.%s", recv.Name(), obj.Name()) name = fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), recv.Name(), obj.Name()) @@ -439,7 +439,7 @@ func linkData(obj types.Object, enclosing *types.TypeName) (name, importPath, an anchor = obj.Name() name = fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name()) } - return name, importPath, anchor + return name, packagePath, anchor } func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) { @@ -448,7 +448,7 @@ func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) { if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" { return "", "", false } - impPkg, err := i.pkg.GetImport(path) + impPkg, err := i.pkg.DirectDep(path) if err != nil { return "", "", false } @@ -535,11 +535,11 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob } case *ast.ImportSpec: // Try to find the package documentation for an imported package. - pkgPath, err := strconv.Unquote(node.Path.Value) + importPath, err := strconv.Unquote(node.Path.Value) if err != nil { return nil, err } - imp, err := pkg.GetImport(pkgPath) + imp, err := pkg.ResolveImportPath(importPath) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 7bf1e12b745..f11817f5865 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -456,21 +456,21 @@ func importSpec(snapshot Snapshot, pkg Package, file *ast.File, pos token.Pos) ( if err != nil { return nil, fmt.Errorf("import path not quoted: %s (%v)", imp.Path.Value, err) } + imported, err := pkg.ResolveImportPath(importPath) + if err != nil { + return nil, err + } result := &IdentifierInfo{ Snapshot: snapshot, - Name: importPath, + Name: importPath, // should this perhaps be imported.PkgPath()? pkg: pkg, } if result.MappedRange, err = posToMappedRange(snapshot.FileSet(), pkg, imp.Path.Pos(), imp.Path.End()); err != nil { return nil, err } // Consider the "declaration" of an import spec to be the imported package. - importedPkg, err := pkg.GetImport(importPath) - if err != nil { - return nil, err - } // Return all of the files in the package as the definition of the import spec. - for _, dst := range importedPkg.GetSyntax() { + for _, dst := range imported.GetSyntax() { rng, err := posToMappedRange(snapshot.FileSet(), pkg, dst.Pos(), dst.End()) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go index d7f229ecc80..e2d950ba00b 100644 --- a/gopls/internal/lsp/source/known_packages.go +++ b/gopls/internal/lsp/source/known_packages.go @@ -28,6 +28,8 @@ func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandl for _, imp := range pgf.File.Imports { alreadyImported[imp.Path.Value] = struct{}{} } + // TODO(adonovan): this whole algorithm could be more + // simply expressed in terms of Metadata, not Packages. pkgs, err := snapshot.CachedImportPaths(ctx) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go index 6fb7ddf9ba5..c4d5709bf74 100644 --- a/gopls/internal/lsp/source/rename_check.go +++ b/gopls/internal/lsp/source/rename_check.go @@ -838,11 +838,11 @@ func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Po if err != nil { continue } - importPkg, err := pkg.GetImport(importPath) + imported, err := pkg.ResolveImportPath(importPath) if err != nil { return nil, nil, nil, false } - pkgs = append(pkgs, importPkg) + pkgs = append(pkgs, imported) } } for _, p := range pkgs { diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 2d00ea5272c..28a2e2b23d3 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -255,8 +255,10 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method eiface := iface.Embedded(i).Obj() depPkg := ifacePkg if eiface.Pkg().Path() != ifacePkg.PkgPath() { + // TODO(adonovan): I'm not sure what this is trying to do, but it + // looks wrong the in case of type aliases. var err error - depPkg, err = ifacePkg.GetImport(eiface.Pkg().Path()) + depPkg, err = ifacePkg.DirectDep(eiface.Pkg().Path()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index d242cf4026e..5fb99ea4054 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -152,8 +152,8 @@ type Snapshot interface { GetReverseDependencies(ctx context.Context, id string) ([]Package, error) // CachedImportPaths returns all the imported packages loaded in this - // snapshot, indexed by their import path and checked in TypecheckWorkspace - // mode. + // snapshot, indexed by their package path (not import path, despite the name) + // and checked in TypecheckWorkspace mode. CachedImportPaths(ctx context.Context) (map[string]Package, error) // KnownPackages returns all the packages loaded in this snapshot, checked @@ -510,6 +510,14 @@ func (h Hash) Less(other Hash) bool { return bytes.Compare(h[:], other[:]) < 0 } +// XORWith updates *h to *h XOR h2. +func (h *Hash) XORWith(h2 Hash) { + // Small enough that we don't need crypto/subtle.XORBytes. + for i := range h { + h[i] ^= h2[i] + } +} + // FileIdentity uniquely identifies a file at a version from a FileSystem. type FileIdentity struct { URI span.URI @@ -581,9 +589,9 @@ func (a Analyzer) IsEnabled(view View) bool { // Package represents a Go package that has been type-checked. It maintains // only the relevant fields of a *go/packages.Package. type Package interface { - ID() string - Name() string - PkgPath() string + ID() string // logically a cache.PackageID + Name() string // logically a cache.PackageName + PkgPath() string // logically a cache.PackagePath CompiledGoFiles() []*ParsedGoFile File(uri span.URI) (*ParsedGoFile, error) GetSyntax() []*ast.File @@ -591,8 +599,9 @@ type Package interface { GetTypesInfo() *types.Info GetTypesSizes() types.Sizes ForTest() string - GetImport(pkgPath string) (Package, error) - MissingDependencies() []string + DirectDep(packagePath string) (Package, error) // logically a cache.PackagePath + ResolveImportPath(importPath string) (Package, error) // logically a cache.ImportPath + MissingDependencies() []string // unordered; logically cache.ImportPaths Imports() []Package Version() *module.Version HasListOrParseErrors() bool From 649df2ea1a9b6e73d1fd5c4dacd95a1be74683d5 Mon Sep 17 00:00:00 2001 From: Heschi Kreinick Date: Wed, 19 Oct 2022 17:24:22 -0400 Subject: [PATCH 355/723] go.mod: mark as requiring -compat 1.16 When the tagging process runs go mod tidy it needs to pass -compat 1.16. This magic comment will cause it to do so. See CL 443857. For golang/go#48523. Change-Id: I6824a2c78462604b9d4737444b4d16d4c21ecc99 Reviewed-on: https://go-review.googlesource.com/c/tools/+/444295 Auto-Submit: Heschi Kreinick Reviewed-by: Robert Findley Run-TryBot: Heschi Kreinick TryBot-Result: Gopher Robot --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 492efe92e5f..e87b036172f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module golang.org/x/tools -go 1.18 +go 1.18 // tagx:compat 1.16 require ( github.com/yuin/goldmark v1.4.13 From 61280309af36ad3b716dc3d4a1703b362ff5fc35 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 17 Oct 2022 16:35:50 -0400 Subject: [PATCH 356/723] gopls/internal: support renaming packages with int. test variants We need to search intermediate test variants to find all imports of a renamed package, but were missing them because we only considered "active packages". Fix this by building up the set of renamed packages based on metadata alone: it is unnecessary and potentially too costly to request all (typechecked) known packages, when we only need access to the metadata graph to determine which packages must be renamed. In doing so, it becomes possible that we produce duplicate edits by renaming through a test variant. Avoid this by keeping track of import path changes that we have already processed. While at it, add a few more checks for package renaming: - check for valid identifiers - disallow renaming x_test packages - disallow renaming to x_test packages Also refactor package renaming slightly to pass around an edit map. This fixes a bug where nested import paths were not renamed in the original renaming package. Fix some testing bugs that were exercised by new tests. For golang/go#41567 Change-Id: I18ab442b33a3ee5bf527f078dcaa81d47f0220c7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/443637 Reviewed-by: Dylan Le gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/check.go | 1 - gopls/internal/lsp/cache/snapshot.go | 22 ++ gopls/internal/lsp/fake/editor.go | 21 +- gopls/internal/lsp/fake/workdir.go | 17 + gopls/internal/lsp/regtest/wrappers.go | 11 + gopls/internal/lsp/source/references.go | 2 + gopls/internal/lsp/source/rename.go | 300 ++++++++++++------ gopls/internal/lsp/source/view.go | 7 + .../completion/postfix_snippet_test.go | 4 +- .../internal/regtest/misc/definition_test.go | 1 - gopls/internal/regtest/misc/rename_test.go | 228 ++++++++++++- 11 files changed, 501 insertions(+), 113 deletions(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 235eacd2d91..70eaed0d193 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -150,7 +150,6 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so experimentalKey := s.View().Options().ExperimentalPackageCacheKey phKey := computePackageKey(m.ID, compiledGoFiles, m, depKey, mode, experimentalKey) promise, release := s.store.Promise(phKey, func(ctx context.Context, arg interface{}) interface{} { - pkg, err := typeCheckImpl(ctx, arg.(*snapshot), goFiles, compiledGoFiles, m.Metadata, mode, deps) return typeCheckResult{pkg, err} }) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index eda1ff7e137..c7e5e8ad640 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1195,6 +1195,28 @@ func (s *snapshot) KnownPackages(ctx context.Context) ([]source.Package, error) return pkgs, nil } +func (s *snapshot) AllValidMetadata(ctx context.Context) ([]source.Metadata, error) { + if err := s.awaitLoaded(ctx); err != nil { + return nil, err + } + + s.mu.Lock() + defer s.mu.Unlock() + + var meta []source.Metadata + for _, m := range s.meta.metadata { + if m.Valid { + meta = append(meta, m) + } + } + return meta, nil +} + +func (s *snapshot) WorkspacePackageByID(ctx context.Context, id string) (source.Package, error) { + packageID := PackageID(id) + return s.checkedPackage(ctx, packageID, s.workspaceParseMode(packageID)) +} + func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Package, error) { // Don't reload workspace package metadata. // This function is meant to only return currently cached information. diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index b9f62f3028d..dfd17c7e55a 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -366,7 +366,12 @@ func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) { } // OpenFile creates a buffer for the given workdir-relative file. +// +// If the file is already open, it is a no-op. func (e *Editor) OpenFile(ctx context.Context, path string) error { + if e.HasBuffer(path) { + return nil + } content, err := e.sandbox.Workdir.ReadFile(path) if err != nil { return err @@ -383,6 +388,11 @@ func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error { e.mu.Lock() + if _, ok := e.buffers[path]; ok { + e.mu.Unlock() + return fmt.Errorf("buffer %q already exists", path) + } + buf := buffer{ windowsLineEndings: e.config.WindowsLineEndings, version: 1, @@ -712,7 +722,7 @@ func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit } content, err := applyEdits(buf.lines, edits) if err != nil { - return err + return fmt.Errorf("editing %q: %v; edits:\n%v", path, err, edits) } return e.setBufferContentLocked(ctx, path, true, content, edits) } @@ -1171,6 +1181,15 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin if e.Server == nil { return nil } + + // Verify that PrepareRename succeeds. + prepareParams := &protocol.PrepareRenameParams{} + prepareParams.TextDocument = e.TextDocumentIdentifier(path) + prepareParams.Position = pos.ToProtocolPosition() + if _, err := e.Server.PrepareRename(ctx, prepareParams); err != nil { + return fmt.Errorf("preparing rename: %v", err) + } + params := &protocol.RenameParams{ TextDocument: e.TextDocumentIdentifier(path), Position: pos.ToProtocolPosition(), diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index f194e263fdd..2b426e40c3b 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "runtime" + "sort" "strings" "sync" "time" @@ -349,6 +350,22 @@ func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error return nil } +// ListFiles returns a new sorted list of the relative paths of files in dir, +// recursively. +func (w *Workdir) ListFiles(dir string) ([]string, error) { + m, err := w.listFiles(dir) + if err != nil { + return nil, err + } + + var paths []string + for p := range m { + paths = append(paths, p) + } + sort.Strings(paths) + return paths, nil +} + // listFiles lists files in the given directory, returning a map of relative // path to contents and modification time. func (w *Workdir) listFiles(dir string) (map[string]fileID, error) { diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index 63b59917c10..13e5f7bc819 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -58,6 +58,17 @@ func (e *Env) WriteWorkspaceFiles(files map[string]string) { } } +// ListFiles lists relative paths to files in the given directory. +// It calls t.Fatal on any error. +func (e *Env) ListFiles(dir string) []string { + e.T.Helper() + paths, err := e.Sandbox.Workdir.ListFiles(dir) + if err != nil { + e.T.Fatal(err) + } + return paths +} + // OpenFile opens a file in the editor, calling t.Fatal on any error. func (e *Env) OpenFile(name string) { e.T.Helper() diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index de6687ad49f..e26091c9fe8 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -62,6 +62,8 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit } if inPackageName { + // TODO(rfindley): this is inaccurate, excluding test variants, and + // redundant with package renaming. Refactor to share logic. renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckWorkspace, NarrowestPackage) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 2f0ad56ec66..2d4a188b8ac 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -76,26 +76,44 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot err := errors.New("can't rename package: LSP client does not support file renaming") return nil, err, err } - renamingPkg, err := snapshot.PackageForFile(ctx, f.URI(), TypecheckWorkspace, NarrowestPackage) + fileMeta, err := snapshot.MetadataForFile(ctx, f.URI()) if err != nil { return nil, err, err } - if renamingPkg.Name() == "main" { + if len(fileMeta) == 0 { + err := fmt.Errorf("no packages found for file %q", f.URI()) + return nil, err, err + } + + meta := fileMeta[0] + + if meta.PackageName() == "main" { err := errors.New("can't rename package \"main\"") return nil, err, err } - if renamingPkg.Version() == nil { - err := fmt.Errorf("can't rename package: missing module information for package %q", renamingPkg.PkgPath()) + if strings.HasSuffix(meta.PackageName(), "_test") { + err := errors.New("can't rename x_test packages") return nil, err, err } - if renamingPkg.Version().Path == renamingPkg.PkgPath() { - err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", renamingPkg.PkgPath(), renamingPkg.Version().Path) + if meta.ModuleInfo() == nil { + err := fmt.Errorf("can't rename package: missing module information for package %q", meta.PackagePath()) return nil, err, err } - result, err := computePrepareRenameResp(snapshot, renamingPkg, pgf.File.Name, renamingPkg.Name()) + + if meta.ModuleInfo().Path == meta.PackagePath() { + err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PackagePath(), meta.ModuleInfo().Path) + return nil, err, err + } + // TODO(rfindley): we should not need the package here. + pkg, err := snapshot.WorkspacePackageByID(ctx, meta.PackageID()) + if err != nil { + err = fmt.Errorf("error building package to rename: %v", err) + return nil, err, err + } + result, err := computePrepareRenameResp(snapshot, pkg, pgf.File.Name, pkg.Name()) if err != nil { return nil, nil, err } @@ -164,65 +182,46 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, } if inPackageName { - // Since we only take one package below, no need to include test variants. - // - // TODO(rfindley): but is this correct? What about x_test packages that - // import the renaming package? - const includeTestVariants = false - pkgs, err := s.PackagesForFile(ctx, f.URI(), TypecheckWorkspace, includeTestVariants) + if !isValidIdentifier(newName) { + return nil, true, fmt.Errorf("%q is not a valid identifier", newName) + } + + fileMeta, err := s.MetadataForFile(ctx, f.URI()) if err != nil { return nil, true, err } - var pkg Package // TODO(rfindley): we should consider all packages, so that we get the full reverse transitive closure. - for _, p := range pkgs { - // pgf.File.Name must not be nil, else this will panic. - if pgf.File.Name.Name == p.Name() { - pkg = p - break - } + + if len(fileMeta) == 0 { + return nil, true, fmt.Errorf("no packages found for file %q", f.URI()) } - activePkgs, err := s.ActivePackages(ctx) - if err != nil { - return nil, true, err + + // We need metadata for the relevant package and module paths. These should + // be the same for all packages containing the file. + // + // TODO(rfindley): we mix package path and import path here haphazardly. + // Fix this. + meta := fileMeta[0] + oldPath := meta.PackagePath() + var modulePath string + if mi := meta.ModuleInfo(); mi == nil { + return nil, true, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PackagePath()) + } else { + modulePath = mi.Path + } + + if strings.HasSuffix(newName, "_test") { + return nil, true, fmt.Errorf("cannot rename to _test package") } - renamingEdits, err := computeImportRenamingEdits(ctx, s, pkg, activePkgs, newName) + + metadata, err := s.AllValidMetadata(ctx) if err != nil { return nil, true, err } - pkgNameEdits, err := computePackageNameRenamingEdits(pkg, newName) + + renamingEdits, err := renamePackage(ctx, s, modulePath, oldPath, newName, metadata) if err != nil { return nil, true, err } - for uri, edits := range pkgNameEdits { - renamingEdits[uri] = edits - } - // Rename test packages - for _, activePkg := range activePkgs { - if activePkg.ForTest() != pkg.PkgPath() { - continue - } - // Filter out intermediate test variants. - if activePkg.PkgPath() != pkg.PkgPath() && activePkg.PkgPath() != pkg.PkgPath()+"_test" { - continue - } - newTestPkgName := newName - if strings.HasSuffix(activePkg.Name(), "_test") { - newTestPkgName += "_test" - } - perPackageEdits, err := computeRenamePackageImportEditsPerPackage(ctx, s, activePkg, newTestPkgName, pkg.PkgPath()) - for uri, edits := range perPackageEdits { - renamingEdits[uri] = append(renamingEdits[uri], edits...) - } - pkgNameEdits, err := computePackageNameRenamingEdits(activePkg, newTestPkgName) - if err != nil { - return nil, true, err - } - for uri, edits := range pkgNameEdits { - if _, ok := renamingEdits[uri]; !ok { - renamingEdits[uri] = edits - } - } - } return renamingEdits, true, nil } @@ -239,98 +238,187 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, return result, false, nil } -// computeImportRenamingEdits computes all edits to files in other packages that import -// the renaming package. -func computeImportRenamingEdits(ctx context.Context, s Snapshot, renamingPkg Package, pkgs []Package, newName string) (map[span.URI][]protocol.TextEdit, error) { - result := make(map[span.URI][]protocol.TextEdit) +// renamePackage computes all workspace edits required to rename the package +// described by the given metadata, to newName, by renaming its package +// directory. +// +// It updates package clauses and import paths for the renamed package as well +// as any other packages affected by the directory renaming among packages +// described by allMetadata. +func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath, newName string, allMetadata []Metadata) (map[span.URI][]protocol.TextEdit, error) { + if modulePath == oldPath { + return nil, fmt.Errorf("cannot rename package: module path %q is the same as the package path, so renaming the package directory would have no effect", modulePath) + } + + newPathPrefix := path.Join(path.Dir(oldPath), newName) + + edits := make(map[span.URI][]protocol.TextEdit) + seen := make(seenPackageRename) // track per-file import renaming we've already processed + // Rename imports to the renamed package from other packages. - for _, pkg := range pkgs { - if renamingPkg.Version() == nil { - return nil, fmt.Errorf("cannot rename package: missing module information for package %q", renamingPkg.PkgPath()) + for _, m := range allMetadata { + // Special case: x_test packages for the renamed package will not have the + // package path as as a dir prefix, but still need their package clauses + // renamed. + if m.PackagePath() == oldPath+"_test" { + newTestName := newName + "_test" + + if err := renamePackageClause(ctx, m, s, newTestName, seen, edits); err != nil { + return nil, err + } + continue } - renamingPkgModulePath := renamingPkg.Version().Path - activePkgModulePath := pkg.Version().Path - if !strings.HasPrefix(pkg.PkgPath()+"/", renamingPkg.PkgPath()+"/") { - continue // not a nested package or the renaming package. + + // Subtle: check this condition before checking for valid module info + // below, because we should not fail this operation if unrelated packages + // lack module info. + if !strings.HasPrefix(m.PackagePath()+"/", oldPath+"/") { + continue // not affected by the package renaming } - if activePkgModulePath == pkg.PkgPath() { - continue // don't edit imports to nested package whose path and module path is the same. + if m.ModuleInfo() == nil { + return nil, fmt.Errorf("cannot rename package: missing module information for package %q", m.PackagePath()) } - if renamingPkgModulePath != "" && renamingPkgModulePath != activePkgModulePath { - continue // don't edit imports if nested package and renaming package has different module path. + if modulePath != m.ModuleInfo().Path { + continue // don't edit imports if nested package and renaming package have different module paths } - // Compute all edits for other files that import this nested package - // when updating the its path. - perFileEdits, err := computeRenamePackageImportEditsPerPackage(ctx, s, pkg, newName, renamingPkg.PkgPath()) - if err != nil { - return nil, err + // Renaming a package consists of changing its import path and package name. + suffix := strings.TrimPrefix(m.PackagePath(), oldPath) + newPath := newPathPrefix + suffix + + pkgName := m.PackageName() + if m.PackagePath() == oldPath { + pkgName = newName + + if err := renamePackageClause(ctx, m, s, newName, seen, edits); err != nil { + return nil, err + } } - for uri, edits := range perFileEdits { - result[uri] = append(result[uri], edits...) + + if err := renameImports(ctx, s, m, newPath, pkgName, seen, edits); err != nil { + return nil, err } } - return result, nil + return edits, nil } -// computePackageNameRenamingEdits computes all edits to files within the renming packages. -func computePackageNameRenamingEdits(renamingPkg Package, newName string) (map[span.URI][]protocol.TextEdit, error) { - result := make(map[span.URI][]protocol.TextEdit) +// seenPackageRename tracks import path renamings that have already been +// processed. +// +// Due to test variants, files may appear multiple times in the reverse +// transitive closure of a renamed package, or in the reverse transitive +// closure of different variants of a renamed package (both are possible). +// However, in all cases the resulting edits will be the same. +type seenPackageRename map[seenPackageKey]bool +type seenPackageKey struct { + uri span.URI + importPath string +} + +// add reports whether uri and importPath have been seen, and records them as +// seen if not. +func (s seenPackageRename) add(uri span.URI, importPath string) bool { + key := seenPackageKey{uri, importPath} + seen := s[key] + if !seen { + s[key] = true + } + return seen +} + +// renamePackageClause computes edits renaming the package clause of files in +// the package described by the given metadata, to newName. +// +// As files may belong to multiple packages, the seen map tracks files whose +// package clause has already been updated, to prevent duplicate edits. +// +// Edits are written into the edits map. +func renamePackageClause(ctx context.Context, m Metadata, s Snapshot, newName string, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { + pkg, err := s.WorkspacePackageByID(ctx, m.PackageID()) + if err != nil { + return err + } + // Rename internal references to the package in the renaming package. - for _, f := range renamingPkg.CompiledGoFiles() { + for _, f := range pkg.CompiledGoFiles() { + if seen.add(f.URI, m.PackagePath()) { + continue + } + if f.File.Name == nil { continue } pkgNameMappedRange := NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()) - // Invalid range for the package name. rng, err := pkgNameMappedRange.Range() if err != nil { - return nil, err + return err } - result[f.URI] = append(result[f.URI], protocol.TextEdit{ + edits[f.URI] = append(edits[f.URI], protocol.TextEdit{ Range: rng, NewText: newName, }) } - return result, nil + return nil } -// computeRenamePackageImportEditsPerPackage computes the set of edits (to imports) -// among the files of package nestedPkg that are necessary when package renamedPkg -// is renamed to newName. -func computeRenamePackageImportEditsPerPackage(ctx context.Context, s Snapshot, nestedPkg Package, newName, renamingPath string) (map[span.URI][]protocol.TextEdit, error) { - rdeps, err := s.GetReverseDependencies(ctx, nestedPkg.ID()) +// renameImports computes the set of edits to imports resulting from renaming +// the package described by the given metadata, to a package with import path +// newPath and name newName. +// +// Edits are written into the edits map. +func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath, newName string, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { + // TODO(rfindley): we should get reverse dependencies as metadata first, + // rather then building the package immediately. We don't need reverse + // dependencies if they are intermediate test variants. + rdeps, err := s.GetReverseDependencies(ctx, m.PackageID()) if err != nil { - return nil, err + return err } - result := make(map[span.URI][]protocol.TextEdit) for _, dep := range rdeps { + // Subtle: don't perform renaming in this package if it is not fully + // parsed. This can occur inside the workspace if dep is an intermediate + // test variant. ITVs are only ever parsed in export mode, and no file is + // found only in an ITV. Therefore the renaming will eventually occur in a + // full package. + // + // An alternative algorithm that may be more robust would be to first + // collect *files* that need to have their imports updated, and then + // perform the rename using s.PackageForFile(..., NarrowestPackage). + if dep.ParseMode() != ParseFull { + continue + } + for _, f := range dep.CompiledGoFiles() { + if seen.add(f.URI, m.PackagePath()) { + continue + } + for _, imp := range f.File.Imports { - if impPath, _ := strconv.Unquote(imp.Path.Value); impPath != nestedPkg.PkgPath() { - continue // not the import we're looking for. + if impPath, _ := strconv.Unquote(imp.Path.Value); impPath != m.PackagePath() { + continue // not the import we're looking for } // Create text edit for the import path (string literal). impPathMappedRange := NewMappedRange(f.Tok, f.Mapper, imp.Path.Pos(), imp.Path.End()) rng, err := impPathMappedRange.Range() if err != nil { - return nil, err + return err } - newText := strconv.Quote(path.Join(path.Dir(renamingPath), newName) + strings.TrimPrefix(nestedPkg.PkgPath(), renamingPath)) - result[f.URI] = append(result[f.URI], protocol.TextEdit{ + newText := strconv.Quote(newPath) + edits[f.URI] = append(edits[f.URI], protocol.TextEdit{ Range: rng, NewText: newText, }) - // If the nested package is not the renaming package or its import path already - // has an local package name then we don't need to update the local package name. - if nestedPkg.PkgPath() != renamingPath || imp.Name != nil { + // If the package name of an import has not changed or if its import + // path already has a local package name, then we don't need to update + // the local package name. + if newName == m.PackageName() || imp.Name != nil { continue } @@ -344,6 +432,7 @@ func computeRenamePackageImportEditsPerPackage(ctx context.Context, s Snapshot, var changes map[span.URI][]protocol.TextEdit localName := newName try := 0 + // Keep trying with fresh names until one succeeds. for fileScope.Lookup(localName) != nil || pkgScope.Lookup(localName) != nil { try++ @@ -351,8 +440,9 @@ func computeRenamePackageImportEditsPerPackage(ctx context.Context, s Snapshot, } changes, err = renameObj(ctx, s, localName, qos) if err != nil { - return nil, err + return err } + // If the chosen local package name matches the package's new name, delete the // change that would have inserted an explicit local name, which is always // the lexically first change. @@ -363,14 +453,14 @@ func computeRenamePackageImportEditsPerPackage(ctx context.Context, s Snapshot, }) changes[f.URI] = v[1:] } - for uri, edits := range changes { - result[uri] = append(result[uri], edits...) + for uri, changeEdits := range changes { + edits[uri] = append(edits[uri], changeEdits...) } } } } - return result, nil + return nil } // renameObj returns a map of TextEdits for renaming an identifier within a file diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 5fb99ea4054..25fa7d704d0 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -166,6 +166,13 @@ type Snapshot interface { // mode, this is just the reverse transitive closure of open packages. ActivePackages(ctx context.Context) ([]Package, error) + // AllValidMetadata returns all valid metadata loaded for the snapshot. + AllValidMetadata(ctx context.Context) ([]Metadata, error) + + // WorkspacePackageByID returns the workspace package with id, type checked + // in 'workspace' mode. + WorkspacePackageByID(ctx context.Context, id string) (Package, error) + // Symbols returns all symbols in the snapshot. Symbols(ctx context.Context) map[span.URI][]Symbol diff --git a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go index e02f4c8ce10..56e26a235bf 100644 --- a/gopls/internal/regtest/completion/postfix_snippet_test.go +++ b/gopls/internal/regtest/completion/postfix_snippet_test.go @@ -438,12 +438,14 @@ func foo() string { }, ) r.Run(t, mod, func(t *testing.T, env *Env) { + env.CreateBuffer("foo.go", "") + for _, c := range cases { t.Run(c.name, func(t *testing.T) { c.before = strings.Trim(c.before, "\n") c.after = strings.Trim(c.after, "\n") - env.CreateBuffer("foo.go", c.before) + env.SetBufferContent("foo.go", c.before) pos := env.RegexpSearch("foo.go", "\n}") completions := env.Completion("foo.go", pos) diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index f2b7b1adbec..a5030d71fb3 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -87,7 +87,6 @@ func TestUnexportedStdlib_Issue40809(t *testing.T) { Run(t, stdlibDefinition, func(t *testing.T, env *Env) { env.OpenFile("main.go") name, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Printf)`)) - env.OpenFile(name) pos := env.RegexpSearch(name, `:=\s*(newPrinter)\(\)`) diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index aa0a47ad7b8..a9fd920317f 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -10,6 +10,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/testenv" ) @@ -448,7 +449,7 @@ package b }) } -func TestRenameWithTestPackage(t *testing.T) { +func TestRenamePackage_Tests(t *testing.T) { testenv.NeedsGo1Point(t, 17) const files = ` -- go.mod -- @@ -518,7 +519,7 @@ func main() { }) } -func TestRenameWithNestedModule(t *testing.T) { +func TestRenamePackage_NestedModule(t *testing.T) { testenv.NeedsGo1Point(t, 17) const files = ` -- go.mod -- @@ -577,7 +578,7 @@ func main() { }) } -func TestRenamePackageWithNonBlankSameImportPaths(t *testing.T) { +func TestRenamePackage_DuplicateImport(t *testing.T) { testenv.NeedsGo1Point(t, 17) const files = ` -- go.mod -- @@ -620,7 +621,7 @@ func main() { }) } -func TestRenamePackageWithBlankSameImportPaths(t *testing.T) { +func TestRenamePackage_DuplicateBlankImport(t *testing.T) { testenv.NeedsGo1Point(t, 17) const files = ` -- go.mod -- @@ -662,3 +663,222 @@ func main() { env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) }) } + +func TestRenamePackage_TestVariant(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- foo/foo.go -- +package foo + +const Foo = 42 +-- bar/bar.go -- +package bar + +import "mod.com/foo" + +const Bar = foo.Foo +-- bar/bar_test.go -- +package bar + +import "mod.com/foo" + +const Baz = foo.Foo +-- testdata/bar/bar.go -- +package bar + +import "mod.com/foox" + +const Bar = foox.Foo +-- testdata/bar/bar_test.go -- +package bar + +import "mod.com/foox" + +const Baz = foox.Foo +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo/foo.go") + env.Rename("foo/foo.go", env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") + + checkTestdata(t, env) + }) +} + +func TestRenamePackage_IntermediateTestVariant(t *testing.T) { + // In this test set up, we have the following import edges: + // bar_test -> baz -> foo -> bar + // bar_test -> foo -> bar + // bar_test -> bar + // + // As a consequence, bar_x_test.go is in the reverse closure of both + // `foo [bar.test]` and `baz [bar.test]`. This test confirms that we don't + // produce duplicate edits in this case. + const files = ` +-- go.mod -- +module foo.mod + +go 1.12 +-- foo/foo.go -- +package foo + +import "foo.mod/bar" + +const Foo = 42 + +const _ = bar.Bar +-- baz/baz.go -- +package baz + +import "foo.mod/foo" + +const Baz = foo.Foo +-- bar/bar.go -- +package bar + +var Bar = 123 +-- bar/bar_test.go -- +package bar + +const _ = Bar +-- bar/bar_x_test.go -- +package bar_test + +import ( + "foo.mod/bar" + "foo.mod/baz" + "foo.mod/foo" +) + +const _ = bar.Bar + baz.Baz + foo.Foo +-- testdata/foox/foo.go -- +package foox + +import "foo.mod/bar" + +const Foo = 42 + +const _ = bar.Bar +-- testdata/baz/baz.go -- +package baz + +import "foo.mod/foox" + +const Baz = foox.Foo +-- testdata/bar/bar_x_test.go -- +package bar_test + +import ( + "foo.mod/bar" + "foo.mod/baz" + "foo.mod/foox" +) + +const _ = bar.Bar + baz.Baz + foox.Foo +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo/foo.go") + env.Rename("foo/foo.go", env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") + + checkTestdata(t, env) + }) +} + +func TestRenamePackage_Nesting(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +import "mod.com/lib/nested" + +const A = 1 + nested.B +-- lib/nested/a.go -- +package nested + +const B = 1 +-- other/other.go -- +package other + +import ( + "mod.com/lib" + "mod.com/lib/nested" +) + +const C = lib.A + nested.B +-- testdata/libx/a.go -- +package libx + +import "mod.com/libx/nested" + +const A = 1 + nested.B +-- testdata/other/other.go -- +package other + +import ( + "mod.com/libx" + "mod.com/libx/nested" +) + +const C = libx.A + nested.B +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "package (lib)") + env.Rename("lib/a.go", pos, "libx") + + checkTestdata(t, env) + }) +} + +func TestRenamePackage_InvalidName(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +import "mod.com/lib/nested" + +const A = 1 + nested.B +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "package (lib)") + + for _, badName := range []string{"$$$", "lib_test"} { + if err := env.Editor.Rename(env.Ctx, "lib/a.go", pos, badName); err == nil { + t.Errorf("Rename(lib, libx) succeeded, want non-nil error") + } + } + }) +} + +// checkTestdata checks that current buffer contents match their corresponding +// expected content in the testdata directory. +func checkTestdata(t *testing.T, env *Env) { + t.Helper() + files := env.ListFiles("testdata") + if len(files) == 0 { + t.Fatal("no files in testdata directory") + } + for _, file := range files { + suffix := strings.TrimPrefix(file, "testdata/") + got := env.Editor.BufferText(suffix) + want := env.ReadWorkspaceFile(file) + if diff := compare.Text(want, got); diff != "" { + t.Errorf("Rename: unexpected buffer content for %s (-want +got):\n%s", suffix, diff) + } + } +} From 65196caeed0001ac363270d53feaf62d9b90873f Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 19 Oct 2022 14:35:16 -0400 Subject: [PATCH 357/723] gopls/README.md: fix wording around supported Go versions Change-Id: I25eb83edc5fca0958833371d2d4a6c28af6c21c7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/444175 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gopls/README.md b/gopls/README.md index 9afc2e48c1e..646419b7d06 100644 --- a/gopls/README.md +++ b/gopls/README.md @@ -76,9 +76,9 @@ meaning that it officially supports the last 2 major Go releases. Per support for the last 4 major Go releases, but this support extends only to not breaking the build and avoiding easily fixable regressions. -The following table shows the final gopls version that supports being built at -a given Go Version. Any more recent Go versions missing from this table can -still be built with the latest version of gopls. +The following table shows the final gopls version that supports being built +with a given Go version. Go releases more recent than any in the table can +build any version of gopls. | Go Version | Final gopls Version With Support | | ----------- | -------------------------------- | From 207f456f2d4057372e2612f699c66598e5cdd5f4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Oct 2022 14:03:47 -0400 Subject: [PATCH 358/723] go/internal/gcimporter: bump version number in skew check This constant controls the "version skew" warning printed in case of panic, but doesn't control the version that is actually accepted, which is currently v2. Also, clarify meaning of 'path' parameter. The subtle difference between "package path" (linker symbol prefix, which may have "vendor/" prefix) and "import path" (string literal appearing in import declaration, sans "vendor/") is a pervasive source of bugs in gopls' handling of vendored packages. Change-Id: I8c3001a23b84abb1f18d861e01334bb3f0a5c27a Reviewed-on: https://go-review.googlesource.com/c/tools/+/444537 gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Matthew Dempsky Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot --- go/gcexportdata/gcexportdata.go | 6 +++++- go/internal/gcimporter/iimport.go | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go index 2ed25a75024..42adb8f697b 100644 --- a/go/gcexportdata/gcexportdata.go +++ b/go/gcexportdata/gcexportdata.go @@ -87,7 +87,11 @@ func NewReader(r io.Reader) (io.Reader, error) { // Read reads export data from in, decodes it, and returns type // information for the package. -// The package name is specified by path. +// +// The package path (effectively its linker symbol prefix) is +// specified by path, since unlike the package name, this information +// may not be recorded in the export data. +// // File position information is added to fset. // // Read may inspect and add to the imports map to ensure that references diff --git a/go/internal/gcimporter/iimport.go b/go/internal/gcimporter/iimport.go index 4caa0f55d9d..6e4c066b69b 100644 --- a/go/internal/gcimporter/iimport.go +++ b/go/internal/gcimporter/iimport.go @@ -51,6 +51,8 @@ const ( iexportVersionPosCol = 1 iexportVersionGo1_18 = 2 iexportVersionGenerics = 2 + + iexportVersionCurrent = 2 ) type ident struct { @@ -96,7 +98,7 @@ func IImportBundle(fset *token.FileSet, imports map[string]*types.Package, data } func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data []byte, bundle bool, path string) (pkgs []*types.Package, err error) { - const currentVersion = 1 + const currentVersion = iexportVersionCurrent version := int64(-1) if !debug { defer func() { From f112c43328372460f7ac5bc951711609e22b01cc Mon Sep 17 00:00:00 2001 From: Heschi Kreinick Date: Thu, 20 Oct 2022 17:36:10 +0000 Subject: [PATCH 359/723] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Once this CL is submitted, and post-submit testing succeeds on all first-class ports across all supported Go versions, this repository will be tagged with its next minor version. Change-Id: Ic6643ddc8dc991da1e6817b994390c38ef9ed231 Reviewed-on: https://go-review.googlesource.com/c/tools/+/444536 Auto-Submit: Heschi Kreinick Reviewed-by: Robert Findley Reviewed-by: Heschi Kreinick Run-TryBot: Heschi Kreinick TryBot-Result: Gopher Robot gopls-CI: kokoro --- go.mod | 6 +++--- go.sum | 14 +++++++++++--- gopls/go.mod | 6 +++--- gopls/go.sum | 21 +++++++++------------ 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index e87b036172f..cfc184e5fb3 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 // tagx:compat 1.16 require ( github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - golang.org/x/net v0.0.0-20220722155237-a158d28d115b - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f + golang.org/x/mod v0.6.0 + golang.org/x/net v0.1.0 + golang.org/x/sys v0.1.0 ) diff --git a/go.sum b/go.sum index 20a9d4158b0..da2cda7d75f 100644 --- a/go.sum +++ b/go.sum @@ -2,24 +2,32 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gopls/go.mod b/gopls/go.mod index 9a16f38c1e6..efb9be1189e 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -7,10 +7,10 @@ require ( github.com/jba/printsrc v0.2.2 github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 + golang.org/x/mod v0.6.0 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 - golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 - golang.org/x/text v0.3.7 + golang.org/x/sys v0.1.0 + golang.org/x/text v0.4.0 golang.org/x/tools v0.1.13-0.20220928184430-f80e98464e27 golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 gopkg.in/yaml.v3 v3.0.1 diff --git a/gopls/go.sum b/gopls/go.sum index e4293b40ac6..78ff483dc7a 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -42,16 +42,18 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e h1:7Xs2YCOpMlNqSQSmrrnhlzBXIE/bpMecZplbLePTJvE= golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -60,19 +62,14 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs= -golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/vuln v0.0.0-20221004232641-2aa0553d353b h1:8Tu9pgIV7kt8ulNtzidzpLl9E9l1i+U4QLdKG0ZzHyE= -golang.org/x/vuln v0.0.0-20221004232641-2aa0553d353b/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68= -golang.org/x/vuln v0.0.0-20221006005703-27389ae96df4 h1:rj0uNKXz70TlwVjkDL/rF4qGHp0RzIXzDg7d7b0pnQo= -golang.org/x/vuln v0.0.0-20221006005703-27389ae96df4/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 h1:KaYZQUtEEaV8aVADIHAuYBTjo77aUcCvC7KTGKM3J1I= golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 06041c93ef8ec5c5e02f6203ed714ef2e11c2fb2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 20 Oct 2022 16:39:41 -0400 Subject: [PATCH 360/723] gopls/doc: update manual documentation of the newDiff setting Because we normally suppress internal options, the documentation for this setting is managed manually, and was stale. Update it, and bring it in-line with the actual setting docstring. Change-Id: Id3ceaa7303df4ee2a6bf07c54d087451169962cf Reviewed-on: https://go-review.googlesource.com/c/tools/+/444539 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Peter Weinberger --- gopls/doc/settings.md | 15 ++++++++------- gopls/internal/lsp/source/options.go | 9 ++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 34fb8d01ce8..5595976363f 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -510,13 +510,14 @@ Default: `false`. #### **newDiff** *string* -newDiff enables the new diff implementation. If this is "both", -for now both diffs will be run and statistics will be generateted in -a file in $TMPDIR. This is a risky setting; help in trying it -is appreciated. If it is "old" the old implementation is used, -and if it is "new", just the new implementation is used. - -Default: 'old'. +newDiff enables the new diff implementation. If this is "both", for now both +diffs will be run and statistics will be generated in a file in $TMPDIR. This +is a risky setting; help in trying it is appreciated. If it is "old" the old +implementation is used, and if it is "new", just the new implementation is +used. This setting will eventually be deleted, once gopls has fully migrated to +the new diff algorithm. + +Default: 'both'. ## Code Lenses diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 136ba23d86c..89442a32bd6 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -613,11 +613,10 @@ type InternalOptions struct { // This option applies only during initialization. ShowBugReports bool - // NewDiff controls the choice of the new diff implementation. - // It can be 'new', 'checked', or 'old' which is the default. - // 'checked' computes diffs with both algorithms, checks - // that the new algorithm has worked, and write some summary - // statistics to a file in os.TmpDir() + // NewDiff controls the choice of the new diff implementation. It can be + // 'new', 'old', or 'both', which is the default. 'both' computes diffs with + // both algorithms, checks that the new algorithm has worked, and write some + // summary statistics to a file in os.TmpDir(). NewDiff string // ChattyDiagnostics controls whether to report file diagnostics for each From 21f61277b08a61d57a0478163d21fc224035b4b6 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Oct 2022 14:32:57 -0400 Subject: [PATCH 361/723] gopls/internal/lsp/cache: add PkgPath->PackageID index to Metadata DepsBy{Pkg,Imp}Path are two different ways to look up the PackageID of a direct dependency. (We need both.) MissingDeps is now represented as a blank value in DepsByImpPath. Also try to make sense of MissingDependencies. (The deleted pkg.types==nil condition was provably false.) Change-Id: I6a04c69d3d97b3d78b77d4934592735bb941d05f Reviewed-on: https://go-review.googlesource.com/c/tools/+/444538 Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/analysis.go | 4 +- gopls/internal/lsp/cache/check.go | 20 ++++------ gopls/internal/lsp/cache/graph.go | 23 +---------- gopls/internal/lsp/cache/load.go | 14 +++---- gopls/internal/lsp/cache/metadata.go | 4 +- gopls/internal/lsp/cache/pkg.go | 60 ++++++++++++++-------------- gopls/internal/lsp/cache/snapshot.go | 18 +++++---- 7 files changed, 63 insertions(+), 80 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index fa2f7eaf08f..e15b43e91ce 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -150,8 +150,8 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A // An analysis that consumes/produces facts // must run on the package's dependencies too. if len(a.FactTypes) > 0 { - for _, importID := range ph.m.Imports { - depActionHandle, err := s.actionHandle(ctx, importID, a) + for _, depID := range ph.m.DepsByPkgPath { + depActionHandle, err := s.actionHandle(ctx, depID, a) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 70eaed0d193..65f786a2167 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -100,11 +100,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // the recursive key building of dependencies in parallel. deps := make(map[PackageID]*packageHandle) var depKey source.Hash // XOR of all unique deps - for _, depID := range m.Imports { - depHandle, ok := deps[depID] - if ok { - continue // e.g. duplicate import - } + for _, depID := range m.DepsByPkgPath { depHandle, err := s.buildPackageHandle(ctx, depID, s.workspaceParseMode(depID)) // Don't use invalid metadata for dependencies if the top-level // metadata is valid. We only load top-level packages, so if the @@ -452,10 +448,10 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil defer done() pkg := &pkg{ - m: m, - mode: mode, - depsByPkgPath: make(map[PackagePath]*pkg), - types: types.NewPackage(string(m.PkgPath), string(m.Name)), + m: m, + mode: mode, + deps: make(map[PackageID]*pkg), + types: types.NewPackage(string(m.PkgPath), string(m.Name)), typesInfo: &types.Info{ Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object), @@ -528,14 +524,14 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil // based on the metadata before we start type checking, // reporting them via types.Importer places the errors // at the correct source location. - id, ok := pkg.m.Imports[ImportPath(path)] + id, ok := pkg.m.DepsByImpPath[ImportPath(path)] if !ok { // If the import declaration is broken, // go list may fail to report metadata about it. // See TestFixImportDecl for an example. return nil, fmt.Errorf("missing metadata for import of %q", path) } - dep, ok := deps[id] + dep, ok := deps[id] // id may be "" if !ok { return nil, snapshot.missingPkgError(ctx, path) } @@ -546,7 +542,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil if err != nil { return nil, err } - pkg.depsByPkgPath[depPkg.m.PkgPath] = depPkg + pkg.deps[depPkg.m.ID] = depPkg return depPkg.types, nil }), } diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index 047a55c7920..1dac0767afb 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -53,8 +53,8 @@ func (g *metadataGraph) build() { // Build the import graph. g.importedBy = make(map[PackageID][]PackageID) for id, m := range g.metadata { - for _, importID := range uniqueDeps(m.Imports) { - g.importedBy[importID] = append(g.importedBy[importID], id) + for _, depID := range m.DepsByPkgPath { + g.importedBy[depID] = append(g.importedBy[depID], id) } } @@ -129,25 +129,6 @@ func (g *metadataGraph) build() { } } -// uniqueDeps returns a new sorted and duplicate-free slice containing the -// IDs of the package's direct dependencies. -func uniqueDeps(imports map[ImportPath]PackageID) []PackageID { - // TODO(adonovan): use generic maps.SortedUniqueValues(m.Imports) when available. - ids := make([]PackageID, 0, len(imports)) - for _, id := range imports { - ids = append(ids, id) - } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) - // de-duplicate in place - out := ids[:0] - for _, id := range ids { - if len(out) == 0 || id != out[len(out)-1] { - out = append(out, id) - } - } - return out -} - // reverseTransitiveClosure calculates the set of packages that transitively // import an id in ids. The result also includes given ids. // diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 83ea2cab41b..67b235a8093 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -542,10 +542,10 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con m.GoFiles = append(m.GoFiles, uri) } - imports := make(map[ImportPath]PackageID) + depsByImpPath := make(map[ImportPath]PackageID) + depsByPkgPath := make(map[PackagePath]PackageID) for importPath, imported := range pkg.Imports { importPath := ImportPath(importPath) - imports[importPath] = PackageID(imported.ID) // It is not an invariant that importPath == imported.PkgPath. // For example, package "net" imports "golang.org/x/net/dns/dnsmessage" @@ -590,18 +590,18 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con // TODO(adonovan): clarify this. Perhaps go/packages should // report which nodes were synthesized. if importPath != "unsafe" && len(imported.CompiledGoFiles) == 0 { - if m.MissingDeps == nil { - m.MissingDeps = make(map[ImportPath]struct{}) - } - m.MissingDeps[importPath] = struct{}{} + depsByImpPath[importPath] = "" // missing continue } + depsByImpPath[importPath] = PackageID(imported.ID) + depsByPkgPath[PackagePath(imported.PkgPath)] = PackageID(imported.ID) if err := buildMetadata(ctx, imported, cfg, query, updates, append(path, id)); err != nil { event.Error(ctx, "error in dependency", err) } } - m.Imports = imports + m.DepsByImpPath = depsByImpPath + m.DepsByPkgPath = depsByPkgPath return nil } diff --git a/gopls/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go index 5ac741a338f..c8b0a537222 100644 --- a/gopls/internal/lsp/cache/metadata.go +++ b/gopls/internal/lsp/cache/metadata.go @@ -33,8 +33,8 @@ type Metadata struct { ForTest PackagePath // package path under test, or "" TypesSizes types.Sizes Errors []packages.Error - Imports map[ImportPath]PackageID // may contain duplicate IDs - MissingDeps map[ImportPath]struct{} + DepsByImpPath map[ImportPath]PackageID // may contain dups; empty ID => missing + DepsByPkgPath map[PackagePath]PackageID // values are unique and non-empty Module *packages.Module depsErrors []*packagesinternal.PackageError diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 2f7389db18e..0b767b4b5b8 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -9,6 +9,7 @@ import ( "go/ast" "go/scanner" "go/types" + "sort" "golang.org/x/mod/module" "golang.org/x/tools/gopls/internal/lsp/source" @@ -23,7 +24,7 @@ type pkg struct { goFiles []*source.ParsedGoFile compiledGoFiles []*source.ParsedGoFile diagnostics []*source.Diagnostic - depsByPkgPath map[PackagePath]*pkg + deps map[PackageID]*pkg // use m.DepsBy{Pkg,Imp}Path to look up ID version *module.Version parseErrors []scanner.ErrorList typeErrors []types.Error @@ -116,38 +117,28 @@ func (p *pkg) ForTest() string { // from an import declaration, use ResolveImportPath instead. // They may differ in case of vendoring.) func (p *pkg) DirectDep(pkgPath string) (source.Package, error) { - if imp := p.depsByPkgPath[PackagePath(pkgPath)]; imp != nil { - return imp, nil + if id, ok := p.m.DepsByPkgPath[PackagePath(pkgPath)]; ok { + if imp := p.deps[id]; imp != nil { + return imp, nil + } } - // Don't return a nil pointer because that still satisfies the interface. - return nil, fmt.Errorf("no imported package for %s", pkgPath) + return nil, fmt.Errorf("package does not import package with path %s", pkgPath) } // ResolveImportPath returns the directly imported dependency of this package, // given its ImportPath. See also DirectDep. func (p *pkg) ResolveImportPath(importPath string) (source.Package, error) { - if id, ok := p.m.Imports[ImportPath(importPath)]; ok { - for _, imported := range p.depsByPkgPath { - if PackageID(imported.ID()) == id { - return imported, nil - } + if id, ok := p.m.DepsByImpPath[ImportPath(importPath)]; ok && id != "" { + if imp := p.deps[id]; imp != nil { + return imp, nil } } return nil, fmt.Errorf("package does not import %s", importPath) } func (p *pkg) MissingDependencies() []string { - // We don't invalidate metadata for import deletions, so check the package - // imports via the *types.Package. Only use metadata if p.types is nil. - if p.types == nil { - var md []string - for importPath := range p.m.MissingDeps { - md = append(md, string(importPath)) - } - return md - } - - // This looks wrong. + // We don't invalidate metadata for import deletions, + // so check the package imports via the *types.Package. // // rfindley says: it looks like this is intending to implement // a heuristic "if go list couldn't resolve import paths to @@ -158,20 +149,31 @@ func (p *pkg) MissingDependencies() []string { // doesn't need that dep anymore we shouldn't show the warning". // But either we're outside of GOPATH/Module, or we're not... // - // TODO(adonovan): figure out what it is trying to do. - var md []string + // adonovan says: I think this effectively reverses the + // heuristic used by the type checker when Importer.Import + // returns an error---go/types synthesizes a package whose + // Path is the import path (sans "vendor/")---hence the + // dubious ImportPath() conversion. A blank DepsByImpPath + // entry means a missing import. + // + // If we invalidate the metadata for import deletions (which + // should be fast) then we can simply return the blank entries + // in DepsByImpPath. (They are PackageIDs not PackagePaths, + // but the caller only cares whether the set is empty!) + var missing []string for _, pkg := range p.types.Imports() { - if _, ok := p.m.MissingDeps[ImportPath(pkg.Path())]; ok { - md = append(md, pkg.Path()) + if id, ok := p.m.DepsByImpPath[ImportPath(pkg.Path())]; ok && id == "" { + missing = append(missing, pkg.Path()) } } - return md + sort.Strings(missing) + return missing } func (p *pkg) Imports() []source.Package { - var result []source.Package - for _, imp := range p.depsByPkgPath { - result = append(result, imp) + var result []source.Package // unordered + for _, dep := range p.deps { + result = append(result, dep) } return result } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index c7e5e8ad640..30eb9520b5c 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -892,7 +892,7 @@ func (s *snapshot) isActiveLocked(id PackageID) (active bool) { } // TODO(rfindley): it looks incorrect that we don't also check GoFiles here. // If a CGo file is open, we want to consider the package active. - for _, dep := range m.Imports { + for _, dep := range m.DepsByPkgPath { if s.isActiveLocked(dep) { return true } @@ -1231,14 +1231,15 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac if err != nil { return } - for pkgPath, newPkg := range cachedPkg.depsByPkgPath { - if oldPkg, ok := results[string(pkgPath)]; ok { + for _, newPkg := range cachedPkg.deps { + pkgPath := newPkg.PkgPath() + if oldPkg, ok := results[pkgPath]; ok { // Using the same trick as NarrowestPackage, prefer non-variants. if len(newPkg.compiledGoFiles) < len(oldPkg.(*pkg).compiledGoFiles) { - results[string(pkgPath)] = newPkg + results[pkgPath] = newPkg } } else { - results[string(pkgPath)] = newPkg + results[pkgPath] = newPkg } } }) @@ -1893,8 +1894,11 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // package with missing dependencies. if anyFileAdded { for id, metadata := range s.meta.metadata { - if len(metadata.MissingDeps) > 0 { - directIDs[id] = true + for _, impID := range metadata.DepsByImpPath { + if impID == "" { // missing import + directIDs[id] = true + break + } } } } From 051f03f2c9772d22886683ef79559710ac6c4098 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 21 Oct 2022 11:10:37 -0400 Subject: [PATCH 362/723] gopls/internal/lsp/cache: remove unnecessary params The Context arg to missingPkgError was unused. Also, simplify Sprintf + Write -> Fprintf. Also, add missing newline to error message. Change-Id: I1728fa5029b2da398fdb5b606c8381256b87276b Reviewed-on: https://go-review.googlesource.com/c/tools/+/444775 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan --- gopls/internal/lsp/cache/check.go | 16 +++++++--------- gopls/internal/lsp/cache/snapshot.go | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 65f786a2167..17943efb707 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -533,7 +533,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil } dep, ok := deps[id] // id may be "" if !ok { - return nil, snapshot.missingPkgError(ctx, path) + return nil, snapshot.missingPkgError(path) } if !source.IsValidImport(string(m.PkgPath), string(dep.m.PkgPath)) { return nil, fmt.Errorf("invalid use of internal package %s", path) @@ -757,21 +757,19 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost // missingPkgError returns an error message for a missing package that varies // based on the user's workspace mode. -func (s *snapshot) missingPkgError(ctx context.Context, pkgPath string) error { +func (s *snapshot) missingPkgError(pkgPath string) error { var b strings.Builder if s.workspaceMode()&moduleMode == 0 { gorootSrcPkg := filepath.FromSlash(filepath.Join(s.view.goroot, "src", pkgPath)) - - b.WriteString(fmt.Sprintf("cannot find package %q in any of \n\t%s (from $GOROOT)", pkgPath, gorootSrcPkg)) - + fmt.Fprintf(&b, "cannot find package %q in any of \n\t%s (from $GOROOT)", pkgPath, gorootSrcPkg) for _, gopath := range filepath.SplitList(s.view.gopath) { gopathSrcPkg := filepath.FromSlash(filepath.Join(gopath, "src", pkgPath)) - b.WriteString(fmt.Sprintf("\n\t%s (from $GOPATH)", gopathSrcPkg)) + fmt.Fprintf(&b, "\n\t%s (from $GOPATH)", gopathSrcPkg) } } else { - b.WriteString(fmt.Sprintf("no required module provides package %q", pkgPath)) - if err := s.getInitializationError(ctx); err != nil { - b.WriteString(fmt.Sprintf("(workspace configuration error: %s)", err.MainError)) + fmt.Fprintf(&b, "no required module provides package %q", pkgPath) + if err := s.getInitializationError(); err != nil { + fmt.Fprintf(&b, "\n(workspace configuration error: %s)", err.MainError) } } return errors.New(b.String()) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 30eb9520b5c..eed7dfc6ea0 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1528,7 +1528,7 @@ func (s *snapshot) awaitLoadedAllErrors(ctx context.Context) *source.CriticalErr return nil } -func (s *snapshot) getInitializationError(ctx context.Context) *source.CriticalError { +func (s *snapshot) getInitializationError() *source.CriticalError { s.mu.Lock() defer s.mu.Unlock() From d212f7d04f7df1af68c1b8bb2d5679129cfea008 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 21 Oct 2022 11:11:42 -0400 Subject: [PATCH 363/723] gopls/internal/regtest/workspace: fix bugs in test The test had a missing import of "fmt" that is somehow ignored by the current analysis implementation (but was flagged as an error by my pending redesign). Add the import, and update the go.sum hashes. Change-Id: I6dd91b2863a7cbd0f16018151c942867bddc92e4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/444795 TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley --- gopls/internal/regtest/workspace/workspace_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index a271bb0fa7a..916d40f1eb3 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -35,6 +35,8 @@ go 1.12 -- example.com@v1.2.3/blah/blah.go -- package blah +import "fmt" + func SaySomething() { fmt.Println("something") } @@ -62,7 +64,7 @@ require ( random.org v1.2.3 ) -- pkg/go.sum -- -example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= +example.com v1.2.3 h1:veRD4tUnatQRgsULqULZPjeoBGFr2qBhevSCZllD2Ds= example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= random.org v1.2.3 h1:+JE2Fkp7gS0zsHXGEQJ7hraom3pNTlkxC4b2qPfA+/Q= random.org v1.2.3/go.mod h1:E9KM6+bBX2g5ykHZ9H27w16sWo3QwgonyjM44Dnej3I= @@ -216,6 +218,8 @@ go 1.12 -- example.com@v1.2.3/blah/blah.go -- package blah +import "fmt" + func SaySomething() { fmt.Println("something") } @@ -284,6 +288,8 @@ require b.com v1.2.3 -- c.com@v1.2.3/blah/blah.go -- package blah +import "fmt" + func SaySomething() { fmt.Println("something") } @@ -518,7 +524,7 @@ module b.com require example.com v1.2.3 -- modb/go.sum -- -example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= +example.com v1.2.3 h1:veRD4tUnatQRgsULqULZPjeoBGFr2qBhevSCZllD2Ds= example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= -- modb/b/b.go -- package b From d476af71084fbe280fed67c83190b31e5bbb7189 Mon Sep 17 00:00:00 2001 From: cuiweixie Date: Sat, 24 Sep 2022 16:16:42 +0800 Subject: [PATCH 364/723] go/ssa: add slice to array conversion For golang/go#46505 Change-Id: I642409e383c851277845b37dd8423dc673c12a8b Reviewed-on: https://go-review.googlesource.com/c/tools/+/433816 Run-TryBot: xie cui <523516579@qq.com> Reviewed-by: David Chase Reviewed-by: Zvonimir Pavlinovic TryBot-Result: Gopher Robot Reviewed-by: Tim King gopls-CI: kokoro --- go/ssa/builder.go | 2 + go/ssa/builder_go117_test.go | 2 - go/ssa/builder_go120_test.go | 48 +++++++++++++++++++++++ go/ssa/emit.go | 25 ++++++++++-- go/ssa/interp/interp_go120_test.go | 12 ++++++ go/ssa/interp/testdata/slice2array.go | 56 +++++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 go/ssa/builder_go120_test.go create mode 100644 go/ssa/interp/interp_go120_test.go create mode 100644 go/ssa/interp/testdata/slice2array.go diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 98ed49dfead..23674c3d0d2 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -669,6 +669,8 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { y.pos = e.Lparen case *SliceToArrayPointer: y.pos = e.Lparen + case *UnOp: // conversion from slice to array. + y.pos = e.Lparen } } return y diff --git a/go/ssa/builder_go117_test.go b/go/ssa/builder_go117_test.go index 46a09526f59..69985970596 100644 --- a/go/ssa/builder_go117_test.go +++ b/go/ssa/builder_go117_test.go @@ -57,8 +57,6 @@ func TestBuildPackageFailuresGo117(t *testing.T) { importer types.Importer }{ {"slice to array pointer - source is not a slice", "package p; var s [4]byte; var _ = (*[4]byte)(s)", nil}, - // TODO(taking) re-enable test below for Go versions < Go 1.20 - see issue #54822 - // {"slice to array pointer - dest is not a pointer", "package p; var s []byte; var _ = ([4]byte)(s)", nil}, {"slice to array pointer - dest pointer elem is not an array", "package p; var s []byte; var _ = (*byte)(s)", nil}, } diff --git a/go/ssa/builder_go120_test.go b/go/ssa/builder_go120_test.go new file mode 100644 index 00000000000..84bdd4c41ab --- /dev/null +++ b/go/ssa/builder_go120_test.go @@ -0,0 +1,48 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.20 +// +build go1.20 + +package ssa_test + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func TestBuildPackageGo120(t *testing.T) { + tests := []struct { + name string + src string + importer types.Importer + }{ + {"slice to array", "package p; var s []byte; var _ = ([4]byte)(s)", nil}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", tc.src, parser.ParseComments) + if err != nil { + t.Error(err) + } + files := []*ast.File{f} + + pkg := types.NewPackage("p", "") + conf := &types.Config{Importer: tc.importer} + if _, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions); err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} diff --git a/go/ssa/emit.go b/go/ssa/emit.go index fb11c3558d3..f6537acc97f 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -177,7 +177,6 @@ func emitConv(f *Function, val Value, typ types.Type) Value { if types.Identical(t_src, typ) { return val } - ut_dst := typ.Underlying() ut_src := t_src.Underlying() @@ -229,12 +228,32 @@ func emitConv(f *Function, val Value, typ types.Type) Value { // Conversion from slice to array pointer? if slice, ok := ut_src.(*types.Slice); ok { - if ptr, ok := ut_dst.(*types.Pointer); ok { + switch t := ut_dst.(type) { + case *types.Pointer: + ptr := t if arr, ok := ptr.Elem().Underlying().(*types.Array); ok && types.Identical(slice.Elem(), arr.Elem()) { c := &SliceToArrayPointer{X: val} - c.setType(ut_dst) + // TODO(taking): Check if this should be ut_dst or ptr. + c.setType(ptr) return f.emit(c) } + case *types.Array: + arr := t + if arr.Len() == 0 { + return zeroValue(f, arr) + } + if types.Identical(slice.Elem(), arr.Elem()) { + c := &SliceToArrayPointer{X: val} + c.setType(types.NewPointer(arr)) + x := f.emit(c) + unOp := &UnOp{ + Op: token.MUL, + X: x, + CommaOk: false, + } + unOp.setType(typ) + return f.emit(unOp) + } } } // A representation-changing conversion? diff --git a/go/ssa/interp/interp_go120_test.go b/go/ssa/interp/interp_go120_test.go new file mode 100644 index 00000000000..d8eb2c21341 --- /dev/null +++ b/go/ssa/interp/interp_go120_test.go @@ -0,0 +1,12 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.20 +// +build go1.20 + +package interp_test + +func init() { + testdataTests = append(testdataTests, "slice2array.go") +} diff --git a/go/ssa/interp/testdata/slice2array.go b/go/ssa/interp/testdata/slice2array.go new file mode 100644 index 00000000000..43c0543eabf --- /dev/null +++ b/go/ssa/interp/testdata/slice2array.go @@ -0,0 +1,56 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Test for slice to array conversion introduced in go1.20 +// See: https://tip.golang.org/ref/spec#Conversions_from_slice_to_array_pointer + +package main + +func main() { + s := make([]byte, 3, 4) + s[0], s[1], s[2] = 2, 3, 5 + a := ([2]byte)(s) + s[0] = 7 + + if a != [2]byte{2, 3} { + panic("converted from non-nil slice to array") + } + + { + var s []int + a:= ([0]int)(s) + if a != [0]int{} { + panic("zero len array is not equal") + } + } + + if emptyToEmptyDoesNotPanic() { + panic("no panic expected from emptyToEmptyDoesNotPanic()") + } + if !threeToFourDoesPanic() { + panic("panic expected from threeToFourDoesPanic()") + } +} + +func emptyToEmptyDoesNotPanic() (raised bool) { + defer func() { + if e := recover(); e != nil { + raised = true + } + }() + var s []int + _ = ([0]int)(s) + return false +} + +func threeToFourDoesPanic() (raised bool) { + defer func() { + if e := recover(); e != nil { + raised = true + } + }() + s := make([]int, 3, 5) + _ = ([4]int)(s) + return false +} \ No newline at end of file From 2dcdbd43ac63a0131b7955c050c0611f3207389e Mon Sep 17 00:00:00 2001 From: David Chase Date: Wed, 19 Oct 2022 15:15:23 -0400 Subject: [PATCH 365/723] go/internal/gcimporter: port CL 431495 to tools, add tests Changes to the export format need to be ported here too; added the tests that check for this bug. Notable changes to the copypasta -- there were name clashes between locally defined types and some new imports, resolved with import renaming. Change-Id: Ie7149595f65e91581e963ae4fe871d29fb98f4c0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/444235 Run-TryBot: David Chase Reviewed-by: Matthew Dempsky --- go/internal/gcimporter/gcimporter_test.go | 127 ++++++++++++++++++++++ go/internal/gcimporter/support_go118.go | 14 +++ go/internal/gcimporter/ureader_yes.go | 5 + 3 files changed, 146 insertions(+) diff --git a/go/internal/gcimporter/gcimporter_test.go b/go/internal/gcimporter/gcimporter_test.go index 5e1ca4bebcc..a71c1880bf1 100644 --- a/go/internal/gcimporter/gcimporter_test.go +++ b/go/internal/gcimporter/gcimporter_test.go @@ -10,8 +10,12 @@ package gcimporter import ( "bytes" "fmt" + "go/ast" "go/build" "go/constant" + goimporter "go/importer" + goparser "go/parser" + "go/token" "go/types" "io/ioutil" "os" @@ -156,6 +160,129 @@ func TestImportTestdata(t *testing.T) { } } +func TestImportTypeparamTests(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // requires generics + + // This package only handles gc export data. + if runtime.Compiler != "gc" { + t.Skipf("gc-built packages not available (compiler = %s)", runtime.Compiler) + } + + tmpdir := mktmpdir(t) + defer os.RemoveAll(tmpdir) + + // Check go files in test/typeparam, except those that fail for a known + // reason. + rootDir := filepath.Join(runtime.GOROOT(), "test", "typeparam") + list, err := os.ReadDir(rootDir) + if err != nil { + t.Fatal(err) + } + + var skip map[string]string + if !unifiedIR { + // The Go 1.18 frontend still fails several cases. + skip = map[string]string{ + "equal.go": "inconsistent embedded sorting", // TODO(rfindley): investigate this. + "nested.go": "fails to compile", // TODO(rfindley): investigate this. + "issue47631.go": "can not handle local type declarations", + "issue55101.go": "fails to compile", + } + } + + for _, entry := range list { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { + // For now, only consider standalone go files. + continue + } + + t.Run(entry.Name(), func(t *testing.T) { + if reason, ok := skip[entry.Name()]; ok { + t.Skip(reason) + } + + filename := filepath.Join(rootDir, entry.Name()) + src, err := os.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(src, []byte("// run")) && !bytes.HasPrefix(src, []byte("// compile")) { + // We're bypassing the logic of run.go here, so be conservative about + // the files we consider in an attempt to make this test more robust to + // changes in test/typeparams. + t.Skipf("not detected as a run test") + } + + // Compile and import, and compare the resulting package with the package + // that was type-checked directly. + compile(t, rootDir, entry.Name(), filepath.Join(tmpdir, "testdata")) + pkgName := strings.TrimSuffix(entry.Name(), ".go") + imported := importPkg(t, "./testdata/"+pkgName, tmpdir) + checked := checkFile(t, filename, src) + + seen := make(map[string]bool) + for _, name := range imported.Scope().Names() { + if !token.IsExported(name) { + continue // ignore synthetic names like .inittask and .dict.* + } + seen[name] = true + + importedObj := imported.Scope().Lookup(name) + got := types.ObjectString(importedObj, types.RelativeTo(imported)) + got = sanitizeObjectString(got) + + checkedObj := checked.Scope().Lookup(name) + if checkedObj == nil { + t.Fatalf("imported object %q was not type-checked", name) + } + want := types.ObjectString(checkedObj, types.RelativeTo(checked)) + want = sanitizeObjectString(want) + + if got != want { + t.Errorf("imported %q as %q, want %q", name, got, want) + } + } + + for _, name := range checked.Scope().Names() { + if !token.IsExported(name) || seen[name] { + continue + } + t.Errorf("did not import object %q", name) + } + }) + } +} + +// sanitizeObjectString removes type parameter debugging markers from an object +// string, to normalize it for comparison. +// TODO(rfindley): this should not be necessary. +func sanitizeObjectString(s string) string { + var runes []rune + for _, r := range s { + if '₀' <= r && r < '₀'+10 { + continue // trim type parameter subscripts + } + runes = append(runes, r) + } + return string(runes) +} + +func checkFile(t *testing.T, filename string, src []byte) *types.Package { + fset := token.NewFileSet() + f, err := goparser.ParseFile(fset, filename, src, 0) + if err != nil { + t.Fatal(err) + } + config := types.Config{ + Importer: goimporter.Default(), + } + pkg, err := config.Check("", fset, []*ast.File{f}, nil) + if err != nil { + t.Fatal(err) + } + return pkg +} + func TestVersionHandling(t *testing.T) { if debug { t.Skip("TestVersionHandling panics in debug mode") diff --git a/go/internal/gcimporter/support_go118.go b/go/internal/gcimporter/support_go118.go index a993843230c..edbe6ea7041 100644 --- a/go/internal/gcimporter/support_go118.go +++ b/go/internal/gcimporter/support_go118.go @@ -21,3 +21,17 @@ func additionalPredeclared() []types.Type { types.Universe.Lookup("any").Type(), } } + +// See cmd/compile/internal/types.SplitVargenSuffix. +func splitVargenSuffix(name string) (base, suffix string) { + i := len(name) + for i > 0 && name[i-1] >= '0' && name[i-1] <= '9' { + i-- + } + const dot = "·" + if i >= len(dot) && name[i-len(dot):i] == dot { + i -= len(dot) + return name[:i], name[i:] + } + return name, "" +} diff --git a/go/internal/gcimporter/ureader_yes.go b/go/internal/gcimporter/ureader_yes.go index 2d421c9619d..e8dff0d8537 100644 --- a/go/internal/gcimporter/ureader_yes.go +++ b/go/internal/gcimporter/ureader_yes.go @@ -490,6 +490,11 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { return objPkg, objName } + // Ignore local types promoted to global scope (#55110). + if _, suffix := splitVargenSuffix(objName); suffix != "" { + return objPkg, objName + } + if objPkg.Scope().Lookup(objName) == nil { dict := pr.objDictIdx(idx) From 8166dca1cec9410a3488f2ca7fa69fd425fb50ff Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Tue, 18 Oct 2022 11:39:53 +0800 Subject: [PATCH 366/723] go/analysis/passes/asmdecl: define register-ABI result registers for RISCV64 Change-Id: I7f88d31186704a7d83637acdf127e0522d725289 Reviewed-on: https://go-review.googlesource.com/c/tools/+/443575 gopls-CI: kokoro Reviewed-by: David Chase Run-TryBot: Wayne Zuo TryBot-Result: Gopher Robot Reviewed-by: Cherry Mui --- go/analysis/passes/asmdecl/asmdecl.go | 2 +- go/analysis/passes/asmdecl/asmdecl_test.go | 11 ++++++----- go/analysis/passes/asmdecl/testdata/src/a/asm11.s | 13 +++++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 go/analysis/passes/asmdecl/testdata/src/a/asm11.s diff --git a/go/analysis/passes/asmdecl/asmdecl.go b/go/analysis/passes/asmdecl/asmdecl.go index 6fbfe7e181c..7288559fc0e 100644 --- a/go/analysis/passes/asmdecl/asmdecl.go +++ b/go/analysis/passes/asmdecl/asmdecl.go @@ -92,7 +92,7 @@ var ( asmArchMips64LE = asmArch{name: "mips64le", bigEndian: false, stack: "R29", lr: true} asmArchPpc64 = asmArch{name: "ppc64", bigEndian: true, stack: "R1", lr: true, retRegs: []string{"R3", "F1"}} asmArchPpc64LE = asmArch{name: "ppc64le", bigEndian: false, stack: "R1", lr: true, retRegs: []string{"R3", "F1"}} - asmArchRISCV64 = asmArch{name: "riscv64", bigEndian: false, stack: "SP", lr: true} + asmArchRISCV64 = asmArch{name: "riscv64", bigEndian: false, stack: "SP", lr: true, retRegs: []string{"X10", "F10"}} asmArchS390X = asmArch{name: "s390x", bigEndian: true, stack: "R15", lr: true} asmArchWasm = asmArch{name: "wasm", bigEndian: false, stack: "SP", lr: false} diff --git a/go/analysis/passes/asmdecl/asmdecl_test.go b/go/analysis/passes/asmdecl/asmdecl_test.go index f6b01a9c308..50938a07571 100644 --- a/go/analysis/passes/asmdecl/asmdecl_test.go +++ b/go/analysis/passes/asmdecl/asmdecl_test.go @@ -19,11 +19,12 @@ var goosarches = []string{ "linux/arm", // asm3.s // TODO: skip test on loong64 until go toolchain supported loong64. // "linux/loong64", // asm10.s - "linux/mips64", // asm5.s - "linux/s390x", // asm6.s - "linux/ppc64", // asm7.s - "linux/mips", // asm8.s, - "js/wasm", // asm9.s + "linux/mips64", // asm5.s + "linux/s390x", // asm6.s + "linux/ppc64", // asm7.s + "linux/mips", // asm8.s, + "js/wasm", // asm9.s + "linux/riscv64", // asm11.s } func Test(t *testing.T) { diff --git a/go/analysis/passes/asmdecl/testdata/src/a/asm11.s b/go/analysis/passes/asmdecl/testdata/src/a/asm11.s new file mode 100644 index 00000000000..e81e8ee179f --- /dev/null +++ b/go/analysis/passes/asmdecl/testdata/src/a/asm11.s @@ -0,0 +1,13 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build riscv64 + +// writing to result in ABIInternal function +TEXT ·returnABIInternal(SB), NOSPLIT, $8 + MOV $123, X10 + RET +TEXT ·returnmissingABIInternal(SB), NOSPLIT, $8 + MOV $123, X20 + RET // want `RET without writing to result register` From 1928cea0f0cc5f74e1840d00b5c809f7ed73402b Mon Sep 17 00:00:00 2001 From: Tim King Date: Wed, 12 Oct 2022 14:24:32 -0700 Subject: [PATCH 367/723] go/ssa: emit field and index lvals on demand Adds a new lazyAddress construct. This is the same as an *address except it emits a FieldAddr selection, Field selection, or IndexAddr on demand. This fixes issues with ordering on assignment statements. For example, x.f = e panics on x being nil in phase 2 of assignment statements. This change delays the introduction of the FieldAddr for x.f until it is used instead of as a side effect of (*builder).addr. The nil deref panic is from FieldAddr is now after side-effects of evaluating x and e but before the assignment to x.f. Fixes golang/go#55086 Change-Id: I0f215b209de5c5fd319aef3af677e071dbd168f8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/442655 Reviewed-by: Alan Donovan Reviewed-by: Zvonimir Pavlinovic --- .../vta/testdata/src/function_alias.go | 44 +++--- go/ssa/builder.go | 32 +++-- go/ssa/interp/interp_test.go | 1 + .../interp/testdata/fixedbugs/issue55086.go | 132 ++++++++++++++++++ go/ssa/lvalue.go | 36 +++++ 5 files changed, 212 insertions(+), 33 deletions(-) create mode 100644 go/ssa/interp/testdata/fixedbugs/issue55086.go diff --git a/go/callgraph/vta/testdata/src/function_alias.go b/go/callgraph/vta/testdata/src/function_alias.go index b38e0e00d69..0a8dffe79d4 100644 --- a/go/callgraph/vta/testdata/src/function_alias.go +++ b/go/callgraph/vta/testdata/src/function_alias.go @@ -33,42 +33,42 @@ func Baz(f func()) { // t2 = *t1 // *t2 = Baz$1 // t3 = local A (a) -// t4 = &t3.foo [#0] -// t5 = *t1 -// t6 = *t5 -// *t4 = t6 +// t4 = *t1 +// t5 = *t4 +// t6 = &t3.foo [#0] +// *t6 = t5 // t7 = &t3.foo [#0] // t8 = *t7 // t9 = t8() -// t10 = &t3.do [#1] *Doer -// t11 = &t3.foo [#0] *func() -// t12 = *t11 func() -// t13 = changetype Doer <- func() (t12) Doer -// *t10 = t13 +// t10 = &t3.foo [#0] *func() +// t11 = *t10 func() +// t12 = &t3.do [#1] *Doer +// t13 = changetype Doer <- func() (t11) Doer +// *t12 = t13 // t14 = &t3.do [#1] *Doer // t15 = *t14 Doer // t16 = t15() () // Flow chain showing that Baz$1 reaches t8(): -// Baz$1 -> t2 <-> PtrFunction(func()) <-> t5 -> t6 -> t4 <-> Field(testdata.A:foo) <-> t7 -> t8 +// Baz$1 -> t2 <-> PtrFunction(func()) <-> t4 -> t5 -> t6 <-> Field(testdata.A:foo) <-> t7 -> t8 // Flow chain showing that Baz$1 reaches t15(): -// Field(testdata.A:foo) <-> t11 -> t12 -> t13 -> t10 <-> Field(testdata.A:do) <-> t14 -> t15 +// Field(testdata.A:foo) <-> t10 -> t11 -> t13 -> t12 <-> Field(testdata.A:do) <-> t14 -> t15 // WANT: // Local(f) -> Local(t0) // Local(t0) -> PtrFunction(func()) // Function(Baz$1) -> Local(t2) -// PtrFunction(func()) -> Local(t0), Local(t2), Local(t5) +// PtrFunction(func()) -> Local(t0), Local(t2), Local(t4) // Local(t2) -> PtrFunction(func()) -// Local(t4) -> Field(testdata.A:foo) -// Local(t5) -> Local(t6), PtrFunction(func()) -// Local(t6) -> Local(t4) +// Local(t6) -> Field(testdata.A:foo) +// Local(t4) -> Local(t5), PtrFunction(func()) +// Local(t5) -> Local(t6) // Local(t7) -> Field(testdata.A:foo), Local(t8) -// Field(testdata.A:foo) -> Local(t11), Local(t4), Local(t7) -// Local(t4) -> Field(testdata.A:foo) -// Field(testdata.A:do) -> Local(t10), Local(t14) -// Local(t10) -> Field(testdata.A:do) -// Local(t11) -> Field(testdata.A:foo), Local(t12) -// Local(t12) -> Local(t13) -// Local(t13) -> Local(t10) +// Field(testdata.A:foo) -> Local(t10), Local(t6), Local(t7) +// Local(t6) -> Field(testdata.A:foo) +// Field(testdata.A:do) -> Local(t12), Local(t14) +// Local(t12) -> Field(testdata.A:do) +// Local(t10) -> Field(testdata.A:foo), Local(t11) +// Local(t11) -> Local(t13) +// Local(t13) -> Local(t12) // Local(t14) -> Field(testdata.A:do), Local(t15) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 23674c3d0d2..8ec8f6e310b 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -453,12 +453,16 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { } wantAddr := true v := b.receiver(fn, e.X, wantAddr, escaping, sel) - last := len(sel.index) - 1 - return &address{ - addr: emitFieldSelection(fn, v, sel.index[last], true, e.Sel), - pos: e.Sel.Pos(), - expr: e.Sel, + index := sel.index[len(sel.index)-1] + fld := typeparams.CoreType(deref(v.Type())).(*types.Struct).Field(index) + + // Due to the two phases of resolving AssignStmt, a panic from x.f = p() + // when x is nil is required to come after the side-effects of + // evaluating x and p(). + emit := func(fn *Function) Value { + return emitFieldSelection(fn, v, index, true, e.Sel) } + return &lazyAddress{addr: emit, t: fld.Type(), pos: e.Sel.Pos(), expr: e.Sel} case *ast.IndexExpr: var x Value @@ -487,13 +491,19 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { if isUntyped(index.Type()) { index = emitConv(fn, index, tInt) } - v := &IndexAddr{ - X: x, - Index: index, + // Due to the two phases of resolving AssignStmt, a panic from x[i] = p() + // when x is nil or i is out-of-bounds is required to come after the + // side-effects of evaluating x, i and p(). + emit := func(fn *Function) Value { + v := &IndexAddr{ + X: x, + Index: index, + } + v.setPos(e.Lbrack) + v.setType(et) + return fn.emit(v) } - v.setPos(e.Lbrack) - v.setType(et) - return &address{addr: fn.emit(v), pos: e.Lbrack, expr: e} + return &lazyAddress{addr: emit, t: deref(et), pos: e.Lbrack, expr: e} case *ast.StarExpr: return &address{addr: b.expr(fn, e.X), pos: e.Star, expr: e} diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go index a0acf2f968a..51a74015c95 100644 --- a/go/ssa/interp/interp_test.go +++ b/go/ssa/interp/interp_test.go @@ -127,6 +127,7 @@ var testdataTests = []string{ "width32.go", "fixedbugs/issue52342.go", + "fixedbugs/issue55086.go", } func init() { diff --git a/go/ssa/interp/testdata/fixedbugs/issue55086.go b/go/ssa/interp/testdata/fixedbugs/issue55086.go new file mode 100644 index 00000000000..84c81e91a26 --- /dev/null +++ b/go/ssa/interp/testdata/fixedbugs/issue55086.go @@ -0,0 +1,132 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +func a() (r string) { + s := "initial" + var p *struct{ i int } + defer func() { + recover() + r = s + }() + + s, p.i = "set", 2 // s must be set before p.i panics + return "unreachable" +} + +func b() (r string) { + s := "initial" + fn := func() []int { panic("") } + defer func() { + recover() + r = s + }() + + s, fn()[0] = "set", 2 // fn() panics before any assignment occurs + return "unreachable" +} + +func c() (r string) { + s := "initial" + var p map[int]int + defer func() { + recover() + r = s + }() + + s, p[0] = "set", 2 //s must be set before p[0] index panics" + return "unreachable" +} + +func d() (r string) { + s := "initial" + var p map[int]int + defer func() { + recover() + r = s + }() + fn := func() int { panic("") } + + s, p[0] = "set", fn() // fn() panics before s is set + return "unreachable" +} + +func e() (r string) { + s := "initial" + p := map[int]int{} + defer func() { + recover() + r = s + }() + fn := func() int { panic("") } + + s, p[fn()] = "set", 0 // fn() panics before any assignment occurs + return "unreachable" +} + +func f() (r string) { + s := "initial" + p := []int{} + defer func() { + recover() + r = s + }() + + s, p[1] = "set", 0 // p[1] panics after s is set + return "unreachable" +} + +func g() (r string) { + s := "initial" + p := map[any]any{} + defer func() { + recover() + r = s + }() + var i any = func() {} + s, p[i] = "set", 0 // p[i] panics after s is set + return "unreachable" +} + +func h() (r string) { + fail := false + defer func() { + recover() + if fail { + r = "fail" + } else { + r = "success" + } + }() + + type T struct{ f int } + var p *struct{ *T } + + // The implicit "p.T" operand should be evaluated in phase 1 (and panic), + // before the "fail = true" assignment in phase 2. + fail, p.f = true, 0 + return "unreachable" +} + +func main() { + for _, test := range []struct { + fn func() string + want string + desc string + }{ + {a, "set", "s must be set before p.i panics"}, + {b, "initial", "p() panics before s is set"}, + {c, "set", "s must be set before p[0] index panics"}, + {d, "initial", "fn() panics before s is set"}, + {e, "initial", "fn() panics before s is set"}, + {f, "set", "p[1] panics after s is set"}, + {g, "set", "p[i] panics after s is set"}, + {h, "success", "p.T panics before fail is set"}, + } { + if test.fn() != test.want { + panic(test.desc) + } + } +} diff --git a/go/ssa/lvalue.go b/go/ssa/lvalue.go index 64262def8b2..455b1e50fa4 100644 --- a/go/ssa/lvalue.go +++ b/go/ssa/lvalue.go @@ -93,6 +93,42 @@ func (e *element) typ() types.Type { return e.t } +// A lazyAddress is an lvalue whose address is the result of an instruction. +// These work like an *address except a new address.address() Value +// is created on each load, store and address call. +// A lazyAddress can be used to control when a side effect (nil pointer +// dereference, index out of bounds) of using a location happens. +type lazyAddress struct { + addr func(fn *Function) Value // emit to fn the computation of the address + t types.Type // type of the location + pos token.Pos // source position + expr ast.Expr // source syntax of the value (not address) [debug mode] +} + +func (l *lazyAddress) load(fn *Function) Value { + load := emitLoad(fn, l.addr(fn)) + load.pos = l.pos + return load +} + +func (l *lazyAddress) store(fn *Function, v Value) { + store := emitStore(fn, l.addr(fn), v, l.pos) + if l.expr != nil { + // store.Val is v, converted for assignability. + emitDebugRef(fn, l.expr, store.Val, false) + } +} + +func (l *lazyAddress) address(fn *Function) Value { + addr := l.addr(fn) + if l.expr != nil { + emitDebugRef(fn, l.expr, addr, true) + } + return addr +} + +func (l *lazyAddress) typ() types.Type { return l.t } + // A blank is a dummy variable whose name is "_". // It is not reified: loads are illegal and stores are ignored. type blank struct{} From 2e0ca3aded9465405194f01b5523bed9f49f16d8 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 24 Oct 2022 16:51:25 -0400 Subject: [PATCH 368/723] go/internal/gcimporter: fix bug in struct iexport The iexport function was emitting the wrong implicit qualifying package name for unexported struct fields. The logic was deceptively similar to that in the compiler, and that was the bug: the compiler records which package a struct{...} syntax node appears in, and uses that (correctly) as the qualifier for unexported fields; but go/types only records a package in each Object, such as the field Vars, which led the author of the previous code to use the current package, not the struct's package, as the implicit qualifier. The solution is to use the package of any field; they should all be the same. (Unlike interfaces, where embedding is flattened out, leading to interface types in which method Func objects may belong to different packages, struct embedding is not flattened out, so all field Vars belong to the same package in which the struct was declared.) Also, a regression test. Thanks to Rob Findley for identifying the cause. I don't understand how this hasn't shown up sooner, since the test case is far from obscure. Change-Id: I0a6c58a566b87a148827fb0ab4655a020806c31a Reviewed-on: https://go-review.googlesource.com/c/tools/+/445097 gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Robert Griesemer Run-TryBot: Alan Donovan --- go/internal/gcimporter/iexport.go | 9 +++-- go/internal/gcimporter/iexport_test.go | 48 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/go/internal/gcimporter/iexport.go b/go/internal/gcimporter/iexport.go index 9a4ff329e12..db2753217a3 100644 --- a/go/internal/gcimporter/iexport.go +++ b/go/internal/gcimporter/iexport.go @@ -602,14 +602,17 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { case *types.Struct: w.startType(structType) - w.setPkg(pkg, true) - n := t.NumFields() + if n > 0 { + w.setPkg(t.Field(0).Pkg(), true) // qualifying package for field objects + } else { + w.setPkg(pkg, true) + } w.uint64(uint64(n)) for i := 0; i < n; i++ { f := t.Field(i) w.pos(f.Pos()) - w.string(f.Name()) + w.string(f.Name()) // unexported fields implicitly qualified by prior setPkg w.typ(f.Type(), pkg) w.bool(f.Anonymous()) w.string(t.Tag(i)) // note (or tag) diff --git a/go/internal/gcimporter/iexport_test.go b/go/internal/gcimporter/iexport_test.go index f0e83e519fe..899c9af7a48 100644 --- a/go/internal/gcimporter/iexport_test.go +++ b/go/internal/gcimporter/iexport_test.go @@ -30,6 +30,7 @@ import ( "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/buildutil" + "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/internal/gcimporter" "golang.org/x/tools/go/loader" "golang.org/x/tools/internal/typeparams/genericfeatures" @@ -403,3 +404,50 @@ func valueToRat(x constant.Value) *big.Rat { } return new(big.Rat).SetInt(new(big.Int).SetBytes(bytes)) } + +// This is a regression test for a bug in iexport of types.Struct: +// unexported fields were losing their implicit package qualifier. +func TestUnexportedStructFields(t *testing.T) { + fset := token.NewFileSet() + export := make(map[string][]byte) + + // process parses and type-checks a single-file + // package and saves its export data. + process := func(path, content string) { + syntax, err := parser.ParseFile(fset, path+"/x.go", content, 0) + if err != nil { + t.Fatal(err) + } + packages := make(map[string]*types.Package) // keys are package paths + cfg := &types.Config{ + Importer: importerFunc(func(path string) (*types.Package, error) { + data, ok := export[path] + if !ok { + return nil, fmt.Errorf("missing export data for %s", path) + } + return gcexportdata.Read(bytes.NewReader(data), fset, packages, path) + }), + } + pkg := types.NewPackage(path, syntax.Name.Name) + check := types.NewChecker(cfg, fset, pkg, nil) + if err := check.Files([]*ast.File{syntax}); err != nil { + t.Fatal(err) + } + var out bytes.Buffer + if err := gcexportdata.Write(&out, fset, pkg); err != nil { + t.Fatal(err) + } + export[path] = out.Bytes() + } + + // Historically this led to a spurious error: + // "cannot convert a.M (variable of type a.MyTime) to type time.Time" + // because the private fields of Time and MyTime were not identical. + process("time", `package time; type Time struct { x, y int }`) + process("a", `package a; import "time"; type MyTime time.Time; var M MyTime`) + process("b", `package b; import ("a"; "time"); var _ = time.Time(a.M)`) +} + +type importerFunc func(path string) (*types.Package, error) + +func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } From d6511e5e9f58a975b5568d11282c30bc70bdc2e5 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 21 Oct 2022 13:13:46 -0400 Subject: [PATCH 369/723] internal/facts: share go/analysis/internal/facts with gopls This change moves the facts package so that it can be reused by gopls. It remains a tools-internal API. A forthcoming reimplementation of gopls's analysis driver will make use of the new packages and the features mentioned below. Also: - change parameter of read() callback from 'path string' to *Package. - use NewDecoder().Decode() to amortize import-map computation across calls. Change-Id: Id10cd02c0c241353524d568d5299d81457f571f8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/444796 Reviewed-by: Tim King Reviewed-by: Robert Findley Run-TryBot: Alan Donovan --- go/analysis/unitchecker/unitchecker.go | 8 ++--- .../internal => internal}/facts/facts.go | 35 +++++++++++++------ .../internal => internal}/facts/facts_test.go | 10 +++--- .../internal => internal}/facts/imports.go | 3 ++ 4 files changed, 36 insertions(+), 20 deletions(-) rename {go/analysis/internal => internal}/facts/facts.go (91%) rename {go/analysis/internal => internal}/facts/facts_test.go (96%) rename {go/analysis/internal => internal}/facts/imports.go (95%) diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go index 9827b57f529..d9c8f11cdd4 100644 --- a/go/analysis/unitchecker/unitchecker.go +++ b/go/analysis/unitchecker/unitchecker.go @@ -50,7 +50,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/internal/analysisflags" - "golang.org/x/tools/go/analysis/internal/facts" + "golang.org/x/tools/internal/facts" "golang.org/x/tools/internal/typeparams" ) @@ -287,13 +287,13 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re analyzers = filtered // Read facts from imported packages. - read := func(path string) ([]byte, error) { - if vetx, ok := cfg.PackageVetx[path]; ok { + read := func(imp *types.Package) ([]byte, error) { + if vetx, ok := cfg.PackageVetx[imp.Path()]; ok { return ioutil.ReadFile(vetx) } return nil, nil // no .vetx file, no facts } - facts, err := facts.Decode(pkg, read) + facts, err := facts.NewDecoder(pkg).Decode(read) if err != nil { return nil, err } diff --git a/go/analysis/internal/facts/facts.go b/internal/facts/facts.go similarity index 91% rename from go/analysis/internal/facts/facts.go rename to internal/facts/facts.go index 006abab84ef..81df45161a8 100644 --- a/go/analysis/internal/facts/facts.go +++ b/internal/facts/facts.go @@ -152,6 +152,23 @@ type gobFact struct { Fact analysis.Fact // type and value of user-defined Fact } +// A Decoder decodes the facts from the direct imports of the package +// provided to NewEncoder. A single decoder may be used to decode +// multiple fact sets (e.g. each for a different set of fact types) +// for the same package. Each call to Decode returns an independent +// fact set. +type Decoder struct { + pkg *types.Package + packages map[string]*types.Package +} + +// NewDecoder returns a fact decoder for the specified package. +func NewDecoder(pkg *types.Package) *Decoder { + // Compute the import map for this package. + // See the package doc comment. + return &Decoder{pkg, importMap(pkg.Imports())} +} + // Decode decodes all the facts relevant to the analysis of package pkg. // The read function reads serialized fact data from an external source // for one of of pkg's direct imports. The empty file is a valid @@ -159,28 +176,24 @@ type gobFact struct { // // It is the caller's responsibility to call gob.Register on all // necessary fact types. -func Decode(pkg *types.Package, read func(packagePath string) ([]byte, error)) (*Set, error) { - // Compute the import map for this package. - // See the package doc comment. - packages := importMap(pkg.Imports()) - +func (d *Decoder) Decode(read func(*types.Package) ([]byte, error)) (*Set, error) { // Read facts from imported packages. // Facts may describe indirectly imported packages, or their objects. m := make(map[key]analysis.Fact) // one big bucket - for _, imp := range pkg.Imports() { + for _, imp := range d.pkg.Imports() { logf := func(format string, args ...interface{}) { if debug { prefix := fmt.Sprintf("in %s, importing %s: ", - pkg.Path(), imp.Path()) + d.pkg.Path(), imp.Path()) log.Print(prefix, fmt.Sprintf(format, args...)) } } // Read the gob-encoded facts. - data, err := read(imp.Path()) + data, err := read(imp) if err != nil { return nil, fmt.Errorf("in %s, can't import facts for package %q: %v", - pkg.Path(), imp.Path(), err) + d.pkg.Path(), imp.Path(), err) } if len(data) == 0 { continue // no facts @@ -195,7 +208,7 @@ func Decode(pkg *types.Package, read func(packagePath string) ([]byte, error)) ( // Parse each one into a key and a Fact. for _, f := range gobFacts { - factPkg := packages[f.PkgPath] + factPkg := d.packages[f.PkgPath] if factPkg == nil { // Fact relates to a dependency that was // unused in this translation unit. Skip. @@ -222,7 +235,7 @@ func Decode(pkg *types.Package, read func(packagePath string) ([]byte, error)) ( } } - return &Set{pkg: pkg, m: m}, nil + return &Set{pkg: d.pkg, m: m}, nil } // Encode encodes a set of facts to a memory buffer. diff --git a/go/analysis/internal/facts/facts_test.go b/internal/facts/facts_test.go similarity index 96% rename from go/analysis/internal/facts/facts_test.go rename to internal/facts/facts_test.go index c8379c58aa8..5c7b12ef1d4 100644 --- a/go/analysis/internal/facts/facts_test.go +++ b/internal/facts/facts_test.go @@ -14,8 +14,8 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/go/analysis/internal/facts" "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/facts" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" ) @@ -216,7 +216,7 @@ type pkgLookups struct { // are passed during analysis. It operates on a group of Go file contents. Then // for each in tests it does the following: // 1. loads and type checks the package, -// 2. calls facts.Decode to loads the facts exported by its imports, +// 2. calls (*facts.Decoder).Decode to load the facts exported by its imports, // 3. exports a myFact Fact for all of package level objects, // 4. For each lookup for the current package: // 4.a) lookup the types.Object for an Go source expression in the curent package @@ -239,7 +239,7 @@ func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) // factmap represents the passing of encoded facts from one // package to another. In practice one would use the file system. factmap := make(map[string][]byte) - read := func(path string) ([]byte, error) { return factmap[path], nil } + read := func(imp *types.Package) ([]byte, error) { return factmap[imp.Path()], nil } // Analyze packages in order, look up various objects accessible within // each package, and see if they have a fact. The "analysis" exports a @@ -255,7 +255,7 @@ func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) } // decode - facts, err := facts.Decode(pkg, read) + facts, err := facts.NewDecoder(pkg).Decode(read) if err != nil { t.Fatalf("Decode failed: %v", err) } @@ -357,7 +357,7 @@ func TestFactFilter(t *testing.T) { } obj := pkg.Scope().Lookup("A") - s, err := facts.Decode(pkg, func(string) ([]byte, error) { return nil, nil }) + s, err := facts.NewDecoder(pkg).Decode(func(*types.Package) ([]byte, error) { return nil, nil }) if err != nil { t.Fatal(err) } diff --git a/go/analysis/internal/facts/imports.go b/internal/facts/imports.go similarity index 95% rename from go/analysis/internal/facts/imports.go rename to internal/facts/imports.go index 8a5553e2e9b..a3aa90dd1c5 100644 --- a/go/analysis/internal/facts/imports.go +++ b/internal/facts/imports.go @@ -20,6 +20,9 @@ import ( // // Packages in the map that are only indirectly imported may be // incomplete (!pkg.Complete()). +// +// TODO(adonovan): opt: compute this information more efficiently +// by obtaining it from the internals of the gcexportdata decoder. func importMap(imports []*types.Package) map[string]*types.Package { objects := make(map[types.Object]bool) packages := make(map[string]*types.Package) From 121f889bbcde58dff814df66c9c1d1bac935c0f6 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Fri, 21 Oct 2022 17:29:55 -0400 Subject: [PATCH 370/723] gopls/internal/lsp/mod: merge vuln diagnostics to one, and add a hover When a module contains multiple vulnerabilities, previously gopls published one diagnostic message for each vulnerability. This change modifies this behavior to publish only one vuln diagnostic for each module. This will make the PROBLEMS panel more concise and readable. However, the information about each vulnerability finding is useful, so we supplement this diagnostics by sending a hover message in the module require line. An added benefit of this approach is that, unlike the Diagnostics, VS Code supports rich text rendering for Hover messages. So we can use markdown to add links and necessary highlighting. Before this change, go.mod require hover messages (e.g. go mod why result) were associated only with the module path part, excluding the version string part. But for vulnerability information hover message, I think it is better to be applied to the entire module require line (both module path & version) because they are information about the specific module/version. Currently LSP hover returns only one hover, so we cannot use this different range only to the vulnerability information hover. Thus, one side effect of this change is that the module info hover message will be also shown to the version part of each require statement. Change-Id: Iccacd19fdebadc4768abcad8a218bbae14f9d7e2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/444798 Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot --- gopls/internal/lsp/mod/diagnostics.go | 77 +++++++++++----- gopls/internal/lsp/mod/hover.go | 110 ++++++++++++++++++++--- gopls/internal/regtest/misc/vuln_test.go | 30 ++++--- 3 files changed, 175 insertions(+), 42 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 4b4f421ece1..829a03f9114 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -195,29 +195,49 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, vs := snapshot.View().Vulnerabilities(fh.URI()) // TODO(suzmue): should we just store the vulnerabilities like this? - vulns := make(map[string][]govulncheck.Vuln) + affecting := make(map[string][]govulncheck.Vuln) + nonaffecting := make(map[string][]govulncheck.Vuln) for _, v := range vs { - vulns[v.ModPath] = append(vulns[v.ModPath], v) + if len(v.Trace) > 0 { + affecting[v.ModPath] = append(affecting[v.ModPath], v) + } else { + nonaffecting[v.ModPath] = append(nonaffecting[v.ModPath], v) + } } for _, req := range pm.File.Require { - vulnList, ok := vulns[req.Mod.Path] - if !ok { + affectingVulns, ok := affecting[req.Mod.Path] + nonaffectingVulns, ok2 := nonaffecting[req.Mod.Path] + if !ok && !ok2 { continue } rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) if err != nil { return nil, err } - for _, v := range vulnList { + // Map affecting vulns to 'warning' level diagnostics, + // others to 'info' level diagnostics. + // Fixes will include only the upgrades for warning level diagnostics. + var fixes []source.SuggestedFix + var warning, info []string + for _, v := range nonaffectingVulns { + // Only show the diagnostic if the vulnerability was calculated + // for the module at the current version. + if semver.IsValid(v.FoundIn) && semver.Compare(req.Mod.Version, v.FoundIn) != 0 { + continue + } + info = append(info, v.OSV.ID) + } + for _, v := range affectingVulns { // Only show the diagnostic if the vulnerability was calculated // for the module at the current version. if semver.IsValid(v.FoundIn) && semver.Compare(req.Mod.Version, v.FoundIn) != 0 { continue } + warning = append(warning, v.OSV.ID) // Upgrade to the exact version we offer the user, not the most recent. // TODO(hakim): Produce fixes only for affecting vulnerabilities (if len(v.Trace) > 0) - var fixes []source.SuggestedFix + if fixedVersion := v.FixedIn; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { cmd, err := getUpgradeCodeAction(fh, req, fixedVersion) if err != nil { @@ -235,24 +255,41 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, source.SuggestedFixFromCommand(latest, protocol.QuickFix), } } + } - severity := protocol.SeverityInformation - if len(v.Trace) > 0 { - severity = protocol.SeverityWarning - } + if len(warning) == 0 && len(info) == 0 { + return nil, nil + } + severity := protocol.SeverityInformation + if len(warning) > 0 { + severity = protocol.SeverityWarning + } - vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: severity, - Source: source.Vulncheck, - Code: v.OSV.ID, - CodeHref: href(v.OSV), - Message: formatMessage(v), - SuggestedFixes: fixes, - }) + sort.Strings(warning) + sort.Strings(info) + + var b strings.Builder + if len(warning) == 1 { + fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", req.Mod.Path, warning[0]) + } else { + fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", ")) + } + if len(warning) == 0 { + if len(info) == 1 { + fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", req.Mod.Path, info[0]) + } else { + fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", req.Mod.Path, strings.Join(info, ", ")) + } } + vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: severity, + Source: source.Vulncheck, + Message: b.String(), + SuggestedFixes: fixes, + }) } return vulnDiagnostics, nil diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 29812749df3..b1dfe3a3fb5 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -8,9 +8,11 @@ import ( "bytes" "context" "fmt" + "sort" "strings" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" @@ -55,18 +57,22 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Shift the start position to the location of the // dependency within the require statement. - startPos, endPos = s+i, s+i+len(dep) + startPos, endPos = s+i, e if startPos <= offset && offset <= endPos { req = r break } } + // TODO(hyangah): find position for info about vulnerabilities in Go // The cursor position is not on a require statement. if req == nil { return nil, nil } + // Get the vulnerability info. + affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI()), req) + // Get the `go mod why` results for the given file. why, err := snapshot.ModWhy(ctx, fh) if err != nil { @@ -78,38 +84,120 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Get the range to highlight for the hover. + // TODO(hyangah): adjust the hover range to include the version number + // to match the diagnostics' range. rng, err := pm.Mapper.OffsetRange(startPos, endPos) if err != nil { return nil, err } - if err != nil { - return nil, err - } options := snapshot.View().Options() isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path) + header := formatHeader(req.Mod.Path, options) explanation = formatExplanation(explanation, req, options, isPrivate) + vulns := formatVulnerabilities(affecting, nonaffecting, options) + return &protocol.Hover{ Contents: protocol.MarkupContent{ Kind: options.PreferredContentFormat, - Value: explanation, + Value: header + vulns + explanation, }, Range: rng, }, nil } -func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string { - text = strings.TrimSuffix(text, "\n") - splt := strings.Split(text, "\n") - length := len(splt) - +func formatHeader(modpath string, options *source.Options) string { var b strings.Builder // Write the heading as an H3. - b.WriteString("##" + splt[0]) + b.WriteString("#### " + modpath) if options.PreferredContentFormat == protocol.Markdown { b.WriteString("\n\n") } else { b.WriteRune('\n') } + return b.String() +} + +func compareVuln(i, j govulncheck.Vuln) bool { + if i.OSV.ID == j.OSV.ID { + return i.PkgPath < j.PkgPath + } + return i.OSV.ID < j.OSV.ID +} + +func lookupVulns(vulns []govulncheck.Vuln, req *modfile.Require) (affecting, nonaffecting []govulncheck.Vuln) { + modpath, modversion := req.Mod.Path, req.Mod.Version + + var info, warning []govulncheck.Vuln + for _, vuln := range vulns { + if vuln.ModPath != modpath || vuln.FoundIn != modversion { + continue + } + if len(vuln.Trace) == 0 { + info = append(info, vuln) + } else { + warning = append(warning, vuln) + } + } + sort.Slice(info, func(i, j int) bool { return compareVuln(info[i], info[j]) }) + sort.Slice(warning, func(i, j int) bool { return compareVuln(warning[i], warning[j]) }) + return warning, info +} + +func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options *source.Options) string { + if len(affecting) == 0 && len(nonaffecting) == 0 { + return "" + } + + // TODO(hyangah): can we use go templates to generate hover messages? + // Then, we can use a different template for markdown case. + useMarkdown := options.PreferredContentFormat == protocol.Markdown + + var b strings.Builder + + if len(affecting) > 0 { + // TODO(hyangah): make the message more eyecatching (icon/codicon/color) + if len(affecting) == 1 { + b.WriteString(fmt.Sprintf("\n**WARNING:** Found %d reachable vulnerability.\n", len(affecting))) + } else { + b.WriteString(fmt.Sprintf("\n**WARNING:** Found %d reachable vulnerabilities.\n", len(affecting))) + } + } + for _, v := range affecting { + fix := "No fix is available." + if v.FixedIn != "" { + fix = "Fixed in " + v.FixedIn + "." + } + + if useMarkdown { + fmt.Fprintf(&b, " - [**%v**](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) + } else { + fmt.Fprintf(&b, " - [%v] %v (%v) %v\n", v.OSV.ID, formatMessage(v), href(v.OSV), fix) + } + } + if len(nonaffecting) > 0 { + fmt.Fprintf(&b, "The project imports packages affected by the following vulnerabilities, but does not use vulnerable symbols.") + } + for _, v := range nonaffecting { + fix := "No fix is available." + if v.FixedIn != "" { + fix = "Fixed in " + v.FixedIn + "." + } + if useMarkdown { + fmt.Fprintf(&b, " - [%v](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) + } else { + fmt.Fprintf(&b, " - [%v] %v %v (%v)\n", v.OSV.ID, formatMessage(v), fix, href(v.OSV)) + } + } + b.WriteString("\n") + return b.String() +} + +func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string { + text = strings.TrimSuffix(text, "\n") + splt := strings.Split(text, "\n") + length := len(splt) + + var b strings.Builder // If the explanation is 2 lines, then it is of the form: // # golang.org/x/text/encoding diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index ecebfb52202..2afef9df78a 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -9,6 +9,7 @@ package misc import ( "context" + "strings" "testing" "golang.org/x/tools/gopls/internal/lsp/command" @@ -341,40 +342,36 @@ func TestRunVulncheckExp(t *testing.T) { // codeActions is a list titles of code actions that we get with context // diagnostics. codeActions []string + // hover message is the list of expected hover message parts for this go.mod require line. + // all parts must appear in the hover message. + hover []string }{ "golang.org/amod": { applyAction: "Upgrade to v1.0.4", diagnostics: []diagnostic{ { - msg: "golang.org/amod has a known vulnerability: vuln in amod", + msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.", severity: protocol.SeverityWarning, codeActions: []string{ "Upgrade to latest", "Upgrade to v1.0.4", }, }, - { - msg: "golang.org/amod has a known vulnerability: unaffecting vulnerability", - severity: protocol.SeverityInformation, - codeActions: []string{ - "Upgrade to latest", - "Upgrade to v1.0.6", - }, - }, }, codeActions: []string{ "Upgrade to latest", - "Upgrade to v1.0.6", "Upgrade to v1.0.4", }, + hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, }, "golang.org/bmod": { diagnostics: []diagnostic{ { - msg: "golang.org/bmod has a known vulnerability: vuln in bmod\n\nThis is a long description of this vulnerability.", + msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.", severity: protocol.SeverityWarning, }, }, + hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."}, }, } @@ -405,6 +402,17 @@ func TestRunVulncheckExp(t *testing.T) { t.Errorf("code actions for %q do not match, expected %v, got %v\n", w.msg, w.codeActions, gotActions) continue } + + // Check that useful info is supplemented as hover. + if len(want.hover) > 0 { + hover, _ := env.Hover("go.mod", pos) + for _, part := range want.hover { + if !strings.Contains(hover.Value, part) { + t.Errorf("hover contents for %q do not match, expected %v, got %v\n", w.msg, strings.Join(want.hover, ","), hover.Value) + break + } + } + } } // Check that the actions we get when including all diagnostics at a location return the same result From 3e1371fd13da29af15644a44d476217fb0cc26b0 Mon Sep 17 00:00:00 2001 From: pjw Date: Fri, 14 Oct 2022 09:26:03 -0400 Subject: [PATCH 371/723] gopls/internal: start on LSP stub generator in Go. This is the first in a series of CLs implementing the new stub generator. The code is intended to reproduce exactly the current state of the generated code. This CL has the final file layout, but primarily consists of the parsing of the specification. The LSP maintainers now provide a .json file describing the messages and types used in the protocol. The new code in this CL, written in Go, parses this file and generates Go definitions. The tests need to be run by hand because the metaModel.json file is not available to the presubmit tests. Related golang/go#52969 Change-Id: Id2fc58c973a92c39ba98c936f2af03b1c40ada44 Reviewed-on: https://go-review.googlesource.com/c/tools/+/443055 Reviewed-by: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Peter Weinberger --- .../internal/lsp/protocol/generate/compare.go | 10 + gopls/internal/lsp/protocol/generate/data.go | 104 +++++++++++ gopls/internal/lsp/protocol/generate/doc.go | 32 ++++ .../lsp/protocol/generate/generate.go | 10 + gopls/internal/lsp/protocol/generate/main.go | 93 ++++++++++ .../lsp/protocol/generate/main_test.go | 122 ++++++++++++ .../internal/lsp/protocol/generate/naming.go | 64 +++++++ .../internal/lsp/protocol/generate/output.go | 10 + gopls/internal/lsp/protocol/generate/parse.go | 174 ++++++++++++++++++ gopls/internal/lsp/protocol/generate/types.go | 173 +++++++++++++++++ .../lsp/protocol/generate/utilities.go | 55 ++++++ 11 files changed, 847 insertions(+) create mode 100644 gopls/internal/lsp/protocol/generate/compare.go create mode 100644 gopls/internal/lsp/protocol/generate/data.go create mode 100644 gopls/internal/lsp/protocol/generate/doc.go create mode 100644 gopls/internal/lsp/protocol/generate/generate.go create mode 100644 gopls/internal/lsp/protocol/generate/main.go create mode 100644 gopls/internal/lsp/protocol/generate/main_test.go create mode 100644 gopls/internal/lsp/protocol/generate/naming.go create mode 100644 gopls/internal/lsp/protocol/generate/output.go create mode 100644 gopls/internal/lsp/protocol/generate/parse.go create mode 100644 gopls/internal/lsp/protocol/generate/types.go create mode 100644 gopls/internal/lsp/protocol/generate/utilities.go diff --git a/gopls/internal/lsp/protocol/generate/compare.go b/gopls/internal/lsp/protocol/generate/compare.go new file mode 100644 index 00000000000..d341307821d --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/compare.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// compare the generated files in two directories diff --git a/gopls/internal/lsp/protocol/generate/data.go b/gopls/internal/lsp/protocol/generate/data.go new file mode 100644 index 00000000000..435f594bcf7 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/data.go @@ -0,0 +1,104 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// various data tables + +// methodNames is a map from the method to the name of the function that handles it +var methodNames = map[string]string{ + "$/cancelRequest": "CancelRequest", + "$/logTrace": "LogTrace", + "$/progress": "Progress", + "$/setTrace": "SetTrace", + "callHierarchy/incomingCalls": "IncomingCalls", + "callHierarchy/outgoingCalls": "OutgoingCalls", + "client/registerCapability": "RegisterCapability", + "client/unregisterCapability": "UnregisterCapability", + "codeAction/resolve": "ResolveCodeAction", + "codeLens/resolve": "ResolveCodeLens", + "completionItem/resolve": "ResolveCompletionItem", + "documentLink/resolve": "ResolveDocumentLink", + "exit": "Exit", + "initialize": "Initialize", + "initialized": "Initialized", + "inlayHint/resolve": "Resolve", + "notebookDocument/didChange": "DidChangeNotebookDocument", + "notebookDocument/didClose": "DidCloseNotebookDocument", + "notebookDocument/didOpen": "DidOpenNotebookDocument", + "notebookDocument/didSave": "DidSaveNotebookDocument", + "shutdown": "Shutdown", + "telemetry/event": "Event", + "textDocument/codeAction": "CodeAction", + "textDocument/codeLens": "CodeLens", + "textDocument/colorPresentation": "ColorPresentation", + "textDocument/completion": "Completion", + "textDocument/declaration": "Declaration", + "textDocument/definition": "Definition", + "textDocument/diagnostic": "Diagnostic", + "textDocument/didChange": "DidChange", + "textDocument/didClose": "DidClose", + "textDocument/didOpen": "DidOpen", + "textDocument/didSave": "DidSave", + "textDocument/documentColor": "DocumentColor", + "textDocument/documentHighlight": "DocumentHighlight", + "textDocument/documentLink": "DocumentLink", + "textDocument/documentSymbol": "DocumentSymbol", + "textDocument/foldingRange": "FoldingRange", + "textDocument/formatting": "Formatting", + "textDocument/hover": "Hover", + "textDocument/implementation": "Implementation", + "textDocument/inlayHint": "InlayHint", + "textDocument/inlineValue": "InlineValue", + "textDocument/linkedEditingRange": "LinkedEditingRange", + "textDocument/moniker": "Moniker", + "textDocument/onTypeFormatting": "OnTypeFormatting", + "textDocument/prepareCallHierarchy": "PrepareCallHierarchy", + "textDocument/prepareRename": "PrepareRename", + "textDocument/prepareTypeHierarchy": "PrepareTypeHierarchy", + "textDocument/publishDiagnostics": "PublishDiagnostics", + "textDocument/rangeFormatting": "RangeFormatting", + "textDocument/references": "References", + "textDocument/rename": "Rename", + "textDocument/selectionRange": "SelectionRange", + "textDocument/semanticTokens/full": "SemanticTokensFull", + "textDocument/semanticTokens/full/delta": "SemanticTokensFullDelta", + "textDocument/semanticTokens/range": "SemanticTokensRange", + "textDocument/signatureHelp": "SignatureHelp", + "textDocument/typeDefinition": "TypeDefinition", + "textDocument/willSave": "WillSave", + "textDocument/willSaveWaitUntil": "WillSaveWaitUntil", + "typeHierarchy/subtypes": "Subtypes", + "typeHierarchy/supertypes": "Supertypes", + "window/logMessage": "LogMessage", + "window/showDocument": "ShowDocument", + "window/showMessage": "ShowMessage", + "window/showMessageRequest": "ShowMessageRequest", + "window/workDoneProgress/cancel": "WorkDoneProgressCancel", + "window/workDoneProgress/create": "WorkDoneProgressCreate", + "workspace/applyEdit": "ApplyEdit", + "workspace/codeLens/refresh": "CodeLensRefresh", + "workspace/configuration": "Configuration", + "workspace/diagnostic": "DiagnosticWorkspace", + "workspace/diagnostic/refresh": "DiagnosticRefresh", + "workspace/didChangeConfiguration": "DidChangeConfiguration", + "workspace/didChangeWatchedFiles": "DidChangeWatchedFiles", + "workspace/didChangeWorkspaceFolders": "DidChangeWorkspaceFolders", + "workspace/didCreateFiles": "DidCreateFiles", + "workspace/didDeleteFiles": "DidDeleteFiles", + "workspace/didRenameFiles": "DidRenameFiles", + "workspace/executeCommand": "ExecuteCommand", + "workspace/inlayHint/refresh": "InlayHintRefresh", + "workspace/inlineValue/refresh": "InlineValueRefresh", + "workspace/semanticTokens/refresh": "SemanticTokensRefresh", + "workspace/symbol": "Symbol", + "workspace/willCreateFiles": "WillCreateFiles", + "workspace/willDeleteFiles": "WillDeleteFiles", + "workspace/willRenameFiles": "WillRenameFiles", + "workspace/workspaceFolders": "WorkspaceFolders", + "workspaceSymbol/resolve": "ResolveWorkspaceSymbol", +} diff --git a/gopls/internal/lsp/protocol/generate/doc.go b/gopls/internal/lsp/protocol/generate/doc.go new file mode 100644 index 00000000000..74685559c8e --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/doc.go @@ -0,0 +1,32 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +/* +GenLSP generates the files tsprotocol.go, tsclient.go, +tsserver.go, tsjson.go that support the language server protocol +for gopls. + +Usage: + + go run . [flags] + +The flags are: + + -d + The directory containing the vscode-languageserver-node repository. + (git clone https://github.com/microsoft/vscode-languageserver-node.git). + If not specified, the default is $HOME/vscode-languageserver-node. + + -o + The directory to write the generated files to. It must exist. + The default is "gen". + + -c + Compare the generated files to the files in the specified directory. + If this flag is not specified, no comparison is done. +*/ +package main diff --git a/gopls/internal/lsp/protocol/generate/generate.go b/gopls/internal/lsp/protocol/generate/generate.go new file mode 100644 index 00000000000..86c332856af --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/generate.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// generate the Go code diff --git a/gopls/internal/lsp/protocol/generate/main.go b/gopls/internal/lsp/protocol/generate/main.go new file mode 100644 index 00000000000..38d25705d32 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/main.go @@ -0,0 +1,93 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import ( + "flag" + "fmt" + "log" + "os" +) + +var ( + // git clone https://github.com/microsoft/vscode-languageserver-node.git + repodir = flag.String("d", "", "directory of vscode-languageserver-node") + outputdir = flag.String("o", "gen", "output directory") + cmpolder = flag.String("c", "", "directory of older generated code") +) + +func main() { + log.SetFlags(log.Lshortfile) // log file name and line number, not time + flag.Parse() + + if *repodir == "" { + *repodir = fmt.Sprintf("%s/vscode-languageserver-node", os.Getenv("HOME")) + } + spec := parse(*repodir) + + // index the information in the specification + spec.indexRPCInfo() // messages + spec.indexDefInfo() // named types + +} + +func (s *spec) indexRPCInfo() { + for _, r := range s.model.Requests { + r := r + s.byMethod[r.Method] = &r + } + for _, n := range s.model.Notifications { + n := n + if n.Method == "$/cancelRequest" { + // viewed as too confusing to generate + continue + } + s.byMethod[n.Method] = &n + } +} + +func (sp *spec) indexDefInfo() { + for _, s := range sp.model.Structures { + s := s + sp.byName[s.Name] = &s + } + for _, e := range sp.model.Enumerations { + e := e + sp.byName[e.Name] = &e + } + for _, ta := range sp.model.TypeAliases { + ta := ta + sp.byName[ta.Name] = &ta + } + + // some Structure and TypeAlias names need to be changed for Go + // so byName contains the name used in the .json file, and + // the Name field contains the Go version of the name. + v := sp.model.Structures + for i, s := range v { + switch s.Name { + case "_InitializeParams": // _ is not upper case + v[i].Name = "XInitializeParams" + case "ConfigurationParams": // gopls compatibility + v[i].Name = "ParamConfiguration" + case "InitializeParams": // gopls compatibility + v[i].Name = "ParamInitialize" + case "PreviousResultId": // Go naming convention + v[i].Name = "PreviousResultID" + case "WorkspaceFoldersServerCapabilities": // gopls compatibility + v[i].Name = "WorkspaceFolders5Gn" + } + } + w := sp.model.TypeAliases + for i, t := range w { + switch t.Name { + case "PrepareRenameResult": // gopls compatibility + w[i].Name = "PrepareRename2Gn" + } + } +} diff --git a/gopls/internal/lsp/protocol/generate/main_test.go b/gopls/internal/lsp/protocol/generate/main_test.go new file mode 100644 index 00000000000..d986b59cee9 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/main_test.go @@ -0,0 +1,122 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "testing" +) + +// this is not a test, but an easy way to invoke the debugger +func TestAll(t *testing.T) { + t.Skip("run by hand") + log.SetFlags(log.Lshortfile) + main() +} + +// this is not a test, but an easy way to invoke the debugger +func TestCompare(t *testing.T) { + t.Skip("run by hand") + log.SetFlags(log.Lshortfile) + *cmpolder = "../lsp/gen" // instead use a directory containing the older generated files + main() +} + +// check that the parsed file includes all the information +// from the json file. This test will fail if the spec +// introduces new fields. (one can test this test by +// commenting out some special handling in parse.go.) +func TestParseContents(t *testing.T) { + t.Skip("run by hand") + log.SetFlags(log.Lshortfile) + + // compute our parse of the specification + dir := os.Getenv("HOME") + "/vscode-languageserver-node" + v := parse(dir) + out, err := json.Marshal(v.model) + if err != nil { + t.Fatal(err) + } + var our interface{} + if err := json.Unmarshal(out, &our); err != nil { + t.Fatal(err) + } + + // process the json file + fname := dir + "/protocol/metaModel.json" + buf, err := os.ReadFile(fname) + if err != nil { + t.Fatalf("could not read metaModel.json: %v", err) + } + var raw interface{} + if err := json.Unmarshal(buf, &raw); err != nil { + t.Fatal(err) + } + + // convert to strings showing the fields + them := flatten(raw) + us := flatten(our) + + // everything in them should be in us + lesser := make(sortedMap[bool]) + for _, s := range them { + lesser[s] = true + } + greater := make(sortedMap[bool]) // set of fields we have + for _, s := range us { + greater[s] = true + } + for _, k := range lesser.keys() { // set if fields they have + if !greater[k] { + t.Errorf("missing %s", k) + } + } +} + +// flatten(nil) = "nil" +// flatten(v string) = fmt.Sprintf("%q", v) +// flatten(v float64)= fmt.Sprintf("%g", v) +// flatten(v bool) = fmt.Sprintf("%v", v) +// flatten(v []any) = []string{"[0]"flatten(v[0]), "[1]"flatten(v[1]), ...} +// flatten(v map[string]any) = {"key1": flatten(v["key1"]), "key2": flatten(v["key2"]), ...} +func flatten(x any) []string { + switch v := x.(type) { + case nil: + return []string{"nil"} + case string: + return []string{fmt.Sprintf("%q", v)} + case float64: + return []string{fmt.Sprintf("%g", v)} + case bool: + return []string{fmt.Sprintf("%v", v)} + case []any: + var ans []string + for i, x := range v { + idx := fmt.Sprintf("[%.3d]", i) + for _, s := range flatten(x) { + ans = append(ans, idx+s) + } + } + return ans + case map[string]any: + var ans []string + for k, x := range v { + idx := fmt.Sprintf("%q:", k) + for _, s := range flatten(x) { + ans = append(ans, idx+s) + } + } + return ans + default: + log.Fatalf("unexpected type %T", x) + return nil + } +} diff --git a/gopls/internal/lsp/protocol/generate/naming.go b/gopls/internal/lsp/protocol/generate/naming.go new file mode 100644 index 00000000000..9d9201a49d9 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/naming.go @@ -0,0 +1,64 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// assign names to types. many types come with names, but names +// have to be provided for "or", "and", "tuple", and "literal" types. +// Only one tuple type occurs, so it poses no problem. Otherwise +// the name cannot depend on the ordering of the components, as permuting +// them doesn't change the type. One possibility is to build the name +// of the type out of the names of its components, done in an +// earlier version of this code, but rejected by code reviewers. +// (the name would change if the components changed.) +// An alternate is to use the definition context, which is what is done here +// and works for the existing code. However, it cannot work in general. +// (This easiest case is an "or" type with two "literal" components. +// The components will get the same name, as their definition contexts +// are identical.) spec.byName contains enough information to detect +// such cases. (Note that sometimes giving the same name to different +// types is correct, for instance when they involve stringLiterals.) + +import ( + "strings" +) + +// stacks contain information about the ancestry of a type +// (spaces and initial capital letters are treated specially in stack.name()) +type stack []string + +func (s stack) push(v string) stack { + return append(s, v) +} + +func (s stack) pop() { + s = s[:len(s)-1] +} + +// generate a type name from the stack that contains its ancestry +// +// For instance, ["Result textDocument/implementation"] becomes "_textDocument_implementation" +// which, after being returned, becomes "Or_textDocument_implementation", +// which will become "[]Location" eventually (for gopls compatibility). +func (s stack) name(prefix string) string { + var nm string + var seen int + // use the most recent 2 entries, if there are 2, + // or just the only one. + for i := len(s) - 1; i >= 0 && seen < 2; i-- { + x := s[i] + if x[0] <= 'Z' && x[0] >= 'A' { + // it may contain a message + if idx := strings.Index(x, " "); idx >= 0 { + x = prefix + strings.Replace(x[idx+1:], "/", "_", -1) + } + nm += x + seen++ + } + } + return nm +} diff --git a/gopls/internal/lsp/protocol/generate/output.go b/gopls/internal/lsp/protocol/generate/output.go new file mode 100644 index 00000000000..14a04864e85 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/output.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// Write the output diff --git a/gopls/internal/lsp/protocol/generate/parse.go b/gopls/internal/lsp/protocol/generate/parse.go new file mode 100644 index 00000000000..9f8067eff4d --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/parse.go @@ -0,0 +1,174 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "time" +) + +// a spec contains the specification of the protocol, and derived information. +type spec struct { + model *Model + + // combined Requests and Notifications, indexed by method (e.g., "textDocument/didOpen") + byMethod sortedMap[Message] + + // Structures, Enumerations, and TypeAliases, indexed by name used in + // the .json specification file + // (Some Structure and Enumeration names need to be changed for Go, + // such as _Initialize) + byName sortedMap[Defined] + + // computed type information + nameToTypes sortedMap[[]*Type] // all the uses of a type name + + // remember which types are in a union type + orTypes sortedMap[sortedMap[bool]] + + // information about the version of vscode-languageclient-node + githash string + modTime time.Time +} + +// parse the specification file and return a spec. +// (TestParseContents checks that the parse gets all the fields of the specification) +func parse(dir string) *spec { + fname := filepath.Join(dir, "protocol", "metaModel.json") + buf, err := os.ReadFile(fname) + if err != nil { + log.Fatalf("could not read metaModel.json: %v", err) + } + // line numbers in the .json file occur as comments in tsprotocol.go + newbuf := addLineNumbers(buf) + var v Model + if err := json.Unmarshal(newbuf, &v); err != nil { + log.Fatalf("could not unmarshal metaModel.json: %v", err) + } + + ans := &spec{ + model: &v, + byMethod: make(sortedMap[Message]), + byName: make(sortedMap[Defined]), + nameToTypes: make(sortedMap[[]*Type]), + orTypes: make(sortedMap[sortedMap[bool]]), + } + ans.githash, ans.modTime = gitInfo(dir) + return ans +} + +// gitInfo returns the git hash and modtime of the repository. +func gitInfo(dir string) (string, time.Time) { + fname := dir + "/.git/HEAD" + buf, err := os.ReadFile(fname) + if err != nil { + log.Fatal(err) + } + buf = bytes.TrimSpace(buf) + var githash string + if len(buf) == 40 { + githash = string(buf[:40]) + } else if bytes.HasPrefix(buf, []byte("ref: ")) { + fname = dir + "/.git/" + string(buf[5:]) + buf, err = os.ReadFile(fname) + if err != nil { + log.Fatal(err) + } + githash = string(buf[:40]) + } else { + log.Fatalf("githash cannot be recovered from %s", fname) + } + loadTime := time.Now() + return githash, loadTime +} + +// addLineNumbers adds a "line" field to each object in the JSON. +func addLineNumbers(buf []byte) []byte { + var ans []byte + // In the specification .json file, the delimiter '{' is + // always followed by a newline. There are other {s embedded in strings. + // json.Token does not return \n, or :, or , so using it would + // require parsing the json to reconstruct the missing information. + for linecnt, i := 1, 0; i < len(buf); i++ { + ans = append(ans, buf[i]) + switch buf[i] { + case '{': + if buf[i+1] == '\n' { + ans = append(ans, fmt.Sprintf(`"line": %d, `, linecnt)...) + // warning: this would fail if the spec file had + // `"value": {\n}`, but it does not, as comma is a separator. + } + case '\n': + linecnt++ + } + } + return ans +} + +// Type.Value has to be treated specially for literals and maps +func (t *Type) UnmarshalJSON(data []byte) error { + // First unmarshal only the unambiguous fields. + var x struct { + Kind string `json:"kind"` + Items []*Type `json:"items"` + Element *Type `json:"element"` + Name string `json:"name"` + Key *Type `json:"key"` + Value any `json:"value"` + Line int `json:"line"` + } + if err := json.Unmarshal(data, &x); err != nil { + return err + } + *t = Type{ + Kind: x.Kind, + Items: x.Items, + Element: x.Element, + Name: x.Name, + Value: x.Value, + Line: x.Line, + } + + // Then unmarshal the 'value' field based on the kind. + // This depends on Unmarshal ignoring fields it doesn't know about. + switch x.Kind { + case "map": + var x struct { + Key *Type `json:"key"` + Value *Type `json:"value"` + } + if err := json.Unmarshal(data, &x); err != nil { + return fmt.Errorf("Type.kind=map: %v", err) + } + t.Key = x.Key + t.Value = x.Value + + case "literal": + var z struct { + Value ParseLiteral `json:"value"` + } + + if err := json.Unmarshal(data, &z); err != nil { + return fmt.Errorf("Type.kind=literal: %v", err) + } + t.Value = z.Value + + case "base", "reference", "array", "and", "or", "tuple", + "stringLiteral": + // nop. never seen integerLiteral or booleanLiteral. + + default: + return fmt.Errorf("cannot decode Type.kind %q: %s", x.Kind, data) + } + return nil +} diff --git a/gopls/internal/lsp/protocol/generate/types.go b/gopls/internal/lsp/protocol/generate/types.go new file mode 100644 index 00000000000..e8abd600815 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/types.go @@ -0,0 +1,173 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import "sort" + +// Model contains the parsed version of the spec +type Model struct { + Version Metadata `json:"metaData"` + Requests []Request `json:"requests"` + Notifications []Notification `json:"notifications"` + Structures []Structure `json:"structures"` + Enumerations []Enumeration `json:"enumerations"` + TypeAliases []TypeAlias `json:"typeAliases"` + Line int `json:"line"` +} + +// Metadata is information about the version of the spec +type Metadata struct { + Version string `json:"version"` + Line int `json:"line"` +} + +// A Request is the parsed version of an LSP request +type Request struct { + Documentation string `json:"documentation"` + ErrorData *Type `json:"errorData"` + Direction string `json:"messageDirection"` + Method string `json:"method"` + Params *Type `json:"params"` + PartialResult *Type `json:"partialResult"` + Proposed bool `json:"proposed"` + RegistrationMethod string `json:"registrationMethod"` + RegistrationOptions *Type `json:"registrationOptions"` + Result *Type `json:"result"` + Since string `json:"since"` + Line int `json:"line"` +} + +// A Notificatin is the parsed version of an LSP notification +type Notification struct { + Documentation string `json:"documentation"` + Direction string `json:"messageDirection"` + Method string `json:"method"` + Params *Type `json:"params"` + Proposed bool `json:"proposed"` + RegistrationMethod string `json:"registrationMethod"` + RegistrationOptions *Type `json:"registrationOptions"` + Since string `json:"since"` + Line int `json:"line"` +} + +// A Structure is the parsed version of an LSP structure from the spec +type Structure struct { + Documentation string `json:"documentation"` + Extends []*Type `json:"extends"` + Mixins []*Type `json:"mixins"` + Name string `json:"name"` + Properties []NameType `json:"properties"` + Proposed bool `json:"proposed"` + Since string `json:"since"` + Line int `json:"line"` +} + +// An enumeration is the parsed version of an LSP enumeration from the spec +type Enumeration struct { + Documentation string `json:"documentation"` + Name string `json:"name"` + Proposed bool `json:"proposed"` + Since string `json:"since"` + SupportsCustomValues bool `json:"supportsCustomValues"` + Type *Type `json:"type"` + Values []NameValue `json:"values"` + Line int `json:"line"` +} + +// A TypeAlias is the parsed version of an LSP type alias from the spec +type TypeAlias struct { + Documentation string `json:"documentation"` + Name string `json:"name"` + Proposed bool `json:"proposed"` + Since string `json:"since"` + Type *Type `json:"type"` + Line int `json:"line"` +} + +// A NameValue describes an enumeration constant +type NameValue struct { + Documentation string `json:"documentation"` + Name string `json:"name"` + Proposed bool `json:"proposed"` + Since string `json:"since"` + Value any `json:"value"` // number or string + Line int `json:"line"` +} + +// common to Request and Notification +type Message interface { + direction() string +} + +func (r Request) direction() string { + return r.Direction +} + +func (n Notification) direction() string { + return n.Direction +} + +// A Defined is one of Structure, Enumeration, TypeAlias, for type checking +type Defined interface { + tag() +} + +func (s Structure) tag() { +} + +func (e Enumeration) tag() { +} + +func (ta TypeAlias) tag() { +} + +// A Type is the parsed version of an LSP type from the spec, +// or a Type the code constructs +type Type struct { + Kind string `json:"kind"` // -- which kind goes with which field -- + Items []*Type `json:"items"` // "and", "or", "tuple" + Element *Type `json:"element"` // "array" + Name string `json:"name"` // "base", "reference" + Key *Type `json:"key"` // "map" + Value any `json:"value"` // "map", "stringLiteral", "literal" + // used to tie generated code to the specification + Line int `json:"line"` + + name string // these are generated names, like Uint32 + typeName string // these are actual type names, like uint32 +} + +// ParsedLiteral is Type.Value when Type.Kind is "literal" +type ParseLiteral struct { + Properties `json:"properties"` +} + +// A NameType represents the name and type of a structure element +type NameType struct { + Name string `json:"name"` + Type *Type `json:"type"` + Optional bool `json:"optional"` + Documentation string `json:"documentation"` + Since string `json:"since"` + Proposed bool `json:"proposed"` + Line int `json:"line"` +} + +// Properties are the collection of structure elements +type Properties []NameType + +type sortedMap[T any] map[string]T + +func (s sortedMap[T]) keys() []string { + var keys []string + for k := range s { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/gopls/internal/lsp/protocol/generate/utilities.go b/gopls/internal/lsp/protocol/generate/utilities.go new file mode 100644 index 00000000000..b091a0d145f --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/utilities.go @@ -0,0 +1,55 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import ( + "fmt" + "log" + "runtime" + "strings" + "time" +) + +// goName returns the Go version of a name. +func goName(s string) string { + if s == "" { + return s // doesn't happen + } + s = strings.ToUpper(s[:1]) + s[1:] + if rest := strings.TrimSuffix(s, "Uri"); rest != s { + s = rest + "URI" + } + if rest := strings.TrimSuffix(s, "Id"); rest != s { + s = rest + "ID" + } + return s +} + +// the common header for all generated files +func (s *spec) createHeader() string { + format := `// Copyright 2022 The Go Authors. All rights reserved. + // Use of this source code is governed by a BSD-style + // license that can be found in the LICENSE file. + + // Code generated for LSP. DO NOT EDIT. + + package protocol + + // Code generated from version %s of protocol/metaModel.json. + // git hash %s (as of %s) + + ` + hdr := fmt.Sprintf(format, s.model.Version.Version, s.githash, s.modTime.Format(time.ANSIC)) + return hdr +} + +// useful in debugging +func here() { + _, f, l, _ := runtime.Caller(1) + log.Printf("here: %s:%d", f, l) +} From de675d547938a7540a5ea69462d4d1ebb65fefd2 Mon Sep 17 00:00:00 2001 From: pjw Date: Sun, 23 Oct 2022 15:54:03 -0400 Subject: [PATCH 372/723] tools/gopls: argument in function bodies marked as parameter by semantic tokens In func f(x int) int {return x;} the second x used to be marked as a variable, but now is marked as a parameter, visually tying it to its definition. Fixes golang/go#56257 Change-Id: I8aa506b1ddff5ed9a3d2716d48c64521bdea0fd5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/445095 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Peter Weinberger gopls-CI: kokoro --- gopls/internal/lsp/semantic.go | 31 ++++++++++++++++++- .../lsp/testdata/semantic/a.go.golden | 6 ++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 713c3ac4c0f..a8b38f044ab 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -526,9 +526,14 @@ func (e *encoded) ident(x *ast.Ident) { tok(x.Pos(), len(x.Name), tokFunction, nil) } else if _, ok := y.Type().(*typeparams.TypeParam); ok { tok(x.Pos(), len(x.Name), tokTypeParam, nil) + } else if e.isParam(use.Pos()) { + // variable, unless use.pos is the pos of a Field in an ancestor FuncDecl + // or FuncLit and then it's a parameter + tok(x.Pos(), len(x.Name), tokParameter, nil) } else { tok(x.Pos(), len(x.Name), tokVariable, nil) } + default: // can't happen if use == nil { @@ -543,6 +548,30 @@ func (e *encoded) ident(x *ast.Ident) { } } +func (e *encoded) isParam(pos token.Pos) bool { + for i := len(e.stack) - 1; i >= 0; i-- { + switch n := e.stack[i].(type) { + case *ast.FuncDecl: + for _, f := range n.Type.Params.List { + for _, id := range f.Names { + if id.Pos() == pos { + return true + } + } + } + case *ast.FuncLit: + for _, f := range n.Type.Params.List { + for _, id := range f.Names { + if id.Pos() == pos { + return true + } + } + } + } + } + return false +} + func isSignature(use types.Object) bool { if true { return false //PJW: fix after generics seem ok @@ -640,7 +669,7 @@ func (e *encoded) unkIdent(x *ast.Ident) (tokenType, []string) { if nd.Tok != token.DEFINE { def = nil } - return tokVariable, def + return tokVariable, def // '_' in _ = ... } } // RHS, = x diff --git a/gopls/internal/lsp/testdata/semantic/a.go.golden b/gopls/internal/lsp/testdata/semantic/a.go.golden index 071dd171c84..34b70e0f4f2 100644 --- a/gopls/internal/lsp/testdata/semantic/a.go.golden +++ b/gopls/internal/lsp/testdata/semantic/a.go.golden @@ -65,7 +65,7 @@ /*⇒2,variable,[definition]*/ff /*⇒2,operator,[]*/:= /*⇒4,keyword,[]*/func() {} /*⇒5,keyword,[]*/defer /*⇒2,variable,[]*/ff() /*⇒2,keyword,[]*/go /*⇒3,namespace,[]*/utf./*⇒9,function,[]*/RuneCount(/*⇒2,string,[]*/"") - /*⇒2,keyword,[]*/go /*⇒4,namespace,[]*/utf8./*⇒9,function,[]*/RuneCount(/*⇒2,variable,[]*/vv.(/*⇒6,type,[]*/string)) + /*⇒2,keyword,[]*/go /*⇒4,namespace,[]*/utf8./*⇒9,function,[]*/RuneCount(/*⇒2,parameter,[]*/vv.(/*⇒6,type,[]*/string)) /*⇒2,keyword,[]*/if /*⇒4,variable,[readonly]*/true { } /*⇒4,keyword,[]*/else { } @@ -73,9 +73,9 @@ /*⇒3,keyword,[]*/for /*⇒1,variable,[definition]*/i /*⇒2,operator,[]*/:= /*⇒1,number,[]*/0; /*⇒1,variable,[]*/i /*⇒1,operator,[]*/< /*⇒2,number,[]*/10; { /*⇒5,keyword,[]*/break Never } - _, /*⇒2,variable,[definition]*/ok /*⇒2,operator,[]*/:= /*⇒2,variable,[]*/vv[/*⇒1,number,[]*/0].(/*⇒1,type,[]*/A) + _, /*⇒2,variable,[definition]*/ok /*⇒2,operator,[]*/:= /*⇒2,parameter,[]*/vv[/*⇒1,number,[]*/0].(/*⇒1,type,[]*/A) /*⇒2,keyword,[]*/if /*⇒1,operator,[]*/!/*⇒2,variable,[]*/ok { - /*⇒6,keyword,[]*/switch /*⇒1,variable,[definition]*/x /*⇒2,operator,[]*/:= /*⇒2,variable,[]*/vv[/*⇒1,number,[]*/0].(/*⇒4,keyword,[]*/type) { + /*⇒6,keyword,[]*/switch /*⇒1,variable,[definition]*/x /*⇒2,operator,[]*/:= /*⇒2,parameter,[]*/vv[/*⇒1,number,[]*/0].(/*⇒4,keyword,[]*/type) { } /*⇒4,keyword,[]*/goto Never } From 541f4c5166cebaec5b7ad1627a5bcad2ceafb4d7 Mon Sep 17 00:00:00 2001 From: eNV25 Date: Tue, 25 Oct 2022 18:45:13 +0000 Subject: [PATCH 373/723] cmd/bundle: quote command-line arguments in output The previous version of bundle simply joins together with a space " ". While this works for simple arguments this causes problems when you need to pass special strings like an empty string or a string containing a space. The following example shows how the previous version handles an empty string argument `-prefix ""`. //go:generate bundle -o /dev/stdout -prefix example.com/mod This change quotes the arguments with strconv.Quote, if needed, before joining together with a space: //go:generate bundle -o /dev/stdout -prefix "" example.com/mod Change-Id: Ic706a3bd7916515ba91dbe5e0def956703ab2988 GitHub-Last-Rev: 8dc0c88fc17c73bb432ed60a9578ec222814e68b GitHub-Pull-Request: golang/tools#411 Reviewed-on: https://go-review.googlesource.com/c/tools/+/444956 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Heschi Kreinick Auto-Submit: Alan Donovan Reviewed-by: Alan Donovan --- cmd/bundle/main.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/cmd/bundle/main.go b/cmd/bundle/main.go index 96cbce9a131..194797bd822 100644 --- a/cmd/bundle/main.go +++ b/cmd/bundle/main.go @@ -84,6 +84,7 @@ import ( "os" "strconv" "strings" + "unicode" "golang.org/x/tools/go/packages" ) @@ -233,7 +234,7 @@ func bundle(src, dst, dstpkg, prefix, buildTags string) ([]byte, error) { fmt.Fprintf(&out, "// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.\n") if *outputFile != "" && buildTags == "" { - fmt.Fprintf(&out, "//go:generate bundle %s\n", strings.Join(os.Args[1:], " ")) + fmt.Fprintf(&out, "//go:generate bundle %s\n", strings.Join(quoteArgs(os.Args[1:]), " ")) } else { fmt.Fprintf(&out, "// $ bundle %s\n", strings.Join(os.Args[1:], " ")) } @@ -447,6 +448,35 @@ func printSameLineComment(out *bytes.Buffer, comments []*ast.CommentGroup, fset return pos } +func quoteArgs(ss []string) []string { + // From go help generate: + // + // > The arguments to the directive are space-separated tokens or + // > double-quoted strings passed to the generator as individual + // > arguments when it is run. + // + // > Quoted strings use Go syntax and are evaluated before execution; a + // > quoted string appears as a single argument to the generator. + // + var qs []string + for _, s := range ss { + if s == "" || containsSpace(s) { + s = strconv.Quote(s) + } + qs = append(qs, s) + } + return qs +} + +func containsSpace(s string) bool { + for _, r := range s { + if unicode.IsSpace(r) { + return true + } + } + return false +} + type flagFunc func(string) func (f flagFunc) Set(s string) error { From 875c31f1e968e42a193b8c3300e5e449db4c7958 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 24 Oct 2022 16:02:50 -0400 Subject: [PATCH 374/723] go/internal/gcimporter: add test of export/import on std This test ensures that all std and x/tools packages can be re-typechecked using export data. Change-Id: I189a8138d74cb38f69dfb51c613849e43b316322 Reviewed-on: https://go-review.googlesource.com/c/tools/+/445096 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- go/internal/gcimporter/stdlib_test.go | 82 +++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 go/internal/gcimporter/stdlib_test.go diff --git a/go/internal/gcimporter/stdlib_test.go b/go/internal/gcimporter/stdlib_test.go new file mode 100644 index 00000000000..ec1be3ea031 --- /dev/null +++ b/go/internal/gcimporter/stdlib_test.go @@ -0,0 +1,82 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gcimporter_test + +import ( + "bytes" + "fmt" + "go/token" + "go/types" + "testing" + "unsafe" + + "golang.org/x/tools/go/gcexportdata" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/testenv" +) + +// TestStdlib ensures that all packages in std and x/tools can be +// type-checked using export data. Takes around 3s. +func TestStdlib(t *testing.T) { + testenv.NeedsGoPackages(t) + + // gcexportdata.Read rapidly consumes FileSet address space, + // so disable the test on 32-bit machines. + // (We could use a fresh FileSet per type-check, but that + // would require us to re-parse the source using it.) + if unsafe.Sizeof(token.NoPos) < 8 { + t.Skip("skipping test on 32-bit machine") + } + + // Load, parse and type-check the standard library and x/tools. + cfg := &packages.Config{Mode: packages.LoadAllSyntax} + pkgs, err := packages.Load(cfg, "std", "golang.org/x/tools/...") + if err != nil { + t.Fatalf("failed to load/parse/type-check: %v", err) + } + if packages.PrintErrors(pkgs) > 0 { + t.Fatal("there were errors during loading") + } + if len(pkgs) < 240 { + t.Errorf("too few packages (%d) were loaded", len(pkgs)) + } + + export := make(map[string][]byte) // keys are package IDs + + // Re-type check them all in post-order, using export data. + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + packages := make(map[string]*types.Package) // keys are package paths + cfg := &types.Config{ + Error: func(e error) { + t.Errorf("type error: %v", e) + }, + Importer: importerFunc(func(importPath string) (*types.Package, error) { + // Resolve import path to (vendored?) package path. + imported := pkg.Imports[importPath] + + if imported.PkgPath == "unsafe" { + return types.Unsafe, nil // unsafe has no exportdata + } + + data, ok := export[imported.ID] + if !ok { + return nil, fmt.Errorf("missing export data for %s", importPath) + } + return gcexportdata.Read(bytes.NewReader(data), pkg.Fset, packages, imported.PkgPath) + }), + } + + // Re-typecheck the syntax and save the export data in the map. + newPkg := types.NewPackage(pkg.PkgPath, pkg.Name) + check := types.NewChecker(cfg, pkg.Fset, newPkg, nil) + check.Files(pkg.Syntax) + + var out bytes.Buffer + if err := gcexportdata.Write(&out, pkg.Fset, newPkg); err != nil { + t.Fatalf("internal error writing export data: %v", err) + } + export[pkg.ID] = out.Bytes() + }) +} From e4bb34383f73dd4890f65b54ff29b148b2d6f62b Mon Sep 17 00:00:00 2001 From: Michael Matloob Date: Tue, 25 Oct 2022 16:58:30 -0700 Subject: [PATCH 375/723] go/internal/gcimporter: update to anticipate missing targets and .as This cl updates go/internal/gcimporter the to anticiapte missing targets and .a files the same way CL 442303 updates the two other versions of gcimporter to do the same. It also adds a couple of helpers to create importcfg files for the compiler and list the locations of cached stdlib .a files in internal/goroot and internal/testenv, the analogues of their import paths in the go distribution. Change-Id: Ie207882c13df0e886a51d31e7957a1e508331f10 Reviewed-on: https://go-review.googlesource.com/c/tools/+/445455 TryBot-Result: Gopher Robot Reviewed-by: Bryan Mills Run-TryBot: Michael Matloob gopls-CI: kokoro Reviewed-by: Michael Matloob --- go/internal/gcimporter/gcimporter.go | 43 ++++++++++++-- go/internal/gcimporter/gcimporter_test.go | 38 +++++++----- internal/goroot/importcfg.go | 71 +++++++++++++++++++++++ internal/testenv/testenv.go | 20 +++++++ 4 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 internal/goroot/importcfg.go diff --git a/go/internal/gcimporter/gcimporter.go b/go/internal/gcimporter/gcimporter.go index e96c39600d1..85a801c6a3d 100644 --- a/go/internal/gcimporter/gcimporter.go +++ b/go/internal/gcimporter/gcimporter.go @@ -22,11 +22,14 @@ import ( "io" "io/ioutil" "os" + "path" "path/filepath" "sort" "strconv" "strings" "text/scanner" + + "golang.org/x/tools/internal/goroot" ) const ( @@ -38,6 +41,25 @@ const ( trace = false ) +func lookupGorootExport(pkgpath, srcRoot, srcDir string) (string, bool) { + pkgpath = filepath.ToSlash(pkgpath) + m, err := goroot.PkgfileMap() + if err != nil { + return "", false + } + if export, ok := m[pkgpath]; ok { + return export, true + } + vendorPrefix := "vendor" + if strings.HasPrefix(srcDir, filepath.Join(srcRoot, "cmd")) { + vendorPrefix = path.Join("cmd", vendorPrefix) + } + pkgpath = path.Join(vendorPrefix, pkgpath) + fmt.Fprintln(os.Stderr, "looking up ", pkgpath) + export, ok := m[pkgpath] + return export, ok +} + var pkgExts = [...]string{".a", ".o"} // FindPkg returns the filename and unique package id for an import @@ -60,11 +82,18 @@ func FindPkg(path, srcDir string) (filename, id string) { } bp, _ := build.Import(path, srcDir, build.FindOnly|build.AllowBinary) if bp.PkgObj == "" { - id = path // make sure we have an id to print in error message - return + var ok bool + if bp.Goroot { + filename, ok = lookupGorootExport(path, bp.SrcRoot, srcDir) + } + if !ok { + id = path // make sure we have an id to print in error message + return + } + } else { + noext = strings.TrimSuffix(bp.PkgObj, ".a") + id = bp.ImportPath } - noext = strings.TrimSuffix(bp.PkgObj, ".a") - id = bp.ImportPath case build.IsLocalImport(path): // "./x" -> "/this/directory/x.ext", "/this/directory/x" @@ -85,6 +114,12 @@ func FindPkg(path, srcDir string) (filename, id string) { } } + if filename != "" { + if f, err := os.Stat(filename); err == nil && !f.IsDir() { + return + } + } + // try extensions for _, ext := range pkgExts { filename = noext + ext diff --git a/go/internal/gcimporter/gcimporter_test.go b/go/internal/gcimporter/gcimporter_test.go index a71c1880bf1..e4029c0d5e1 100644 --- a/go/internal/gcimporter/gcimporter_test.go +++ b/go/internal/gcimporter/gcimporter_test.go @@ -48,25 +48,31 @@ func needsCompiler(t *testing.T, compiler string) { // compile runs the compiler on filename, with dirname as the working directory, // and writes the output file to outdirname. -func compile(t *testing.T, dirname, filename, outdirname string) string { - return compilePkg(t, dirname, filename, outdirname, "p") +// compile gives the resulting package a packagepath of p. +func compile(t *testing.T, dirname, filename, outdirname string, packagefiles map[string]string) string { + return compilePkg(t, dirname, filename, outdirname, packagefiles, "p") } -func compilePkg(t *testing.T, dirname, filename, outdirname, pkg string) string { +func compilePkg(t *testing.T, dirname, filename, outdirname string, packagefiles map[string]string, pkg string) string { testenv.NeedsGoBuild(t) // filename must end with ".go" - if !strings.HasSuffix(filename, ".go") { + basename := strings.TrimSuffix(filepath.Base(filename), ".go") + ok := filename != basename + if !ok { t.Fatalf("filename doesn't end in .go: %s", filename) } - basename := filepath.Base(filename) - outname := filepath.Join(outdirname, basename[:len(basename)-2]+"o") - cmd := exec.Command("go", "tool", "compile", "-p="+pkg, "-o", outname, filename) + objname := basename + ".o" + outname := filepath.Join(outdirname, objname) + importcfgfile := filepath.Join(outdirname, basename) + ".importcfg" + testenv.WriteImportcfg(t, importcfgfile, packagefiles) + importreldir := strings.ReplaceAll(outdirname, string(os.PathSeparator), "/") + cmd := exec.Command("go", "tool", "compile", "-p", pkg, "-D", importreldir, "-importcfg", importcfgfile, "-o", outname, filename) cmd.Dir = dirname out, err := cmd.CombinedOutput() if err != nil { t.Logf("%s", out) - t.Fatalf("(cd %v && %v) failed: %s", cmd.Dir, cmd, err) + t.Fatalf("go tool compile %s failed: %s", filename, err) } return outname } @@ -133,7 +139,7 @@ func TestImportTestdata(t *testing.T) { tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) - compile(t, "testdata", testfile, filepath.Join(tmpdir, "testdata")) + compile(t, "testdata", testfile, filepath.Join(tmpdir, "testdata"), nil) // filename should end with ".go" filename := testfile[:len(testfile)-3] @@ -215,7 +221,7 @@ func TestImportTypeparamTests(t *testing.T) { // Compile and import, and compare the resulting package with the package // that was type-checked directly. - compile(t, rootDir, entry.Name(), filepath.Join(tmpdir, "testdata")) + compile(t, rootDir, entry.Name(), filepath.Join(tmpdir, "testdata"), nil) pkgName := strings.TrimSuffix(entry.Name(), ".go") imported := importPkg(t, "./testdata/"+pkgName, tmpdir) checked := checkFile(t, filename, src) @@ -586,8 +592,8 @@ func TestIssue13566(t *testing.T) { if err != nil { t.Fatal(err) } - compilePkg(t, "testdata", "a.go", testoutdir, apkg(testoutdir)) - compile(t, testoutdir, bpath, testoutdir) + compilePkg(t, "testdata", "a.go", testoutdir, nil, apkg(testoutdir)) + compile(t, testoutdir, bpath, testoutdir, map[string]string{apkg(testoutdir): filepath.Join(testoutdir, "a.o")}) // import must succeed (test for issue at hand) pkg := importPkg(t, "./testdata/b", tmpdir) @@ -655,7 +661,7 @@ func TestIssue15517(t *testing.T) { tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) - compile(t, "testdata", "p.go", filepath.Join(tmpdir, "testdata")) + compile(t, "testdata", "p.go", filepath.Join(tmpdir, "testdata"), nil) // Multiple imports of p must succeed without redeclaration errors. // We use an import path that's not cleaned up so that the eventual @@ -746,8 +752,8 @@ func TestIssue51836(t *testing.T) { if err != nil { t.Fatal(err) } - compilePkg(t, dir, "a.go", testoutdir, apkg(testoutdir)) - compile(t, testoutdir, bpath, testoutdir) + compilePkg(t, dir, "a.go", testoutdir, nil, apkg(testoutdir)) + compile(t, testoutdir, bpath, testoutdir, map[string]string{apkg(testoutdir): filepath.Join(testoutdir, "a.o")}) // import must succeed (test for issue at hand) _ = importPkg(t, "./testdata/aa", tmpdir) @@ -773,7 +779,7 @@ func importPkg(t *testing.T, path, srcDir string) *types.Package { func compileAndImportPkg(t *testing.T, name string) *types.Package { tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) - compile(t, "testdata", name+".go", filepath.Join(tmpdir, "testdata")) + compile(t, "testdata", name+".go", filepath.Join(tmpdir, "testdata"), nil) return importPkg(t, "./testdata/"+name, tmpdir) } diff --git a/internal/goroot/importcfg.go b/internal/goroot/importcfg.go new file mode 100644 index 00000000000..6575cfb9df6 --- /dev/null +++ b/internal/goroot/importcfg.go @@ -0,0 +1,71 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package goroot is a copy of package internal/goroot +// in the main GO repot. It provides a utility to produce +// an importcfg and import path to package file map mapping +// standard library packages to the locations of their export +// data files. +package goroot + +import ( + "bytes" + "fmt" + "os/exec" + "strings" + "sync" +) + +// Importcfg returns an importcfg file to be passed to the +// Go compiler that contains the cached paths for the .a files for the +// standard library. +func Importcfg() (string, error) { + var icfg bytes.Buffer + + m, err := PkgfileMap() + if err != nil { + return "", err + } + fmt.Fprintf(&icfg, "# import config") + for importPath, export := range m { + if importPath != "unsafe" && export != "" { // unsafe + fmt.Fprintf(&icfg, "\npackagefile %s=%s", importPath, export) + } + } + s := icfg.String() + return s, nil +} + +var ( + stdlibPkgfileMap map[string]string + stdlibPkgfileErr error + once sync.Once +) + +// PkgfileMap returns a map of package paths to the location on disk +// of the .a file for the package. +// The caller must not modify the map. +func PkgfileMap() (map[string]string, error) { + once.Do(func() { + m := make(map[string]string) + output, err := exec.Command("go", "list", "-export", "-e", "-f", "{{.ImportPath}} {{.Export}}", "std", "cmd").Output() + if err != nil { + stdlibPkgfileErr = err + } + for _, line := range strings.Split(string(output), "\n") { + if line == "" { + continue + } + sp := strings.SplitN(line, " ", 2) + if len(sp) != 2 { + err = fmt.Errorf("determining pkgfile map: invalid line in go list output: %q", line) + return + } + importPath, export := sp[0], sp[1] + m[importPath] = export + } + stdlibPkgfileMap = m + }) + return stdlibPkgfileMap, stdlibPkgfileErr +} diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index bfadb44be65..f606cb71543 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -16,8 +16,11 @@ import ( "runtime/debug" "strings" "sync" + "testing" "time" + "golang.org/x/tools/internal/goroot" + exec "golang.org/x/sys/execabs" ) @@ -329,3 +332,20 @@ func Deadline(t Testing) (time.Time, bool) { } return td.Deadline() } + +// WriteImportcfg writes an importcfg file used by the compiler or linker to +// dstPath containing entries for the packages in std and cmd in addition +// to the package to package file mappings in additionalPackageFiles. +func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) { + importcfg, err := goroot.Importcfg() + for k, v := range additionalPackageFiles { + importcfg += fmt.Sprintf("\npackagefile %s=%s", k, v) + } + if err != nil { + t.Fatalf("preparing the importcfg failed: %s", err) + } + ioutil.WriteFile(dstPath, []byte(importcfg), 0655) + if err != nil { + t.Fatalf("writing the importcfg failed: %s", err) + } +} From 2af106efed6594091daf763358305aecff9681b4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 26 Oct 2022 18:09:24 -0400 Subject: [PATCH 376/723] gopls/internal/hooks: fixes to diff disaster logic Previously, the disaster logic in the new diff implementation would "encrypt" the before/after files using a monoalphabetic substitution, which has been insecure since the 9th century. Instead, save plain text, in file with mode 0600, and invite the user to audit the file before sharing it with us. Also, separate the two files using a NUL byte, not a newline, which is highly ambiguous. Also, in the JSON diff stats writer: - print a warning if we can't create the log file. (The previous code was subtle--it stored a nil *os.File in an io.Writer, which caused Writes to fail with an error, in effect, silently.) - Don't hold the mutex around the write operation. - Fix minor off-by-one error (re: 15) - Crash if JSON encoding fails; it "can't happen". Change-Id: I9b6a4145451afd77594f0ef9868143634a9c4561 Reviewed-on: https://go-review.googlesource.com/c/tools/+/445580 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/hooks/diff.go | 132 +++++++++++------------------- gopls/internal/hooks/diff_test.go | 38 ++------- 2 files changed, 54 insertions(+), 116 deletions(-) diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go index cac136ae192..a0383b87675 100644 --- a/gopls/internal/hooks/diff.go +++ b/gopls/internal/hooks/diff.go @@ -5,19 +5,15 @@ package hooks import ( - "crypto/rand" "encoding/json" "fmt" - "io" + "io/ioutil" "log" - "math/big" "os" "path/filepath" "runtime" - "strings" "sync" "time" - "unicode" "github.com/sergi/go-diff/diffmatchpatch" "golang.org/x/tools/internal/bug" @@ -36,108 +32,74 @@ type diffstat struct { } var ( - mu sync.Mutex // serializes writes and protects ignored - difffd io.Writer - ignored int // lots of the diff calls have 0 diffs -) + ignoredMu sync.Mutex + ignored int // counter of diff requests on equal strings -var fileonce sync.Once + diffStatsOnce sync.Once + diffStats *os.File // never closed +) +// save writes a JSON record of statistics about diff requests to a temporary file. func (s *diffstat) save() { - // save log records in a file in os.TempDir(). - // diff is frequently called with identical strings, so - // these are somewhat compressed out - fileonce.Do(func() { - fname := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-diff-%x", os.Getpid())) - fd, err := os.Create(fname) + diffStatsOnce.Do(func() { + f, err := ioutil.TempFile("", "gopls-diff-stats-*") if err != nil { - // now what? + log.Printf("can't create diff stats temp file: %v", err) // e.g. disk full + return } - difffd = fd + diffStats = f }) + if diffStats == nil { + return + } - mu.Lock() - defer mu.Unlock() + // diff is frequently called with equal strings, + // so we count repeated instances but only print every 15th. + ignoredMu.Lock() if s.Oldedits == 0 && s.Newedits == 0 { + ignored++ if ignored < 15 { - // keep track of repeated instances of no diffs - // but only print every 15th - ignored++ + ignoredMu.Unlock() return } - s.Ignored = ignored + 1 - } else { - s.Ignored = ignored } + s.Ignored = ignored ignored = 0 - // it would be really nice to see why diff was called - _, f, l, ok := runtime.Caller(2) - if ok { - var fname string - fname = filepath.Base(f) // diff is only called from a few places - s.Stack = fmt.Sprintf("%s:%d", fname, l) + ignoredMu.Unlock() + + // Record the name of the file in which diff was called. + // There aren't many calls, so only the base name is needed. + if _, file, line, ok := runtime.Caller(2); ok { + s.Stack = fmt.Sprintf("%s:%d", filepath.Base(file), line) } x, err := json.Marshal(s) if err != nil { - log.Print(err) // failure to print statistics should not stop gopls + log.Fatalf("internal error marshalling JSON: %v", err) } - fmt.Fprintf(difffd, "%s\n", x) + fmt.Fprintf(diffStats, "%s\n", x) } -// save encrypted versions of the broken input and return the file name -// (the saved strings will have the same diff behavior as the user's strings) +// disaster is called when the diff algorithm panics or produces a +// diff that cannot be applied. It saves the broken input in a +// new temporary file and logs the file name, which is returned. func disaster(before, after string) string { - // encrypt before and after for privacy. (randomized monoalphabetic cipher) - // got will contain the substitution cipher - // for the runes in before and after - got := map[rune]rune{} - for _, r := range before { - got[r] = ' ' // value doesn't matter - } - for _, r := range after { - got[r] = ' ' - } - repl := initrepl(len(got)) - i := 0 - for k := range got { // randomized - got[k] = repl[i] - i++ - } - // use got to encrypt before and after - subst := func(r rune) rune { return got[r] } - first := strings.Map(subst, before) - second := strings.Map(subst, after) - - // one failure per session is enough, and more private. - // this saves the last one. - fname := fmt.Sprintf("%s/gopls-failed-%x", os.TempDir(), os.Getpid()) - fd, err := os.Create(fname) - defer fd.Close() - _, err = fmt.Fprintf(fd, "%s\n%s\n", first, second) - if err != nil { - // what do we tell the user? + // We use the pid to salt the name, not os.TempFile, + // so that each process creates at most one file. + // One is sufficient for a bug report. + filename := fmt.Sprintf("%s/gopls-diff-bug-%x", os.TempDir(), os.Getpid()) + + // We use NUL as a separator: it should never appear in Go source. + data := before + "\x00" + after + + if err := ioutil.WriteFile(filename, []byte(data), 0600); err != nil { + log.Printf("failed to write diff bug report: %v", err) return "" } - // ask the user to send us the file, somehow - return fname -} -func initrepl(n int) []rune { - repl := make([]rune, 0, n) - for r := rune(0); len(repl) < n; r++ { - if unicode.IsLetter(r) || unicode.IsNumber(r) { - repl = append(repl, r) - } - } - // randomize repl - rdr := rand.Reader - lim := big.NewInt(int64(len(repl))) - for i := 1; i < n; i++ { - v, _ := rand.Int(rdr, lim) - k := v.Int64() - repl[i], repl[k] = repl[k], repl[i] - } - return repl + // TODO(adonovan): is there a better way to surface this? + log.Printf("Bug detected in diff algorithm! Please send file %s to the maintainers of gopls if you are comfortable sharing its contents.", filename) + + return filename } // BothDiffs edits calls both the new and old diffs, checks that the new diffs @@ -145,7 +107,7 @@ func initrepl(n int) []rune { func BothDiffs(before, after string) (edits []diff.Edit) { // The new diff code contains a lot of internal checks that panic when they // fail. This code catches the panics, or other failures, tries to save - // the failing example (and ut wiykd ask the user to send it back to us, and + // the failing example (and it would ask the user to send it back to us, and // changes options.newDiff to 'old', if only we could figure out how.) stat := diffstat{Before: len(before), After: len(after)} now := time.Now() diff --git a/gopls/internal/hooks/diff_test.go b/gopls/internal/hooks/diff_test.go index acc5d29991f..a46bf3b2d28 100644 --- a/gopls/internal/hooks/diff_test.go +++ b/gopls/internal/hooks/diff_test.go @@ -5,11 +5,9 @@ package hooks import ( - "fmt" "io/ioutil" "os" "testing" - "unicode/utf8" "golang.org/x/tools/internal/diff/difftest" ) @@ -18,40 +16,18 @@ func TestDiff(t *testing.T) { difftest.DiffTest(t, ComputeEdits) } -func TestRepl(t *testing.T) { - t.Skip("just for checking repl by looking at it") - repl := initrepl(800) - t.Errorf("%q", string(repl)) - t.Errorf("%d", len(repl)) -} - func TestDisaster(t *testing.T) { - a := "This is a string,(\u0995) just for basic functionality" - b := "Ths is another string, (\u0996) to see if disaster will store stuff correctly" + a := "This is a string,(\u0995) just for basic\nfunctionality" + b := "This is another string, (\u0996) to see if disaster will store stuff correctly" fname := disaster(a, b) buf, err := ioutil.ReadFile(fname) if err != nil { - t.Errorf("error %v reading %s", err, fname) - } - var x, y string - n, err := fmt.Sscanf(string(buf), "%s\n%s\n", &x, &y) - if n != 2 { - t.Errorf("got %d, expected 2", n) - t.Logf("read %q", string(buf)) - } - if a == x || b == y { - t.Error("failed to encrypt") - } - err = os.Remove(fname) - if err != nil { - t.Errorf("%v removing %s", err, fname) + t.Fatal(err) } - alen, blen := utf8.RuneCount([]byte(a)), utf8.RuneCount([]byte(b)) - xlen, ylen := utf8.RuneCount([]byte(x)), utf8.RuneCount([]byte(y)) - if alen != xlen { - t.Errorf("a; got %d, expected %d", xlen, alen) + if string(buf) != a+"\x00"+b { + t.Error("failed to record original strings") } - if blen != ylen { - t.Errorf("b: got %d expected %d", ylen, blen) + if err := os.Remove(fname); err != nil { + t.Error(err) } } From 42cb7bed6e70327aed112a0cc65bf6fe50a7eb59 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 26 Oct 2022 18:57:08 -0400 Subject: [PATCH 377/723] gopls/internal/lsp: improve the Go version deprecation message Improve the Go version deprecation message to be a warning at 1.13-15, and provide better instructions for making it go away. Clarify support in the gopls README. Change-Id: I6b08e0bd698f5c085eee7a851a130c53affb8ab5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/445581 Run-TryBot: Robert Findley Reviewed-by: Hyang-Ah Hana Kim Auto-Submit: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/README.md | 17 ++++-- gopls/internal/lsp/general.go | 56 ++++++++++++++++--- gopls/internal/lsp/general_test.go | 43 ++++++++++++++ .../regtest/diagnostics/diagnostics_test.go | 2 +- .../regtest/workspace/workspace_test.go | 14 +++-- 5 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 gopls/internal/lsp/general_test.go diff --git a/gopls/README.md b/gopls/README.md index 646419b7d06..9692a1d0866 100644 --- a/gopls/README.md +++ b/gopls/README.md @@ -72,17 +72,22 @@ If you are having issues with `gopls`, please follow the steps described in the `gopls` follows the [Go Release Policy](https://golang.org/doc/devel/release.html#policy), meaning that it officially supports the last 2 major Go releases. Per -[issue #39146](golang.org/issues/39146), we attempt to maintain best-effort +[issue #39146](https://go.dev/issues/39146), we attempt to maintain best-effort support for the last 4 major Go releases, but this support extends only to not breaking the build and avoiding easily fixable regressions. -The following table shows the final gopls version that supports being built -with a given Go version. Go releases more recent than any in the table can -build any version of gopls. +In the context of this discussion, gopls "supports" a Go version if it supports +being built with that Go version as well as integrating with the `go` command +of that Go version. -| Go Version | Final gopls Version With Support | -| ----------- | -------------------------------- | +The following table shows the final gopls version that supports a given Go +version. Go releases more recent than any in the table can be used with any +version of gopls. + +| Go Version | Final gopls version with support (without warnings) | +| ----------- | --------------------------------------------------- | | Go 1.12 | [gopls@v0.7.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.7.5) | +| Go 1.15 | [gopls@v0.9.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.9.5) | Our extended support is enforced via [continuous integration with older Go versions](doc/contributing.md#ci). This legacy Go CI may not block releases: diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index da4e4bfb89c..d387c6d43ab 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -226,11 +226,54 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa return nil } -// OldestSupportedGoVersion is the last X in Go 1.X that we support. +// GoVersionTable maps Go versions to the gopls version in which support will +// be deprecated, and the final gopls version supporting them without warnings. +// Keep this in sync with gopls/README.md // -// Mutable for testing, since we won't otherwise run CI on unsupported Go -// versions. -var OldestSupportedGoVersion = 16 +// Must be sorted in ascending order of Go version. +// +// Mutable for testing. +var GoVersionTable = []GoVersionSupport{ + {12, "", "v0.7.5"}, + {15, "v0.11.0", "v0.9.5"}, +} + +// GoVersionSupport holds information about end-of-life Go version support. +type GoVersionSupport struct { + GoVersion int + DeprecatedVersion string // if unset, the version is already deprecated + InstallGoplsVersion string +} + +// OldestSupportedGoVersion is the last X in Go 1.X that this version of gopls +// supports. +func OldestSupportedGoVersion() int { + return GoVersionTable[len(GoVersionTable)-1].GoVersion + 1 +} + +func versionMessage(oldestVersion int) (string, protocol.MessageType) { + for _, v := range GoVersionTable { + if oldestVersion <= v.GoVersion { + var msgBuilder strings.Builder + + mType := protocol.Error + fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", oldestVersion) + if v.DeprecatedVersion != "" { + // not deprecated yet, just a warning + fmt.Fprintf(&msgBuilder, ", which will be unsupported by gopls %s. ", v.DeprecatedVersion) + mType = protocol.Warning + } else { + fmt.Fprint(&msgBuilder, ", which is not supported by this version of gopls. ") + } + fmt.Fprintf(&msgBuilder, "Please upgrade to Go 1.%d or later and reinstall gopls. ", OldestSupportedGoVersion()) + fmt.Fprintf(&msgBuilder, "If you can't upgrade and want this message to go away, please install gopls %s. ", v.InstallGoplsVersion) + fmt.Fprint(&msgBuilder, "See https://go.dev/s/gopls-support-policy for more details.") + + return msgBuilder.String(), mType + } + } + return "", 0 +} // checkViewGoVersions checks whether any Go version used by a view is too old, // raising a showMessage notification if so. @@ -245,10 +288,9 @@ func (s *Server) checkViewGoVersions() { } } - if oldestVersion >= 0 && oldestVersion < OldestSupportedGoVersion { - msg := fmt.Sprintf("Found Go version 1.%d, which is unsupported. Please upgrade to Go 1.%d or later.", oldestVersion, OldestSupportedGoVersion) + if msg, mType := versionMessage(oldestVersion); msg != "" { s.eventuallyShowMessage(context.Background(), &protocol.ShowMessageParams{ - Type: protocol.Error, + Type: mType, Message: msg, }) } diff --git a/gopls/internal/lsp/general_test.go b/gopls/internal/lsp/general_test.go new file mode 100644 index 00000000000..55f07e4ed91 --- /dev/null +++ b/gopls/internal/lsp/general_test.go @@ -0,0 +1,43 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lsp + +import ( + "strings" + "testing" + + "golang.org/x/tools/gopls/internal/lsp/protocol" +) + +func TestVersionMessage(t *testing.T) { + tests := []struct { + goVer int + wantContains []string // string fragments that we expect to see + wantType protocol.MessageType + }{ + {12, []string{"1.12", "not supported", "upgrade to Go 1.16", "install gopls v0.7.5"}, protocol.Error}, + {13, []string{"1.13", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning}, + {15, []string{"1.15", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning}, + {16, nil, 0}, + } + + for _, test := range tests { + gotMsg, gotType := versionMessage(test.goVer) + + if len(test.wantContains) == 0 && gotMsg != "" { + t.Errorf("versionMessage(%d) = %q, want \"\"", test.goVer, gotMsg) + } + + for _, want := range test.wantContains { + if !strings.Contains(gotMsg, want) { + t.Errorf("versionMessage(%d) = %q, want containing %q", test.goVer, gotMsg, want) + } + } + + if gotType != test.wantType { + t.Errorf("versionMessage(%d) = returned message type %d, want %d", test.goVer, gotType, test.wantType) + } + } +} diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index f0082073326..18c022cb2a5 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1358,7 +1358,7 @@ func _() { func TestEnableAllExperiments(t *testing.T) { // Before the oldest supported Go version, gopls sends a warning to upgrade // Go, which fails the expectation below. - testenv.NeedsGo1Point(t, lsp.OldestSupportedGoVersion) + testenv.NeedsGo1Point(t, lsp.OldestSupportedGoVersion()) const mod = ` -- go.mod -- diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 916d40f1eb3..e92ba8dbf93 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1248,7 +1248,7 @@ import ( // supported. func TestOldGoNotification_SupportedVersion(t *testing.T) { v := goVersion(t) - if v < lsp.OldestSupportedGoVersion { + if v < lsp.OldestSupportedGoVersion() { t.Skipf("go version 1.%d is unsupported", v) } @@ -1267,7 +1267,7 @@ func TestOldGoNotification_SupportedVersion(t *testing.T) { // legacy Go versions (see also TestOldGoNotification_Fake) func TestOldGoNotification_UnsupportedVersion(t *testing.T) { v := goVersion(t) - if v >= lsp.OldestSupportedGoVersion { + if v >= lsp.OldestSupportedGoVersion() { t.Skipf("go version 1.%d is supported", v) } @@ -1291,10 +1291,12 @@ func TestOldGoNotification_Fake(t *testing.T) { if err != nil { t.Fatal(err) } - defer func(v int) { - lsp.OldestSupportedGoVersion = v - }(lsp.OldestSupportedGoVersion) - lsp.OldestSupportedGoVersion = goversion + 1 + defer func(t []lsp.GoVersionSupport) { + lsp.GoVersionTable = t + }(lsp.GoVersionTable) + lsp.GoVersionTable = []lsp.GoVersionSupport{ + {GoVersion: goversion, InstallGoplsVersion: "v1.0.0"}, + } Run(t, "", func(t *testing.T, env *Env) { env.Await( From 7fba77ce5d82cbc25da134d6d95d45aec52ae4c6 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 27 Oct 2022 17:55:52 -0400 Subject: [PATCH 378/723] gopls/internal/lsp/source: remove deprecated settings from EnableAllExperiments VSCode Go Nightly uses `allExperiments` setting which triggers calling this option. It doesn't make sense to add the settings that are scheduled to be deleted. Change-Id: I443d7b1722feafee04b6c63a06ff514a396c5d50 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446095 Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/source/options.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 89442a32bd6..c690d29cea9 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -844,8 +844,6 @@ func (o *Options) AddStaticcheckAnalyzer(a *analysis.Analyzer, enabled bool, sev // should be enabled in enableAllExperimentMaps. func (o *Options) EnableAllExperiments() { o.SemanticTokens = true - o.ExperimentalUseInvalidMetadata = true - o.ExperimentalWatchedFileDelay = 50 * time.Millisecond } func (o *Options) enableAllExperimentMaps() { From e074ef8db85a3d8176ac25d642946989aa92ee7f Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 28 Oct 2022 09:11:20 -0400 Subject: [PATCH 379/723] gopls/internal: don't show a warning if the Go version is undetected CL 445581 inadvertently removed suppression of the Go version error if the Go version was undetected. Add it back, with a test. Fixes golang/go#56465 Change-Id: I352369096280c8d3423a7345123ec9309359fb58 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446175 gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- gopls/internal/lsp/general.go | 14 +++++++++++--- gopls/internal/lsp/general_test.go | 11 ++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index d387c6d43ab..43973b94ed0 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -251,13 +251,21 @@ func OldestSupportedGoVersion() int { return GoVersionTable[len(GoVersionTable)-1].GoVersion + 1 } -func versionMessage(oldestVersion int) (string, protocol.MessageType) { +// versionMessage returns the warning/error message to display if the user is +// on the given Go version, if any. The goVersion variable is the X in Go 1.X. +// +// If goVersion is invalid (< 0), it returns "", 0. +func versionMessage(goVersion int) (string, protocol.MessageType) { + if goVersion < 0 { + return "", 0 + } + for _, v := range GoVersionTable { - if oldestVersion <= v.GoVersion { + if goVersion <= v.GoVersion { var msgBuilder strings.Builder mType := protocol.Error - fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", oldestVersion) + fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", goVersion) if v.DeprecatedVersion != "" { // not deprecated yet, just a warning fmt.Fprintf(&msgBuilder, ", which will be unsupported by gopls %s. ", v.DeprecatedVersion) diff --git a/gopls/internal/lsp/general_test.go b/gopls/internal/lsp/general_test.go index 55f07e4ed91..a0312ba1b43 100644 --- a/gopls/internal/lsp/general_test.go +++ b/gopls/internal/lsp/general_test.go @@ -13,10 +13,11 @@ import ( func TestVersionMessage(t *testing.T) { tests := []struct { - goVer int + goVersion int wantContains []string // string fragments that we expect to see wantType protocol.MessageType }{ + {-1, nil, 0}, {12, []string{"1.12", "not supported", "upgrade to Go 1.16", "install gopls v0.7.5"}, protocol.Error}, {13, []string{"1.13", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning}, {15, []string{"1.15", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning}, @@ -24,20 +25,20 @@ func TestVersionMessage(t *testing.T) { } for _, test := range tests { - gotMsg, gotType := versionMessage(test.goVer) + gotMsg, gotType := versionMessage(test.goVersion) if len(test.wantContains) == 0 && gotMsg != "" { - t.Errorf("versionMessage(%d) = %q, want \"\"", test.goVer, gotMsg) + t.Errorf("versionMessage(%d) = %q, want \"\"", test.goVersion, gotMsg) } for _, want := range test.wantContains { if !strings.Contains(gotMsg, want) { - t.Errorf("versionMessage(%d) = %q, want containing %q", test.goVer, gotMsg, want) + t.Errorf("versionMessage(%d) = %q, want containing %q", test.goVersion, gotMsg, want) } } if gotType != test.wantType { - t.Errorf("versionMessage(%d) = returned message type %d, want %d", test.goVer, gotType, test.wantType) + t.Errorf("versionMessage(%d) = returned message type %d, want %d", test.goVersion, gotType, test.wantType) } } } From f1c8f7f8d5ac22654e879bbaa5c6887bb3782733 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 28 Oct 2022 12:07:01 -0400 Subject: [PATCH 380/723] internal/lsp/source: optimize filter regular expression When the default filter glob changed to **/node_modules, the performance of BenchmarkWorkspaceSymbols was observed to degrade by an astonishing 20%. (CPU profiles of the benchmark reported that the Disallow functions percentage had increased only slightly, but these measures are misleading since the benchmark has a very CPU-intensive set-up step, so all the percentages are quotients of this figure, masking their relative importance to the small region during which the benchmark timer is running.) This change removes the unnecessary ^.* prefix from the generated regular expression. Really the regexp package ought to do this. Also, minor cleanups and tweaks to the surrounding code. Change-Id: I806aad810ce2e7bbfb2c9b04009d8db752a3b10d Reviewed-on: https://go-review.googlesource.com/c/tools/+/446177 Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/source/workspace_symbol.go | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/gopls/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go index bd1e7b12adc..ee4e020e257 100644 --- a/gopls/internal/lsp/source/workspace_symbol.go +++ b/gopls/internal/lsp/source/workspace_symbol.go @@ -379,6 +379,7 @@ func NewFilterer(rawFilters []string) *Filterer { var f Filterer for _, filter := range rawFilters { filter = path.Clean(filepath.ToSlash(filter)) + // TODO(dungtuanle): fix: validate [+-] prefix. op, prefix := filter[0], filter[1:] // convertFilterToRegexp adds "/" at the end of prefix to handle cases where a filter is a prefix of another filter. // For example, it prevents [+foobar, -foo] from excluding "foobar". @@ -391,20 +392,19 @@ func NewFilterer(rawFilters []string) *Filterer { // Disallow return true if the path is excluded from the filterer's filters. func (f *Filterer) Disallow(path string) bool { + // Ensure trailing but not leading slash. path = strings.TrimPrefix(path, "/") - var excluded bool + if !strings.HasSuffix(path, "/") { + path += "/" + } + // TODO(adonovan): opt: iterate in reverse and break at first match. + excluded := false for i, filter := range f.filters { - path := path - if !strings.HasSuffix(path, "/") { - path += "/" - } - if !filter.MatchString(path) { - continue + if filter.MatchString(path) { + excluded = f.excluded[i] // last match wins } - excluded = f.excluded[i] } - return excluded } @@ -419,6 +419,7 @@ func convertFilterToRegexp(filter string) *regexp.Regexp { ret.WriteString("^") segs := strings.Split(filter, "/") for _, seg := range segs { + // Inv: seg != "" since path is clean. if seg == "**" { ret.WriteString(".*") } else { @@ -426,8 +427,15 @@ func convertFilterToRegexp(filter string) *regexp.Regexp { } ret.WriteString("/") } + pattern := ret.String() + + // Remove unnecessary "^.*" prefix, which increased + // BenchmarkWorkspaceSymbols time by ~20% (even though + // filter CPU time increased by only by ~2.5%) when the + // default filter was changed to "**/node_modules". + pattern = strings.TrimPrefix(pattern, "^.*") - return regexp.MustCompile(ret.String()) + return regexp.MustCompile(pattern) } // symbolFile holds symbol information for a single file. From e172e97c522ebb5261f74f70c3d84dadc57560c8 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 5 Oct 2022 11:49:35 -0400 Subject: [PATCH 381/723] go/types/typeutil: break recursion through anonymous interfaces In a type such as type X interface { m() []*interface { X } } the traversal never encounters the named type X in the result of m since Interface.Methods expands it to a set of methods, one that includes m, causing the traversal to get stuck. This change uses an alternative, shallow hash function on the types of interface methods to avoid the possibility of getting stuck in such a cycle. (An earlier draft used a stack of interface types to detect cycles, but the logic of caching made this approach quite tricky.) Fixes golang/go#56048 Fixes golang/go#26863 Change-Id: I28a604e6affae5dfdd05a62e405d49a3efc8d709 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439117 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Tim King Run-TryBot: Alan Donovan --- go/types/typeutil/map.go | 77 ++++++++++++++++++++++++++++++++++- go/types/typeutil/map_test.go | 25 +++++++++--- 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/go/types/typeutil/map.go b/go/types/typeutil/map.go index dcc029b8733..7bd2fdb38be 100644 --- a/go/types/typeutil/map.go +++ b/go/types/typeutil/map.go @@ -332,7 +332,9 @@ func (h Hasher) hashFor(t types.Type) uint32 { // Method order is not significant. // Ignore m.Pkg(). m := t.Method(i) - hash += 3*hashString(m.Name()) + 5*h.Hash(m.Type()) + // Use shallow hash on method signature to + // avoid anonymous interface cycles. + hash += 3*hashString(m.Name()) + 5*h.shallowHash(m.Type()) } // Hash type restrictions. @@ -434,3 +436,76 @@ func (h Hasher) hashPtr(ptr interface{}) uint32 { h.ptrMap[ptr] = hash return hash } + +// shallowHash computes a hash of t without looking at any of its +// element Types, to avoid potential anonymous cycles in the types of +// interface methods. +// +// When an unnamed non-empty interface type appears anywhere among the +// arguments or results of an interface method, there is a potential +// for endless recursion. Consider: +// +// type X interface { m() []*interface { X } } +// +// The problem is that the Methods of the interface in m's result type +// include m itself; there is no mention of the named type X that +// might help us break the cycle. +// (See comment in go/types.identical, case *Interface, for more.) +func (h Hasher) shallowHash(t types.Type) uint32 { + // t is the type of an interface method (Signature), + // its params or results (Tuples), or their immediate + // elements (mostly Slice, Pointer, Basic, Named), + // so there's no need to optimize anything else. + switch t := t.(type) { + case *types.Signature: + var hash uint32 = 604171 + if t.Variadic() { + hash *= 971767 + } + // The Signature/Tuple recursion is always finite + // and invariably shallow. + return hash + 1062599*h.shallowHash(t.Params()) + 1282529*h.shallowHash(t.Results()) + + case *types.Tuple: + n := t.Len() + hash := 9137 + 2*uint32(n) + for i := 0; i < n; i++ { + hash += 53471161 * h.shallowHash(t.At(i).Type()) + } + return hash + + case *types.Basic: + return 45212177 * uint32(t.Kind()) + + case *types.Array: + return 1524181 + 2*uint32(t.Len()) + + case *types.Slice: + return 2690201 + + case *types.Struct: + return 3326489 + + case *types.Pointer: + return 4393139 + + case *typeparams.Union: + return 562448657 + + case *types.Interface: + return 2124679 // no recursion here + + case *types.Map: + return 9109 + + case *types.Chan: + return 9127 + + case *types.Named: + return h.hashPtr(t.Obj()) + + case *typeparams.TypeParam: + return h.hashPtr(t.Obj()) + } + panic(fmt.Sprintf("shallowHash: %T: %v", t, t)) +} diff --git a/go/types/typeutil/map_test.go b/go/types/typeutil/map_test.go index 8cd643e5b48..ee73ff9cfd5 100644 --- a/go/types/typeutil/map_test.go +++ b/go/types/typeutil/map_test.go @@ -244,6 +244,14 @@ func Bar[P Constraint[P]]() {} func Baz[Q any]() {} // The underlying type of Constraint[P] is any. // But Quux is not. func Quux[Q interface{ quux() }]() {} + + +type Issue56048_I interface{ m() interface { Issue56048_I } } +var Issue56048 = Issue56048_I.m + +type Issue56048_Ib interface{ m() chan []*interface { Issue56048_Ib } } +var Issue56048b = Issue56048_Ib.m + ` fset := token.NewFileSet() @@ -296,12 +304,14 @@ func Quux[Q interface{ quux() }]() {} ME1Type = scope.Lookup("ME1Type").Type() ME2 = scope.Lookup("ME2").Type() - Constraint = scope.Lookup("Constraint").Type() - Foo = scope.Lookup("Foo").Type() - Fn = scope.Lookup("Fn").Type() - Bar = scope.Lookup("Foo").Type() - Baz = scope.Lookup("Foo").Type() - Quux = scope.Lookup("Quux").Type() + Constraint = scope.Lookup("Constraint").Type() + Foo = scope.Lookup("Foo").Type() + Fn = scope.Lookup("Fn").Type() + Bar = scope.Lookup("Foo").Type() + Baz = scope.Lookup("Foo").Type() + Quux = scope.Lookup("Quux").Type() + Issue56048 = scope.Lookup("Issue56048").Type() + Issue56048b = scope.Lookup("Issue56048b").Type() ) tmap := new(typeutil.Map) @@ -371,6 +381,9 @@ func Quux[Q interface{ quux() }]() {} {Bar, "Bar", false}, {Baz, "Baz", false}, {Quux, "Quux", true}, + + {Issue56048, "Issue56048", true}, // (not actually about generics) + {Issue56048b, "Issue56048b", true}, // (not actually about generics) } for _, step := range steps { From 739f55d751e0ca58829cdce834ffa707edb3120a Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Fri, 1 Apr 2022 10:59:34 -0400 Subject: [PATCH 382/723] internal/jsonrpc2_v2: rework Connection concurrency This change fixes the semantics of Close to actually wait for in-flight requests before closing the ReadWriteCloser. (Previously, the Close method closed the ReadWriteCloser immediately, which I suspect is what led to many of the failures observed in golang/go#49387 and golang/go#46520.) It achieves this by explicitly tracking the number of in-flight requests, including requests with pending async responses, and explicitly rejecting new Call requests (while keeping the read loop open!) once Close has begun. To make it easier for me to reason about the request lifetimes, I reduced the number of long-lived goroutines from three to just one (the Read loop), with an additional Handler goroutine that runs only while the Handler queue is non-empty. Now, it is clearer (I hope!) that the number of in-flight async requests strictly decreases after Close has begun, even though the Read goroutine continues to read requests (and, importantly, responses) and to forward Notifications to the preempter. For golang/go#49387 For golang/go#46520 Change-Id: Idf5960f848108a7ced78c5382099c8692e9b181e Reviewed-on: https://go-review.googlesource.com/c/tools/+/388134 gopls-CI: kokoro Run-TryBot: Bryan Mills TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- internal/jsonrpc2_v2/conn.go | 788 ++++++++++++++++----------- internal/jsonrpc2_v2/net.go | 33 +- internal/jsonrpc2_v2/serve.go | 17 +- internal/jsonrpc2_v2/serve_go116.go | 19 + internal/jsonrpc2_v2/serve_pre116.go | 30 + internal/jsonrpc2_v2/serve_test.go | 83 ++- 6 files changed, 621 insertions(+), 349 deletions(-) create mode 100644 internal/jsonrpc2_v2/serve_go116.go create mode 100644 internal/jsonrpc2_v2/serve_pre116.go diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 7995f404e58..3d59fc61d1d 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -7,12 +7,15 @@ package jsonrpc2 import ( "context" "encoding/json" + "errors" "fmt" "io" "sync" "sync/atomic" + "time" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/event/label" "golang.org/x/tools/internal/event/tag" ) @@ -48,6 +51,10 @@ type ConnectionOptions struct { // Handler is used as the queued message handler for inbound messages. // If nil, all responses will be ErrNotHandled. Handler Handler + // OnInternalError, if non-nil, is called with any internal errors that occur + // while serving the connection, such as protocol errors or invariant + // violations. (If nil, internal errors result in panics.) + OnInternalError func(error) } // Connection manages the jsonrpc2 protocol, connecting responses back to their @@ -57,34 +64,65 @@ type ConnectionOptions struct { type Connection struct { seq int64 // must only be accessed using atomic operations - closeOnce sync.Once - closer io.Closer + stateMu sync.Mutex + state inFlightState // accessed only in updateInFlight - writer chan Writer - outgoing chan map[ID]chan<- *Response - incoming chan map[ID]*incoming - async *async + closer io.Closer // shuts down connection when Close has been called or the reader fails + closeErr chan error // 1-buffered; stores the error from closer.Close + writer chan Writer // 1-buffered; stores the writer when not in use + + handler Handler + + onInternalError func(error) } -type AsyncCall struct { - id ID - response chan *Response // the channel a response will be delivered on - result chan asyncResult - endSpan func() // close the tracing span when all processing for the message is complete +// inFlightState records the state of the incoming and outgoing calls on a +// Connection. +type inFlightState struct { + closing bool // disallow enqueuing further requests, and close the Closer when transitioning to idle + readErr error + + outgoing map[ID]*AsyncCall // calls only + + // incoming stores the total number of incoming calls and notifications + // that have not yet written or processed a result. + incoming int + + incomingByID map[ID]*incomingRequest // calls only + + // handlerQueue stores the backlog of calls and notifications that were not + // already handled by a preempter. + // The queue does not include the request currently being handled (if any). + handlerQueue []*incomingRequest + handlerRunning bool + + closed bool // true after the closer has been invoked } -type asyncResult struct { - result []byte - err error +// updateInFlight locks the state of the connection's in-flight requests, allows +// f to mutate that state, and closes the connection if it is idle and either +// is closing or has a read error. +func (c *Connection) updateInFlight(f func(*inFlightState)) { + c.stateMu.Lock() + defer c.stateMu.Unlock() + + s := &c.state + + f(s) + + idle := s.incoming == 0 && len(s.outgoing) == 0 && !s.handlerRunning + if idle && (s.closing || s.readErr != nil) && !s.closed { + c.closeErr <- c.closer.Close() + s.closed = true + } } -// incoming is used to track an incoming request as it is being handled -type incoming struct { - request *Request // the request being processed - baseCtx context.Context // a base context for the message processing - done func() // a function called when all processing for the message is complete - handleCtx context.Context // the context for handling the message, child of baseCtx - cancel func() // a function that cancels the handling context +// incomingRequest is used to track an incoming request as it is being handled +type incomingRequest struct { + *Request // the request being processed + ctx context.Context + cancel context.CancelFunc + endSpan func() // called (and set to nil) when the response is sent } // Bind returns the options unmodified. @@ -94,41 +132,35 @@ func (o ConnectionOptions) Bind(context.Context, *Connection) (ConnectionOptions // newConnection creates a new connection and runs it. // This is used by the Dial and Serve functions to build the actual connection. -func newConnection(ctx context.Context, rwc io.ReadWriteCloser, binder Binder) (*Connection, error) { +func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder) (*Connection, error) { + // TODO: Should we create a new event span here? + // This will propagate cancellation from ctx; should it? + ctx := notDone{bindCtx} + c := &Connection{ closer: rwc, + closeErr: make(chan error, 1), writer: make(chan Writer, 1), - outgoing: make(chan map[ID]chan<- *Response, 1), - incoming: make(chan map[ID]*incoming, 1), - async: newAsync(), } - options, err := binder.Bind(ctx, c) + options, err := binder.Bind(bindCtx, c) if err != nil { return nil, err } - if options.Framer == nil { - options.Framer = HeaderFramer() - } - if options.Preempter == nil { - options.Preempter = defaultHandler{} + framer := options.Framer + if framer == nil { + framer = HeaderFramer() } - if options.Handler == nil { - options.Handler = defaultHandler{} + c.handler = options.Handler + if c.handler == nil { + c.handler = defaultHandler{} } - c.outgoing <- make(map[ID]chan<- *Response) - c.incoming <- make(map[ID]*incoming) - // the goroutines started here will continue until the underlying stream is closed - reader := options.Framer.Reader(rwc) - readToQueue := make(chan *incoming) - queueToDeliver := make(chan *incoming) - go c.readIncoming(ctx, reader, readToQueue) - go c.manageQueue(ctx, options.Preempter, readToQueue, queueToDeliver) - go c.deliverMessages(ctx, options.Handler, queueToDeliver) - - // releaseing the writer must be the last thing we do in case any requests - // are blocked waiting for the connection to be ready - c.writer <- options.Framer.Writer(rwc) + c.onInternalError = options.OnInternalError + + c.writer <- framer.Writer(rwc) + reader := framer.Reader(rwc) + // The goroutines started here will continue until the underlying stream is closed. + go c.readIncoming(ctx, reader, options.Preempter) return c, nil } @@ -146,12 +178,7 @@ func (c *Connection) Notify(ctx context.Context, method string, params interface ) event.Metric(ctx, tag.Started.Of(1)) err = c.write(ctx, notify) - switch { - case err != nil: - event.Label(ctx, tag.StatusCode.Of("ERROR")) - default: - event.Label(ctx, tag.StatusCode.Of("OK")) - } + labelStatus(ctx, err) done() return err } @@ -162,375 +189,439 @@ func (c *Connection) Notify(ctx context.Context, method string, params interface // You do not have to wait for the response, it can just be ignored if not needed. // If sending the call failed, the response will be ready and have the error in it. func (c *Connection) Call(ctx context.Context, method string, params interface{}) *AsyncCall { - result := &AsyncCall{ - id: Int64ID(atomic.AddInt64(&c.seq, 1)), - result: make(chan asyncResult, 1), - } - // generate a new request identifier - call, err := NewCall(result.id, method, params) - if err != nil { - //set the result to failed - result.result <- asyncResult{err: fmt.Errorf("marshaling call parameters: %w", err)} - return result - } + // Generate a new request identifier. + id := Int64ID(atomic.AddInt64(&c.seq, 1)) ctx, endSpan := event.Start(ctx, method, tag.Method.Of(method), tag.RPCDirection.Of(tag.Outbound), - tag.RPCID.Of(fmt.Sprintf("%q", result.id)), + tag.RPCID.Of(fmt.Sprintf("%q", id)), ) - result.endSpan = endSpan - event.Metric(ctx, tag.Started.Of(1)) - // We have to add ourselves to the pending map before we send, otherwise we - // are racing the response. - // rchan is buffered in case the response arrives without a listener. - result.response = make(chan *Response, 1) - outgoing, ok := <-c.outgoing - if !ok { - // If the call failed due to (say) an I/O error or broken pipe, attribute it - // as such. (If the error is nil, then the connection must have been shut - // down cleanly.) - err := c.async.wait() - if err == nil { + + ac := &AsyncCall{ + id: id, + ready: make(chan struct{}), + ctx: ctx, + endSpan: endSpan, + } + // When this method returns, either ac is retired, or the request has been + // written successfully and the call is awaiting a response (to be provided by + // the readIncoming goroutine). + + call, err := NewCall(ac.id, method, params) + if err != nil { + ac.retire(&Response{ID: id, Error: fmt.Errorf("marshaling call parameters: %w", err)}) + return ac + } + + c.updateInFlight(func(s *inFlightState) { + if s.closing { err = ErrClientClosing + return } - - resp, respErr := NewResponse(result.id, nil, err) - if respErr != nil { - panic(fmt.Errorf("unexpected error from NewResponse: %w", respErr)) + if s.readErr != nil { + // We must not start a new Call request if the read end of the connection + // has already failed: a Call request requires a response, but with the + // read side broken we have no way to receive that response. + err = fmt.Errorf("%w: %v", ErrClientClosing, s.readErr) + return } - result.response <- resp - return result + if s.outgoing == nil { + s.outgoing = make(map[ID]*AsyncCall) + } + s.outgoing[ac.id] = ac + }) + if err != nil { + ac.retire(&Response{ID: id, Error: err}) + return ac } - outgoing[result.id] = result.response - c.outgoing <- outgoing - // now we are ready to send + + event.Metric(ctx, tag.Started.Of(1)) if err := c.write(ctx, call); err != nil { - // sending failed, we will never get a response, so deliver a fake one - r, _ := NewResponse(result.id, nil, err) - c.incomingResponse(r) + // Sending failed. We will never get a response, so deliver a fake one if it + // wasn't already retired by the connection breaking. + c.updateInFlight(func(s *inFlightState) { + if s.outgoing[ac.id] == ac { + delete(s.outgoing, ac.id) + ac.retire(&Response{ID: id, Error: err}) + } else { + // ac was already retired by the readIncoming goroutine: + // perhaps our write raced with the Read side of the connection breaking. + } + }) } - return result + return ac +} + +type AsyncCall struct { + id ID + ready chan struct{} // closed after response has been set and span has been ended + response *Response + ctx context.Context // for event logging only + endSpan func() // close the tracing span when all processing for the message is complete } // ID used for this call. // This can be used to cancel the call if needed. -func (a *AsyncCall) ID() ID { return a.id } +func (ac *AsyncCall) ID() ID { return ac.id } // IsReady can be used to check if the result is already prepared. // This is guaranteed to return true on a result for which Await has already // returned, or a call that failed to send in the first place. -func (a *AsyncCall) IsReady() bool { +func (ac *AsyncCall) IsReady() bool { select { - case r := <-a.result: - a.result <- r + case <-ac.ready: return true default: return false } } -// Await the results of a Call. +// retire processes the response to the call. +func (ac *AsyncCall) retire(response *Response) { + select { + case <-ac.ready: + panic(fmt.Sprintf("jsonrpc2: retire called twice for ID %v", ac.id)) + default: + } + + ac.response = response + labelStatus(ac.ctx, response.Error) + ac.endSpan() + // Allow the trace context, which may retain a lot of reachable values, + // to be garbage-collected. + ac.ctx, ac.endSpan = nil, nil + + close(ac.ready) +} + +// Await waits for (and decodes) the results of a Call. // The response will be unmarshaled from JSON into the result. -func (a *AsyncCall) Await(ctx context.Context, result interface{}) error { - defer a.endSpan() - var r asyncResult +func (ac *AsyncCall) Await(ctx context.Context, result interface{}) error { select { - case response := <-a.response: - // response just arrived, prepare the result - switch { - case response.Error != nil: - r.err = response.Error - event.Label(ctx, tag.StatusCode.Of("ERROR")) - default: - r.result = response.Result - event.Label(ctx, tag.StatusCode.Of("OK")) - } - case r = <-a.result: - // result already available case <-ctx.Done(): - event.Label(ctx, tag.StatusCode.Of("CANCELLED")) return ctx.Err() + case <-ac.ready: } - // refill the box for the next caller - a.result <- r - // and unpack the result - if r.err != nil { - return r.err + if ac.response.Error != nil { + return ac.response.Error } - if result == nil || len(r.result) == 0 { + if result == nil { return nil } - return json.Unmarshal(r.result, result) + return json.Unmarshal(ac.response.Result, result) } // Respond delivers a response to an incoming Call. // // Respond must be called exactly once for any message for which a handler // returns ErrAsyncResponse. It must not be called for any other message. -func (c *Connection) Respond(id ID, result interface{}, rerr error) error { - pending := <-c.incoming - defer func() { c.incoming <- pending }() - entry, found := pending[id] - if !found { - return nil +func (c *Connection) Respond(id ID, result interface{}, err error) error { + var req *incomingRequest + c.updateInFlight(func(s *inFlightState) { + req = s.incomingByID[id] + }) + if req == nil { + return c.internalErrorf("Request not found for ID %v", id) + } + + if err == ErrAsyncResponse { + // Respond is supposed to supply the asynchronous response, so it would be + // confusing to call Respond with an error that promises to call Respond + // again. + err = c.internalErrorf("Respond called with ErrAsyncResponse for %q", req.Method) } - delete(pending, id) - return c.respond(entry, result, rerr) + return c.processResult("Respond", req, result, err) } -// Cancel is used to cancel an inbound message by ID, it does not cancel -// outgoing messages. -// This is only used inside a message handler that is layering a -// cancellation protocol on top of JSON RPC 2. -// It will not complain if the ID is not a currently active message, and it will -// not cause any messages that have not arrived yet with that ID to be +// Cancel cancels the Context passed to the Handle call for the inbound message +// with the given ID. +// +// Cancel will not complain if the ID is not a currently active message, and it +// will not cause any messages that have not arrived yet with that ID to be // cancelled. func (c *Connection) Cancel(id ID) { - pending := <-c.incoming - defer func() { c.incoming <- pending }() - if entry, found := pending[id]; found && entry.cancel != nil { - entry.cancel() - entry.cancel = nil + var req *incomingRequest + c.updateInFlight(func(s *inFlightState) { + req = s.incomingByID[id] + }) + if req != nil { + req.cancel() } } // Wait blocks until the connection is fully closed, but does not close it. func (c *Connection) Wait() error { - return c.async.wait() + err := <-c.closeErr + c.closeErr <- err + return err } -// Close can be used to close the underlying stream, and then wait for the connection to -// fully shut down. -// This does not cancel in flight requests, but waits for them to gracefully complete. +// Close stops accepting new requests, waits for in-flight requests and enqueued +// Handle calls to complete, and then closes the underlying stream. +// +// After the start of a Close, notification requests (that lack IDs and do not +// receive responses) will continue to be passed to the Preempter, but calls +// with IDs will receive immediate responses with ErrServerClosing, and no new +// requests (not even notifications!) will be enqueued to the Handler. func (c *Connection) Close() error { - // close the underlying stream - c.closeOnce.Do(func() { - if err := c.closer.Close(); err != nil { - c.async.setError(err) - } - }) - // and then wait for it to cause the connection to close + // Stop handling new requests, and interrupt the reader (by closing the + // connection) as soon as the active requests finish. + c.updateInFlight(func(s *inFlightState) { s.closing = true }) + return c.Wait() } // readIncoming collects inbound messages from the reader and delivers them, either responding // to outgoing calls or feeding requests to the queue. -func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue chan<- *incoming) (err error) { - defer func() { - // Retire any outgoing requests that were still in flight. - // With the Reader no longer being processed, they necessarily cannot receive a response. - outgoing := <-c.outgoing - close(c.outgoing) // Prevent new outgoing requests, which would deadlock. - for id, response := range outgoing { - response <- &Response{ID: id, Error: err} - } - - close(toQueue) - }() - +func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter Preempter) { + var err error for { - // get the next message - // no lock is needed, this is the only reader - msg, n, err := reader.Read(ctx) + var ( + msg Message + n int64 + ) + msg, n, err = reader.Read(ctx) if err != nil { - // The stream failed, we cannot continue - if !isClosingError(err) { - c.async.setError(err) - } - return err + break } + switch msg := msg.(type) { case *Request: - entry := &incoming{ - request: msg, - } - // add a span to the context for this request - labels := append(make([]label.Label, 0, 3), // make space for the id if present - tag.Method.Of(msg.Method), - tag.RPCDirection.Of(tag.Inbound), - ) - if msg.IsCall() { - labels = append(labels, tag.RPCID.Of(fmt.Sprintf("%q", msg.ID))) - } - entry.baseCtx, entry.done = event.Start(ctx, msg.Method, labels...) - event.Metric(entry.baseCtx, - tag.Started.Of(1), - tag.ReceivedBytes.Of(n)) - // in theory notifications cannot be cancelled, but we build them a cancel context anyway - entry.handleCtx, entry.cancel = context.WithCancel(entry.baseCtx) - // if the request is a call, add it to the incoming map so it can be - // cancelled by id - if msg.IsCall() { - pending := <-c.incoming - pending[msg.ID] = entry - c.incoming <- pending - } - // send the message to the incoming queue - toQueue <- entry + c.acceptRequest(ctx, msg, n, preempter) + case *Response: - // If method is not set, this should be a response, in which case we must - // have an id to send the response back to the caller. - c.incomingResponse(msg) + c.updateInFlight(func(s *inFlightState) { + if ac, ok := s.outgoing[msg.ID]; ok { + delete(s.outgoing, msg.ID) + ac.retire(msg) + } else { + // TODO: How should we report unexpected responses? + } + }) + + default: + c.internalErrorf("Read returned an unexpected message of type %T", msg) } } + + c.updateInFlight(func(s *inFlightState) { + s.readErr = err + + // Retire any outgoing requests that were still in flight: with the Reader no + // longer being processed, they necessarily cannot receive a response. + for id, ac := range s.outgoing { + ac.retire(&Response{ID: id, Error: err}) + } + s.outgoing = nil + }) } -func (c *Connection) incomingResponse(msg *Response) { - var response chan<- *Response - if outgoing, ok := <-c.outgoing; ok { - response = outgoing[msg.ID] - delete(outgoing, msg.ID) - c.outgoing <- outgoing +// acceptRequest either handles msg synchronously or enqueues it to be handled +// asynchronously. +func (c *Connection) acceptRequest(ctx context.Context, msg *Request, msgBytes int64, preempter Preempter) { + // Add a span to the context for this request. + labels := append(make([]label.Label, 0, 3), // Make space for the ID if present. + tag.Method.Of(msg.Method), + tag.RPCDirection.Of(tag.Inbound), + ) + if msg.IsCall() { + labels = append(labels, tag.RPCID.Of(fmt.Sprintf("%q", msg.ID))) } - if response != nil { - response <- msg + ctx, endSpan := event.Start(ctx, msg.Method, labels...) + event.Metric(ctx, + tag.Started.Of(1), + tag.ReceivedBytes.Of(msgBytes)) + + // In theory notifications cannot be cancelled, but we build them a cancel + // context anyway. + ctx, cancel := context.WithCancel(ctx) + req := &incomingRequest{ + Request: msg, + ctx: ctx, + cancel: cancel, + endSpan: endSpan, } -} -// manageQueue reads incoming requests, attempts to process them with the preempter, or queue them -// up for normal handling. -func (c *Connection) manageQueue(ctx context.Context, preempter Preempter, fromRead <-chan *incoming, toDeliver chan<- *incoming) { - defer close(toDeliver) - q := []*incoming{} - ok := true - for { - var nextReq *incoming - if len(q) == 0 { - // no messages in the queue - // if we were closing, then we are done - if !ok { + // If the request is a call, add it to the incoming map so it can be + // cancelled (or responded) by ID. + var err error + c.updateInFlight(func(s *inFlightState) { + s.incoming++ + + if req.IsCall() { + if s.incomingByID[req.ID] != nil { + err = fmt.Errorf("%w: request ID %v already in use", ErrInvalidRequest, req.ID) + req.ID = ID{} // Don't misattribute this error to the existing request. return } - // not closing, but nothing in the queue, so just block waiting for a read - nextReq, ok = <-fromRead - } else { - // we have a non empty queue, so pick whichever of reading or delivering - // that we can make progress on - select { - case nextReq, ok = <-fromRead: - case toDeliver <- q[0]: - //TODO: this causes a lot of shuffling, should we use a growing ring buffer? compaction? - q = q[1:] - } - } - if nextReq != nil { - // TODO: should we allow to limit the queue size? - var result interface{} - rerr := nextReq.handleCtx.Err() - if rerr == nil { - // only preempt if not already cancelled - result, rerr = preempter.Preempt(nextReq.handleCtx, nextReq.request) + + if s.incomingByID == nil { + s.incomingByID = make(map[ID]*incomingRequest) } - switch { - case rerr == ErrNotHandled: - // message not handled, add it to the queue for the main handler - q = append(q, nextReq) - case rerr == ErrAsyncResponse: - // message handled but the response will come later - default: - // anything else means the message is fully handled - c.reply(nextReq, result, rerr) + s.incomingByID[req.ID] = req + + if s.closing { + // When closing, reject all new Call requests, even if they could + // theoretically be handled by the preempter. The preempter could return + // ErrAsyncResponse, which would increase the amount of work in flight + // when we're trying to ensure that it strictly decreases. + err = ErrServerClosing + return } } + }) + if err != nil { + c.processResult("acceptRequest", req, nil, err) + return + } + + if preempter != nil { + result, err := preempter.Preempt(req.ctx, req.Request) + + if req.IsCall() && errors.Is(err, ErrAsyncResponse) { + // This request will remain in flight until Respond is called for it. + return + } + + if !errors.Is(err, ErrNotHandled) { + c.processResult("Preempt", req, result, err) + return + } + } + + c.updateInFlight(func(s *inFlightState) { + if s.closing { + // If the connection is closing, don't enqueue anything to the handler — not + // even notifications. That ensures that if the handler continues to make + // progress, it will eventually become idle and close the connection. + err = ErrServerClosing + return + } + + // We enqueue requests that have not been preempted to an unbounded slice. + // Unfortunately, we cannot in general limit the size of the handler + // queue: we have to read every response that comes in on the wire + // (because it may be responding to a request issued by, say, an + // asynchronous handler), and in order to get to that response we have + // to read all of the requests that came in ahead of it. + s.handlerQueue = append(s.handlerQueue, req) + if !s.handlerRunning { + // We start the handleAsync goroutine when it has work to do, and let it + // exit when the queue empties. + // + // Otherwise, in order to synchronize the handler we would need some other + // goroutine (probably readIncoming?) to explicitly wait for handleAsync + // to finish, and that would complicate error reporting: either the error + // report from the goroutine would be blocked on the handler emptying its + // queue (which was tried, and introduced a deadlock detected by + // TestCloseCallRace), or the error would need to be reported separately + // from synchronizing completion. Allowing the handler goroutine to exit + // when idle seems simpler than trying to implement either of those + // alternatives correctly. + s.handlerRunning = true + go c.handleAsync() + } + }) + if err != nil { + c.processResult("acceptRequest", req, nil, err) } } -func (c *Connection) deliverMessages(ctx context.Context, handler Handler, fromQueue <-chan *incoming) { - defer func() { - // Close the underlying ReadWriteCloser if not already closed. We're about - // to mark the Connection as done, so we'd better actually be done! 😅 - // - // TODO(bcmills): This is actually a bit premature, since we may have - // asynchronous handlers still in flight at this point, but it's at least no - // more premature than calling c.async.done at this point (which we were - // already doing). This will get a proper fix in https://go.dev/cl/388134. - c.closeOnce.Do(func() { - if err := c.closer.Close(); err != nil { - c.async.setError(err) +// handleAsync invokes the handler on the requests in the handler queue +// sequentially until the queue is empty. +func (c *Connection) handleAsync() { + for { + var req *incomingRequest + c.updateInFlight(func(s *inFlightState) { + if len(s.handlerQueue) > 0 { + req, s.handlerQueue = s.handlerQueue[0], s.handlerQueue[1:] + } else { + s.handlerRunning = false } }) + if req == nil { + return + } - c.async.done() - }() - - for entry := range fromQueue { - // cancel any messages in the queue that we have a pending cancel for var result interface{} - rerr := entry.handleCtx.Err() - if rerr == nil { - // only deliver if not already cancelled - result, rerr = handler.Handle(entry.handleCtx, entry.request) - } - switch { - case rerr == ErrNotHandled: - // message not handled, report it back to the caller as an error - c.reply(entry, nil, fmt.Errorf("%w: %q", ErrMethodNotFound, entry.request.Method)) - case rerr == ErrAsyncResponse: - // message handled but the response will come later - default: - c.reply(entry, result, rerr) + err := req.ctx.Err() + if err == nil { + // Only deliver to the Handler if not already cancelled. + result, err = c.handler.Handle(req.ctx, req.Request) } + c.processResult(c.handler, req, result, err) } } -// reply is used to reply to an incoming request that has just been handled -func (c *Connection) reply(entry *incoming, result interface{}, rerr error) { - if entry.request.IsCall() { - // we have a call finishing, remove it from the incoming map - pending := <-c.incoming - defer func() { c.incoming <- pending }() - delete(pending, entry.request.ID) +// processResult processes the result of a request and, if appropriate, sends a response. +func (c *Connection) processResult(from interface{}, req *incomingRequest, result interface{}, err error) error { + switch err { + case ErrAsyncResponse: + if !req.IsCall() { + return c.internalErrorf("%#v returned ErrAsyncResponse for a %q Request without an ID", from, req.Method) + } + return nil // This request is still in flight, so don't record the result yet. + case ErrNotHandled, ErrMethodNotFound: + // Add detail describing the unhandled method. + err = fmt.Errorf("%w: %q", ErrMethodNotFound, req.Method) } - if err := c.respond(entry, result, rerr); err != nil { - // no way to propagate this error - //TODO: should we do more than just log it? - event.Error(entry.baseCtx, "jsonrpc2 message delivery failed", err) + + if req.endSpan == nil { + return c.internalErrorf("%#v produced a duplicate %q Response", from, req.Method) } -} -// respond sends a response. -// This is the code shared between reply and SendResponse. -func (c *Connection) respond(entry *incoming, result interface{}, rerr error) error { - var err error - if entry.request.IsCall() { - // send the response - if result == nil && rerr == nil { - // call with no response, send an error anyway - rerr = fmt.Errorf("%w: %q produced no response", ErrInternal, entry.request.Method) + if result != nil && err != nil { + c.internalErrorf("%#v returned a non-nil result with a non-nil error for %s:\n%v\n%#v", from, req.Method, err, result) + result = nil // Discard the spurious result and respond with err. + } + + if req.IsCall() { + if result == nil && err == nil { + err = c.internalErrorf("%#v returned a nil result and nil error for a %q Request that requires a Response", from, req.Method) } - var response *Response - response, err = NewResponse(entry.request.ID, result, rerr) - if err == nil { - // we write the response with the base context, in case the message was cancelled - err = c.write(entry.baseCtx, response) + + response, respErr := NewResponse(req.ID, result, err) + + // The caller could theoretically reuse the request's ID as soon as we've + // sent the response, so ensure that it is removed from the incoming map + // before sending. + c.updateInFlight(func(s *inFlightState) { + delete(s.incomingByID, req.ID) + }) + if respErr == nil { + writeErr := c.write(notDone{req.ctx}, response) + if err == nil { + err = writeErr + } + } else { + err = c.internalErrorf("%#v returned a malformed result for %q: %w", from, req.Method, respErr) } - } else { - switch { - case rerr != nil: - // notification failed - err = fmt.Errorf("%w: %q notification failed: %v", ErrInternal, entry.request.Method, rerr) - rerr = nil - case result != nil: - //notification produced a response, which is an error - err = fmt.Errorf("%w: %q produced unwanted response", ErrInternal, entry.request.Method) - default: - // normal notification finish + } else { // req is a notification + if result != nil { + err = c.internalErrorf("%#v returned a non-nil result for a %q Request without an ID", from, req.Method) + } else if err != nil { + err = fmt.Errorf("%w: %q notification failed: %v", ErrInternal, req.Method, err) + } + if err != nil { + // TODO: can/should we do anything with this error beyond writing it to the event log? + // (Is this the right label to attach to the log?) + event.Label(req.ctx, keys.Err.Of(err)) } } - switch { - case rerr != nil || err != nil: - event.Label(entry.baseCtx, tag.StatusCode.Of("ERROR")) - default: - event.Label(entry.baseCtx, tag.StatusCode.Of("OK")) - } - // and just to be clean, invoke and clear the cancel if needed - if entry.cancel != nil { - entry.cancel() - entry.cancel = nil - } - // mark the entire request processing as done - entry.done() - return err + + labelStatus(req.ctx, err) + + // Cancel the request and finalize the event span to free any associated resources. + req.cancel() + req.endSpan() + req.endSpan = nil + c.updateInFlight(func(s *inFlightState) { + if s.incoming == 0 { + panic("jsonrpc2_v2: processResult called when incoming count is already zero") + } + s.incoming-- + }) + return nil } // write is used by all things that write outgoing messages, including replies. @@ -540,5 +631,46 @@ func (c *Connection) write(ctx context.Context, msg Message) error { defer func() { c.writer <- writer }() n, err := writer.Write(ctx, msg) event.Metric(ctx, tag.SentBytes.Of(n)) + + // TODO: if err != nil, that suggests that future writes will not succeed, + // so we cannot possibly write the results of incoming Call requests. + // If the read side of the connection is also broken, we also might not have + // a way to receive cancellation notifications. + // + // Should we cancel the pending calls implicitly? + return err } + +// internalErrorf reports an internal error. By default it panics, but if +// c.onInternalError is non-nil it instead calls that and returns an error +// wrapping ErrInternal. +func (c *Connection) internalErrorf(format string, args ...interface{}) error { + err := fmt.Errorf(format, args...) + if c.onInternalError == nil { + panic("jsonrpc2: " + err.Error()) + } + c.onInternalError(err) + + return fmt.Errorf("%w: %v", ErrInternal, err) +} + +// labelStatus labels the status of the event in ctx based on whether err is nil. +func labelStatus(ctx context.Context, err error) { + if err == nil { + event.Label(ctx, tag.StatusCode.Of("OK")) + } else { + event.Label(ctx, tag.StatusCode.Of("ERROR")) + } +} + +// notDone is a context.Context wrapper that returns a nil Done channel. +type notDone struct{ ctx context.Context } + +func (ic notDone) Value(key interface{}) interface{} { + return ic.ctx.Value(key) +} + +func (notDone) Done() <-chan struct{} { return nil } +func (notDone) Err() error { return nil } +func (notDone) Deadline() (time.Time, bool) { return time.Time{}, false } diff --git a/internal/jsonrpc2_v2/net.go b/internal/jsonrpc2_v2/net.go index f1e2b0c7b36..15d0aea3af0 100644 --- a/internal/jsonrpc2_v2/net.go +++ b/internal/jsonrpc2_v2/net.go @@ -9,7 +9,6 @@ import ( "io" "net" "os" - "time" ) // This file contains implementations of the transport primitives that use the standard network @@ -36,7 +35,7 @@ type netListener struct { } // Accept blocks waiting for an incoming connection to the listener. -func (l *netListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { +func (l *netListener) Accept(context.Context) (io.ReadWriteCloser, error) { return l.net.Accept() } @@ -56,9 +55,7 @@ func (l *netListener) Close() error { // Dialer returns a dialer that can be used to connect to the listener. func (l *netListener) Dialer() Dialer { - return NetDialer(l.net.Addr().Network(), l.net.Addr().String(), net.Dialer{ - Timeout: 5 * time.Second, - }) + return NetDialer(l.net.Addr().Network(), l.net.Addr().String(), net.Dialer{}) } // NetDialer returns a Dialer using the supplied standard network dialer. @@ -98,15 +95,19 @@ type netPiper struct { } // Accept blocks waiting for an incoming connection to the listener. -func (l *netPiper) Accept(ctx context.Context) (io.ReadWriteCloser, error) { - // block until we have a listener, or are closed or cancelled +func (l *netPiper) Accept(context.Context) (io.ReadWriteCloser, error) { + // Block until the pipe is dialed or the listener is closed, + // preferring the latter if already closed at the start of Accept. + select { + case <-l.done: + return nil, errClosed + default: + } select { case rwc := <-l.dialed: return rwc, nil case <-l.done: - return nil, io.EOF - case <-ctx.Done(): - return nil, ctx.Err() + return nil, errClosed } } @@ -124,6 +125,14 @@ func (l *netPiper) Dialer() Dialer { func (l *netPiper) Dial(ctx context.Context) (io.ReadWriteCloser, error) { client, server := net.Pipe() - l.dialed <- server - return client, nil + + select { + case l.dialed <- server: + return client, nil + + case <-l.done: + client.Close() + server.Close() + return nil, errClosed + } } diff --git a/internal/jsonrpc2_v2/serve.go b/internal/jsonrpc2_v2/serve.go index 5ffce681e91..eb1e20891f3 100644 --- a/internal/jsonrpc2_v2/serve.go +++ b/internal/jsonrpc2_v2/serve.go @@ -105,6 +105,8 @@ func (s *Server) run(ctx context.Context) { if err != nil { if !isClosingError(err) { s.async.setError(err) + s.listener.Close() + break } continue } @@ -120,10 +122,12 @@ func (s *Server) run(ctx context.Context) { func onlyActive(conns []*Connection) []*Connection { i := 0 for _, c := range conns { - if !c.async.isDone() { - conns[i] = c - i++ - } + c.updateInFlight(func(s *inFlightState) { + if !s.closed { + conns[i] = c + i++ + } + }) } // trim the slice down return conns[:i] @@ -151,10 +155,7 @@ func isClosingError(err error) bool { return true } - // Per https://github.com/golang/go/issues/4373, this error string should not - // change. This is not ideal, but since the worst that could happen here is - // some superfluous logging, it is acceptable. - if err.Error() == "use of closed network connection" { + if isErrClosed(err) { return true } diff --git a/internal/jsonrpc2_v2/serve_go116.go b/internal/jsonrpc2_v2/serve_go116.go new file mode 100644 index 00000000000..29549f1059d --- /dev/null +++ b/internal/jsonrpc2_v2/serve_go116.go @@ -0,0 +1,19 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.16 +// +build go1.16 + +package jsonrpc2 + +import ( + "errors" + "net" +) + +var errClosed = net.ErrClosed + +func isErrClosed(err error) bool { + return errors.Is(err, errClosed) +} diff --git a/internal/jsonrpc2_v2/serve_pre116.go b/internal/jsonrpc2_v2/serve_pre116.go new file mode 100644 index 00000000000..14afa834962 --- /dev/null +++ b/internal/jsonrpc2_v2/serve_pre116.go @@ -0,0 +1,30 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.16 +// +build !go1.16 + +package jsonrpc2 + +import ( + "errors" + "strings" +) + +// errClosed is an error with the same string as net.ErrClosed, +// which was added in Go 1.16. +var errClosed = errors.New("use of closed network connection") + +// isErrClosed reports whether err ends in the same string as errClosed. +func isErrClosed(err error) bool { + // As of Go 1.16, this could be 'errors.Is(err, net.ErrClosing)', but + // unfortunately gopls still requires compatiblity with + // (otherwise-unsupported) older Go versions. + // + // In the meantime, this error strirng has not changed on any supported Go + // version, and is not expected to change in the future. + // This is not ideal, but since the worst that could happen here is some + // superfluous logging, it is acceptable. + return strings.HasSuffix(err.Error(), "use of closed network connection") +} diff --git a/internal/jsonrpc2_v2/serve_test.go b/internal/jsonrpc2_v2/serve_test.go index f0c27a8be56..4154d64b9e1 100644 --- a/internal/jsonrpc2_v2/serve_test.go +++ b/internal/jsonrpc2_v2/serve_test.go @@ -230,7 +230,7 @@ func TestIdleListenerAcceptCloseRace(t *testing.T) { watchdog := time.Duration(n) * 1000 * time.Millisecond timer := time.AfterFunc(watchdog, func() { debug.SetTraceback("all") - panic(fmt.Sprintf("TestAcceptCloseRace deadlocked after %v", watchdog)) + panic(fmt.Sprintf("%s deadlocked after %v", t.Name(), watchdog)) }) defer timer.Stop() @@ -261,3 +261,84 @@ func TestIdleListenerAcceptCloseRace(t *testing.T) { <-done } } + +// TestCloseCallRace checks for a race resulting in a deadlock when a Call on +// one side of the connection races with a Close (or otherwise broken +// connection) initiated from the other side. +// +// (The Call method was waiting for a result from the Read goroutine to +// determine which error value to return, but the Read goroutine was waiting for +// in-flight calls to complete before reporting that result.) +func TestCloseCallRace(t *testing.T) { + ctx := context.Background() + n := 10 + + watchdog := time.Duration(n) * 1000 * time.Millisecond + timer := time.AfterFunc(watchdog, func() { + debug.SetTraceback("all") + panic(fmt.Sprintf("%s deadlocked after %v", t.Name(), watchdog)) + }) + defer timer.Stop() + + for ; n > 0; n-- { + listener, err := jsonrpc2.NetPipeListener(ctx) + if err != nil { + t.Fatal(err) + } + + pokec := make(chan *jsonrpc2.AsyncCall, 1) + + s, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.BinderFunc(func(_ context.Context, srvConn *jsonrpc2.Connection) (jsonrpc2.ConnectionOptions, error) { + h := jsonrpc2.HandlerFunc(func(ctx context.Context, _ *jsonrpc2.Request) (interface{}, error) { + // Start a concurrent call from the server to the client. + // The point of this test is to ensure this doesn't deadlock + // if the client shuts down the connection concurrently. + // + // The racing Call may or may not receive a response: it should get a + // response if it is sent before the client closes the connection, and + // it should fail with some kind of "connection closed" error otherwise. + go func() { + pokec <- srvConn.Call(ctx, "poke", nil) + }() + + return &msg{"pong"}, nil + }) + return jsonrpc2.ConnectionOptions{Handler: h}, nil + })) + if err != nil { + listener.Close() + t.Fatal(err) + } + + dialConn, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + if err != nil { + listener.Close() + s.Wait() + t.Fatal(err) + } + + // Calling any method on the server should provoke it to asynchronously call + // us back. While it is starting that call, we will close the connection. + if err := dialConn.Call(ctx, "ping", nil).Await(ctx, nil); err != nil { + t.Error(err) + } + if err := dialConn.Close(); err != nil { + t.Error(err) + } + + // Ensure that the Call on the server side did not block forever when the + // connection closed. + pokeCall := <-pokec + if err := pokeCall.Await(ctx, nil); err == nil { + t.Errorf("unexpected nil error from server-initited call") + } else if errors.Is(err, jsonrpc2.ErrMethodNotFound) { + // The call completed before the Close reached the handler. + } else { + // The error was something else. + t.Logf("server-initiated call completed with expected error: %v", err) + } + + listener.Close() + s.Wait() + } +} From 4885f7c90f1dd2c75ad677deb99f785f27aa4eb5 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 1 Mar 2022 12:04:14 -0500 Subject: [PATCH 383/723] internal/jsonrpc2_v2: eliminate a temporary connection leak in (*Server).run Prior to this CL, (*Server).run only filters out inactive connections when it accepts a new connection. If existing connections complete, their associated resources can't be garbage-collected until either the next connection is accepted or the Listener is closed. This change moves the open-connection accounting to an explicit hook passed to newConnection, eliminating the need to call Wait entirely. For golang/go#46047 Change-Id: I3732cb463fcea0c142f17f2b1510fdfd2dbc81da Reviewed-on: https://go-review.googlesource.com/c/tools/+/388774 Auto-Submit: Bryan Mills Reviewed-by: Ian Cottrell gopls-CI: kokoro Run-TryBot: Bryan Mills TryBot-Result: Gopher Robot --- internal/jsonrpc2_v2/conn.go | 12 +++++++++- internal/jsonrpc2_v2/serve.go | 41 ++++++++++------------------------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 3d59fc61d1d..57a2db4e8fb 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -74,6 +74,7 @@ type Connection struct { handler Handler onInternalError func(error) + onDone func() } // inFlightState records the state of the incoming and outgoing calls on a @@ -113,6 +114,9 @@ func (c *Connection) updateInFlight(f func(*inFlightState)) { idle := s.incoming == 0 && len(s.outgoing) == 0 && !s.handlerRunning if idle && (s.closing || s.readErr != nil) && !s.closed { c.closeErr <- c.closer.Close() + if c.onDone != nil { + c.onDone() + } s.closed = true } } @@ -131,8 +135,13 @@ func (o ConnectionOptions) Bind(context.Context, *Connection) (ConnectionOptions } // newConnection creates a new connection and runs it. +// // This is used by the Dial and Serve functions to build the actual connection. -func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder) (*Connection, error) { +// +// The connection is closed automatically (and its resources cleaned up) when +// the last request has completed after the underlying ReadWriteCloser breaks, +// but it may be stopped earlier by calling Close (for a clean shutdown). +func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder, onDone func()) (*Connection, error) { // TODO: Should we create a new event span here? // This will propagate cancellation from ctx; should it? ctx := notDone{bindCtx} @@ -141,6 +150,7 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde closer: rwc, closeErr: make(chan error, 1), writer: make(chan Writer, 1), + onDone: onDone, } options, err := binder.Bind(bindCtx, c) diff --git a/internal/jsonrpc2_v2/serve.go b/internal/jsonrpc2_v2/serve.go index eb1e20891f3..64a5916134a 100644 --- a/internal/jsonrpc2_v2/serve.go +++ b/internal/jsonrpc2_v2/serve.go @@ -54,7 +54,7 @@ func Dial(ctx context.Context, dialer Dialer, binder Binder) (*Connection, error if err != nil { return nil, err } - return newConnection(ctx, rwc, binder) + return newConnection(ctx, rwc, binder, nil) } // Serve starts a new server listening for incoming connections and returns @@ -84,25 +84,25 @@ func (s *Server) Wait() error { // duration, otherwise it exits only on error. func (s *Server) run(ctx context.Context) { defer s.async.done() - var activeConns []*Connection + + var activeConns sync.WaitGroup for { - // we never close the accepted connection, we rely on the other end - // closing or the socket closing itself naturally + // We never close the accepted connection — we rely on the other end + // closing or the socket closing itself naturally. rwc, err := s.listener.Accept(ctx) if err != nil { if !isClosingError(err) { s.async.setError(err) } - // we are done generating new connections for good + // We are done generating new connections for good. break } - // see if any connections were closed while we were waiting - activeConns = onlyActive(activeConns) - - // a new inbound connection, - conn, err := newConnection(ctx, rwc, s.binder) + // A new inbound connection. + activeConns.Add(1) + _, err = newConnection(ctx, rwc, s.binder, activeConns.Done) if err != nil { + activeConns.Done() if !isClosingError(err) { s.async.setError(err) s.listener.Close() @@ -110,27 +110,8 @@ func (s *Server) run(ctx context.Context) { } continue } - activeConns = append(activeConns, conn) - } - - // wait for all active conns to finish - for _, c := range activeConns { - c.Wait() - } -} - -func onlyActive(conns []*Connection) []*Connection { - i := 0 - for _, c := range conns { - c.updateInFlight(func(s *inFlightState) { - if !s.closed { - conns[i] = c - i++ - } - }) } - // trim the slice down - return conns[:i] + activeConns.Wait() } // isClosingError reports if the error occurs normally during the process of From eabc3a08b7f55017929a08da5a002009777b50f0 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 18 Oct 2022 10:13:17 -0400 Subject: [PATCH 384/723] internal/jsonrpc2_v2: eliminate isClosingErr Also implement and use the Shutdown method, which was mentioned in a doc comment in CL 292169 but not actually present at that time. With proper synchronization, we don't need heuristics to determine whether an error is due to a connection or listener being closed. We know whether we have called Close (and why), and we can assume that if we have called Close then that is probably the reason for any returned error. Fixes golang/go#56281. Change-Id: I5e0ed7db0f736ca8df8cd8cf556b674e7a863a69 Reviewed-on: https://go-review.googlesource.com/c/tools/+/443675 gopls-CI: kokoro Reviewed-by: Alan Donovan Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills --- gopls/internal/lsp/lsprpc/binder.go | 2 +- gopls/internal/lsp/lsprpc/binder_test.go | 14 +-- .../internal/lsp/lsprpc/commandinterceptor.go | 2 +- gopls/internal/lsp/lsprpc/middleware_test.go | 2 +- internal/jsonrpc2_v2/serve.go | 98 ++++++++----------- 5 files changed, 47 insertions(+), 71 deletions(-) diff --git a/gopls/internal/lsp/lsprpc/binder.go b/gopls/internal/lsp/lsprpc/binder.go index b12cc491ffb..c4b6d7cf6cd 100644 --- a/gopls/internal/lsp/lsprpc/binder.go +++ b/gopls/internal/lsp/lsprpc/binder.go @@ -9,9 +9,9 @@ import ( "encoding/json" "fmt" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/xcontext" ) diff --git a/gopls/internal/lsp/lsprpc/binder_test.go b/gopls/internal/lsp/lsprpc/binder_test.go index 8b048ab34e7..9e2ad6cf7ea 100644 --- a/gopls/internal/lsp/lsprpc/binder_test.go +++ b/gopls/internal/lsp/lsprpc/binder_test.go @@ -11,23 +11,20 @@ import ( "testing" "time" - jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" "golang.org/x/tools/gopls/internal/lsp/protocol" + jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" . "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) type TestEnv struct { - Listeners []jsonrpc2_v2.Listener - Conns []*jsonrpc2_v2.Connection - Servers []*jsonrpc2_v2.Server + Conns []*jsonrpc2_v2.Connection + Servers []*jsonrpc2_v2.Server } func (e *TestEnv) Shutdown(t *testing.T) { - for _, l := range e.Listeners { - if err := l.Close(); err != nil { - t.Error(err) - } + for _, s := range e.Servers { + s.Shutdown() } for _, c := range e.Conns { if err := c.Close(); err != nil { @@ -46,7 +43,6 @@ func (e *TestEnv) serve(ctx context.Context, t *testing.T, server jsonrpc2_v2.Bi if err != nil { t.Fatal(err) } - e.Listeners = append(e.Listeners, l) s, err := jsonrpc2_v2.Serve(ctx, l, server) if err != nil { t.Fatal(err) diff --git a/gopls/internal/lsp/lsprpc/commandinterceptor.go b/gopls/internal/lsp/lsprpc/commandinterceptor.go index be68efe78aa..cd582c0e199 100644 --- a/gopls/internal/lsp/lsprpc/commandinterceptor.go +++ b/gopls/internal/lsp/lsprpc/commandinterceptor.go @@ -8,8 +8,8 @@ import ( "context" "encoding/json" - jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" "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. diff --git a/gopls/internal/lsp/lsprpc/middleware_test.go b/gopls/internal/lsp/lsprpc/middleware_test.go index a37294a31a1..18bba819eaf 100644 --- a/gopls/internal/lsp/lsprpc/middleware_test.go +++ b/gopls/internal/lsp/lsprpc/middleware_test.go @@ -11,8 +11,8 @@ import ( "testing" "time" - jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" . "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, error) { diff --git a/internal/jsonrpc2_v2/serve.go b/internal/jsonrpc2_v2/serve.go index 64a5916134a..fcc641151f1 100644 --- a/internal/jsonrpc2_v2/serve.go +++ b/internal/jsonrpc2_v2/serve.go @@ -6,13 +6,11 @@ package jsonrpc2 import ( "context" - "errors" "fmt" "io" "runtime" - "strings" "sync" - "syscall" + "sync/atomic" "time" ) @@ -43,18 +41,31 @@ type Server struct { listener Listener binder Binder async *async + + shutdownOnce sync.Once + closing int32 // atomic: set to nonzero when Shutdown is called } // Dial uses the dialer to make a new connection, wraps the returned // reader and writer using the framer to make a stream, and then builds // a connection on top of that stream using the binder. +// +// The returned Connection will operate independently using the Preempter and/or +// Handler provided by the Binder, and will release its own resources when the +// connection is broken, but the caller may Close it earlier to stop accepting +// (or sending) new requests. func Dial(ctx context.Context, dialer Dialer, binder Binder) (*Connection, error) { // dial a server rwc, err := dialer.Dial(ctx) if err != nil { return nil, err } - return newConnection(ctx, rwc, binder, nil) + conn, err := newConnection(ctx, rwc, binder, nil) + if err != nil { + rwc.Close() + return nil, err + } + return conn, nil } // Serve starts a new server listening for incoming connections and returns @@ -79,6 +90,14 @@ func (s *Server) Wait() error { return s.async.wait() } +// Shutdown informs the server to stop accepting new connections. +func (s *Server) Shutdown() { + s.shutdownOnce.Do(func() { + atomic.StoreInt32(&s.closing, 1) + s.listener.Close() + }) +} + // run accepts incoming connections from the listener, // If IdleTimeout is non-zero, run exits after there are no clients for this // duration, otherwise it exits only on error. @@ -87,11 +106,12 @@ func (s *Server) run(ctx context.Context) { var activeConns sync.WaitGroup for { - // We never close the accepted connection — we rely on the other end - // closing or the socket closing itself naturally. rwc, err := s.listener.Accept(ctx) if err != nil { - if !isClosingError(err) { + // Only Shutdown closes the listener. If we get an error after Shutdown is + // called, assume that that was the cause and don't report the error; + // otherwise, report the error in case it is unexpected. + if atomic.LoadInt32(&s.closing) == 0 { s.async.setError(err) } // We are done generating new connections for good. @@ -100,59 +120,16 @@ func (s *Server) run(ctx context.Context) { // A new inbound connection. activeConns.Add(1) - _, err = newConnection(ctx, rwc, s.binder, activeConns.Done) + _, err = newConnection(ctx, rwc, s.binder, activeConns.Done) // unregisters itself when done if err != nil { + rwc.Close() activeConns.Done() - if !isClosingError(err) { - s.async.setError(err) - s.listener.Close() - break - } - continue + s.async.setError(err) } } activeConns.Wait() } -// isClosingError reports if the error occurs normally during the process of -// closing a network connection. It uses imperfect heuristics that err on the -// side of false negatives, and should not be used for anything critical. -func isClosingError(err error) bool { - if err == nil { - return false - } - // Fully unwrap the error, so the following tests work. - for wrapped := err; wrapped != nil; wrapped = errors.Unwrap(err) { - err = wrapped - } - - // Was it based on an EOF error? - if err == io.EOF { - return true - } - - // Was it based on a closed pipe? - if err == io.ErrClosedPipe { - return true - } - - if isErrClosed(err) { - return true - } - - if runtime.GOOS == "plan9" { - // Error reading from a closed connection. - if err == syscall.EINVAL { - return true - } - // Error trying to accept a new connection from a closed listener. - if strings.HasSuffix(err.Error(), " listen hungup") { - return true - } - } - return false -} - // NewIdleListener wraps a listener with an idle timeout. // // When there are no active connections for at least the timeout duration, @@ -188,10 +165,6 @@ type idleListener struct { func (l *idleListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { rwc, err := l.wrapped.Accept(ctx) - if err != nil && !isClosingError(err) { - return nil, err - } - select { case n, ok := <-l.active: if err != nil { @@ -216,13 +189,20 @@ func (l *idleListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { // active and closed due to idleness, which would be contradictory and // confusing. Close the connection and pretend that it never happened. rwc.Close() + } else { + // In theory the timeout could have raced with an unrelated error return + // from Accept. However, ErrIdleTimeout is arguably still valid (since we + // would have closed due to the timeout independent of the error), and the + // harm from returning a spurious ErrIdleTimeout is negliglible anyway. } return nil, ErrIdleTimeout case timer := <-l.idleTimer: if err != nil { - // The idle timer hasn't run yet, so err can't be ErrIdleTimeout. - // Leave the idle timer as it was and return whatever error we got. + // The idle timer doesn't run until it receives itself from the idleTimer + // channel, so it can't have called l.wrapped.Close yet and thus err can't + // be ErrIdleTimeout. Leave the idle timer as it was and return whatever + // error we got. l.idleTimer <- timer return nil, err } From 28e9e509a6aba8d0b33ef87dd5c9486f93d8eb73 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 18 Oct 2022 11:36:38 -0400 Subject: [PATCH 385/723] internal/jsonrpc2_v2: eliminate error return from Bind Also make (*Connection).Close safe to call from Bind. A jsonrpc2_v2.Server has no good way to report an error from Bind. If the Server saves the error to return from its own Wait method, that might not ever actually happen: Wait waits for in-flight connections to complete, but if some existing connection stays up then Wait will not return. If the Server goes ahead with establishing the connection and installs its own Handler, that Handler needs to decide whether to serve the error from Bind or something more opaque, and at that point Bind may as well return a handler that makes that choice more precisely. If the Server merely logs the error and closes the Connection, then the Bind method itself may as well do that directly too. It seems to me that the only winning move is not to play. Only Bind is in a position to decide how to handle its errors appropriately, so it should not return them to the Server. Updates golang/go#56281. Change-Id: I07dc43ddf31253ce23da21a92d2b6c0f8d4b3afe Reviewed-on: https://go-review.googlesource.com/c/tools/+/443677 Run-TryBot: Bryan Mills Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/lsprpc/binder.go | 24 ++++++++------ gopls/internal/lsp/lsprpc/binder_test.go | 1 + .../internal/lsp/lsprpc/commandinterceptor.go | 9 ++---- gopls/internal/lsp/lsprpc/middleware.go | 9 ++---- gopls/internal/lsp/lsprpc/middleware_test.go | 4 +-- internal/jsonrpc2_v2/conn.go | 32 +++++++++++-------- internal/jsonrpc2_v2/jsonrpc2_test.go | 4 +-- internal/jsonrpc2_v2/serve.go | 14 ++------ internal/jsonrpc2_v2/serve_test.go | 4 +-- 9 files changed, 48 insertions(+), 53 deletions(-) diff --git a/gopls/internal/lsp/lsprpc/binder.go b/gopls/internal/lsp/lsprpc/binder.go index c4b6d7cf6cd..01e59f7bb62 100644 --- a/gopls/internal/lsp/lsprpc/binder.go +++ b/gopls/internal/lsp/lsprpc/binder.go @@ -17,9 +17,9 @@ import ( // The BinderFunc type adapts a bind function to implement the jsonrpc2.Binder // interface. -type BinderFunc func(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) +type BinderFunc func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions -func (f BinderFunc) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { +func (f BinderFunc) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { return f(ctx, conn) } @@ -39,7 +39,7 @@ func NewServerBinder(newServer ServerFunc) *ServerBinder { return &ServerBinder{newServer: newServer} } -func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { +func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { client := protocol.ClientDispatcherV2(conn) server := b.newServer(ctx, client) serverHandler := protocol.ServerHandlerV2(server) @@ -55,7 +55,7 @@ func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) ( return jsonrpc2_v2.ConnectionOptions{ Handler: wrapped, Preempter: preempter, - }, nil + } } type canceler struct { @@ -94,13 +94,19 @@ func NewForwardBinder(dialer jsonrpc2_v2.Dialer) *ForwardBinder { } } -func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (opts jsonrpc2_v2.ConnectionOptions, _ error) { +func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (opts jsonrpc2_v2.ConnectionOptions) { client := protocol.ClientDispatcherV2(conn) clientBinder := NewClientBinder(func(context.Context, protocol.Server) protocol.Client { return client }) + serverConn, err := jsonrpc2_v2.Dial(context.Background(), b.dialer, clientBinder) if err != nil { - return opts, err + return jsonrpc2_v2.ConnectionOptions{ + Handler: jsonrpc2_v2.HandlerFunc(func(context.Context, *jsonrpc2_v2.Request) (interface{}, error) { + return nil, fmt.Errorf("%w: %v", jsonrpc2_v2.ErrInternal, err) + }), + } } + if b.onBind != nil { b.onBind(serverConn) } @@ -118,7 +124,7 @@ func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) return jsonrpc2_v2.ConnectionOptions{ Handler: protocol.ServerHandlerV2(server), Preempter: preempter, - }, nil + } } // A ClientFunc is used to construct an LSP client for a given server. @@ -133,10 +139,10 @@ func NewClientBinder(newClient ClientFunc) *ClientBinder { return &ClientBinder{newClient} } -func (b *ClientBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { +func (b *ClientBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { server := protocol.ServerDispatcherV2(conn) client := b.newClient(ctx, server) return jsonrpc2_v2.ConnectionOptions{ Handler: protocol.ClientHandlerV2(client), - }, nil + } } diff --git a/gopls/internal/lsp/lsprpc/binder_test.go b/gopls/internal/lsp/lsprpc/binder_test.go index 9e2ad6cf7ea..9e9fa3c5ea2 100644 --- a/gopls/internal/lsp/lsprpc/binder_test.go +++ b/gopls/internal/lsp/lsprpc/binder_test.go @@ -45,6 +45,7 @@ func (e *TestEnv) serve(ctx context.Context, t *testing.T, server jsonrpc2_v2.Bi } s, err := jsonrpc2_v2.Serve(ctx, l, server) if err != nil { + l.Close() t.Fatal(err) } e.Servers = append(e.Servers, s) diff --git a/gopls/internal/lsp/lsprpc/commandinterceptor.go b/gopls/internal/lsp/lsprpc/commandinterceptor.go index cd582c0e199..607ee9c9e9f 100644 --- a/gopls/internal/lsp/lsprpc/commandinterceptor.go +++ b/gopls/internal/lsp/lsprpc/commandinterceptor.go @@ -18,13 +18,10 @@ type HandlerMiddleware func(jsonrpc2_v2.Handler) jsonrpc2_v2.Handler // BindHandler transforms a HandlerMiddleware into a Middleware. func BindHandler(hmw HandlerMiddleware) Middleware { return Middleware(func(binder jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { - return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { - opts, err := binder.Bind(ctx, conn) - if err != nil { - return opts, err - } + return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { + opts := binder.Bind(ctx, conn) opts.Handler = hmw(opts.Handler) - return opts, nil + return opts }) }) } diff --git a/gopls/internal/lsp/lsprpc/middleware.go b/gopls/internal/lsp/lsprpc/middleware.go index f703217dd0b..50089cde7dc 100644 --- a/gopls/internal/lsp/lsprpc/middleware.go +++ b/gopls/internal/lsp/lsprpc/middleware.go @@ -62,11 +62,8 @@ func (h *Handshaker) Peers() []PeerInfo { // Middleware is a jsonrpc2 middleware function to augment connection binding // to handle the handshake method, and record disconnections. func (h *Handshaker) Middleware(inner jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { - return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { - opts, err := inner.Bind(ctx, conn) - if err != nil { - return opts, err - } + return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { + opts := inner.Bind(ctx, conn) localID := h.nextID() info := &PeerInfo{ @@ -93,7 +90,7 @@ func (h *Handshaker) Middleware(inner jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { // Record the dropped client. go h.cleanupAtDisconnect(conn, localID) - return opts, nil + return opts }) } diff --git a/gopls/internal/lsp/lsprpc/middleware_test.go b/gopls/internal/lsp/lsprpc/middleware_test.go index 18bba819eaf..c528eae5c62 100644 --- a/gopls/internal/lsp/lsprpc/middleware_test.go +++ b/gopls/internal/lsp/lsprpc/middleware_test.go @@ -15,8 +15,8 @@ import ( jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" ) -var noopBinder = BinderFunc(func(context.Context, *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { - return jsonrpc2_v2.ConnectionOptions{}, nil +var noopBinder = BinderFunc(func(context.Context, *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { + return jsonrpc2_v2.ConnectionOptions{} }) func TestHandshakeMiddleware(t *testing.T) { diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 57a2db4e8fb..7c48e2ec616 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -27,14 +27,16 @@ import ( type Binder interface { // Bind returns the ConnectionOptions to use when establishing the passed-in // Connection. - // The connection is not ready to use when Bind is called. - Bind(context.Context, *Connection) (ConnectionOptions, error) + // + // The connection is not ready to use when Bind is called, + // but Bind may close it without reading or writing to it. + Bind(context.Context, *Connection) ConnectionOptions } // A BinderFunc implements the Binder interface for a standalone Bind function. -type BinderFunc func(context.Context, *Connection) (ConnectionOptions, error) +type BinderFunc func(context.Context, *Connection) ConnectionOptions -func (f BinderFunc) Bind(ctx context.Context, c *Connection) (ConnectionOptions, error) { +func (f BinderFunc) Bind(ctx context.Context, c *Connection) ConnectionOptions { return f(ctx, c) } @@ -130,8 +132,8 @@ type incomingRequest struct { } // Bind returns the options unmodified. -func (o ConnectionOptions) Bind(context.Context, *Connection) (ConnectionOptions, error) { - return o, nil +func (o ConnectionOptions) Bind(context.Context, *Connection) ConnectionOptions { + return o } // newConnection creates a new connection and runs it. @@ -141,7 +143,7 @@ func (o ConnectionOptions) Bind(context.Context, *Connection) (ConnectionOptions // The connection is closed automatically (and its resources cleaned up) when // the last request has completed after the underlying ReadWriteCloser breaks, // but it may be stopped earlier by calling Close (for a clean shutdown). -func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder, onDone func()) (*Connection, error) { +func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder, onDone func()) *Connection { // TODO: Should we create a new event span here? // This will propagate cancellation from ctx; should it? ctx := notDone{bindCtx} @@ -153,10 +155,7 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde onDone: onDone, } - options, err := binder.Bind(bindCtx, c) - if err != nil { - return nil, err - } + options := binder.Bind(bindCtx, c) framer := options.Framer if framer == nil { framer = HeaderFramer() @@ -169,9 +168,14 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde c.writer <- framer.Writer(rwc) reader := framer.Reader(rwc) - // The goroutines started here will continue until the underlying stream is closed. - go c.readIncoming(ctx, reader, options.Preempter) - return c, nil + + c.updateInFlight(func(s *inFlightState) { + if !s.closed { + // The goroutine started here will continue until the underlying stream is closed. + go c.readIncoming(ctx, reader, options.Preempter) + } + }) + return c } // Notify invokes the target method but does not wait for a response. diff --git a/internal/jsonrpc2_v2/jsonrpc2_test.go b/internal/jsonrpc2_v2/jsonrpc2_test.go index b2fa4963b74..40c2dd3826c 100644 --- a/internal/jsonrpc2_v2/jsonrpc2_test.go +++ b/internal/jsonrpc2_v2/jsonrpc2_test.go @@ -253,7 +253,7 @@ func verifyResults(t *testing.T, method string, results interface{}, expect inte } } -func (b binder) Bind(ctx context.Context, conn *jsonrpc2.Connection) (jsonrpc2.ConnectionOptions, error) { +func (b binder) Bind(ctx context.Context, conn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions { h := &handler{ conn: conn, waiters: make(chan map[string]chan struct{}, 1), @@ -267,7 +267,7 @@ func (b binder) Bind(ctx context.Context, conn *jsonrpc2.Connection) (jsonrpc2.C Framer: b.framer, Preempter: h, Handler: h, - }, nil + } } func (h *handler) waiter(name string) chan struct{} { diff --git a/internal/jsonrpc2_v2/serve.go b/internal/jsonrpc2_v2/serve.go index fcc641151f1..7235be91da3 100644 --- a/internal/jsonrpc2_v2/serve.go +++ b/internal/jsonrpc2_v2/serve.go @@ -60,12 +60,7 @@ func Dial(ctx context.Context, dialer Dialer, binder Binder) (*Connection, error if err != nil { return nil, err } - conn, err := newConnection(ctx, rwc, binder, nil) - if err != nil { - rwc.Close() - return nil, err - } - return conn, nil + return newConnection(ctx, rwc, binder, nil), nil } // Serve starts a new server listening for incoming connections and returns @@ -120,12 +115,7 @@ func (s *Server) run(ctx context.Context) { // A new inbound connection. activeConns.Add(1) - _, err = newConnection(ctx, rwc, s.binder, activeConns.Done) // unregisters itself when done - if err != nil { - rwc.Close() - activeConns.Done() - s.async.setError(err) - } + _ = newConnection(ctx, rwc, s.binder, activeConns.Done) // unregisters itself when done } activeConns.Wait() } diff --git a/internal/jsonrpc2_v2/serve_test.go b/internal/jsonrpc2_v2/serve_test.go index 4154d64b9e1..4901a069ab7 100644 --- a/internal/jsonrpc2_v2/serve_test.go +++ b/internal/jsonrpc2_v2/serve_test.go @@ -288,7 +288,7 @@ func TestCloseCallRace(t *testing.T) { pokec := make(chan *jsonrpc2.AsyncCall, 1) - s, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.BinderFunc(func(_ context.Context, srvConn *jsonrpc2.Connection) (jsonrpc2.ConnectionOptions, error) { + s, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.BinderFunc(func(_ context.Context, srvConn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions { h := jsonrpc2.HandlerFunc(func(ctx context.Context, _ *jsonrpc2.Request) (interface{}, error) { // Start a concurrent call from the server to the client. // The point of this test is to ensure this doesn't deadlock @@ -303,7 +303,7 @@ func TestCloseCallRace(t *testing.T) { return &msg{"pong"}, nil }) - return jsonrpc2.ConnectionOptions{Handler: h}, nil + return jsonrpc2.ConnectionOptions{Handler: h} })) if err != nil { listener.Close() From 7cdb0e7352382b649b0f6f3fac05ca31d2cab1e4 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 18 Oct 2022 12:06:21 -0400 Subject: [PATCH 386/723] internal/jsonrpc2_v2: rename Serve to NewServer and eliminate its error return Serve had a misleading name and signature: it did not actually block on serving the connection, and never returned a non-nil error. Updates golang/go#56281. Change-Id: Ia6df0ba20066811b0551df3b3267dff2fffd7881 Reviewed-on: https://go-review.googlesource.com/c/tools/+/443678 Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Auto-Submit: Bryan Mills Run-TryBot: Bryan Mills --- gopls/internal/lsp/lsprpc/binder_test.go | 6 +----- internal/jsonrpc2_v2/jsonrpc2_test.go | 5 +---- internal/jsonrpc2_v2/serve.go | 6 +++--- internal/jsonrpc2_v2/serve_test.go | 16 +++------------- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/gopls/internal/lsp/lsprpc/binder_test.go b/gopls/internal/lsp/lsprpc/binder_test.go index 9e9fa3c5ea2..3315c3eb775 100644 --- a/gopls/internal/lsp/lsprpc/binder_test.go +++ b/gopls/internal/lsp/lsprpc/binder_test.go @@ -43,11 +43,7 @@ func (e *TestEnv) serve(ctx context.Context, t *testing.T, server jsonrpc2_v2.Bi if err != nil { t.Fatal(err) } - s, err := jsonrpc2_v2.Serve(ctx, l, server) - if err != nil { - l.Close() - t.Fatal(err) - } + s := jsonrpc2_v2.NewServer(ctx, l, server) e.Servers = append(e.Servers, s) return l, s } diff --git a/internal/jsonrpc2_v2/jsonrpc2_test.go b/internal/jsonrpc2_v2/jsonrpc2_test.go index 40c2dd3826c..dd8d09c8870 100644 --- a/internal/jsonrpc2_v2/jsonrpc2_test.go +++ b/internal/jsonrpc2_v2/jsonrpc2_test.go @@ -136,10 +136,7 @@ func testConnection(t *testing.T, framer jsonrpc2.Framer) { if err != nil { t.Fatal(err) } - server, err := jsonrpc2.Serve(ctx, listener, binder{framer, nil}) - if err != nil { - t.Fatal(err) - } + server := jsonrpc2.NewServer(ctx, listener, binder{framer, nil}) defer func() { listener.Close() server.Wait() diff --git a/internal/jsonrpc2_v2/serve.go b/internal/jsonrpc2_v2/serve.go index 7235be91da3..7df785655d7 100644 --- a/internal/jsonrpc2_v2/serve.go +++ b/internal/jsonrpc2_v2/serve.go @@ -63,21 +63,21 @@ func Dial(ctx context.Context, dialer Dialer, binder Binder) (*Connection, error return newConnection(ctx, rwc, binder, nil), nil } -// Serve starts a new server listening for incoming connections and returns +// NewServer starts a new server listening for incoming connections and returns // it. // This returns a fully running and connected server, it does not block on // the listener. // You can call Wait to block on the server, or Shutdown to get the sever to // terminate gracefully. // To notice incoming connections, use an intercepting Binder. -func Serve(ctx context.Context, listener Listener, binder Binder) (*Server, error) { +func NewServer(ctx context.Context, listener Listener, binder Binder) *Server { server := &Server{ listener: listener, binder: binder, async: newAsync(), } go server.run(ctx) - return server, nil + return server } // Wait returns only when the server has shut down. diff --git a/internal/jsonrpc2_v2/serve_test.go b/internal/jsonrpc2_v2/serve_test.go index 4901a069ab7..21bf0bbd465 100644 --- a/internal/jsonrpc2_v2/serve_test.go +++ b/internal/jsonrpc2_v2/serve_test.go @@ -41,10 +41,7 @@ func TestIdleTimeout(t *testing.T) { listener = jsonrpc2.NewIdleListener(d, listener) defer listener.Close() - server, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.ConnectionOptions{}) - if err != nil { - t.Fatal(err) - } + server := jsonrpc2.NewServer(ctx, listener, jsonrpc2.ConnectionOptions{}) // Exercise some connection/disconnection patterns, and then assert that when // our timer fires, the server exits. @@ -187,12 +184,9 @@ func TestServe(t *testing.T) { } func newFake(t *testing.T, ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2.Connection, func(), error) { - server, err := jsonrpc2.Serve(ctx, l, jsonrpc2.ConnectionOptions{ + server := jsonrpc2.NewServer(ctx, l, jsonrpc2.ConnectionOptions{ Handler: fakeHandler{}, }) - if err != nil { - return nil, nil, err - } client, err := jsonrpc2.Dial(ctx, l.Dialer(), @@ -288,7 +282,7 @@ func TestCloseCallRace(t *testing.T) { pokec := make(chan *jsonrpc2.AsyncCall, 1) - s, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.BinderFunc(func(_ context.Context, srvConn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions { + s := jsonrpc2.NewServer(ctx, listener, jsonrpc2.BinderFunc(func(_ context.Context, srvConn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions { h := jsonrpc2.HandlerFunc(func(ctx context.Context, _ *jsonrpc2.Request) (interface{}, error) { // Start a concurrent call from the server to the client. // The point of this test is to ensure this doesn't deadlock @@ -305,10 +299,6 @@ func TestCloseCallRace(t *testing.T) { }) return jsonrpc2.ConnectionOptions{Handler: h} })) - if err != nil { - listener.Close() - t.Fatal(err) - } dialConn, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) if err != nil { From 3566f695a758c57ebe9bf6aca724b60f1a0c961e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 28 Oct 2022 15:45:27 -0400 Subject: [PATCH 387/723] gopls/internal/lsp/source: minor space optimizations Memory profiles show heavy allocation for the stack and the function closure of FindDeclAndField. This change moves both outside the loop, reducing this function's fraction of allocation from 6.7% before to 5.0% after, and reducing running time by 3-7% on these benchmarks. before BenchmarkStructCompletion/completion-8 100 10432280 ns/op BenchmarkImportCompletion/completion-8 1350 921785 ns/op BenchmarkSliceCompletion/completion-8 100 10876852 ns/op BenchmarkFuncDeepCompletion/completion-8 142 7136768 ns/op BenchmarkCompletionFollowingEdit/completion-8 63 21267031 ns/op After BenchmarkStructCompletion/completion-8 100 10030458 ns/op BenchmarkImportCompletion/completion-8 1311 918306 ns/op BenchmarkSliceCompletion/completion-8 100 10179937 ns/op BenchmarkFuncDeepCompletion/completion-8 150 6986303 ns/op BenchmarkCompletionFollowingEdit/completion-8 63 20575987 ns/op Change-Id: Ia459e41ecf20851ff4544f76ad7b415a24606cd1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446185 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan Reviewed-by: Robert Findley --- gopls/internal/lsp/source/hover.go | 114 +++++++++++++++-------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index c758d688dd2..09f7224c80d 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -904,78 +904,80 @@ func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *a }() // Visit the files in search of the node at pos. - var stack []ast.Node - for _, file := range files { - ast.Inspect(file, func(n ast.Node) bool { - if n != nil { - stack = append(stack, n) // push - } else { - stack = stack[:len(stack)-1] // pop - return false - } + stack := make([]ast.Node, 0, 20) + // Allocate the closure once, outside the loop. + f := func(n ast.Node) bool { + if n != nil { + stack = append(stack, n) // push + } else { + stack = stack[:len(stack)-1] // pop + return false + } - // Skip subtrees (incl. files) that don't contain the search point. - if !(n.Pos() <= pos && pos < n.End()) { - return false - } + // Skip subtrees (incl. files) that don't contain the search point. + if !(n.Pos() <= pos && pos < n.End()) { + return false + } - switch n := n.(type) { - case *ast.Field: - checkField := func(f ast.Node) { - if f.Pos() == pos { - field = n - for i := len(stack) - 1; i >= 0; i-- { - if d, ok := stack[i].(ast.Decl); ok { - decl = d // innermost enclosing decl - break - } + switch n := n.(type) { + case *ast.Field: + checkField := func(f ast.Node) { + if f.Pos() == pos { + field = n + for i := len(stack) - 1; i >= 0; i-- { + if d, ok := stack[i].(ast.Decl); ok { + decl = d // innermost enclosing decl + break } - panic(nil) // found } + panic(nil) // found } + } - // Check *ast.Field itself. This handles embedded - // fields which have no associated *ast.Ident name. - checkField(n) + // Check *ast.Field itself. This handles embedded + // fields which have no associated *ast.Ident name. + checkField(n) - // Check each field name since you can have - // multiple names for the same type expression. - for _, name := range n.Names { - checkField(name) - } + // Check each field name since you can have + // multiple names for the same type expression. + for _, name := range n.Names { + checkField(name) + } - // Also check "X" in "...X". This makes it easy - // to format variadic signature params properly. - if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil { - checkField(ell.Elt) - } + // Also check "X" in "...X". This makes it easy + // to format variadic signature params properly. + if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil { + checkField(ell.Elt) + } - case *ast.FuncDecl: - if n.Name.Pos() == pos { - decl = n - panic(nil) // found - } + case *ast.FuncDecl: + if n.Name.Pos() == pos { + decl = n + panic(nil) // found + } - case *ast.GenDecl: - for _, spec := range n.Specs { - switch spec := spec.(type) { - case *ast.TypeSpec: - if spec.Name.Pos() == pos { + case *ast.GenDecl: + for _, spec := range n.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + if spec.Name.Pos() == pos { + decl = n + panic(nil) // found + } + case *ast.ValueSpec: + for _, id := range spec.Names { + if id.Pos() == pos { decl = n panic(nil) // found } - case *ast.ValueSpec: - for _, id := range spec.Names { - if id.Pos() == pos { - decl = n - panic(nil) // found - } - } } } } - return true - }) + } + return true + } + for _, file := range files { + ast.Inspect(file, f) } return nil, nil From 3e8da475a3daa320ae0d991de6ea687ec21acb35 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Fri, 28 Oct 2022 17:18:32 -0400 Subject: [PATCH 388/723] internal/jsonrpc2_v2: initiate shutdown when the Writer breaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this CL we already shut down a jsonrpc2_v2.Conn when its Reader breaks, which we expect to be the common shutdown path. However, with certain kinds of connections (notably those over stdin+stdout), it is possible for the Writer side to fail while the Reader remains working. If the Writer has failed, we have no way to return the required Response messages for incoming calls, nor to write new Request messages of our own. Since we have no way to return a response, we will now mark those incoming calls as canceled. However, even if the Writer has failed we may still be able to read the responses for any outgoing calls that are already in flight. When our in-flight calls complete, we could in theory even continue to process Notification messages from the Reader; however, those are unlikely to be useful with half the connection broken. It seems more helpful — and less surprising — to go ahead and shut down the connection completely when it becomes idle. Updates golang/go#46520. Updates golang/go#49387. Change-Id: I713f172ca7031f4211da321560fe7eae57960a48 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446315 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Auto-Submit: Bryan Mills Run-TryBot: Bryan Mills --- internal/jsonrpc2_v2/conn.go | 129 +++++++++++++++++++++++++---------- 1 file changed, 94 insertions(+), 35 deletions(-) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 7c48e2ec616..74f1de15352 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -82,10 +82,12 @@ type Connection struct { // inFlightState records the state of the incoming and outgoing calls on a // Connection. type inFlightState struct { - closing bool // disallow enqueuing further requests, and close the Closer when transitioning to idle - readErr error + closing bool // disallow enqueuing further requests, and close the Closer when transitioning to idle + readErr error + writeErr error - outgoing map[ID]*AsyncCall // calls only + outgoingCalls map[ID]*AsyncCall // calls only + outgoingNotifications int // # of notifications awaiting "write" // incoming stores the total number of incoming calls and notifications // that have not yet written or processed a result. @@ -104,7 +106,7 @@ type inFlightState struct { // updateInFlight locks the state of the connection's in-flight requests, allows // f to mutate that state, and closes the connection if it is idle and either -// is closing or has a read error. +// is closing or has a read or write error. func (c *Connection) updateInFlight(f func(*inFlightState)) { c.stateMu.Lock() defer c.stateMu.Unlock() @@ -113,8 +115,8 @@ func (c *Connection) updateInFlight(f func(*inFlightState)) { f(s) - idle := s.incoming == 0 && len(s.outgoing) == 0 && !s.handlerRunning - if idle && (s.closing || s.readErr != nil) && !s.closed { + idle := len(s.outgoingCalls) == 0 && s.outgoingNotifications == 0 && s.incoming == 0 && !s.handlerRunning + if idle && (s.closing || s.readErr != nil || s.writeErr != nil) && !s.closed { c.closeErr <- c.closer.Close() if c.onDone != nil { c.onDone() @@ -181,20 +183,42 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde // Notify invokes the target method but does not wait for a response. // The params will be marshaled to JSON before sending over the wire, and will // be handed to the method invoked. -func (c *Connection) Notify(ctx context.Context, method string, params interface{}) error { - notify, err := NewNotification(method, params) - if err != nil { - return fmt.Errorf("marshaling notify parameters: %v", err) - } +func (c *Connection) Notify(ctx context.Context, method string, params interface{}) (err error) { ctx, done := event.Start(ctx, method, tag.Method.Of(method), tag.RPCDirection.Of(tag.Outbound), ) + attempted := false + + defer func() { + labelStatus(ctx, err) + done() + if attempted { + c.updateInFlight(func(s *inFlightState) { + s.outgoingNotifications-- + }) + } + }() + + c.updateInFlight(func(s *inFlightState) { + if s.writeErr != nil { + err = fmt.Errorf("%w: %v", ErrClientClosing, s.writeErr) + return + } + s.outgoingNotifications++ + attempted = true + }) + if err != nil { + return err + } + + notify, err := NewNotification(method, params) + if err != nil { + return fmt.Errorf("marshaling notify parameters: %v", err) + } + event.Metric(ctx, tag.Started.Of(1)) - err = c.write(ctx, notify) - labelStatus(ctx, err) - done() - return err + return c.write(ctx, notify) } // Call invokes the target method and returns an object that can be used to await the response. @@ -239,10 +263,18 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{} err = fmt.Errorf("%w: %v", ErrClientClosing, s.readErr) return } - if s.outgoing == nil { - s.outgoing = make(map[ID]*AsyncCall) + if s.writeErr != nil { + // Don't start the call if the write end has failed, either. + // We have reason to believe that the write would not succeed, + // and if we avoid adding in-flight calls then eventually + // the connection will go idle and be closed. + err = fmt.Errorf("%w: %v", ErrClientClosing, s.writeErr) + return + } + if s.outgoingCalls == nil { + s.outgoingCalls = make(map[ID]*AsyncCall) } - s.outgoing[ac.id] = ac + s.outgoingCalls[ac.id] = ac }) if err != nil { ac.retire(&Response{ID: id, Error: err}) @@ -254,8 +286,8 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{} // Sending failed. We will never get a response, so deliver a fake one if it // wasn't already retired by the connection breaking. c.updateInFlight(func(s *inFlightState) { - if s.outgoing[ac.id] == ac { - delete(s.outgoing, ac.id) + if s.outgoingCalls[ac.id] == ac { + delete(s.outgoingCalls, ac.id) ac.retire(&Response{ID: id, Error: err}) } else { // ac was already retired by the readIncoming goroutine: @@ -405,8 +437,8 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter case *Response: c.updateInFlight(func(s *inFlightState) { - if ac, ok := s.outgoing[msg.ID]; ok { - delete(s.outgoing, msg.ID) + if ac, ok := s.outgoingCalls[msg.ID]; ok { + delete(s.outgoingCalls, msg.ID) ac.retire(msg) } else { // TODO: How should we report unexpected responses? @@ -423,10 +455,10 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter // Retire any outgoing requests that were still in flight: with the Reader no // longer being processed, they necessarily cannot receive a response. - for id, ac := range s.outgoing { + for id, ac := range s.outgoingCalls { ac.retire(&Response{ID: id, Error: err}) } - s.outgoing = nil + s.outgoingCalls = nil }) } @@ -482,6 +514,14 @@ func (c *Connection) acceptRequest(ctx context.Context, msg *Request, msgBytes i err = ErrServerClosing return } + + if s.writeErr != nil { + // The write side of the connection appears to be broken, + // so we won't be able to write a response to this request. + // Avoid unnecessary work to compute it. + err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr) + return + } } }) if err != nil { @@ -557,12 +597,19 @@ func (c *Connection) handleAsync() { return } - var result interface{} - err := req.ctx.Err() - if err == nil { - // Only deliver to the Handler if not already cancelled. - result, err = c.handler.Handle(req.ctx, req.Request) + // Only deliver to the Handler if not already canceled. + if err := req.ctx.Err(); err != nil { + c.updateInFlight(func(s *inFlightState) { + if s.writeErr != nil { + // Assume that req.ctx was canceled due to s.writeErr. + // TODO(#51365): use a Context API to plumb this through req.ctx. + err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr) + } + }) + c.processResult("handleAsync", req, nil, err) } + + result, err := c.handler.Handle(req.ctx, req.Request) c.processResult(c.handler, req, result, err) } } @@ -646,12 +693,24 @@ func (c *Connection) write(ctx context.Context, msg Message) error { n, err := writer.Write(ctx, msg) event.Metric(ctx, tag.SentBytes.Of(n)) - // TODO: if err != nil, that suggests that future writes will not succeed, - // so we cannot possibly write the results of incoming Call requests. - // If the read side of the connection is also broken, we also might not have - // a way to receive cancellation notifications. - // - // Should we cancel the pending calls implicitly? + if err != nil && ctx.Err() == nil { + // The call to Write failed, and since ctx.Err() is nil we can't attribute + // the failure (even indirectly) to Context cancellation. The writer appears + // to be broken, and future writes are likely to also fail. + // + // If the read side of the connection is also broken, we might not even be + // able to receive cancellation notifications. Since we can't reliably write + // the results of incoming calls and can't receive explicit cancellations, + // cancel the calls now. + c.updateInFlight(func(s *inFlightState) { + if s.writeErr == nil { + s.writeErr = err + for _, r := range s.incomingByID { + r.cancel() + } + } + }) + } return err } From 70a130ebec0493e708c660233787063ea33e71e4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Sun, 23 Oct 2022 15:01:13 -0400 Subject: [PATCH 389/723] gopls/api-diff: simplify the api-diff implementation Simplify the api-diff implementation to use `go run` and cmp.Diff. The latter is more maintainable and produces more readable output, due to supporting line diffs for multi-line strings. For golang/go#54459 Change-Id: I11c00e9728ce241aef8f9828f3840b4202294a9a Reviewed-on: https://go-review.googlesource.com/c/tools/+/444799 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro --- gopls/api-diff/api_diff.go | 252 ++++++------------------------------- 1 file changed, 38 insertions(+), 214 deletions(-) diff --git a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go index f39feaa6107..8bb54186bab 100644 --- a/gopls/api-diff/api_diff.go +++ b/gopls/api-diff/api_diff.go @@ -13,253 +13,77 @@ import ( "encoding/json" "flag" "fmt" - "io" - "io/ioutil" "log" "os" "os/exec" - "path/filepath" - "strings" + "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/lsp/source" - diffpkg "golang.org/x/tools/internal/diff" - "golang.org/x/tools/internal/gocommand" ) -var ( - previousVersionFlag = flag.String("prev", "", "version to compare against") - versionFlag = flag.String("version", "", "version being tagged, or current version if omitted") -) +const usage = `api-diff [] + +Compare the API of two gopls versions. If the second argument is provided, it +will be used as the new version to compare against. Otherwise, compare against +the current API. +` func main() { flag.Parse() - apiDiff, err := diffAPI(*versionFlag, *previousVersionFlag) + if flag.NArg() < 1 || flag.NArg() > 2 { + fmt.Fprint(os.Stderr, usage) + os.Exit(2) + } + + oldVer := flag.Arg(0) + newVer := "" + if flag.NArg() == 2 { + newVer = flag.Arg(1) + } + + apiDiff, err := diffAPI(oldVer, newVer) if err != nil { log.Fatal(err) } - fmt.Printf(` -%s -`, apiDiff) -} - -type JSON interface { - String() string - Write(io.Writer) + fmt.Println("\n" + apiDiff) } -func diffAPI(version, prev string) (string, error) { +func diffAPI(oldVer, newVer string) (string, error) { ctx := context.Background() - previousApi, err := loadAPI(ctx, prev) + previousAPI, err := loadAPI(ctx, oldVer) if err != nil { - return "", fmt.Errorf("load previous API: %v", err) + return "", fmt.Errorf("loading %s: %v", oldVer, err) } - var currentApi *source.APIJSON - if version == "" { - currentApi = source.GeneratedAPIJSON + var currentAPI *source.APIJSON + if newVer == "" { + currentAPI = source.GeneratedAPIJSON } else { var err error - currentApi, err = loadAPI(ctx, version) + currentAPI, err = loadAPI(ctx, newVer) if err != nil { - return "", fmt.Errorf("load current API: %v", err) + return "", fmt.Errorf("loading %s: %v", newVer, err) } } - b := &strings.Builder{} - if err := diff(b, previousApi.Commands, currentApi.Commands, "command", func(c *source.CommandJSON) string { - return c.Command - }, diffCommands); err != nil { - return "", fmt.Errorf("diff commands: %v", err) - } - if diff(b, previousApi.Analyzers, currentApi.Analyzers, "analyzer", func(a *source.AnalyzerJSON) string { - return a.Name - }, diffAnalyzers); err != nil { - return "", fmt.Errorf("diff analyzers: %v", err) - } - if err := diff(b, previousApi.Lenses, currentApi.Lenses, "code lens", func(l *source.LensJSON) string { - return l.Lens - }, diffLenses); err != nil { - return "", fmt.Errorf("diff lenses: %v", err) - } - for key, prev := range previousApi.Options { - current, ok := currentApi.Options[key] - if !ok { - panic(fmt.Sprintf("unexpected option key: %s", key)) - } - if err := diff(b, prev, current, "option", func(o *source.OptionJSON) string { - return o.Name - }, diffOptions); err != nil { - return "", fmt.Errorf("diff options (%s): %v", key, err) - } - } - - return b.String(), nil + return cmp.Diff(previousAPI, currentAPI), nil } -func diff[T JSON](b *strings.Builder, previous, new []T, kind string, uniqueKey func(T) string, diffFunc func(*strings.Builder, T, T)) error { - prevJSON := collect(previous, uniqueKey) - newJSON := collect(new, uniqueKey) - for k := range newJSON { - delete(prevJSON, k) - } - for _, deleted := range prevJSON { - b.WriteString(fmt.Sprintf("%s %s was deleted.\n", kind, deleted)) - } - for _, prev := range previous { - delete(newJSON, uniqueKey(prev)) - } - if len(newJSON) > 0 { - b.WriteString("The following commands were added:\n") - for _, n := range newJSON { - n.Write(b) - b.WriteByte('\n') - } - } - previousMap := collect(previous, uniqueKey) - for _, current := range new { - prev, ok := previousMap[uniqueKey(current)] - if !ok { - continue - } - c, p := bytes.NewBuffer(nil), bytes.NewBuffer(nil) - prev.Write(p) - current.Write(c) - if diff := diffStr(p.String(), c.String()); diff != "" { - diffFunc(b, prev, current) - b.WriteString("\n--\n") - } - } - return nil -} - -func collect[T JSON](args []T, uniqueKey func(T) string) map[string]T { - m := map[string]T{} - for _, arg := range args { - m[uniqueKey(arg)] = arg - } - return m -} - -var goCmdRunner = gocommand.Runner{} - func loadAPI(ctx context.Context, version string) (*source.APIJSON, error) { - tmpGopath, err := ioutil.TempDir("", "gopath*") - if err != nil { - return nil, fmt.Errorf("temp dir: %v", err) - } - defer os.RemoveAll(tmpGopath) + ver := fmt.Sprintf("golang.org/x/tools/gopls@%s", version) + cmd := exec.Command("go", "run", ver, "api-json") - exampleDir := fmt.Sprintf("%s/src/example.com", tmpGopath) - if err := os.MkdirAll(exampleDir, 0776); err != nil { - return nil, fmt.Errorf("mkdir: %v", err) - } + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + cmd.Stdout = stdout + cmd.Stderr = stderr - if stdout, err := goCmdRunner.Run(ctx, gocommand.Invocation{ - Verb: "mod", - Args: []string{"init", "example.com"}, - WorkingDir: exampleDir, - Env: append(os.Environ(), fmt.Sprintf("GOPATH=%s", tmpGopath)), - }); err != nil { - return nil, fmt.Errorf("go mod init failed: %v (stdout: %v)", err, stdout) - } - if stdout, err := goCmdRunner.Run(ctx, gocommand.Invocation{ - Verb: "install", - Args: []string{fmt.Sprintf("golang.org/x/tools/gopls@%s", version)}, - WorkingDir: exampleDir, - Env: append(os.Environ(), fmt.Sprintf("GOPATH=%s", tmpGopath)), - }); err != nil { - return nil, fmt.Errorf("go install failed: %v (stdout: %v)", err, stdout.String()) - } - cmd := exec.Cmd{ - Path: filepath.Join(tmpGopath, "bin", "gopls"), - Args: []string{"gopls", "api-json"}, - Dir: tmpGopath, - } - out, err := cmd.Output() - if err != nil { - return nil, fmt.Errorf("output: %v", err) + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("go run failed: %v; stderr:\n%s", err, stderr) } apiJson := &source.APIJSON{} - if err := json.Unmarshal(out, apiJson); err != nil { + if err := json.Unmarshal(stdout.Bytes(), apiJson); err != nil { return nil, fmt.Errorf("unmarshal: %v", err) } return apiJson, nil } - -func diffCommands(b *strings.Builder, prev, current *source.CommandJSON) { - if prev.Title != current.Title { - b.WriteString(fmt.Sprintf("Title changed from %q to %q\n", prev.Title, current.Title)) - } - if prev.Doc != current.Doc { - b.WriteString(fmt.Sprintf("Documentation changed from %q to %q\n", prev.Doc, current.Doc)) - } - if prev.ArgDoc != current.ArgDoc { - b.WriteString("Arguments changed from " + formatBlock(prev.ArgDoc) + " to " + formatBlock(current.ArgDoc)) - } - if prev.ResultDoc != current.ResultDoc { - b.WriteString("Results changed from " + formatBlock(prev.ResultDoc) + " to " + formatBlock(current.ResultDoc)) - } -} - -func diffAnalyzers(b *strings.Builder, previous, current *source.AnalyzerJSON) { - b.WriteString(fmt.Sprintf("Changes to analyzer %s:\n\n", current.Name)) - if previous.Doc != current.Doc { - b.WriteString(fmt.Sprintf("Documentation changed from %q to %q\n", previous.Doc, current.Doc)) - } - if previous.Default != current.Default { - b.WriteString(fmt.Sprintf("Default changed from %v to %v\n", previous.Default, current.Default)) - } -} - -func diffLenses(b *strings.Builder, previous, current *source.LensJSON) { - b.WriteString(fmt.Sprintf("Changes to code lens %s:\n\n", current.Title)) - if previous.Title != current.Title { - b.WriteString(fmt.Sprintf("Title changed from %q to %q\n", previous.Title, current.Title)) - } - if previous.Doc != current.Doc { - b.WriteString(fmt.Sprintf("Documentation changed from %q to %q\n", previous.Doc, current.Doc)) - } -} - -func diffOptions(b *strings.Builder, previous, current *source.OptionJSON) { - b.WriteString(fmt.Sprintf("Changes to option %s:\n\n", current.Name)) - if previous.Doc != current.Doc { - diff := diffStr(previous.Doc, current.Doc) - fmt.Fprintf(b, "Documentation changed:\n%s\n", diff) - } - if previous.Default != current.Default { - b.WriteString(fmt.Sprintf("Default changed from %q to %q\n", previous.Default, current.Default)) - } - if previous.Hierarchy != current.Hierarchy { - b.WriteString(fmt.Sprintf("Categorization changed from %q to %q\n", previous.Hierarchy, current.Hierarchy)) - } - if previous.Status != current.Status { - b.WriteString(fmt.Sprintf("Status changed from %q to %q\n", previous.Status, current.Status)) - } - if previous.Type != current.Type { - b.WriteString(fmt.Sprintf("Type changed from %q to %q\n", previous.Type, current.Type)) - } - // TODO(rstambler): Handle possibility of same number but different keys/values. - if len(previous.EnumKeys.Keys) != len(current.EnumKeys.Keys) { - b.WriteString(fmt.Sprintf("Enum keys changed from\n%s\n to \n%s\n", previous.EnumKeys, current.EnumKeys)) - } - if len(previous.EnumValues) != len(current.EnumValues) { - b.WriteString(fmt.Sprintf("Enum values changed from\n%s\n to \n%s\n", previous.EnumValues, current.EnumValues)) - } -} - -func formatBlock(str string) string { - if str == "" { - return `""` - } - return "\n```\n" + str + "\n```\n" -} - -func diffStr(before, after string) string { - if before == after { - return "" - } - // Add newlines to avoid newline messages in diff. - unified := diffpkg.Unified("previous", "current", before+"\n", after+"\n") - return fmt.Sprintf("%q", unified) -} From 73fcd88827877d4d7a9fa5a016f12b08e892e906 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 1 Nov 2022 11:25:50 -0400 Subject: [PATCH 390/723] Revert "internal/jsonrpc2_v2: initiate shutdown when the Writer breaks" This reverts CL 446315 due to yet-undiagnosed bugs exposed on the -race builders. Fixes golang/go#56510. Change-Id: I41084359b74580f65cc82db0a174194bd2102ff1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446859 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills Reviewed-by: Robert Findley Auto-Submit: Bryan Mills --- internal/jsonrpc2_v2/conn.go | 129 ++++++++++------------------------- 1 file changed, 35 insertions(+), 94 deletions(-) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 74f1de15352..7c48e2ec616 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -82,12 +82,10 @@ type Connection struct { // inFlightState records the state of the incoming and outgoing calls on a // Connection. type inFlightState struct { - closing bool // disallow enqueuing further requests, and close the Closer when transitioning to idle - readErr error - writeErr error + closing bool // disallow enqueuing further requests, and close the Closer when transitioning to idle + readErr error - outgoingCalls map[ID]*AsyncCall // calls only - outgoingNotifications int // # of notifications awaiting "write" + outgoing map[ID]*AsyncCall // calls only // incoming stores the total number of incoming calls and notifications // that have not yet written or processed a result. @@ -106,7 +104,7 @@ type inFlightState struct { // updateInFlight locks the state of the connection's in-flight requests, allows // f to mutate that state, and closes the connection if it is idle and either -// is closing or has a read or write error. +// is closing or has a read error. func (c *Connection) updateInFlight(f func(*inFlightState)) { c.stateMu.Lock() defer c.stateMu.Unlock() @@ -115,8 +113,8 @@ func (c *Connection) updateInFlight(f func(*inFlightState)) { f(s) - idle := len(s.outgoingCalls) == 0 && s.outgoingNotifications == 0 && s.incoming == 0 && !s.handlerRunning - if idle && (s.closing || s.readErr != nil || s.writeErr != nil) && !s.closed { + idle := s.incoming == 0 && len(s.outgoing) == 0 && !s.handlerRunning + if idle && (s.closing || s.readErr != nil) && !s.closed { c.closeErr <- c.closer.Close() if c.onDone != nil { c.onDone() @@ -183,42 +181,20 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde // Notify invokes the target method but does not wait for a response. // The params will be marshaled to JSON before sending over the wire, and will // be handed to the method invoked. -func (c *Connection) Notify(ctx context.Context, method string, params interface{}) (err error) { - ctx, done := event.Start(ctx, method, - tag.Method.Of(method), - tag.RPCDirection.Of(tag.Outbound), - ) - attempted := false - - defer func() { - labelStatus(ctx, err) - done() - if attempted { - c.updateInFlight(func(s *inFlightState) { - s.outgoingNotifications-- - }) - } - }() - - c.updateInFlight(func(s *inFlightState) { - if s.writeErr != nil { - err = fmt.Errorf("%w: %v", ErrClientClosing, s.writeErr) - return - } - s.outgoingNotifications++ - attempted = true - }) - if err != nil { - return err - } - +func (c *Connection) Notify(ctx context.Context, method string, params interface{}) error { notify, err := NewNotification(method, params) if err != nil { return fmt.Errorf("marshaling notify parameters: %v", err) } - + ctx, done := event.Start(ctx, method, + tag.Method.Of(method), + tag.RPCDirection.Of(tag.Outbound), + ) event.Metric(ctx, tag.Started.Of(1)) - return c.write(ctx, notify) + err = c.write(ctx, notify) + labelStatus(ctx, err) + done() + return err } // Call invokes the target method and returns an object that can be used to await the response. @@ -263,18 +239,10 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{} err = fmt.Errorf("%w: %v", ErrClientClosing, s.readErr) return } - if s.writeErr != nil { - // Don't start the call if the write end has failed, either. - // We have reason to believe that the write would not succeed, - // and if we avoid adding in-flight calls then eventually - // the connection will go idle and be closed. - err = fmt.Errorf("%w: %v", ErrClientClosing, s.writeErr) - return - } - if s.outgoingCalls == nil { - s.outgoingCalls = make(map[ID]*AsyncCall) + if s.outgoing == nil { + s.outgoing = make(map[ID]*AsyncCall) } - s.outgoingCalls[ac.id] = ac + s.outgoing[ac.id] = ac }) if err != nil { ac.retire(&Response{ID: id, Error: err}) @@ -286,8 +254,8 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{} // Sending failed. We will never get a response, so deliver a fake one if it // wasn't already retired by the connection breaking. c.updateInFlight(func(s *inFlightState) { - if s.outgoingCalls[ac.id] == ac { - delete(s.outgoingCalls, ac.id) + if s.outgoing[ac.id] == ac { + delete(s.outgoing, ac.id) ac.retire(&Response{ID: id, Error: err}) } else { // ac was already retired by the readIncoming goroutine: @@ -437,8 +405,8 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter case *Response: c.updateInFlight(func(s *inFlightState) { - if ac, ok := s.outgoingCalls[msg.ID]; ok { - delete(s.outgoingCalls, msg.ID) + if ac, ok := s.outgoing[msg.ID]; ok { + delete(s.outgoing, msg.ID) ac.retire(msg) } else { // TODO: How should we report unexpected responses? @@ -455,10 +423,10 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter // Retire any outgoing requests that were still in flight: with the Reader no // longer being processed, they necessarily cannot receive a response. - for id, ac := range s.outgoingCalls { + for id, ac := range s.outgoing { ac.retire(&Response{ID: id, Error: err}) } - s.outgoingCalls = nil + s.outgoing = nil }) } @@ -514,14 +482,6 @@ func (c *Connection) acceptRequest(ctx context.Context, msg *Request, msgBytes i err = ErrServerClosing return } - - if s.writeErr != nil { - // The write side of the connection appears to be broken, - // so we won't be able to write a response to this request. - // Avoid unnecessary work to compute it. - err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr) - return - } } }) if err != nil { @@ -597,19 +557,12 @@ func (c *Connection) handleAsync() { return } - // Only deliver to the Handler if not already canceled. - if err := req.ctx.Err(); err != nil { - c.updateInFlight(func(s *inFlightState) { - if s.writeErr != nil { - // Assume that req.ctx was canceled due to s.writeErr. - // TODO(#51365): use a Context API to plumb this through req.ctx. - err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr) - } - }) - c.processResult("handleAsync", req, nil, err) + var result interface{} + err := req.ctx.Err() + if err == nil { + // Only deliver to the Handler if not already cancelled. + result, err = c.handler.Handle(req.ctx, req.Request) } - - result, err := c.handler.Handle(req.ctx, req.Request) c.processResult(c.handler, req, result, err) } } @@ -693,24 +646,12 @@ func (c *Connection) write(ctx context.Context, msg Message) error { n, err := writer.Write(ctx, msg) event.Metric(ctx, tag.SentBytes.Of(n)) - if err != nil && ctx.Err() == nil { - // The call to Write failed, and since ctx.Err() is nil we can't attribute - // the failure (even indirectly) to Context cancellation. The writer appears - // to be broken, and future writes are likely to also fail. - // - // If the read side of the connection is also broken, we might not even be - // able to receive cancellation notifications. Since we can't reliably write - // the results of incoming calls and can't receive explicit cancellations, - // cancel the calls now. - c.updateInFlight(func(s *inFlightState) { - if s.writeErr == nil { - s.writeErr = err - for _, r := range s.incomingByID { - r.cancel() - } - } - }) - } + // TODO: if err != nil, that suggests that future writes will not succeed, + // so we cannot possibly write the results of incoming Call requests. + // If the read side of the connection is also broken, we also might not have + // a way to receive cancellation notifications. + // + // Should we cancel the pending calls implicitly? return err } From 6e9dc865e2d3de2afb0dc7096f92fa4b3995b5b5 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 31 Oct 2022 20:53:11 -0400 Subject: [PATCH 391/723] gopls/internal/lsp/source/completion: fix panic in completion on *error Fix a panic during completion on variables of type *error. As a predeclared type, the error type has nil package. Fix the crash resulting from this oversight, as well as a related crash in the tests analyzer, from which the new completion code was adapted. Fixes golang/go#56505 Change-Id: I0707924d0666b238821fd14b6fc34639cc7a9c53 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446815 Auto-Submit: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- go/analysis/passes/tests/testdata/src/a/go118_test.go | 5 +++++ go/analysis/passes/tests/tests.go | 4 +++- gopls/internal/lsp/source/completion/completion.go | 4 +++- gopls/internal/lsp/testdata/issues/issue56505.go | 8 ++++++++ gopls/internal/lsp/testdata/summary.txt.golden | 2 +- gopls/internal/lsp/testdata/summary_go1.18.txt.golden | 2 +- 6 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 gopls/internal/lsp/testdata/issues/issue56505.go diff --git a/go/analysis/passes/tests/testdata/src/a/go118_test.go b/go/analysis/passes/tests/testdata/src/a/go118_test.go index dc898daca0b..e2bc3f3a0bd 100644 --- a/go/analysis/passes/tests/testdata/src/a/go118_test.go +++ b/go/analysis/passes/tests/testdata/src/a/go118_test.go @@ -94,3 +94,8 @@ func FuzzObjectMethod(f *testing.F) { } f.Fuzz(obj.myVar) // ok } + +// Test for golang/go#56505: checking fuzz arguments should not panic on *error. +func FuzzIssue56505(f *testing.F) { + f.Fuzz(func(e *error) {}) // want "the first parameter of a fuzz target must be \\*testing.T" +} diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go index cab2fa20fa5..935aad00c98 100644 --- a/go/analysis/passes/tests/tests.go +++ b/go/analysis/passes/tests/tests.go @@ -269,7 +269,9 @@ func isTestingType(typ types.Type, testingType string) bool { if !ok { return false } - return named.Obj().Pkg().Path() == "testing" && named.Obj().Name() == testingType + obj := named.Obj() + // obj.Pkg is nil for the error type. + return obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "testing" && obj.Name() == testingType } // Validate that fuzz target function's arguments are of accepted types. diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 7d98205d020..c3b7c2b461b 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -1280,7 +1280,9 @@ func isStarTestingDotF(typ types.Type) bool { if named == nil { return false } - return named.Obj() != nil && named.Obj().Pkg().Path() == "testing" && named.Obj().Name() == "F" + obj := named.Obj() + // obj.Pkg is nil for the error type. + return obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "testing" && obj.Name() == "F" } // lexical finds completions in the lexical environment. diff --git a/gopls/internal/lsp/testdata/issues/issue56505.go b/gopls/internal/lsp/testdata/issues/issue56505.go new file mode 100644 index 00000000000..8c641bfb852 --- /dev/null +++ b/gopls/internal/lsp/testdata/issues/issue56505.go @@ -0,0 +1,8 @@ +package issues + +// Test for golang/go#56505: completion on variables of type *error should not +// panic. +func _() { + var e *error + e.x //@complete(" //") +} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 107e900a731..cfe8e4a267d 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,7 +1,7 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 262 +CompletionsCount = 263 CompletionSnippetCount = 106 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 1e1c8762a8f..2b7bf976b2f 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -1,7 +1,7 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 263 +CompletionsCount = 264 CompletionSnippetCount = 115 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 From feeb0ba9141e8a7e3dbb09036f9d5bdbe7742eed Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 1 Nov 2022 10:27:58 -0400 Subject: [PATCH 392/723] gopls/internal/lsp/cmd: fix deadlock when opening a file Rename (*connection).AddFile to openFile, which is more accurate, and fix a deadlock resulting from holding a Client lock while issuing a Server request. Fixes golang/go#56450 Change-Id: Ie6f34613e1e10e3274c3e6728b12f77e3a523b89 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446856 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Auto-Submit: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan --- gopls/internal/lsp/cmd/call_hierarchy.go | 6 ++--- gopls/internal/lsp/cmd/check.go | 2 +- gopls/internal/lsp/cmd/cmd.go | 28 ++++++++++++---------- gopls/internal/lsp/cmd/definition.go | 4 ++-- gopls/internal/lsp/cmd/folding_range.go | 2 +- gopls/internal/lsp/cmd/format.go | 2 +- gopls/internal/lsp/cmd/highlight.go | 2 +- gopls/internal/lsp/cmd/implementation.go | 4 ++-- gopls/internal/lsp/cmd/imports.go | 2 +- gopls/internal/lsp/cmd/links.go | 2 +- gopls/internal/lsp/cmd/prepare_rename.go | 2 +- gopls/internal/lsp/cmd/references.go | 4 ++-- gopls/internal/lsp/cmd/rename.go | 4 ++-- gopls/internal/lsp/cmd/semantictokens.go | 2 +- gopls/internal/lsp/cmd/signature.go | 2 +- gopls/internal/lsp/cmd/suggested_fix.go | 2 +- gopls/internal/lsp/cmd/workspace_symbol.go | 2 +- 17 files changed, 37 insertions(+), 35 deletions(-) diff --git a/gopls/internal/lsp/cmd/call_hierarchy.go b/gopls/internal/lsp/cmd/call_hierarchy.go index 7736eecbe59..295dea8b0d4 100644 --- a/gopls/internal/lsp/cmd/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/call_hierarchy.go @@ -47,7 +47,7 @@ func (c *callHierarchy) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -114,7 +114,7 @@ func (c *callHierarchy) Run(ctx context.Context, args ...string) error { // callItemPrintString returns a protocol.CallHierarchyItem object represented as a string. // item and call ranges (protocol.Range) are converted to user friendly spans (1-indexed). func callItemPrintString(ctx context.Context, conn *connection, item protocol.CallHierarchyItem, callsURI protocol.DocumentURI, calls []protocol.Range) (string, error) { - itemFile := conn.AddFile(ctx, item.URI.SpanURI()) + itemFile := conn.openFile(ctx, item.URI.SpanURI()) if itemFile.err != nil { return "", itemFile.err } @@ -123,7 +123,7 @@ func callItemPrintString(ctx context.Context, conn *connection, item protocol.Ca return "", err } - callsFile := conn.AddFile(ctx, callsURI.SpanURI()) + callsFile := conn.openFile(ctx, callsURI.SpanURI()) if callsURI != "" && callsFile.err != nil { return "", callsFile.err } diff --git a/gopls/internal/lsp/cmd/check.go b/gopls/internal/lsp/cmd/check.go index 5e4fe39eb36..cf081ca2615 100644 --- a/gopls/internal/lsp/cmd/check.go +++ b/gopls/internal/lsp/cmd/check.go @@ -48,7 +48,7 @@ func (c *check) Run(ctx context.Context, args ...string) error { for _, arg := range args { uri := span.URIFromPath(arg) uris = append(uris, uri) - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index 38accb6b9bb..5c64e108668 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -399,7 +399,7 @@ type cmdFile struct { uri span.URI mapper *protocol.ColumnMapper err error - added bool + open bool diagnostics []protocol.Diagnostic } @@ -558,22 +558,24 @@ func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile { return file } -func (c *connection) AddFile(ctx context.Context, uri span.URI) *cmdFile { - c.Client.filesMu.Lock() - defer c.Client.filesMu.Unlock() +func (c *cmdClient) openFile(ctx context.Context, uri span.URI) *cmdFile { + c.filesMu.Lock() + defer c.filesMu.Unlock() - file := c.Client.getFile(ctx, uri) - // This should never happen. - if file == nil { - return &cmdFile{ - uri: uri, - err: fmt.Errorf("no file found for %s", uri), - } + file := c.getFile(ctx, uri) + if file.err != nil || file.open { + return file } - if file.err != nil || file.added { + file.open = true + return file +} + +func (c *connection) openFile(ctx context.Context, uri span.URI) *cmdFile { + file := c.Client.openFile(ctx, uri) + if file.err != nil { return file } - file.added = true + p := &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{ URI: protocol.URIFromSpanURI(uri), diff --git a/gopls/internal/lsp/cmd/definition.go b/gopls/internal/lsp/cmd/definition.go index 9096e17153e..edfd7392902 100644 --- a/gopls/internal/lsp/cmd/definition.go +++ b/gopls/internal/lsp/cmd/definition.go @@ -80,7 +80,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { } defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -113,7 +113,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { if hover == nil { return fmt.Errorf("%v: not an identifier", from) } - file = conn.AddFile(ctx, fileURI(locs[0].URI)) + file = conn.openFile(ctx, fileURI(locs[0].URI)) if file.err != nil { return fmt.Errorf("%v: %v", from, file.err) } diff --git a/gopls/internal/lsp/cmd/folding_range.go b/gopls/internal/lsp/cmd/folding_range.go index 17cead91f00..7a9cbf9e8fb 100644 --- a/gopls/internal/lsp/cmd/folding_range.go +++ b/gopls/internal/lsp/cmd/folding_range.go @@ -44,7 +44,7 @@ func (r *foldingRanges) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go index 1ad9614b476..2b8109c670a 100644 --- a/gopls/internal/lsp/cmd/format.go +++ b/gopls/internal/lsp/cmd/format.go @@ -57,7 +57,7 @@ func (c *format) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) for _, arg := range args { spn := span.Parse(arg) - file := conn.AddFile(ctx, spn.URI()) + file := conn.openFile(ctx, spn.URI()) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/highlight.go b/gopls/internal/lsp/cmd/highlight.go index dcbd1097a06..0737e9c424a 100644 --- a/gopls/internal/lsp/cmd/highlight.go +++ b/gopls/internal/lsp/cmd/highlight.go @@ -47,7 +47,7 @@ func (r *highlight) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/implementation.go b/gopls/internal/lsp/cmd/implementation.go index 259b72572b4..dbc5fc3223b 100644 --- a/gopls/internal/lsp/cmd/implementation.go +++ b/gopls/internal/lsp/cmd/implementation.go @@ -47,7 +47,7 @@ func (i *implementation) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -71,7 +71,7 @@ func (i *implementation) Run(ctx context.Context, args ...string) error { var spans []string for _, impl := range implementations { - f := conn.AddFile(ctx, fileURI(impl.URI)) + f := conn.openFile(ctx, fileURI(impl.URI)) span, err := f.mapper.Span(impl) if err != nil { return err diff --git a/gopls/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go index 5b741739421..fadc8466834 100644 --- a/gopls/internal/lsp/cmd/imports.go +++ b/gopls/internal/lsp/cmd/imports.go @@ -56,7 +56,7 @@ func (t *imports) Run(ctx context.Context, args ...string) error { from := span.Parse(args[0]) uri := from.URI() - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/links.go b/gopls/internal/lsp/cmd/links.go index aec36da910c..b5413bba59f 100644 --- a/gopls/internal/lsp/cmd/links.go +++ b/gopls/internal/lsp/cmd/links.go @@ -53,7 +53,7 @@ func (l *links) Run(ctx context.Context, args ...string) error { from := span.Parse(args[0]) uri := from.URI() - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/prepare_rename.go b/gopls/internal/lsp/cmd/prepare_rename.go index 434904b6920..e61bd622fe0 100644 --- a/gopls/internal/lsp/cmd/prepare_rename.go +++ b/gopls/internal/lsp/cmd/prepare_rename.go @@ -51,7 +51,7 @@ func (r *prepareRename) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/references.go b/gopls/internal/lsp/cmd/references.go index bbebc9f917d..2abbb919299 100644 --- a/gopls/internal/lsp/cmd/references.go +++ b/gopls/internal/lsp/cmd/references.go @@ -51,7 +51,7 @@ func (r *references) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -74,7 +74,7 @@ func (r *references) Run(ctx context.Context, args ...string) error { } var spans []string for _, l := range locations { - f := conn.AddFile(ctx, fileURI(l.URI)) + f := conn.openFile(ctx, fileURI(l.URI)) // convert location to span for user-friendly 1-indexed line // and column numbers span, err := f.mapper.Span(l) diff --git a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go index 48c67e3d30d..2cbd260febb 100644 --- a/gopls/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -61,7 +61,7 @@ func (r *rename) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -92,7 +92,7 @@ func (r *rename) Run(ctx context.Context, args ...string) error { for _, u := range orderedURIs { uri := span.URIFromURI(u) - cmdFile := conn.AddFile(ctx, uri) + cmdFile := conn.openFile(ctx, uri) filename := cmdFile.uri.Filename() newContent, renameEdits, err := source.ApplyProtocolEdits(cmdFile.mapper, edits[uri]) diff --git a/gopls/internal/lsp/cmd/semantictokens.go b/gopls/internal/lsp/cmd/semantictokens.go index f90d49ccf34..3ed08d0248b 100644 --- a/gopls/internal/lsp/cmd/semantictokens.go +++ b/gopls/internal/lsp/cmd/semantictokens.go @@ -82,7 +82,7 @@ func (c *semtok) Run(ctx context.Context, args ...string) error { } defer conn.terminate(ctx) uri := span.URIFromPath(args[0]) - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/signature.go b/gopls/internal/lsp/cmd/signature.go index 657d77235f0..77805628ad0 100644 --- a/gopls/internal/lsp/cmd/signature.go +++ b/gopls/internal/lsp/cmd/signature.go @@ -46,7 +46,7 @@ func (r *signature) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go index 082e0481007..78310b3b3b9 100644 --- a/gopls/internal/lsp/cmd/suggested_fix.go +++ b/gopls/internal/lsp/cmd/suggested_fix.go @@ -56,7 +56,7 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { from := span.Parse(args[0]) uri := from.URI() - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } diff --git a/gopls/internal/lsp/cmd/workspace_symbol.go b/gopls/internal/lsp/cmd/workspace_symbol.go index 71a121e3362..be1e24ef324 100644 --- a/gopls/internal/lsp/cmd/workspace_symbol.go +++ b/gopls/internal/lsp/cmd/workspace_symbol.go @@ -73,7 +73,7 @@ func (r *workspaceSymbol) Run(ctx context.Context, args ...string) error { return err } for _, s := range symbols { - f := conn.AddFile(ctx, fileURI(s.Location.URI)) + f := conn.openFile(ctx, fileURI(s.Location.URI)) span, err := f.mapper.Span(s.Location) if err != nil { return err From 32e1cb7aeda130054b8750169c91731e30c40400 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 27 Oct 2022 12:24:51 -0400 Subject: [PATCH 393/723] gopls/internal/lsp: clarify control around diagnostics This CL includes some clarifications while trying to understand the performance of the initial workspace load and analysis. No significant behavior changes. Server.diagnose: - Factor the four copies of the logic for dealing with diagnostics and errors. - Make the ActivePackages blocking step explicit. Previously mod.Diagnostics would do this implicitly, making it look more expensive than it is. Server.addFolders: - eliminate TODO. The logic is not in fact fishy. - use informative names and comments for WaitGroups. - use a channel in place of a non-counting WaitGroup. Also, give pkg a String method. Change-Id: Ia3eff4e784fc04796b636a4635abdfe8ca4e7b5a Reviewed-on: https://go-review.googlesource.com/c/tools/+/445897 Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/pkg.go | 2 + gopls/internal/lsp/diagnostics.go | 105 ++++++++++++-------------- gopls/internal/lsp/general.go | 47 ++++++------ gopls/internal/lsp/mod/diagnostics.go | 7 +- 4 files changed, 80 insertions(+), 81 deletions(-) diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 0b767b4b5b8..ddfb9ea7e96 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -36,6 +36,8 @@ type pkg struct { analyses memoize.Store // maps analyzer.Name to Promise[actionResult] } +func (p *pkg) String() string { return p.ID() } + // A loadScope defines a package loading scope for use with go/packages. type loadScope interface { aScope() diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index ff3e4b2ec24..603acca607a 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -217,6 +217,10 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn defer done() // Wait for a free diagnostics slot. + // TODO(adonovan): opt: shouldn't it be the analysis implementation's + // job to de-dup and limit resource consumption? In any case this + // this function spends most its time waiting for awaitLoaded, at + // least initially. select { case <-ctx.Done(): return @@ -226,73 +230,62 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn <-s.diagnosticsSema }() - // First, diagnose the go.mod file. - modReports, modErr := mod.Diagnostics(ctx, snapshot) - if ctx.Err() != nil { - log.Trace.Log(ctx, "diagnose cancelled") - return - } - if modErr != nil { - event.Error(ctx, "warning: diagnose go.mod", modErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID())) - } - for id, diags := range modReports { - if id.URI == "" { - event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) - continue + // common code for dispatching diagnostics + store := func(dsource diagnosticSource, operation string, diagsByFileID map[source.VersionedFileIdentity][]*source.Diagnostic, err error) { + if err != nil { + event.Error(ctx, "warning: while "+operation, err, + tag.Directory.Of(snapshot.View().Folder().Filename()), + tag.Snapshot.Of(snapshot.ID())) + } + for id, diags := range diagsByFileID { + if id.URI == "" { + event.Error(ctx, "missing URI while "+operation, fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) + continue + } + s.storeDiagnostics(snapshot, id.URI, dsource, diags) } - s.storeDiagnostics(snapshot, id.URI, modSource, diags) } - upgradeModReports, upgradeErr := mod.UpgradeDiagnostics(ctx, snapshot) + + // Diagnose go.mod upgrades. + upgradeReports, upgradeErr := mod.UpgradeDiagnostics(ctx, snapshot) if ctx.Err() != nil { log.Trace.Log(ctx, "diagnose cancelled") return } - if upgradeErr != nil { - event.Error(ctx, "warning: diagnose go.mod upgrades", upgradeErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID())) - } - for id, diags := range upgradeModReports { - if id.URI == "" { - event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) - continue - } - s.storeDiagnostics(snapshot, id.URI, modCheckUpgradesSource, diags) - } - vulnerabilityReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) + store(modCheckUpgradesSource, "diagnosing go.mod upgrades", upgradeReports, upgradeErr) + + // Diagnose vulnerabilities. + vulnReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) if ctx.Err() != nil { log.Trace.Log(ctx, "diagnose cancelled") return } - if vulnErr != nil { - event.Error(ctx, "warning: checking vulnerabilities", vulnErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID())) - } - for id, diags := range vulnerabilityReports { - if id.URI == "" { - event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) - continue - } - s.storeDiagnostics(snapshot, id.URI, modVulncheckSource, diags) - } + store(modVulncheckSource, "diagnosing vulnerabilities", vulnReports, vulnErr) - // Diagnose the go.work file, if it exists. + // Diagnose go.work file. workReports, workErr := work.Diagnostics(ctx, snapshot) if ctx.Err() != nil { log.Trace.Log(ctx, "diagnose cancelled") return } - if workErr != nil { - event.Error(ctx, "warning: diagnose go.work", workErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID())) - } - for id, diags := range workReports { - if id.URI == "" { - event.Error(ctx, "missing URI for work file diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) - continue - } - s.storeDiagnostics(snapshot, id.URI, workSource, diags) + store(workSource, "diagnosing go.work file", workReports, workErr) + + // All subsequent steps depend on the completion of + // type-checking of the all active packages in the workspace. + // This step may take many seconds initially. + // (mod.Diagnostics would implicitly wait for this too, + // but the control is clearer if it is explicit here.) + activePkgs, activeErr := snapshot.ActivePackages(ctx) + + // Diagnose go.mod file. + modReports, modErr := mod.Diagnostics(ctx, snapshot) + if ctx.Err() != nil { + log.Trace.Log(ctx, "diagnose cancelled") + return } + store(modSource, "diagnosing go.mod file", modReports, modErr) - // Diagnose all of the packages in the workspace. - wsPkgs, err := snapshot.ActivePackages(ctx) - if s.shouldIgnoreError(ctx, snapshot, err) { + if s.shouldIgnoreError(ctx, snapshot, activeErr) { return } criticalErr := snapshot.GetCriticalError(ctx) @@ -303,7 +296,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn // error progress reports will be closed. s.showCriticalErrorStatus(ctx, snapshot, criticalErr) - // There may be .tmpl files. + // Diagnose template (.tmpl) files. for _, f := range snapshot.Templates() { diags := template.Diagnose(f) s.storeDiagnostics(snapshot, f.URI(), typeCheckSource, diags) @@ -311,29 +304,31 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn // If there are no workspace packages, there is nothing to diagnose and // there are no orphaned files. - if len(wsPkgs) == 0 { + if len(activePkgs) == 0 { return } + // Run go/analysis diagnosis of packages in parallel. + // TODO(adonovan): opt: it may be more efficient to + // have diagnosePkg take a set of packages. var ( wg sync.WaitGroup seen = map[span.URI]struct{}{} ) - for _, pkg := range wsPkgs { - wg.Add(1) - + for _, pkg := range activePkgs { for _, pgf := range pkg.CompiledGoFiles() { seen[pgf.URI] = struct{}{} } + wg.Add(1) go func(pkg source.Package) { defer wg.Done() - s.diagnosePkg(ctx, snapshot, pkg, forceAnalysis) }(pkg) } wg.Wait() + // Orphaned files. // Confirm that every opened file belongs to a package (if any exist in // the workspace). Otherwise, add a diagnostic to the file. for _, o := range s.session.Overlays() { diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index 43973b94ed0..57348cd564b 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -308,18 +308,18 @@ func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol originalViews := len(s.session.Views()) viewErrors := make(map[span.URI]error) - var wg sync.WaitGroup + var ndiagnose sync.WaitGroup // number of unfinished diagnose calls if s.session.Options().VerboseWorkDoneProgress { work := s.progress.Start(ctx, DiagnosticWorkTitle(FromInitialWorkspaceLoad), "Calculating diagnostics for initial workspace load...", nil, nil) defer func() { go func() { - wg.Wait() + ndiagnose.Wait() work.End(ctx, "Done.") }() }() } // Only one view gets to have a workspace. - var allFoldersWg sync.WaitGroup + var nsnapshots sync.WaitGroup // number of unfinished snapshot initializations for _, folder := range folders { uri := span.URIFromURI(folder.URI) // Ignore non-file URIs. @@ -338,41 +338,40 @@ func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol } // Inv: release() must be called once. - var swg sync.WaitGroup - swg.Add(1) - allFoldersWg.Add(1) - // TODO(adonovan): this looks fishy. Is AwaitInitialized - // supposed to be called once per folder? - go func() { - defer swg.Done() - defer allFoldersWg.Done() - snapshot.AwaitInitialized(ctx) - work.End(ctx, "Finished loading packages.") - }() - // Print each view's environment. - buf := &bytes.Buffer{} - if err := snapshot.WriteEnv(ctx, buf); err != nil { + var buf bytes.Buffer + if err := snapshot.WriteEnv(ctx, &buf); err != nil { viewErrors[uri] = err release() continue } event.Log(ctx, buf.String()) - // Diagnose the newly created view. - wg.Add(1) + // Initialize snapshot asynchronously. + initialized := make(chan struct{}) + nsnapshots.Add(1) + go func() { + snapshot.AwaitInitialized(ctx) + work.End(ctx, "Finished loading packages.") + nsnapshots.Done() + close(initialized) // signal + }() + + // Diagnose the newly created view asynchronously. + ndiagnose.Add(1) go func() { s.diagnoseDetached(snapshot) - swg.Wait() + <-initialized release() - wg.Done() + ndiagnose.Done() }() } + // Wait for snapshots to be initialized so that all files are known. + // (We don't need to wait for diagnosis to finish.) + nsnapshots.Wait() + // Register for file watching notifications, if they are supported. - // Wait for all snapshots to be initialized first, since all files might - // not yet be known to the snapshots. - allFoldersWg.Wait() if err := s.updateWatchedDirectories(ctx); err != nil { event.Error(ctx, "failed to register for file watching notifications", err) } diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 829a03f9114..6e0067c5ef3 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -25,6 +25,8 @@ import ( ) // Diagnostics returns diagnostics for the modules in the workspace. +// +// It waits for completion of type-checking of all active packages. func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { ctx, done := event.Start(ctx, "mod.Diagnostics", tag.Snapshot.Of(snapshot.ID())) defer done() @@ -73,8 +75,9 @@ func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn fu return reports, nil } -// ModDiagnostics returns diagnostics from diagnosing the packages in the workspace and -// from tidying the go.mod file. +// ModDiagnostics waits for completion of type-checking of all active +// packages, then returns diagnostics from diagnosing the packages in +// the workspace and from tidying the go.mod file. func ModDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (diagnostics []*source.Diagnostic, err error) { pm, err := snapshot.ParseMod(ctx, fh) if err != nil { From 039b24b6251ccdce1094cd8e581fd38b357aa38e Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 1 Nov 2022 12:38:53 -0400 Subject: [PATCH 394/723] internal/jsonrpc2_v2: initiate shutdown when the Writer breaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this CL we already shut down a jsonrpc2_v2.Conn when its Reader breaks, which we expect to be the common shutdown path. However, with certain kinds of connections (notably those over stdin+stdout), it is possible for the Writer side to fail while the Reader remains working. If the Writer has failed, we have no way to return the required Response messages for incoming calls, nor to write new Request messages of our own. Since we have no way to return a response, we will now mark those incoming calls as canceled. However, even if the Writer has failed we may still be able to read the responses for any outgoing calls that are already in flight. When our in-flight calls complete, we could in theory even continue to process Notification messages from the Reader; however, those are unlikely to be useful with half the connection broken. It seems more helpful — and less surprising — to go ahead and shut down the connection completely when it becomes idle. This is a redo of CL 446315, with additional fixes for bugs exposed on the -race builders and some extra code cleanup from the process of diagnosing those bugs. Updates golang/go#46520. Updates golang/go#49387. Change-Id: I746409a7aa2c22d5651448ed0135b5ac21a9808e Reviewed-on: https://go-review.googlesource.com/c/tools/+/447035 Auto-Submit: Bryan Mills Run-TryBot: Bryan Mills TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan --- internal/jsonrpc2_v2/conn.go | 267 ++++++++++++++++++++++++---------- internal/jsonrpc2_v2/frame.go | 6 + 2 files changed, 196 insertions(+), 77 deletions(-) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 7c48e2ec616..085e775a741 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -68,10 +68,9 @@ type Connection struct { stateMu sync.Mutex state inFlightState // accessed only in updateInFlight + done chan struct{} // closed (under stateMu) when state.closed is true and all goroutines have completed - closer io.Closer // shuts down connection when Close has been called or the reader fails - closeErr chan error // 1-buffered; stores the error from closer.Close - writer chan Writer // 1-buffered; stores the writer when not in use + writer chan Writer // 1-buffered; stores the writer when not in use handler Handler @@ -82,10 +81,22 @@ type Connection struct { // inFlightState records the state of the incoming and outgoing calls on a // Connection. type inFlightState struct { - closing bool // disallow enqueuing further requests, and close the Closer when transitioning to idle - readErr error + connClosing bool // true when the Connection's Close method has been called + readErr error // non-nil when the readIncoming goroutine exits (typically io.EOF) + writeErr error // non-nil if a call to the Writer has failed with a non-canceled Context + + // closer shuts down and cleans up the Reader and Writer state, ideally + // interrupting any Read or Write call that is currently blocked. It is closed + // when the state is idle and one of: connClosing is true, readErr is non-nil, + // or writeErr is non-nil. + // + // After the closer has been invoked, the closer field is set to nil + // and the closeErr field is simultaneously set to its result. + closer io.Closer + closeErr error // error returned from closer.Close - outgoing map[ID]*AsyncCall // calls only + outgoingCalls map[ID]*AsyncCall // calls only + outgoingNotifications int // # of notifications awaiting "write" // incoming stores the total number of incoming calls and notifications // that have not yet written or processed a result. @@ -98,13 +109,11 @@ type inFlightState struct { // The queue does not include the request currently being handled (if any). handlerQueue []*incomingRequest handlerRunning bool - - closed bool // true after the closer has been invoked } // updateInFlight locks the state of the connection's in-flight requests, allows // f to mutate that state, and closes the connection if it is idle and either -// is closing or has a read error. +// is closing or has a read or write error. func (c *Connection) updateInFlight(f func(*inFlightState)) { c.stateMu.Lock() defer c.stateMu.Unlock() @@ -113,14 +122,70 @@ func (c *Connection) updateInFlight(f func(*inFlightState)) { f(s) - idle := s.incoming == 0 && len(s.outgoing) == 0 && !s.handlerRunning - if idle && (s.closing || s.readErr != nil) && !s.closed { - c.closeErr <- c.closer.Close() - if c.onDone != nil { - c.onDone() + select { + case <-c.done: + // The connection was already completely done at the start of this call to + // updateInFlight, so it must remain so. (The call to f should have noticed + // that and avoided making any updates that would cause the state to be + // non-idle.) + if !s.idle() { + panic("jsonrpc2_v2: updateInFlight transitioned to non-idle when already done") + } + return + default: + } + + if s.idle() && s.shuttingDown(ErrUnknown) != nil { + if s.closer != nil { + s.closeErr = s.closer.Close() + s.closer = nil // prevent duplicate Close calls } - s.closed = true + if s.readErr == nil { + // The readIncoming goroutine is still running. Our call to Close should + // cause it to exit soon, at which point it will make another call to + // updateInFlight, set s.readErr to a non-nil error, and mark the + // Connection done. + } else { + // The readIncoming goroutine has exited. Since everything else is idle, + // we're completely done. + if c.onDone != nil { + c.onDone() + } + close(c.done) + } + } +} + +// idle reports whether the connction is in a state with no pending calls or +// notifications. +// +// If idle returns true, the readIncoming goroutine may still be running, +// but no other goroutines are doing work on behalf of the connnection. +func (s *inFlightState) idle() bool { + return len(s.outgoingCalls) == 0 && s.outgoingNotifications == 0 && s.incoming == 0 && !s.handlerRunning +} + +// shuttingDown reports whether the connection is in a state that should +// disallow new (incoming and outgoing) calls. It returns either nil or +// an error that is or wraps the provided errClosing. +func (s *inFlightState) shuttingDown(errClosing error) error { + if s.connClosing { + // If Close has been called explicitly, it doesn't matter what state the + // Reader and Writer are in: we shouldn't be starting new work because the + // caller told us not to start new work. + return errClosing + } + if s.readErr != nil { + // If the read side of the connection is broken, we cannot read new call + // requests, and cannot read responses to our outgoing calls. + return fmt.Errorf("%w: %v", errClosing, s.readErr) } + if s.writeErr != nil { + // If the write side of the connection is broken, we cannot write responses + // for incoming calls, and cannot write requests for outgoing calls. + return fmt.Errorf("%w: %v", errClosing, s.writeErr) + } + return nil } // incomingRequest is used to track an incoming request as it is being handled @@ -149,11 +214,16 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde ctx := notDone{bindCtx} c := &Connection{ - closer: rwc, - closeErr: make(chan error, 1), - writer: make(chan Writer, 1), - onDone: onDone, + state: inFlightState{closer: rwc}, + done: make(chan struct{}), + writer: make(chan Writer, 1), + onDone: onDone, } + // It's tempting to set a finalizer on c to verify that the state has gone + // idle when the connection becomes unreachable. Unfortunately, the Binder + // interface makes that unsafe: it allows the Handler to close over the + // Connection, which could create a reference cycle that would cause the + // Connection to become uncollectable. options := binder.Bind(bindCtx, c) framer := options.Framer @@ -170,10 +240,11 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde reader := framer.Reader(rwc) c.updateInFlight(func(s *inFlightState) { - if !s.closed { - // The goroutine started here will continue until the underlying stream is closed. - go c.readIncoming(ctx, reader, options.Preempter) - } + // The goroutine started here will continue until the underlying stream is closed. + // + // (If the Binder closed the Connection already, this should error out and + // return almost immediately.) + go c.readIncoming(ctx, reader, options.Preempter) }) return c } @@ -181,20 +252,48 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde // Notify invokes the target method but does not wait for a response. // The params will be marshaled to JSON before sending over the wire, and will // be handed to the method invoked. -func (c *Connection) Notify(ctx context.Context, method string, params interface{}) error { - notify, err := NewNotification(method, params) - if err != nil { - return fmt.Errorf("marshaling notify parameters: %v", err) - } +func (c *Connection) Notify(ctx context.Context, method string, params interface{}) (err error) { ctx, done := event.Start(ctx, method, tag.Method.Of(method), tag.RPCDirection.Of(tag.Outbound), ) + attempted := false + + defer func() { + labelStatus(ctx, err) + done() + if attempted { + c.updateInFlight(func(s *inFlightState) { + s.outgoingNotifications-- + }) + } + }() + + c.updateInFlight(func(s *inFlightState) { + // If the connection is shutting down, allow outgoing notifications only if + // there is at least one call still in flight. The number of calls in flight + // cannot increase once shutdown begins, and allowing outgoing notifications + // may permit notifications that will cancel in-flight calls. + if len(s.outgoingCalls) == 0 && len(s.incomingByID) == 0 { + err = s.shuttingDown(ErrClientClosing) + if err != nil { + return + } + } + s.outgoingNotifications++ + attempted = true + }) + if err != nil { + return err + } + + notify, err := NewNotification(method, params) + if err != nil { + return fmt.Errorf("marshaling notify parameters: %v", err) + } + event.Metric(ctx, tag.Started.Of(1)) - err = c.write(ctx, notify) - labelStatus(ctx, err) - done() - return err + return c.write(ctx, notify) } // Call invokes the target method and returns an object that can be used to await the response. @@ -228,21 +327,14 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{} } c.updateInFlight(func(s *inFlightState) { - if s.closing { - err = ErrClientClosing - return - } - if s.readErr != nil { - // We must not start a new Call request if the read end of the connection - // has already failed: a Call request requires a response, but with the - // read side broken we have no way to receive that response. - err = fmt.Errorf("%w: %v", ErrClientClosing, s.readErr) + err = s.shuttingDown(ErrClientClosing) + if err != nil { return } - if s.outgoing == nil { - s.outgoing = make(map[ID]*AsyncCall) + if s.outgoingCalls == nil { + s.outgoingCalls = make(map[ID]*AsyncCall) } - s.outgoing[ac.id] = ac + s.outgoingCalls[ac.id] = ac }) if err != nil { ac.retire(&Response{ID: id, Error: err}) @@ -254,8 +346,8 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{} // Sending failed. We will never get a response, so deliver a fake one if it // wasn't already retired by the connection breaking. c.updateInFlight(func(s *inFlightState) { - if s.outgoing[ac.id] == ac { - delete(s.outgoing, ac.id) + if s.outgoingCalls[ac.id] == ac { + delete(s.outgoingCalls, ac.id) ac.retire(&Response{ID: id, Error: err}) } else { // ac was already retired by the readIncoming goroutine: @@ -365,8 +457,11 @@ func (c *Connection) Cancel(id ID) { // Wait blocks until the connection is fully closed, but does not close it. func (c *Connection) Wait() error { - err := <-c.closeErr - c.closeErr <- err + var err error + <-c.done + c.updateInFlight(func(s *inFlightState) { + err = s.closeErr + }) return err } @@ -380,7 +475,7 @@ func (c *Connection) Wait() error { func (c *Connection) Close() error { // Stop handling new requests, and interrupt the reader (by closing the // connection) as soon as the active requests finish. - c.updateInFlight(func(s *inFlightState) { s.closing = true }) + c.updateInFlight(func(s *inFlightState) { s.connClosing = true }) return c.Wait() } @@ -405,8 +500,8 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter case *Response: c.updateInFlight(func(s *inFlightState) { - if ac, ok := s.outgoing[msg.ID]; ok { - delete(s.outgoing, msg.ID) + if ac, ok := s.outgoingCalls[msg.ID]; ok { + delete(s.outgoingCalls, msg.ID) ac.retire(msg) } else { // TODO: How should we report unexpected responses? @@ -423,10 +518,10 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter // Retire any outgoing requests that were still in flight: with the Reader no // longer being processed, they necessarily cannot receive a response. - for id, ac := range s.outgoing { + for id, ac := range s.outgoingCalls { ac.retire(&Response{ID: id, Error: err}) } - s.outgoing = nil + s.outgoingCalls = nil }) } @@ -474,14 +569,11 @@ func (c *Connection) acceptRequest(ctx context.Context, msg *Request, msgBytes i } s.incomingByID[req.ID] = req - if s.closing { - // When closing, reject all new Call requests, even if they could - // theoretically be handled by the preempter. The preempter could return - // ErrAsyncResponse, which would increase the amount of work in flight - // when we're trying to ensure that it strictly decreases. - err = ErrServerClosing - return - } + // When shutting down, reject all new Call requests, even if they could + // theoretically be handled by the preempter. The preempter could return + // ErrAsyncResponse, which would increase the amount of work in flight + // when we're trying to ensure that it strictly decreases. + err = s.shuttingDown(ErrServerClosing) } }) if err != nil { @@ -504,11 +596,12 @@ func (c *Connection) acceptRequest(ctx context.Context, msg *Request, msgBytes i } c.updateInFlight(func(s *inFlightState) { - if s.closing { - // If the connection is closing, don't enqueue anything to the handler — not - // even notifications. That ensures that if the handler continues to make - // progress, it will eventually become idle and close the connection. - err = ErrServerClosing + // If the connection is shutting down, don't enqueue anything to the + // handler — not even notifications. That ensures that if the handler + // continues to make progress, it will eventually become idle and + // close the connection. + err = s.shuttingDown(ErrServerClosing) + if err != nil { return } @@ -557,12 +650,20 @@ func (c *Connection) handleAsync() { return } - var result interface{} - err := req.ctx.Err() - if err == nil { - // Only deliver to the Handler if not already cancelled. - result, err = c.handler.Handle(req.ctx, req.Request) + // Only deliver to the Handler if not already canceled. + if err := req.ctx.Err(); err != nil { + c.updateInFlight(func(s *inFlightState) { + if s.writeErr != nil { + // Assume that req.ctx was canceled due to s.writeErr. + // TODO(#51365): use a Context API to plumb this through req.ctx. + err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr) + } + }) + c.processResult("handleAsync", req, nil, err) + continue } + + result, err := c.handler.Handle(req.ctx, req.Request) c.processResult(c.handler, req, result, err) } } @@ -646,12 +747,24 @@ func (c *Connection) write(ctx context.Context, msg Message) error { n, err := writer.Write(ctx, msg) event.Metric(ctx, tag.SentBytes.Of(n)) - // TODO: if err != nil, that suggests that future writes will not succeed, - // so we cannot possibly write the results of incoming Call requests. - // If the read side of the connection is also broken, we also might not have - // a way to receive cancellation notifications. - // - // Should we cancel the pending calls implicitly? + if err != nil && ctx.Err() == nil { + // The call to Write failed, and since ctx.Err() is nil we can't attribute + // the failure (even indirectly) to Context cancellation. The writer appears + // to be broken, and future writes are likely to also fail. + // + // If the read side of the connection is also broken, we might not even be + // able to receive cancellation notifications. Since we can't reliably write + // the results of incoming calls and can't receive explicit cancellations, + // cancel the calls now. + c.updateInFlight(func(s *inFlightState) { + if s.writeErr == nil { + s.writeErr = err + for _, r := range s.incomingByID { + r.cancel() + } + } + }) + } return err } diff --git a/internal/jsonrpc2_v2/frame.go b/internal/jsonrpc2_v2/frame.go index b2b7dc1a172..e4248328132 100644 --- a/internal/jsonrpc2_v2/frame.go +++ b/internal/jsonrpc2_v2/frame.go @@ -120,6 +120,12 @@ func (r *headerReader) Read(ctx context.Context) (Message, int64, error) { line, err := r.in.ReadString('\n') total += int64(len(line)) if err != nil { + if err == io.EOF { + if total == 0 { + return nil, 0, io.EOF + } + err = io.ErrUnexpectedEOF + } return nil, total, fmt.Errorf("failed reading header line: %w", err) } line = strings.TrimSpace(line) From d5e9e3592c8a808278ce1178637742b6ea320708 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 1 Nov 2022 17:33:33 -0400 Subject: [PATCH 395/723] go/analysis/passes/loopclosure: enable analysis of parallel subtests Remove the internal guard preventing the loopclosure analyzer from checking parallel subtests. Also, improve the accuracy of the parallel subtest check: - only consider statements after the final labeled statement in the subtest body - verify that the *testing.T value for which T.Parallel() is invoked matches the argument to the subtest literal Fixes golang/go#55972 Change-Id: Ia2d9e08dfa88b5e31a9151872025272560d4b5e8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/447256 gopls-CI: kokoro Reviewed-by: Tim King TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley --- go/analysis/passes/loopclosure/loopclosure.go | 96 +++++++++++++++---- .../passes/loopclosure/loopclosure_test.go | 11 +-- .../testdata/src/subtests/subtest.go | 77 +++++++++++++++ gopls/doc/analyzers.md | 7 +- gopls/internal/lsp/source/api_json.go | 4 +- internal/analysisinternal/analysis.go | 4 - internal/loopclosure/main.go | 22 ----- 7 files changed, 164 insertions(+), 57 deletions(-) delete mode 100644 internal/loopclosure/main.go diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 35fe15c9a20..bb0715c02b5 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/analysisinternal" ) const Doc = `check references to loop variables from within nested functions @@ -24,10 +23,11 @@ literal inside the loop body. It checks for patterns where access to a loop variable is known to escape the current loop iteration: 1. a call to go or defer at the end of the loop body 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body + 3. a call testing.T.Run where the subtest body invokes t.Parallel() -The analyzer only considers references in the last statement of the loop body -as it is not deep enough to understand the effects of subsequent statements -which might render the reference benign. +In the case of (1) and (2), the analyzer only considers references in the last +statement of the loop body as it is not deep enough to understand the effects +of subsequent statements which might render the reference benign. For example: @@ -39,10 +39,6 @@ For example: See: https://golang.org/doc/go_faq.html#closures_and_goroutines` -// TODO(rfindley): enable support for checking parallel subtests, pending -// investigation, adding: -// 3. a call testing.T.Run where the subtest body invokes t.Parallel() - var Analyzer = &analysis.Analyzer{ Name: "loopclosure", Doc: Doc, @@ -121,7 +117,7 @@ func run(pass *analysis.Pass) (interface{}, error) { if i == lastStmt { stmts = litStmts(goInvoke(pass.TypesInfo, call)) } - if stmts == nil && analysisinternal.LoopclosureParallelSubtests { + if stmts == nil { stmts = parallelSubtest(pass.TypesInfo, call) } } @@ -178,15 +174,19 @@ func goInvoke(info *types.Info, call *ast.CallExpr) ast.Expr { return call.Args[0] } -// parallelSubtest returns statements that would would be executed -// asynchronously via the go test runner, as t.Run has been invoked with a +// parallelSubtest returns statements that can be easily proven to execute +// concurrently via the go test runner, as t.Run has been invoked with a // function literal that calls t.Parallel. // // In practice, users rely on the fact that statements before the call to // t.Parallel are synchronous. For example by declaring test := test inside the // function literal, but before the call to t.Parallel. // -// Therefore, we only flag references that occur after the call to t.Parallel: +// Therefore, we only flag references in statements that are obviously +// dominated by a call to t.Parallel. As a simple heuristic, we only consider +// statements following the final labeled statement in the function body, to +// avoid scenarios where a jump would cause either the call to t.Parallel or +// the problematic reference to be skipped. // // import "testing" // @@ -210,17 +210,81 @@ func parallelSubtest(info *types.Info, call *ast.CallExpr) []ast.Stmt { return nil } - for i, stmt := range lit.Body.List { + // Capture the *testing.T object for the first argument to the function + // literal. + if len(lit.Type.Params.List[0].Names) == 0 { + return nil + } + + tObj := info.Defs[lit.Type.Params.List[0].Names[0]] + if tObj == nil { + return nil + } + + // Match statements that occur after a call to t.Parallel following the final + // labeled statement in the function body. + // + // We iterate over lit.Body.List to have a simple, fast and "frequent enough" + // dominance relationship for t.Parallel(): lit.Body.List[i] dominates + // lit.Body.List[j] for i < j unless there is a jump. + var stmts []ast.Stmt + afterParallel := false + for _, stmt := range lit.Body.List { + stmt, labeled := unlabel(stmt) + if labeled { + // Reset: naively we don't know if a jump could have caused the + // previously considered statements to be skipped. + stmts = nil + afterParallel = false + } + + if afterParallel { + stmts = append(stmts, stmt) + continue + } + + // Check if stmt is a call to t.Parallel(), for the correct t. exprStmt, ok := stmt.(*ast.ExprStmt) if !ok { continue } - if isMethodCall(info, exprStmt.X, "testing", "T", "Parallel") { - return lit.Body.List[i+1:] + expr := exprStmt.X + if isMethodCall(info, expr, "testing", "T", "Parallel") { + call, _ := expr.(*ast.CallExpr) + if call == nil { + continue + } + x, _ := call.Fun.(*ast.SelectorExpr) + if x == nil { + continue + } + id, _ := x.X.(*ast.Ident) + if id == nil { + continue + } + if info.Uses[id] == tObj { + afterParallel = true + } } } - return nil + return stmts +} + +// unlabel returns the inner statement for the possibly labeled statement stmt, +// stripping any (possibly nested) *ast.LabeledStmt wrapper. +// +// The second result reports whether stmt was an *ast.LabeledStmt. +func unlabel(stmt ast.Stmt) (ast.Stmt, bool) { + labeled := false + for { + labelStmt, ok := stmt.(*ast.LabeledStmt) + if !ok { + return stmt, labeled + } + labeled = true + stmt = labelStmt.Stmt + } } // isMethodCall reports whether expr is a method call of diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go index 8bf1d7a7bcc..55fb2a4a3d6 100644 --- a/go/analysis/passes/loopclosure/loopclosure_test.go +++ b/go/analysis/passes/loopclosure/loopclosure_test.go @@ -9,23 +9,14 @@ import ( "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/loopclosure" - "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/typeparams" ) func Test(t *testing.T) { testdata := analysistest.TestData() - tests := []string{"a", "golang.org/..."} + tests := []string{"a", "golang.org/...", "subtests"} if typeparams.Enabled { tests = append(tests, "typeparams") } analysistest.Run(t, testdata, loopclosure.Analyzer, tests...) - - // Enable checking of parallel subtests. - defer func(parallelSubtest bool) { - analysisinternal.LoopclosureParallelSubtests = parallelSubtest - }(analysisinternal.LoopclosureParallelSubtests) - analysisinternal.LoopclosureParallelSubtests = true - - analysistest.Run(t, testdata, loopclosure.Analyzer, "subtests") } diff --git a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go index 2a97244a1a5..c95fa1f0b1e 100644 --- a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go +++ b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go @@ -47,6 +47,14 @@ func _(t *testing.T) { println(test) // want "loop variable test captured by func literal" }) + // Check that *testing.T value matters. + t.Run("", func(t *testing.T) { + var x testing.T + x.Parallel() + println(i) + println(test) + }) + // Check that shadowing the loop variables within the test literal is OK if // it occurs before t.Parallel(). t.Run("", func(t *testing.T) { @@ -92,6 +100,46 @@ func _(t *testing.T) { println(i) println(test) }) + + // Check that there is no diagnostic when a jump to a label may have caused + // the call to t.Parallel to have been skipped. + t.Run("", func(t *testing.T) { + if true { + goto Test + } + t.Parallel() + Test: + println(i) + println(test) + }) + + // Check that there is no diagnostic when a jump to a label may have caused + // the loop variable reference to be skipped, but there is a diagnostic + // when both the call to t.Parallel and the loop variable reference occur + // after the final label in the block. + t.Run("", func(t *testing.T) { + if true { + goto Test + } + t.Parallel() + println(i) // maybe OK + Test: + t.Parallel() + println(test) // want "loop variable test captured by func literal" + }) + + // Check that multiple labels are handled. + t.Run("", func(t *testing.T) { + if true { + goto Test1 + } else { + goto Test2 + } + Test1: + Test2: + t.Parallel() + println(test) // want "loop variable test captured by func literal" + }) } } @@ -119,3 +167,32 @@ func _(t *T) { }) } } + +// Check that the top-level must be parallel in order to cause a diagnostic. +// +// From https://pkg.go.dev/testing: +// +// "Run does not return until parallel subtests have completed, providing a +// way to clean up after a group of parallel tests" +func _(t *testing.T) { + for _, test := range []int{1, 2, 3} { + // In this subtest, a/b must complete before the synchronous subtest "a" + // completes, so the reference to test does not escape the current loop + // iteration. + t.Run("a", func(s *testing.T) { + s.Run("b", func(u *testing.T) { + u.Parallel() + println(test) + }) + }) + + // In this subtest, c executes concurrently, so the reference to test may + // escape the current loop iteration. + t.Run("c", func(s *testing.T) { + s.Parallel() + s.Run("d", func(u *testing.T) { + println(test) // want "loop variable test captured by func literal" + }) + }) + } +} diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index dfe2a43c2b9..176c32f1ba4 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -223,10 +223,11 @@ literal inside the loop body. It checks for patterns where access to a loop variable is known to escape the current loop iteration: 1. a call to go or defer at the end of the loop body 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body + 3. a call testing.T.Run where the subtest body invokes t.Parallel() -The analyzer only considers references in the last statement of the loop body -as it is not deep enough to understand the effects of subsequent statements -which might render the reference benign. +In the case of (1) and (2), the analyzer only considers references in the last +statement of the loop body as it is not deep enough to understand the effects +of subsequent statements which might render the reference benign. For example: diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 2b7362cb124..762054d10b8 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -307,7 +307,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "\"loopclosure\"", - Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n\nThe analyzer only considers references in the last statement of the loop body\nas it is not deep enough to understand the effects of subsequent statements\nwhich might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", + Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n 3. a call testing.T.Run where the subtest body invokes t.Parallel()\n\nIn the case of (1) and (2), the analyzer only considers references in the last\nstatement of the loop body as it is not deep enough to understand the effects\nof subsequent statements which might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", Default: "true", }, { @@ -933,7 +933,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "loopclosure", - Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n\nThe analyzer only considers references in the last statement of the loop body\nas it is not deep enough to understand the effects of subsequent statements\nwhich might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", + Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n 3. a call testing.T.Run where the subtest body invokes t.Parallel()\n\nIn the case of (1) and (2), the analyzer only considers references in the last\nstatement of the loop body as it is not deep enough to understand the effects\nof subsequent statements which might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", Default: true, }, { diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index 3b983ccf7d8..6fceef5e720 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -18,10 +18,6 @@ import ( // in Go 1.18+. var DiagnoseFuzzTests bool = false -// LoopclosureParallelSubtests controls whether the 'loopclosure' analyzer -// diagnoses loop variables references in parallel subtests. -var LoopclosureParallelSubtests = false - var ( GetTypeErrors func(p interface{}) []types.Error SetTypeErrors func(p interface{}, errors []types.Error) diff --git a/internal/loopclosure/main.go b/internal/loopclosure/main.go deleted file mode 100644 index 03238edae13..00000000000 --- a/internal/loopclosure/main.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// The loopclosure command applies the golang.org/x/tools/go/analysis/passes/loopclosure -// analysis to the specified packages of Go source code. It enables -// experimental checking of parallel subtests. -// -// TODO: Once the parallel subtest experiment is complete, this can be made -// public at go/analysis/passes/loopclosure/cmd, or deleted. -package main - -import ( - "golang.org/x/tools/go/analysis/passes/loopclosure" - "golang.org/x/tools/go/analysis/singlechecker" - "golang.org/x/tools/internal/analysisinternal" -) - -func main() { - analysisinternal.LoopclosureParallelSubtests = true - singlechecker.Main(loopclosure.Analyzer) -} From e5f03c104122a9f4aa32ab19847290987c22ab25 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 1 Nov 2022 11:11:16 -0400 Subject: [PATCH 396/723] gopls/doc: clean up README and add a release policy Clean up some broken links and stale documentation in gopls/README.md, and add new documentation for the gopls release policy. Fixes golang/go#55267 Change-Id: I9c7ed1f1d3949025f3c02edb69b475cf34f214eb Reviewed-on: https://go-review.googlesource.com/c/tools/+/446863 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Reviewed-by: Hyang-Ah Hana Kim Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/README.md | 62 ++++++++++++++++++++++++++----------------- gopls/doc/releases.md | 25 +++++++++++++++++ 2 files changed, 62 insertions(+), 25 deletions(-) create mode 100644 gopls/doc/releases.md diff --git a/gopls/README.md b/gopls/README.md index 9692a1d0866..aefad905144 100644 --- a/gopls/README.md +++ b/gopls/README.md @@ -5,56 +5,56 @@ `gopls` (pronounced "Go please") is the official Go [language server] developed by the Go team. It provides IDE features to any [LSP]-compatible editor. - + You should not need to interact with `gopls` directly--it will be automatically integrated into your editor. The specific features and settings vary slightly -by editor, so we recommend that you proceed to the [documentation for your -editor](#editors) below. +by editor, so we recommend that you proceed to the +[documentation for your editor](#editors) below. ## Editors To get started with `gopls`, install an LSP plugin in your editor of choice. -* [VSCode](https://github.com/golang/vscode-go/blob/master/README.md) +* [VS Code](https://github.com/golang/vscode-go/blob/master/README.md) * [Vim / Neovim](doc/vim.md) * [Emacs](doc/emacs.md) * [Atom](https://github.com/MordFustang21/ide-gopls) * [Sublime Text](doc/subl.md) * [Acme](https://github.com/fhs/acme-lsp) -If you use `gopls` with an editor that is not on this list, please let us know -by [filing an issue](#new-issue) or [modifying this documentation](doc/contributing.md). +If you use `gopls` with an editor that is not on this list, please send us a CL +[updating this documentation](doc/contributing.md). ## Installation For the most part, you should not need to install or update `gopls`. Your editor should handle that step for you. -If you do want to get the latest stable version of `gopls`, change to any -directory that is both outside of your `GOPATH` and outside of a module (a temp -directory is fine), and run: +If you do want to get the latest stable version of `gopls`, run the following +command: ```sh go install golang.org/x/tools/gopls@latest ``` -Learn more in the [advanced installation -instructions](doc/advanced.md#installing-unreleased-versions). +Learn more in the +[advanced installation instructions](doc/advanced.md#installing-unreleased-versions). + +Learn more about gopls releases in the [release policy](doc/releases.md). ## Setting up your workspace -`gopls` supports both Go module and GOPATH modes, but if you are working with -multiple modules or uncommon project layouts, you will need to specifically -configure your workspace. See the [Workspace document](doc/workspace.md) for -information on supported workspace layouts. +`gopls` supports both Go module, multi-module and GOPATH modes. See the +[workspace documentation](doc/workspace.md) for information on supported +workspace layouts. ## Configuration You can configure `gopls` to change your editor experience or view additional debugging information. Configuration options will be made available by your editor, so see your [editor's instructions](#editors) for specific details. A -full list of `gopls` settings can be found in the [Settings documentation](doc/settings.md). +full list of `gopls` settings can be found in the [settings documentation](doc/settings.md). ### Environment variables @@ -62,12 +62,16 @@ full list of `gopls` settings can be found in the [Settings documentation](doc/s variables you configure. Some editors, such as VS Code, allow users to selectively override the values of some environment variables. -## Troubleshooting +## Support Policy -If you are having issues with `gopls`, please follow the steps described in the -[troubleshooting guide](doc/troubleshooting.md). +Gopls is maintained by engineers on the +[Go tools team](https://github.com/orgs/golang/teams/tools-team/members), +who actively monitor the +[Go](https://github.com/golang/go/issues?q=is%3Aissue+is%3Aopen+label%3Agopls) +and +[VS Code Go](https://github.com/golang/vscode-go/issues) issue trackers. -## Supported Go versions and build systems +### Supported Go versions `gopls` follows the [Go Release Policy](https://golang.org/doc/devel/release.html#policy), @@ -95,13 +99,22 @@ test failures may be skipped rather than fixed. Furthermore, if a regression in an older Go version causes irreconcilable CI failures, we may drop support for that Go version in CI if it is 3 or 4 Go versions old. -`gopls` currently only supports the `go` command, so if you are using a -different build system, `gopls` will not work well. Bazel is not officially -supported, but Bazel support is in development (see -[bazelbuild/rules_go#512](https://github.com/bazelbuild/rules_go/issues/512)). +### Supported build systems + +`gopls` currently only supports the `go` command, so if you are using +a different build system, `gopls` will not work well. Bazel is not officially +supported, but may be made to work with an appropriately configured +`go/packages` driver. See +[bazelbuild/rules_go#512](https://github.com/bazelbuild/rules_go/issues/512) +for more information. You can follow [these instructions](https://github.com/bazelbuild/rules_go/wiki/Editor-setup) to configure your `gopls` to work with Bazel. +### Troubleshooting + +If you are having issues with `gopls`, please follow the steps described in the +[troubleshooting guide](doc/troubleshooting.md). + ## Additional information * [Features](doc/features.md) @@ -115,4 +128,3 @@ to configure your `gopls` to work with Bazel. [language server]: https://langserver.org [LSP]: https://microsoft.github.io/language-server-protocol/ -[Gophers Slack]: https://gophers.slack.com/ diff --git a/gopls/doc/releases.md b/gopls/doc/releases.md new file mode 100644 index 00000000000..befb92c3966 --- /dev/null +++ b/gopls/doc/releases.md @@ -0,0 +1,25 @@ +# Gopls release policy + +Gopls releases follow [semver](http://semver.org), with major changes and new +features introduced only in new minor versions (i.e. versions of the form +`v*.N.0` for some N). Subsequent patch releases contain only cherry-picked +fixes or superficial updates. + +In order to align with the +[Go release timeline](https://github.com/golang/go/wiki/Go-Release-Cycle#timeline), +we aim to release a new minor version of Gopls approximately every three +months, with patch releases approximately every month, according to the +following table: + +| Month | Version(s) | +| ---- | ------- | +| Jan | `v*..0` | +| Jan-Mar | `v*..*` | +| Apr | `v*..0` | +| Apr-Jun | `v*..*` | +| Jul | `v*..0` | +| Jul-Sep | `v*..*` | +| Oct | `v*..0` | +| Oct-Dec | `v*..*` | + +For more background on this policy, see https://go.dev/issue/55267. From 4ada35e5cb3b9c54d4cc21a8d026e09ae7869f65 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 25 Oct 2022 21:23:07 -0400 Subject: [PATCH 397/723] gopls/internal/lsp: handle modVulncheckSource in diagnosticSource.String Change-Id: If6ebdfa2db3de8915842cf09da279d8ea7fa9b97 Reviewed-on: https://go-review.googlesource.com/c/tools/+/447735 gopls-CI: kokoro Reviewed-by: Suzy Mueller Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot --- gopls/internal/lsp/diagnostics.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 603acca607a..a01898b26a4 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -97,6 +97,8 @@ func (d diagnosticSource) String() string { return "FromGoWork" case modCheckUpgradesSource: return "FromCheckForUpgrades" + case modVulncheckSource: + return "FromModVulncheck" default: return fmt.Sprintf("From?%d?", d) } From a77a1fb99589316992433124a2942480b334a549 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 3 Nov 2022 14:30:08 -0400 Subject: [PATCH 398/723] gopls/internal/lsp/mod: fix vulncheck hover message This fixes multi-paragraph vulnerability description rendering. Change-Id: I2960c5f3a839fb4161ae5e25d3e88b5a7345b65d Reviewed-on: https://go-review.googlesource.com/c/tools/+/447736 gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller --- gopls/internal/lsp/mod/diagnostics.go | 3 +-- gopls/internal/lsp/mod/hover.go | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 6e0067c5ef3..770fb3729f7 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -7,7 +7,6 @@ package mod import ( - "bytes" "context" "fmt" "sort" @@ -306,7 +305,7 @@ func formatMessage(v govulncheck.Vuln) string { details[i] = ' ' } } - return fmt.Sprintf("%s has a known vulnerability: %s", v.ModPath, string(bytes.TrimSpace(details))) + return strings.TrimSpace(strings.Replace(string(details), "\n\n", "\n\n ", -1)) } // href returns a URL embedded in the entry if any. diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index b1dfe3a3fb5..5d5b6158212 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -169,13 +169,13 @@ func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options * } if useMarkdown { - fmt.Fprintf(&b, " - [**%v**](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) + fmt.Fprintf(&b, "- [**%v**](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) } else { fmt.Fprintf(&b, " - [%v] %v (%v) %v\n", v.OSV.ID, formatMessage(v), href(v.OSV), fix) } } if len(nonaffecting) > 0 { - fmt.Fprintf(&b, "The project imports packages affected by the following vulnerabilities, but does not use vulnerable symbols.") + fmt.Fprintf(&b, "\n**FYI:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n") } for _, v := range nonaffecting { fix := "No fix is available." @@ -183,7 +183,7 @@ func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options * fix = "Fixed in " + v.FixedIn + "." } if useMarkdown { - fmt.Fprintf(&b, " - [%v](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) + fmt.Fprintf(&b, "- [%v](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) } else { fmt.Fprintf(&b, " - [%v] %v %v (%v)\n", v.OSV.ID, formatMessage(v), fix, href(v.OSV)) } From affa6031324486cd6fbf188952d18bfd42034a3d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 4 Nov 2022 11:09:44 -0400 Subject: [PATCH 399/723] internal/gcimporter: moved from go/internal/gcimporter We plan to add experimental features to this package for use by gopls, but the directory structure makes this tricky using the "internal directory" mechanism. Change-Id: Ib842c0b100b167f6978c6ff783ea0e5d0704b4a7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/447955 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan gopls-CI: kokoro --- go/gcexportdata/gcexportdata.go | 2 +- {go/internal => internal}/gcimporter/bexport.go | 0 .../gcimporter/bexport_test.go | 2 +- {go/internal => internal}/gcimporter/bimport.go | 0 {go/internal => internal}/gcimporter/exportdata.go | 0 {go/internal => internal}/gcimporter/gcimporter.go | 2 +- .../gcimporter/gcimporter_test.go | 0 {go/internal => internal}/gcimporter/iexport.go | 0 .../gcimporter/iexport_common_test.go | 0 .../gcimporter/iexport_go118_test.go | 2 +- .../gcimporter/iexport_test.go | 2 +- {go/internal => internal}/gcimporter/iimport.go | 0 {go/internal => internal}/gcimporter/israce_test.go | 0 .../gcimporter/newInterface10.go | 0 .../gcimporter/newInterface11.go | 0 {go/internal => internal}/gcimporter/stdlib_test.go | 0 .../gcimporter/support_go117.go | 0 .../gcimporter/support_go118.go | 0 {go/internal => internal}/gcimporter/testdata/a.go | 0 {go/internal => internal}/gcimporter/testdata/b.go | 0 .../gcimporter/testdata/exports.go | 0 .../gcimporter/testdata/issue15920.go | 0 .../gcimporter/testdata/issue20046.go | 0 .../gcimporter/testdata/issue25301.go | 0 .../gcimporter/testdata/issue51836/a.go | 0 .../gcimporter/testdata/issue51836/aa.go | 0 {go/internal => internal}/gcimporter/testdata/p.go | 0 .../gcimporter/testdata/versions/test.go | 0 .../gcimporter/testdata/versions/test_go1.11_0i.a | Bin .../gcimporter/testdata/versions/test_go1.11_6b.a | Bin .../gcimporter/testdata/versions/test_go1.11_999b.a | Bin .../gcimporter/testdata/versions/test_go1.11_999i.a | Bin .../gcimporter/testdata/versions/test_go1.7_0.a | Bin .../gcimporter/testdata/versions/test_go1.7_1.a | Bin .../gcimporter/testdata/versions/test_go1.8_4.a | Bin .../gcimporter/testdata/versions/test_go1.8_5.a | Bin {go/internal => internal}/gcimporter/unified_no.go | 0 {go/internal => internal}/gcimporter/unified_yes.go | 0 {go/internal => internal}/gcimporter/ureader_no.go | 0 {go/internal => internal}/gcimporter/ureader_yes.go | 2 +- {go/internal => internal}/pkgbits/codes.go | 0 {go/internal => internal}/pkgbits/decoder.go | 0 {go/internal => internal}/pkgbits/doc.go | 0 {go/internal => internal}/pkgbits/encoder.go | 0 {go/internal => internal}/pkgbits/flags.go | 0 {go/internal => internal}/pkgbits/frames_go1.go | 0 {go/internal => internal}/pkgbits/frames_go17.go | 0 {go/internal => internal}/pkgbits/reloc.go | 0 {go/internal => internal}/pkgbits/support.go | 0 {go/internal => internal}/pkgbits/sync.go | 0 .../pkgbits/syncmarker_string.go | 0 51 files changed, 6 insertions(+), 6 deletions(-) rename {go/internal => internal}/gcimporter/bexport.go (100%) rename {go/internal => internal}/gcimporter/bexport_test.go (99%) rename {go/internal => internal}/gcimporter/bimport.go (100%) rename {go/internal => internal}/gcimporter/exportdata.go (100%) rename {go/internal => internal}/gcimporter/gcimporter.go (99%) rename {go/internal => internal}/gcimporter/gcimporter_test.go (100%) rename {go/internal => internal}/gcimporter/iexport.go (100%) rename {go/internal => internal}/gcimporter/iexport_common_test.go (100%) rename {go/internal => internal}/gcimporter/iexport_go118_test.go (99%) rename {go/internal => internal}/gcimporter/iexport_test.go (99%) rename {go/internal => internal}/gcimporter/iimport.go (100%) rename {go/internal => internal}/gcimporter/israce_test.go (100%) rename {go/internal => internal}/gcimporter/newInterface10.go (100%) rename {go/internal => internal}/gcimporter/newInterface11.go (100%) rename {go/internal => internal}/gcimporter/stdlib_test.go (100%) rename {go/internal => internal}/gcimporter/support_go117.go (100%) rename {go/internal => internal}/gcimporter/support_go118.go (100%) rename {go/internal => internal}/gcimporter/testdata/a.go (100%) rename {go/internal => internal}/gcimporter/testdata/b.go (100%) rename {go/internal => internal}/gcimporter/testdata/exports.go (100%) rename {go/internal => internal}/gcimporter/testdata/issue15920.go (100%) rename {go/internal => internal}/gcimporter/testdata/issue20046.go (100%) rename {go/internal => internal}/gcimporter/testdata/issue25301.go (100%) rename {go/internal => internal}/gcimporter/testdata/issue51836/a.go (100%) rename {go/internal => internal}/gcimporter/testdata/issue51836/aa.go (100%) rename {go/internal => internal}/gcimporter/testdata/p.go (100%) rename {go/internal => internal}/gcimporter/testdata/versions/test.go (100%) rename {go/internal => internal}/gcimporter/testdata/versions/test_go1.11_0i.a (100%) rename {go/internal => internal}/gcimporter/testdata/versions/test_go1.11_6b.a (100%) rename {go/internal => internal}/gcimporter/testdata/versions/test_go1.11_999b.a (100%) rename {go/internal => internal}/gcimporter/testdata/versions/test_go1.11_999i.a (100%) rename {go/internal => internal}/gcimporter/testdata/versions/test_go1.7_0.a (100%) rename {go/internal => internal}/gcimporter/testdata/versions/test_go1.7_1.a (100%) rename {go/internal => internal}/gcimporter/testdata/versions/test_go1.8_4.a (100%) rename {go/internal => internal}/gcimporter/testdata/versions/test_go1.8_5.a (100%) rename {go/internal => internal}/gcimporter/unified_no.go (100%) rename {go/internal => internal}/gcimporter/unified_yes.go (100%) rename {go/internal => internal}/gcimporter/ureader_no.go (100%) rename {go/internal => internal}/gcimporter/ureader_yes.go (99%) rename {go/internal => internal}/pkgbits/codes.go (100%) rename {go/internal => internal}/pkgbits/decoder.go (100%) rename {go/internal => internal}/pkgbits/doc.go (100%) rename {go/internal => internal}/pkgbits/encoder.go (100%) rename {go/internal => internal}/pkgbits/flags.go (100%) rename {go/internal => internal}/pkgbits/frames_go1.go (100%) rename {go/internal => internal}/pkgbits/frames_go17.go (100%) rename {go/internal => internal}/pkgbits/reloc.go (100%) rename {go/internal => internal}/pkgbits/support.go (100%) rename {go/internal => internal}/pkgbits/sync.go (100%) rename {go/internal => internal}/pkgbits/syncmarker_string.go (100%) diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go index 42adb8f697b..620446207e2 100644 --- a/go/gcexportdata/gcexportdata.go +++ b/go/gcexportdata/gcexportdata.go @@ -30,7 +30,7 @@ import ( "io/ioutil" "os/exec" - "golang.org/x/tools/go/internal/gcimporter" + "golang.org/x/tools/internal/gcimporter" ) // Find returns the name of an object (.o) or archive (.a) file diff --git a/go/internal/gcimporter/bexport.go b/internal/gcimporter/bexport.go similarity index 100% rename from go/internal/gcimporter/bexport.go rename to internal/gcimporter/bexport.go diff --git a/go/internal/gcimporter/bexport_test.go b/internal/gcimporter/bexport_test.go similarity index 99% rename from go/internal/gcimporter/bexport_test.go rename to internal/gcimporter/bexport_test.go index 3da5397eb50..93ee3cd21a0 100644 --- a/go/internal/gcimporter/bexport_test.go +++ b/internal/gcimporter/bexport_test.go @@ -21,8 +21,8 @@ import ( "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/buildutil" - "golang.org/x/tools/go/internal/gcimporter" "golang.org/x/tools/go/loader" + "golang.org/x/tools/internal/gcimporter" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typeparams/genericfeatures" ) diff --git a/go/internal/gcimporter/bimport.go b/internal/gcimporter/bimport.go similarity index 100% rename from go/internal/gcimporter/bimport.go rename to internal/gcimporter/bimport.go diff --git a/go/internal/gcimporter/exportdata.go b/internal/gcimporter/exportdata.go similarity index 100% rename from go/internal/gcimporter/exportdata.go rename to internal/gcimporter/exportdata.go diff --git a/go/internal/gcimporter/gcimporter.go b/internal/gcimporter/gcimporter.go similarity index 99% rename from go/internal/gcimporter/gcimporter.go rename to internal/gcimporter/gcimporter.go index 85a801c6a3d..f8369cdc52e 100644 --- a/go/internal/gcimporter/gcimporter.go +++ b/internal/gcimporter/gcimporter.go @@ -9,7 +9,7 @@ // Package gcimporter provides various functions for reading // gc-generated object files that can be used to implement the // Importer interface defined by the Go 1.5 standard library package. -package gcimporter // import "golang.org/x/tools/go/internal/gcimporter" +package gcimporter // import "golang.org/x/tools/internal/gcimporter" import ( "bufio" diff --git a/go/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go similarity index 100% rename from go/internal/gcimporter/gcimporter_test.go rename to internal/gcimporter/gcimporter_test.go diff --git a/go/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go similarity index 100% rename from go/internal/gcimporter/iexport.go rename to internal/gcimporter/iexport.go diff --git a/go/internal/gcimporter/iexport_common_test.go b/internal/gcimporter/iexport_common_test.go similarity index 100% rename from go/internal/gcimporter/iexport_common_test.go rename to internal/gcimporter/iexport_common_test.go diff --git a/go/internal/gcimporter/iexport_go118_test.go b/internal/gcimporter/iexport_go118_test.go similarity index 99% rename from go/internal/gcimporter/iexport_go118_test.go rename to internal/gcimporter/iexport_go118_test.go index 5dfa2580f6b..27ba8cec5ac 100644 --- a/go/internal/gcimporter/iexport_go118_test.go +++ b/internal/gcimporter/iexport_go118_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - "golang.org/x/tools/go/internal/gcimporter" + "golang.org/x/tools/internal/gcimporter" ) // TODO(rfindley): migrate this to testdata, as has been done in the standard library. diff --git a/go/internal/gcimporter/iexport_test.go b/internal/gcimporter/iexport_test.go similarity index 99% rename from go/internal/gcimporter/iexport_test.go rename to internal/gcimporter/iexport_test.go index 899c9af7a48..702528aef3b 100644 --- a/go/internal/gcimporter/iexport_test.go +++ b/internal/gcimporter/iexport_test.go @@ -31,8 +31,8 @@ import ( "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/gcexportdata" - "golang.org/x/tools/go/internal/gcimporter" "golang.org/x/tools/go/loader" + "golang.org/x/tools/internal/gcimporter" "golang.org/x/tools/internal/typeparams/genericfeatures" ) diff --git a/go/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go similarity index 100% rename from go/internal/gcimporter/iimport.go rename to internal/gcimporter/iimport.go diff --git a/go/internal/gcimporter/israce_test.go b/internal/gcimporter/israce_test.go similarity index 100% rename from go/internal/gcimporter/israce_test.go rename to internal/gcimporter/israce_test.go diff --git a/go/internal/gcimporter/newInterface10.go b/internal/gcimporter/newInterface10.go similarity index 100% rename from go/internal/gcimporter/newInterface10.go rename to internal/gcimporter/newInterface10.go diff --git a/go/internal/gcimporter/newInterface11.go b/internal/gcimporter/newInterface11.go similarity index 100% rename from go/internal/gcimporter/newInterface11.go rename to internal/gcimporter/newInterface11.go diff --git a/go/internal/gcimporter/stdlib_test.go b/internal/gcimporter/stdlib_test.go similarity index 100% rename from go/internal/gcimporter/stdlib_test.go rename to internal/gcimporter/stdlib_test.go diff --git a/go/internal/gcimporter/support_go117.go b/internal/gcimporter/support_go117.go similarity index 100% rename from go/internal/gcimporter/support_go117.go rename to internal/gcimporter/support_go117.go diff --git a/go/internal/gcimporter/support_go118.go b/internal/gcimporter/support_go118.go similarity index 100% rename from go/internal/gcimporter/support_go118.go rename to internal/gcimporter/support_go118.go diff --git a/go/internal/gcimporter/testdata/a.go b/internal/gcimporter/testdata/a.go similarity index 100% rename from go/internal/gcimporter/testdata/a.go rename to internal/gcimporter/testdata/a.go diff --git a/go/internal/gcimporter/testdata/b.go b/internal/gcimporter/testdata/b.go similarity index 100% rename from go/internal/gcimporter/testdata/b.go rename to internal/gcimporter/testdata/b.go diff --git a/go/internal/gcimporter/testdata/exports.go b/internal/gcimporter/testdata/exports.go similarity index 100% rename from go/internal/gcimporter/testdata/exports.go rename to internal/gcimporter/testdata/exports.go diff --git a/go/internal/gcimporter/testdata/issue15920.go b/internal/gcimporter/testdata/issue15920.go similarity index 100% rename from go/internal/gcimporter/testdata/issue15920.go rename to internal/gcimporter/testdata/issue15920.go diff --git a/go/internal/gcimporter/testdata/issue20046.go b/internal/gcimporter/testdata/issue20046.go similarity index 100% rename from go/internal/gcimporter/testdata/issue20046.go rename to internal/gcimporter/testdata/issue20046.go diff --git a/go/internal/gcimporter/testdata/issue25301.go b/internal/gcimporter/testdata/issue25301.go similarity index 100% rename from go/internal/gcimporter/testdata/issue25301.go rename to internal/gcimporter/testdata/issue25301.go diff --git a/go/internal/gcimporter/testdata/issue51836/a.go b/internal/gcimporter/testdata/issue51836/a.go similarity index 100% rename from go/internal/gcimporter/testdata/issue51836/a.go rename to internal/gcimporter/testdata/issue51836/a.go diff --git a/go/internal/gcimporter/testdata/issue51836/aa.go b/internal/gcimporter/testdata/issue51836/aa.go similarity index 100% rename from go/internal/gcimporter/testdata/issue51836/aa.go rename to internal/gcimporter/testdata/issue51836/aa.go diff --git a/go/internal/gcimporter/testdata/p.go b/internal/gcimporter/testdata/p.go similarity index 100% rename from go/internal/gcimporter/testdata/p.go rename to internal/gcimporter/testdata/p.go diff --git a/go/internal/gcimporter/testdata/versions/test.go b/internal/gcimporter/testdata/versions/test.go similarity index 100% rename from go/internal/gcimporter/testdata/versions/test.go rename to internal/gcimporter/testdata/versions/test.go diff --git a/go/internal/gcimporter/testdata/versions/test_go1.11_0i.a b/internal/gcimporter/testdata/versions/test_go1.11_0i.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.11_0i.a rename to internal/gcimporter/testdata/versions/test_go1.11_0i.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.11_6b.a b/internal/gcimporter/testdata/versions/test_go1.11_6b.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.11_6b.a rename to internal/gcimporter/testdata/versions/test_go1.11_6b.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.11_999b.a b/internal/gcimporter/testdata/versions/test_go1.11_999b.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.11_999b.a rename to internal/gcimporter/testdata/versions/test_go1.11_999b.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.11_999i.a b/internal/gcimporter/testdata/versions/test_go1.11_999i.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.11_999i.a rename to internal/gcimporter/testdata/versions/test_go1.11_999i.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.7_0.a b/internal/gcimporter/testdata/versions/test_go1.7_0.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.7_0.a rename to internal/gcimporter/testdata/versions/test_go1.7_0.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.7_1.a b/internal/gcimporter/testdata/versions/test_go1.7_1.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.7_1.a rename to internal/gcimporter/testdata/versions/test_go1.7_1.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.8_4.a b/internal/gcimporter/testdata/versions/test_go1.8_4.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.8_4.a rename to internal/gcimporter/testdata/versions/test_go1.8_4.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.8_5.a b/internal/gcimporter/testdata/versions/test_go1.8_5.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.8_5.a rename to internal/gcimporter/testdata/versions/test_go1.8_5.a diff --git a/go/internal/gcimporter/unified_no.go b/internal/gcimporter/unified_no.go similarity index 100% rename from go/internal/gcimporter/unified_no.go rename to internal/gcimporter/unified_no.go diff --git a/go/internal/gcimporter/unified_yes.go b/internal/gcimporter/unified_yes.go similarity index 100% rename from go/internal/gcimporter/unified_yes.go rename to internal/gcimporter/unified_yes.go diff --git a/go/internal/gcimporter/ureader_no.go b/internal/gcimporter/ureader_no.go similarity index 100% rename from go/internal/gcimporter/ureader_no.go rename to internal/gcimporter/ureader_no.go diff --git a/go/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go similarity index 99% rename from go/internal/gcimporter/ureader_yes.go rename to internal/gcimporter/ureader_yes.go index e8dff0d8537..e09053bd37a 100644 --- a/go/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -14,7 +14,7 @@ import ( "go/types" "strings" - "golang.org/x/tools/go/internal/pkgbits" + "golang.org/x/tools/internal/pkgbits" ) // A pkgReader holds the shared state for reading a unified IR package diff --git a/go/internal/pkgbits/codes.go b/internal/pkgbits/codes.go similarity index 100% rename from go/internal/pkgbits/codes.go rename to internal/pkgbits/codes.go diff --git a/go/internal/pkgbits/decoder.go b/internal/pkgbits/decoder.go similarity index 100% rename from go/internal/pkgbits/decoder.go rename to internal/pkgbits/decoder.go diff --git a/go/internal/pkgbits/doc.go b/internal/pkgbits/doc.go similarity index 100% rename from go/internal/pkgbits/doc.go rename to internal/pkgbits/doc.go diff --git a/go/internal/pkgbits/encoder.go b/internal/pkgbits/encoder.go similarity index 100% rename from go/internal/pkgbits/encoder.go rename to internal/pkgbits/encoder.go diff --git a/go/internal/pkgbits/flags.go b/internal/pkgbits/flags.go similarity index 100% rename from go/internal/pkgbits/flags.go rename to internal/pkgbits/flags.go diff --git a/go/internal/pkgbits/frames_go1.go b/internal/pkgbits/frames_go1.go similarity index 100% rename from go/internal/pkgbits/frames_go1.go rename to internal/pkgbits/frames_go1.go diff --git a/go/internal/pkgbits/frames_go17.go b/internal/pkgbits/frames_go17.go similarity index 100% rename from go/internal/pkgbits/frames_go17.go rename to internal/pkgbits/frames_go17.go diff --git a/go/internal/pkgbits/reloc.go b/internal/pkgbits/reloc.go similarity index 100% rename from go/internal/pkgbits/reloc.go rename to internal/pkgbits/reloc.go diff --git a/go/internal/pkgbits/support.go b/internal/pkgbits/support.go similarity index 100% rename from go/internal/pkgbits/support.go rename to internal/pkgbits/support.go diff --git a/go/internal/pkgbits/sync.go b/internal/pkgbits/sync.go similarity index 100% rename from go/internal/pkgbits/sync.go rename to internal/pkgbits/sync.go diff --git a/go/internal/pkgbits/syncmarker_string.go b/internal/pkgbits/syncmarker_string.go similarity index 100% rename from go/internal/pkgbits/syncmarker_string.go rename to internal/pkgbits/syncmarker_string.go From 2b29c66d7e18b0e4d40a64b74129f7abfb509773 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 3 Nov 2022 14:55:29 -0400 Subject: [PATCH 400/723] internal/gcimporter: API for shallow export data This change adds an internal API for marshalling and unmarshalling a types.Package to "shallow" export data, which does not index packages other than the main one. The import function accepts a function that loads symbols on demand (e.g. by recursively reading export data for indirect dependencies). The CL includes a test that the entire standard library can be type-checked using shallow data. Also: - break dependency on go/ast. - narrow the name and type of qualifiedObject. - add (test) dependency on errgroup, and tidy go.mod. Change-Id: I92d31efd343cf5dd6fca6d7b918a23749e2d1e83 Reviewed-on: https://go-review.googlesource.com/c/tools/+/447737 Run-TryBot: Alan Donovan Reviewed-by: Matthew Dempsky TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- go.mod | 2 + go.sum | 2 + gopls/go.mod | 2 +- gopls/go.sum | 3 +- internal/gcimporter/bexport.go | 9 +- internal/gcimporter/bexport_test.go | 2 +- internal/gcimporter/iexport.go | 61 +++++++++-- internal/gcimporter/iexport_test.go | 5 +- internal/gcimporter/iimport.go | 26 ++++- internal/gcimporter/shallow_test.go | 153 ++++++++++++++++++++++++++++ 10 files changed, 242 insertions(+), 23 deletions(-) create mode 100644 internal/gcimporter/shallow_test.go diff --git a/go.mod b/go.mod index cfc184e5fb3..2216421d51a 100644 --- a/go.mod +++ b/go.mod @@ -8,3 +8,5 @@ require ( golang.org/x/net v0.1.0 golang.org/x/sys v0.1.0 ) + +require golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index da2cda7d75f..50d82dec897 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/gopls/go.mod b/gopls/go.mod index efb9be1189e..2e4fb3261a0 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -8,7 +8,7 @@ require ( github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 golang.org/x/mod v0.6.0 - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 + golang.org/x/sync v0.1.0 golang.org/x/sys v0.1.0 golang.org/x/text v0.4.0 golang.org/x/tools v0.1.13-0.20220928184430-f80e98464e27 diff --git a/gopls/go.sum b/gopls/go.sum index 78ff483dc7a..dc2d738b310 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -55,8 +55,9 @@ golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/gcimporter/bexport.go b/internal/gcimporter/bexport.go index 196cb3f9b41..30582ed6d3d 100644 --- a/internal/gcimporter/bexport.go +++ b/internal/gcimporter/bexport.go @@ -12,7 +12,6 @@ import ( "bytes" "encoding/binary" "fmt" - "go/ast" "go/constant" "go/token" "go/types" @@ -145,7 +144,7 @@ func BExportData(fset *token.FileSet, pkg *types.Package) (b []byte, err error) objcount := 0 scope := pkg.Scope() for _, name := range scope.Names() { - if !ast.IsExported(name) { + if !token.IsExported(name) { continue } if trace { @@ -482,7 +481,7 @@ func (p *exporter) method(m *types.Func) { p.pos(m) p.string(m.Name()) - if m.Name() != "_" && !ast.IsExported(m.Name()) { + if m.Name() != "_" && !token.IsExported(m.Name()) { p.pkg(m.Pkg(), false) } @@ -501,7 +500,7 @@ func (p *exporter) fieldName(f *types.Var) { // 3) field name doesn't match base type name (alias name) bname := basetypeName(f.Type()) if name == bname { - if ast.IsExported(name) { + if token.IsExported(name) { name = "" // 1) we don't need to know the field name or package } else { name = "?" // 2) use unexported name "?" to force package export @@ -514,7 +513,7 @@ func (p *exporter) fieldName(f *types.Var) { } p.string(name) - if name != "" && !ast.IsExported(name) { + if name != "" && !token.IsExported(name) { p.pkg(f.Pkg(), false) } } diff --git a/internal/gcimporter/bexport_test.go b/internal/gcimporter/bexport_test.go index 93ee3cd21a0..b5e9ce10044 100644 --- a/internal/gcimporter/bexport_test.go +++ b/internal/gcimporter/bexport_test.go @@ -109,7 +109,7 @@ type UnknownType undefined // Compare the packages' corresponding members. for _, name := range pkg.Scope().Names() { - if !ast.IsExported(name) { + if !token.IsExported(name) { continue } obj1 := pkg.Scope().Lookup(name) diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index db2753217a3..7d90f00f323 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -12,7 +12,6 @@ import ( "bytes" "encoding/binary" "fmt" - "go/ast" "go/constant" "go/token" "go/types" @@ -26,6 +25,41 @@ import ( "golang.org/x/tools/internal/typeparams" ) +// IExportShallow encodes "shallow" export data for the specified package. +// +// No promises are made about the encoding other than that it can be +// decoded by the same version of IIExportShallow. If you plan to save +// export data in the file system, be sure to include a cryptographic +// digest of the executable in the key to avoid version skew. +func IExportShallow(fset *token.FileSet, pkg *types.Package) ([]byte, error) { + // In principle this operation can only fail if out.Write fails, + // but that's impossible for bytes.Buffer---and as a matter of + // fact iexportCommon doesn't even check for I/O errors. + // TODO(adonovan): handle I/O errors properly. + // TODO(adonovan): use byte slices throughout, avoiding copying. + const bundle, shallow = false, true + var out bytes.Buffer + err := iexportCommon(&out, fset, bundle, shallow, iexportVersion, []*types.Package{pkg}) + return out.Bytes(), err +} + +// IImportShallow decodes "shallow" types.Package data encoded by IExportShallow +// in the same executable. This function cannot import data from +// cmd/compile or gcexportdata.Write. +func IImportShallow(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string, insert InsertType) (*types.Package, error) { + const bundle = false + pkgs, err := iimportCommon(fset, imports, data, bundle, path, insert) + if err != nil { + return nil, err + } + return pkgs[0], nil +} + +// InsertType is the type of a function that creates a types.TypeName +// object for a named type and inserts it into the scope of the +// specified Package. +type InsertType = func(pkg *types.Package, name string) + // Current bundled export format version. Increase with each format change. // 0: initial implementation const bundleVersion = 0 @@ -36,15 +70,17 @@ const bundleVersion = 0 // The package path of the top-level package will not be recorded, // so that calls to IImportData can override with a provided package path. func IExportData(out io.Writer, fset *token.FileSet, pkg *types.Package) error { - return iexportCommon(out, fset, false, iexportVersion, []*types.Package{pkg}) + const bundle, shallow = false, false + return iexportCommon(out, fset, bundle, shallow, iexportVersion, []*types.Package{pkg}) } // IExportBundle writes an indexed export bundle for pkgs to out. func IExportBundle(out io.Writer, fset *token.FileSet, pkgs []*types.Package) error { - return iexportCommon(out, fset, true, iexportVersion, pkgs) + const bundle, shallow = true, false + return iexportCommon(out, fset, bundle, shallow, iexportVersion, pkgs) } -func iexportCommon(out io.Writer, fset *token.FileSet, bundle bool, version int, pkgs []*types.Package) (err error) { +func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, version int, pkgs []*types.Package) (err error) { if !debug { defer func() { if e := recover(); e != nil { @@ -61,6 +97,7 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle bool, version int, p := iexporter{ fset: fset, version: version, + shallow: shallow, allPkgs: map[*types.Package]bool{}, stringIndex: map[string]uint64{}, declIndex: map[types.Object]uint64{}, @@ -82,7 +119,7 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle bool, version int, for _, pkg := range pkgs { scope := pkg.Scope() for _, name := range scope.Names() { - if ast.IsExported(name) { + if token.IsExported(name) { p.pushDecl(scope.Lookup(name)) } } @@ -205,7 +242,8 @@ type iexporter struct { out *bytes.Buffer version int - localpkg *types.Package + shallow bool // don't put types from other packages in the index + localpkg *types.Package // (nil in bundle mode) // allPkgs tracks all packages that have been referenced by // the export data, so we can ensure to include them in the @@ -256,6 +294,11 @@ func (p *iexporter) pushDecl(obj types.Object) { panic("cannot export package unsafe") } + // Shallow export data: don't index decls from other packages. + if p.shallow && obj.Pkg() != p.localpkg { + return + } + if _, ok := p.declIndex[obj]; ok { return } @@ -497,7 +540,7 @@ func (w *exportWriter) pkg(pkg *types.Package) { w.string(w.exportPath(pkg)) } -func (w *exportWriter) qualifiedIdent(obj types.Object) { +func (w *exportWriter) qualifiedType(obj *types.TypeName) { name := w.p.exportName(obj) // Ensure any referenced declarations are written out too. @@ -556,11 +599,11 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { return } w.startType(definedType) - w.qualifiedIdent(t.Obj()) + w.qualifiedType(t.Obj()) case *typeparams.TypeParam: w.startType(typeParamType) - w.qualifiedIdent(t.Obj()) + w.qualifiedType(t.Obj()) case *types.Pointer: w.startType(pointerType) diff --git a/internal/gcimporter/iexport_test.go b/internal/gcimporter/iexport_test.go index 702528aef3b..93183f9dc6f 100644 --- a/internal/gcimporter/iexport_test.go +++ b/internal/gcimporter/iexport_test.go @@ -59,7 +59,8 @@ func readExportFile(filename string) ([]byte, error) { func iexport(fset *token.FileSet, version int, pkg *types.Package) ([]byte, error) { var buf bytes.Buffer - if err := gcimporter.IExportCommon(&buf, fset, false, version, []*types.Package{pkg}); err != nil { + const bundle, shallow = false, false + if err := gcimporter.IExportCommon(&buf, fset, bundle, shallow, version, []*types.Package{pkg}); err != nil { return nil, err } return buf.Bytes(), nil @@ -197,7 +198,7 @@ func testPkg(t *testing.T, fset *token.FileSet, version int, pkg *types.Package, // Compare the packages' corresponding members. for _, name := range pkg.Scope().Names() { - if !ast.IsExported(name) { + if !token.IsExported(name) { continue } obj1 := pkg.Scope().Lookup(name) diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index 6e4c066b69b..a1c46965350 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -85,7 +85,7 @@ const ( // If the export data version is not recognized or the format is otherwise // compromised, an error is returned. func IImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (int, *types.Package, error) { - pkgs, err := iimportCommon(fset, imports, data, false, path) + pkgs, err := iimportCommon(fset, imports, data, false, path, nil) if err != nil { return 0, nil, err } @@ -94,10 +94,10 @@ func IImportData(fset *token.FileSet, imports map[string]*types.Package, data [] // IImportBundle imports a set of packages from the serialized package bundle. func IImportBundle(fset *token.FileSet, imports map[string]*types.Package, data []byte) ([]*types.Package, error) { - return iimportCommon(fset, imports, data, true, "") + return iimportCommon(fset, imports, data, true, "", nil) } -func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data []byte, bundle bool, path string) (pkgs []*types.Package, err error) { +func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data []byte, bundle bool, path string, insert InsertType) (pkgs []*types.Package, err error) { const currentVersion = iexportVersionCurrent version := int64(-1) if !debug { @@ -147,6 +147,7 @@ func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data p := iimporter{ version: int(version), ipath: path, + insert: insert, stringData: stringData, stringCache: make(map[uint64]string), @@ -187,11 +188,18 @@ func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data } else if pkg.Name() != pkgName { errorf("conflicting names %s and %s for package %q", pkg.Name(), pkgName, path) } + if i == 0 && !bundle { + p.localpkg = pkg + } p.pkgCache[pkgPathOff] = pkg + // Read index for package. nameIndex := make(map[string]uint64) - for nSyms := r.uint64(); nSyms > 0; nSyms-- { + nSyms := r.uint64() + // In shallow mode we don't expect an index for other packages. + assert(nSyms == 0 || p.localpkg == pkg || p.insert == nil) + for ; nSyms > 0; nSyms-- { name := p.stringAt(r.uint64()) nameIndex[name] = r.uint64() } @@ -267,6 +275,9 @@ type iimporter struct { version int ipath string + localpkg *types.Package + insert func(pkg *types.Package, name string) // "shallow" mode only + stringData []byte stringCache map[uint64]string pkgCache map[uint64]*types.Package @@ -310,6 +321,13 @@ func (p *iimporter) doDecl(pkg *types.Package, name string) { off, ok := p.pkgIndex[pkg][name] if !ok { + // In "shallow" mode, call back to the application to + // find the object and insert it into the package scope. + if p.insert != nil { + assert(pkg != p.localpkg) + p.insert(pkg, name) // "can't fail" + return + } errorf("%v.%v not in index", pkg, name) } diff --git a/internal/gcimporter/shallow_test.go b/internal/gcimporter/shallow_test.go new file mode 100644 index 00000000000..084604c7e0b --- /dev/null +++ b/internal/gcimporter/shallow_test.go @@ -0,0 +1,153 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gcimporter_test + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/sync/errgroup" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/gcimporter" + "golang.org/x/tools/internal/testenv" +) + +// TestStd type-checks the standard library using shallow export data. +func TestShallowStd(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode; too slow (https://golang.org/issue/14113)") + } + testenv.NeedsTool(t, "go") + + // Load import graph of the standard library. + // (No parsing or type-checking.) + cfg := &packages.Config{ + Mode: packages.LoadImports, + Tests: false, + } + pkgs, err := packages.Load(cfg, "std") + if err != nil { + t.Fatalf("load: %v", err) + } + if len(pkgs) < 200 { + t.Fatalf("too few packages: %d", len(pkgs)) + } + + // Type check the packages in parallel postorder. + done := make(map[*packages.Package]chan struct{}) + packages.Visit(pkgs, nil, func(p *packages.Package) { + done[p] = make(chan struct{}) + }) + packages.Visit(pkgs, nil, + func(pkg *packages.Package) { + go func() { + // Wait for all deps to be done. + for _, imp := range pkg.Imports { + <-done[imp] + } + typecheck(t, pkg) + close(done[pkg]) + }() + }) + for _, root := range pkgs { + <-done[root] + } +} + +// typecheck reads, parses, and type-checks a package. +// It squirrels the export data in the the ppkg.ExportFile field. +func typecheck(t *testing.T, ppkg *packages.Package) { + if ppkg.PkgPath == "unsafe" { + return // unsafe is special + } + + // Create a local FileSet just for this package. + fset := token.NewFileSet() + + // Parse files in parallel. + syntax := make([]*ast.File, len(ppkg.CompiledGoFiles)) + var group errgroup.Group + for i, filename := range ppkg.CompiledGoFiles { + i, filename := i, filename + group.Go(func() error { + f, err := parser.ParseFile(fset, filename, nil, parser.SkipObjectResolution) + if err != nil { + return err // e.g. missing file + } + syntax[i] = f + return nil + }) + } + if err := group.Wait(); err != nil { + t.Fatal(err) + } + // Inv: all files were successfully parsed. + + // importer state + var ( + insert func(p *types.Package, name string) + importMap = make(map[string]*types.Package) // keys are PackagePaths + ) + + loadFromExportData := func(imp *packages.Package) (*types.Package, error) { + data := []byte(imp.ExportFile) + return gcimporter.IImportShallow(fset, importMap, data, imp.PkgPath, insert) + } + insert = func(p *types.Package, name string) { + // Hunt for p among the transitive dependencies (inefficient). + var imp *packages.Package + packages.Visit([]*packages.Package{ppkg}, func(q *packages.Package) bool { + if q.PkgPath == p.Path() { + imp = q + return false + } + return true + }, nil) + if imp == nil { + t.Fatalf("can't find dependency: %q", p.Path()) + } + imported, err := loadFromExportData(imp) + if err != nil { + t.Fatalf("unmarshal: %v", err) + } + obj := imported.Scope().Lookup(name) + if obj == nil { + t.Fatalf("lookup %q.%s failed", imported.Path(), name) + } + if imported != p { + t.Fatalf("internal error: inconsistent packages") + } + } + + cfg := &types.Config{ + Error: func(e error) { + t.Error(e) + }, + Importer: importerFunc(func(importPath string) (*types.Package, error) { + if importPath == "unsafe" { + return types.Unsafe, nil // unsafe has no exportdata + } + imp, ok := ppkg.Imports[importPath] + if !ok { + return nil, fmt.Errorf("missing import %q", importPath) + } + return loadFromExportData(imp) + }), + } + + // Type-check the syntax trees. + tpkg, _ := cfg.Check(ppkg.PkgPath, fset, syntax, nil) + + // Save the export data. + data, err := gcimporter.IExportShallow(fset, tpkg) + if err != nil { + t.Fatalf("internal error marshalling export data: %v", err) + } + ppkg.ExportFile = string(data) +} From ec044b1a47c798cae2fae412fde350f7e3d0b7fa Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 31 Oct 2022 16:25:47 -0400 Subject: [PATCH 401/723] gopls: update dependencies following the v0.10.0 release Update selected dependencies following the v0.10.0 release, excluding sergi/go-diff and x/vuln. Gofumpt@v0.4.0 requires go1.18, so link it selectively following the pattern of staticcheck. While at it, clean up some things related to the wiring of staticcheck and gofumpt support. Notably, in VS Code error messages do not support formatting such as newlines or tabs. Add a test for the conditional Gofumpt support. For golang/go#56211 Change-Id: Id09fdcc30ad83c0ace11b0dea9a5556a6461d552 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446736 Run-TryBot: Robert Findley Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/go.mod | 14 ++--- gopls/go.sum | 31 ++++++---- gopls/internal/hooks/analysis.go | 62 ------------------- gopls/internal/hooks/analysis_116.go | 14 +++++ gopls/internal/hooks/analysis_117.go | 58 +++++++++++++++-- gopls/internal/hooks/gofumpt_117.go | 13 ++++ gopls/internal/hooks/gofumpt_118.go | 24 +++++++ gopls/internal/hooks/hooks.go | 10 +-- gopls/internal/hooks/licenses_test.go | 6 +- gopls/internal/lsp/source/options.go | 14 +++-- .../regtest/misc/configuration_test.go | 15 +++++ .../internal/regtest/misc/formatting_test.go | 2 + 12 files changed, 159 insertions(+), 104 deletions(-) delete mode 100644 gopls/internal/hooks/analysis.go create mode 100644 gopls/internal/hooks/analysis_116.go create mode 100644 gopls/internal/hooks/gofumpt_117.go create mode 100644 gopls/internal/hooks/gofumpt_118.go diff --git a/gopls/go.mod b/gopls/go.mod index 2e4fb3261a0..23293c769ce 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -3,7 +3,7 @@ module golang.org/x/tools/gopls go 1.18 require ( - github.com/google/go-cmp v0.5.8 + github.com/google/go-cmp v0.5.9 github.com/jba/printsrc v0.2.2 github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 @@ -11,20 +11,20 @@ require ( golang.org/x/sync v0.1.0 golang.org/x/sys v0.1.0 golang.org/x/text v0.4.0 - golang.org/x/tools v0.1.13-0.20220928184430-f80e98464e27 + golang.org/x/tools v0.2.0 golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.3.3 - mvdan.cc/gofumpt v0.3.1 + mvdan.cc/gofumpt v0.4.0 mvdan.cc/xurls/v2 v2.4.0 ) -require golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect +require golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect require ( - github.com/BurntSushi/toml v1.2.0 // indirect - github.com/google/safehtml v0.0.2 // indirect - golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/google/safehtml v0.1.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326 // indirect ) replace golang.org/x/tools => ../ diff --git a/gopls/go.sum b/gopls/go.sum index dc2d738b310..57bf799d67b 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -1,23 +1,25 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= -github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= -github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/safehtml v0.0.2 h1:ZOt2VXg4x24bW0m2jtzAOkhoXV0iM8vNKc0paByCZqM= github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= +github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= +github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/jba/printsrc v0.2.2 h1:9OHK51UT+/iMAEBlQIIXW04qvKyF3/vvLuwW/hL8tDU= github.com/jba/printsrc v0.2.2/go.mod h1:1xULjw59sL0dPdWpDoVU06TIEO/Wnfv6AHRpiElTwYM= github.com/jba/templatecheck v0.6.0 h1:SwM8C4hlK/YNLsdcXStfnHWE2HKkuTVwy5FKQHt5ro8= @@ -33,8 +35,9 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -43,11 +46,12 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= +golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e h1:7Xs2YCOpMlNqSQSmrrnhlzBXIE/bpMecZplbLePTJvE= -golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326 h1:fl8k2zg28yA23264d82M4dp+YlJ3ngDcpuB1bewkQi4= +golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= @@ -56,14 +60,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -89,8 +94,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.3.3 h1:oDx7VAwstgpYpb3wv0oxiZlxY+foCpRAwY7Vk6XpAgA= honnef.co/go/tools v0.3.3/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= -mvdan.cc/gofumpt v0.3.1 h1:avhhrOmv0IuvQVK7fvwV91oFSGAk5/6Po8GXTzICeu8= -mvdan.cc/gofumpt v0.3.1/go.mod h1:w3ymliuxvzVx8DAutBnVyDqYb1Niy/yCJt/lk821YCE= +mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= +mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5/go.mod h1:b8RRCBm0eeiWR8cfN88xeq2G5SG3VKGO+5UPWi5FSOY= mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc= diff --git a/gopls/internal/hooks/analysis.go b/gopls/internal/hooks/analysis.go deleted file mode 100644 index 27ab9a699f9..00000000000 --- a/gopls/internal/hooks/analysis.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.17 -// +build go1.17 - -package hooks - -import ( - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" - "honnef.co/go/tools/analysis/lint" - "honnef.co/go/tools/quickfix" - "honnef.co/go/tools/simple" - "honnef.co/go/tools/staticcheck" - "honnef.co/go/tools/stylecheck" -) - -func updateAnalyzers(options *source.Options) { - options.StaticcheckSupported = true - - mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity { - switch severity { - case lint.SeverityError: - return protocol.SeverityError - case lint.SeverityDeprecated: - // TODO(dh): in LSP, deprecated is a tag, not a severity. - // We'll want to support this once we enable SA5011. - return protocol.SeverityWarning - case lint.SeverityWarning: - return protocol.SeverityWarning - case lint.SeverityInfo: - return protocol.SeverityInformation - case lint.SeverityHint: - return protocol.SeverityHint - default: - return protocol.SeverityWarning - } - } - add := func(analyzers []*lint.Analyzer, skip map[string]struct{}) { - for _, a := range analyzers { - if _, ok := skip[a.Analyzer.Name]; ok { - continue - } - - enabled := !a.Doc.NonDefault - options.AddStaticcheckAnalyzer(a.Analyzer, enabled, mapSeverity(a.Doc.Severity)) - } - } - - add(simple.Analyzers, nil) - add(staticcheck.Analyzers, map[string]struct{}{ - // This check conflicts with the vet printf check (golang/go#34494). - "SA5009": {}, - // This check relies on facts from dependencies, which - // we don't currently compute. - "SA5011": {}, - }) - add(stylecheck.Analyzers, nil) - add(quickfix.Analyzers, nil) -} diff --git a/gopls/internal/hooks/analysis_116.go b/gopls/internal/hooks/analysis_116.go new file mode 100644 index 00000000000..dd429dea898 --- /dev/null +++ b/gopls/internal/hooks/analysis_116.go @@ -0,0 +1,14 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.17 +// +build !go1.17 + +package hooks + +import "golang.org/x/tools/gopls/internal/lsp/source" + +func updateAnalyzers(options *source.Options) { + options.StaticcheckSupported = false +} diff --git a/gopls/internal/hooks/analysis_117.go b/gopls/internal/hooks/analysis_117.go index dd429dea898..27ab9a699f9 100644 --- a/gopls/internal/hooks/analysis_117.go +++ b/gopls/internal/hooks/analysis_117.go @@ -1,14 +1,62 @@ -// Copyright 2021 The Go Authors. All rights reserved. +// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !go1.17 -// +build !go1.17 +//go:build go1.17 +// +build go1.17 package hooks -import "golang.org/x/tools/gopls/internal/lsp/source" +import ( + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "honnef.co/go/tools/analysis/lint" + "honnef.co/go/tools/quickfix" + "honnef.co/go/tools/simple" + "honnef.co/go/tools/staticcheck" + "honnef.co/go/tools/stylecheck" +) func updateAnalyzers(options *source.Options) { - options.StaticcheckSupported = false + options.StaticcheckSupported = true + + mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity { + switch severity { + case lint.SeverityError: + return protocol.SeverityError + case lint.SeverityDeprecated: + // TODO(dh): in LSP, deprecated is a tag, not a severity. + // We'll want to support this once we enable SA5011. + return protocol.SeverityWarning + case lint.SeverityWarning: + return protocol.SeverityWarning + case lint.SeverityInfo: + return protocol.SeverityInformation + case lint.SeverityHint: + return protocol.SeverityHint + default: + return protocol.SeverityWarning + } + } + add := func(analyzers []*lint.Analyzer, skip map[string]struct{}) { + for _, a := range analyzers { + if _, ok := skip[a.Analyzer.Name]; ok { + continue + } + + enabled := !a.Doc.NonDefault + options.AddStaticcheckAnalyzer(a.Analyzer, enabled, mapSeverity(a.Doc.Severity)) + } + } + + add(simple.Analyzers, nil) + add(staticcheck.Analyzers, map[string]struct{}{ + // This check conflicts with the vet printf check (golang/go#34494). + "SA5009": {}, + // This check relies on facts from dependencies, which + // we don't currently compute. + "SA5011": {}, + }) + add(stylecheck.Analyzers, nil) + add(quickfix.Analyzers, nil) } diff --git a/gopls/internal/hooks/gofumpt_117.go b/gopls/internal/hooks/gofumpt_117.go new file mode 100644 index 00000000000..71886357704 --- /dev/null +++ b/gopls/internal/hooks/gofumpt_117.go @@ -0,0 +1,13 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.18 +// +build !go1.18 + +package hooks + +import "golang.org/x/tools/gopls/internal/lsp/source" + +func updateGofumpt(options *source.Options) { +} diff --git a/gopls/internal/hooks/gofumpt_118.go b/gopls/internal/hooks/gofumpt_118.go new file mode 100644 index 00000000000..4eb523261dc --- /dev/null +++ b/gopls/internal/hooks/gofumpt_118.go @@ -0,0 +1,24 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package hooks + +import ( + "context" + + "golang.org/x/tools/gopls/internal/lsp/source" + "mvdan.cc/gofumpt/format" +) + +func updateGofumpt(options *source.Options) { + options.GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { + return format.Source(src, format.Options{ + LangVersion: langVersion, + ModulePath: modulePath, + }) + } +} diff --git a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go index 085fa53a39a..5624a5eb386 100644 --- a/gopls/internal/hooks/hooks.go +++ b/gopls/internal/hooks/hooks.go @@ -8,11 +8,8 @@ package hooks // import "golang.org/x/tools/gopls/internal/hooks" import ( - "context" - "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/diff" - "mvdan.cc/gofumpt/format" "mvdan.cc/xurls/v2" ) @@ -29,11 +26,6 @@ func Options(options *source.Options) { } } options.URLRegexp = xurls.Relaxed() - options.GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { - return format.Source(src, format.Options{ - LangVersion: langVersion, - ModulePath: modulePath, - }) - } updateAnalyzers(options) + updateGofumpt(options) } diff --git a/gopls/internal/hooks/licenses_test.go b/gopls/internal/hooks/licenses_test.go index 3b61d348d95..b10d7e2b36c 100644 --- a/gopls/internal/hooks/licenses_test.go +++ b/gopls/internal/hooks/licenses_test.go @@ -15,9 +15,9 @@ import ( ) func TestLicenses(t *testing.T) { - // License text differs for older Go versions because staticcheck isn't - // supported for those versions. - testenv.NeedsGo1Point(t, 17) + // License text differs for older Go versions because staticcheck or gofumpt + // isn't supported for those versions. + testenv.NeedsGo1Point(t, 18) if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { t.Skip("generating licenses only works on Unixes") diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index c690d29cea9..23d795ef0e5 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -1036,10 +1036,8 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) if v, ok := result.asBool(); ok { o.Staticcheck = v if v && !o.StaticcheckSupported { - // Warn if the user is trying to enable staticcheck, but staticcheck is - // unsupported. - result.Error = fmt.Errorf("applying setting %q: staticcheck is not supported at %s\n"+ - "\trebuild gopls with a more recent version of Go", result.Name, runtime.Version()) + result.Error = fmt.Errorf("applying setting %q: staticcheck is not supported at %s;"+ + " rebuild gopls with a more recent version of Go", result.Name, runtime.Version()) } } @@ -1059,7 +1057,13 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.ShowBugReports) case "gofumpt": - result.setBool(&o.Gofumpt) + if v, ok := result.asBool(); ok { + o.Gofumpt = v + if v && o.GofumptFormat == nil { + result.Error = fmt.Errorf("applying setting %q: gofumpt is not supported at %s;"+ + " rebuild gopls with a more recent version of Go", result.Name, runtime.Version()) + } + } case "semanticTokens": result.setBool(&o.SemanticTokens) diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index a1d46f036c6..5bb2c8620a0 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -80,6 +80,21 @@ var FooErr = errors.New("foo") }) } +func TestGofumptWarning(t *testing.T) { + testenv.SkipAfterGo1Point(t, 17) + + WithOptions( + Settings{"gofumpt": true}, + ).Run(t, "", func(t *testing.T, env *Env) { + env.Await( + OnceMet( + InitialWorkspaceLoad, + ShownMessage("gofumpt is not supported"), + ), + ) + }) +} + func TestDeprecatedSettings(t *testing.T) { WithOptions( Settings{ diff --git a/gopls/internal/regtest/misc/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go index 6b20afa98f0..39d58229896 100644 --- a/gopls/internal/regtest/misc/formatting_test.go +++ b/gopls/internal/regtest/misc/formatting_test.go @@ -10,6 +10,7 @@ import ( . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/testenv" ) const unformattedProgram = ` @@ -302,6 +303,7 @@ func main() { } func TestGofumptFormatting(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // Exercise some gofumpt formatting rules: // - No empty lines following an assignment operator From 39c2fd8bffdf79eca7f67b3fe37d3571d05625a4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 4 Nov 2022 13:14:16 -0400 Subject: [PATCH 402/723] internal/lsp/cache: simplify importsState modfile hashing logic While checking for changes to go.mod files in the importsState, there is no reason for special handling based on workspace mode: we can simply hash all active modfiles. This moves us towards an improved API for the workspace: it should simply be responsible for tracking active modfiles. This also incidentally avoids the panic reported in golang/go#55837. Fixes golang/go#55837 Change-Id: I8cb345d1689be12382683186afe3f9addb19d467 Reviewed-on: https://go-review.googlesource.com/c/tools/+/447956 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/imports.go | 29 ++++++---------------- gopls/internal/lsp/cache/load.go | 4 +-- gopls/internal/lsp/cache/snapshot.go | 8 +++--- gopls/internal/lsp/cache/view.go | 8 +++--- gopls/internal/lsp/cache/workspace.go | 5 +++- gopls/internal/lsp/cache/workspace_test.go | 2 +- 6 files changed, 22 insertions(+), 34 deletions(-) diff --git a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go index a73f9beff3e..2bda377746d 100644 --- a/gopls/internal/lsp/cache/imports.go +++ b/gopls/internal/lsp/cache/imports.go @@ -13,11 +13,11 @@ import ( "sync" "time" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/gopls/internal/lsp/source" ) type importsState struct { @@ -37,33 +37,18 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot s.mu.Lock() defer s.mu.Unlock() - // Find the hash of the active mod file, if any. Using the unsaved content + // Find the hash of active mod files, if any. Using the unsaved content // is slightly wasteful, since we'll drop caches a little too often, but // the mod file shouldn't be changing while people are autocompleting. - var modFileHash source.Hash - // If we are using 'legacyWorkspace' mode, we can just read the modfile from - // the snapshot. Otherwise, we need to get the synthetic workspace mod file. // - // TODO(rfindley): we should be able to just always use the synthetic - // workspace module, or alternatively use the go.work file. - if snapshot.workspace.moduleSource == legacyWorkspace { - for m := range snapshot.workspace.getActiveModFiles() { // range to access the only element - modFH, err := snapshot.GetFile(ctx, m) - if err != nil { - return err - } - modFileHash = modFH.FileIdentity().Hash - } - } else { - modFile, err := snapshot.workspace.modFile(ctx, snapshot) - if err != nil { - return err - } - modBytes, err := modFile.Format() + // TODO(rfindley): consider instead hashing on-disk modfiles here. + var modFileHash source.Hash + for m := range snapshot.workspace.ActiveModFiles() { + fh, err := snapshot.GetFile(ctx, m) if err != nil { return err } - modFileHash = source.HashOf(modBytes) + modFileHash.XORWith(fh.FileIdentity().Hash) } // view.goEnv is immutable -- changes make a new view. Options can change. diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 67b235a8093..9cabffc0d03 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -352,10 +352,10 @@ https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` // If the user has one active go.mod file, they may still be editing files // in nested modules. Check the module of each open file and add warnings // that the nested module must be opened as a workspace folder. - if len(s.workspace.getActiveModFiles()) == 1 { + if len(s.workspace.ActiveModFiles()) == 1 { // Get the active root go.mod file to compare against. var rootModURI span.URI - for uri := range s.workspace.getActiveModFiles() { + for uri := range s.workspace.ActiveModFiles() { rootModURI = uri } nestedModules := map[string][]source.VersionedFileHandle{} diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index eed7dfc6ea0..b05f401c52a 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -250,7 +250,7 @@ func (s *snapshot) FileSet() *token.FileSet { func (s *snapshot) ModFiles() []span.URI { var uris []span.URI - for modURI := range s.workspace.getActiveModFiles() { + for modURI := range s.workspace.ActiveModFiles() { uris = append(uris, modURI) } return uris @@ -281,7 +281,7 @@ func (s *snapshot) ValidBuildConfiguration() bool { } // Check if the user is working within a module or if we have found // multiple modules in the workspace. - if len(s.workspace.getActiveModFiles()) > 0 { + if len(s.workspace.ActiveModFiles()) > 0 { return true } // The user may have a multiple directories in their GOPATH. @@ -308,7 +308,7 @@ func (s *snapshot) workspaceMode() workspaceMode { // If the view is not in a module and contains no modules, but still has a // valid workspace configuration, do not create the workspace module. // It could be using GOPATH or a different build system entirely. - if len(s.workspace.getActiveModFiles()) == 0 && validBuildConfiguration { + if len(s.workspace.ActiveModFiles()) == 0 && validBuildConfiguration { return mode } mode |= moduleMode @@ -480,7 +480,7 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat if mode == source.LoadWorkspace { switch s.workspace.moduleSource { case legacyWorkspace: - for m := range s.workspace.getActiveModFiles() { // range to access the only element + for m := range s.workspace.ActiveModFiles() { // range to access the only element modURI = m } case goWorkWorkspace: diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index dec2cb0808b..a408ee7a03a 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -581,13 +581,13 @@ func (v *View) Session() *Session { func (s *snapshot) IgnoredFile(uri span.URI) bool { filename := uri.Filename() var prefixes []string - if len(s.workspace.getActiveModFiles()) == 0 { + if len(s.workspace.ActiveModFiles()) == 0 { for _, entry := range filepath.SplitList(s.view.gopath) { prefixes = append(prefixes, filepath.Join(entry, "src")) } } else { prefixes = append(prefixes, s.view.gomodcache) - for m := range s.workspace.getActiveModFiles() { + for m := range s.workspace.ActiveModFiles() { prefixes = append(prefixes, dirURI(m).Filename()) } } @@ -679,8 +679,8 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { }) } - if len(s.workspace.getActiveModFiles()) > 0 { - for modURI := range s.workspace.getActiveModFiles() { + if len(s.workspace.ActiveModFiles()) > 0 { + for modURI := range s.workspace.ActiveModFiles() { // Be careful not to add context cancellation errors as critical module // errors. fh, err := s.GetFile(ctx, modURI) diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go index 2020718ea7d..b280ef369a8 100644 --- a/gopls/internal/lsp/cache/workspace.go +++ b/gopls/internal/lsp/cache/workspace.go @@ -73,6 +73,7 @@ type workspaceCommon struct { type workspace struct { workspaceCommon + // The source of modules in this workspace. moduleSource workspaceSource // activeModFiles holds the active go.mod files. @@ -192,11 +193,13 @@ func (ws *workspace) loadExplicitWorkspaceFile(ctx context.Context, fs source.Fi var noHardcodedWorkspace = errors.New("no hardcoded workspace") +// TODO(rfindley): eliminate getKnownModFiles. func (w *workspace) getKnownModFiles() map[span.URI]struct{} { return w.knownModFiles } -func (w *workspace) getActiveModFiles() map[span.URI]struct{} { +// ActiveModFiles returns the set of active mod files for the current workspace. +func (w *workspace) ActiveModFiles() map[span.URI]struct{} { return w.activeModFiles } diff --git a/gopls/internal/lsp/cache/workspace_test.go b/gopls/internal/lsp/cache/workspace_test.go index 37e8f2cc46d..188869562c5 100644 --- a/gopls/internal/lsp/cache/workspace_test.go +++ b/gopls/internal/lsp/cache/workspace_test.go @@ -386,7 +386,7 @@ func checkState(ctx context.Context, t *testing.T, fs source.FileSource, rel fak t.Errorf("module source = %v, want %v", got.moduleSource, want.source) } modules := make(map[span.URI]struct{}) - for k := range got.getActiveModFiles() { + for k := range got.ActiveModFiles() { modules[k] = struct{}{} } for _, modPath := range want.modules { From 3c8152e28aa16f4d31edd456e089eaf33a1f81a0 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 4 Nov 2022 15:23:06 -0400 Subject: [PATCH 403/723] internal/gcimporter: optimize dependency lookup The old code based on packages.Visit traversed in a deterministic order, and didn't stop when it found its target (the 'return false' only prunes that subtree). This CL replaces it with a precomputation of the PkgPath-to-*Package mapping. The performance difference is small for this test but it nearly dominates on a larger input (e.g. k8s). Example code shouldn't steer users into asymptotic traps. Change-Id: I19f4fc2c25da3d2ae00090704df30a54d8516bf5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/447958 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan Reviewed-by: Robert Findley --- internal/gcimporter/shallow_test.go | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/internal/gcimporter/shallow_test.go b/internal/gcimporter/shallow_test.go index 084604c7e0b..717cb878952 100644 --- a/internal/gcimporter/shallow_test.go +++ b/internal/gcimporter/shallow_test.go @@ -89,6 +89,23 @@ func typecheck(t *testing.T, ppkg *packages.Package) { } // Inv: all files were successfully parsed. + // Build map of dependencies by package path. + // (We don't compute this mapping for the entire + // packages graph because it is not globally consistent.) + depsByPkgPath := make(map[string]*packages.Package) + { + var visit func(*packages.Package) + visit = func(pkg *packages.Package) { + if depsByPkgPath[pkg.PkgPath] == nil { + depsByPkgPath[pkg.PkgPath] = pkg + for path := range pkg.Imports { + visit(pkg.Imports[path]) + } + } + } + visit(ppkg) + } + // importer state var ( insert func(p *types.Package, name string) @@ -100,16 +117,8 @@ func typecheck(t *testing.T, ppkg *packages.Package) { return gcimporter.IImportShallow(fset, importMap, data, imp.PkgPath, insert) } insert = func(p *types.Package, name string) { - // Hunt for p among the transitive dependencies (inefficient). - var imp *packages.Package - packages.Visit([]*packages.Package{ppkg}, func(q *packages.Package) bool { - if q.PkgPath == p.Path() { - imp = q - return false - } - return true - }, nil) - if imp == nil { + imp, ok := depsByPkgPath[p.Path()] + if !ok { t.Fatalf("can't find dependency: %q", p.Path()) } imported, err := loadFromExportData(imp) From fe725d9349e46c10f4b33b3d7c9dc961ab179697 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 4 Nov 2022 17:05:15 -0400 Subject: [PATCH 404/723] gopls/internal/regtest: simplify awaiting diagnostics from a change When awaiting diagnostics, we should almost always wrap expectations in a OnceMet(precondition, ...), so that tests do not hang indefinitely if the diagnostic pass completes and the expectations are still not met. Before this change, the user must be careful to pass in the correct precondition (or combination of preconditions), else they may be susceptible to races. This change adds an AllOf combinator, and uses it to define a new DoneDiagnosingChanges expectation that waits for all anticipated diagnostic passes to complete. This should fix the race in TestUnknownRevision. We should apply a similar transformation throughout the regression test suites. To make this easier, add a shorter AfterChange helper that implements the common pattern. Fixes golang/go#55070 Change-Id: Ie0e3c4701fba7b1d10de6b43d776562d198ffac9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448115 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/regtest/expectation.go | 76 +++++++++++++++++++ gopls/internal/lsp/text_synchronization.go | 3 + .../internal/regtest/modfile/modfile_test.go | 20 +++-- 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 335a46fc589..d09398779c1 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -7,6 +7,7 @@ package regtest import ( "fmt" "regexp" + "sort" "strings" "golang.org/x/tools/gopls/internal/lsp" @@ -130,6 +131,33 @@ func AnyOf(anyOf ...Expectation) *SimpleExpectation { } } +// AllOf expects that all given expectations are met. +// +// TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf +// and AllOf) is that we lose the information of *why* they failed: the Awaiter +// is not smart enough to look inside. +// +// Refactor the API such that the Check function is responsible for explaining +// why an expectation failed. This should allow us to significantly improve +// test output: we won't need to summarize state at all, as the verdict +// explanation itself should describe clearly why the expectation not met. +func AllOf(allOf ...Expectation) *SimpleExpectation { + check := func(s State) Verdict { + verdict := Met + for _, e := range allOf { + if v := e.Check(s); v > verdict { + verdict = v + } + } + return verdict + } + description := describeExpectations(allOf...) + return &SimpleExpectation{ + check: check, + description: fmt.Sprintf("All of:\n%s", description), + } +} + // ReadDiagnostics is an 'expectation' that is used to read diagnostics // atomically. It is intended to be used with 'OnceMet'. func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) *SimpleExpectation { @@ -218,6 +246,54 @@ func ShowMessageRequest(title string) SimpleExpectation { } } +// DoneDiagnosingChanges expects that diagnostics are complete from common +// change notifications: didOpen, didChange, didSave, didChangeWatchedFiles, +// and didClose. +// +// This can be used when multiple notifications may have been sent, such as +// when a didChange is immediately followed by a didSave. It is insufficient to +// simply await NoOutstandingWork, because the LSP client has no control over +// when the server starts processing a notification. Therefore, we must keep +// track of +func (e *Env) DoneDiagnosingChanges() Expectation { + stats := e.Editor.Stats() + statsBySource := map[lsp.ModificationSource]uint64{ + lsp.FromDidOpen: stats.DidOpen, + lsp.FromDidChange: stats.DidChange, + lsp.FromDidSave: stats.DidSave, + lsp.FromDidChangeWatchedFiles: stats.DidChangeWatchedFiles, + lsp.FromDidClose: stats.DidClose, + } + + var expected []lsp.ModificationSource + for k, v := range statsBySource { + if v > 0 { + expected = append(expected, k) + } + } + + // Sort for stability. + sort.Slice(expected, func(i, j int) bool { + return expected[i] < expected[j] + }) + + var all []Expectation + for _, source := range expected { + all = append(all, CompletedWork(lsp.DiagnosticWorkTitle(source), statsBySource[source], true)) + } + + return AllOf(all...) +} + +// AfterChange expects that the given expectations will be met after all +// state-changing notifications have been processed by the server. +func (e *Env) AfterChange(expectations ...Expectation) Expectation { + return OnceMet( + e.DoneDiagnosingChanges(), + expectations..., + ) +} + // DoneWithOpen expects all didOpen notifications currently sent by the editor // to be completely processed. func (e *Env) DoneWithOpen() Expectation { diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go index 63bc0e8e561..ab765b60dd3 100644 --- a/gopls/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -40,6 +40,9 @@ const ( // FromDidClose is a file modification caused by closing a file. FromDidClose + // TODO: add FromDidChangeConfiguration, once configuration changes cause a + // new snapshot to be created. + // FromRegenerateCgo refers to file modifications caused by regenerating // the cgo sources for the workspace. FromRegenerateCgo diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index eb3f9665696..64892be5966 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -633,7 +633,7 @@ func main() { d := protocol.PublishDiagnosticsParams{} env.Await( - OnceMet( + env.AfterChange( // Make sure the diagnostic mentions the new version -- the old diagnostic is in the same place. env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "example.com@v1.2.3"), ReadDiagnostics("a/go.mod", &d), @@ -646,8 +646,10 @@ func main() { env.ApplyCodeAction(qfs[0]) // Arbitrarily pick a single fix to apply. Applying all of them seems to cause trouble in this particular test. env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. env.Await( - EmptyDiagnostics("a/go.mod"), - env.DiagnosticAtRegexp("a/main.go", "x = "), + env.AfterChange( + EmptyDiagnostics("a/go.mod"), + env.DiagnosticAtRegexp("a/main.go", "x = "), + ), ) }) }) @@ -677,17 +679,23 @@ func main() { runner.Run(t, known, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") env.Await( - env.DiagnosticAtRegexp("a/main.go", "x = "), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", "x = "), + ), ) env.RegexpReplace("a/go.mod", "v1.2.3", "v1.2.2") env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk env.Await( - env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"), + env.AfterChange( + env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"), + ), ) env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk env.Await( - env.DiagnosticAtRegexp("a/main.go", "x = "), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", "x = "), + ), ) }) }) From 8e0240af74102670439ba4ddc10d2be2d18b2ef7 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 4 Nov 2022 17:34:56 -0400 Subject: [PATCH 405/723] internal/regtest/workspace: permanently skip TestDeleteModule_Interdependent This test is flaky, likely due to some race in the experimentalWorkspaceModule logic. Since we're about to delete support for that experimental feature, simply skip the test to stop the flakes. Leave the test as an artifact, as it will be deleted as part of the clean up of experimentalWorkspaceModule. No need to delete it before then. Updates golang/go#55331 Fixes golang/go#55923 Change-Id: Ic17485e42e335459df462af00a2088812ecfb5f4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448116 Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Auto-Submit: Robert Findley --- gopls/internal/regtest/workspace/workspace_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index e92ba8dbf93..5786f0a031b 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -335,7 +335,12 @@ func main() { // This change tests that the version of the module used changes after it has // been deleted from the workspace. +// +// TODO(golang/go#55331): delete this placeholder along with experimental +// workspace module. func TestDeleteModule_Interdependent(t *testing.T) { + t.Skip("golang/go#55331: the experimental workspace module is scheduled for deletion") + const multiModule = ` -- moda/a/go.mod -- module a.com From 88a354830446314a68862f0687ce60950a69c9f9 Mon Sep 17 00:00:00 2001 From: pjw Date: Sun, 6 Nov 2022 10:51:50 -0500 Subject: [PATCH 406/723] gopls/coverage: repair coverage.go Coverage.go computes the test coverage from running all the gopls tests. This CL accounts for the changed source tree (internal/lsp is gone) and new actions returned by go test -json ('pause' and 'cont'). Change-Id: I970b3ec107746ce02e3dcdcad9f8c19cffad8d11 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448295 Run-TryBot: Peter Weinberger Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/coverage/coverage.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gopls/internal/coverage/coverage.go b/gopls/internal/coverage/coverage.go index 1ceabefab58..9a7d219945e 100644 --- a/gopls/internal/coverage/coverage.go +++ b/gopls/internal/coverage/coverage.go @@ -188,7 +188,12 @@ func maybePrint(m result) { if *verbose > 3 { fmt.Printf("%s %s %q %.3f\n", m.Action, m.Test, m.Output, m.Elapsed) } + case "pause", "cont": + if *verbose > 2 { + fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) + } default: + fmt.Printf("%#v\n", m) log.Fatalf("unknown action %s\n", m.Action) } } @@ -228,7 +233,7 @@ func checkCwd() { if err != nil { log.Fatal(err) } - // we expect to be a the root of golang.org/x/tools + // we expect to be at the root of golang.org/x/tools cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "golang.org/x/tools") buf, err := cmd.Output() buf = bytes.Trim(buf, "\n \t") // remove \n at end @@ -243,10 +248,6 @@ func checkCwd() { if err != nil { log.Fatalf("expected a gopls directory, %v", err) } - _, err = os.Stat("internal/lsp") - if err != nil { - log.Fatalf("expected to see internal/lsp, %v", err) - } } func listDirs(dir string) []string { From 50506576b8a6cec06115896c3f09e76d47eb86b0 Mon Sep 17 00:00:00 2001 From: pjw Date: Sun, 6 Nov 2022 11:06:43 -0500 Subject: [PATCH 407/723] gopls/fake: add semantic token modifiers to fake editor This change will make it possible to do semantic token regtests. Change-Id: I9963c60f61af30f973a2ee4cd32aaa5545bdc4ec Reviewed-on: https://go-review.googlesource.com/c/tools/+/448296 TryBot-Result: Gopher Robot Run-TryBot: Peter Weinberger Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/fake/editor.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index dfd17c7e55a..f73301d674c 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -265,6 +265,10 @@ func (e *Editor) initialize(ctx context.Context) error { "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator", } + params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = []string{ + "declaration", "definition", "readonly", "static", + "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", + } // This is a bit of a hack, since the fake editor doesn't actually support // watching changed files that match a specific glob pattern. However, the From 003fde144ea55295b5c7e9bccc8c09c08ce976ed Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 7 Nov 2022 16:11:05 -0500 Subject: [PATCH 408/723] internal/gcimporter: use nondeprecated go/packages mode bits I meant to do this in the first CL, but was prevented by a bug which I have since reported and linked to from the code. Change-Id: I651e728c535cdeb0885eae4d510fda3c24518dcf Reviewed-on: https://go-review.googlesource.com/c/tools/+/448376 Auto-Submit: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- internal/gcimporter/shallow_test.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/gcimporter/shallow_test.go b/internal/gcimporter/shallow_test.go index 717cb878952..3d8c86a1545 100644 --- a/internal/gcimporter/shallow_test.go +++ b/internal/gcimporter/shallow_test.go @@ -28,7 +28,10 @@ func TestShallowStd(t *testing.T) { // Load import graph of the standard library. // (No parsing or type-checking.) cfg := &packages.Config{ - Mode: packages.LoadImports, + Mode: packages.NeedImports | + packages.NeedName | + packages.NeedFiles | // see https://github.com/golang/go/issues/56632 + packages.NeedCompiledGoFiles, Tests: false, } pkgs, err := packages.Load(cfg, "std") @@ -111,7 +114,6 @@ func typecheck(t *testing.T, ppkg *packages.Package) { insert func(p *types.Package, name string) importMap = make(map[string]*types.Package) // keys are PackagePaths ) - loadFromExportData := func(imp *packages.Package) (*types.Package, error) { data := []byte(imp.ExportFile) return gcimporter.IImportShallow(fset, importMap, data, imp.PkgPath, insert) @@ -125,13 +127,12 @@ func typecheck(t *testing.T, ppkg *packages.Package) { if err != nil { t.Fatalf("unmarshal: %v", err) } - obj := imported.Scope().Lookup(name) - if obj == nil { - t.Fatalf("lookup %q.%s failed", imported.Path(), name) - } if imported != p { t.Fatalf("internal error: inconsistent packages") } + if obj := imported.Scope().Lookup(name); obj == nil { + t.Fatalf("lookup %q.%s failed", imported.Path(), name) + } } cfg := &types.Config{ From 9474ca31d0dfcd484dd82608705ea967a1b9a71d Mon Sep 17 00:00:00 2001 From: Angus Dippenaar Date: Tue, 8 Nov 2022 16:03:14 +0000 Subject: [PATCH 409/723] gopls/doc: clarify `go work use` I felt a bit confused on my first reading of the docs for using `go work`. It wasn't clear to me if the `tools` argument in `go work use tools tools/gopls` was an alias or a directory name, so I thought this might make it very clear to understand for first time users. Change-Id: I9c5a04a8928207b53acfb36ce7add8ca5f033d46 GitHub-Last-Rev: 49e125d83e40f06239f3a24c92f16258a25305c3 GitHub-Pull-Request: golang/tools#409 Reviewed-on: https://go-review.googlesource.com/c/tools/+/441415 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Suzy Mueller Reviewed-by: Robert Findley Run-TryBot: Robert Findley --- gopls/doc/workspace.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gopls/doc/workspace.md b/gopls/doc/workspace.md index cb166131fba..4ff9994f939 100644 --- a/gopls/doc/workspace.md +++ b/gopls/doc/workspace.md @@ -34,12 +34,14 @@ your workspace root to the directory containing the `go.work` file. For example, suppose this repo is checked out into the `$WORK/tools` directory. We can work on both `golang.org/x/tools` and `golang.org/x/tools/gopls` -simultaneously by creating a `go.work` file: +simultaneously by creating a `go.work` file using `go work init`, followed by +`go work use MODULE_DIRECTORIES...` to add directories containing `go.mod` files to the +workspace: -``` +```sh cd $WORK go work init -go work use tools tools/gopls +go work use ./tools/ ./tools/gopls/ ``` ...followed by opening the `$WORK` directory in our editor. From ba92ae171104b8973327d11e03aa9f82de79b886 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 8 Nov 2022 15:37:24 -0500 Subject: [PATCH 410/723] internal/persistent: avoid incorrect map validation due to multiple keys Fix the test failure demonstrated in the following failed builder: https://build.golang.org/log/d0511c583201e8701e72066985ebf950d9f5511d It should be OK to set multiple keys in the validated map. Support this by keeping track of seen and deletion clock time. There are still potential problems with this analysis (specifically, if a map is constructed via SetAll), but we ignore those problems for now. Change-Id: I5940d25f18afe31e13bc71f74d4eea7d737d593d Reviewed-on: https://go-review.googlesource.com/c/tools/+/448696 Reviewed-by: Alan Donovan Auto-Submit: Robert Findley Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- internal/persistent/map_test.go | 67 ++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/internal/persistent/map_test.go b/internal/persistent/map_test.go index 1c413d78fa7..9f89a1d300c 100644 --- a/internal/persistent/map_test.go +++ b/internal/persistent/map_test.go @@ -19,14 +19,15 @@ type mapEntry struct { type validatedMap struct { impl *Map - expected map[int]int - deleted map[mapEntry]struct{} - seen map[mapEntry]struct{} + expected map[int]int // current key-value mapping. + deleted map[mapEntry]int // maps deleted entries to their clock time of last deletion + seen map[mapEntry]int // maps seen entries to their clock time of last insertion + clock int } func TestSimpleMap(t *testing.T) { - deletedEntries := make(map[mapEntry]struct{}) - seenEntries := make(map[mapEntry]struct{}) + deletedEntries := make(map[mapEntry]int) + seenEntries := make(map[mapEntry]int) m1 := &validatedMap{ impl: NewMap(func(a, b interface{}) bool { @@ -43,7 +44,7 @@ func TestSimpleMap(t *testing.T) { validateRef(t, m1, m3) m3.destroy() - assertSameMap(t, deletedEntries, map[mapEntry]struct{}{ + assertSameMap(t, entrySet(deletedEntries), map[mapEntry]struct{}{ {key: 8, value: 8}: {}, }) @@ -59,7 +60,7 @@ func TestSimpleMap(t *testing.T) { m1.set(t, 6, 6) validateRef(t, m1) - assertSameMap(t, deletedEntries, map[mapEntry]struct{}{ + assertSameMap(t, entrySet(deletedEntries), map[mapEntry]struct{}{ {key: 2, value: 2}: {}, {key: 8, value: 8}: {}, }) @@ -98,7 +99,7 @@ func TestSimpleMap(t *testing.T) { m1.destroy() - assertSameMap(t, deletedEntries, map[mapEntry]struct{}{ + assertSameMap(t, entrySet(deletedEntries), map[mapEntry]struct{}{ {key: 2, value: 2}: {}, {key: 6, value: 60}: {}, {key: 8, value: 8}: {}, @@ -114,12 +115,12 @@ func TestSimpleMap(t *testing.T) { m2.destroy() - assertSameMap(t, seenEntries, deletedEntries) + assertSameMap(t, entrySet(seenEntries), entrySet(deletedEntries)) } func TestRandomMap(t *testing.T) { - deletedEntries := make(map[mapEntry]struct{}) - seenEntries := make(map[mapEntry]struct{}) + deletedEntries := make(map[mapEntry]int) + seenEntries := make(map[mapEntry]int) m := &validatedMap{ impl: NewMap(func(a, b interface{}) bool { @@ -132,7 +133,7 @@ func TestRandomMap(t *testing.T) { keys := make([]int, 0, 1000) for i := 0; i < 1000; i++ { - key := rand.Int() + key := rand.Intn(10000) m.set(t, key, key) keys = append(keys, key) @@ -148,12 +149,20 @@ func TestRandomMap(t *testing.T) { } m.destroy() - assertSameMap(t, seenEntries, deletedEntries) + assertSameMap(t, entrySet(seenEntries), entrySet(deletedEntries)) +} + +func entrySet(m map[mapEntry]int) map[mapEntry]struct{} { + set := make(map[mapEntry]struct{}) + for k := range m { + set[k] = struct{}{} + } + return set } func TestUpdate(t *testing.T) { - deletedEntries := make(map[mapEntry]struct{}) - seenEntries := make(map[mapEntry]struct{}) + deletedEntries := make(map[mapEntry]int) + seenEntries := make(map[mapEntry]int) m1 := &validatedMap{ impl: NewMap(func(a, b interface{}) bool { @@ -173,15 +182,7 @@ func TestUpdate(t *testing.T) { m1.destroy() m2.destroy() - assertSameMap(t, seenEntries, deletedEntries) -} - -func (vm *validatedMap) onDelete(t *testing.T, key, value int) { - entry := mapEntry{key: key, value: value} - if _, ok := vm.deleted[entry]; ok { - t.Fatalf("tried to delete entry twice, key: %d, value: %d", key, value) - } - vm.deleted[entry] = struct{}{} + assertSameMap(t, entrySet(seenEntries), entrySet(deletedEntries)) } func validateRef(t *testing.T, maps ...*validatedMap) { @@ -234,9 +235,12 @@ func (vm *validatedMap) validate(t *testing.T) { validateNode(t, vm.impl.root, vm.impl.less) + // Note: this validation may not make sense if maps were constructed using + // SetAll operations. If this proves to be problematic, remove the clock, + // deleted, and seen fields. for key, value := range vm.expected { entry := mapEntry{key: key, value: value} - if _, ok := vm.deleted[entry]; ok { + if deleteAt := vm.deleted[entry]; deleteAt > vm.seen[entry] { t.Fatalf("entry is deleted prematurely, key: %d, value: %d", key, value) } } @@ -281,6 +285,9 @@ func validateNode(t *testing.T, node *mapNode, less func(a, b interface{}) bool) func (vm *validatedMap) setAll(t *testing.T, other *validatedMap) { vm.impl.SetAll(other.impl) + + // Note: this is buggy because we are not updating vm.clock, vm.deleted, or + // vm.seen. for key, value := range other.expected { vm.expected[key] = value } @@ -288,12 +295,17 @@ func (vm *validatedMap) setAll(t *testing.T, other *validatedMap) { } func (vm *validatedMap) set(t *testing.T, key, value int) { - vm.seen[mapEntry{key: key, value: value}] = struct{}{} + entry := mapEntry{key: key, value: value} + + vm.clock++ + vm.seen[entry] = vm.clock + vm.impl.Set(key, value, func(deletedKey, deletedValue interface{}) { if deletedKey != key || deletedValue != value { t.Fatalf("unexpected passed in deleted entry: %v/%v, expected: %v/%v", deletedKey, deletedValue, key, value) } - vm.onDelete(t, key, value) + // Not safe if closure shared between two validatedMaps. + vm.deleted[entry] = vm.clock }) vm.expected[key] = value vm.validate(t) @@ -305,6 +317,7 @@ func (vm *validatedMap) set(t *testing.T, key, value int) { } func (vm *validatedMap) remove(t *testing.T, key int) { + vm.clock++ vm.impl.Delete(key) delete(vm.expected, key) vm.validate(t) From 30574650371be0d7cb887e380ab16905d86cc7cc Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Sun, 6 Nov 2022 01:05:01 +0000 Subject: [PATCH 411/723] gopls/doc: Add plugin for Lapce to gopls documentation Add [Lapce](https://lapce.dev) Go plugin to `gopls` documentation Change-Id: I58ec42d69708b519cfba3de1cdee269ffecdbbc4 GitHub-Last-Rev: 37762df491e6e7a5797606025357fcfed28be56d GitHub-Pull-Request: golang/tools#413 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448235 Auto-Submit: Hyang-Ah Hana Kim Reviewed-by: Hyang-Ah Hana Kim Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot --- gopls/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/gopls/README.md b/gopls/README.md index aefad905144..56d15921a70 100644 --- a/gopls/README.md +++ b/gopls/README.md @@ -22,6 +22,7 @@ To get started with `gopls`, install an LSP plugin in your editor of choice. * [Atom](https://github.com/MordFustang21/ide-gopls) * [Sublime Text](doc/subl.md) * [Acme](https://github.com/fhs/acme-lsp) +* [Lapce](https://github.com/lapce-community/lapce-go) If you use `gopls` with an editor that is not on this list, please send us a CL [updating this documentation](doc/contributing.md). From d41a43b94f2f49adda95493f62c78f57ba91eeec Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Fri, 4 Nov 2022 16:37:35 -0400 Subject: [PATCH 412/723] internal/jsonrpc2_v2: fix a potential deadlock when (*Conn).Close is invoked during Bind This fixes the goroutine leak reported in https://build.golang.org/log/ae36d36843ca240e9e080886417a8798dd4c9618. Fixes golang/go#46047 (hopefully for real this time). Change-Id: I360e54d819849a35284c61d3a0655cc175d81f77 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448095 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Bryan Mills Reviewed-by: Alan Donovan gopls-CI: kokoro --- internal/jsonrpc2_v2/conn.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 085e775a741..60afa7060e4 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -82,6 +82,7 @@ type Connection struct { // Connection. type inFlightState struct { connClosing bool // true when the Connection's Close method has been called + reading bool // true while the readIncoming goroutine is running readErr error // non-nil when the readIncoming goroutine exits (typically io.EOF) writeErr error // non-nil if a call to the Writer has failed with a non-canceled Context @@ -140,14 +141,13 @@ func (c *Connection) updateInFlight(f func(*inFlightState)) { s.closeErr = s.closer.Close() s.closer = nil // prevent duplicate Close calls } - if s.readErr == nil { + if s.reading { // The readIncoming goroutine is still running. Our call to Close should // cause it to exit soon, at which point it will make another call to - // updateInFlight, set s.readErr to a non-nil error, and mark the - // Connection done. + // updateInFlight, set s.reading to false, and mark the Connection done. } else { - // The readIncoming goroutine has exited. Since everything else is idle, - // we're completely done. + // The readIncoming goroutine has exited, or never started to begin with. + // Since everything else is idle, we're completely done. if c.onDone != nil { c.onDone() } @@ -240,10 +240,18 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde reader := framer.Reader(rwc) c.updateInFlight(func(s *inFlightState) { + select { + case <-c.done: + // Bind already closed the connection; don't start a goroutine to read it. + return + default: + } + // The goroutine started here will continue until the underlying stream is closed. // // (If the Binder closed the Connection already, this should error out and // return almost immediately.) + s.reading = true go c.readIncoming(ctx, reader, options.Preempter) }) return c @@ -514,6 +522,7 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter } c.updateInFlight(func(s *inFlightState) { + s.reading = false s.readErr = err // Retire any outgoing requests that were still in flight: with the Reader no From bd04e329aedbea5310658e5d1afbfba4ce700178 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Fri, 4 Nov 2022 16:57:15 -0400 Subject: [PATCH 413/723] internal/jsonrpc2_v2: eliminate a potential Accept/Dial race in TestIdleTimeout The only explanation I can think of for the failure in https://go.dev/issue/49387#issuecomment-1303979877 is that maybe the idle timeout started as soon as conn1 was closed, without waiting for conn2 to be closed. That might be possible if the connection returned by Dial was still in the server's accept queue, but never actually accepted. To eliminate that possibility, we can send an RPC on that connection and wait for a response, as we already do with conn1. Since the conn1 RPC succeeded, we know that the connection is non-idle, conn2 should be accepted, and the request on conn2 should succeed unconditionally. Fixes golang/go#49387 (hopefully for real this time). Change-Id: Ie3e74f91d322223d82c000fdf1f3a0ed08afd20d Reviewed-on: https://go-review.googlesource.com/c/tools/+/448096 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills Reviewed-by: Alan Donovan --- internal/jsonrpc2_v2/serve_test.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/internal/jsonrpc2_v2/serve_test.go b/internal/jsonrpc2_v2/serve_test.go index 21bf0bbd465..88ac66b7e66 100644 --- a/internal/jsonrpc2_v2/serve_test.go +++ b/internal/jsonrpc2_v2/serve_test.go @@ -66,15 +66,25 @@ func TestIdleTimeout(t *testing.T) { return false } + // Since conn1 was successfully accepted and remains open, the server is + // definitely non-idle. Dialing another simultaneous connection should + // succeed. conn2, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) if err != nil { conn1.Close() - if since := time.Since(idleStart); since < d { - t.Fatalf("conn2 failed to connect while non-idle: %v", err) - } - t.Log("jsonrpc2.Dial:", err) + t.Fatalf("conn2 failed to connect while non-idle after %v: %v", time.Since(idleStart), err) return false } + // Ensure that conn2 is also accepted on the server side before we close + // conn1. Otherwise, the connection can appear idle if the server processes + // the closure of conn1 and the idle timeout before it finally notices conn2 + // in the accept queue. + // (That failure mode may explain the failure noted in + // https://go.dev/issue/49387#issuecomment-1303979877.) + ac = conn2.Call(ctx, "ping", nil) + if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { + t.Fatalf("conn2 broken while non-idle after %v: %v", time.Since(idleStart), err) + } if err := conn1.Close(); err != nil { t.Fatalf("conn1.Close failed with error: %v", err) From 502c634771c4ba335286d55fc24eeded1704f592 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Wed, 9 Nov 2022 16:56:38 +0000 Subject: [PATCH 414/723] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Once this CL is submitted, and post-submit testing succeeds on all first-class ports across all supported Go versions, this repository will be tagged with its next minor version. Change-Id: Ie52140f20343bd6dd2b73662fce64c8065f5a80b Reviewed-on: https://go-review.googlesource.com/c/tools/+/449096 Auto-Submit: Gopher Robot gopls-CI: kokoro Reviewed-by: Heschi Kreinick Run-TryBot: Gopher Robot TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- go.mod | 6 +++--- go.sum | 15 +++++++-------- gopls/go.mod | 4 ++-- gopls/go.sum | 8 ++++++-- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 2216421d51a..b46da5396a5 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.18 // tagx:compat 1.16 require ( github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.6.0 - golang.org/x/net v0.1.0 - golang.org/x/sys v0.1.0 + golang.org/x/mod v0.7.0 + golang.org/x/net v0.2.0 + golang.org/x/sys v0.2.0 ) require golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index 50d82dec897..92f0a74888d 100644 --- a/go.sum +++ b/go.sum @@ -2,15 +2,14 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -20,11 +19,11 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/gopls/go.mod b/gopls/go.mod index 23293c769ce..979174c67c5 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -7,9 +7,9 @@ require ( github.com/jba/printsrc v0.2.2 github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 - golang.org/x/mod v0.6.0 + golang.org/x/mod v0.7.0 golang.org/x/sync v0.1.0 - golang.org/x/sys v0.1.0 + golang.org/x/sys v0.2.0 golang.org/x/text v0.4.0 golang.org/x/tools v0.2.0 golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 diff --git a/gopls/go.sum b/gopls/go.sum index 57bf799d67b..9810051995b 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -54,10 +54,12 @@ golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326 h1:fl8k2zg28yA232 golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -69,10 +71,12 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= From d56532ab0ee3bf20db3d642aabc7a43290549c49 Mon Sep 17 00:00:00 2001 From: Cherry Mui Date: Fri, 4 Nov 2022 19:53:19 -0400 Subject: [PATCH 415/723] cmd/compilebench: make it work without installed .a's Currently compilebench relies on installed .a files for std and cmd, as it runs the compiler and linker directly. For the upcoming Go 1.20, compiled .a files will not be installed. Don't rely on them. Instead, build importcfg file and pass it to the compiler and the linker. Verified that this approach still works with previous versions of Go (1.19 and 1.18). For golang/go#47257. Change-Id: Ie0eb9541fb995649e5b68d4481a5acfbdfe8f2a7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448118 Reviewed-by: Michael Pratt gopls-CI: kokoro Reviewed-by: Michael Matloob Reviewed-by: Than McIntosh Run-TryBot: Cherry Mui TryBot-Result: Gopher Robot --- cmd/compilebench/main.go | 77 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/cmd/compilebench/main.go b/cmd/compilebench/main.go index abdf28a2d95..754acdca0e4 100644 --- a/cmd/compilebench/main.go +++ b/cmd/compilebench/main.go @@ -335,10 +335,10 @@ type compile struct{ dir string } func (compile) long() bool { return false } func (c compile) run(name string, count int) error { - // Make sure dependencies needed by go tool compile are installed to GOROOT/pkg. - out, err := exec.Command(*flagGoCmd, "build", "-a", c.dir).CombinedOutput() + // Make sure dependencies needed by go tool compile are built. + out, err := exec.Command(*flagGoCmd, "build", c.dir).CombinedOutput() if err != nil { - return fmt.Errorf("go build -a %s: %v\n%s", c.dir, err, out) + return fmt.Errorf("go build %s: %v\n%s", c.dir, err, out) } // Find dir and source file list. @@ -347,6 +347,11 @@ func (c compile) run(name string, count int) error { return err } + importcfg, err := genImportcfgFile(c.dir, false) + if err != nil { + return err + } + // If this package has assembly files, we'll need to pass a symabis // file to the compiler; call a helper to invoke the assembler // to do that. @@ -371,6 +376,10 @@ func (c compile) run(name string, count int) error { if symAbisFile != "" { args = append(args, "-symabis", symAbisFile) } + if importcfg != "" { + args = append(args, "-importcfg", importcfg) + defer os.Remove(importcfg) + } args = append(args, pkg.GoFiles...) if err := runBuildCmd(name, count, pkg.Dir, compiler, args); err != nil { return err @@ -406,18 +415,28 @@ func (r link) run(name string, count int) error { } // Build dependencies. - out, err := exec.Command(*flagGoCmd, "build", "-a", "-o", "/dev/null", r.dir).CombinedOutput() + out, err := exec.Command(*flagGoCmd, "build", "-o", "/dev/null", r.dir).CombinedOutput() if err != nil { return fmt.Errorf("go build -a %s: %v\n%s", r.dir, err, out) } + importcfg, err := genImportcfgFile(r.dir, true) + if err != nil { + return err + } + defer os.Remove(importcfg) + // Build the main package. pkg, err := goList(r.dir) if err != nil { return err } - args := []string{"-o", "_compilebench_.o"} + args := []string{"-o", "_compilebench_.o", "-importcfg", importcfg} args = append(args, pkg.GoFiles...) + if *flagTrace { + fmt.Fprintf(os.Stderr, "running: %s %+v\n", + compiler, args) + } cmd := exec.Command(compiler, args...) cmd.Dir = pkg.Dir cmd.Stdout = os.Stderr @@ -429,7 +448,7 @@ func (r link) run(name string, count int) error { defer os.Remove(pkg.Dir + "/_compilebench_.o") // Link the main package. - args = []string{"-o", "_compilebench_.exe"} + args = []string{"-o", "_compilebench_.exe", "-importcfg", importcfg} args = append(args, strings.Fields(*flagLinkerFlags)...) args = append(args, strings.Fields(r.flags)...) args = append(args, "_compilebench_.o") @@ -578,3 +597,49 @@ func genSymAbisFile(pkg *Pkg, symAbisFile, incdir string) error { } return nil } + +// genImportcfgFile generates an importcfg file for building package +// dir. Returns the generated importcfg file path (or empty string +// if the package has no dependency). +func genImportcfgFile(dir string, full bool) (string, error) { + need := "{{.Imports}}" + if full { + // for linking, we need transitive dependencies + need = "{{.Deps}}" + } + + // find imported/dependent packages + cmd := exec.Command(*flagGoCmd, "list", "-f", need, dir) + cmd.Stderr = os.Stderr + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("go list -f %s %s: %v", need, dir, err) + } + // trim [ ]\n + if len(out) < 3 || out[0] != '[' || out[len(out)-2] != ']' || out[len(out)-1] != '\n' { + return "", fmt.Errorf("unexpected output from go list -f %s %s: %s", need, dir, out) + } + out = out[1 : len(out)-2] + if len(out) == 0 { + return "", nil + } + + // build importcfg for imported packages + cmd = exec.Command(*flagGoCmd, "list", "-export", "-f", "{{if .Export}}packagefile {{.ImportPath}}={{.Export}}{{end}}") + cmd.Args = append(cmd.Args, strings.Fields(string(out))...) + cmd.Stderr = os.Stderr + out, err = cmd.Output() + if err != nil { + return "", fmt.Errorf("generating importcfg for %s: %s: %v", dir, cmd, err) + } + + f, err := os.CreateTemp("", "importcfg") + if err != nil { + return "", fmt.Errorf("creating tmp importcfg file failed: %v", err) + } + defer f.Close() + if _, err := f.Write(out); err != nil { + return "", fmt.Errorf("writing importcfg file %s failed: %v", f.Name(), err) + } + return f.Name(), nil +} From 6f99366264eeb34bcbb93c55e38f2acfb3358d51 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 10 Nov 2022 15:07:47 -0500 Subject: [PATCH 416/723] gopls/internal/lsp/cache: don't pass snapshot.fset to go/packages ...since we do our own parsing. Change-Id: Id762cca408692b9535b8bb36017d6719180e5bb1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/449498 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Auto-Submit: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/cache/snapshot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index b05f401c52a..d6599b1cee5 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -345,7 +345,7 @@ func (s *snapshot) config(ctx context.Context, inv *gocommand.Invocation) *packa packages.NeedModule | packages.LoadMode(packagesinternal.DepsErrors) | packages.LoadMode(packagesinternal.ForTest), - Fset: s.FileSet(), + Fset: nil, // we do our own parsing Overlay: s.buildOverlay(), ParseFile: func(*token.FileSet, string, []byte) (*ast.File, error) { panic("go/packages must not be used to parse files") From d66e9b4ac82f12134bdb56ca1bcbb8bd4c90255f Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 10 Nov 2022 11:30:47 -0500 Subject: [PATCH 417/723] internal/typesinternal: update go/types error codes for 1.20 Change-Id: I4b1d4a7769e90801856cfa15e860b899a3bf62dc Reviewed-on: https://go-review.googlesource.com/c/tools/+/449496 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Griesemer Run-TryBot: Robert Findley --- internal/typesinternal/errorcode.go | 38 +++++++- internal/typesinternal/errorcode_string.go | 26 +++-- internal/typesinternal/errorcode_test.go | 105 +++++++++++++++++++++ 3 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 internal/typesinternal/errorcode_test.go diff --git a/internal/typesinternal/errorcode.go b/internal/typesinternal/errorcode.go index d38ee3c27cd..07484073a57 100644 --- a/internal/typesinternal/errorcode.go +++ b/internal/typesinternal/errorcode.go @@ -30,6 +30,12 @@ type ErrorCode int // convention that "bad" implies a problem with syntax, and "invalid" implies a // problem with types. +const ( + // InvalidSyntaxTree occurs if an invalid syntax tree is provided + // to the type checker. It should never happen. + InvalidSyntaxTree ErrorCode = -1 +) + const ( _ ErrorCode = iota @@ -153,12 +159,12 @@ const ( /* decls > var (+ other variable assignment codes) */ - // UntypedNil occurs when the predeclared (untyped) value nil is used to + // UntypedNilUse occurs when the predeclared (untyped) value nil is used to // initialize a variable declared without an explicit type. // // Example: // var x = nil - UntypedNil + UntypedNilUse // WrongAssignCount occurs when the number of values on the right-hand side // of an assignment or or initialization expression does not match the number @@ -1523,4 +1529,32 @@ const ( // Example: // type T[P any] struct{ *P } MisplacedTypeParam + + // InvalidUnsafeSliceData occurs when unsafe.SliceData is called with + // an argument that is not of slice type. It also occurs if it is used + // in a package compiled for a language version before go1.20. + // + // Example: + // import "unsafe" + // + // var x int + // var _ = unsafe.SliceData(x) + InvalidUnsafeSliceData + + // InvalidUnsafeString occurs when unsafe.String is called with + // a length argument that is not of integer type, negative, or + // out of bounds. It also occurs if it is used in a package + // compiled for a language version before go1.20. + // + // Example: + // import "unsafe" + // + // var b [10]byte + // var _ = unsafe.String(&b[0], -1) + InvalidUnsafeString + + // InvalidUnsafeStringData occurs if it is used in a package + // compiled for a language version before go1.20. + _ // not used anymore + ) diff --git a/internal/typesinternal/errorcode_string.go b/internal/typesinternal/errorcode_string.go index de90e9515ae..15ecf7c5ded 100644 --- a/internal/typesinternal/errorcode_string.go +++ b/internal/typesinternal/errorcode_string.go @@ -8,6 +8,7 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} + _ = x[InvalidSyntaxTree - -1] _ = x[Test-1] _ = x[BlankPkgName-2] _ = x[MismatchedPkgName-3] @@ -23,7 +24,7 @@ func _() { _ = x[InvalidConstInit-13] _ = x[InvalidConstVal-14] _ = x[InvalidConstType-15] - _ = x[UntypedNil-16] + _ = x[UntypedNilUse-16] _ = x[WrongAssignCount-17] _ = x[UnassignableOperand-18] _ = x[NoNewVar-19] @@ -152,16 +153,27 @@ func _() { _ = x[MisplacedConstraintIface-142] _ = x[InvalidMethodTypeParams-143] _ = x[MisplacedTypeParam-144] + _ = x[InvalidUnsafeSliceData-145] + _ = x[InvalidUnsafeString-146] } -const _ErrorCode_name = "TestBlankPkgNameMismatchedPkgNameInvalidPkgUseBadImportPathBrokenImportImportCRenamedUnusedImportInvalidInitCycleDuplicateDeclInvalidDeclCycleInvalidTypeCycleInvalidConstInitInvalidConstValInvalidConstTypeUntypedNilWrongAssignCountUnassignableOperandNoNewVarMultiValAssignOpInvalidIfaceAssignInvalidChanAssignIncompatibleAssignUnaddressableFieldAssignNotATypeInvalidArrayLenBlankIfaceMethodIncomparableMapKeyInvalidIfaceEmbedInvalidPtrEmbedBadRecvInvalidRecvDuplicateFieldAndMethodDuplicateMethodInvalidBlankInvalidIotaMissingInitBodyInvalidInitSigInvalidInitDeclInvalidMainDeclTooManyValuesNotAnExprTruncatedFloatNumericOverflowUndefinedOpMismatchedTypesDivByZeroNonNumericIncDecUnaddressableOperandInvalidIndirectionNonIndexableOperandInvalidIndexSwappedSliceIndicesNonSliceableOperandInvalidSliceExprInvalidShiftCountInvalidShiftOperandInvalidReceiveInvalidSendDuplicateLitKeyMissingLitKeyInvalidLitIndexOversizeArrayLitMixedStructLitInvalidStructLitMissingLitFieldDuplicateLitFieldUnexportedLitFieldInvalidLitFieldUntypedLitInvalidLitAmbiguousSelectorUndeclaredImportedNameUnexportedNameUndeclaredNameMissingFieldOrMethodBadDotDotDotSyntaxNonVariadicDotDotDotMisplacedDotDotDotInvalidDotDotDotOperandInvalidDotDotDotUncalledBuiltinInvalidAppendInvalidCapInvalidCloseInvalidCopyInvalidComplexInvalidDeleteInvalidImagInvalidLenSwappedMakeArgsInvalidMakeInvalidRealInvalidAssertImpossibleAssertInvalidConversionInvalidUntypedConversionBadOffsetofSyntaxInvalidOffsetofUnusedExprUnusedVarMissingReturnWrongResultCountOutOfScopeResultInvalidCondInvalidPostDeclInvalidChanRangeInvalidIterVarInvalidRangeExprMisplacedBreakMisplacedContinueMisplacedFallthroughDuplicateCaseDuplicateDefaultBadTypeKeywordInvalidTypeSwitchInvalidExprSwitchInvalidSelectCaseUndeclaredLabelDuplicateLabelMisplacedLabelUnusedLabelJumpOverDeclJumpIntoBlockInvalidMethodExprWrongArgCountInvalidCallUnusedResultsInvalidDeferInvalidGoBadDeclRepeatedDeclInvalidUnsafeAddInvalidUnsafeSliceUnsupportedFeatureNotAGenericTypeWrongTypeArgCountCannotInferTypeArgsInvalidTypeArgInvalidInstanceCycleInvalidUnionMisplacedConstraintIfaceInvalidMethodTypeParamsMisplacedTypeParam" +const ( + _ErrorCode_name_0 = "InvalidSyntaxTree" + _ErrorCode_name_1 = "TestBlankPkgNameMismatchedPkgNameInvalidPkgUseBadImportPathBrokenImportImportCRenamedUnusedImportInvalidInitCycleDuplicateDeclInvalidDeclCycleInvalidTypeCycleInvalidConstInitInvalidConstValInvalidConstTypeUntypedNilUseWrongAssignCountUnassignableOperandNoNewVarMultiValAssignOpInvalidIfaceAssignInvalidChanAssignIncompatibleAssignUnaddressableFieldAssignNotATypeInvalidArrayLenBlankIfaceMethodIncomparableMapKeyInvalidIfaceEmbedInvalidPtrEmbedBadRecvInvalidRecvDuplicateFieldAndMethodDuplicateMethodInvalidBlankInvalidIotaMissingInitBodyInvalidInitSigInvalidInitDeclInvalidMainDeclTooManyValuesNotAnExprTruncatedFloatNumericOverflowUndefinedOpMismatchedTypesDivByZeroNonNumericIncDecUnaddressableOperandInvalidIndirectionNonIndexableOperandInvalidIndexSwappedSliceIndicesNonSliceableOperandInvalidSliceExprInvalidShiftCountInvalidShiftOperandInvalidReceiveInvalidSendDuplicateLitKeyMissingLitKeyInvalidLitIndexOversizeArrayLitMixedStructLitInvalidStructLitMissingLitFieldDuplicateLitFieldUnexportedLitFieldInvalidLitFieldUntypedLitInvalidLitAmbiguousSelectorUndeclaredImportedNameUnexportedNameUndeclaredNameMissingFieldOrMethodBadDotDotDotSyntaxNonVariadicDotDotDotMisplacedDotDotDotInvalidDotDotDotOperandInvalidDotDotDotUncalledBuiltinInvalidAppendInvalidCapInvalidCloseInvalidCopyInvalidComplexInvalidDeleteInvalidImagInvalidLenSwappedMakeArgsInvalidMakeInvalidRealInvalidAssertImpossibleAssertInvalidConversionInvalidUntypedConversionBadOffsetofSyntaxInvalidOffsetofUnusedExprUnusedVarMissingReturnWrongResultCountOutOfScopeResultInvalidCondInvalidPostDeclInvalidChanRangeInvalidIterVarInvalidRangeExprMisplacedBreakMisplacedContinueMisplacedFallthroughDuplicateCaseDuplicateDefaultBadTypeKeywordInvalidTypeSwitchInvalidExprSwitchInvalidSelectCaseUndeclaredLabelDuplicateLabelMisplacedLabelUnusedLabelJumpOverDeclJumpIntoBlockInvalidMethodExprWrongArgCountInvalidCallUnusedResultsInvalidDeferInvalidGoBadDeclRepeatedDeclInvalidUnsafeAddInvalidUnsafeSliceUnsupportedFeatureNotAGenericTypeWrongTypeArgCountCannotInferTypeArgsInvalidTypeArgInvalidInstanceCycleInvalidUnionMisplacedConstraintIfaceInvalidMethodTypeParamsMisplacedTypeParamInvalidUnsafeSliceDataInvalidUnsafeString" +) -var _ErrorCode_index = [...]uint16{0, 4, 16, 33, 46, 59, 71, 85, 97, 113, 126, 142, 158, 174, 189, 205, 215, 231, 250, 258, 274, 292, 309, 327, 351, 359, 374, 390, 408, 425, 440, 447, 458, 481, 496, 508, 519, 534, 548, 563, 578, 591, 600, 614, 629, 640, 655, 664, 680, 700, 718, 737, 749, 768, 787, 803, 820, 839, 853, 864, 879, 892, 907, 923, 937, 953, 968, 985, 1003, 1018, 1028, 1038, 1055, 1077, 1091, 1105, 1125, 1143, 1163, 1181, 1204, 1220, 1235, 1248, 1258, 1270, 1281, 1295, 1308, 1319, 1329, 1344, 1355, 1366, 1379, 1395, 1412, 1436, 1453, 1468, 1478, 1487, 1500, 1516, 1532, 1543, 1558, 1574, 1588, 1604, 1618, 1635, 1655, 1668, 1684, 1698, 1715, 1732, 1749, 1764, 1778, 1792, 1803, 1815, 1828, 1845, 1858, 1869, 1882, 1894, 1903, 1910, 1922, 1938, 1956, 1974, 1989, 2006, 2025, 2039, 2059, 2071, 2095, 2118, 2136} +var ( + _ErrorCode_index_1 = [...]uint16{0, 4, 16, 33, 46, 59, 71, 85, 97, 113, 126, 142, 158, 174, 189, 205, 218, 234, 253, 261, 277, 295, 312, 330, 354, 362, 377, 393, 411, 428, 443, 450, 461, 484, 499, 511, 522, 537, 551, 566, 581, 594, 603, 617, 632, 643, 658, 667, 683, 703, 721, 740, 752, 771, 790, 806, 823, 842, 856, 867, 882, 895, 910, 926, 940, 956, 971, 988, 1006, 1021, 1031, 1041, 1058, 1080, 1094, 1108, 1128, 1146, 1166, 1184, 1207, 1223, 1238, 1251, 1261, 1273, 1284, 1298, 1311, 1322, 1332, 1347, 1358, 1369, 1382, 1398, 1415, 1439, 1456, 1471, 1481, 1490, 1503, 1519, 1535, 1546, 1561, 1577, 1591, 1607, 1621, 1638, 1658, 1671, 1687, 1701, 1718, 1735, 1752, 1767, 1781, 1795, 1806, 1818, 1831, 1848, 1861, 1872, 1885, 1897, 1906, 1913, 1925, 1941, 1959, 1977, 1992, 2009, 2028, 2042, 2062, 2074, 2098, 2121, 2139, 2161, 2180} +) func (i ErrorCode) String() string { - i -= 1 - if i < 0 || i >= ErrorCode(len(_ErrorCode_index)-1) { - return "ErrorCode(" + strconv.FormatInt(int64(i+1), 10) + ")" + switch { + case i == -1: + return _ErrorCode_name_0 + case 1 <= i && i <= 146: + i -= 1 + return _ErrorCode_name_1[_ErrorCode_index_1[i]:_ErrorCode_index_1[i+1]] + default: + return "ErrorCode(" + strconv.FormatInt(int64(i), 10) + ")" } - return _ErrorCode_name[_ErrorCode_index[i]:_ErrorCode_index[i+1]] } diff --git a/internal/typesinternal/errorcode_test.go b/internal/typesinternal/errorcode_test.go new file mode 100644 index 00000000000..63d13f19eae --- /dev/null +++ b/internal/typesinternal/errorcode_test.go @@ -0,0 +1,105 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package typesinternal_test + +import ( + "fmt" + "go/ast" + "go/constant" + "go/parser" + "go/token" + "go/types" + "path/filepath" + "runtime" + "sort" + "strings" + "testing" +) + +func TestErrorCodes(t *testing.T) { + t.Skip("unskip this test to verify the correctness of errorcode.go for the current Go version") + + // For older go versions, this file was src/go/types/errorcodes.go. + stdPath := filepath.Join(runtime.GOROOT(), "src", "internal", "types", "errors", "codes.go") + stdCodes, err := loadCodes(stdPath) + if err != nil { + t.Fatalf("loading std codes: %v", err) + } + + localPath := "errorcode.go" + localCodes, err := loadCodes(localPath) + if err != nil { + t.Fatalf("loading local codes: %v", err) + } + + // Verify that all std codes are present, with the correct value. + type codeVal struct { + Name string + Value int64 + } + var byValue []codeVal + for k, v := range stdCodes { + byValue = append(byValue, codeVal{k, v}) + } + sort.Slice(byValue, func(i, j int) bool { + return byValue[i].Value < byValue[j].Value + }) + + localLookup := make(map[int64]string) + for k, v := range localCodes { + if _, ok := localLookup[v]; ok { + t.Errorf("duplicate error code value %d", v) + } + localLookup[v] = k + } + + for _, std := range byValue { + local, ok := localCodes[std.Name] + if !ok { + if v, ok := localLookup[std.Value]; ok { + t.Errorf("Missing code for %s (code %d is %s)", std.Name, std.Value, v) + } else { + t.Errorf("Missing code for %s", std.Name) + } + } + if local != std.Value { + t.Errorf("Mismatching value for %s: got %d, but stdlib has %d", std.Name, local, std.Value) + } + } +} + +// loadCodes loads all constant values found in filepath. +// +// The given file must type-check cleanly as a standalone file. +func loadCodes(filepath string) (map[string]int64, error) { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filepath, nil, 0) + if err != nil { + return nil, err + } + var config types.Config + pkg, err := config.Check("p", fset, []*ast.File{f}, nil) + if err != nil { + return nil, err + } + + codes := make(map[string]int64) + for _, name := range pkg.Scope().Names() { + obj := pkg.Scope().Lookup(name) + c, ok := obj.(*types.Const) + if !ok { + continue + } + name := strings.TrimPrefix(name, "_") // compatibility with earlier go versions + codes[name], ok = constant.Int64Val(c.Val()) + if !ok { + return nil, fmt.Errorf("non integral value %v for %s", c.Val(), name) + } + } + if len(codes) < 100 { + return nil, fmt.Errorf("sanity check: got %d codes but expected at least 100", len(codes)) + } + return codes, nil +} From bc08991678f9f60b920f053f72129878f0791805 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 9 Nov 2022 06:57:43 -0500 Subject: [PATCH 418/723] gopls: sync golang.org/x/vuln@3af8368ee4fe golang.org/x/vuln/exp/govulncheck API was significantly changed. The previous Main is gone, and we need to use Source. The returned data types changed significantly too. Change-Id: Ic702ffe9a94a8ddd1867a0f2766bb49e2133d3a3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448975 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim --- gopls/go.mod | 4 +- gopls/go.sum | 5 +- gopls/internal/govulncheck/cache.go | 145 ------------------ gopls/internal/govulncheck/cache_test.go | 165 --------------------- gopls/internal/govulncheck/copy.sh | 32 ---- gopls/internal/govulncheck/summary.go | 46 ------ gopls/internal/govulncheck/types_118.go | 44 ++++++ gopls/internal/govulncheck/types_not118.go | 133 +++++++++++++++++ gopls/internal/lsp/cache/session.go | 2 +- gopls/internal/lsp/cache/view.go | 8 +- gopls/internal/lsp/cmd/vulncheck.go | 8 +- gopls/internal/lsp/command.go | 27 ++-- gopls/internal/lsp/mod/diagnostics.go | 41 +++-- gopls/internal/lsp/mod/hover.go | 64 ++++---- gopls/internal/lsp/source/view.go | 4 +- gopls/internal/vulncheck/command.go | 36 +++-- gopls/internal/vulncheck/vulncheck.go | 2 +- 17 files changed, 289 insertions(+), 477 deletions(-) delete mode 100644 gopls/internal/govulncheck/cache.go delete mode 100644 gopls/internal/govulncheck/cache_test.go delete mode 100755 gopls/internal/govulncheck/copy.sh delete mode 100644 gopls/internal/govulncheck/summary.go create mode 100644 gopls/internal/govulncheck/types_118.go create mode 100644 gopls/internal/govulncheck/types_not118.go diff --git a/gopls/go.mod b/gopls/go.mod index 979174c67c5..a36bccdea18 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -11,8 +11,8 @@ require ( golang.org/x/sync v0.1.0 golang.org/x/sys v0.2.0 golang.org/x/text v0.4.0 - golang.org/x/tools v0.2.0 - golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 + golang.org/x/tools v0.2.1-0.20221108172846-9474ca31d0df + golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.3.3 mvdan.cc/gofumpt v0.4.0 diff --git a/gopls/go.sum b/gopls/go.sum index 9810051995b..ccc7756fb9c 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -61,7 +61,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -80,8 +79,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 h1:KaYZQUtEEaV8aVADIHAuYBTjo77aUcCvC7KTGKM3J1I= -golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68= +golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe h1:qptQiQwEpETwDiz85LKtChqif9xhVkAm8Nhxs0xnTww= +golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe/go.mod h1:8nFLBv8KFyZ2VuczUYssYKh+fcBR3BuXDG/HIWcxlwM= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gopls/internal/govulncheck/cache.go b/gopls/internal/govulncheck/cache.go deleted file mode 100644 index 2fa6a05d727..00000000000 --- a/gopls/internal/govulncheck/cache.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -// Package govulncheck supports the govulncheck command. -package govulncheck - -import ( - "encoding/json" - "go/build" - "os" - "path/filepath" - "sync" - "time" - - "golang.org/x/vuln/client" - "golang.org/x/vuln/osv" -) - -// The cache uses a single JSON index file for each vulnerability database -// which contains the map from packages to the time the last -// vulnerability for that package was added/modified and the time that -// the index was retrieved from the vulnerability database. The JSON -// format is as follows: -// -// $GOPATH/pkg/mod/cache/download/vulndb/{db hostname}/indexes/index.json -// { -// Retrieved time.Time -// Index client.DBIndex -// } -// -// Each package also has a JSON file which contains the array of vulnerability -// entries for the package. The JSON format is as follows: -// -// $GOPATH/pkg/mod/cache/download/vulndb/{db hostname}/{import path}/vulns.json -// []*osv.Entry - -// FSCache is a thread-safe file-system cache implementing osv.Cache -// -// TODO: use something like cmd/go/internal/lockedfile for thread safety? -type FSCache struct { - mu sync.Mutex - rootDir string -} - -// Assert that *FSCache implements client.Cache. -var _ client.Cache = (*FSCache)(nil) - -// use cfg.GOMODCACHE available in cmd/go/internal? -var defaultCacheRoot = filepath.Join(build.Default.GOPATH, "/pkg/mod/cache/download/vulndb") - -func DefaultCache() *FSCache { - return &FSCache{rootDir: defaultCacheRoot} -} - -type cachedIndex struct { - Retrieved time.Time - Index client.DBIndex -} - -func (c *FSCache) ReadIndex(dbName string) (client.DBIndex, time.Time, error) { - c.mu.Lock() - defer c.mu.Unlock() - - b, err := os.ReadFile(filepath.Join(c.rootDir, dbName, "index.json")) - if err != nil { - if os.IsNotExist(err) { - return nil, time.Time{}, nil - } - return nil, time.Time{}, err - } - var index cachedIndex - if err := json.Unmarshal(b, &index); err != nil { - return nil, time.Time{}, err - } - return index.Index, index.Retrieved, nil -} - -func (c *FSCache) WriteIndex(dbName string, index client.DBIndex, retrieved time.Time) error { - c.mu.Lock() - defer c.mu.Unlock() - - path := filepath.Join(c.rootDir, dbName) - if err := os.MkdirAll(path, 0755); err != nil { - return err - } - j, err := json.Marshal(cachedIndex{ - Index: index, - Retrieved: retrieved, - }) - if err != nil { - return err - } - if err := os.WriteFile(filepath.Join(path, "index.json"), j, 0666); err != nil { - return err - } - return nil -} - -func (c *FSCache) ReadEntries(dbName string, p string) ([]*osv.Entry, error) { - c.mu.Lock() - defer c.mu.Unlock() - - ep, err := client.EscapeModulePath(p) - if err != nil { - return nil, err - } - b, err := os.ReadFile(filepath.Join(c.rootDir, dbName, ep, "vulns.json")) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, err - } - var entries []*osv.Entry - if err := json.Unmarshal(b, &entries); err != nil { - return nil, err - } - return entries, nil -} - -func (c *FSCache) WriteEntries(dbName string, p string, entries []*osv.Entry) error { - c.mu.Lock() - defer c.mu.Unlock() - - ep, err := client.EscapeModulePath(p) - if err != nil { - return err - } - path := filepath.Join(c.rootDir, dbName, ep) - if err := os.MkdirAll(path, 0777); err != nil { - return err - } - j, err := json.Marshal(entries) - if err != nil { - return err - } - if err := os.WriteFile(filepath.Join(path, "vulns.json"), j, 0666); err != nil { - return err - } - return nil -} diff --git a/gopls/internal/govulncheck/cache_test.go b/gopls/internal/govulncheck/cache_test.go deleted file mode 100644 index 57e87659046..00000000000 --- a/gopls/internal/govulncheck/cache_test.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -package govulncheck - -import ( - "fmt" - "os" - "path/filepath" - "reflect" - "testing" - "time" - - "golang.org/x/sync/errgroup" - "golang.org/x/vuln/client" - "golang.org/x/vuln/osv" -) - -func TestCache(t *testing.T) { - tmpDir := t.TempDir() - - cache := &FSCache{rootDir: tmpDir} - dbName := "vulndb.golang.org" - - _, _, err := cache.ReadIndex(dbName) - if err != nil { - t.Fatalf("ReadIndex failed for non-existent database: %v", err) - } - - if err = os.Mkdir(filepath.Join(tmpDir, dbName), 0777); err != nil { - t.Fatalf("os.Mkdir failed: %v", err) - } - _, _, err = cache.ReadIndex(dbName) - if err != nil { - t.Fatalf("ReadIndex failed for database without cached index: %v", err) - } - - now := time.Now() - expectedIdx := client.DBIndex{ - "a.vuln.example.com": time.Time{}.Add(time.Hour), - "b.vuln.example.com": time.Time{}.Add(time.Hour * 2), - "c.vuln.example.com": time.Time{}.Add(time.Hour * 3), - } - if err = cache.WriteIndex(dbName, expectedIdx, now); err != nil { - t.Fatalf("WriteIndex failed to write index: %v", err) - } - - idx, retrieved, err := cache.ReadIndex(dbName) - if err != nil { - t.Fatalf("ReadIndex failed for database with cached index: %v", err) - } - if !reflect.DeepEqual(idx, expectedIdx) { - t.Errorf("ReadIndex returned unexpected index, got:\n%s\nwant:\n%s", idx, expectedIdx) - } - if !retrieved.Equal(now) { - t.Errorf("ReadIndex returned unexpected retrieved: got %s, want %s", retrieved, now) - } - - if _, err = cache.ReadEntries(dbName, "vuln.example.com"); err != nil { - t.Fatalf("ReadEntires failed for non-existent package: %v", err) - } - - expectedEntries := []*osv.Entry{ - {ID: "001"}, - {ID: "002"}, - {ID: "003"}, - } - if err := cache.WriteEntries(dbName, "vuln.example.com", expectedEntries); err != nil { - t.Fatalf("WriteEntries failed: %v", err) - } - - entries, err := cache.ReadEntries(dbName, "vuln.example.com") - if err != nil { - t.Fatalf("ReadEntries failed for cached package: %v", err) - } - if !reflect.DeepEqual(entries, expectedEntries) { - t.Errorf("ReadEntries returned unexpected entries, got:\n%v\nwant:\n%v", entries, expectedEntries) - } -} - -func TestConcurrency(t *testing.T) { - tmpDir := t.TempDir() - - cache := &FSCache{rootDir: tmpDir} - dbName := "vulndb.golang.org" - - g := new(errgroup.Group) - for i := 0; i < 1000; i++ { - i := i - g.Go(func() error { - id := i % 5 - p := fmt.Sprintf("example.com/package%d", id) - - entries, err := cache.ReadEntries(dbName, p) - if err != nil { - return err - } - - err = cache.WriteEntries(dbName, p, append(entries, &osv.Entry{ID: fmt.Sprint(id)})) - if err != nil { - return err - } - return nil - }) - } - - if err := g.Wait(); err != nil { - t.Errorf("error in parallel cache entries read/write: %v", err) - } - - // sanity checking - for i := 0; i < 5; i++ { - id := fmt.Sprint(i) - p := fmt.Sprintf("example.com/package%s", id) - - es, err := cache.ReadEntries(dbName, p) - if err != nil { - t.Fatalf("failed to read entries: %v", err) - } - for _, e := range es { - if e.ID != id { - t.Errorf("want %s ID for vuln entry; got %s", id, e.ID) - } - } - } - - // do similar for cache index - start := time.Now() - for i := 0; i < 1000; i++ { - i := i - g.Go(func() error { - id := i % 5 - p := fmt.Sprintf("package%v", id) - - idx, _, err := cache.ReadIndex(dbName) - if err != nil { - return err - } - - if idx == nil { - idx = client.DBIndex{} - } - - // sanity checking - if rt, ok := idx[p]; ok && rt.Before(start) { - return fmt.Errorf("unexpected past time in index: %v before start %v", rt, start) - } - - now := time.Now() - idx[p] = now - if err := cache.WriteIndex(dbName, idx, now); err != nil { - return err - } - return nil - }) - } - - if err := g.Wait(); err != nil { - t.Errorf("error in parallel cache index read/write: %v", err) - } -} diff --git a/gopls/internal/govulncheck/copy.sh b/gopls/internal/govulncheck/copy.sh deleted file mode 100755 index 398cde2f466..00000000000 --- a/gopls/internal/govulncheck/copy.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -eu - -# 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. - -set -o pipefail - -# Copy golang.org/x/vuln/cmd/govulncheck/internal/govulncheck into this directory. -# Assume the x/vuln repo is a sibling of the tools repo. - -rm -f *.go -cp ../../../../vuln/cmd/govulncheck/internal/govulncheck/*.go . - -sed -i '' 's/\"golang.org\/x\/vuln\/internal\/semver\"/\"golang.org\/x\/tools\/gopls\/internal\/govulncheck\/semver\"/g' *.go -sed -i '' -e '4 i\ -' -e '4 i\ -//go:build go1.18' -e '4 i\ -// +build go1.18' *.go - -# Copy golang.org/x/vuln/internal/semver that -# golang.org/x/vuln/cmd/govulncheck/internal/govulncheck -# depends on. - -mkdir -p semver -cd semver -rm -f *.go -cp ../../../../../vuln/internal/semver/*.go . -sed -i '' -e '4 i\ -' -e '4 i\ -//go:build go1.18' -e '4 i\ -// +build go1.18' *.go diff --git a/gopls/internal/govulncheck/summary.go b/gopls/internal/govulncheck/summary.go deleted file mode 100644 index e389b89a4ad..00000000000 --- a/gopls/internal/govulncheck/summary.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package govulncheck - -import "golang.org/x/vuln/osv" - -// TODO(hyangah): Find a better package for these types -// unless golang.org/x/vuln/exp/govulncheck starts to export these. - -// Summary is the govulncheck result. -type Summary struct { - // Vulnerabilities affecting the analysis target binary or source code. - Affecting []Vuln - // Vulnerabilities that may be imported but the vulnerable symbols are - // not called. For binary analysis, this will be always empty. - NonAffecting []Vuln -} - -// Vuln represents a vulnerability relevant to a (module, package). -type Vuln struct { - OSV *osv.Entry - PkgPath string // Package path. - ModPath string // Module path. - FoundIn string // @ if we know when it was introduced. Empty otherwise. - FixedIn string // @ if fix is available. Empty otherwise. - // Trace contains a call stack for each affecting symbol. - // For vulnerabilities found from binary analysis, and vulnerabilities - // that are reported as Unaffecting ones, this will be always empty. - Trace []Trace -} - -// Trace represents a sample trace for a vulnerable symbol. -type Trace struct { - Symbol string // Name of the detected vulnerable function or method. - Desc string // One-line description of the callstack. - Stack []StackEntry // Call stack. - Seen int // Number of similar call stacks. -} - -// StackEntry represents a call stack entry. -type StackEntry struct { - FuncName string // Function name is the function name, adjusted to remove pointer annotation. - CallSite string // Position of the call/reference site. It is one of the formats token.Pos.String() returns or empty if unknown. -} diff --git a/gopls/internal/govulncheck/types_118.go b/gopls/internal/govulncheck/types_118.go new file mode 100644 index 00000000000..74109631950 --- /dev/null +++ b/gopls/internal/govulncheck/types_118.go @@ -0,0 +1,44 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +// Package govulncheck provides an experimental govulncheck API. +package govulncheck + +import "golang.org/x/vuln/exp/govulncheck" + +var ( + // Source reports vulnerabilities that affect the analyzed packages. + Source = govulncheck.Source + + // DefaultCache constructs cache for a vulnerability database client. + DefaultCache = govulncheck.DefaultCache +) + +type ( + // Config is the configuration for Main. + Config = govulncheck.Config + + // Result is the result of executing Source. + Result = govulncheck.Result + + // Vuln represents a single OSV entry. + Vuln = govulncheck.Vuln + + // Module represents a specific vulnerability relevant to a + // single module or package. + Module = govulncheck.Module + + // Package is a Go package with known vulnerable symbols. + Package = govulncheck.Package + + // CallStacks contains a representative call stack for each + // vulnerable symbol that is called. + CallStack = govulncheck.CallStack + + // StackFrame represents a call stack entry. + StackFrame = govulncheck.StackFrame +) diff --git a/gopls/internal/govulncheck/types_not118.go b/gopls/internal/govulncheck/types_not118.go new file mode 100644 index 00000000000..ae92caa5eac --- /dev/null +++ b/gopls/internal/govulncheck/types_not118.go @@ -0,0 +1,133 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.18 +// +build !go1.18 + +package govulncheck + +import ( + "go/token" + + "golang.org/x/vuln/osv" +) + +// Result is the result of executing Source or Binary. +type Result struct { + // Vulns contains all vulnerabilities that are called or imported by + // the analyzed module. + Vulns []*Vuln +} + +// Vuln represents a single OSV entry. +type Vuln struct { + // OSV contains all data from the OSV entry for this vulnerability. + OSV *osv.Entry + + // Modules contains all of the modules in the OSV entry where a + // vulnerable package is imported by the target source code or binary. + // + // For example, a module M with two packages M/p1 and M/p2, where only p1 + // is vulnerable, will appear in this list if and only if p1 is imported by + // the target source code or binary. + Modules []*Module +} + +func (v *Vuln) IsCalled() bool { + return false +} + +// Module represents a specific vulnerability relevant to a single module. +type Module struct { + // Path is the module path of the module containing the vulnerability. + // + // Importable packages in the standard library will have the path "stdlib". + Path string + + // FoundVersion is the module version where the vulnerability was found. + FoundVersion string + + // FixedVersion is the module version where the vulnerability was + // fixed. If there are multiple fixed versions in the OSV report, this will + // be the latest fixed version. + // + // This is empty if a fix is not available. + FixedVersion string + + // Packages contains all the vulnerable packages in OSV entry that are + // imported by the target source code or binary. + // + // For example, given a module M with two packages M/p1 and M/p2, where + // both p1 and p2 are vulnerable, p1 and p2 will each only appear in this + // list they are individually imported by the target source code or binary. + Packages []*Package +} + +// Package is a Go package with known vulnerable symbols. +type Package struct { + // Path is the import path of the package containing the vulnerability. + Path string + + // CallStacks contains a representative call stack for each + // vulnerable symbol that is called. + // + // For vulnerabilities found from binary analysis, only CallStack.Symbol + // will be provided. + // + // For non-affecting vulnerabilities reported from the source mode + // analysis, this will be empty. + CallStacks []CallStack +} + +// CallStacks contains a representative call stack for a vulnerable +// symbol. +type CallStack struct { + // Symbol is the name of the detected vulnerable function + // or method. + // + // This follows the naming convention in the OSV report. + Symbol string + + // Summary is a one-line description of the callstack, used by the + // default govulncheck mode. + // + // Example: module3.main calls github.com/shiyanhui/dht.DHT.Run + Summary string + + // Frames contains an entry for each stack in the call stack. + // + // Frames are sorted starting from the entry point to the + // imported vulnerable symbol. The last frame in Frames should match + // Symbol. + Frames []*StackFrame +} + +// StackFrame represents a call stack entry. +type StackFrame struct { + // PackagePath is the import path. + PkgPath string + + // FuncName is the function name. + FuncName string + + // RecvType is the fully qualified receiver type, + // if the called symbol is a method. + // + // The client can create the final symbol name by + // prepending RecvType to FuncName. + RecvType string + + // Position describes an arbitrary source position + // including the file, line, and column location. + // A Position is valid if the line number is > 0. + Position token.Position +} + +func (sf *StackFrame) Name() string { + return "" +} + +func (sf *StackFrame) Pos() string { + return "" +} diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 57bba47df80..f2651a75a89 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -239,7 +239,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, name: name, folder: folder, moduleUpgrades: map[span.URI]map[string]string{}, - vulns: map[span.URI][]govulncheck.Vuln{}, + vulns: map[span.URI][]*govulncheck.Vuln{}, filesByURI: map[span.URI]*fileBase{}, filesByBase: map[string][]*fileBase{}, rootURI: root, diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index a408ee7a03a..5974fab7268 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -61,7 +61,7 @@ type View struct { // Each modfile has a map of module name to upgrade version. moduleUpgrades map[span.URI]map[string]string - vulns map[span.URI][]govulncheck.Vuln + vulns map[span.URI][]*govulncheck.Vuln // keep track of files by uri and by basename, a single file may be mapped // to multiple uris, and the same basename may map to multiple files @@ -1035,16 +1035,16 @@ func (v *View) ClearModuleUpgrades(modfile span.URI) { delete(v.moduleUpgrades, modfile) } -func (v *View) Vulnerabilities(modfile span.URI) []govulncheck.Vuln { +func (v *View) Vulnerabilities(modfile span.URI) []*govulncheck.Vuln { v.mu.Lock() defer v.mu.Unlock() - vulns := make([]govulncheck.Vuln, len(v.vulns[modfile])) + vulns := make([]*govulncheck.Vuln, len(v.vulns[modfile])) copy(vulns, v.vulns[modfile]) return vulns } -func (v *View) SetVulnerabilities(modfile span.URI, vulns []govulncheck.Vuln) { +func (v *View) SetVulnerabilities(modfile span.URI, vulns []*govulncheck.Vuln) { v.mu.Lock() defer v.mu.Unlock() diff --git a/gopls/internal/lsp/cmd/vulncheck.go b/gopls/internal/lsp/cmd/vulncheck.go index 93b9c106aef..c8f0358d39c 100644 --- a/gopls/internal/lsp/cmd/vulncheck.go +++ b/gopls/internal/lsp/cmd/vulncheck.go @@ -82,9 +82,11 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { } if v.AsSummary { - // vulnchecklib.Main calls os.Exit and never returns. - vulnchecklib.Main(loadCfg, args...) - return nil + if err := vulnchecklib.Main(loadCfg, args...); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(0) } // TODO(hyangah): delete. res, err := vulnchecklib.Govulncheck(ctx, &loadCfg, pattern) diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index e231b4bba4e..9722b7864e9 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -880,34 +880,31 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc return fmt.Errorf("failed to run govulncheck: %v", err) } - var summary govulncheck.Summary - if err := json.Unmarshal(stdout, &summary); err != nil { + var result govulncheck.Result + if err := json.Unmarshal(stdout, &result); err != nil { // TODO: for easy debugging, log the failed stdout somewhere? return fmt.Errorf("failed to parse govulncheck output: %v", err) } - - vulns := append(summary.Affecting, summary.NonAffecting...) + vulns := result.Vulns deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), vulns) c.s.diagnoseSnapshot(deps.snapshot, nil, false) - if len(summary.Affecting) == 0 { + affecting := make([]string, 0, len(vulns)) + for _, v := range vulns { + if v.IsCalled() { + affecting = append(affecting, v.OSV.ID) + } + } + if len(affecting) == 0 { return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ Type: protocol.Info, Message: "No vulnerabilities found", }) } - set := make(map[string]bool) - for _, v := range summary.Affecting { - set[v.OSV.ID] = true - } - list := make([]string, 0, len(set)) - for k := range set { - list = append(list, k) - } - sort.Strings(list) + sort.Strings(affecting) return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ Type: protocol.Warning, - Message: fmt.Sprintf("Found %v", strings.Join(list, ", ")), + Message: fmt.Sprintf("Found %v", strings.Join(affecting, ", ")), }) }) return err diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 770fb3729f7..fbbd34e278e 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -197,13 +197,19 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, vs := snapshot.View().Vulnerabilities(fh.URI()) // TODO(suzmue): should we just store the vulnerabilities like this? - affecting := make(map[string][]govulncheck.Vuln) - nonaffecting := make(map[string][]govulncheck.Vuln) + type modVuln struct { + mod *govulncheck.Module + vuln *govulncheck.Vuln + } + affecting := make(map[string][]modVuln) + nonaffecting := make(map[string][]modVuln) for _, v := range vs { - if len(v.Trace) > 0 { - affecting[v.ModPath] = append(affecting[v.ModPath], v) - } else { - nonaffecting[v.ModPath] = append(nonaffecting[v.ModPath], v) + for _, m := range v.Modules { + if v.IsCalled() { + affecting[m.Path] = append(affecting[m.Path], modVuln{mod: m, vuln: v}) + } else { + nonaffecting[m.Path] = append(nonaffecting[m.Path], modVuln{mod: m, vuln: v}) + } } } @@ -222,25 +228,30 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, // Fixes will include only the upgrades for warning level diagnostics. var fixes []source.SuggestedFix var warning, info []string - for _, v := range nonaffectingVulns { + for _, mv := range nonaffectingVulns { + mod, vuln := mv.mod, mv.vuln // Only show the diagnostic if the vulnerability was calculated // for the module at the current version. - if semver.IsValid(v.FoundIn) && semver.Compare(req.Mod.Version, v.FoundIn) != 0 { + // TODO(hyangah): this prevents from surfacing vulnerable modules + // since module version selection is affected by dependency module + // requirements and replace/exclude/go.work use. Drop this check + // and annotate only the module path. + if semver.IsValid(mod.FoundVersion) && semver.Compare(req.Mod.Version, mod.FoundVersion) != 0 { continue } - info = append(info, v.OSV.ID) + info = append(info, vuln.OSV.ID) } - for _, v := range affectingVulns { + for _, mv := range affectingVulns { + mod, vuln := mv.mod, mv.vuln // Only show the diagnostic if the vulnerability was calculated // for the module at the current version. - if semver.IsValid(v.FoundIn) && semver.Compare(req.Mod.Version, v.FoundIn) != 0 { + if semver.IsValid(mod.FoundVersion) && semver.Compare(req.Mod.Version, mod.FoundVersion) != 0 { continue } - warning = append(warning, v.OSV.ID) + warning = append(warning, vuln.OSV.ID) // Upgrade to the exact version we offer the user, not the most recent. // TODO(hakim): Produce fixes only for affecting vulnerabilities (if len(v.Trace) > 0) - - if fixedVersion := v.FixedIn; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { + if fixedVersion := mod.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { cmd, err := getUpgradeCodeAction(fh, req, fixedVersion) if err != nil { return nil, err @@ -297,7 +308,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return vulnDiagnostics, nil } -func formatMessage(v govulncheck.Vuln) string { +func formatMessage(v *govulncheck.Vuln) string { details := []byte(v.OSV.Details) // Remove any new lines that are not preceded or followed by a new line. for i, r := range details { diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 5d5b6158212..547f25270b7 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -71,7 +71,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Get the vulnerability info. - affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI()), req) + affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI()), req.Mod.Path) // Get the `go mod why` results for the given file. why, err := snapshot.ModWhy(ctx, fh) @@ -94,7 +94,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path) header := formatHeader(req.Mod.Path, options) explanation = formatExplanation(explanation, req, options, isPrivate) - vulns := formatVulnerabilities(affecting, nonaffecting, options) + vulns := formatVulnerabilities(req.Mod.Path, affecting, nonaffecting, options) return &protocol.Hover{ Contents: protocol.MarkupContent{ @@ -117,33 +117,25 @@ func formatHeader(modpath string, options *source.Options) string { return b.String() } -func compareVuln(i, j govulncheck.Vuln) bool { - if i.OSV.ID == j.OSV.ID { - return i.PkgPath < j.PkgPath - } - return i.OSV.ID < j.OSV.ID -} - -func lookupVulns(vulns []govulncheck.Vuln, req *modfile.Require) (affecting, nonaffecting []govulncheck.Vuln) { - modpath, modversion := req.Mod.Path, req.Mod.Version - - var info, warning []govulncheck.Vuln +func lookupVulns(vulns []*govulncheck.Vuln, modpath string) (affecting, nonaffecting []*govulncheck.Vuln) { for _, vuln := range vulns { - if vuln.ModPath != modpath || vuln.FoundIn != modversion { - continue - } - if len(vuln.Trace) == 0 { - info = append(info, vuln) - } else { - warning = append(warning, vuln) + for _, mod := range vuln.Modules { + if mod.Path != modpath { + continue + } + if vuln.IsCalled() { + affecting = append(affecting, vuln) + } else { + nonaffecting = append(nonaffecting, vuln) + } } } - sort.Slice(info, func(i, j int) bool { return compareVuln(info[i], info[j]) }) - sort.Slice(warning, func(i, j int) bool { return compareVuln(warning[i], warning[j]) }) - return warning, info + sort.Slice(nonaffecting, func(i, j int) bool { return nonaffecting[i].OSV.ID < nonaffecting[j].OSV.ID }) + sort.Slice(affecting, func(i, j int) bool { return affecting[i].OSV.ID < affecting[j].OSV.ID }) + return affecting, nonaffecting } -func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options *source.Options) string { +func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulncheck.Vuln, options *source.Options) string { if len(affecting) == 0 && len(nonaffecting) == 0 { return "" } @@ -163,10 +155,7 @@ func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options * } } for _, v := range affecting { - fix := "No fix is available." - if v.FixedIn != "" { - fix = "Fixed in " + v.FixedIn + "." - } + fix := fixedVersionInfo(v, modPath) if useMarkdown { fmt.Fprintf(&b, "- [**%v**](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) @@ -178,10 +167,7 @@ func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options * fmt.Fprintf(&b, "\n**FYI:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n") } for _, v := range nonaffecting { - fix := "No fix is available." - if v.FixedIn != "" { - fix = "Fixed in " + v.FixedIn + "." - } + fix := fixedVersionInfo(v, modPath) if useMarkdown { fmt.Fprintf(&b, "- [%v](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) } else { @@ -192,6 +178,20 @@ func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options * return b.String() } +func fixedVersionInfo(v *govulncheck.Vuln, modPath string) string { + fix := "No fix is available." + for _, m := range v.Modules { + if m.Path != modPath { + continue + } + if m.FixedVersion != "" { + fix = "Fixed in " + m.FixedVersion + "." + } + break + } + return fix +} + func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string { text = strings.TrimSuffix(text, "\n") splt := strings.Split(text, "\n") diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 25fa7d704d0..54c942f1d34 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -278,11 +278,11 @@ type View interface { // Vulnerabilites returns known vulnerabilities for the given modfile. // TODO(suzmue): replace command.Vuln with a different type, maybe // https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary? - Vulnerabilities(modfile span.URI) []govulncheck.Vuln + Vulnerabilities(modfile span.URI) []*govulncheck.Vuln // SetVulnerabilities resets the list of vulnerabilites that exists for the given modules // required by modfile. - SetVulnerabilities(modfile span.URI, vulnerabilities []govulncheck.Vuln) + SetVulnerabilities(modfile span.URI, vulnerabilities []*govulncheck.Vuln) // FileKind returns the type of a file FileKind(FileHandle) FileKind diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index e607eb415e6..510dd844960 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -9,6 +9,7 @@ package vulncheck import ( "context" + "encoding/json" "fmt" "log" "os" @@ -216,19 +217,32 @@ func trimPosPrefix(summary string) string { const GoVersionForVulnTest = "_GOPLS_TEST_VULNCHECK_GOVERSION" func init() { - Main = func(cfg packages.Config, patterns ...string) { - // never return - err := gvcapi.Main(gvcapi.Config{ - AnalysisType: "source", - OutputType: "summary", - Patterns: patterns, - SourceLoadConfig: &cfg, - GoVersion: os.Getenv(GoVersionForVulnTest), + Main = func(cfg packages.Config, patterns ...string) error { + // Set the mode that Source needs. + cfg.Mode = packages.NeedName | packages.NeedImports | packages.NeedTypes | + packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps | + packages.NeedModule + + pkgs, err := packages.Load(&cfg, patterns...) + if err != nil { + return err + } + cli, err := client.NewClient(findGOVULNDB(&cfg), client.Options{ + HTTPCache: gvc.DefaultCache(), }) if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) + return err + } + res, err := gvcapi.Source(context.Background(), &gvcapi.Config{ + Client: cli, + GoVersion: os.Getenv(GoVersionForVulnTest), + }, vulncheck.Convert(pkgs)) + if err != nil { + return err + } + if err := json.NewEncoder(os.Stdout).Encode(res); err != nil { + return err } - os.Exit(0) + return nil } } diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go index 7167ec1c266..1cbf18e86db 100644 --- a/gopls/internal/vulncheck/vulncheck.go +++ b/gopls/internal/vulncheck/vulncheck.go @@ -19,4 +19,4 @@ import ( // With go1.18+, this is swapped with the real implementation. var Govulncheck func(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) = nil -var Main func(cfg packages.Config, patterns ...string) = nil +var Main func(cfg packages.Config, patterns ...string) error = nil From 4a8413c694ff4b1abbe20c55eeb08f5d9fbf2d0e Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 9 Nov 2022 07:06:27 -0500 Subject: [PATCH 419/723] gopls/internal/lsp/mod: deleted unused function pkgVersion Change-Id: I85cabd92a1ee28f60ad355c4f44577e0ccc0bd07 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448977 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Peter Weinberger Run-TryBot: Hyang-Ah Hana Kim --- gopls/internal/lsp/mod/diagnostics.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index fbbd34e278e..05189070cb1 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -165,21 +165,6 @@ func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh sou return upgradeDiagnostics, nil } -func pkgVersion(pkgVersion string) (pkg, ver string) { - if pkgVersion == "" { - return "", "" - } - at := strings.Index(pkgVersion, "@") - switch { - case at < 0: - return pkgVersion, "" - case at == 0: - return "", pkgVersion[1:] - default: - return pkgVersion[:at], pkgVersion[at+1:] - } -} - const upgradeCodeActionPrefix = "Upgrade to " // ModVulnerabilityDiagnostics adds diagnostics for vulnerabilities in individual modules From 004d1181ca0c00605db348475f92ade5cfcded74 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 9 Nov 2022 07:15:29 -0500 Subject: [PATCH 420/723] gopls/internal/lsp/mod: simplify ModVulnerabilityDiagnostics Classify the unaffecting/affecting vulnerabilities as we iterate over the vulnerabilities to generate info/warning level diagnostics, instead of iterating over the list twice. Change-Id: Icfac41c3897d443c368f0292279ca936daffe24e Reviewed-on: https://go-review.googlesource.com/c/tools/+/448978 Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/mod/diagnostics.go | 34 +++++++++------------------ 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 05189070cb1..e455ba3f718 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -186,22 +186,16 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, mod *govulncheck.Module vuln *govulncheck.Vuln } - affecting := make(map[string][]modVuln) - nonaffecting := make(map[string][]modVuln) - for _, v := range vs { - for _, m := range v.Modules { - if v.IsCalled() { - affecting[m.Path] = append(affecting[m.Path], modVuln{mod: m, vuln: v}) - } else { - nonaffecting[m.Path] = append(nonaffecting[m.Path], modVuln{mod: m, vuln: v}) - } + vulnsByModule := make(map[string][]modVuln) + for _, vuln := range vs { + for _, mod := range vuln.Modules { + vulnsByModule[mod.Path] = append(vulnsByModule[mod.Path], modVuln{mod, vuln}) } } for _, req := range pm.File.Require { - affectingVulns, ok := affecting[req.Mod.Path] - nonaffectingVulns, ok2 := nonaffecting[req.Mod.Path] - if !ok && !ok2 { + vulns := vulnsByModule[req.Mod.Path] + if len(vulns) == 0 { continue } rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) @@ -213,7 +207,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, // Fixes will include only the upgrades for warning level diagnostics. var fixes []source.SuggestedFix var warning, info []string - for _, mv := range nonaffectingVulns { + for _, mv := range vulns { mod, vuln := mv.mod, mv.vuln // Only show the diagnostic if the vulnerability was calculated // for the module at the current version. @@ -224,18 +218,12 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, if semver.IsValid(mod.FoundVersion) && semver.Compare(req.Mod.Version, mod.FoundVersion) != 0 { continue } - info = append(info, vuln.OSV.ID) - } - for _, mv := range affectingVulns { - mod, vuln := mv.mod, mv.vuln - // Only show the diagnostic if the vulnerability was calculated - // for the module at the current version. - if semver.IsValid(mod.FoundVersion) && semver.Compare(req.Mod.Version, mod.FoundVersion) != 0 { - continue + if !vuln.IsCalled() { + info = append(info, vuln.OSV.ID) + } else { + warning = append(warning, vuln.OSV.ID) } - warning = append(warning, vuln.OSV.ID) // Upgrade to the exact version we offer the user, not the most recent. - // TODO(hakim): Produce fixes only for affecting vulnerabilities (if len(v.Trace) > 0) if fixedVersion := mod.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { cmd, err := getUpgradeCodeAction(fh, req, fixedVersion) if err != nil { From 3c3713e6a543fc1870ade379dd0acb6bcc3c0c24 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 10 Nov 2022 13:02:38 -0500 Subject: [PATCH 421/723] gopls/internal/lsp/cache: use typed strings (PackagePath et al) throughout This changes uses the string types to enforce better hygiene of conversions. Fishy conversions were flagged with TODO comments. Perhaps some of these contribute to bugs in our support for vendoring. Also, the function formerly known as ImportPath is now UnquoteImportPath. Change-Id: Ia6bf8749908d881fb0a62cb6c98f7dd00563a69e Reviewed-on: https://go-review.googlesource.com/c/tools/+/449497 TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/analysis.go | 8 +-- gopls/internal/lsp/cache/check.go | 8 +-- gopls/internal/lsp/cache/graph.go | 6 +-- gopls/internal/lsp/cache/load.go | 14 ++--- gopls/internal/lsp/cache/metadata.go | 25 +++------ gopls/internal/lsp/cache/pkg.go | 35 +++++-------- gopls/internal/lsp/cache/snapshot.go | 17 +++---- gopls/internal/lsp/command.go | 10 ++-- gopls/internal/lsp/diagnostics.go | 8 +-- gopls/internal/lsp/link.go | 13 +++-- gopls/internal/lsp/semantic.go | 7 ++- gopls/internal/lsp/server.go | 4 +- .../lsp/source/completion/completion.go | 24 +++++---- .../internal/lsp/source/completion/package.go | 24 ++++----- .../lsp/source/completion/package_test.go | 8 ++- .../lsp/source/completion/postfix_snippets.go | 7 +-- gopls/internal/lsp/source/hover.go | 4 +- gopls/internal/lsp/source/identifier.go | 2 +- gopls/internal/lsp/source/implementation.go | 2 +- gopls/internal/lsp/source/known_packages.go | 21 ++++---- gopls/internal/lsp/source/references.go | 2 +- gopls/internal/lsp/source/rename.go | 51 ++++++++++--------- gopls/internal/lsp/source/rename_check.go | 5 +- gopls/internal/lsp/source/stub.go | 6 +-- gopls/internal/lsp/source/util.go | 20 ++++---- gopls/internal/lsp/source/view.go | 39 ++++++++------ gopls/internal/lsp/source/workspace_symbol.go | 14 ++--- 27 files changed, 193 insertions(+), 191 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index e15b43e91ce..cbb6f04e762 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -24,7 +24,7 @@ import ( "golang.org/x/tools/internal/memoize" ) -func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) { +func (s *snapshot) Analyze(ctx context.Context, id PackageID, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) { // TODO(adonovan): merge these two loops. There's no need to // construct all the root action handles before beginning // analysis. Operations should be concurrent (though that first @@ -35,7 +35,7 @@ func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.A if !a.IsEnabled(s.view) { continue } - ah, err := s.actionHandle(ctx, PackageID(id), a.Analyzer) + ah, err := s.actionHandle(ctx, id, a.Analyzer) if err != nil { return nil, err } @@ -416,7 +416,7 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a for _, diag := range rawDiagnostics { srcDiags, err := analysisDiagnosticDiagnostics(snapshot, pkg, analyzer, &diag) if err != nil { - event.Error(ctx, "unable to compute analysis error position", err, tag.Category.Of(diag.Category), tag.Package.Of(pkg.ID())) + event.Error(ctx, "unable to compute analysis error position", err, tag.Category.Of(diag.Category), tag.Package.Of(string(pkg.ID()))) continue } diagnostics = append(diagnostics, srcDiags...) @@ -477,7 +477,7 @@ func (s *snapshot) DiagnosePackage(ctx context.Context, spkg source.Package) (ma errorAnalyzerDiag, err = s.Analyze(ctx, pkg.ID(), analyzers) if err != nil { // Keep going: analysis failures should not block diagnostics. - event.Error(ctx, "type error analysis failed", err, tag.Package.Of(pkg.ID())) + event.Error(ctx, "type error analysis failed", err, tag.Package.Of(string(pkg.ID()))) } } diags := map[span.URI][]*source.Diagnostic{} diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 17943efb707..580ed0c87e2 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -378,7 +378,7 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF for _, e := range m.Errors { diags, err := goPackagesErrorDiagnostics(snapshot, pkg, e) if err != nil { - event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(pkg.ID())) + event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(string(pkg.ID()))) continue } pkg.diagnostics = append(pkg.diagnostics, diags...) @@ -400,7 +400,7 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF for _, e := range pkg.parseErrors { diags, err := parseErrorDiagnostics(snapshot, pkg, e) if err != nil { - event.Error(ctx, "unable to compute positions for parse errors", err, tag.Package.Of(pkg.ID())) + event.Error(ctx, "unable to compute positions for parse errors", err, tag.Package.Of(string(pkg.ID()))) continue } for _, diag := range diags { @@ -418,7 +418,7 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF for _, e := range expandErrors(unexpanded, snapshot.View().Options().RelatedInformationSupported) { diags, err := typeErrorDiagnostics(snapshot, pkg, e) if err != nil { - event.Error(ctx, "unable to compute positions for type errors", err, tag.Package.Of(pkg.ID())) + event.Error(ctx, "unable to compute positions for type errors", err, tag.Package.Of(string(pkg.ID()))) continue } pkg.typeErrors = append(pkg.typeErrors, e.primary) @@ -535,7 +535,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil if !ok { return nil, snapshot.missingPkgError(path) } - if !source.IsValidImport(string(m.PkgPath), string(dep.m.PkgPath)) { + if !source.IsValidImport(m.PkgPath, dep.m.PkgPath) { return nil, fmt.Errorf("invalid use of internal package %s", path) } depPkg, err := dep.await(ctx, snapshot) diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index 1dac0767afb..83336a23e15 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -93,8 +93,8 @@ func (g *metadataGraph) build() { } // 2. command-line-args packages appear later. - cli := source.IsCommandLineArguments(string(ids[i])) - clj := source.IsCommandLineArguments(string(ids[j])) + cli := source.IsCommandLineArguments(ids[i]) + clj := source.IsCommandLineArguments(ids[j]) if cli != clj { return clj } @@ -121,7 +121,7 @@ func (g *metadataGraph) build() { } // If we've seen *anything* prior to command-line arguments package, take // it. Note that ids[0] may itself be command-line-arguments. - if i > 0 && source.IsCommandLineArguments(string(id)) { + if i > 0 && source.IsCommandLineArguments(id) { g.ids[uri] = ids[:i] break } diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 9cabffc0d03..5250a67f141 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -50,7 +50,9 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc for _, scope := range scopes { switch scope := scope.(type) { case packageLoadScope: - if source.IsCommandLineArguments(string(scope)) { + // TODO(adonovan): is this cast sound?? A + // packageLoadScope is really a PackagePath I think. + if source.IsCommandLineArguments(PackageID(scope)) { panic("attempted to load command-line-arguments") } // The only time we pass package paths is when we're doing a @@ -480,10 +482,10 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con // Allow for multiple ad-hoc packages in the workspace (see #47584). pkgPath := PackagePath(pkg.PkgPath) id := PackageID(pkg.ID) - if source.IsCommandLineArguments(pkg.ID) { + if source.IsCommandLineArguments(id) { suffix := ":" + strings.Join(query, ",") - id = PackageID(string(id) + suffix) - pkgPath = PackagePath(string(pkgPath) + suffix) + id = PackageID(pkg.ID + suffix) + pkgPath = PackagePath(pkg.PkgPath + suffix) } if _, ok := updates[id]; ok { @@ -712,7 +714,7 @@ func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[Packag continue } - if source.IsCommandLineArguments(string(m.ID)) { + if source.IsCommandLineArguments(m.ID) { // If all the files contained in m have a real package, we don't need to // keep m as a workspace package. if allFilesHaveRealPackages(meta, m) { @@ -754,7 +756,7 @@ func allFilesHaveRealPackages(g *metadataGraph, m *KnownMetadata) bool { checkURIs: for _, uri := range append(m.CompiledGoFiles[0:n:n], m.GoFiles...) { for _, id := range g.ids[uri] { - if !source.IsCommandLineArguments(string(id)) && (g.metadata[id].Valid || !m.Valid) { + if !source.IsCommandLineArguments(id) && (g.metadata[id].Valid || !m.Valid) { continue checkURIs } } diff --git a/gopls/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go index c8b0a537222..03037f0b09e 100644 --- a/gopls/internal/lsp/cache/metadata.go +++ b/gopls/internal/lsp/cache/metadata.go @@ -8,19 +8,16 @@ import ( "go/types" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/packagesinternal" ) -// Declare explicit types for package paths, names, and IDs to ensure that we -// never use an ID where a path belongs, and vice versa. If we confused these, -// it would result in confusing errors because package IDs often look like -// package paths. type ( - PackageID string // go list's unique identifier for a package (e.g. "vendor/example.com/foo [vendor/example.com/bar.test]") - PackagePath string // name used to prefix linker symbols (e.g. "vendor/example.com/foo") - PackageName string // identifier in 'package' declaration (e.g. "foo") - ImportPath string // path that appears in an import declaration (e.g. "example.com/foo") + PackageID = source.PackageID + PackagePath = source.PackagePath + PackageName = source.PackageName + ImportPath = source.ImportPath ) // Metadata holds package Metadata extracted from a call to packages.Load. @@ -43,19 +40,13 @@ type Metadata struct { } // PackageID implements the source.Metadata interface. -func (m *Metadata) PackageID() string { - return string(m.ID) -} +func (m *Metadata) PackageID() PackageID { return m.ID } // Name implements the source.Metadata interface. -func (m *Metadata) PackageName() string { - return string(m.Name) -} +func (m *Metadata) PackageName() PackageName { return m.Name } // PkgPath implements the source.Metadata interface. -func (m *Metadata) PackagePath() string { - return string(m.PkgPath) -} +func (m *Metadata) PackagePath() PackagePath { return m.PkgPath } // IsIntermediateTestVariant reports whether the given package is an // intermediate test variant, e.g. "net/http [net/url.test]". diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index ddfb9ea7e96..ef8177871b4 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -36,7 +36,7 @@ type pkg struct { analyses memoize.Store // maps analyzer.Name to Promise[actionResult] } -func (p *pkg) String() string { return p.ID() } +func (p *pkg) String() string { return string(p.ID()) } // A loadScope defines a package loading scope for use with go/packages. type loadScope interface { @@ -56,17 +56,9 @@ func (packageLoadScope) aScope() {} func (moduleLoadScope) aScope() {} func (viewLoadScope) aScope() {} -func (p *pkg) ID() string { - return string(p.m.ID) -} - -func (p *pkg) Name() string { - return string(p.m.Name) -} - -func (p *pkg) PkgPath() string { - return string(p.m.PkgPath) -} +func (p *pkg) ID() PackageID { return p.m.ID } +func (p *pkg) Name() PackageName { return p.m.Name } +func (p *pkg) PkgPath() PackagePath { return p.m.PkgPath } func (p *pkg) ParseMode() source.ParseMode { return p.mode @@ -118,8 +110,8 @@ func (p *pkg) ForTest() string { // given its PackagePath. (If you have an ImportPath, e.g. a string // from an import declaration, use ResolveImportPath instead. // They may differ in case of vendoring.) -func (p *pkg) DirectDep(pkgPath string) (source.Package, error) { - if id, ok := p.m.DepsByPkgPath[PackagePath(pkgPath)]; ok { +func (p *pkg) DirectDep(pkgPath PackagePath) (source.Package, error) { + if id, ok := p.m.DepsByPkgPath[pkgPath]; ok { if imp := p.deps[id]; imp != nil { return imp, nil } @@ -129,8 +121,8 @@ func (p *pkg) DirectDep(pkgPath string) (source.Package, error) { // ResolveImportPath returns the directly imported dependency of this package, // given its ImportPath. See also DirectDep. -func (p *pkg) ResolveImportPath(importPath string) (source.Package, error) { - if id, ok := p.m.DepsByImpPath[ImportPath(importPath)]; ok && id != "" { +func (p *pkg) ResolveImportPath(importPath ImportPath) (source.Package, error) { + if id, ok := p.m.DepsByImpPath[importPath]; ok && id != "" { if imp := p.deps[id]; imp != nil { return imp, nil } @@ -138,7 +130,7 @@ func (p *pkg) ResolveImportPath(importPath string) (source.Package, error) { return nil, fmt.Errorf("package does not import %s", importPath) } -func (p *pkg) MissingDependencies() []string { +func (p *pkg) MissingDependencies() []ImportPath { // We don't invalidate metadata for import deletions, // so check the package imports via the *types.Package. // @@ -162,13 +154,14 @@ func (p *pkg) MissingDependencies() []string { // should be fast) then we can simply return the blank entries // in DepsByImpPath. (They are PackageIDs not PackagePaths, // but the caller only cares whether the set is empty!) - var missing []string + var missing []ImportPath for _, pkg := range p.types.Imports() { - if id, ok := p.m.DepsByImpPath[ImportPath(pkg.Path())]; ok && id == "" { - missing = append(missing, pkg.Path()) + importPath := ImportPath(pkg.Path()) + if id, ok := p.m.DepsByImpPath[importPath]; ok && id == "" { + missing = append(missing, importPath) } } - sort.Strings(missing) + sort.Slice(missing, func(i, j int) bool { return missing[i] < missing[j] }) return missing } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index d6599b1cee5..f28030431a1 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -811,17 +811,17 @@ func (s *snapshot) useInvalidMetadata() bool { return s.view.goversion >= 13 && s.view.Options().ExperimentalUseInvalidMetadata } -func (s *snapshot) GetReverseDependencies(ctx context.Context, id string) ([]source.Package, error) { +func (s *snapshot) GetReverseDependencies(ctx context.Context, id PackageID) ([]source.Package, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } s.mu.Lock() meta := s.meta s.mu.Unlock() - ids := meta.reverseTransitiveClosure(s.useInvalidMetadata(), PackageID(id)) + ids := meta.reverseTransitiveClosure(s.useInvalidMetadata(), id) // Make sure to delete the original package ID from the map. - delete(ids, PackageID(id)) + delete(ids, id) var pkgs []source.Package for id := range ids { @@ -1212,12 +1212,11 @@ func (s *snapshot) AllValidMetadata(ctx context.Context) ([]source.Metadata, err return meta, nil } -func (s *snapshot) WorkspacePackageByID(ctx context.Context, id string) (source.Package, error) { - packageID := PackageID(id) - return s.checkedPackage(ctx, packageID, s.workspaceParseMode(packageID)) +func (s *snapshot) WorkspacePackageByID(ctx context.Context, id PackageID) (source.Package, error) { + return s.checkedPackage(ctx, id, s.workspaceParseMode(id)) } -func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Package, error) { +func (s *snapshot) CachedImportPaths(ctx context.Context) (map[PackagePath]source.Package, error) { // Don't reload workspace package metadata. // This function is meant to only return currently cached information. s.AwaitInitialized(ctx) @@ -1225,7 +1224,7 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac s.mu.Lock() defer s.mu.Unlock() - results := map[string]source.Package{} + results := map[PackagePath]source.Package{} s.packages.Range(func(_, v interface{}) { cachedPkg, err := v.(*packageHandle).cached() if err != nil { @@ -1983,7 +1982,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // For metadata that has been newly invalidated, capture package paths // requiring reloading in the shouldLoad map. - if invalidateMetadata && !source.IsCommandLineArguments(string(v.ID)) { + if invalidateMetadata && !source.IsCommandLineArguments(v.ID) { if result.shouldLoad == nil { result.shouldLoad = make(map[PackageID][]PackagePath) } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 9722b7864e9..3b0d3b849de 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -735,8 +735,10 @@ func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URI progress: "Listing packages", forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { - var err error - result.Packages, err = source.KnownPackages(ctx, deps.snapshot, deps.fh) + pkgs, err := source.KnownPackages(ctx, deps.snapshot, deps.fh) + for _, pkg := range pkgs { + result.Packages = append(result.Packages, string(pkg)) + } return err }) return result, err @@ -765,14 +767,14 @@ func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) ( name = imp.Name.Name } result.Imports = append(result.Imports, command.FileImport{ - Path: source.ImportPath(imp), + Path: string(source.UnquoteImportPath(imp)), Name: name, }) } } for _, imp := range pkg.Imports() { result.PackageImports = append(result.PackageImports, command.PackageImport{ - Path: imp.PkgPath(), // This might be the vendored path under GOPATH vendoring, in which case it's a bug. + Path: string(imp.PkgPath()), // This might be the vendored path under GOPATH vendoring, in which case it's a bug. }) } sort.Slice(result.PackageImports, func(i, j int) bool { diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index a01898b26a4..bbc57ffaba4 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -346,7 +346,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn } func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg source.Package, alwaysAnalyze bool) { - ctx, done := event.Start(ctx, "Server.diagnosePkg", tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID())) + ctx, done := event.Start(ctx, "Server.diagnosePkg", tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(string(pkg.ID()))) defer done() enableDiagnostics := false includeAnalysis := alwaysAnalyze // only run analyses for packages with open files @@ -361,7 +361,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) if err != nil { - event.Error(ctx, "warning: diagnosing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID())) + event.Error(ctx, "warning: diagnosing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(string(pkg.ID()))) return } for _, cgf := range pkg.CompiledGoFiles() { @@ -374,7 +374,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg if includeAnalysis && !pkg.HasListOrParseErrors() { reports, err := source.Analyze(ctx, snapshot, pkg, false) if err != nil { - event.Error(ctx, "warning: analyzing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID())) + event.Error(ctx, "warning: analyzing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(string(pkg.ID()))) return } for _, cgf := range pkg.CompiledGoFiles() { @@ -390,7 +390,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg if enableGCDetails { gcReports, err := source.GCOptimizationDetails(ctx, snapshot, pkg) if err != nil { - event.Error(ctx, "warning: gc details", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID())) + event.Error(ctx, "warning: gc details", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(string(pkg.ID()))) } s.gcOptimizationDetailsMu.Lock() _, enableGCDetails := s.gcOptimizationDetails[pkg.ID()] diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go index b26bfb33c80..7eb561fa00d 100644 --- a/gopls/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -12,7 +12,6 @@ import ( "go/token" "net/url" "regexp" - "strconv" "strings" "sync" @@ -133,21 +132,21 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle // https://pkg.go.dev. if view.Options().ImportShortcut.ShowLinks() { for _, imp := range imports { - target, err := strconv.Unquote(imp.Path.Value) - if err != nil { + target := source.UnquoteImportPath(imp) + if target == "" { continue } // See golang/go#36998: don't link to modules matching GOPRIVATE. - if view.IsGoPrivatePath(target) { + if view.IsGoPrivatePath(string(target)) { continue } if mod, version, ok := moduleAtVersion(target, pkg); ok && strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" { - target = strings.Replace(target, mod, mod+"@"+version, 1) + target = source.ImportPath(strings.Replace(string(target), mod, mod+"@"+version, 1)) } // Account for the quotation marks in the positions. start := imp.Path.Pos() + 1 end := imp.Path.End() - 1 - targetURL := source.BuildLink(view.Options().LinkTarget, target, "") + targetURL := source.BuildLink(view.Options().LinkTarget, string(target), "") l, err := toProtocolLink(pgf.Tok, pgf.Mapper, targetURL, start, end) if err != nil { return nil, err @@ -174,7 +173,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle return links, nil } -func moduleAtVersion(targetImportPath string, pkg source.Package) (string, string, bool) { +func moduleAtVersion(targetImportPath source.ImportPath, pkg source.Package) (string, string, bool) { impPkg, err := pkg.ResolveImportPath(targetImportPath) if err != nil { return "", "", false diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index a8b38f044ab..75f88b3efa2 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -15,7 +15,6 @@ import ( "log" "path/filepath" "sort" - "strconv" "strings" "time" @@ -905,8 +904,8 @@ func (e *encoded) importSpec(d *ast.ImportSpec) { } return // don't mark anything for . or _ } - importPath, err := strconv.Unquote(d.Path.Value) - if err != nil { + importPath := source.UnquoteImportPath(d) + if importPath == "" { return } // Import strings are implementation defined. Try to match with parse information. @@ -916,7 +915,7 @@ func (e *encoded) importSpec(d *ast.ImportSpec) { return } // Check whether the original literal contains the package's declared name. - j := strings.LastIndex(d.Path.Value, imported.Name()) + j := strings.LastIndex(d.Path.Value, string(imported.Name())) if j == -1 { // name doesn't show up, for whatever reason, so nothing to report return diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go index 693afaedab5..9f7059141d5 100644 --- a/gopls/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -27,7 +27,7 @@ func NewServer(session *cache.Session, client protocol.ClientCloser) *Server { session.SetProgressTracker(tracker) return &Server{ diagnostics: map[span.URI]*fileReports{}, - gcOptimizationDetails: make(map[string]struct{}), + gcOptimizationDetails: make(map[source.PackageID]struct{}), watchedGlobPatterns: make(map[string]struct{}), changedFiles: make(map[span.URI]struct{}), session: session, @@ -97,7 +97,7 @@ type Server struct { // optimization details to be included in the diagnostics. The key is the // ID of the package. gcOptimizationDetailsMu sync.Mutex - gcOptimizationDetails map[string]struct{} + gcOptimizationDetails map[source.PackageID]struct{} // diagnosticsSema limits the concurrency of diagnostics runs, which can be // expensive. diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index c3b7c2b461b..1f8dd929b2f 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -1089,7 +1089,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { if pkgName, ok := c.pkg.GetTypesInfo().Uses[id].(*types.PkgName); ok { var pkg source.Package for _, imp := range c.pkg.Imports() { - if imp.PkgPath() == pkgName.Imported().Path() { + if imp.PkgPath() == source.PackagePath(pkgName.Imported().Path()) { pkg = imp } } @@ -1140,7 +1140,7 @@ func (c *completer) unimportedMembers(ctx context.Context, id *ast.Ident) error if pkg.GetTypes().Name() != id.Name { continue } - paths = append(paths, path) + paths = append(paths, string(path)) } var relevances map[string]float64 @@ -1158,7 +1158,7 @@ func (c *completer) unimportedMembers(ctx context.Context, id *ast.Ident) error }) for _, path := range paths { - pkg := known[path] + pkg := known[source.PackagePath(path)] if pkg.GetTypes().Name() != id.Name { continue } @@ -1182,7 +1182,8 @@ func (c *completer) unimportedMembers(ctx context.Context, id *ast.Ident) error add := func(pkgExport imports.PackageExport) { mu.Lock() defer mu.Unlock() - if _, ok := known[pkgExport.Fix.StmtInfo.ImportPath]; ok { + // TODO(adonovan): what if the actual package has a vendor/ prefix? + if _, ok := known[source.PackagePath(pkgExport.Fix.StmtInfo.ImportPath)]; ok { return // We got this one above. } @@ -1379,7 +1380,8 @@ 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. - if _, ok := seen[pkg.Name()]; !ok && pkg != c.pkg.GetTypes() && !alreadyImports(c.file, pkg.Path()) { + // 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())) { seen[pkg.Name()] = struct{}{} obj := types.NewPkgName(0, nil, pkg.Name(), pkg) imp := &importInfo{ @@ -1481,12 +1483,12 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru if err != nil { return err } - var paths []string + var paths []string // actually PackagePaths for path, pkg := range known { if !strings.HasPrefix(pkg.GetTypes().Name(), prefix) { continue } - paths = append(paths, path) + paths = append(paths, string(path)) } var relevances map[string]float64 @@ -1511,7 +1513,7 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru }) for _, path := range paths { - pkg := known[path] + pkg := known[source.PackagePath(path)] if _, ok := seen[pkg.GetTypes().Name()]; ok { continue } @@ -1526,7 +1528,7 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru } c.deepState.enqueue(candidate{ // Pass an empty *types.Package to disable deep completions. - obj: types.NewPkgName(0, nil, pkg.GetTypes().Name(), types.NewPackage(path, pkg.Name())), + obj: types.NewPkgName(0, nil, pkg.GetTypes().Name(), types.NewPackage(path, string(pkg.Name()))), score: unimportedScore(relevances[path]), imp: imp, }) @@ -1573,9 +1575,9 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru } // alreadyImports reports whether f has an import with the specified path. -func alreadyImports(f *ast.File, path string) bool { +func alreadyImports(f *ast.File, path source.ImportPath) bool { for _, s := range f.Imports { - if source.ImportPath(s) == path { + if source.UnquoteImportPath(s) == path { return true } } diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index b7fad0fa513..1b7c731fd81 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -240,7 +240,7 @@ func packageSuggestions(ctx context.Context, snapshot source.Snapshot, fileURI s } pkgName := convertDirNameToPkgName(dirName) - seenPkgs := make(map[string]struct{}) + seenPkgs := make(map[source.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. @@ -267,30 +267,30 @@ func packageSuggestions(ctx context.Context, snapshot source.Snapshot, fileURI s // Add a found package used in current directory as a high relevance // suggestion and the test package for it as a medium relevance // suggestion. - if score := float64(matcher.Score(pkg.Name())); score > 0 { - packages = append(packages, toCandidate(pkg.Name(), score*highScore)) + if score := float64(matcher.Score(string(pkg.Name()))); score > 0 { + packages = append(packages, toCandidate(string(pkg.Name()), score*highScore)) } seenPkgs[pkg.Name()] = struct{}{} testPkgName := pkg.Name() + "_test" - if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(pkg.Name(), "_test") { + if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(string(pkg.Name()), "_test") { continue } - if score := float64(matcher.Score(testPkgName)); score > 0 { - packages = append(packages, toCandidate(testPkgName, score*stdScore)) + if score := float64(matcher.Score(string(testPkgName))); score > 0 { + packages = append(packages, toCandidate(string(testPkgName), score*stdScore)) } seenPkgs[testPkgName] = struct{}{} } // Add current directory name as a low relevance suggestion. if _, ok := seenPkgs[pkgName]; !ok { - if score := float64(matcher.Score(pkgName)); score > 0 { - packages = append(packages, toCandidate(pkgName, score*lowScore)) + if score := float64(matcher.Score(string(pkgName))); score > 0 { + packages = append(packages, toCandidate(string(pkgName), score*lowScore)) } testPkgName := pkgName + "_test" - if score := float64(matcher.Score(testPkgName)); score > 0 { - packages = append(packages, toCandidate(testPkgName, score*lowScore)) + if score := float64(matcher.Score(string(testPkgName))); score > 0 { + packages = append(packages, toCandidate(string(testPkgName), score*lowScore)) } } @@ -330,7 +330,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) string { +func convertDirNameToPkgName(dirName string) source.PackageName { var buf bytes.Buffer for _, ch := range dirName { switch { @@ -341,7 +341,7 @@ func convertDirNameToPkgName(dirName string) string { buf.WriteRune(ch) } } - return buf.String() + return source.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/lsp/source/completion/package_test.go index 6436984fdc9..614359fa5dc 100644 --- a/gopls/internal/lsp/source/completion/package_test.go +++ b/gopls/internal/lsp/source/completion/package_test.go @@ -4,7 +4,11 @@ package completion -import "testing" +import ( + "testing" + + "golang.org/x/tools/gopls/internal/lsp/source" +) func TestIsValidDirName(t *testing.T) { tests := []struct { @@ -51,7 +55,7 @@ func TestIsValidDirName(t *testing.T) { func TestConvertDirNameToPkgName(t *testing.T) { tests := []struct { dirName string - pkgName string + pkgName source.PackageName }{ {dirName: "a", pkgName: "a"}, {dirName: "abcdef", pkgName: "abcdef"}, diff --git a/gopls/internal/lsp/source/completion/postfix_snippets.go b/gopls/internal/lsp/source/completion/postfix_snippets.go index 414db42592e..3c805a77e1f 100644 --- a/gopls/internal/lsp/source/completion/postfix_snippets.go +++ b/gopls/internal/lsp/source/completion/postfix_snippets.go @@ -16,11 +16,11 @@ import ( "sync" "text/template" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/imports" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/imports" ) // Postfix snippets are artificial methods that allow the user to @@ -442,7 +442,8 @@ func (c *completer) importIfNeeded(pkgPath string, scope *types.Scope) (string, // Check if file already imports pkgPath. for _, s := range c.file.Imports { - if source.ImportPath(s) == pkgPath { + // TODO(adonovan): what if pkgPath has a vendor/ prefix? + if source.UnquoteImportPath(s) == source.ImportPath(pkgPath) { if s.Name == nil { return defaultName, nil, nil } diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 09f7224c80d..0bd5a3055e9 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -448,7 +448,7 @@ func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) { if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" { return "", "", false } - impPkg, err := i.pkg.DirectDep(path) + impPkg, err := i.pkg.DirectDep(PackagePath(path)) if err != nil { return "", "", false } @@ -539,7 +539,7 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob if err != nil { return nil, err } - imp, err := pkg.ResolveImportPath(importPath) + imp, err := pkg.ResolveImportPath(ImportPath(importPath)) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index f11817f5865..526c1084706 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -456,7 +456,7 @@ func importSpec(snapshot Snapshot, pkg Package, file *ast.File, pos token.Pos) ( if err != nil { return nil, fmt.Errorf("import path not quoted: %s (%v)", imp.Path.Value, err) } - imported, err := pkg.ResolveImportPath(importPath) + imported, err := pkg.ResolveImportPath(ImportPath(importPath)) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 2da488dd67a..87f435108d9 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -323,7 +323,7 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s // Look up the implicit *types.PkgName. obj := searchpkg.GetTypesInfo().Implicits[leaf] if obj == nil { - return nil, fmt.Errorf("%w for import %q", errNoObjectFound, ImportPath(leaf)) + return nil, fmt.Errorf("%w for import %s", errNoObjectFound, UnquoteImportPath(leaf)) } objs = append(objs, obj) } diff --git a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go index e2d950ba00b..efaaee57aed 100644 --- a/gopls/internal/lsp/source/known_packages.go +++ b/gopls/internal/lsp/source/known_packages.go @@ -19,14 +19,15 @@ import ( // KnownPackages returns a list of all known packages // in the package graph that could potentially be imported // by the given file. -func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle) ([]string, error) { +func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle) ([]PackagePath, error) { pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return nil, fmt.Errorf("GetParsedFile: %w", err) } - alreadyImported := map[string]struct{}{} + alreadyImported := map[PackagePath]struct{}{} for _, imp := range pgf.File.Imports { - alreadyImported[imp.Path.Value] = struct{}{} + // TODO(adonovan): the correct PackagePath might need a "vendor/" prefix. + alreadyImported[PackagePath(imp.Path.Value)] = struct{}{} } // TODO(adonovan): this whole algorithm could be more // simply expressed in terms of Metadata, not Packages. @@ -35,8 +36,8 @@ func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandl return nil, err } var ( - seen = make(map[string]struct{}) - paths []string + seen = make(map[PackagePath]struct{}) + paths []PackagePath ) for path, knownPkg := range pkgs { gofiles := knownPkg.CompiledGoFiles() @@ -79,10 +80,12 @@ func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandl return imports.GetAllCandidates(ctx, func(ifix imports.ImportFix) { mu.Lock() defer mu.Unlock() - if _, ok := seen[ifix.StmtInfo.ImportPath]; ok { + // TODO(adonovan): what if the actual package path has a vendor/ prefix? + path := PackagePath(ifix.StmtInfo.ImportPath) + if _, ok := seen[path]; ok { return } - paths = append(paths, ifix.StmtInfo.ImportPath) + paths = append(paths, path) }, "", pgf.URI.Filename(), pkg.GetTypes().Name(), o.Env) }) if err != nil { @@ -92,8 +95,8 @@ func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandl } sort.Slice(paths, func(i, j int) bool { importI, importJ := paths[i], paths[j] - iHasDot := strings.Contains(importI, ".") - jHasDot := strings.Contains(importJ, ".") + iHasDot := strings.Contains(string(importI), ".") + jHasDot := strings.Contains(string(importJ), ".") if iHasDot && !jHasDot { return false } diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index e26091c9fe8..4db716e6dd4 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -78,7 +78,7 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit for _, dep := range rdeps { for _, f := range dep.CompiledGoFiles() { for _, imp := range f.File.Imports { - if path, err := strconv.Unquote(imp.Path.Value); err == nil && path == renamingPkg.PkgPath() { + if path, err := strconv.Unquote(imp.Path.Value); err == nil && path == string(renamingPkg.PkgPath()) { refs = append(refs, &ReferenceInfo{ Name: packageName, MappedRange: NewMappedRange(f.Tok, f.Mapper, imp.Pos(), imp.End()), diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 2d4a188b8ac..3a8d7cfb6ba 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -93,7 +93,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot return nil, err, err } - if strings.HasSuffix(meta.PackageName(), "_test") { + if strings.HasSuffix(string(meta.PackageName()), "_test") { err := errors.New("can't rename x_test packages") return nil, err, err } @@ -103,7 +103,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot return nil, err, err } - if meta.ModuleInfo().Path == meta.PackagePath() { + if meta.ModuleInfo().Path == string(meta.PackagePath()) { err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PackagePath(), meta.ModuleInfo().Path) return nil, err, err } @@ -113,7 +113,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot err = fmt.Errorf("error building package to rename: %v", err) return nil, err, err } - result, err := computePrepareRenameResp(snapshot, pkg, pgf.File.Name, pkg.Name()) + result, err := computePrepareRenameResp(snapshot, pkg, pgf.File.Name, string(pkg.Name())) if err != nil { return nil, nil, err } @@ -202,11 +202,11 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, // Fix this. meta := fileMeta[0] oldPath := meta.PackagePath() - var modulePath string + var modulePath PackagePath if mi := meta.ModuleInfo(); mi == nil { return nil, true, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PackagePath()) } else { - modulePath = mi.Path + modulePath = PackagePath(mi.Path) } if strings.HasSuffix(newName, "_test") { @@ -218,7 +218,7 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, return nil, true, err } - renamingEdits, err := renamePackage(ctx, s, modulePath, oldPath, newName, metadata) + renamingEdits, err := renamePackage(ctx, s, modulePath, oldPath, PackageName(newName), metadata) if err != nil { return nil, true, err } @@ -245,12 +245,12 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, // It updates package clauses and import paths for the renamed package as well // as any other packages affected by the directory renaming among packages // described by allMetadata. -func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath, newName string, allMetadata []Metadata) (map[span.URI][]protocol.TextEdit, error) { +func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath PackagePath, newName PackageName, allMetadata []Metadata) (map[span.URI][]protocol.TextEdit, error) { if modulePath == oldPath { return nil, fmt.Errorf("cannot rename package: module path %q is the same as the package path, so renaming the package directory would have no effect", modulePath) } - newPathPrefix := path.Join(path.Dir(oldPath), newName) + newPathPrefix := path.Join(path.Dir(string(oldPath)), string(newName)) edits := make(map[span.URI][]protocol.TextEdit) seen := make(seenPackageRename) // track per-file import renaming we've already processed @@ -272,7 +272,7 @@ func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath, newName // Subtle: check this condition before checking for valid module info // below, because we should not fail this operation if unrelated packages // lack module info. - if !strings.HasPrefix(m.PackagePath()+"/", oldPath+"/") { + if !strings.HasPrefix(string(m.PackagePath())+"/", string(oldPath)+"/") { continue // not affected by the package renaming } @@ -280,16 +280,16 @@ func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath, newName return nil, fmt.Errorf("cannot rename package: missing module information for package %q", m.PackagePath()) } - if modulePath != m.ModuleInfo().Path { + if modulePath != PackagePath(m.ModuleInfo().Path) { continue // don't edit imports if nested package and renaming package have different module paths } // Renaming a package consists of changing its import path and package name. - suffix := strings.TrimPrefix(m.PackagePath(), oldPath) + suffix := strings.TrimPrefix(string(m.PackagePath()), string(oldPath)) newPath := newPathPrefix + suffix pkgName := m.PackageName() - if m.PackagePath() == oldPath { + if m.PackagePath() == PackagePath(oldPath) { pkgName = newName if err := renamePackageClause(ctx, m, s, newName, seen, edits); err != nil { @@ -297,7 +297,8 @@ func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath, newName } } - if err := renameImports(ctx, s, m, newPath, pkgName, seen, edits); err != nil { + imp := ImportPath(newPath) // TODO(adonovan): what if newPath has vendor/ prefix? + if err := renameImports(ctx, s, m, imp, pkgName, seen, edits); err != nil { return nil, err } } @@ -314,14 +315,14 @@ func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath, newName // However, in all cases the resulting edits will be the same. type seenPackageRename map[seenPackageKey]bool type seenPackageKey struct { - uri span.URI - importPath string + uri span.URI + path PackagePath } // add reports whether uri and importPath have been seen, and records them as // seen if not. -func (s seenPackageRename) add(uri span.URI, importPath string) bool { - key := seenPackageKey{uri, importPath} +func (s seenPackageRename) add(uri span.URI, path PackagePath) bool { + key := seenPackageKey{uri, path} seen := s[key] if !seen { s[key] = true @@ -336,7 +337,7 @@ func (s seenPackageRename) add(uri span.URI, importPath string) bool { // package clause has already been updated, to prevent duplicate edits. // // Edits are written into the edits map. -func renamePackageClause(ctx context.Context, m Metadata, s Snapshot, newName string, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { +func renamePackageClause(ctx context.Context, m Metadata, s Snapshot, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { pkg, err := s.WorkspacePackageByID(ctx, m.PackageID()) if err != nil { return err @@ -358,7 +359,7 @@ func renamePackageClause(ctx context.Context, m Metadata, s Snapshot, newName st } edits[f.URI] = append(edits[f.URI], protocol.TextEdit{ Range: rng, - NewText: newName, + NewText: string(newName), }) } @@ -370,7 +371,7 @@ func renamePackageClause(ctx context.Context, m Metadata, s Snapshot, newName st // newPath and name newName. // // Edits are written into the edits map. -func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath, newName string, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { +func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath ImportPath, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { // TODO(rfindley): we should get reverse dependencies as metadata first, // rather then building the package immediately. We don't need reverse // dependencies if they are intermediate test variants. @@ -399,7 +400,8 @@ func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath, newName } for _, imp := range f.File.Imports { - if impPath, _ := strconv.Unquote(imp.Path.Value); impPath != m.PackagePath() { + // TODO(adonovan): what if RHS has "vendor/" prefix? + if UnquoteImportPath(imp) != ImportPath(m.PackagePath()) { continue // not the import we're looking for } @@ -409,10 +411,9 @@ func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath, newName if err != nil { return err } - newText := strconv.Quote(newPath) edits[f.URI] = append(edits[f.URI], protocol.TextEdit{ Range: rng, - NewText: newText, + NewText: strconv.Quote(string(newPath)), }) // If the package name of an import has not changed or if its import @@ -430,7 +431,7 @@ func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath, newName fileScope := dep.GetTypesInfo().Scopes[f.File] var changes map[span.URI][]protocol.TextEdit - localName := newName + localName := string(newName) try := 0 // Keep trying with fresh names until one succeeds. @@ -446,7 +447,7 @@ func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath, newName // If the chosen local package name matches the package's new name, delete the // change that would have inserted an explicit local name, which is always // the lexically first change. - if localName == newName { + if localName == string(newName) { v := changes[f.URI] sort.Slice(v, func(i, j int) bool { return protocol.CompareRange(v[i].Range, v[j].Range) < 0 diff --git a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go index c4d5709bf74..b81b6f68566 100644 --- a/gopls/internal/lsp/source/rename_check.go +++ b/gopls/internal/lsp/source/rename_check.go @@ -12,7 +12,6 @@ import ( "go/token" "go/types" "reflect" - "strconv" "strings" "unicode" @@ -834,8 +833,8 @@ func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Po if imp == nil { continue } - importPath, err := strconv.Unquote(imp.Path.Value) - if err != nil { + importPath := UnquoteImportPath(imp) + if importPath == "" { continue } imported, err := pkg.ResolveImportPath(importPath) diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 28a2e2b23d3..8e3d8567d0a 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -194,7 +194,7 @@ func deducePkgFromTypes(ctx context.Context, snapshot Snapshot, ifaceObj types.O return nil, err } for _, p := range pkgs { - if p.PkgPath() == ifaceObj.Pkg().Path() { + if p.PkgPath() == PackagePath(ifaceObj.Pkg().Path()) { return p, nil } } @@ -254,11 +254,11 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method for i := 0; i < iface.NumEmbeddeds(); i++ { eiface := iface.Embedded(i).Obj() depPkg := ifacePkg - if eiface.Pkg().Path() != ifacePkg.PkgPath() { + if path := PackagePath(eiface.Pkg().Path()); path != ifacePkg.PkgPath() { // TODO(adonovan): I'm not sure what this is trying to do, but it // looks wrong the in case of type aliases. var err error - depPkg, err = ifacePkg.DirectDep(eiface.Pkg().Path()) + depPkg, err = ifacePkg.DirectDep(path) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index c933fbadd00..3ad17c0a842 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -314,7 +314,7 @@ func CompareDiagnostic(a, b *Diagnostic) int { // findFileInDeps finds uri in pkg or its dependencies. func findFileInDeps(pkg Package, uri span.URI) (*ParsedGoFile, Package, error) { queue := []Package{pkg} - seen := make(map[string]bool) + seen := make(map[PackageID]bool) for len(queue) > 0 { pkg := queue[0] @@ -333,14 +333,14 @@ func findFileInDeps(pkg Package, uri span.URI) (*ParsedGoFile, Package, error) { return nil, nil, fmt.Errorf("no file for %s in package %s", uri, pkg.ID()) } -// ImportPath returns the unquoted import path of s, +// UnquoteImportPath returns the unquoted import path of s, // or "" if the path is not properly quoted. -func ImportPath(s *ast.ImportSpec) string { - t, err := strconv.Unquote(s.Path.Value) +func UnquoteImportPath(s *ast.ImportSpec) ImportPath { + path, err := strconv.Unquote(s.Path.Value) if err != nil { return "" } - return t + return ImportPath(path) } // NodeContains returns true if a node encloses a given position pos. @@ -532,14 +532,14 @@ func InDirLex(dir, path string) bool { // IsValidImport returns whether importPkgPath is importable // by pkgPath -func IsValidImport(pkgPath, importPkgPath string) bool { +func IsValidImport(pkgPath, importPkgPath PackagePath) bool { i := strings.LastIndex(string(importPkgPath), "/internal/") if i == -1 { return true } // TODO(rfindley): this looks wrong: IsCommandLineArguments is meant to // operate on package IDs, not package paths. - if IsCommandLineArguments(string(pkgPath)) { + if IsCommandLineArguments(PackageID(pkgPath)) { return true } // TODO(rfindley): this is wrong. mod.testx/p should not be able to @@ -551,10 +551,8 @@ func IsValidImport(pkgPath, importPkgPath string) bool { // "command-line-arguments" package, which is a package with an unknown ID // created by the go command. It can have a test variant, which is why callers // should not check that a value equals "command-line-arguments" directly. -// -// TODO(rfindley): this should accept a PackageID. -func IsCommandLineArguments(s string) bool { - return strings.Contains(s, "command-line-arguments") +func IsCommandLineArguments(id PackageID) bool { + return strings.Contains(string(id), "command-line-arguments") } // RecvIdent returns the type identifier of a method receiver. diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 54c942f1d34..be2b7f6a428 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -84,7 +84,7 @@ type Snapshot interface { DiagnosePackage(ctx context.Context, pkg Package) (map[span.URI][]*Diagnostic, error) // Analyze runs the analyses for the given package at this snapshot. - Analyze(ctx context.Context, pkgID string, analyzers []*Analyzer) ([]*Diagnostic, error) + Analyze(ctx context.Context, pkgID PackageID, analyzers []*Analyzer) ([]*Diagnostic, error) // RunGoCommandPiped runs the given `go` command, writing its output // to stdout and stderr. Verb, Args, and WorkingDir must be specified. @@ -149,12 +149,12 @@ type Snapshot interface { // GetActiveReverseDeps returns the active files belonging to the reverse // dependencies of this file's package, checked in TypecheckWorkspace mode. - GetReverseDependencies(ctx context.Context, id string) ([]Package, error) + GetReverseDependencies(ctx context.Context, id PackageID) ([]Package, error) // CachedImportPaths returns all the imported packages loaded in this // snapshot, indexed by their package path (not import path, despite the name) // and checked in TypecheckWorkspace mode. - CachedImportPaths(ctx context.Context) (map[string]Package, error) + CachedImportPaths(ctx context.Context) (map[PackagePath]Package, error) // KnownPackages returns all the packages loaded in this snapshot, checked // in TypecheckWorkspace mode. @@ -171,7 +171,7 @@ type Snapshot interface { // WorkspacePackageByID returns the workspace package with id, type checked // in 'workspace' mode. - WorkspacePackageByID(ctx context.Context, id string) (Package, error) + WorkspacePackageByID(ctx context.Context, id PackageID) (Package, error) // Symbols returns all symbols in the snapshot. Symbols(ctx context.Context) map[span.URI][]Symbol @@ -338,17 +338,15 @@ type TidiedModule struct { } // Metadata represents package metadata retrieved from go/packages. -// -// TODO(rfindley): move the strongly typed strings from the cache package here. type Metadata interface { // PackageID is the unique package id. - PackageID() string + PackageID() PackageID // PackageName is the package name. - PackageName() string + PackageName() PackageName // PackagePath is the package path. - PackagePath() string + PackagePath() PackagePath // ModuleInfo returns the go/packages module information for the given package. ModuleInfo() *packages.Module @@ -593,12 +591,23 @@ func (a Analyzer) IsEnabled(view View) bool { return a.Enabled } +// Declare explicit types for package paths, names, and IDs to ensure that we +// never use an ID where a path belongs, and vice versa. If we confused these, +// it would result in confusing errors because package IDs often look like +// package paths. +type ( + PackageID string // go list's unique identifier for a package (e.g. "vendor/example.com/foo [vendor/example.com/bar.test]") + PackagePath string // name used to prefix linker symbols (e.g. "vendor/example.com/foo") + PackageName string // identifier in 'package' declaration (e.g. "foo") + ImportPath string // path that appears in an import declaration (e.g. "example.com/foo") +) + // Package represents a Go package that has been type-checked. It maintains // only the relevant fields of a *go/packages.Package. type Package interface { - ID() string // logically a cache.PackageID - Name() string // logically a cache.PackageName - PkgPath() string // logically a cache.PackagePath + ID() PackageID + Name() PackageName + PkgPath() PackagePath CompiledGoFiles() []*ParsedGoFile File(uri span.URI) (*ParsedGoFile, error) GetSyntax() []*ast.File @@ -606,9 +615,9 @@ type Package interface { GetTypesInfo() *types.Info GetTypesSizes() types.Sizes ForTest() string - DirectDep(packagePath string) (Package, error) // logically a cache.PackagePath - ResolveImportPath(importPath string) (Package, error) // logically a cache.ImportPath - MissingDependencies() []string // unordered; logically cache.ImportPaths + DirectDep(path PackagePath) (Package, error) + ResolveImportPath(path ImportPath) (Package, error) + MissingDependencies() []ImportPath // unordered Imports() []Package Version() *module.Version HasListOrParseErrors() bool diff --git a/gopls/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go index ee4e020e257..dfb5f39dc15 100644 --- a/gopls/internal/lsp/source/workspace_symbol.go +++ b/gopls/internal/lsp/source/workspace_symbol.go @@ -92,7 +92,7 @@ type symbolizer func(space []string, name string, pkg Metadata, m matcherFunc) ( func fullyQualifiedSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { if _, score := dynamicSymbolMatch(space, name, pkg, matcher); score > 0 { - return append(space, pkg.PackagePath(), ".", name), score + return append(space, string(pkg.PackagePath()), ".", name), score } return nil, 0 } @@ -106,12 +106,12 @@ func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher match var score float64 - endsInPkgName := strings.HasSuffix(pkg.PackagePath(), pkg.PackageName()) + endsInPkgName := strings.HasSuffix(string(pkg.PackagePath()), string(pkg.PackageName())) // If the package path does not end in the package name, we need to check the // package-qualified symbol as an extra pass first. if !endsInPkgName { - pkgQualified := append(space, pkg.PackageName(), ".", name) + pkgQualified := append(space, string(pkg.PackageName()), ".", name) idx, score := matcher(pkgQualified) nameStart := len(pkg.PackageName()) + 1 if score > 0 { @@ -126,7 +126,7 @@ func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher match } // Now try matching the fully qualified symbol. - fullyQualified := append(space, pkg.PackagePath(), ".", name) + fullyQualified := append(space, string(pkg.PackagePath()), ".", name) idx, score := matcher(fullyQualified) // As above, check if we matched just the unqualified symbol name. @@ -141,7 +141,7 @@ func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher match if endsInPkgName && idx >= 0 { pkgStart := len(pkg.PackagePath()) - len(pkg.PackageName()) if idx >= pkgStart { - return append(space, pkg.PackageName(), ".", name), score + return append(space, string(pkg.PackageName()), ".", name), score } } @@ -151,7 +151,7 @@ func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher match } func packageSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { - qualified := append(space, pkg.PackageName(), ".", name) + qualified := append(space, string(pkg.PackageName()), ".", name) if _, s := matcher(qualified); s > 0 { return qualified, s } @@ -526,7 +526,7 @@ func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, r kind: sym.Kind, uri: i.uri, rng: sym.Range, - container: i.md.PackagePath(), + container: string(i.md.PackagePath()), } store.store(si) } From 8f3241104a0805df0b0ad8ac064be120889b28ac Mon Sep 17 00:00:00 2001 From: Sasha Melentyev Date: Thu, 10 Nov 2022 16:53:41 +0000 Subject: [PATCH 422/723] cmd/stringer: replace ioutil with os MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I16c8556a90a775e5e3addb2f5907cba04d9c4bb1 GitHub-Last-Rev: 12dc5e78722f0512dbcb5fcedbf9d88b952b33a0 GitHub-Pull-Request: golang/tools#408 Reviewed-on: https://go-review.googlesource.com/c/tools/+/439516 gopls-CI: kokoro Reviewed-by: Bryan Mills TryBot-Result: Gopher Robot Reviewed-by: Daniel Martí Run-TryBot: Daniel Martí Reviewed-by: Michael Knyszek --- cmd/stringer/endtoend_test.go | 7 +++---- cmd/stringer/golden_test.go | 5 ++--- cmd/stringer/stringer.go | 3 +-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cmd/stringer/endtoend_test.go b/cmd/stringer/endtoend_test.go index 5b969a52e36..268c2f61be2 100644 --- a/cmd/stringer/endtoend_test.go +++ b/cmd/stringer/endtoend_test.go @@ -14,7 +14,6 @@ import ( "fmt" "go/build" "io" - "io/ioutil" "os" "os/exec" "path" @@ -113,7 +112,7 @@ func TestTags(t *testing.T) { if err != nil { t.Fatal(err) } - result, err := ioutil.ReadFile(output) + result, err := os.ReadFile(output) if err != nil { t.Fatal(err) } @@ -128,7 +127,7 @@ func TestTags(t *testing.T) { if err != nil { t.Fatal(err) } - result, err = ioutil.ReadFile(output) + result, err = os.ReadFile(output) if err != nil { t.Fatal(err) } @@ -184,7 +183,7 @@ func buildStringer(t *testing.T) (dir string, stringer string) { t.Helper() testenv.NeedsTool(t, "go") - dir, err := ioutil.TempDir("", "stringer") + dir, err := os.MkdirTemp("", "stringer") if err != nil { t.Fatal(err) } diff --git a/cmd/stringer/golden_test.go b/cmd/stringer/golden_test.go index b29763174b3..2e2ec58de69 100644 --- a/cmd/stringer/golden_test.go +++ b/cmd/stringer/golden_test.go @@ -10,7 +10,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" "strings" @@ -452,7 +451,7 @@ func (i Token) String() string { func TestGolden(t *testing.T) { testenv.NeedsTool(t, "go") - dir, err := ioutil.TempDir("", "stringer") + dir, err := os.MkdirTemp("", "stringer") if err != nil { t.Error(err) } @@ -466,7 +465,7 @@ func TestGolden(t *testing.T) { input := "package test\n" + test.input file := test.name + ".go" absFile := filepath.Join(dir, file) - err := ioutil.WriteFile(absFile, []byte(input), 0644) + err := os.WriteFile(absFile, []byte(input), 0644) if err != nil { t.Error(err) } diff --git a/cmd/stringer/stringer.go b/cmd/stringer/stringer.go index b079985b36c..998d1a51bfd 100644 --- a/cmd/stringer/stringer.go +++ b/cmd/stringer/stringer.go @@ -76,7 +76,6 @@ import ( "go/format" "go/token" "go/types" - "io/ioutil" "log" "os" "path/filepath" @@ -166,7 +165,7 @@ func main() { baseName := fmt.Sprintf("%s_string.go", types[0]) outputName = filepath.Join(dir, strings.ToLower(baseName)) } - err := ioutil.WriteFile(outputName, src, 0644) + err := os.WriteFile(outputName, src, 0644) if err != nil { log.Fatalf("writing output: %s", err) } From 13648cdeaf9ceb853cfba57e63c7ae5cfe4e7b7d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 7 Nov 2022 18:08:35 -0500 Subject: [PATCH 423/723] gopls/internal/lsp/cache: use Package.FileSet where possible This change adds a FileSet field to Package, and uses it in preference to Snapshot.FileSet wherever that is appropriate: all but a handful of places. For now, they must continue to refer to the same instance, but once we do away with the Snapshot's cache of parsed files, there will be no need for a global FileSet and each Package will be able to create its own. (Some care will be required to make sure it is always clear which package owns each each token.Pos/ast.Node/types.Object when there are several in play.) Updates golang/go#56291 Change-Id: I017eb794ffb737550da6ae88462d23a8f5c1e1e7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448377 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/analysis.go | 2 +- gopls/internal/lsp/cache/check.go | 6 +++-- gopls/internal/lsp/cache/errors.go | 21 ++++++++------- gopls/internal/lsp/cache/load.go | 5 ---- gopls/internal/lsp/cache/parse.go | 2 ++ gopls/internal/lsp/cache/pkg.go | 8 +++++- gopls/internal/lsp/command.go | 2 +- gopls/internal/lsp/completion.go | 10 +++---- gopls/internal/lsp/semantic.go | 2 +- gopls/internal/lsp/source/call_hierarchy.go | 2 +- .../lsp/source/completion/completion.go | 20 ++++++-------- .../internal/lsp/source/completion/format.go | 6 ++--- .../internal/lsp/source/completion/literal.go | 6 ++--- .../internal/lsp/source/completion/package.go | 3 ++- .../lsp/source/completion/postfix_snippets.go | 7 ++--- .../internal/lsp/source/completion/snippet.go | 2 +- .../lsp/source/completion/statements.go | 4 +-- gopls/internal/lsp/source/fix.go | 14 +++++----- gopls/internal/lsp/source/format.go | 3 +-- gopls/internal/lsp/source/highlight.go | 2 +- gopls/internal/lsp/source/hover.go | 8 +++--- gopls/internal/lsp/source/identifier.go | 25 +++++++++-------- gopls/internal/lsp/source/implementation.go | 22 +++++++++++---- gopls/internal/lsp/source/references.go | 4 +-- gopls/internal/lsp/source/rename.go | 10 +++---- gopls/internal/lsp/source/rename_check.go | 6 ++--- gopls/internal/lsp/source/signature_help.go | 2 +- gopls/internal/lsp/source/stub.go | 27 ++++++++++--------- gopls/internal/lsp/source/types_format.go | 19 ++++++------- gopls/internal/lsp/source/util.go | 11 ++++---- gopls/internal/lsp/source/view.go | 6 +++++ 31 files changed, 144 insertions(+), 123 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index cbb6f04e762..43f1afbe89c 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -286,7 +286,7 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a var rawDiagnostics []analysis.Diagnostic pass := &analysis.Pass{ Analyzer: analyzer, - Fset: snapshot.FileSet(), + Fset: pkg.FileSet(), Files: syntax, Pkg: pkg.GetTypes(), TypesInfo: pkg.GetTypesInfo(), diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 580ed0c87e2..4b1ea3debe7 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -450,6 +450,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil pkg := &pkg{ m: m, mode: mode, + fset: snapshot.FileSet(), // must match parse call below (snapshot.ParseGo for now) deps: make(map[PackageID]*pkg), types: types.NewPackage(string(m.PkgPath), string(m.Name)), typesInfo: &types.Info{ @@ -565,7 +566,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil // We passed typecheckCgo to go/packages when we Loaded. typesinternal.SetUsesCgo(cfg) - check := types.NewChecker(cfg, snapshot.FileSet(), pkg.types, pkg.typesInfo) + check := types.NewChecker(cfg, pkg.fset, pkg.types, pkg.typesInfo) var files []*ast.File for _, cgf := range pkg.compiledGoFiles { @@ -593,7 +594,7 @@ func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHand if mode == source.ParseFull { pgf, err = snapshot.ParseGo(ctx, fh, mode) } else { - pgf, err = parseGoImpl(ctx, snapshot.FileSet(), fh, mode) // ~20us/KB + pgf, err = parseGoImpl(ctx, pkg.fset, fh, mode) // ~20us/KB } if err != nil { return err @@ -661,6 +662,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost } allImports := map[string][]fileImport{} for _, cgf := range pkg.compiledGoFiles { + // TODO(adonovan): modify Imports() to accept a single token.File (cgf.Tok). for _, group := range astutil.Imports(s.FileSet(), cgf.File) { for _, imp := range group { if imp.Path == nil { diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index bca68d50491..1c9c21097e9 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -104,7 +104,7 @@ var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`) var unsupportedFeatureRe = regexp.MustCompile(`.*require.* go(\d+\.\d+) or later`) func typeErrorDiagnostics(snapshot *snapshot, pkg *pkg, e extendedError) ([]*source.Diagnostic, error) { - code, spn, err := typeErrorData(snapshot.FileSet(), pkg, e.primary) + code, spn, err := typeErrorData(pkg, e.primary) if err != nil { return nil, err } @@ -129,7 +129,7 @@ func typeErrorDiagnostics(snapshot *snapshot, pkg *pkg, e extendedError) ([]*sou } for _, secondary := range e.secondaries { - _, secondarySpan, err := typeErrorData(snapshot.FileSet(), pkg, secondary) + _, secondarySpan, err := typeErrorData(pkg, secondary) if err != nil { return nil, err } @@ -201,7 +201,7 @@ func analysisDiagnosticDiagnostics(snapshot *snapshot, pkg *pkg, a *analysis.Ana break } } - tokFile := snapshot.FileSet().File(e.Pos) + tokFile := pkg.fset.File(e.Pos) if tokFile == nil { return nil, bug.Errorf("no file for position of %q diagnostic", e.Category) } @@ -221,7 +221,7 @@ func analysisDiagnosticDiagnostics(snapshot *snapshot, pkg *pkg, a *analysis.Ana if len(srcAnalyzer.ActionKind) == 0 { kinds = append(kinds, protocol.QuickFix) } - fixes, err := suggestedAnalysisFixes(snapshot, pkg, e, kinds) + fixes, err := suggestedAnalysisFixes(pkg, e, kinds) if err != nil { return nil, err } @@ -238,7 +238,7 @@ func analysisDiagnosticDiagnostics(snapshot *snapshot, pkg *pkg, a *analysis.Ana fixes = append(fixes, source.SuggestedFixFromCommand(cmd, kind)) } } - related, err := relatedInformation(pkg, snapshot.FileSet(), e) + related, err := relatedInformation(pkg, e) if err != nil { return nil, err } @@ -289,12 +289,12 @@ func typesCodeHref(snapshot *snapshot, code typesinternal.ErrorCode) string { return source.BuildLink(target, "golang.org/x/tools/internal/typesinternal", code.String()) } -func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnostic, kinds []protocol.CodeActionKind) ([]source.SuggestedFix, error) { +func suggestedAnalysisFixes(pkg *pkg, diag *analysis.Diagnostic, kinds []protocol.CodeActionKind) ([]source.SuggestedFix, error) { var fixes []source.SuggestedFix for _, fix := range diag.SuggestedFixes { edits := make(map[span.URI][]protocol.TextEdit) for _, e := range fix.TextEdits { - tokFile := snapshot.FileSet().File(e.Pos) + tokFile := pkg.fset.File(e.Pos) if tokFile == nil { return nil, bug.Errorf("no file for edit position") } @@ -327,10 +327,10 @@ func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnos return fixes, nil } -func relatedInformation(pkg *pkg, fset *token.FileSet, diag *analysis.Diagnostic) ([]source.RelatedInformation, error) { +func relatedInformation(pkg *pkg, diag *analysis.Diagnostic) ([]source.RelatedInformation, error) { var out []source.RelatedInformation for _, related := range diag.Related { - tokFile := fset.File(related.Pos) + tokFile := pkg.fset.File(related.Pos) if tokFile == nil { return nil, bug.Errorf("no file for %q diagnostic position", diag.Category) } @@ -355,7 +355,7 @@ func relatedInformation(pkg *pkg, fset *token.FileSet, diag *analysis.Diagnostic return out, nil } -func typeErrorData(fset *token.FileSet, pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Span, error) { +func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Span, error) { ecode, start, end, ok := typesinternal.ReadGo116ErrorData(terr) if !ok { start, end = terr.Pos, terr.Pos @@ -368,6 +368,7 @@ func typeErrorData(fset *token.FileSet, pkg *pkg, terr types.Error) (typesintern } // go/types errors retain their FileSet. // Sanity-check that we're using the right one. + fset := pkg.FileSet() if fset != terr.Fset { return 0, span.Span{}, bug.Errorf("wrong FileSet for type error") } diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 5250a67f141..e779cbac4a6 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -50,11 +50,6 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc for _, scope := range scopes { switch scope := scope.(type) { case packageLoadScope: - // TODO(adonovan): is this cast sound?? A - // packageLoadScope is really a PackagePath I think. - if source.IsCommandLineArguments(PackageID(scope)) { - panic("attempted to load command-line-arguments") - } // The only time we pass package paths is when we're doing a // partial workspace load. In those cases, the paths came back from // go list and should already be GOPATH-vendorized when appropriate. diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index 5bac8713153..4f905e96f7a 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -36,6 +36,8 @@ type parseKey struct { // ParseGo parses the file whose contents are provided by fh, using a cache. // The resulting tree may have be fixed up. // +// Token position information will be added to the snapshot's FileSet. +// // The parser mode must not be ParseExported: that mode is used during // type checking to destructively trim the tree to reduce work, // which is not safe for values from a shared cache. diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index ef8177871b4..6e1233a37b4 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -8,6 +8,7 @@ import ( "fmt" "go/ast" "go/scanner" + "go/token" "go/types" "sort" @@ -21,6 +22,7 @@ import ( type pkg struct { m *Metadata mode source.ParseMode + fset *token.FileSet // for now, same as the snapshot's FileSet goFiles []*source.ParsedGoFile compiledGoFiles []*source.ParsedGoFile diagnostics []*source.Diagnostic @@ -45,7 +47,7 @@ type loadScope interface { type ( fileLoadScope span.URI // load packages containing a file (including command-line-arguments) - packageLoadScope string // load a specific package + packageLoadScope string // load a specific package (the value is its PackageID) moduleLoadScope string // load packages in a specific module viewLoadScope span.URI // load the workspace ) @@ -90,6 +92,10 @@ func (p *pkg) GetSyntax() []*ast.File { return syntax } +func (p *pkg) FileSet() *token.FileSet { + return p.fset +} + func (p *pkg) GetTypes() *types.Package { return p.types } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 3b0d3b849de..a3d93e265cb 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -757,7 +757,7 @@ func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) ( if err != nil { return err } - for _, group := range astutil.Imports(deps.snapshot.FileSet(), pgf.File) { + for _, group := range astutil.Imports(pkg.FileSet(), pgf.File) { for _, imp := range group { if imp.Path == nil { continue diff --git a/gopls/internal/lsp/completion.go b/gopls/internal/lsp/completion.go index 465526668a6..c967c1faa22 100644 --- a/gopls/internal/lsp/completion.go +++ b/gopls/internal/lsp/completion.go @@ -9,14 +9,14 @@ import ( "fmt" "strings" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/lsppos" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/completion" "golang.org/x/tools/gopls/internal/lsp/template" "golang.org/x/tools/gopls/internal/lsp/work" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" ) func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { @@ -64,9 +64,9 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara if err != nil { return nil, err } - tf := snapshot.FileSet().File(surrounding.Start()) - mapper := lsppos.NewTokenMapper(src, tf) - rng, err := mapper.Range(surrounding.Start(), surrounding.End()) + srng := surrounding.Range() + tf := snapshot.FileSet().File(srng.Start) // not same as srng.TokFile due to //line + rng, err := lsppos.NewTokenMapper(src, tf).Range(srng.Start, srng.End) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 75f88b3efa2..4cdca20556b 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -112,7 +112,7 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu rng: rng, ti: pkg.GetTypesInfo(), pkg: pkg, - fset: snapshot.FileSet(), + fset: pkg.FileSet(), tokTypes: s.session.Options().SemanticTypes, tokMods: s.session.Options().SemanticMods, noStrings: vv.Options().NoSemanticString, diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index 1097d629072..5c5c584fd9a 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -205,7 +205,7 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr // TODO(adonovan): avoid Fileset.File call by somehow getting at // declMappedRange.spanRange.TokFile, or making Identifier retain the // token.File of the identifier and its declaration, since it looks up both anyway. - tokFile := snapshot.FileSet().File(node.Pos()) + tokFile := identifier.pkg.FileSet().File(node.Pos()) if tokFile == nil { return nil, fmt.Errorf("no file for position") } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 1f8dd929b2f..1260e0e7c3b 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -289,7 +289,7 @@ type completionContext struct { // A Selection represents the cursor position and surrounding identifier. type Selection struct { content string - cursor token.Pos + cursor token.Pos // relative to rng.TokFile rng span.Range } @@ -297,12 +297,8 @@ func (p Selection) Content() string { return p.content } -func (p Selection) Start() token.Pos { - return p.rng.Start -} - -func (p Selection) End() token.Pos { - return p.rng.End +func (p Selection) Range() span.Range { + return p.rng } func (p Selection) Prefix() string { @@ -693,7 +689,7 @@ func (c *completer) containingIdent(src []byte) *ast.Ident { // scanToken scans pgh's contents for the token containing pos. func (c *completer) scanToken(contents []byte) (token.Pos, token.Token, string) { - tok := c.snapshot.FileSet().File(c.pos) + tok := c.pkg.FileSet().File(c.pos) var s scanner.Scanner s.Init(tok, contents, nil, 0) @@ -879,7 +875,7 @@ func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast } // Using the comment position find the line after - file := c.snapshot.FileSet().File(comment.End()) + file := c.pkg.FileSet().File(comment.End()) if file == nil { return } @@ -1246,7 +1242,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.snapshot.FileSet()) { + if c.fuzz(typ, mset, imp, cb, c.pkg.FileSet()) { return } } @@ -1331,7 +1327,7 @@ func (c *completer) lexical(ctx context.Context) error { node = c.path[i-1] } if node != nil { - if resolved := resolveInvalid(c.snapshot.FileSet(), obj, node, c.pkg.GetTypesInfo()); resolved != nil { + if resolved := resolveInvalid(c.pkg.FileSet(), obj, node, c.pkg.GetTypesInfo()); resolved != nil { obj = resolved } } @@ -2033,7 +2029,7 @@ Nodes: // // TODO: remove this after https://go.dev/issue/52503 info := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)} - types.CheckExpr(c.snapshot.FileSet(), c.pkg.GetTypes(), node.Fun.Pos(), node.Fun, info) + types.CheckExpr(c.pkg.FileSet(), c.pkg.GetTypes(), node.Fun.Pos(), node.Fun, info) sig, _ = info.Types[node.Fun].Type.(*types.Signature) } diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go index c2693dc12d4..2ff449a454c 100644 --- a/gopls/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -80,7 +80,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e if _, ok := obj.Type().(*types.Struct); ok { detail = "struct{...}" // for anonymous structs } else if obj.IsField() { - detail = source.FormatVarType(c.snapshot.FileSet(), c.pkg, obj, c.qf) + detail = source.FormatVarType(c.pkg, obj, c.qf) } if obj.IsField() { kind = protocol.FieldCompletion @@ -227,7 +227,7 @@ Suffixes: if !c.opts.documentation { return item, nil } - pos := c.snapshot.FileSet().Position(obj.Pos()) + pos := c.pkg.FileSet().Position(obj.Pos()) // We ignore errors here, because some types, like "unsafe" or "error", // may not have valid positions that we can use to get documentation. @@ -237,7 +237,7 @@ Suffixes: uri := span.URIFromPath(pos.Filename) // Find the source file of the candidate. - pkg, err := source.FindPackageFromPos(c.snapshot.FileSet(), c.pkg, obj.Pos()) + pkg, err := source.FindPackageFromPos(c.pkg, obj.Pos()) if err != nil { return item, nil } diff --git a/gopls/internal/lsp/source/completion/literal.go b/gopls/internal/lsp/source/completion/literal.go index 0a9fc83e66f..2975c8fe592 100644 --- a/gopls/internal/lsp/source/completion/literal.go +++ b/gopls/internal/lsp/source/completion/literal.go @@ -202,7 +202,7 @@ func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) { // If the param has no name in the signature, guess a name based // on the type. Use an empty qualifier to ignore the package. // For example, we want to name "http.Request" "r", not "hr". - name = source.FormatVarType(c.snapshot.FileSet(), c.pkg, p, func(p *types.Package) string { + name = source.FormatVarType(c.pkg, p, func(p *types.Package) string { return "" }) name = abbreviateTypeName(name) @@ -264,7 +264,7 @@ func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) { // of "i int, j int". if i == sig.Params().Len()-1 || !types.Identical(p.Type(), sig.Params().At(i+1).Type()) { snip.WriteText(" ") - typeStr := source.FormatVarType(c.snapshot.FileSet(), c.pkg, p, c.qf) + typeStr := source.FormatVarType(c.pkg, p, c.qf) if sig.Variadic() && i == sig.Params().Len()-1 { typeStr = strings.Replace(typeStr, "[]", "...", 1) } @@ -314,7 +314,7 @@ func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) { snip.WriteText(name + " ") } - text := source.FormatVarType(c.snapshot.FileSet(), c.pkg, r, c.qf) + text := source.FormatVarType(c.pkg, r, c.qf) if tp, _ := r.Type().(*typeparams.TypeParam); tp != nil && !c.typeParamInScope(tp) { snip.WritePlaceholder(func(snip *snippet.Builder) { snip.WriteText(text) diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index 1b7c731fd81..19c52491cef 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -31,6 +31,7 @@ import ( func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) ([]CompletionItem, *Selection, error) { // 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. + fset := snapshot.FileSet() pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) if err != nil { return nil, nil, err @@ -41,7 +42,7 @@ func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh return nil, nil, err } - surrounding, err := packageCompletionSurrounding(snapshot.FileSet(), pgf, pos) + surrounding, err := packageCompletionSurrounding(fset, pgf, pos) if err != nil { return nil, nil, fmt.Errorf("invalid position for package completion: %w", err) } diff --git a/gopls/internal/lsp/source/completion/postfix_snippets.go b/gopls/internal/lsp/source/completion/postfix_snippets.go index 3c805a77e1f..07d07235df0 100644 --- a/gopls/internal/lsp/source/completion/postfix_snippets.go +++ b/gopls/internal/lsp/source/completion/postfix_snippets.go @@ -322,7 +322,7 @@ func (c *completer) addPostfixSnippetCandidates(ctx context.Context, sel *ast.Se return } - tokFile := c.snapshot.FileSet().File(c.pos) + tokFile := c.pkg.FileSet().File(c.pos) // Only replace sel with a statement if sel is already a statement. var stmtOK bool @@ -379,7 +379,7 @@ func (c *completer) addPostfixSnippetCandidates(ctx context.Context, sel *ast.Se } tmplArgs := postfixTmplArgs{ - X: source.FormatNode(c.snapshot.FileSet(), sel.X), + X: source.FormatNode(c.pkg.FileSet(), sel.X), StmtOK: stmtOK, Obj: exprObj(c.pkg.GetTypesInfo(), sel.X), Type: selType, @@ -442,7 +442,8 @@ func (c *completer) importIfNeeded(pkgPath string, scope *types.Scope) (string, // Check if file already imports pkgPath. for _, s := range c.file.Imports { - // TODO(adonovan): what if pkgPath has a vendor/ prefix? + // TODO(adonovan): what if pkgPath has a vendor/ suffix? + // This may be the cause of go.dev/issue/56291. if source.UnquoteImportPath(s) == source.ImportPath(pkgPath) { if s.Name == nil { return defaultName, nil, nil diff --git a/gopls/internal/lsp/source/completion/snippet.go b/gopls/internal/lsp/source/completion/snippet.go index 1a9ebb1d4be..751f61b7a25 100644 --- a/gopls/internal/lsp/source/completion/snippet.go +++ b/gopls/internal/lsp/source/completion/snippet.go @@ -39,7 +39,7 @@ func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snip } }) - fset := c.snapshot.FileSet() + fset := c.pkg.FileSet() // If the cursor position is on a different line from the literal's opening brace, // we are in a multiline literal. diff --git a/gopls/internal/lsp/source/completion/statements.go b/gopls/internal/lsp/source/completion/statements.go index 1f80193f194..dcce76ff76e 100644 --- a/gopls/internal/lsp/source/completion/statements.go +++ b/gopls/internal/lsp/source/completion/statements.go @@ -52,7 +52,7 @@ func (c *completer) addAssignAppend() { // needsLHS is true if we need to prepend the LHS slice name and // "=" to our candidate. needsLHS = false - fset = c.snapshot.FileSet() + fset = c.pkg.FileSet() ) switch n := c.path[1].(type) { @@ -213,7 +213,7 @@ func (c *completer) addErrCheck() { var ( // errVar is e.g. "err" in "foo, err := bar()". - errVar = source.FormatNode(c.snapshot.FileSet(), lastAssignee) + errVar = source.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/fix.go b/gopls/internal/lsp/source/fix.go index a4c6676cf05..64498e22b83 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -26,7 +26,7 @@ type ( // suggested fixes with their diagnostics, so we have to compute them // separately. Such analyzers should provide a function with a signature of // SuggestedFixFunc. - SuggestedFixFunc func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*analysis.SuggestedFix, error) + SuggestedFixFunc func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) singleFileFixFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) ) @@ -51,12 +51,13 @@ var suggestedFixes = map[string]SuggestedFixFunc{ // singleFile calls analyzers that expect inputs for a single file func singleFile(sf singleFileFixFunc) SuggestedFixFunc { - return func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*analysis.SuggestedFix, error) { + return func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { fset, rng, src, file, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng) if err != nil { - return nil, err + return nil, nil, err } - return sf(fset, rng, src, file, pkg, info) + fix, err := sf(fset, rng, src, file, pkg, info) + return fset, fix, err } } @@ -75,14 +76,13 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi if !ok { return nil, fmt.Errorf("no suggested fix function for %s", fix) } - suggestion, err := handler(ctx, snapshot, fh, pRng) + fset, suggestion, err := handler(ctx, snapshot, fh, pRng) if err != nil { return nil, err } if suggestion == nil { return nil, nil } - fset := snapshot.FileSet() editsPerFile := map[span.URI]*protocol.TextDocumentEdit{} for _, edit := range suggestion.TextEdits { tokFile := fset.File(edit.Pos) @@ -140,5 +140,5 @@ func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHan if err != nil { return nil, span.Range{}, nil, nil, nil, nil, err } - return snapshot.FileSet(), rng, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo(), nil + return pkg.FileSet(), rng, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo(), nil } diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index b713893fbab..a83c9e471e2 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -34,6 +34,7 @@ func Format(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.T return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Filename()) } + fset := snapshot.FileSet() pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, err @@ -49,8 +50,6 @@ func Format(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.T return computeTextEdits(ctx, snapshot, pgf, string(formatted)) } - fset := snapshot.FileSet() - // format.Node changes slightly from one release to another, so the version // of Go used to build the LSP server will determine how it formats code. // This should be acceptable for all users, who likely be prompted to rebuild diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index 5ec71d075a8..166dc3cd46b 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -59,7 +59,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p } var ranges []protocol.Range for rng := range result { - mRng, err := posToMappedRange(snapshot.FileSet(), pkg, rng.start, rng.end) + mRng, err := posToMappedRange(pkg, rng.start, rng.end) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 0bd5a3055e9..26d5e23ad34 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -237,7 +237,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr return 0, MappedRange{}, ErrNoRuneFound } - mappedRange, err := posToMappedRange(snapshot.FileSet(), pkg, start, end) + mappedRange, err := posToMappedRange(pkg, start, end) if err != nil { return 0, MappedRange{}, err } @@ -258,7 +258,7 @@ func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) Synopsis: doc.Synopsis(hoverCtx.Comment.Text()), } - fset := i.Snapshot.FileSet() + fset := i.pkg.FileSet() // Determine the symbol's signature. switch x := hoverCtx.signatureSource.(type) { case string: @@ -564,7 +564,7 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob } // obj may not have been produced by type checking the AST containing // node, so we need to be careful about using token.Pos. - tok := s.FileSet().File(obj.Pos()) + tok := pkg.FileSet().File(obj.Pos()) offset, err := safetoken.Offset(tok, obj.Pos()) if err != nil { return nil, err @@ -572,7 +572,7 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob // fullTok and fullPos are the *token.File and object position in for the // full AST. - fullTok := s.FileSet().File(node.Pos()) + fullTok := pkg.FileSet().File(node.Pos()) fullPos, err := safetoken.Pos(fullTok, offset) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 526c1084706..53ad88bc337 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -26,7 +26,7 @@ import ( // IdentifierInfo holds information about an identifier in Go source. type IdentifierInfo struct { Name string - Snapshot Snapshot + Snapshot Snapshot // only needed for .View(); TODO(adonovan): reduce. MappedRange Type struct { @@ -122,7 +122,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // Special case for package declarations, since they have no // corresponding types.Object. if ident == file.Name { - rng, err := posToMappedRange(snapshot.FileSet(), pkg, file.Name.Pos(), file.Name.End()) + rng, err := posToMappedRange(pkg, file.Name.Pos(), file.Name.End()) if err != nil { return nil, err } @@ -136,7 +136,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if declAST == nil { declAST = file } - declRng, err := posToMappedRange(snapshot.FileSet(), pkg, declAST.Name.Pos(), declAST.Name.End()) + declRng, err := posToMappedRange(pkg, declAST.Name.Pos(), declAST.Name.End()) if err != nil { return nil, err } @@ -164,7 +164,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa result.Name = result.ident.Name var err error - if result.MappedRange, err = posToMappedRange(snapshot.FileSet(), pkg, result.ident.Pos(), result.ident.End()); err != nil { + if result.MappedRange, err = posToMappedRange(pkg, result.ident.Pos(), result.ident.End()); err != nil { return nil, err } @@ -263,13 +263,13 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa } } - rng, err := objToMappedRange(snapshot.FileSet(), pkg, result.Declaration.obj) + rng, err := objToMappedRange(pkg, result.Declaration.obj) if err != nil { return nil, err } result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) - declPkg, err := FindPackageFromPos(snapshot.FileSet(), pkg, result.Declaration.obj.Pos()) + declPkg, err := FindPackageFromPos(pkg, result.Declaration.obj.Pos()) if err != nil { return nil, err } @@ -277,7 +277,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // Ensure that we have the full declaration, in case the declaration was // parsed in ParseExported and therefore could be missing information. - if result.Declaration.fullDecl, err = fullNode(snapshot, result.Declaration.obj, declPkg); err != nil { + if result.Declaration.fullDecl, err = fullNode(pkg.FileSet(), result.Declaration.obj, declPkg); err != nil { return nil, err } typ := pkg.GetTypesInfo().TypeOf(result.ident) @@ -293,7 +293,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if hasErrorType(result.Type.Object) { return result, nil } - if result.Type.MappedRange, err = objToMappedRange(snapshot.FileSet(), pkg, result.Type.Object); err != nil { + if result.Type.MappedRange, err = objToMappedRange(pkg, result.Type.Object); err != nil { return nil, err } } @@ -315,9 +315,9 @@ func findGenDecl(f *ast.File, spec ast.Spec) *ast.GenDecl { // fullNode tries to extract the full spec corresponding to obj's declaration. // If the package was not parsed in full, the declaration file will be // re-parsed to ensure it has complete syntax. -func fullNode(snapshot Snapshot, obj types.Object, pkg Package) (ast.Decl, error) { +func fullNode(fset *token.FileSet, obj types.Object, pkg Package) (ast.Decl, error) { // declaration in a different package... make sure we have full AST information. - tok := snapshot.FileSet().File(obj.Pos()) + tok := fset.File(obj.Pos()) uri := span.URIFromPath(tok.Name()) pgf, err := pkg.File(uri) if err != nil { @@ -326,7 +326,6 @@ func fullNode(snapshot Snapshot, obj types.Object, pkg Package) (ast.Decl, error file := pgf.File pos := obj.Pos() if pgf.Mode != ParseFull { - fset := snapshot.FileSet() file2, _ := parser.ParseFile(fset, tok.Name(), pgf.Src, parser.AllErrors|parser.ParseComments) if file2 != nil { offset, err := safetoken.Offset(tok, obj.Pos()) @@ -465,13 +464,13 @@ func importSpec(snapshot Snapshot, pkg Package, file *ast.File, pos token.Pos) ( Name: importPath, // should this perhaps be imported.PkgPath()? pkg: pkg, } - if result.MappedRange, err = posToMappedRange(snapshot.FileSet(), pkg, imp.Path.Pos(), imp.Path.End()); err != nil { + if result.MappedRange, err = posToMappedRange(pkg, imp.Path.Pos(), imp.Path.End()); err != nil { return nil, err } // Consider the "declaration" of an import spec to be the imported package. // Return all of the files in the package as the definition of the import spec. for _, dst := range imported.GetSyntax() { - rng, err := posToMappedRange(snapshot.FileSet(), pkg, dst.Pos(), dst.End()) + rng, err := posToMappedRange(pkg, dst.Pos(), dst.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 87f435108d9..ca62f4e664d 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -32,7 +32,7 @@ func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp pro if impl.pkg == nil || len(impl.pkg.CompiledGoFiles()) == 0 { continue } - rng, err := objToMappedRange(snapshot.FileSet(), impl.pkg, impl.obj) + rng, err := objToMappedRange(impl.pkg, impl.obj) if err != nil { return nil, err } @@ -145,16 +145,28 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. candObj = sel.Obj() } - pos := s.FileSet().Position(candObj.Pos()) - if candObj == queryMethod || seen[pos] { + if candObj == queryMethod { continue } - seen[pos] = true + pkg := pkgs[candObj.Pkg()] // may be nil (e.g. error) + + // TODO(adonovan): the logic below assumes there is only one + // predeclared (pkg=nil) object of interest, the error type. + // That could change in a future version of Go. + + var posn token.Position + if pkg != nil { + posn = pkg.FileSet().Position(candObj.Pos()) + } + if seen[posn] { + continue + } + seen[posn] = true impls = append(impls, qualifiedObject{ obj: candObj, - pkg: pkgs[candObj.Pkg()], // may be nil (e.g. error) + pkg: pkg, }) } } diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 4db716e6dd4..d0310560c10 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -144,7 +144,7 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i } // Inv: qos[0].pkg != nil, since Pos is valid. // Inv: qos[*].pkg != nil, since all qos are logically the same declaration. - filename := snapshot.FileSet().Position(pos).Filename + filename := qos[0].pkg.FileSet().File(pos).Name() pgf, err := qos[0].pkg.File(span.URIFromPath(filename)) if err != nil { return nil, err @@ -208,7 +208,7 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i continue } seen[key] = true - rng, err := posToMappedRange(snapshot.FileSet(), pkg, ident.Pos(), ident.End()) + rng, err := posToMappedRange(pkg, ident.Pos(), ident.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 3a8d7cfb6ba..9586d8418cf 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -28,7 +28,6 @@ import ( type renamer struct { ctx context.Context - fset *token.FileSet refs []*ReferenceInfo objsToUpdate map[types.Object]bool hadConflicts bool @@ -136,7 +135,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot } func computePrepareRenameResp(snapshot Snapshot, pkg Package, node ast.Node, text string) (*PrepareItem, error) { - mr, err := posToMappedRange(snapshot.FileSet(), pkg, node.Pos(), node.End()) + mr, err := posToMappedRange(pkg, node.Pos(), node.End()) if err != nil { return nil, err } @@ -484,7 +483,6 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO } r := renamer{ ctx: ctx, - fset: s.FileSet(), refs: refs, objsToUpdate: make(map[types.Object]bool), from: obj.Name(), @@ -604,7 +602,7 @@ func (r *renamer) update() (map[span.URI][]diff.Edit, error) { // TODO(adonovan): why are we looping over lines? // Just run the loop body once over the entire multiline comment. lines := strings.Split(comment.Text, "\n") - tokFile := r.fset.File(comment.Pos()) + tokFile := ref.pkg.FileSet().File(comment.Pos()) commentLine := tokFile.Line(comment.Pos()) uri := span.URIFromPath(tokFile.Name()) for i, line := range lines { @@ -632,7 +630,7 @@ func (r *renamer) update() (map[span.URI][]diff.Edit, error) { // docComment returns the doc for an identifier. func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { - _, tokFile, nodes, _ := pathEnclosingInterval(r.fset, pkg, id.Pos(), id.End()) + _, tokFile, nodes, _ := pathEnclosingInterval(pkg, id.Pos(), id.End()) for _, node := range nodes { switch decl := node.(type) { case *ast.FuncDecl: @@ -685,7 +683,7 @@ func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.Edit, error) { // Modify ImportSpec syntax to add or remove the Name as needed. pkg := r.packages[pkgName.Pkg()] - _, tokFile, path, _ := pathEnclosingInterval(r.fset, pkg, pkgName.Pos(), pkgName.Pos()) + _, tokFile, path, _ := pathEnclosingInterval(pkg, pkgName.Pos(), pkgName.Pos()) if len(path) < 2 { return nil, fmt.Errorf("no path enclosing interval for %s", pkgName.Name()) } diff --git a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go index b81b6f68566..ce625dedd63 100644 --- a/gopls/internal/lsp/source/rename_check.go +++ b/gopls/internal/lsp/source/rename_check.go @@ -371,7 +371,7 @@ func (r *renamer) checkStructField(from *types.Var) { if !ok { return } - pkg, _, path, _ := pathEnclosingInterval(r.fset, fromPkg, from.Pos(), from.Pos()) + pkg, _, path, _ := pathEnclosingInterval(fromPkg, from.Pos(), from.Pos()) if pkg == nil || path == nil { return } @@ -826,7 +826,7 @@ func someUse(info *types.Info, obj types.Object) *ast.Ident { // exact is defined as for astutil.PathEnclosingInterval. // // The zero value is returned if not found. -func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Pos) (resPkg Package, tokFile *token.File, path []ast.Node, exact bool) { +func pathEnclosingInterval(pkg Package, start, end token.Pos) (resPkg Package, tokFile *token.File, path []ast.Node, exact bool) { pkgs := []Package{pkg} for _, f := range pkg.GetSyntax() { for _, imp := range f.Imports { @@ -852,7 +852,7 @@ func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Po // (Use parser.AllErrors to prevent that.) continue } - tokFile := fset.File(f.Pos()) + tokFile := p.FileSet().File(f.Pos()) if !tokenFileContainsPos(tokFile, start) { continue } diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index 68ac1beeb25..8c62a779619 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -94,7 +94,7 @@ FindCall: comment *ast.CommentGroup ) if obj != nil { - declPkg, err := FindPackageFromPos(snapshot.FileSet(), pkg, obj.Pos()) + declPkg, err := FindPackageFromPos(pkg, obj.Pos()) if err != nil { return nil, 0, err } diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 8e3d8567d0a..3dcdb0ec6ba 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -24,22 +24,22 @@ import ( "golang.org/x/tools/internal/typeparams" ) -func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, rng protocol.Range) (*analysis.SuggestedFix, error) { +func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { - return nil, fmt.Errorf("GetParsedFile: %w", err) + return nil, nil, fmt.Errorf("GetParsedFile: %w", err) } nodes, pos, err := getStubNodes(pgf, rng) if err != nil { - return nil, fmt.Errorf("getNodes: %w", err) + return nil, nil, fmt.Errorf("getNodes: %w", err) } si := stubmethods.GetStubInfo(pkg.GetTypesInfo(), nodes, pos) if si == nil { - return nil, fmt.Errorf("nil interface request") + return nil, nil, fmt.Errorf("nil interface request") } parsedConcreteFile, concreteFH, err := getStubFile(ctx, si.Concrete.Obj(), snapshot) if err != nil { - return nil, fmt.Errorf("getFile(concrete): %w", err) + return nil, nil, fmt.Errorf("getFile(concrete): %w", err) } var ( methodsSrc []byte @@ -51,16 +51,16 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi methodsSrc, stubImports, err = stubMethods(ctx, parsedConcreteFile.File, si, snapshot) } if err != nil { - return nil, fmt.Errorf("stubMethods: %w", err) + return nil, nil, fmt.Errorf("stubMethods: %w", err) } nodes, _ = astutil.PathEnclosingInterval(parsedConcreteFile.File, si.Concrete.Obj().Pos(), si.Concrete.Obj().Pos()) concreteSrc, err := concreteFH.Read() if err != nil { - return nil, fmt.Errorf("error reading concrete file source: %w", err) + return nil, nil, fmt.Errorf("error reading concrete file source: %w", err) } insertPos, err := safetoken.Offset(parsedConcreteFile.Tok, nodes[1].End()) if err != nil || insertPos >= len(concreteSrc) { - return nil, fmt.Errorf("insertion position is past the end of the file") + return nil, nil, fmt.Errorf("insertion position is past the end of the file") } var buf bytes.Buffer buf.Write(concreteSrc[:insertPos]) @@ -70,7 +70,7 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi fset := token.NewFileSet() newF, err := parser.ParseFile(fset, parsedConcreteFile.File.Name.Name, buf.Bytes(), parser.ParseComments) if err != nil { - return nil, fmt.Errorf("could not reparse file: %w", err) + return nil, nil, fmt.Errorf("could not reparse file: %w", err) } for _, imp := range stubImports { astutil.AddNamedImport(fset, newF, imp.Name, imp.Path) @@ -78,7 +78,7 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi var source bytes.Buffer err = format.Node(&source, fset, newF) if err != nil { - return nil, fmt.Errorf("format.Node: %w", err) + return nil, nil, fmt.Errorf("format.Node: %w", err) } diffs := snapshot.View().Options().ComputeEdits(string(parsedConcreteFile.Src), source.String()) tf := parsedConcreteFile.Mapper.TokFile @@ -90,9 +90,9 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi NewText: []byte(edit.New), }) } - return &analysis.SuggestedFix{ - TextEdits: edits, - }, nil + return snapshot.FileSet(), // from getStubFile + &analysis.SuggestedFix{TextEdits: edits}, + nil } // stubMethods returns the Go code of all methods @@ -304,6 +304,7 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method return missing, nil } +// Token position information for obj.Pos and the ParsedGoFile result is in Snapshot.FileSet. func getStubFile(ctx context.Context, obj types.Object, snapshot Snapshot) (*ParsedGoFile, VersionedFileHandle, error) { objPos := snapshot.FileSet().Position(obj.Pos()) objFile := span.URIFromPath(objPos.Filename) diff --git a/gopls/internal/lsp/source/types_format.go b/gopls/internal/lsp/source/types_format.go index f1c03cea658..e3a884d8542 100644 --- a/gopls/internal/lsp/source/types_format.go +++ b/gopls/internal/lsp/source/types_format.go @@ -86,6 +86,7 @@ func (s *signature) Params() []string { // NewBuiltinSignature returns signature for the builtin object with a given // name, if a builtin object with the name exists. func NewBuiltinSignature(ctx context.Context, s Snapshot, name string) (*signature, error) { + fset := s.FileSet() builtin, err := s.BuiltinFile(ctx) if err != nil { return nil, err @@ -109,8 +110,8 @@ func NewBuiltinSignature(ctx context.Context, s Snapshot, name string) (*signatu variadic = true } } - params, _ := formatFieldList(ctx, s, decl.Type.Params, variadic) - results, needResultParens := formatFieldList(ctx, s, decl.Type.Results, false) + params, _ := formatFieldList(ctx, fset, decl.Type.Params, variadic) + results, needResultParens := formatFieldList(ctx, fset, decl.Type.Results, false) d := decl.Doc.Text() switch s.View().Options().HoverKind { case SynopsisDocumentation: @@ -134,7 +135,7 @@ var replacer = strings.NewReplacer( `IntegerType`, `int`, ) -func formatFieldList(ctx context.Context, snapshot Snapshot, list *ast.FieldList, variadic bool) ([]string, bool) { +func formatFieldList(ctx context.Context, fset *token.FileSet, list *ast.FieldList, variadic bool) ([]string, bool) { if list == nil { return nil, false } @@ -147,7 +148,7 @@ func formatFieldList(ctx context.Context, snapshot Snapshot, list *ast.FieldList p := list.List[i] cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4} b := &bytes.Buffer{} - if err := cfg.Fprint(b, snapshot.FileSet(), p.Type); err != nil { + if err := cfg.Fprint(b, fset, p.Type); err != nil { event.Error(ctx, "unable to print type", nil, tag.Type.Of(p.Type)) continue } @@ -205,7 +206,7 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa params := make([]string, 0, sig.Params().Len()) for i := 0; i < sig.Params().Len(); i++ { el := sig.Params().At(i) - typ := FormatVarType(s.FileSet(), pkg, el, qf) + typ := FormatVarType(pkg, el, qf) p := typ if el.Name() != "" { p = el.Name() + " " + typ @@ -220,7 +221,7 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa needResultParens = true } el := sig.Results().At(i) - typ := FormatVarType(s.FileSet(), pkg, el, qf) + typ := FormatVarType(pkg, el, qf) if el.Name() == "" { results = append(results, typ) } else { @@ -253,8 +254,8 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa // FormatVarType formats a *types.Var, accounting for type aliases. // To do this, it looks in the AST of the file in which the object is declared. // On any errors, it always falls back to types.TypeString. -func FormatVarType(fset *token.FileSet, srcpkg Package, obj *types.Var, qf types.Qualifier) string { - pkg, err := FindPackageFromPos(fset, srcpkg, obj.Pos()) +func FormatVarType(srcpkg Package, obj *types.Var, qf types.Qualifier) string { + pkg, err := FindPackageFromPos(srcpkg, obj.Pos()) if err != nil { return types.TypeString(obj.Type(), qf) } @@ -283,7 +284,7 @@ func FormatVarType(fset *token.FileSet, srcpkg Package, obj *types.Var, qf types // If the request came from a different package than the one in which the // types are defined, we may need to modify the qualifiers. qualified = qualifyExpr(qualified, srcpkg, pkg, clonedInfo, qf) - fmted := FormatNode(fset, qualified) + fmted := FormatNode(srcpkg.FileSet(), qualified) return fmted } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 3ad17c0a842..d7ab5c579b7 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -125,7 +125,7 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { return false } -func objToMappedRange(fset *token.FileSet, pkg Package, obj types.Object) (MappedRange, error) { +func objToMappedRange(pkg Package, obj types.Object) (MappedRange, error) { nameLen := len(obj.Name()) if pkgName, ok := obj.(*types.PkgName); ok { // An imported Go package has a package-local, unqualified name. @@ -142,12 +142,12 @@ func objToMappedRange(fset *token.FileSet, pkg Package, obj types.Object) (Mappe nameLen = len(pkgName.Imported().Path()) + len(`""`) } } - return posToMappedRange(fset, pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen)) + return posToMappedRange(pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen)) } // posToMappedRange returns the MappedRange for the given [start, end) span, // which must be among the transitive dependencies of pkg. -func posToMappedRange(fset *token.FileSet, pkg Package, pos, end token.Pos) (MappedRange, error) { +func posToMappedRange(pkg Package, pos, end token.Pos) (MappedRange, error) { if !pos.IsValid() { return MappedRange{}, fmt.Errorf("invalid start position") } @@ -155,6 +155,7 @@ func posToMappedRange(fset *token.FileSet, pkg Package, pos, end token.Pos) (Map return MappedRange{}, fmt.Errorf("invalid end position") } + fset := pkg.FileSet() tokFile := fset.File(pos) // Subtle: it is not safe to simplify this to tokFile.Name // because, due to //line directives, a Position within a @@ -183,11 +184,11 @@ func posToMappedRange(fset *token.FileSet, pkg Package, pos, end token.Pos) (Map // TODO(rfindley): is this the best factoring of this API? This function is // really a trivial wrapper around findFileInDeps, which may be a more useful // function to expose. -func FindPackageFromPos(fset *token.FileSet, pkg Package, pos token.Pos) (Package, error) { +func FindPackageFromPos(pkg Package, pos token.Pos) (Package, error) { if !pos.IsValid() { return nil, fmt.Errorf("invalid position") } - fileName := fset.File(pos).Name() + fileName := pkg.FileSet().File(pos).Name() uri := span.URIFromPath(fileName) _, pkg, err := findFileInDeps(pkg, uri) return pkg, err diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index be2b7f6a428..45708b3aa4c 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -40,6 +40,11 @@ type Snapshot interface { BackgroundContext() context.Context // Fileset returns the Fileset used to parse all the Go files in this snapshot. + // + // If the files are known to belong to a specific Package, use + // Package.FileSet instead. (We plan to eliminate the + // Snapshot's cache of parsed files, and thus the need for a + // snapshot-wide FileSet.) FileSet() *token.FileSet // ValidBuildConfiguration returns true if there is some error in the @@ -610,6 +615,7 @@ type Package interface { PkgPath() PackagePath CompiledGoFiles() []*ParsedGoFile File(uri span.URI) (*ParsedGoFile, error) + FileSet() *token.FileSet GetSyntax() []*ast.File GetTypes() *types.Package GetTypesInfo() *types.Info From e3b3c0100d02d580162d63ee52e0ea505b2ea233 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Fri, 11 Nov 2022 09:51:39 -0500 Subject: [PATCH 424/723] go/pointer: break TestInput into subtests and update skips TestInput is fiendishly slow and memory-hungry. Breaking it into subtests reveals that the vast majority of that cost can be attributed to a single test case: "testdata/a_test.go". Update the address-space-based skip to skip only that one input, skip it under the race detector (it is timing out on the new "linux-amd64-longtest-race" builder), and run the remaining inputs in parallel (to reduce test latency now that we've better identified the culprit). Updates golang/go#14113. Updates golang/go#54630. Change-Id: I105cfa173edc74692eaa41efd50ae08eeacf0d7d Reviewed-on: https://go-review.googlesource.com/c/tools/+/449518 TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Bryan Mills Run-TryBot: Bryan Mills Reviewed-by: Alan Donovan --- go/pointer/pointer_race_test.go | 12 +++++++++ go/pointer/pointer_test.go | 47 ++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 go/pointer/pointer_race_test.go diff --git a/go/pointer/pointer_race_test.go b/go/pointer/pointer_race_test.go new file mode 100644 index 00000000000..d3c9b475e25 --- /dev/null +++ b/go/pointer/pointer_race_test.go @@ -0,0 +1,12 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build race +// +build race + +package pointer_test + +func init() { + raceEnabled = true +} diff --git a/go/pointer/pointer_test.go b/go/pointer/pointer_test.go index 47074f620f2..05fe981f86e 100644 --- a/go/pointer/pointer_test.go +++ b/go/pointer/pointer_test.go @@ -66,6 +66,8 @@ var inputs = []string{ // "testdata/timer.go", // TODO(adonovan): fix broken assumptions about runtime timers } +var raceEnabled = false + // Expectation grammar: // // @calls f -> g @@ -609,10 +611,6 @@ func TestInput(t *testing.T) { if testing.Short() { t.Skip("skipping in short mode; this test requires tons of memory; https://golang.org/issue/14113") } - if unsafe.Sizeof(unsafe.Pointer(nil)) <= 4 { - t.Skip("skipping memory-intensive test on platform with small address space; https://golang.org/issue/14113") - } - ok := true wd, err := os.Getwd() if err != nil { @@ -627,23 +625,34 @@ func TestInput(t *testing.T) { fmt.Fprintf(os.Stderr, "Entering directory `%s'\n", wd) for _, filename := range inputs { - content, err := ioutil.ReadFile(filename) - if err != nil { - t.Errorf("couldn't read file '%s': %s", filename, err) - continue - } + filename := filename + t.Run(filename, func(t *testing.T) { + if filename == "testdata/a_test.go" { + // For some reason this particular file is way more expensive than the others. + if unsafe.Sizeof(unsafe.Pointer(nil)) <= 4 { + t.Skip("skipping memory-intensive test on platform with small address space; https://golang.org/issue/14113") + } + if raceEnabled { + t.Skip("skipping memory-intensive test under race detector; https://golang.org/issue/14113") + } + } else { + t.Parallel() + } - fpath, err := filepath.Abs(filename) - if err != nil { - t.Errorf("couldn't get absolute path for '%s': %s", filename, err) - } + content, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatalf("couldn't read file '%s': %s", filename, err) + } - if !doOneInput(t, string(content), fpath) { - ok = false - } - } - if !ok { - t.Fail() + fpath, err := filepath.Abs(filename) + if err != nil { + t.Fatalf("couldn't get absolute path for '%s': %s", filename, err) + } + + if !doOneInput(t, string(content), fpath) { + t.Fail() + } + }) } } From 0c71b564b93e24ffab35f4d2f41c8c74f413801a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 14 Nov 2022 11:58:07 -0500 Subject: [PATCH 425/723] gopls/internal/lsp: fix diagnostic suppression when folders change Diagnostic suppression used the view-relative snapshot ID to avoid publishing stale diagnostics. When the layout of views changes due to a didChangeWorkspaceFolders notification, this suppression is broken as snapshot IDs reset to 0. Fix this (hopefully temporarily) by introducing a globally monotonic snapshot ID. Fixes golang/go#56731 Change-Id: Ib108b1436e800cf5a45fbba298c9975a2cf1d735 Reviewed-on: https://go-review.googlesource.com/c/tools/+/450275 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Auto-Submit: Robert Findley --- gopls/internal/lsp/cache/check.go | 4 +- gopls/internal/lsp/cache/load.go | 12 +-- gopls/internal/lsp/cache/session.go | 9 ++- gopls/internal/lsp/cache/snapshot.go | 26 +++++-- gopls/internal/lsp/diagnostics.go | 75 ++++++++++--------- gopls/internal/lsp/mod/diagnostics.go | 7 +- gopls/internal/lsp/source/view.go | 29 ++++++- gopls/internal/lsp/work/diagnostics.go | 3 +- .../internal/regtest/workspace/broken_test.go | 29 +++++-- 9 files changed, 128 insertions(+), 66 deletions(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 4b1ea3debe7..25da922db10 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -107,9 +107,9 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // top-level is valid, all of its dependencies should be as well. if err != nil || m.Valid && !depHandle.m.Valid { if err != nil { - event.Error(ctx, fmt.Sprintf("%s: no dep handle for %s", id, depID), err, tag.Snapshot.Of(s.id)) + event.Error(ctx, fmt.Sprintf("%s: no dep handle for %s", id, depID), err, source.SnapshotLabels(s)...) } else { - event.Log(ctx, fmt.Sprintf("%s: invalid dep handle for %s", id, depID), tag.Snapshot.Of(s.id)) + event.Log(ctx, fmt.Sprintf("%s: invalid dep handle for %s", id, depID), source.SnapshotLabels(s)...) } // This check ensures we break out of the slow diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index e779cbac4a6..dcd343a56b9 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -141,10 +141,11 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc } // This log message is sought for by TestReloadOnlyOnce. + labels := append(source.SnapshotLabels(s), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) if err != nil { - event.Error(ctx, eventName, err, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) + event.Error(ctx, eventName, err, labels...) } else { - event.Log(ctx, eventName, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) + event.Log(ctx, eventName, labels...) } if len(pkgs) == 0 { @@ -174,11 +175,12 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc } if !containsDir || s.view.Options().VerboseOutput { - event.Log(ctx, eventName, - tag.Snapshot.Of(s.ID()), + event.Log(ctx, eventName, append( + source.SnapshotLabels(s), tag.Package.Of(pkg.ID), - tag.Files.Of(pkg.CompiledGoFiles)) + tag.Files.Of(pkg.CompiledGoFiles))...) } + // Ignore packages with no sources, since we will never be able to // correctly invalidate that metadata. if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 { diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index f2651a75a89..fe88a46fc94 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -187,7 +187,7 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt return view, snapshot, release, nil } -func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, snapshotID uint64) (*View, *snapshot, func(), error) { +func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, seqID uint64) (*View, *snapshot, func(), error) { index := atomic.AddInt64(&viewIndex, 1) if s.cache.options != nil { @@ -264,7 +264,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, }, } v.snapshot = &snapshot{ - id: snapshotID, + sequenceID: seqID, + globalID: nextSnapshotID(), view: v, backgroundCtx: backgroundCtx, cancel: cancel, @@ -401,7 +402,7 @@ func (s *Session) updateView(ctx context.Context, view *View, options *source.Op view.snapshotMu.Unlock() panic("updateView called after View was already shut down") } - snapshotID := view.snapshot.id + seqID := view.snapshot.sequenceID // Preserve sequence IDs when updating a view in place. view.snapshotMu.Unlock() i, err := s.dropView(ctx, view) @@ -409,7 +410,7 @@ func (s *Session) updateView(ctx context.Context, view *View, options *source.Op return nil, err } - v, _, release, err := s.createView(ctx, view.name, view.folder, options, snapshotID) + v, _, release, err := s.createView(ctx, view.name, view.folder, options, seqID) release() if err != nil { diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index f28030431a1..70c26f253c7 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -44,8 +44,9 @@ import ( ) type snapshot struct { - id uint64 - view *View + sequenceID uint64 + globalID source.GlobalSnapshotID + view *View cancel func() backgroundCtx context.Context @@ -156,6 +157,12 @@ type snapshot struct { unprocessedSubdirChanges []*fileChange } +var globalSnapshotID uint64 + +func nextSnapshotID() source.GlobalSnapshotID { + return source.GlobalSnapshotID(atomic.AddUint64(&globalSnapshotID, 1)) +} + var _ memoize.RefCounted = (*snapshot)(nil) // snapshots are reference-counted // Acquire prevents the snapshot from being destroyed until the returned function is called. @@ -170,7 +177,7 @@ var _ memoize.RefCounted = (*snapshot)(nil) // snapshots are reference-counted func (s *snapshot) Acquire() func() { type uP = unsafe.Pointer if destroyedBy := atomic.LoadPointer((*uP)(uP(&s.destroyedBy))); destroyedBy != nil { - log.Panicf("%d: acquire() after Destroy(%q)", s.id, *(*string)(destroyedBy)) + log.Panicf("%d: acquire() after Destroy(%q)", s.globalID, *(*string)(destroyedBy)) } s.refcount.Add(1) return s.refcount.Done @@ -209,7 +216,7 @@ func (s *snapshot) destroy(destroyedBy string) { // Not foolproof: another thread could acquire() at this moment. type uP = unsafe.Pointer // looking forward to generics... if old := atomic.SwapPointer((*uP)(uP(&s.destroyedBy)), uP(&destroyedBy)); old != nil { - log.Panicf("%d: Destroy(%q) after Destroy(%q)", s.id, destroyedBy, *(*string)(old)) + log.Panicf("%d: Destroy(%q) after Destroy(%q)", s.globalID, destroyedBy, *(*string)(old)) } s.packages.Destroy() @@ -232,8 +239,12 @@ func (s *snapshot) destroy(destroyedBy string) { } } -func (s *snapshot) ID() uint64 { - return s.id +func (s *snapshot) SequenceID() uint64 { + return s.sequenceID +} + +func (s *snapshot) GlobalID() source.GlobalSnapshotID { + return s.globalID } func (s *snapshot) View() source.View { @@ -1726,7 +1737,8 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC bgCtx, cancel := context.WithCancel(bgCtx) result := &snapshot{ - id: s.id + 1, + sequenceID: s.sequenceID + 1, + globalID: nextSnapshotID(), store: s.store, view: s.view, backgroundCtx: bgCtx, diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index bbc57ffaba4..e6e644de245 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -43,8 +43,8 @@ const ( // A diagnosticReport holds results for a single diagnostic source. type diagnosticReport struct { - snapshotID uint64 // snapshot ID on which the report was computed - publishedHash string // last published hash for this (URI, source) + snapshotID source.GlobalSnapshotID // global snapshot ID on which the report was computed + publishedHash string // last published hash for this (URI, source) diags map[string]*source.Diagnostic } @@ -68,7 +68,7 @@ type fileReports struct { // yet published. // // This prevents gopls from publishing stale diagnostics. - publishedSnapshotID uint64 + publishedSnapshotID source.GlobalSnapshotID // publishedHash is a hash of the latest diagnostics published for the file. publishedHash string @@ -141,7 +141,7 @@ func (s *Server) diagnoseSnapshots(snapshots map[source.Snapshot][]span.URI, onD func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.URI, onDisk bool) { ctx := snapshot.BackgroundContext() - ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", tag.Snapshot.Of(snapshot.ID())) + ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", source.SnapshotLabels(snapshot)...) defer done() delay := snapshot.View().Options().DiagnosticsDelay @@ -155,7 +155,13 @@ func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.U // delay. s.diagnoseChangedFiles(ctx, snapshot, changedURIs, onDisk) s.publishDiagnostics(ctx, false, snapshot) - if ok := <-s.diagDebouncer.debounce(snapshot.View().Name(), snapshot.ID(), time.After(delay)); ok { + + // We debounce diagnostics separately for each view, using the snapshot + // local ID as logical ordering. + // + // TODO(rfindley): it would be cleaner to simply put the diagnostic + // debouncer on the view, and remove the "key" argument to debouncing. + if ok := <-s.diagDebouncer.debounce(snapshot.View().Name(), snapshot.SequenceID(), time.After(delay)); ok { s.diagnose(ctx, snapshot, false) s.publishDiagnostics(ctx, true, snapshot) } @@ -168,7 +174,7 @@ func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.U } func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snapshot, uris []span.URI, onDisk bool) { - ctx, done := event.Start(ctx, "Server.diagnoseChangedFiles", tag.Snapshot.Of(snapshot.ID())) + ctx, done := event.Start(ctx, "Server.diagnoseChangedFiles", source.SnapshotLabels(snapshot)...) defer done() packages := make(map[source.Package]struct{}) @@ -215,7 +221,7 @@ func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snaps // diagnose is a helper function for running diagnostics with a given context. // Do not call it directly. forceAnalysis is only true for testing purposes. func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAnalysis bool) { - ctx, done := event.Start(ctx, "Server.diagnose", tag.Snapshot.Of(snapshot.ID())) + ctx, done := event.Start(ctx, "Server.diagnose", source.SnapshotLabels(snapshot)...) defer done() // Wait for a free diagnostics slot. @@ -235,9 +241,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn // common code for dispatching diagnostics store := func(dsource diagnosticSource, operation string, diagsByFileID map[source.VersionedFileIdentity][]*source.Diagnostic, err error) { if err != nil { - event.Error(ctx, "warning: while "+operation, err, - tag.Directory.Of(snapshot.View().Folder().Filename()), - tag.Snapshot.Of(snapshot.ID())) + event.Error(ctx, "warning: while "+operation, err, source.SnapshotLabels(snapshot)...) } for id, diags := range diagsByFileID { if id.URI == "" { @@ -346,7 +350,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn } func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg source.Package, alwaysAnalyze bool) { - ctx, done := event.Start(ctx, "Server.diagnosePkg", tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(string(pkg.ID()))) + ctx, done := event.Start(ctx, "Server.diagnosePkg", append(source.SnapshotLabels(snapshot), tag.Package.Of(string(pkg.ID())))...) defer done() enableDiagnostics := false includeAnalysis := alwaysAnalyze // only run analyses for packages with open files @@ -361,7 +365,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) if err != nil { - event.Error(ctx, "warning: diagnosing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(string(pkg.ID()))) + event.Error(ctx, "warning: diagnosing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(pkg.ID())))...) return } for _, cgf := range pkg.CompiledGoFiles() { @@ -374,7 +378,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg if includeAnalysis && !pkg.HasListOrParseErrors() { reports, err := source.Analyze(ctx, snapshot, pkg, false) if err != nil { - event.Error(ctx, "warning: analyzing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(string(pkg.ID()))) + event.Error(ctx, "warning: analyzing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(pkg.ID())))...) return } for _, cgf := range pkg.CompiledGoFiles() { @@ -390,7 +394,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg if enableGCDetails { gcReports, err := source.GCOptimizationDetails(ctx, snapshot, pkg) if err != nil { - event.Error(ctx, "warning: gc details", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(string(pkg.ID()))) + event.Error(ctx, "warning: gc details", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(pkg.ID())))...) } s.gcOptimizationDetailsMu.Lock() _, enableGCDetails := s.gcOptimizationDetails[pkg.ID()] @@ -452,13 +456,13 @@ func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsourc } report := s.diagnostics[uri].reports[dsource] // Don't set obsolete diagnostics. - if report.snapshotID > snapshot.ID() { + if report.snapshotID > snapshot.GlobalID() { return } - if report.diags == nil || report.snapshotID != snapshot.ID() { + if report.diags == nil || report.snapshotID != snapshot.GlobalID() { report.diags = map[string]*source.Diagnostic{} } - report.snapshotID = snapshot.ID() + report.snapshotID = snapshot.GlobalID() for _, d := range diags { report.diags[hashDiagnostics(d)] = d } @@ -488,7 +492,7 @@ func (s *Server) showCriticalErrorStatus(ctx context.Context, snapshot source.Sn // status bar. var errMsg string if err != nil { - event.Error(ctx, "errors loading workspace", err.MainError, tag.Snapshot.Of(snapshot.ID()), tag.Directory.Of(snapshot.View().Folder())) + event.Error(ctx, "errors loading workspace", err.MainError, source.SnapshotLabels(snapshot)...) for _, d := range err.Diagnostics { s.storeDiagnostics(snapshot, d.URI, modSource, []*source.Diagnostic{d}) } @@ -567,31 +571,34 @@ Otherwise, see the troubleshooting guidelines for help investigating (https://gi // publishDiagnostics collects and publishes any unpublished diagnostic reports. func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot source.Snapshot) { - ctx, done := event.Start(ctx, "Server.publishDiagnostics", tag.Snapshot.Of(snapshot.ID())) + ctx, done := event.Start(ctx, "Server.publishDiagnostics", source.SnapshotLabels(snapshot)...) defer done() + s.diagnosticsMu.Lock() defer s.diagnosticsMu.Unlock() - // TODO(rfindley): remove this noisy (and not useful) logging. - published := 0 - defer func() { - log.Trace.Logf(ctx, "published %d diagnostics", published) - }() - for uri, r := range s.diagnostics { - // Snapshot IDs are always increasing, so we use them instead of file - // versions to create the correct order for diagnostics. - + // Global snapshot IDs are monotonic, so we use them to enforce an ordering + // for diagnostics. + // // If we've already delivered diagnostics for a future snapshot for this - // file, do not deliver them. - if r.publishedSnapshotID > snapshot.ID() { + // file, do not deliver them. See golang/go#42837 for an example of why + // this is necessary. + // + // TODO(rfindley): even using a global snapshot ID, this mechanism is + // potentially racy: elsewhere in the code (e.g. invalidateContent) we + // allow for multiple views track a given file. In this case, we should + // either only report diagnostics for snapshots from the "best" view of a + // URI, or somehow merge diagnostics from multiple views. + if r.publishedSnapshotID > snapshot.GlobalID() { continue } + anyReportsChanged := false reportHashes := map[diagnosticSource]string{} var diags []*source.Diagnostic for dsource, report := range r.reports { - if report.snapshotID != snapshot.ID() { + if report.snapshotID != snapshot.GlobalID() { continue } var reportDiags []*source.Diagnostic @@ -611,12 +618,13 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so // new information. continue } + source.SortDiagnostics(diags) hash := hashDiagnostics(diags...) if hash == r.publishedHash && !r.mustPublish { // Update snapshotID to be the latest snapshot for which this diagnostic // hash is valid. - r.publishedSnapshotID = snapshot.ID() + r.publishedSnapshotID = snapshot.GlobalID() continue } var version int32 @@ -628,10 +636,9 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so URI: protocol.URIFromSpanURI(uri), Version: version, }); err == nil { - published++ r.publishedHash = hash r.mustPublish = false // diagnostics have been successfully published - r.publishedSnapshotID = snapshot.ID() + r.publishedSnapshotID = snapshot.GlobalID() for dsource, hash := range reportHashes { report := r.reports[dsource] report.publishedHash = hash diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index e455ba3f718..82155738a5c 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -19,7 +19,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" "golang.org/x/vuln/osv" ) @@ -27,7 +26,7 @@ import ( // // It waits for completion of type-checking of all active packages. func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { - ctx, done := event.Start(ctx, "mod.Diagnostics", tag.Snapshot.Of(snapshot.ID())) + ctx, done := event.Start(ctx, "mod.Diagnostics", source.SnapshotLabels(snapshot)...) defer done() return collectDiagnostics(ctx, snapshot, ModDiagnostics) @@ -36,7 +35,7 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.Vers // UpgradeDiagnostics returns upgrade diagnostics for the modules in the // workspace with known upgrades. func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { - ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", tag.Snapshot.Of(snapshot.ID())) + ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", source.SnapshotLabels(snapshot)...) defer done() return collectDiagnostics(ctx, snapshot, ModUpgradeDiagnostics) @@ -45,7 +44,7 @@ func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[sour // VulnerabilityDiagnostics returns vulnerability diagnostics for the active modules in the // workspace with known vulnerabilites. func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { - ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", tag.Snapshot.Of(snapshot.ID())) + ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", source.SnapshotLabels(snapshot)...) defer done() return collectDiagnostics(ctx, snapshot, ModVulnerabilityDiagnostics) diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 45708b3aa4c..47d642d95f3 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -24,13 +24,34 @@ import ( "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event/label" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" ) +// A GlobalSnapshotID uniquely identifies a snapshot within this process and +// increases monotonically with snapshot creation time. +// +// We use a distinct integral type for global IDs to help enforce correct +// usage. +type GlobalSnapshotID uint64 + // Snapshot represents the current state for the given view. type Snapshot interface { - ID() uint64 + // SequenceID is the sequence id of this snapshot within its containing + // view. + // + // Relative to their view sequence ids are monotonically increasing, but this + // does not hold globally: when new views are created their initial snapshot + // has sequence ID 0. For operations that span multiple views, use global + // IDs. + SequenceID() uint64 + + // GlobalID is a globally unique identifier for this snapshot. Global IDs are + // monotonic: subsequent snapshots will have higher global ID, though + // subsequent snapshots in a view may not have adjacent global IDs. + GlobalID() GlobalSnapshotID // View returns the View associated with this snapshot. View() View @@ -192,6 +213,12 @@ type Snapshot interface { BuildGoplsMod(ctx context.Context) (*modfile.File, error) } +// SnapshotLabels returns a new slice of labels that should be used for events +// related to a snapshot. +func SnapshotLabels(snapshot Snapshot) []label.Label { + return []label.Label{tag.Snapshot.Of(snapshot.SequenceID()), tag.Directory.Of(snapshot.View().Folder())} +} + // PackageFilter sets how a package is filtered out from a set of packages // containing a given file. type PackageFilter int diff --git a/gopls/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go index 0d0f4eb18d9..c64027c54b2 100644 --- a/gopls/internal/lsp/work/diagnostics.go +++ b/gopls/internal/lsp/work/diagnostics.go @@ -15,11 +15,10 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" ) func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { - ctx, done := event.Start(ctx, "work.Diagnostics", tag.Snapshot.Of(snapshot.ID())) + ctx, done := event.Start(ctx, "work.Diagnostics", source.SnapshotLabels(snapshot)...) defer done() reports := map[source.VersionedFileIdentity][]*source.Diagnostic{} diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index f839b0a3419..8cafa3ec1ab 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -207,21 +207,30 @@ package b env.OpenFile("a/empty.go") env.OpenFile("b/go.mod") env.Await( - env.DiagnosticAtRegexp("a/a.go", "package a"), - env.DiagnosticAtRegexp("b/go.mod", "module b.com"), - OutstandingWork(lsp.WorkspaceLoadFailure, msg), + env.AfterChange( + env.DiagnosticAtRegexp("a/a.go", "package a"), + env.DiagnosticAtRegexp("b/go.mod", "module b.com"), + OutstandingWork(lsp.WorkspaceLoadFailure, msg), + ), ) // Changing the workspace folders to the valid modules should resolve - // the workspace error. + // the workspace errors and diagnostics. + // + // TODO(rfindley): verbose work tracking doesn't follow changing the + // workspace folder, therefore we can't invoke AfterChange here. env.ChangeWorkspaceFolders("a", "b") - env.Await(NoOutstandingWork()) + env.Await( + EmptyDiagnostics("a/a.go"), + EmptyDiagnostics("b/go.mod"), + NoOutstandingWork(), + ) env.ChangeWorkspaceFolders(".") // TODO(rfindley): when GO111MODULE=auto, we need to open or change a // file here in order to detect a critical error. This is because gopls - // has forgotten about a/a.go, and therefor doesn't hit the heuristic + // has forgotten about a/a.go, and therefore doesn't hit the heuristic // "all packages are command-line-arguments". // // This is broken, and could be fixed by adjusting the heuristic to @@ -229,7 +238,13 @@ package b // (better) trying to get workspace packages for each open file. See // also golang/go#54261. env.OpenFile("b/b.go") - env.Await(OutstandingWork(lsp.WorkspaceLoadFailure, msg)) + env.Await( + // TODO(rfindley): fix these missing diagnostics. + // env.DiagnosticAtRegexp("a/a.go", "package a"), + // env.DiagnosticAtRegexp("b/go.mod", "module b.com"), + env.DiagnosticAtRegexp("b/b.go", "package b"), + OutstandingWork(lsp.WorkspaceLoadFailure, msg), + ) }) }) } From 1cb4c17983d3f5bec09ed1fcaf71c6082ef7881b Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 15 Nov 2022 09:52:14 -0500 Subject: [PATCH 426/723] gopls/internal/regtest: make AfterChange do the awaiting The only expected use of the AfterChange helper is in the pattern env.Await(env.AfterChange(...)). Simplify this pattern by having AfterChange do the awaiting. Change-Id: I832d619e8d44daae952f250e7aef69eba4e6715a Reviewed-on: https://go-review.googlesource.com/c/tools/+/450676 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/regtest/expectation.go | 14 +++++--- .../internal/regtest/modfile/modfile_test.go | 36 +++++++------------ .../internal/regtest/workspace/broken_test.go | 10 +++--- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index d09398779c1..e5beb042713 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -287,10 +287,16 @@ func (e *Env) DoneDiagnosingChanges() Expectation { // AfterChange expects that the given expectations will be met after all // state-changing notifications have been processed by the server. -func (e *Env) AfterChange(expectations ...Expectation) Expectation { - return OnceMet( - e.DoneDiagnosingChanges(), - expectations..., +// +// It awaits the completion of all anticipated work before checking the given +// expectations. +func (e *Env) AfterChange(expectations ...Expectation) { + e.T.Helper() + e.Await( + OnceMet( + e.DoneDiagnosingChanges(), + expectations..., + ), ) } diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 64892be5966..4369aefd3f2 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -632,12 +632,10 @@ func main() { env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. d := protocol.PublishDiagnosticsParams{} - env.Await( - env.AfterChange( - // Make sure the diagnostic mentions the new version -- the old diagnostic is in the same place. - env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "example.com@v1.2.3"), - ReadDiagnostics("a/go.mod", &d), - ), + env.AfterChange( + // Make sure the diagnostic mentions the new version -- the old diagnostic is in the same place. + env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "example.com@v1.2.3"), + ReadDiagnostics("a/go.mod", &d), ) qfs := env.GetQuickFixes("a/go.mod", d.Diagnostics) if len(qfs) == 0 { @@ -645,11 +643,9 @@ func main() { } env.ApplyCodeAction(qfs[0]) // Arbitrarily pick a single fix to apply. Applying all of them seems to cause trouble in this particular test. env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. - env.Await( - env.AfterChange( - EmptyDiagnostics("a/go.mod"), - env.DiagnosticAtRegexp("a/main.go", "x = "), - ), + env.AfterChange( + EmptyDiagnostics("a/go.mod"), + env.DiagnosticAtRegexp("a/main.go", "x = "), ) }) }) @@ -678,24 +674,18 @@ func main() { t.Run("good", func(t *testing.T) { runner.Run(t, known, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") - env.Await( - env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", "x = "), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", "x = "), ) env.RegexpReplace("a/go.mod", "v1.2.3", "v1.2.2") env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk - env.Await( - env.AfterChange( - env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"), ) env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk - env.Await( - env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", "x = "), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", "x = "), ) }) }) diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index 8cafa3ec1ab..fb101d366e2 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -206,12 +206,10 @@ package b env.OpenFile("a/a.go") env.OpenFile("a/empty.go") env.OpenFile("b/go.mod") - env.Await( - env.AfterChange( - env.DiagnosticAtRegexp("a/a.go", "package a"), - env.DiagnosticAtRegexp("b/go.mod", "module b.com"), - OutstandingWork(lsp.WorkspaceLoadFailure, msg), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/a.go", "package a"), + env.DiagnosticAtRegexp("b/go.mod", "module b.com"), + OutstandingWork(lsp.WorkspaceLoadFailure, msg), ) // Changing the workspace folders to the valid modules should resolve From fc702c522d87139f25a871604bfac1fa2c787bf6 Mon Sep 17 00:00:00 2001 From: Florian Zenker Date: Tue, 15 Nov 2022 16:34:03 +0000 Subject: [PATCH 427/723] internal/gcimporter: fix performance regression for unified IR flattenImports used to traverse the transitive closure of all imports by recursively visiting all imported packages. This is a lot of redundant work, because flattening happens for every package, the recursion is not necessary and this change removes the recursion. Change-Id: Id5a1b3b15ef3ce8e0fc6c945b5484b3dd06bd6b6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/450755 Run-TryBot: Matthew Dempsky gopls-CI: kokoro Reviewed-by: Florian Zenker Reviewed-by: Matthew Dempsky TryBot-Result: Gopher Robot --- internal/gcimporter/ureader_yes.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go index e09053bd37a..3d3cf698b2c 100644 --- a/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -259,22 +259,22 @@ func (r *reader) doPkg() *types.Package { // packages rooted from pkgs. func flattenImports(pkgs []*types.Package) []*types.Package { var res []*types.Package - - seen := make(map[*types.Package]bool) - var add func(pkg *types.Package) - add = func(pkg *types.Package) { - if seen[pkg] { - return + seen := make(map[*types.Package]struct{}) + for _, pkg := range pkgs { + if _, ok := seen[pkg]; ok { + continue } - seen[pkg] = true + seen[pkg] = struct{}{} res = append(res, pkg) - for _, imp := range pkg.Imports() { - add(imp) - } - } - for _, pkg := range pkgs { - add(pkg) + // pkg.Imports() is already flattened. + for _, pkg := range pkg.Imports() { + if _, ok := seen[pkg]; ok { + continue + } + seen[pkg] = struct{}{} + res = append(res, pkg) + } } return res } From 06fb723d2f266d816afd5bf124845404dbd51fce Mon Sep 17 00:00:00 2001 From: David Chase Date: Thu, 10 Nov 2022 15:19:38 -0500 Subject: [PATCH 428/723] internal/gcimporter: port memory reuse optimizations from Go tree original CL was https://go.dev/cl/433037 "cmd/compile: introduce "temporary" readers for more storage reuse" Change-Id: I8989ae45b17a84451d29c7dc95b8ab2a06b6f153 Reviewed-on: https://go-review.googlesource.com/c/tools/+/449499 TryBot-Result: Gopher Robot Reviewed-by: Matthew Dempsky Run-TryBot: David Chase --- internal/gcimporter/ureader_yes.go | 101 ++++++++++++++++++----------- internal/pkgbits/decoder.go | 70 +++++++++++++++++--- 2 files changed, 126 insertions(+), 45 deletions(-) diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go index 3d3cf698b2c..20b99903c51 100644 --- a/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -158,6 +158,17 @@ func (pr *pkgReader) newReader(k pkgbits.RelocKind, idx pkgbits.Index, marker pk } } +func (pr *pkgReader) tempReader(k pkgbits.RelocKind, idx pkgbits.Index, marker pkgbits.SyncMarker) *reader { + return &reader{ + Decoder: pr.TempDecoder(k, idx, marker), + p: pr, + } +} + +func (pr *pkgReader) retireReader(r *reader) { + pr.RetireDecoder(&r.Decoder) +} + // @@@ Positions func (r *reader) pos() token.Pos { @@ -182,26 +193,29 @@ func (pr *pkgReader) posBaseIdx(idx pkgbits.Index) string { return b } - r := pr.newReader(pkgbits.RelocPosBase, idx, pkgbits.SyncPosBase) + var filename string + { + r := pr.tempReader(pkgbits.RelocPosBase, idx, pkgbits.SyncPosBase) - // Within types2, position bases have a lot more details (e.g., - // keeping track of where //line directives appeared exactly). - // - // For go/types, we just track the file name. + // Within types2, position bases have a lot more details (e.g., + // keeping track of where //line directives appeared exactly). + // + // For go/types, we just track the file name. - filename := r.String() + filename = r.String() - if r.Bool() { // file base - // Was: "b = token.NewTrimmedFileBase(filename, true)" - } else { // line base - pos := r.pos() - line := r.Uint() - col := r.Uint() + if r.Bool() { // file base + // Was: "b = token.NewTrimmedFileBase(filename, true)" + } else { // line base + pos := r.pos() + line := r.Uint() + col := r.Uint() - // Was: "b = token.NewLineBase(pos, filename, true, line, col)" - _, _, _ = pos, line, col + // Was: "b = token.NewLineBase(pos, filename, true, line, col)" + _, _, _ = pos, line, col + } + pr.retireReader(r) } - b := filename pr.posBases[idx] = b return b @@ -307,12 +321,15 @@ func (pr *pkgReader) typIdx(info typeInfo, dict *readerDict) types.Type { return typ } - r := pr.newReader(pkgbits.RelocType, idx, pkgbits.SyncTypeIdx) - r.dict = dict - - typ := r.doTyp() - assert(typ != nil) + var typ types.Type + { + r := pr.tempReader(pkgbits.RelocType, idx, pkgbits.SyncTypeIdx) + r.dict = dict + typ = r.doTyp() + assert(typ != nil) + pr.retireReader(r) + } // See comment in pkgReader.typIdx explaining how this happens. if prev := *where; prev != nil { return prev @@ -478,12 +495,19 @@ func (r *reader) obj() (types.Object, []types.Type) { } func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { - rname := pr.newReader(pkgbits.RelocName, idx, pkgbits.SyncObject1) - objPkg, objName := rname.qualifiedIdent() - assert(objName != "") + var objPkg *types.Package + var objName string + var tag pkgbits.CodeObj + { + rname := pr.tempReader(pkgbits.RelocName, idx, pkgbits.SyncObject1) - tag := pkgbits.CodeObj(rname.Code(pkgbits.SyncCodeObj)) + objPkg, objName = rname.qualifiedIdent() + assert(objName != "") + + tag = pkgbits.CodeObj(rname.Code(pkgbits.SyncCodeObj)) + pr.retireReader(rname) + } if tag == pkgbits.ObjStub { assert(objPkg == nil || objPkg == types.Unsafe) @@ -588,25 +612,28 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { } func (pr *pkgReader) objDictIdx(idx pkgbits.Index) *readerDict { - r := pr.newReader(pkgbits.RelocObjDict, idx, pkgbits.SyncObject1) var dict readerDict - if implicits := r.Len(); implicits != 0 { - errorf("unexpected object with %v implicit type parameter(s)", implicits) - } + { + r := pr.tempReader(pkgbits.RelocObjDict, idx, pkgbits.SyncObject1) + if implicits := r.Len(); implicits != 0 { + errorf("unexpected object with %v implicit type parameter(s)", implicits) + } - dict.bounds = make([]typeInfo, r.Len()) - for i := range dict.bounds { - dict.bounds[i] = r.typInfo() - } + dict.bounds = make([]typeInfo, r.Len()) + for i := range dict.bounds { + dict.bounds[i] = r.typInfo() + } - dict.derived = make([]derivedInfo, r.Len()) - dict.derivedTypes = make([]types.Type, len(dict.derived)) - for i := range dict.derived { - dict.derived[i] = derivedInfo{r.Reloc(pkgbits.RelocType), r.Bool()} - } + dict.derived = make([]derivedInfo, r.Len()) + dict.derivedTypes = make([]types.Type, len(dict.derived)) + for i := range dict.derived { + dict.derived[i] = derivedInfo{r.Reloc(pkgbits.RelocType), r.Bool()} + } + pr.retireReader(r) + } // function references follow, but reader doesn't need those return &dict diff --git a/internal/pkgbits/decoder.go b/internal/pkgbits/decoder.go index e08099c6635..297e5b043a7 100644 --- a/internal/pkgbits/decoder.go +++ b/internal/pkgbits/decoder.go @@ -52,6 +52,8 @@ type PkgDecoder struct { // For example, section K's end positions start at elemEndsEnds[K-1] // (or 0, if K==0) and end at elemEndsEnds[K]. elemEndsEnds [numRelocs]uint32 + + scratchRelocEnt []RelocEnt } // PkgPath returns the package path for the package @@ -165,6 +167,21 @@ func (pr *PkgDecoder) NewDecoder(k RelocKind, idx Index, marker SyncMarker) Deco return r } +// TempDecoder returns a Decoder for the given (section, index) pair, +// and decodes the given SyncMarker from the element bitstream. +// If possible the Decoder should be RetireDecoder'd when it is no longer +// needed, this will avoid heap allocations. +func (pr *PkgDecoder) TempDecoder(k RelocKind, idx Index, marker SyncMarker) Decoder { + r := pr.TempDecoderRaw(k, idx) + r.Sync(marker) + return r +} + +func (pr *PkgDecoder) RetireDecoder(d *Decoder) { + pr.scratchRelocEnt = d.Relocs + d.Relocs = nil +} + // NewDecoderRaw returns a Decoder for the given (section, index) pair. // // Most callers should use NewDecoder instead. @@ -188,6 +205,30 @@ func (pr *PkgDecoder) NewDecoderRaw(k RelocKind, idx Index) Decoder { return r } +func (pr *PkgDecoder) TempDecoderRaw(k RelocKind, idx Index) Decoder { + r := Decoder{ + common: pr, + k: k, + Idx: idx, + } + + r.Data.Reset(pr.DataIdx(k, idx)) + r.Sync(SyncRelocs) + l := r.Len() + if cap(pr.scratchRelocEnt) >= l { + r.Relocs = pr.scratchRelocEnt[:l] + pr.scratchRelocEnt = nil + } else { + r.Relocs = make([]RelocEnt, l) + } + for i := range r.Relocs { + r.Sync(SyncReloc) + r.Relocs[i] = RelocEnt{RelocKind(r.Len()), Index(r.Len())} + } + + return r +} + // A Decoder provides methods for decoding an individual element's // bitstream data. type Decoder struct { @@ -410,8 +451,12 @@ func (r *Decoder) bigFloat() *big.Float { // PeekPkgPath returns the package path for the specified package // index. func (pr *PkgDecoder) PeekPkgPath(idx Index) string { - r := pr.NewDecoder(RelocPkg, idx, SyncPkgDef) - path := r.String() + var path string + { + r := pr.TempDecoder(RelocPkg, idx, SyncPkgDef) + path = r.String() + pr.RetireDecoder(&r) + } if path == "" { path = pr.pkgPath } @@ -421,14 +466,23 @@ func (pr *PkgDecoder) PeekPkgPath(idx Index) string { // PeekObj returns the package path, object name, and CodeObj for the // specified object index. func (pr *PkgDecoder) PeekObj(idx Index) (string, string, CodeObj) { - r := pr.NewDecoder(RelocName, idx, SyncObject1) - r.Sync(SyncSym) - r.Sync(SyncPkg) - path := pr.PeekPkgPath(r.Reloc(RelocPkg)) - name := r.String() + var ridx Index + var name string + var rcode int + { + r := pr.TempDecoder(RelocName, idx, SyncObject1) + r.Sync(SyncSym) + r.Sync(SyncPkg) + ridx = r.Reloc(RelocPkg) + name = r.String() + rcode = r.Code(SyncCodeObj) + pr.RetireDecoder(&r) + } + + path := pr.PeekPkgPath(ridx) assert(name != "") - tag := CodeObj(r.Code(SyncCodeObj)) + tag := CodeObj(rcode) return path, name, tag } From 6e8da3febb8508ea8ae70ed5c3e6fed36cf73b5f Mon Sep 17 00:00:00 2001 From: David Chase Date: Tue, 15 Nov 2022 14:23:21 -0500 Subject: [PATCH 429/723] internal/pkgbits: port small optimization from compiler to tools Tools version of https://go.dev/cl/435336 I believe this helped the compiler (though I don't see the benchmark). We probably need benchmarks for tools, too. Change-Id: I3450ecb82fcef7050f28a4cffcbc224972680f36 Reviewed-on: https://go-review.googlesource.com/c/tools/+/450678 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: David Chase Reviewed-by: Matthew Dempsky --- internal/pkgbits/decoder.go | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/internal/pkgbits/decoder.go b/internal/pkgbits/decoder.go index 297e5b043a7..3205c9a16c5 100644 --- a/internal/pkgbits/decoder.go +++ b/internal/pkgbits/decoder.go @@ -6,6 +6,7 @@ package pkgbits import ( "encoding/binary" + "errors" "fmt" "go/constant" "go/token" @@ -248,11 +249,39 @@ func (r *Decoder) checkErr(err error) { } func (r *Decoder) rawUvarint() uint64 { - x, err := binary.ReadUvarint(&r.Data) + x, err := readUvarint(&r.Data) r.checkErr(err) return x } +// readUvarint is a type-specialized copy of encoding/binary.ReadUvarint. +// This avoids the interface conversion and thus has better escape properties, +// which flows up the stack. +func readUvarint(r *strings.Reader) (uint64, error) { + var x uint64 + var s uint + for i := 0; i < binary.MaxVarintLen64; i++ { + b, err := r.ReadByte() + if err != nil { + if i > 0 && err == io.EOF { + err = io.ErrUnexpectedEOF + } + return x, err + } + if b < 0x80 { + if i == binary.MaxVarintLen64-1 && b > 1 { + return x, overflow + } + return x | uint64(b)< Date: Mon, 7 Nov 2022 11:16:53 -0500 Subject: [PATCH 430/723] gopls/semantic: semantic tokens for type parameters Fixes confusion in the code for recognizing type paramenters. also, in f := func() {}; defer f() the second f is now tagged as a function Fixes golang/go#56529 Change-Id: Ida37e7fb1caa0bec86376cc24ebfad5c1228a905 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448375 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Peter Weinberger Reviewed-by: Robert Findley --- gopls/internal/lsp/semantic.go | 38 ++++-- .../lsp/testdata/semantic/a.go.golden | 2 +- .../regtest/misc/semantictokens_test.go | 114 ++++++++++++++++++ 3 files changed, 141 insertions(+), 13 deletions(-) diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 4cdca20556b..0d76ad548d6 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -100,7 +100,7 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu if err != nil { return nil, err } - // ignore pgf.ParseErr. Do what we can. + if rng == nil && len(pgf.Src) > maxFullFileSize { err := fmt.Errorf("semantic tokens: file %s too large for full (%d>%d)", fh.URI().Filename(), len(pgf.Src), maxFullFileSize) @@ -180,7 +180,7 @@ const ( func (e *encoded) token(start token.Pos, leng int, typ tokenType, mods []string) { if !start.IsValid() { - // This is not worth reporting + // This is not worth reporting. TODO(pjw): does it still happen? return } if start >= e.end || start+token.Pos(leng) <= e.start { @@ -296,7 +296,7 @@ func (e *encoded) inspector(n ast.Node) bool { e.token(x.TokPos, len(x.Tok.String()), tokOperator, nil) case *ast.BasicLit: if strings.Contains(x.Value, "\n") { - // has to be a string + // has to be a string. e.multiline(x.Pos(), x.End(), x.Value, tokString) break } @@ -379,7 +379,7 @@ func (e *encoded) inspector(n ast.Node) bool { case *ast.IncDecStmt: e.token(x.TokPos, len(x.Tok.String()), tokOperator, nil) case *ast.IndexExpr: - case *typeparams.IndexListExpr: // accommodate generics + case *typeparams.IndexListExpr: case *ast.InterfaceType: e.token(x.Interface, len("interface"), tokKeyword, nil) case *ast.KeyValueExpr: @@ -523,8 +523,6 @@ func (e *encoded) ident(x *ast.Ident) { case *types.Var: if isSignature(y) { tok(x.Pos(), len(x.Name), tokFunction, nil) - } else if _, ok := y.Type().(*typeparams.TypeParam); ok { - tok(x.Pos(), len(x.Name), tokTypeParam, nil) } else if e.isParam(use.Pos()) { // variable, unless use.pos is the pos of a Field in an ancestor FuncDecl // or FuncLit and then it's a parameter @@ -572,9 +570,6 @@ func (e *encoded) isParam(pos token.Pos) bool { } func isSignature(use types.Object) bool { - if true { - return false //PJW: fix after generics seem ok - } if _, ok := use.(*types.Var); !ok { return false } @@ -606,7 +601,7 @@ func (e *encoded) unkIdent(x *ast.Ident) (tokenType, []string) { *ast.IfStmt, /* condition */ *ast.KeyValueExpr: // either key or value return tokVariable, nil - case *typeparams.IndexListExpr: // generic? + case *typeparams.IndexListExpr: return tokVariable, nil case *ast.Ellipsis: return tokType, nil @@ -726,7 +721,7 @@ func isDeprecated(n *ast.CommentGroup) bool { func (e *encoded) definitionFor(x *ast.Ident, def types.Object) (tokenType, []string) { // PJW: def == types.Label? probably a nothing - // PJW: look into replaceing these syntactic tests with types more generally + // 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] @@ -761,7 +756,10 @@ func (e *encoded) definitionFor(x *ast.Ident, def types.Object) (tokenType, []st } // if x < ... < FieldList < FuncType < FuncDecl, this is a param return tokParameter, mods - case *ast.FuncType: + case *ast.FuncType: // is it in the TypeParams? + if isTypeParam(x, y) { + return tokTypeParam, mods + } return tokParameter, mods case *ast.InterfaceType: return tokMethod, mods @@ -793,6 +791,21 @@ func (e *encoded) definitionFor(x *ast.Ident, def types.Object) (tokenType, []st return "", []string{""} } +func isTypeParam(x *ast.Ident, y *ast.FuncType) bool { + tp := typeparams.ForFuncType(y) + 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 @@ -940,6 +953,7 @@ func SemType(n int) string { if n >= 0 && n < len(tokTypes) { return tokTypes[n] } + // not found for some reason return fmt.Sprintf("?%d[%d,%d]?", n, len(tokTypes), len(tokMods)) } diff --git a/gopls/internal/lsp/testdata/semantic/a.go.golden b/gopls/internal/lsp/testdata/semantic/a.go.golden index 34b70e0f4f2..047a031a784 100644 --- a/gopls/internal/lsp/testdata/semantic/a.go.golden +++ b/gopls/internal/lsp/testdata/semantic/a.go.golden @@ -63,7 +63,7 @@ /*⇒4,keyword,[]*/func /*⇒1,function,[definition]*/g(/*⇒2,parameter,[definition]*/vv /*⇒3,operator,[]*/.../*⇒9,keyword,[]*/interface{}) { /*⇒2,variable,[definition]*/ff /*⇒2,operator,[]*/:= /*⇒4,keyword,[]*/func() {} - /*⇒5,keyword,[]*/defer /*⇒2,variable,[]*/ff() + /*⇒5,keyword,[]*/defer /*⇒2,function,[]*/ff() /*⇒2,keyword,[]*/go /*⇒3,namespace,[]*/utf./*⇒9,function,[]*/RuneCount(/*⇒2,string,[]*/"") /*⇒2,keyword,[]*/go /*⇒4,namespace,[]*/utf8./*⇒9,function,[]*/RuneCount(/*⇒2,parameter,[]*/vv.(/*⇒6,type,[]*/string)) /*⇒2,keyword,[]*/if /*⇒4,variable,[readonly]*/true { diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index b0296ee771f..8921995cf7e 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -5,10 +5,14 @@ package misc import ( + "strings" "testing" + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/lsp" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/typeparams" ) func TestBadURICrash_VSCodeIssue1498(t *testing.T) { @@ -40,3 +44,113 @@ func main() {} } }) } + +// fix bug involving type parameters and regular parameters +// (golang/vscode-go#2527) +func TestSemantic_2527(t *testing.T) { + if !typeparams.Enabled { + t.Skip("type parameters are needed for this test") + } + // these are the expected types of identfiers in textt order + want := []result{ + {"package", "keyword", ""}, + {"foo", "namespace", ""}, + {"func", "keyword", ""}, + {"Add", "function", "definition deprecated"}, + {"T", "typeParameter", "definition"}, + {"int", "type", "defaultLibrary"}, + {"target", "parameter", "definition"}, + {"T", "typeParameter", ""}, + {"l", "parameter", "definition"}, + {"T", "typeParameter", ""}, + {"T", "typeParameter", ""}, + {"return", "keyword", ""}, + {"append", "function", "defaultLibrary"}, + {"l", "parameter", ""}, + {"target", "parameter", ""}, + {"for", "keyword", ""}, + {"range", "keyword", ""}, + {"l", "parameter", ""}, + {"return", "keyword", ""}, + {"nil", "variable", "readonly defaultLibrary"}, + } + src := ` +-- go.mod -- +module example.com + +go 1.19 +-- main.go -- +package foo +// Deprecated (for testing) +func Add[T int](target T, l []T) []T { + return append(l, target) + for range l {} // test coverage + return nil +} +` + WithOptions( + Modes(Default), + Settings{"semanticTokens": true}, + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + //env.Await(EmptyOrNoDiagnostics("main.go`")) + env.Await(env.AfterChange(env.DiagnosticAtRegexp("main.go", "for range"))) + p := &protocol.SemanticTokensParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: env.Sandbox.Workdir.URI("main.go"), + }, + } + v, err := env.Editor.Server.SemanticTokensFull(env.Ctx, p) + if err != nil { + t.Fatal(err) + } + seen := interpret(v.Data, env.Editor.BufferText("main.go")) + if x := cmp.Diff(want, seen); x != "" { + t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) + } + }) + +} + +type result struct { + Token string + TokenType string + Mod string +} + +// human-readable version of the semantic tokens +// comment, string, number are elided +// (and in the future, maybe elide other things, like operators) +func interpret(x []uint32, contents string) []result { + lines := strings.Split(contents, "\n") + ans := []result{} + line, col := 1, 1 + for i := 0; i < len(x); i += 5 { + line += int(x[i]) + col += int(x[i+1]) + if x[i] != 0 { // new line + col = int(x[i+1]) + 1 // 1-based column numbers + } + sz := x[i+2] + t := semanticTypes[x[i+3]] + if t == "comment" || t == "string" || t == "number" { + continue + } + l := x[i+4] + var mods []string + for i, mod := range semanticModifiers { + if l&(1< Date: Wed, 16 Nov 2022 10:40:44 -0500 Subject: [PATCH 431/723] gopls/internal/regtest/misc: fix use of the AfterChange API Fix a broken build due to a change in the AfterChange API. Change-Id: I9719a4c7721c768e2704655c80cd510386255b3e Reviewed-on: https://go-review.googlesource.com/c/tools/+/451236 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Bryan Mills Run-TryBot: Robert Findley Auto-Submit: Robert Findley --- gopls/internal/regtest/misc/semantictokens_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index 8921995cf7e..cd9dce35c68 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -93,8 +93,7 @@ func Add[T int](target T, l []T) []T { Settings{"semanticTokens": true}, ).Run(t, src, func(t *testing.T, env *Env) { env.OpenFile("main.go") - //env.Await(EmptyOrNoDiagnostics("main.go`")) - env.Await(env.AfterChange(env.DiagnosticAtRegexp("main.go", "for range"))) + env.AfterChange(env.DiagnosticAtRegexp("main.go", "for range")) p := &protocol.SemanticTokensParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: env.Sandbox.Workdir.URI("main.go"), From ce26db4201145d6f18df8e520311478e3de38a52 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 22 Aug 2022 15:21:17 -0400 Subject: [PATCH 432/723] go/analysis: add Pass.TypeErrors field The result of type checking is a package (types.Package), type annotations on syntax trees (types.Info), and zero or more type errors (types.Error). Hitherto we had assumed that analyzers don't need access to type errors, but in fact it turns out to be useful: for example, analyzers can look at broken code and suggest quick fixes to repair the mistakes. This change adds a Pass.TypeErrors field that holds the errors produced during type checking. It may be non-nil only when the Analyzer.RunDespiteErrors flag was enabled. Similarly, it adds a TypeErrors field to go/packages.Package. (The existing Packages.Error field loses important details.) Gopls was already using analyzers in this way, (ab)using its privileged position in the x/tools repo. This change removes the need for such hacks. We expect that all analysis drivers that support RunDespiteErrors will in due course populate the Pass.TypesErrors field. This change updates the go/packages-based driver to do so; no changes were needed to unitchecker since it does not support RunDespiteErrors. In the meantime, not populating this field is not expected to cause any compatibility problems. Fixes golang/go#54619 Change-Id: Ia7c72242e332782e8919a4c30b2107c37bcec9ab Reviewed-on: https://go-review.googlesource.com/c/tools/+/425095 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan Reviewed-by: Tim King --- go/analysis/analysis.go | 15 +--- go/analysis/internal/checker/checker.go | 86 +++---------------- go/analysis/unitchecker/unitchecker.go | 1 + go/packages/packages.go | 4 + .../lsp/analysis/fillreturns/fillreturns.go | 3 +- .../lsp/analysis/nonewvars/nonewvars.go | 5 +- .../analysis/noresultvalues/noresultvalues.go | 5 +- .../lsp/analysis/stubmethods/stubmethods.go | 2 +- .../lsp/analysis/undeclaredname/undeclared.go | 4 +- .../analysis/unusedvariable/unusedvariable.go | 3 +- gopls/internal/lsp/cache/analysis.go | 3 +- internal/analysisinternal/analysis.go | 16 +--- 12 files changed, 29 insertions(+), 118 deletions(-) diff --git a/go/analysis/analysis.go b/go/analysis/analysis.go index d11505a165c..44ada22a03a 100644 --- a/go/analysis/analysis.go +++ b/go/analysis/analysis.go @@ -11,8 +11,6 @@ import ( "go/token" "go/types" "reflect" - - "golang.org/x/tools/internal/analysisinternal" ) // An Analyzer describes an analysis function and its options. @@ -48,6 +46,7 @@ type Analyzer struct { // RunDespiteErrors allows the driver to invoke // the Run method of this analyzer even on a // package that contains parse or type errors. + // The Pass.TypeErrors field may consequently be non-empty. RunDespiteErrors bool // Requires is a set of analyzers that must run successfully @@ -75,17 +74,6 @@ type Analyzer struct { func (a *Analyzer) String() string { return a.Name } -func init() { - // Set the analysisinternal functions to be able to pass type errors - // to the Pass type without modifying the go/analysis API. - analysisinternal.SetTypeErrors = func(p interface{}, errors []types.Error) { - p.(*Pass).typeErrors = errors - } - analysisinternal.GetTypeErrors = func(p interface{}) []types.Error { - return p.(*Pass).typeErrors - } -} - // A Pass provides information to the Run function that // applies a specific analyzer to a single Go package. // @@ -106,6 +94,7 @@ type Pass struct { Pkg *types.Package // type information about the package TypesInfo *types.Info // type information about the syntax trees TypesSizes types.Sizes // function for computing sizes of types + TypeErrors []types.Error // type errors (only if Analyzer.RunDespiteErrors) // Report reports a Diagnostic, a finding about a specific location // in the analyzed source code such as a potential mistake. diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index 2972e44155a..cf76bdebe71 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -26,7 +26,6 @@ import ( "runtime/pprof" "runtime/trace" "sort" - "strconv" "strings" "sync" "time" @@ -34,7 +33,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/internal/analysisflags" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/analysisinternal" ) var ( @@ -699,14 +697,16 @@ func (act *action) execOnce() { // Run the analysis. pass := &analysis.Pass{ - Analyzer: act.a, - Fset: act.pkg.Fset, - Files: act.pkg.Syntax, - OtherFiles: act.pkg.OtherFiles, - IgnoredFiles: act.pkg.IgnoredFiles, - Pkg: act.pkg.Types, - TypesInfo: act.pkg.TypesInfo, - TypesSizes: act.pkg.TypesSizes, + Analyzer: act.a, + Fset: act.pkg.Fset, + Files: act.pkg.Syntax, + OtherFiles: act.pkg.OtherFiles, + IgnoredFiles: act.pkg.IgnoredFiles, + Pkg: act.pkg.Types, + TypesInfo: act.pkg.TypesInfo, + TypesSizes: act.pkg.TypesSizes, + TypeErrors: act.pkg.TypeErrors, + ResultOf: inputs, Report: func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) }, ImportObjectFact: act.importObjectFact, @@ -718,72 +718,6 @@ func (act *action) execOnce() { } act.pass = pass - var errors []types.Error - // Get any type errors that are attributed to the pkg. - // This is necessary to test analyzers that provide - // suggested fixes for compiler/type errors. - // TODO(adonovan): eliminate this hack; - // see https://github.com/golang/go/issues/54619. - for _, err := range act.pkg.Errors { - if err.Kind != packages.TypeError { - continue - } - - // Parse err.Pos, a string of form: "file:line:col" or "file:line" or "" or "-" - // The filename may have a single ASCII letter Windows drive prefix such as "C:" - var file string - var line, col int - var convErr error - words := strings.Split(err.Pos, ":") - if runtime.GOOS == "windows" && - len(words) > 2 && - len(words[0]) == 1 && - ('A' <= words[0][0] && words[0][0] <= 'Z' || - 'a' <= words[0][0] && words[0][0] <= 'z') { - words[1] = words[0] + ":" + words[1] - words = words[1:] - } - switch len(words) { - case 2: - // file:line - file = words[0] - line, convErr = strconv.Atoi(words[1]) - case 3: - // file:line:col - file = words[0] - line, convErr = strconv.Atoi(words[1]) - if convErr == nil { - col, convErr = strconv.Atoi(words[2]) - } - default: - continue - } - if convErr != nil { - continue - } - - // Extract the token positions from the error string. - // (This is guesswork: Fset may contain all manner - // of stale files with the same name.) - offset := -1 - act.pkg.Fset.Iterate(func(f *token.File) bool { - if f.Name() != file { - return true - } - offset = int(f.LineStart(line)) + col - 1 - return false - }) - if offset == -1 { - continue - } - errors = append(errors, types.Error{ - Fset: act.pkg.Fset, - Msg: err.Msg, - Pos: token.Pos(offset), - }) - } - analysisinternal.SetTypeErrors(pass, errors) - var err error if act.pkg.IllTyped && !pass.Analyzer.RunDespiteErrors { err = fmt.Errorf("analysis skipped due to errors in package") diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go index d9c8f11cdd4..6e6907d261f 100644 --- a/go/analysis/unitchecker/unitchecker.go +++ b/go/analysis/unitchecker/unitchecker.go @@ -340,6 +340,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re Pkg: pkg, TypesInfo: info, TypesSizes: tc.Sizes, + TypeErrors: nil, // unitchecker doesn't RunDespiteErrors ResultOf: inputs, Report: func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) }, ImportObjectFact: facts.ImportObjectFact, diff --git a/go/packages/packages.go b/go/packages/packages.go index 54d880d206e..046212347c0 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -303,6 +303,9 @@ type Package struct { // of the package, or while parsing or type-checking its files. Errors []Error + // TypeErrors contains the subset of errors produced during type checking. + TypeErrors []types.Error + // GoFiles lists the absolute file paths of the package's Go source files. GoFiles []string @@ -911,6 +914,7 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { case types.Error: // from type checker + lpkg.TypeErrors = append(lpkg.TypeErrors, err) errs = append(errs, Error{ Pos: err.Fset.Position(err.Pos).String(), Msg: err.Msg, diff --git a/gopls/internal/lsp/analysis/fillreturns/fillreturns.go b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go index 4415ddd66ec..c8146df2dd0 100644 --- a/gopls/internal/lsp/analysis/fillreturns/fillreturns.go +++ b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go @@ -52,9 +52,8 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, fmt.Errorf("nil TypeInfo") } - errors := analysisinternal.GetTypeErrors(pass) outer: - for _, typeErr := range errors { + for _, typeErr := range pass.TypeErrors { // Filter out the errors that are not relevant to this analyzer. if !FixesError(typeErr) { continue diff --git a/gopls/internal/lsp/analysis/nonewvars/nonewvars.go b/gopls/internal/lsp/analysis/nonewvars/nonewvars.go index e7fa430cc53..2d2044eb397 100644 --- a/gopls/internal/lsp/analysis/nonewvars/nonewvars.go +++ b/gopls/internal/lsp/analysis/nonewvars/nonewvars.go @@ -30,7 +30,7 @@ will turn into ` var Analyzer = &analysis.Analyzer{ - Name: string(analysisinternal.NoNewVars), + Name: "nonewvars", Doc: Doc, Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: run, @@ -39,7 +39,6 @@ var Analyzer = &analysis.Analyzer{ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - errors := analysisinternal.GetTypeErrors(pass) nodeFilter := []ast.Node{(*ast.AssignStmt)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { @@ -60,7 +59,7 @@ func run(pass *analysis.Pass) (interface{}, error) { return } - for _, err := range errors { + for _, err := range pass.TypeErrors { if !FixesError(err.Msg) { continue } diff --git a/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go index b9f21f3135e..b3fd84bcc96 100644 --- a/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go +++ b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go @@ -29,7 +29,7 @@ will turn into ` var Analyzer = &analysis.Analyzer{ - Name: string(analysisinternal.NoResultValues), + Name: "noresultvalues", Doc: Doc, Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: run, @@ -38,7 +38,6 @@ var Analyzer = &analysis.Analyzer{ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - errors := analysisinternal.GetTypeErrors(pass) nodeFilter := []ast.Node{(*ast.ReturnStmt)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { @@ -55,7 +54,7 @@ func run(pass *analysis.Pass) (interface{}, error) { return } - for _, err := range errors { + for _, err := range pass.TypeErrors { if !FixesError(err.Msg) { continue } diff --git a/gopls/internal/lsp/analysis/stubmethods/stubmethods.go b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go index f9dc69a9652..effeb8a405d 100644 --- a/gopls/internal/lsp/analysis/stubmethods/stubmethods.go +++ b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go @@ -35,7 +35,7 @@ var Analyzer = &analysis.Analyzer{ } func run(pass *analysis.Pass) (interface{}, error) { - for _, err := range analysisinternal.GetTypeErrors(pass) { + for _, err := range pass.TypeErrors { ifaceErr := strings.Contains(err.Msg, "missing method") || strings.HasPrefix(err.Msg, "cannot convert") if !ifaceErr { continue diff --git a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go index fd8d35eb1b9..742a4d265df 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -38,7 +38,7 @@ func <>(inferred parameters) { ` var Analyzer = &analysis.Analyzer{ - Name: string(analysisinternal.UndeclaredName), + Name: "undeclaredname", Doc: Doc, Requires: []*analysis.Analyzer{}, Run: run, @@ -49,7 +49,7 @@ var Analyzer = &analysis.Analyzer{ var undeclaredNamePrefixes = []string{"undeclared name: ", "undefined: "} func run(pass *analysis.Pass) (interface{}, error) { - for _, err := range analysisinternal.GetTypeErrors(pass) { + for _, err := range pass.TypeErrors { runForError(pass, err) } return nil, nil diff --git a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go index 134cbb2c436..904016be71e 100644 --- a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go +++ b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go @@ -16,7 +16,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/internal/analysisinternal" ) const Doc = `check for unused variables @@ -36,7 +35,7 @@ var Analyzer = &analysis.Analyzer{ var unusedVariableSuffixes = []string{" declared and not used", " declared but not used"} func run(pass *analysis.Pass) (interface{}, error) { - for _, typeErr := range analysisinternal.GetTypeErrors(pass) { + for _, typeErr := range pass.TypeErrors { for _, suffix := range unusedVariableSuffixes { if strings.HasSuffix(typeErr.Msg, suffix) { varName := strings.TrimSuffix(typeErr.Msg, suffix) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 43f1afbe89c..e2c56a53c0f 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -17,7 +17,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" @@ -291,6 +290,7 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a Pkg: pkg.GetTypes(), TypesInfo: pkg.GetTypesInfo(), TypesSizes: pkg.GetTypesSizes(), + TypeErrors: pkg.typeErrors, ResultOf: inputs, Report: func(d analysis.Diagnostic) { // Prefix the diagnostic category with the analyzer's name. @@ -351,7 +351,6 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a return facts }, } - analysisinternal.SetTypeErrors(pass, pkg.typeErrors) if (pkg.HasListOrParseErrors() || pkg.HasTypeErrors()) && !analyzer.RunDespiteErrors { return nil, fmt.Errorf("skipping analysis %s because package %s contains errors", analyzer.Name, pkg.ID()) diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index 6fceef5e720..d15f0eb7abf 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package analysisinternal exposes internal-only fields from go/analysis. +// Package analysisinternal provides gopls' internal analyses with a +// number of helper functions that operate on typed syntax trees. package analysisinternal import ( @@ -18,11 +19,6 @@ import ( // in Go 1.18+. var DiagnoseFuzzTests bool = false -var ( - GetTypeErrors func(p interface{}) []types.Error - SetTypeErrors func(p interface{}, errors []types.Error) -) - func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos { // Get the end position for the type error. offset, end := fset.PositionFor(start, false).Offset, start @@ -210,14 +206,6 @@ func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { } } -type TypeErrorPass string - -const ( - NoNewVars TypeErrorPass = "nonewvars" - NoResultValues TypeErrorPass = "noresultvalues" - UndeclaredName TypeErrorPass = "undeclaredname" -) - // StmtToInsertVarBefore returns the ast.Stmt before which we can safely insert a new variable. // Some examples: // From 434d569df75794e10a5cc02a37ba796737f4de5d Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 15 Nov 2022 10:56:08 -0500 Subject: [PATCH 433/723] gopls/internal/lsp/regtest: improve documentation Start turning the regtest package documentation into more of a guide for new users. Along the way, move runner options into a separate options.go file, for discoverability. Change-Id: I18dec7c632df3e491d166a00959b9b5648d9ddf0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/450677 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/regtest/doc.go | 163 ++++++++++++++++++++++---- gopls/internal/lsp/regtest/options.go | 105 +++++++++++++++++ gopls/internal/lsp/regtest/runner.go | 102 +--------------- 3 files changed, 249 insertions(+), 121 deletions(-) create mode 100644 gopls/internal/lsp/regtest/options.go diff --git a/gopls/internal/lsp/regtest/doc.go b/gopls/internal/lsp/regtest/doc.go index 39eddd8dcff..4f4c7c020ba 100644 --- a/gopls/internal/lsp/regtest/doc.go +++ b/gopls/internal/lsp/regtest/doc.go @@ -12,25 +12,146 @@ // // The regtest package provides an API for developers to express these types of // user interactions in ordinary Go tests, validate them, and run them in a -// variety of execution modes (see gopls/doc/daemon.md for more information on -// execution modes). This is achieved roughly as follows: -// - the Runner type starts and connects to a gopls instance for each -// configured execution mode. -// - the Env type provides a collection of resources to use in writing tests -// (for example a temporary working directory and fake text editor) -// - user interactions with these resources are scripted using test wrappers -// around the API provided by the golang.org/x/tools/gopls/internal/lsp/fake -// package. -// -// Regressions are expressed in terms of Expectations, which at a high level -// are conditions that we expect to be met (or not to be met) at some point -// after performing the interactions in the test. This is necessary because the -// LSP is by construction asynchronous: both client and server can send -// each other notifications without formal acknowledgement that they have been -// fully processed. -// -// Simple Expectations may be combined to match specific conditions reported by -// the user. In the example above, a regtest validating that the user-reported -// bug had been fixed would "expect" that the editor never displays the -// confusing diagnostic. +// variety of execution modes. +// +// # Test package setup +// +// The regression test package uses a couple of uncommon patterns to reduce +// boilerplate in test bodies. First, it is intended to be imported as "." so +// that helpers do not need to be qualified. Second, it requires some setup +// that is currently implemented in the regtest.Main function, which must be +// invoked by TestMain. Therefore, a minimal regtest testing package looks +// like this: +// +// package lsptests +// +// import ( +// "fmt" +// "testing" +// +// "golang.org/x/tools/gopls/internal/hooks" +// . "golang.org/x/tools/gopls/internal/lsp/regtest" +// ) +// +// func TestMain(m *testing.M) { +// Main(m, hooks.Options) +// } +// +// # Writing a simple regression test +// +// To run a regression test use the regtest.Run function, which accepts a +// txtar-encoded archive defining the initial workspace state. This function +// sets up the workspace in a temporary directory, creates a fake text editor, +// starts gopls, and initializes an LSP session. It then invokes the provided +// test function with an *Env handle encapsulating the newly created +// environment. Because gopls may be run in various modes (as a sidecar or +// daemon process, with different settings), the test runner may perform this +// process multiple times, re-running the test function each time with a new +// environment. +// +// func TestOpenFile(t *testing.T) { +// const files = ` +// -- go.mod -- +// module mod.com +// +// go 1.12 +// -- foo.go -- +// package foo +// ` +// Run(t, files, func(t *testing.T, env *Env) { +// env.OpenFile("foo.go") +// }) +// } +// +// # Configuring Regtest Execution +// +// The regtest package exposes several options that affect the setup process +// described above. To use these options, use the WithOptions function: +// +// WithOptions(opts...).Run(...) +// +// See options.go for a full list of available options. +// +// # Operating on editor state +// +// To operate on editor state within the test body, the Env type provides +// access to the workspace directory (Env.SandBox), text editor (Env.Editor), +// LSP server (Env.Server), and 'awaiter' (Env.Awaiter). +// +// In most cases, operations on these primitive building blocks of the +// regression test environment expect a Context (which should be a child of +// env.Ctx), and return an error. To avoid boilerplate, the Env exposes a set +// of wrappers in wrappers.go for use in scripting: +// +// env.CreateBuffer("c/c.go", "") +// env.EditBuffer("c/c.go", fake.Edit{ +// Text: `package c`, +// }) +// +// These wrappers thread through Env.Ctx, and call t.Fatal on any errors. +// +// # Expressing expectations +// +// The general pattern for a regression test is to script interactions with the +// fake editor and sandbox, and assert that gopls behaves correctly after each +// state change. Unfortunately, this is complicated by the fact that state +// changes are communicated to gopls via unidirectional client->server +// notifications (didOpen, didChange, etc.), and resulting gopls behavior such +// as diagnostics, logs, or messages is communicated back via server->client +// notifications. Therefore, within regression tests we must be able to say "do +// this, and then eventually gopls should do that". To achieve this, the +// regtest package provides a framework for expressing conditions that must +// eventually be met, in terms of the Expectation type. +// +// To express the assertion that "eventually gopls must meet these +// expectations", use env.Await(...): +// +// env.RegexpReplace("x/x.go", `package x`, `package main`) +// env.Await(env.DiagnosticAtRegexp("x/main.go", `fmt`)) +// +// Await evaluates the provided expectations atomically, whenever the client +// receives a state-changing notification from gopls. See expectation.go for a +// full list of available expectations. +// +// A fundamental problem with this model is that if gopls never meets the +// provided expectations, the test runner will hang until the test timeout +// (which defaults to 10m). There are two ways to work around this poor +// behavior: +// +// 1. Use a precondition to define precisely when we expect conditions to be +// met. Gopls provides the OnceMet(precondition, expectations...) pattern +// to express ("once this precondition is met, the following expectations +// must all hold"). To instrument preconditions, gopls uses verbose +// progress notifications to inform the client about ongoing work (see +// CompletedWork). The most common precondition is to wait for gopls to be +// done processing all change notifications, for which the regtest package +// provides the AfterChange helper. For example: +// +// // We expect diagnostics to be cleared after gopls is done processing the +// // didSave notification. +// env.SaveBuffer("a/go.mod") +// env.AfterChange(EmptyDiagnostics("a/go.mod")) +// +// 2. Set a shorter timeout during development, if you expect to be breaking +// tests. By setting the environment variable GOPLS_REGTEST_TIMEOUT=5s, +// regression tests will time out after 5 seconds. +// +// # Tips & Tricks +// +// Here are some tips and tricks for working with regression tests: +// +// 1. Set the environment variable GOPLS_REGTEST_TIMEOUT=5s during development. +// 2. Run tests with -short. This will only run regression tests in the +// default gopls execution mode. +// 3. Use capture groups to narrow regexp positions. All regular-expression +// based positions (such as DiagnosticAtRegexp) will match the position of +// the first capture group, if any are provided. This can be used to +// identify a specific position in the code for a pattern that may occur in +// multiple places. For example `var (mu) sync.Mutex` matches the position +// of "mu" within the variable declaration. +// 4. Read diagnostics into a variable to implement more complicated +// assertions about diagnostic state in the editor. To do this, use the +// pattern OnceMet(precondition, ReadDiagnostics("file.go", &d)) to capture +// the current diagnostics as soon as the precondition is met. This is +// preferable to accessing the diagnostics directly, as it avoids races. package regtest diff --git a/gopls/internal/lsp/regtest/options.go b/gopls/internal/lsp/regtest/options.go new file mode 100644 index 00000000000..3820e96b37f --- /dev/null +++ b/gopls/internal/lsp/regtest/options.go @@ -0,0 +1,105 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package regtest + +import "golang.org/x/tools/gopls/internal/lsp/fake" + +type runConfig struct { + editor fake.EditorConfig + sandbox fake.SandboxConfig + modes Mode + skipHooks bool +} + +// A RunOption augments the behavior of the test runner. +type RunOption interface { + set(*runConfig) +} + +type optionSetter func(*runConfig) + +func (f optionSetter) set(opts *runConfig) { + f(opts) +} + +// ProxyFiles configures a file proxy using the given txtar-encoded string. +func ProxyFiles(txt string) RunOption { + return optionSetter(func(opts *runConfig) { + opts.sandbox.ProxyFiles = fake.UnpackTxt(txt) + }) +} + +// Modes configures the execution modes that the test should run in. +// +// By default, modes are configured by the test runner. If this option is set, +// it overrides the set of default modes and the test runs in exactly these +// modes. +func Modes(modes Mode) RunOption { + return optionSetter(func(opts *runConfig) { + if opts.modes != 0 { + panic("modes set more than once") + } + opts.modes = modes + }) +} + +// WindowsLineEndings configures the editor to use windows line endings. +func WindowsLineEndings() RunOption { + return optionSetter(func(opts *runConfig) { + opts.editor.WindowsLineEndings = true + }) +} + +// Settings is a RunOption that sets user-provided configuration for the LSP +// server. +// +// As a special case, the env setting must not be provided via Settings: use +// EnvVars instead. +type Settings map[string]interface{} + +func (s Settings) set(opts *runConfig) { + if opts.editor.Settings == nil { + opts.editor.Settings = make(map[string]interface{}) + } + for k, v := range s { + opts.editor.Settings[k] = v + } +} + +// WorkspaceFolders configures the workdir-relative workspace folders to send +// to the LSP server. By default the editor sends a single workspace folder +// corresponding to the workdir root. To explicitly configure no workspace +// folders, use WorkspaceFolders with no arguments. +func WorkspaceFolders(relFolders ...string) RunOption { + if len(relFolders) == 0 { + // Use an empty non-nil slice to signal explicitly no folders. + relFolders = []string{} + } + return optionSetter(func(opts *runConfig) { + opts.editor.WorkspaceFolders = relFolders + }) +} + +// EnvVars sets environment variables for the LSP session. When applying these +// variables to the session, the special string $SANDBOX_WORKDIR is replaced by +// the absolute path to the sandbox working directory. +type EnvVars map[string]string + +func (e EnvVars) set(opts *runConfig) { + if opts.editor.Env == nil { + opts.editor.Env = make(map[string]string) + } + for k, v := range e { + opts.editor.Env[k] = v + } +} + +// InGOPATH configures the workspace working directory to be GOPATH, rather +// than a separate working directory for use with modules. +func InGOPATH() RunOption { + return optionSetter(func(opts *runConfig) { + opts.sandbox.InGoPath = true + }) +} diff --git a/gopls/internal/lsp/regtest/runner.go b/gopls/internal/lsp/regtest/runner.go index effc8aae4b2..c83318fbee2 100644 --- a/gopls/internal/lsp/regtest/runner.go +++ b/gopls/internal/lsp/regtest/runner.go @@ -23,14 +23,14 @@ import ( exec "golang.org/x/sys/execabs" - "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/debug" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/lsprpc" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/xcontext" @@ -131,104 +131,6 @@ type Runner struct { cancelRemote func() } -type runConfig struct { - editor fake.EditorConfig - sandbox fake.SandboxConfig - modes Mode - skipHooks bool -} - -// A RunOption augments the behavior of the test runner. -type RunOption interface { - set(*runConfig) -} - -type optionSetter func(*runConfig) - -func (f optionSetter) set(opts *runConfig) { - f(opts) -} - -// ProxyFiles configures a file proxy using the given txtar-encoded string. -func ProxyFiles(txt string) RunOption { - return optionSetter(func(opts *runConfig) { - opts.sandbox.ProxyFiles = fake.UnpackTxt(txt) - }) -} - -// Modes configures the execution modes that the test should run in. -// -// By default, modes are configured by the test runner. If this option is set, -// it overrides the set of default modes and the test runs in exactly these -// modes. -func Modes(modes Mode) RunOption { - return optionSetter(func(opts *runConfig) { - if opts.modes != 0 { - panic("modes set more than once") - } - opts.modes = modes - }) -} - -// WindowsLineEndings configures the editor to use windows line endings. -func WindowsLineEndings() RunOption { - return optionSetter(func(opts *runConfig) { - opts.editor.WindowsLineEndings = true - }) -} - -// Settings is a RunOption that sets user-provided configuration for the LSP -// server. -// -// As a special case, the env setting must not be provided via Settings: use -// EnvVars instead. -type Settings map[string]interface{} - -func (s Settings) set(opts *runConfig) { - if opts.editor.Settings == nil { - opts.editor.Settings = make(map[string]interface{}) - } - for k, v := range s { - opts.editor.Settings[k] = v - } -} - -// WorkspaceFolders configures the workdir-relative workspace folders to send -// to the LSP server. By default the editor sends a single workspace folder -// corresponding to the workdir root. To explicitly configure no workspace -// folders, use WorkspaceFolders with no arguments. -func WorkspaceFolders(relFolders ...string) RunOption { - if len(relFolders) == 0 { - // Use an empty non-nil slice to signal explicitly no folders. - relFolders = []string{} - } - return optionSetter(func(opts *runConfig) { - opts.editor.WorkspaceFolders = relFolders - }) -} - -// EnvVars sets environment variables for the LSP session. When applying these -// variables to the session, the special string $SANDBOX_WORKDIR is replaced by -// the absolute path to the sandbox working directory. -type EnvVars map[string]string - -func (e EnvVars) set(opts *runConfig) { - if opts.editor.Env == nil { - opts.editor.Env = make(map[string]string) - } - for k, v := range e { - opts.editor.Env[k] = v - } -} - -// InGOPATH configures the workspace working directory to be GOPATH, rather -// than a separate working directory for use with modules. -func InGOPATH() RunOption { - return optionSetter(func(opts *runConfig) { - opts.sandbox.InGoPath = true - }) -} - type TestFunc func(t *testing.T, env *Env) // Run executes the test function in the default configured gopls execution From 3d085f3fce3726c1fa1dc82f8aac815b3ee3995f Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Thu, 17 Nov 2022 14:02:26 -0500 Subject: [PATCH 434/723] gopls/internal/lsp/lsprpc: eliminate arbitrary timeout in TestEnvForwarding Fixes golang/go#56804. Change-Id: I1df083bbc4f8c7a41d08ec85fceaa018d26b2f8b Reviewed-on: https://go-review.googlesource.com/c/tools/+/451599 Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Bryan Mills Reviewed-by: Robert Findley --- gopls/internal/lsp/lsprpc/lsprpc_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gopls/internal/lsp/lsprpc/lsprpc_test.go b/gopls/internal/lsp/lsprpc/lsprpc_test.go index 5718dfec1fa..1bf20b521cb 100644 --- a/gopls/internal/lsp/lsprpc/lsprpc_test.go +++ b/gopls/internal/lsp/lsprpc/lsprpc_test.go @@ -12,13 +12,13 @@ import ( "testing" "time" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/debug" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/testenv" ) @@ -290,9 +290,9 @@ func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamIniti func TestEnvForwarding(t *testing.T) { testenv.NeedsGo1Point(t, 13) + ctx := context.Background() + server := &initServer{} - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() _, tsForwarded, cleanup := setupForwarding(ctx, t, server) defer cleanup() From ba373eed34972e2019f16e0ba56ea678376edd79 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Thu, 17 Nov 2022 13:57:18 -0500 Subject: [PATCH 435/723] playground/socket: eliminate an arbitrary timeout in TestLimiter Fixes golang/go#55238. Change-Id: I17fe3865f7ec4d8889cdf71497b10a2ad0b41c11 Reviewed-on: https://go-review.googlesource.com/c/tools/+/451598 TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills Auto-Submit: Bryan Mills gopls-CI: kokoro Reviewed-by: Robert Findley --- playground/socket/socket_test.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/playground/socket/socket_test.go b/playground/socket/socket_test.go index b866e37afdb..d410afea875 100644 --- a/playground/socket/socket_test.go +++ b/playground/socket/socket_test.go @@ -69,9 +69,5 @@ func TestLimiter(t *testing.T) { if n != msgLimit+1 { t.Errorf("received %v messages, want %v", n, msgLimit+1) } - select { - case <-kr: - case <-time.After(100 * time.Millisecond): - t.Errorf("process wasn't killed after reaching limit") - } + <-kr } From dea100b118f1bbb1367d7a1a201c670c5f61fbae Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Thu, 17 Nov 2022 11:11:44 -0500 Subject: [PATCH 436/723] internal/testenv: skip tests that need export data for std if 'go tool compile' does not exist Now that .a files are no longer being preinstalled for standard-library packages (#47257), tests that require the export data from those files must have access to 'go list -export' in order to generate and load export data from the Go build cache. `go list -export` does not work without a Go compiler, so assume that tests that need the 'go' command also need the compiler. This may cause the tests currently failing on the android-.*-emu builders to instead be skipped, since the test harness does not copy the compiler to the execution environment. For golang/go#47257. Change-Id: Ie82ab09ac8165a01fc1d3a083b1e86226efc469d Reviewed-on: https://go-review.googlesource.com/c/tools/+/451597 TryBot-Result: Gopher Robot Auto-Submit: Bryan Mills Run-TryBot: Bryan Mills Reviewed-by: Michael Matloob gopls-CI: kokoro --- go/gcexportdata/example_test.go | 4 ++-- go/packages/overlay_test.go | 1 + go/ssa/builder_test.go | 5 +++++ go/ssa/example_test.go | 3 +++ go/ssa/ssautil/load_test.go | 2 ++ gopls/internal/lsp/regtest/runner.go | 1 + gopls/internal/lsp/safetoken/safetoken_test.go | 3 +++ gopls/internal/vulncheck/command_test.go | 4 ++++ internal/gcimporter/gcimporter_test.go | 7 +++++++ internal/gcimporter/iexport_go118_test.go | 3 +++ internal/testenv/testenv.go | 17 +++++++++++++++++ 11 files changed, 48 insertions(+), 2 deletions(-) diff --git a/go/gcexportdata/example_test.go b/go/gcexportdata/example_test.go index cdd68f49c53..7371d31d430 100644 --- a/go/gcexportdata/example_test.go +++ b/go/gcexportdata/example_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.7 && gc -// +build go1.7,gc +//go:build go1.7 && gc && !android && !ios && !js +// +build go1.7,gc,!android,!ios,!js package gcexportdata_test diff --git a/go/packages/overlay_test.go b/go/packages/overlay_test.go index f2164c274e2..93e306a4b94 100644 --- a/go/packages/overlay_test.go +++ b/go/packages/overlay_test.go @@ -1046,6 +1046,7 @@ func Hi() { // This does not use go/packagestest because it needs to write a replace // directive with an absolute path in one of the module's go.mod files. func TestOverlaysInReplace(t *testing.T) { + testenv.NeedsGoPackages(t) t.Parallel() // Create module b.com in a temporary directory. Do not add any Go files diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index 1975e26e45c..6fc844187db 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -24,6 +24,7 @@ import ( "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" ) @@ -32,6 +33,8 @@ func isEmpty(f *ssa.Function) bool { return f.Blocks == nil } // Tests that programs partially loaded from gc object files contain // functions with no code for the external portions, but are otherwise ok. func TestBuildPackage(t *testing.T) { + testenv.NeedsGoBuild(t) // for importer.Default() + input := ` package main @@ -164,6 +167,8 @@ func main() { // TestRuntimeTypes tests that (*Program).RuntimeTypes() includes all necessary types. func TestRuntimeTypes(t *testing.T) { + testenv.NeedsGoBuild(t) // for importer.Default() + tests := []struct { input string want []string diff --git a/go/ssa/example_test.go b/go/ssa/example_test.go index 492a02f766e..9a5fd436928 100644 --- a/go/ssa/example_test.go +++ b/go/ssa/example_test.go @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !android && !ios && !js +// +build !android,!ios,!js + package ssa_test import ( diff --git a/go/ssa/ssautil/load_test.go b/go/ssa/ssautil/load_test.go index 707a1d0b69d..efa2ba40a8b 100644 --- a/go/ssa/ssautil/load_test.go +++ b/go/ssa/ssautil/load_test.go @@ -33,6 +33,8 @@ func main() { ` func TestBuildPackage(t *testing.T) { + testenv.NeedsGoBuild(t) // for importer.Default() + // There is a more substantial test of BuildPackage and the // SSA program it builds in ../ssa/builder_test.go. diff --git a/gopls/internal/lsp/regtest/runner.go b/gopls/internal/lsp/regtest/runner.go index c83318fbee2..02be6fa40ee 100644 --- a/gopls/internal/lsp/regtest/runner.go +++ b/gopls/internal/lsp/regtest/runner.go @@ -141,6 +141,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio // refactoring. t.Helper() checkBuilder(t) + testenv.NeedsGoPackages(t) tests := []struct { name string diff --git a/gopls/internal/lsp/safetoken/safetoken_test.go b/gopls/internal/lsp/safetoken/safetoken_test.go index 1486d68f327..86ece1f8e6f 100644 --- a/gopls/internal/lsp/safetoken/safetoken_test.go +++ b/gopls/internal/lsp/safetoken/safetoken_test.go @@ -10,6 +10,7 @@ import ( "testing" "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/testenv" ) // This test reports any unexpected uses of (*go/token.File).Offset within @@ -17,6 +18,8 @@ import ( // to panicking. All calls to (*go/token.File).Offset should be replaced with // calls to safetoken.Offset. func TestTokenOffset(t *testing.T) { + testenv.NeedsGoPackages(t) + fset := token.NewFileSet() pkgs, err := packages.Load(&packages.Config{ Fset: fset, diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index a9ab2c0a731..36fd76d7858 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -25,6 +25,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/vulncheck/vulntest" + "golang.org/x/tools/internal/testenv" ) func TestCmd_Run(t *testing.T) { @@ -276,6 +277,9 @@ modules: ` func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Context, source.Snapshot)) { + testenv.NeedsGoBuild(t) + testenv.NeedsGoPackages(t) + ws, err := fake.NewSandbox(&fake.SandboxConfig{ Files: fake.UnpackTxt(workspaceData), ProxyFiles: fake.UnpackTxt(proxyData), diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index e4029c0d5e1..56f39182786 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -373,6 +373,7 @@ func TestVersionHandling(t *testing.T) { func TestImportStdLib(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache dt := maxTime if testing.Short() && os.Getenv("GO_BUILDER_NAME") == "" { @@ -422,6 +423,7 @@ func TestImportedTypes(t *testing.T) { testenv.NeedsGo1Point(t, 11) // This package only handles gc export data. needsCompiler(t, "gc") + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache for _, test := range importedObjectTests { obj := importObject(t, test.name) @@ -447,6 +449,8 @@ func TestImportedTypes(t *testing.T) { func TestImportedConsts(t *testing.T) { testenv.NeedsGo1Point(t, 11) + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache + tests := []struct { name string want constant.Kind @@ -530,6 +534,7 @@ func verifyInterfaceMethodRecvs(t *testing.T, named *types.Named, level int) { func TestIssue5815(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache pkg := importPkg(t, "strings", ".") @@ -555,6 +560,7 @@ func TestIssue5815(t *testing.T) { func TestCorrectMethodPackage(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache imports := make(map[string]*types.Package) _, err := Import(imports, "net/http", ".", nil) @@ -609,6 +615,7 @@ func TestIssue13566(t *testing.T) { func TestIssue13898(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache // import go/internal/gcimporter which imports go/types partially imports := make(map[string]*types.Package) diff --git a/internal/gcimporter/iexport_go118_test.go b/internal/gcimporter/iexport_go118_test.go index 27ba8cec5ac..c60a9b5ee86 100644 --- a/internal/gcimporter/iexport_go118_test.go +++ b/internal/gcimporter/iexport_go118_test.go @@ -22,6 +22,7 @@ import ( "testing" "golang.org/x/tools/internal/gcimporter" + "golang.org/x/tools/internal/testenv" ) // TODO(rfindley): migrate this to testdata, as has been done in the standard library. @@ -96,6 +97,8 @@ func testExportSrc(t *testing.T, src []byte) { } func TestImportTypeparamTests(t *testing.T) { + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache + // Check go files in test/typeparam. rootDir := filepath.Join(runtime.GOROOT(), "test", "typeparam") list, err := os.ReadDir(rootDir) diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index f606cb71543..22df96a858c 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -100,6 +100,23 @@ func hasTool(tool string) error { GOROOT := strings.TrimSpace(string(out)) if GOROOT != runtime.GOROOT() { checkGoGoroot.err = fmt.Errorf("'go env GOROOT' does not match runtime.GOROOT:\n\tgo env: %s\n\tGOROOT: %s", GOROOT, runtime.GOROOT()) + return + } + + // Also ensure that that GOROOT includes a compiler: 'go' commands + // don't in general work without it, and some builders + // (such as android-amd64-emu) seem to lack it in the test environment. + cmd := exec.Command(tool, "tool", "-n", "compile") + stderr := new(bytes.Buffer) + stderr.Write([]byte("\n")) + cmd.Stderr = stderr + out, err = cmd.Output() + if err != nil { + checkGoGoroot.err = fmt.Errorf("%v: %v%s", cmd, err, stderr) + return + } + if _, err := os.Stat(string(bytes.TrimSpace(out))); err != nil { + checkGoGoroot.err = err } }) if checkGoGoroot.err != nil { From 32a17c01dd476e07b4ac57d807247a48bbc7885e Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Fri, 11 Nov 2022 19:44:26 -0500 Subject: [PATCH 437/723] gopls/internal/lsp/mod: fix unaffecting vuln diagnostics msgs When there is no affecting vulns, we should not add the warning-level message. This bug made informational diagnostic messages look like "example.com/mod has vulnerabilities used in the code: . example.com/mod has a vulnerability GO-2022-0493 that is not used in the code." Also, add a test case that checks the code with only non-affecting vulnerability. Change-Id: I88700a538495bc5c1c1a515bd1f119b2705964a3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/450276 Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/mod/diagnostics.go | 25 ++- gopls/internal/regtest/misc/vuln_test.go | 275 +++++++++++++++-------- 2 files changed, 200 insertions(+), 100 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 82155738a5c..a51b1e3e5f5 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -197,7 +197,14 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, if len(vulns) == 0 { continue } - rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) + // note: req.Syntax is the line corresponding to 'require', which means + // req.Syntax.Start can point to the beginning of the "require" keyword + // for a single line require (e.g. "require golang.org/x/mod v0.0.0"). + start := req.Syntax.Start.Byte + if len(req.Syntax.Token) == 3 { + start += len("require ") + } + rng, err := pm.Mapper.OffsetRange(start, req.Syntax.End.Byte) if err != nil { return nil, err } @@ -254,17 +261,17 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, sort.Strings(info) var b strings.Builder - if len(warning) == 1 { - fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", req.Mod.Path, warning[0]) - } else { - fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", ")) - } - if len(warning) == 0 { - if len(info) == 1 { + switch len(warning) { + case 0: + if n := len(info); n == 1 { fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", req.Mod.Path, info[0]) - } else { + } else if n > 1 { fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", req.Mod.Path, strings.Join(info, ", ")) } + case 1: + fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", req.Mod.Path, warning[0]) + default: + fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", ")) } vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 2afef9df78a..b38b184a610 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -94,7 +94,7 @@ description: | of this vulnerability. references: - href: pkg.go.dev/vuln/GO-2022-03 --- STD.yaml -- +-- GOSTDLIB.yaml -- modules: - module: stdlib versions: @@ -104,7 +104,7 @@ modules: symbols: - OpenReader references: - - href: pkg.go.dev/vuln/STD + - href: pkg.go.dev/vuln/GOSTDLIB ` func TestRunVulncheckExpStd(t *testing.T) { @@ -123,7 +123,7 @@ import ( ) func main() { - _, err := zip.OpenReader("file.zip") // vulnerability id: STD + _, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB fmt.Println(err) } ` @@ -175,7 +175,7 @@ func main() { env.Await( CompletedWork("govulncheck", 1, true), // TODO(hyangah): once the diagnostics are published, wait for diagnostics. - ShownMessage("Found STD"), + ShownMessage("Found GOSTDLIB"), ) }) } @@ -225,6 +225,7 @@ func Y() { } ` +// cmod/c imports amod/avuln and bmod/bvuln. const proxy1 = ` -- golang.org/cmod@v1.1.3/go.mod -- module golang.org/cmod @@ -284,71 +285,54 @@ func Vuln() { } ` -func TestRunVulncheckExp(t *testing.T) { +func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) { + db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) + if err != nil { + return nil, nil, nil + } + settings := Settings{ + "codelenses": map[string]bool{ + "run_vulncheck_exp": true, + }, + } + ev := EnvVars{ + // Let the analyzer read vulnerabilities data from the testdata/vulndb. + "GOVULNDB": db.URI(), + // When fetching stdlib package vulnerability info, + // behave as if our go version is go1.18 for this testing. + // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). + vulncheck.GoVersionForVulnTest: "go1.18", + "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. + "GOSUMDB": "off", + } + return db, []RunOption{ProxyFiles(proxyData), ev, settings}, nil +} + +func TestRunVulncheckWarning(t *testing.T) { testenv.NeedsGo1Point(t, 18) - db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) + db, opts, err := vulnTestEnv(vulnsData, proxy1) if err != nil { t.Fatal(err) } defer db.Clean() - WithOptions( - ProxyFiles(proxy1), - EnvVars{ - // Let the analyzer read vulnerabilities data from the testdata/vulndb. - "GOVULNDB": db.URI(), - // When fetching stdlib package vulnerability info, - // behave as if our go version is go1.18 for this testing. - // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). - vulncheck.GoVersionForVulnTest: "go1.18", - "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. - }, - Settings{ - "codelenses": map[string]bool{ - "run_vulncheck_exp": true, - }, - }, - ).Run(t, workspace1, func(t *testing.T, env *Env) { + WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp) - d := &protocol.PublishDiagnosticsParams{} + gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.Await( CompletedWork("govulncheck", 1, true), ShownMessage("Found"), OnceMet( env.DiagnosticAtRegexp("go.mod", `golang.org/amod`), - ReadDiagnostics("go.mod", d), + ReadDiagnostics("go.mod", gotDiagnostics), ), ) - - type diagnostic struct { - msg string - severity protocol.DiagnosticSeverity - // codeActions is a list titles of code actions that we get with this - // diagnostics as the context. - codeActions []string - } - // wantDiagnostics maps a module path in the require - // section of a go.mod to diagnostics that will be returned - // when running vulncheck. - wantDiagnostics := map[string]struct { - // applyAction is the title of the code action to run for this module. - // If empty, no code actions will be executed. - applyAction string - // diagnostics is the list of diagnostics we expect at the require line for - // the module path. - diagnostics []diagnostic - // codeActions is a list titles of code actions that we get with context - // diagnostics. - codeActions []string - // hover message is the list of expected hover message parts for this go.mod require line. - // all parts must appear in the hover message. - hover []string - }{ + wantDiagnostics := map[string]vulnDiagExpectation{ "golang.org/amod": { applyAction: "Upgrade to v1.0.4", - diagnostics: []diagnostic{ + diagnostics: []vulnDiag{ { msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.", severity: protocol.SeverityWarning, @@ -365,7 +349,7 @@ func TestRunVulncheckExp(t *testing.T) { hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, }, "golang.org/bmod": { - diagnostics: []diagnostic{ + diagnostics: []vulnDiag{ { msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.", severity: protocol.SeverityWarning, @@ -376,44 +360,7 @@ func TestRunVulncheckExp(t *testing.T) { } for mod, want := range wantDiagnostics { - pos := env.RegexpSearch("go.mod", mod) - var modPathDiagnostics []protocol.Diagnostic - for _, w := range want.diagnostics { - // Find the diagnostics at pos. - var diag *protocol.Diagnostic - for _, g := range d.Diagnostics { - g := g - if g.Range.Start == pos.ToProtocolPosition() && w.msg == g.Message { - modPathDiagnostics = append(modPathDiagnostics, g) - diag = &g - break - } - } - if diag == nil { - t.Errorf("no diagnostic at %q matching %q found\n", mod, w.msg) - continue - } - if diag.Severity != w.severity { - t.Errorf("incorrect severity for %q, expected %s got %s\n", w.msg, w.severity, diag.Severity) - } - - gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag}) - if !sameCodeActions(gotActions, w.codeActions) { - t.Errorf("code actions for %q do not match, expected %v, got %v\n", w.msg, w.codeActions, gotActions) - continue - } - - // Check that useful info is supplemented as hover. - if len(want.hover) > 0 { - hover, _ := env.Hover("go.mod", pos) - for _, part := range want.hover { - if !strings.Contains(hover.Value, part) { - t.Errorf("hover contents for %q do not match, expected %v, got %v\n", w.msg, strings.Join(want.hover, ","), hover.Value) - break - } - } - } - } + modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) // Check that the actions we get when including all diagnostics at a location return the same result gotActions := env.CodeAction("go.mod", modPathDiagnostics) @@ -432,7 +379,6 @@ func TestRunVulncheckExp(t *testing.T) { break } } - } env.Await(env.DoneWithChangeWatchedFiles()) @@ -468,3 +414,150 @@ func sameCodeActions(gotActions []protocol.CodeAction, want []string) bool { } return true } + +const workspace2 = ` +-- go.mod -- +module golang.org/entry + +go 1.18 + +require golang.org/bmod v0.5.0 + +-- go.sum -- +golang.org/bmod v0.5.0 h1:MT/ysNRGbCiURc5qThRFWaZ5+rK3pQRPo9w7dYZfMDk= +golang.org/bmod v0.5.0/go.mod h1:k+zl+Ucu4yLIjndMIuWzD/MnOHy06wqr3rD++y0abVs= +-- x/x.go -- +package x + +import "golang.org/bmod/bvuln" + +func F() { + // Calls a benign func in bvuln. + bvuln.OK() +} +` + +const proxy2 = ` +-- golang.org/bmod@v0.5.0/bvuln/bvuln.go -- +package bvuln + +func Vuln() {} // vulnerable. +func OK() {} // ok. +` + +func TestRunVulncheckInfo(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + + db, opts, err := vulnTestEnv(vulnsData, proxy2) + if err != nil { + t.Fatal(err) + } + defer db.Clean() + WithOptions(opts...).Run(t, workspace2, func(t *testing.T, env *Env) { + env.OpenFile("go.mod") + env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp) + gotDiagnostics := &protocol.PublishDiagnosticsParams{} + env.Await( + CompletedWork("govulncheck", 1, true), + OnceMet( + env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"), + ReadDiagnostics("go.mod", gotDiagnostics)), + ShownMessage("No vulnerabilities found")) // only count affecting vulnerabilities. + + // wantDiagnostics maps a module path in the require + // section of a go.mod to diagnostics that will be returned + // when running vulncheck. + wantDiagnostics := map[string]vulnDiagExpectation{ + "golang.org/bmod": { + diagnostics: []vulnDiag{ + { + msg: "golang.org/bmod has a vulnerability GO-2022-02 that is not used in the code.", + severity: protocol.SeverityInformation, + }, + }, + hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."}, + }, + } + + for mod, want := range wantDiagnostics { + modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) + // Check that the actions we get when including all diagnostics at a location return the same result + gotActions := env.CodeAction("go.mod", modPathDiagnostics) + if !sameCodeActions(gotActions, want.codeActions) { + t.Errorf("code actions for %q do not match, expected %v, got %v\n", mod, want.codeActions, gotActions) + continue + } + } + }) +} + +// testVulnDiagnostics finds the require statement line for the requireMod in go.mod file +// and runs checks if diagnostics and code actions associated with the line match expectation. +func testVulnDiagnostics(t *testing.T, env *Env, requireMod string, want vulnDiagExpectation, got *protocol.PublishDiagnosticsParams) []protocol.Diagnostic { + t.Helper() + pos := env.RegexpSearch("go.mod", requireMod) + var modPathDiagnostics []protocol.Diagnostic + for _, w := range want.diagnostics { + // Find the diagnostics at pos. + var diag *protocol.Diagnostic + for _, g := range got.Diagnostics { + g := g + if g.Range.Start == pos.ToProtocolPosition() && w.msg == g.Message { + modPathDiagnostics = append(modPathDiagnostics, g) + diag = &g + break + } + } + if diag == nil { + t.Errorf("no diagnostic at %q matching %q found\n", requireMod, w.msg) + continue + } + if diag.Severity != w.severity { + t.Errorf("incorrect severity for %q, expected %s got %s\n", w.msg, w.severity, diag.Severity) + } + + gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag}) + if !sameCodeActions(gotActions, w.codeActions) { + t.Errorf("code actions for %q do not match, expected %v, got %v\n", w.msg, w.codeActions, gotActions) + continue + } + + // Check that useful info is supplemented as hover. + if len(want.hover) > 0 { + hover, _ := env.Hover("go.mod", pos) + for _, part := range want.hover { + if !strings.Contains(hover.Value, part) { + t.Errorf("hover contents for %q do not match, expected %v, got %v\n", w.msg, strings.Join(want.hover, ","), hover.Value) + break + } + } + } + } + return modPathDiagnostics +} + +type vulnDiag struct { + msg string + severity protocol.DiagnosticSeverity + // codeActions is a list titles of code actions that we get with this + // diagnostics as the context. + codeActions []string +} + +// wantVulncheckModDiagnostics maps a module path in the require +// section of a go.mod to diagnostics that will be returned +// when running vulncheck. +type vulnDiagExpectation struct { + // applyAction is the title of the code action to run for this module. + // If empty, no code actions will be executed. + applyAction string + // diagnostics is the list of diagnostics we expect at the require line for + // the module path. + diagnostics []vulnDiag + // codeActions is a list titles of code actions that we get with context + // diagnostics. + codeActions []string + // hover message is the list of expected hover message parts for this go.mod require line. + // all parts must appear in the hover message. + hover []string +} From 5a4eba5af06f7508c8a90c5568193de040e17de1 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 3 Nov 2022 18:28:39 -0400 Subject: [PATCH 438/723] internal/lsp/cache: remove support for invalid metadata Remove support for keeping track of invalid metadata, as discussed in golang/go#55333. Fixes golang/go#55333 Change-Id: I7727f43e2cffef610096d20af4898f326c5a8450 Reviewed-on: https://go-review.googlesource.com/c/tools/+/447741 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley --- gopls/doc/settings.md | 13 -- gopls/internal/lsp/cache/check.go | 20 +-- gopls/internal/lsp/cache/debug.go | 4 +- gopls/internal/lsp/cache/graph.go | 38 +---- gopls/internal/lsp/cache/load.go | 46 +++--- gopls/internal/lsp/cache/metadata.go | 9 -- gopls/internal/lsp/cache/mod_tidy.go | 2 +- gopls/internal/lsp/cache/snapshot.go | 95 +++-------- gopls/internal/lsp/source/api_json.go | 8 - gopls/internal/lsp/source/options.go | 13 +- .../regtest/diagnostics/diagnostics_test.go | 153 ------------------ 11 files changed, 55 insertions(+), 346 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 5595976363f..4c3c7a8182a 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -161,19 +161,6 @@ be removed. Default: `false`. -#### **experimentalUseInvalidMetadata** *bool* - -**This setting is experimental and may be deleted.** - -experimentalUseInvalidMetadata enables gopls to fall back on outdated -package metadata to provide editor features if the go command fails to -load packages for some reason (like an invalid go.mod file). - -Deprecated: this setting is deprecated and will be removed in a future -version of gopls (https://go.dev/issue/55333). - -Default: `false`. - #### **standaloneTags** *[]string* standaloneTags specifies a set of build constraints that identify diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 25da922db10..1f3622a73d1 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -46,7 +46,7 @@ type packageHandle struct { promise *memoize.Promise // [typeCheckResult] // m is the metadata associated with the package. - m *KnownMetadata + m *Metadata // key is the hashed key for the package. // @@ -105,12 +105,8 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // Don't use invalid metadata for dependencies if the top-level // metadata is valid. We only load top-level packages, so if the // top-level is valid, all of its dependencies should be as well. - if err != nil || m.Valid && !depHandle.m.Valid { - if err != nil { - event.Error(ctx, fmt.Sprintf("%s: no dep handle for %s", id, depID), err, source.SnapshotLabels(s)...) - } else { - event.Log(ctx, fmt.Sprintf("%s: invalid dep handle for %s", id, depID), source.SnapshotLabels(s)...) - } + if err != nil { + event.Error(ctx, fmt.Sprintf("%s: no dep handle for %s", id, depID), err, source.SnapshotLabels(s)...) // This check ensures we break out of the slow // buildPackageHandle recursion quickly when @@ -136,7 +132,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // syntax trees are available from (*pkg).File(URI). // TODO(adonovan): consider parsing them on demand? // The need should be rare. - goFiles, compiledGoFiles, err := readGoFiles(ctx, s, m.Metadata) + goFiles, compiledGoFiles, err := readGoFiles(ctx, s, m) if err != nil { return nil, err } @@ -146,7 +142,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so experimentalKey := s.View().Options().ExperimentalPackageCacheKey phKey := computePackageKey(m.ID, compiledGoFiles, m, depKey, mode, experimentalKey) promise, release := s.store.Promise(phKey, func(ctx context.Context, arg interface{}) interface{} { - pkg, err := typeCheckImpl(ctx, arg.(*snapshot), goFiles, compiledGoFiles, m.Metadata, mode, deps) + pkg, err := typeCheckImpl(ctx, arg.(*snapshot), goFiles, compiledGoFiles, m, mode, deps) return typeCheckResult{pkg, err} }) @@ -166,7 +162,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // packageHandle to the handles for parsing its files and the // handles for type-checking its immediate deps, at which // point there will be no need to even access s.meta.) - if s.meta.metadata[ph.m.ID].Metadata != ph.m.Metadata { + if s.meta.metadata[ph.m.ID] != ph.m { return nil, fmt.Errorf("stale metadata for %s", ph.m.ID) } @@ -174,7 +170,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so if prev, ok := s.packages.Get(packageKey); ok { prevPH := prev.(*packageHandle) release() - if prevPH.m.Metadata != ph.m.Metadata { + if prevPH.m != ph.m { return nil, bug.Errorf("existing package handle does not match for %s", ph.m.ID) } return prevPH, nil @@ -225,7 +221,7 @@ func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { // computePackageKey returns a key representing the act of type checking // a package named id containing the specified files, metadata, and // combined dependency hash. -func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata, depsKey source.Hash, mode source.ParseMode, experimentalKey bool) packageHandleKey { +func computePackageKey(id PackageID, files []source.FileHandle, m *Metadata, depsKey source.Hash, mode source.ParseMode, experimentalKey bool) packageHandleKey { // TODO(adonovan): opt: no need to materalize the bytes; hash them directly. // Also, use field separators to avoid spurious collisions. b := bytes.NewBuffer(nil) diff --git a/gopls/internal/lsp/cache/debug.go b/gopls/internal/lsp/cache/debug.go index d665b011daf..fd82aff301e 100644 --- a/gopls/internal/lsp/cache/debug.go +++ b/gopls/internal/lsp/cache/debug.go @@ -47,7 +47,7 @@ func (s *snapshot) dumpWorkspace(context string) { for _, id := range ids { pkgPath := s.workspacePackages[id] - m, ok := s.meta.metadata[id] - debugf(" %s:%s (metadata: %t; valid: %t)", id, pkgPath, ok, m.Valid) + _, ok := s.meta.metadata[id] + debugf(" %s:%s (metadata: %t)", id, pkgPath, ok) } } diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index 83336a23e15..be5a0618caa 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -15,7 +15,7 @@ import ( // graph of Go packages, as obtained from go/packages. type metadataGraph struct { // metadata maps package IDs to their associated metadata. - metadata map[PackageID]*KnownMetadata + metadata map[PackageID]*Metadata // importedBy maps package IDs to the list of packages that import them. importedBy map[PackageID][]PackageID @@ -27,12 +27,12 @@ type metadataGraph struct { // Clone creates a new metadataGraph, applying the given updates to the // receiver. -func (g *metadataGraph) Clone(updates map[PackageID]*KnownMetadata) *metadataGraph { +func (g *metadataGraph) Clone(updates map[PackageID]*Metadata) *metadataGraph { if len(updates) == 0 { // Optimization: since the graph is immutable, we can return the receiver. return g } - result := &metadataGraph{metadata: make(map[PackageID]*KnownMetadata, len(g.metadata))} + result := &metadataGraph{metadata: make(map[PackageID]*Metadata, len(g.metadata))} // Copy metadata. for id, m := range g.metadata { result.metadata[id] = m @@ -74,51 +74,25 @@ func (g *metadataGraph) build() { } // Sort and filter file associations. - // - // We choose the first non-empty set of package associations out of the - // following. For simplicity, call a non-command-line-arguments package a - // "real" package. - // - // 1: valid real packages - // 2: a valid command-line-arguments package - // 3: invalid real packages - // 4: an invalid command-line-arguments package for uri, ids := range g.ids { sort.Slice(ids, func(i, j int) bool { - // 1. valid packages appear earlier. - validi := g.metadata[ids[i]].Valid - validj := g.metadata[ids[j]].Valid - if validi != validj { - return validi - } - - // 2. command-line-args packages appear later. cli := source.IsCommandLineArguments(ids[i]) clj := source.IsCommandLineArguments(ids[j]) if cli != clj { return clj } - // 3. packages appear in name order. + // 2. packages appear in name order. return ids[i] < ids[j] }) // Choose the best IDs for each URI, according to the following rules: // - If there are any valid real packages, choose them. // - Else, choose the first valid command-line-argument package, if it exists. - // - Else, keep using all the invalid metadata. // // TODO(rfindley): it might be better to track all IDs here, and exclude // them later in PackagesForFile, but this is the existing behavior. - hasValidMetadata := false for i, id := range ids { - m := g.metadata[id] - if m.Valid { - hasValidMetadata = true - } else if hasValidMetadata { - g.ids[uri] = ids[:i] - break - } // If we've seen *anything* prior to command-line arguments package, take // it. Note that ids[0] may itself be command-line-arguments. if i > 0 && source.IsCommandLineArguments(id) { @@ -134,7 +108,7 @@ func (g *metadataGraph) build() { // // If includeInvalid is false, the algorithm ignores packages with invalid // metadata (including those in the given list of ids). -func (g *metadataGraph) reverseTransitiveClosure(includeInvalid bool, ids ...PackageID) map[PackageID]bool { +func (g *metadataGraph) reverseTransitiveClosure(ids ...PackageID) map[PackageID]bool { seen := make(map[PackageID]bool) var visitAll func([]PackageID) visitAll = func(ids []PackageID) { @@ -144,7 +118,7 @@ func (g *metadataGraph) reverseTransitiveClosure(includeInvalid bool, ids ...Pac } m := g.metadata[id] // Only use invalid metadata if we support it. - if m == nil || !(m.Valid || includeInvalid) { + if m == nil { continue } seen[id] = true diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index dcd343a56b9..04982643247 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -157,7 +157,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc moduleErrs := make(map[string][]packages.Error) // module path -> errors filterer := buildFilterer(s.view.rootURI.Filename(), s.view.gomodcache, s.view.Options()) - newMetadata := make(map[PackageID]*KnownMetadata) + newMetadata := make(map[PackageID]*Metadata) for _, pkg := range pkgs { // The Go command returns synthetic list results for module queries that // encountered module errors. @@ -222,10 +222,10 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc // // TODO(rfindley): perform a sanity check that metadata matches here. If not, // we have an invalidation bug elsewhere. - updates := make(map[PackageID]*KnownMetadata) + updates := make(map[PackageID]*Metadata) var updatedIDs []PackageID for _, m := range newMetadata { - if existing := s.meta.metadata[m.ID]; existing == nil || !existing.Valid { + if existing := s.meta.metadata[m.ID]; existing == nil { updates[m.ID] = m updatedIDs = append(updatedIDs, m.ID) delete(s.shouldLoad, m.ID) @@ -238,7 +238,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc // // Note that the original metadata is being invalidated here, so we use the // original metadata graph to compute the reverse closure. - invalidatedPackages := s.meta.reverseTransitiveClosure(true, updatedIDs...) + invalidatedPackages := s.meta.reverseTransitiveClosure(updatedIDs...) s.meta = s.meta.Clone(updates) s.resetIsActivePackageLocked() @@ -475,7 +475,7 @@ func makeWorkspaceDir(ctx context.Context, workspace *workspace, fs source.FileS // buildMetadata populates the updates map with metadata updates to // apply, based on the given pkg. It recurs through pkg.Imports to ensure that // metadata exists for all dependencies. -func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { +func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*Metadata, path []PackageID) error { // Allow for multiple ad-hoc packages in the workspace (see #47584). pkgPath := PackagePath(pkg.PkgPath) id := PackageID(pkg.ID) @@ -507,18 +507,15 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con } // Recreate the metadata rather than reusing it to avoid locking. - m := &KnownMetadata{ - Metadata: &Metadata{ - ID: id, - PkgPath: pkgPath, - Name: PackageName(pkg.Name), - ForTest: PackagePath(packagesinternal.GetForTest(pkg)), - TypesSizes: pkg.TypesSizes, - Config: cfg, - Module: pkg.Module, - depsErrors: packagesinternal.GetDepsErrors(pkg), - }, - Valid: true, + m := &Metadata{ + ID: id, + PkgPath: pkgPath, + Name: PackageName(pkg.Name), + ForTest: PackagePath(packagesinternal.GetForTest(pkg)), + TypesSizes: pkg.TypesSizes, + Config: cfg, + Module: pkg.Module, + depsErrors: packagesinternal.GetDepsErrors(pkg), } updates[id] = m @@ -650,7 +647,7 @@ func containsPackageLocked(s *snapshot, m *Metadata) bool { // the snapshot s. // // s.mu must be held while calling this function. -func containsOpenFileLocked(s *snapshot, m *KnownMetadata) bool { +func containsOpenFileLocked(s *snapshot, m *Metadata) bool { uris := map[span.URI]struct{}{} for _, uri := range m.CompiledGoFiles { uris[uri] = struct{}{} @@ -700,14 +697,7 @@ func containsFileInWorkspaceLocked(s *snapshot, m *Metadata) bool { func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[PackageID]PackagePath { workspacePackages := make(map[PackageID]PackagePath) for _, m := range meta.metadata { - // Don't consider invalid packages to be workspace packages. Doing so can - // result in type-checking and diagnosing packages that no longer exist, - // which can lead to memory leaks and confusing errors. - if !m.Valid { - continue - } - - if !containsPackageLocked(s, m.Metadata) { + if !containsPackageLocked(s, m) { continue } @@ -748,12 +738,12 @@ func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[Packag // function returns false. // // If m is not a command-line-arguments package, this is trivially true. -func allFilesHaveRealPackages(g *metadataGraph, m *KnownMetadata) bool { +func allFilesHaveRealPackages(g *metadataGraph, m *Metadata) bool { n := len(m.CompiledGoFiles) checkURIs: for _, uri := range append(m.CompiledGoFiles[0:n:n], m.GoFiles...) { for _, id := range g.ids[uri] { - if !source.IsCommandLineArguments(id) && (g.metadata[id].Valid || !m.Valid) { + if !source.IsCommandLineArguments(id) { continue checkURIs } } diff --git a/gopls/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go index 03037f0b09e..135623fc53b 100644 --- a/gopls/internal/lsp/cache/metadata.go +++ b/gopls/internal/lsp/cache/metadata.go @@ -86,12 +86,3 @@ func (m *Metadata) IsIntermediateTestVariant() bool { func (m *Metadata) ModuleInfo() *packages.Module { return m.Module } - -// KnownMetadata is a wrapper around metadata that tracks its validity. -type KnownMetadata struct { - *Metadata - - // Valid is true if the given metadata is Valid. - // Invalid metadata can still be used if a metadata reload fails. - Valid bool -} diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index cc604c1c86f..fc97ee6b25e 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -196,7 +196,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.Pars } // Read both lists of files of this package, in parallel. - goFiles, compiledGoFiles, err := readGoFiles(ctx, snapshot, m.Metadata) + goFiles, compiledGoFiles, err := readGoFiles(ctx, snapshot, m) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 70c26f253c7..656d08fb114 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -729,8 +729,6 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode // If experimentalUseInvalidMetadata is set, this function may return package // IDs with invalid metadata. func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]PackageID, error) { - useInvalidMetadata := s.useInvalidMetadata() - s.mu.Lock() // Start with the set of package associations derived from the last load. @@ -741,7 +739,7 @@ func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]Pack for _, id := range ids { // TODO(rfindley): remove the defensiveness here. s.meta.metadata[id] must // exist. - if m, ok := s.meta.metadata[id]; ok && m.Valid { + if _, ok := s.meta.metadata[id]; ok { hasValidID = true } if len(s.shouldLoad[id]) > 0 { @@ -757,16 +755,6 @@ func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]Pack s.mu.Unlock() - // Special case: if experimentalUseInvalidMetadata is set and we have any - // ids, just return them. - // - // This is arguably wrong: if the metadata is invalid we should try reloading - // it. However, this was the pre-existing behavior, and - // experimentalUseInvalidMetadata will be removed in a future release. - if !shouldLoad && useInvalidMetadata && len(ids) > 0 { - return ids, nil - } - // Reload if loading is likely to improve the package associations for uri: // - uri is not contained in any valid packages // - ...or one of the packages containing uri is marked 'shouldLoad' @@ -800,28 +788,19 @@ func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]Pack s.mu.Lock() ids = s.meta.ids[uri] - if !useInvalidMetadata { - var validIDs []PackageID - for _, id := range ids { - // TODO(rfindley): remove the defensiveness here as well. - if m, ok := s.meta.metadata[id]; ok && m.Valid { - validIDs = append(validIDs, id) - } + var validIDs []PackageID + for _, id := range ids { + // TODO(rfindley): remove the defensiveness here as well. + if _, ok := s.meta.metadata[id]; ok { + validIDs = append(validIDs, id) } - ids = validIDs } + ids = validIDs s.mu.Unlock() return ids, nil } -// Only use invalid metadata for Go versions >= 1.13. Go 1.12 and below has -// issues with overlays that will cause confusing error messages if we reuse -// old metadata. -func (s *snapshot) useInvalidMetadata() bool { - return s.view.goversion >= 13 && s.view.Options().ExperimentalUseInvalidMetadata -} - func (s *snapshot) GetReverseDependencies(ctx context.Context, id PackageID) ([]source.Package, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err @@ -829,7 +808,7 @@ func (s *snapshot) GetReverseDependencies(ctx context.Context, id PackageID) ([] s.mu.Lock() meta := s.meta s.mu.Unlock() - ids := meta.reverseTransitiveClosure(s.useInvalidMetadata(), id) + ids := meta.reverseTransitiveClosure(id) // Make sure to delete the original package ID from the map. delete(ids, id) @@ -1216,9 +1195,7 @@ func (s *snapshot) AllValidMetadata(ctx context.Context) ([]source.Metadata, err var meta []source.Metadata for _, m := range s.meta.metadata { - if m.Valid { - meta = append(meta, m) - } + meta = append(meta, m) } return meta, nil } @@ -1273,7 +1250,7 @@ func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { return match } -func (s *snapshot) getMetadata(id PackageID) *KnownMetadata { +func (s *snapshot) getMetadata(id PackageID) *Metadata { s.mu.Lock() defer s.mu.Unlock() @@ -1319,7 +1296,7 @@ func (s *snapshot) noValidMetadataForURILocked(uri span.URI) bool { return true } for _, id := range ids { - if m, ok := s.meta.metadata[id]; ok && m.Valid { + if _, ok := s.meta.metadata[id]; ok { return false } } @@ -1409,19 +1386,8 @@ func isFileOpen(fh source.VersionedFileHandle) bool { func (s *snapshot) awaitLoaded(ctx context.Context) error { loadErr := s.awaitLoadedAllErrors(ctx) - s.mu.Lock() - defer s.mu.Unlock() - - // If we still have absolutely no metadata, check if the view failed to - // initialize and return any errors. - if s.useInvalidMetadata() && len(s.meta.metadata) > 0 { - return nil - } - for _, m := range s.meta.metadata { - if m.Valid { - return nil - } - } + // TODO(rfindley): eliminate this function as part of simplifying + // CriticalErrors. if loadErr != nil { return loadErr.MainError } @@ -1968,27 +1934,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC result.shouldLoad[k] = v } - // TODO(rfindley): consolidate the this workspace mode detection with - // workspace invalidation. - workspaceModeChanged := s.workspaceMode() != result.workspaceMode() - - // We delete invalid metadata in the following cases: - // - If we are forcing a reload of metadata. - // - If the workspace mode has changed, as stale metadata may produce - // confusing or incorrect diagnostics. - // - // TODO(rfindley): we should probably also clear metadata if we are - // reinitializing the workspace, as otherwise we could leave around a bunch - // of irrelevant and duplicate metadata (for example, if the module path - // changed). However, this breaks the "experimentalUseInvalidMetadata" - // feature, which relies on stale metadata when, for example, a go.mod file - // is broken via invalid syntax. - deleteInvalidMetadata := forceReloadMetadata || workspaceModeChanged - // Compute which metadata updates are required. We only need to invalidate // packages directly containing the affected file, and only if it changed in // a relevant way. - metadataUpdates := make(map[PackageID]*KnownMetadata) + metadataUpdates := make(map[PackageID]*Metadata) for k, v := range s.meta.metadata { invalidateMetadata := idsToInvalidate[k] @@ -2012,20 +1961,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // Check whether the metadata should be deleted. - if skipID[k] || (invalidateMetadata && deleteInvalidMetadata) { + if skipID[k] || invalidateMetadata { metadataUpdates[k] = nil continue } - - // Check if the metadata has changed. - valid := v.Valid && !invalidateMetadata - if valid != v.Valid { - // Mark invalidated metadata rather than deleting it outright. - metadataUpdates[k] = &KnownMetadata{ - Metadata: v.Metadata, - Valid: valid, - } - } } // Update metadata, if necessary. @@ -2042,6 +1981,10 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Don't bother copying the importedBy graph, // as it changes each time we update metadata. + // TODO(rfindley): consolidate the this workspace mode detection with + // workspace invalidation. + workspaceModeChanged := s.workspaceMode() != result.workspaceMode() + // If the snapshot's workspace mode has changed, the packages loaded using // the previous mode are no longer relevant, so clear them out. if workspaceModeChanged { diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 762054d10b8..e283c3b35c7 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -88,14 +88,6 @@ var GeneratedAPIJSON = &APIJSON{ Status: "experimental", Hierarchy: "build", }, - { - Name: "experimentalUseInvalidMetadata", - Type: "bool", - Doc: "experimentalUseInvalidMetadata enables gopls to fall back on outdated\npackage metadata to provide editor features if the go command fails to\nload packages for some reason (like an invalid go.mod file).\n\nDeprecated: this setting is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55333).\n", - Default: "false", - Status: "experimental", - Hierarchy: "build", - }, { Name: "standaloneTags", Type: "[]string", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 23d795ef0e5..20c8f85ed6d 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -292,14 +292,6 @@ type BuildOptions struct { // be removed. AllowImplicitNetworkAccess bool `status:"experimental"` - // ExperimentalUseInvalidMetadata enables gopls to fall back on outdated - // package metadata to provide editor features if the go command fails to - // load packages for some reason (like an invalid go.mod file). - // - // Deprecated: this setting is deprecated and will be removed in a future - // version of gopls (https://go.dev/issue/55333). - ExperimentalUseInvalidMetadata bool `status:"experimental"` - // StandaloneTags specifies a set of build constraints that identify // individual Go source files that make up the entire main package of an // executable. @@ -1128,10 +1120,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.AllowImplicitNetworkAccess) case "experimentalUseInvalidMetadata": - const msg = "experimentalUseInvalidMetadata is deprecated, and will be removed " + - "in a future version of gopls (https://go.dev/issue/55333)" - result.softErrorf(msg) - result.setBool(&o.ExperimentalUseInvalidMetadata) + result.deprecated("") case "standaloneTags": result.setStringSlice(&o.StandaloneTags) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 18c022cb2a5..45bf60267df 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1901,159 +1901,6 @@ func main() {} }) } -func TestUseOfInvalidMetadata(t *testing.T) { - testenv.NeedsGo1Point(t, 13) - - const mod = ` --- go.mod -- -module mod.com - -go 1.12 --- main.go -- -package main - -import ( - "mod.com/a" - //"os" -) - -func _() { - a.Hello() - os.Getenv("") - //var x int -} --- a/a.go -- -package a - -func Hello() {} -` - WithOptions( - Settings{"experimentalUseInvalidMetadata": true}, - Modes(Default), - ).Run(t, mod, func(t *testing.T, env *Env) { - env.OpenFile("go.mod") - env.RegexpReplace("go.mod", "module mod.com", "modul mod.com") // break the go.mod file - env.SaveBufferWithoutActions("go.mod") - env.Await( - env.DiagnosticAtRegexp("go.mod", "modul"), - ) - // Confirm that language features work with invalid metadata. - env.OpenFile("main.go") - file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", "Hello")) - wantPos := env.RegexpSearch("a/a.go", "Hello") - if file != "a/a.go" && pos != wantPos { - t.Fatalf("expected a/a.go:%s, got %s:%s", wantPos, file, pos) - } - // Confirm that new diagnostics appear with invalid metadata by adding - // an unused variable to the body of the function. - env.RegexpReplace("main.go", "//var x int", "var x int") - env.Await( - env.DiagnosticAtRegexp("main.go", "x"), - ) - // Add an import and confirm that we get a diagnostic for it, since the - // metadata will not have been updated. - env.RegexpReplace("main.go", "//\"os\"", "\"os\"") - env.Await( - env.DiagnosticAtRegexp("main.go", `"os"`), - ) - // Fix the go.mod file and expect the diagnostic to resolve itself. - env.RegexpReplace("go.mod", "modul mod.com", "module mod.com") - env.SaveBuffer("go.mod") - env.Await( - env.DiagnosticAtRegexp("main.go", "x"), - env.NoDiagnosticAtRegexp("main.go", `"os"`), - EmptyDiagnostics("go.mod"), - ) - }) -} - -func TestReloadInvalidMetadata(t *testing.T) { - // We only use invalid metadata for Go versions > 1.12. - testenv.NeedsGo1Point(t, 13) - - const mod = ` --- go.mod -- -module mod.com - -go 1.12 --- main.go -- -package main - -func _() {} -` - WithOptions( - Settings{"experimentalUseInvalidMetadata": true}, - // ExperimentalWorkspaceModule has a different failure mode for this - // case. - Modes(Default), - ).Run(t, mod, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - CompletedWork("Load", 1, false), - ), - ) - - // Break the go.mod file on disk, expecting a reload. - env.WriteWorkspaceFile("go.mod", `modul mod.com - -go 1.12 -`) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - env.DiagnosticAtRegexp("go.mod", "modul"), - CompletedWork("Load", 1, false), - ), - ) - - env.OpenFile("main.go") - env.Await(env.DoneWithOpen()) - // The first edit after the go.mod file invalidation should cause a reload. - // Any subsequent simple edits should not. - content := `package main - -func main() { - _ = 1 -} -` - env.EditBuffer("main.go", fake.NewEdit(0, 0, 3, 0, content)) - env.Await( - OnceMet( - env.DoneWithChange(), - CompletedWork("Load", 2, false), - NoLogMatching(protocol.Error, "error loading file"), - ), - ) - env.RegexpReplace("main.go", "_ = 1", "_ = 2") - env.Await( - OnceMet( - env.DoneWithChange(), - CompletedWork("Load", 2, false), - NoLogMatching(protocol.Error, "error loading file"), - ), - ) - // Add an import to the main.go file and confirm that it does get - // reloaded, but the reload fails, so we see a diagnostic on the new - // "fmt" import. - env.EditBuffer("main.go", fake.NewEdit(0, 0, 5, 0, `package main - -import "fmt" - -func main() { - fmt.Println("") -} -`)) - env.Await( - OnceMet( - env.DoneWithChange(), - env.DiagnosticAtRegexp("main.go", `"fmt"`), - CompletedWork("Load", 3, false), - ), - ) - }) -} - func TestLangVersion(t *testing.T) { testenv.NeedsGo1Point(t, 18) // Requires types.Config.GoVersion, new in 1.18. const files = ` From 23056f613e44acb4341abef1b641e3c067991362 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 3 Nov 2022 19:06:31 -0400 Subject: [PATCH 439/723] internal/lsp/cache: clean up getOrLoadIDsForURI Address some TODOs in getOrLoadIDsForURI. In particular, after this change files will be marked as unloadable if getOrLoadIDsForURI returns fails to load valid IDs. Change-Id: I1f09d1c7edd776e46bf8178157bcf9e439b9f293 Reviewed-on: https://go-review.googlesource.com/c/tools/+/447742 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/cache/snapshot.go | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 656d08fb114..11288e5c75c 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -734,23 +734,14 @@ func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]Pack // Start with the set of package associations derived from the last load. ids := s.meta.ids[uri] - hasValidID := false // whether we have any valid package metadata containing uri shouldLoad := false // whether any packages containing uri are marked 'shouldLoad' for _, id := range ids { - // TODO(rfindley): remove the defensiveness here. s.meta.metadata[id] must - // exist. - if _, ok := s.meta.metadata[id]; ok { - hasValidID = true - } if len(s.shouldLoad[id]) > 0 { shouldLoad = true } } // Check if uri is known to be unloadable. - // - // TODO(rfindley): shouldn't we also mark uri as unloadable if the load below - // fails? Otherwise we endlessly load files with no packages. _, unloadable := s.unloadableFiles[uri] s.mu.Unlock() @@ -759,7 +750,7 @@ func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]Pack // - uri is not contained in any valid packages // - ...or one of the packages containing uri is marked 'shouldLoad' // - ...but uri is not unloadable - if (shouldLoad || !hasValidID) && !unloadable { + if (shouldLoad || len(ids) == 0) && !unloadable { scope := fileLoadScope(uri) err := s.load(ctx, false, scope) @@ -788,14 +779,11 @@ func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]Pack s.mu.Lock() ids = s.meta.ids[uri] - var validIDs []PackageID - for _, id := range ids { - // TODO(rfindley): remove the defensiveness here as well. - if _, ok := s.meta.metadata[id]; ok { - validIDs = append(validIDs, id) - } + // metadata is only ever added by loading, so if we get here and still have + // no ids, uri is unloadable. + if !unloadable && len(ids) == 0 { + s.unloadableFiles[uri] = struct{}{} } - ids = validIDs s.mu.Unlock() return ids, nil @@ -1745,6 +1733,8 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // TODO(rfindley): this looks wrong. Shouldn't we clear unloadableFiles on // changes to environment or workspace layout, or more generally on any // metadata change? + // + // Maybe not, as major configuration changes cause a new view. for k, v := range s.unloadableFiles { result.unloadableFiles[k] = v } From c7ed4b3c0e55eb0b33d8bd823fe463483d0967f7 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 16 Nov 2022 13:52:25 -0500 Subject: [PATCH 440/723] gopls/internal/lsp/cache: clean up tracking of GO111MODULE Gopls views stored at least 4 copies of GO111MODULE: - the value in view.environmentVariables - the value in view.goEnv - the value in view.userGo111Module - the value in view.effectiveGo111Module All of these values may differ from eachother, depending on the user's environment and go version, and their meaning is not clearly documented. Try to clean this up, by having environmentVariables track precisely the variables output by `go env`, and providing a method to implement the derived logic of userGo111Module and effectiveGo111Module. Ignore view.goEnv for now, but leave a TODO. Confusingly, the name 'effectiveGO111MODULE' turned out to be a more appropriate name for what was formerly 'userGo111Module', so the naming has switched. This change is intended to be a no-op cleanup. Change-Id: I529cc005c1875915483ef119a465bf17a96dec3c Reviewed-on: https://go-review.googlesource.com/c/tools/+/451355 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/load.go | 2 +- gopls/internal/lsp/cache/session.go | 2 +- gopls/internal/lsp/cache/snapshot.go | 2 +- gopls/internal/lsp/cache/view.go | 87 +++++++++++++++------------- 4 files changed, 51 insertions(+), 42 deletions(-) diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 04982643247..46e2e891ac9 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -307,7 +307,7 @@ func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalErr } // TODO(rfindley): both of the checks below should be delegated to the workspace. - if s.view.userGo111Module == off { + if s.view.effectiveGO111MODULE() == off { return nil } if s.workspace.moduleSource != legacyWorkspace { diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index fe88a46fc94..9b4355331a6 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -219,7 +219,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, goworkURI := span.URIFromPath(explicitGowork) // Build the gopls workspace, collecting active modules in the view. - workspace, err := newWorkspace(ctx, root, goworkURI, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), wsInfo.userGo111Module == off, options.ExperimentalWorkspaceModule) + workspace, err := newWorkspace(ctx, root, goworkURI, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), wsInfo.effectiveGO111MODULE() == off, options.ExperimentalWorkspaceModule) if err != nil { return nil, nil, func() {}, err } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 11288e5c75c..634afdff713 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -453,7 +453,7 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat // this with a non-empty inv.Env? // // We should refactor to make it clearer that the correct env is being used. - inv.Env = append(append(append(os.Environ(), s.view.options.EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.effectiveGo111Module) + inv.Env = append(append(append(os.Environ(), s.view.options.EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.GO111MODULE()) inv.BuildFlags = append([]string{}, s.view.options.BuildFlags...) s.view.optionsMu.Unlock() cleanup = func() {} // fallback diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 5974fab7268..f373c00508b 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -125,22 +125,48 @@ type workspaceInformation struct { hasGopackagesDriver bool // `go env` variables that need to be tracked by gopls. - environmentVariables - - // userGo111Module is the user's value of GO111MODULE. // - // TODO(rfindley): is there really value in memoizing this variable? It seems - // simpler to make this a function/method. - userGo111Module go111module - - // The value of GO111MODULE we want to run with. - effectiveGo111Module string + // TODO(rfindley): eliminate this in favor of goEnv, or vice-versa. + environmentVariables // goEnv is the `go env` output collected when a view is created. // It includes the values of the environment variables above. goEnv map[string]string } +// effectiveGO111MODULE reports the value of GO111MODULE effective in the go +// command at this go version, accounting for default values at different go +// versions. +func (w workspaceInformation) effectiveGO111MODULE() go111module { + // Off by default until Go 1.12. + go111module := w.GO111MODULE() + if go111module == "off" || (w.goversion < 12 && go111module == "") { + return off + } + // On by default as of Go 1.16. + if go111module == "on" || (w.goversion >= 16 && go111module == "") { + return on + } + return auto +} + +// GO111MODULE returns the value of GO111MODULE to use for running the go +// command. It differs from the user's environment in order to allow for the +// more forgiving default value "auto" when using recent go versions. +// +// TODO(rfindley): it is probably not worthwhile diverging from the go command +// here. The extra forgiveness may be nice, but breaks the invariant that +// running the go command from the command line produces the same build list. +// +// Put differently: we shouldn't go out of our way to make GOPATH work, when +// the go command does not. +func (w workspaceInformation) GO111MODULE() string { + if w.goversion >= 16 && w.go111module == "" { + return "auto" + } + return w.go111module +} + type go111module int const ( @@ -149,8 +175,15 @@ const ( on ) +// environmentVariables holds important environment variables captured by a +// call to `go env`. type environmentVariables struct { - gocache, gopath, goroot, goprivate, gomodcache, go111module string + gocache, gopath, goroot, goprivate, gomodcache string + + // Don't use go111module directly, because we choose to use a different + // default (auto) on Go 1.16 and later, to avoid spurious errors. Use + // the workspaceInformation.GO111MODULE method instead. + go111module string } // workspaceMode holds various flags defining how the gopls workspace should @@ -804,20 +837,11 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, return nil, err } - go111module := os.Getenv("GO111MODULE") - if v, ok := options.Env["GO111MODULE"]; ok { - go111module = v - } // Make sure to get the `go env` before continuing with initialization. - envVars, env, err := s.getGoEnv(ctx, folder.Filename(), goversion, go111module, options.EnvSlice()) + envVars, env, err := s.getGoEnv(ctx, folder.Filename(), goversion, options.EnvSlice()) if err != nil { return nil, err } - // If using 1.16, change the default back to auto. The primary effect of - // GO111MODULE=on is to break GOPATH, which we aren't too interested in. - if goversion >= 16 && go111module == "" { - go111module = "auto" - } // The value of GOPACKAGESDRIVER is not returned through the go command. gopackagesdriver := os.Getenv("GOPACKAGESDRIVER") // TODO(rfindley): this looks wrong, or at least overly defensive. If the @@ -838,26 +862,12 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, return &workspaceInformation{ hasGopackagesDriver: hasGopackagesDriver, - effectiveGo111Module: go111module, - userGo111Module: go111moduleForVersion(go111module, goversion), goversion: goversion, environmentVariables: envVars, goEnv: env, }, nil } -func go111moduleForVersion(go111module string, goversion int) go111module { - // Off by default until Go 1.12. - if go111module == "off" || (goversion < 12 && go111module == "") { - return off - } - // On by default as of Go 1.16. - if go111module == "on" || (goversion >= 16 && go111module == "") { - return on - } - return auto -} - // findWorkspaceRoot searches for the best workspace root according to the // following heuristics: // - First, look for a parent directory containing a gopls.mod file @@ -938,7 +948,7 @@ func defaultCheckPathCase(path string) error { } // getGoEnv gets the view's various GO* values. -func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, go111module string, configEnv []string) (environmentVariables, map[string]string, error) { +func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, configEnv []string) (environmentVariables, map[string]string, error) { envVars := environmentVariables{} vars := map[string]*string{ "GOCACHE": &envVars.gocache, @@ -984,13 +994,12 @@ func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, go } // Old versions of Go don't have GOMODCACHE, so emulate it. + // + // TODO(rfindley): consistent with the treatment of go111module, we should + // provide a wrapper method rather than mutating this value. if envVars.gomodcache == "" && envVars.gopath != "" { envVars.gomodcache = filepath.Join(filepath.SplitList(envVars.gopath)[0], "pkg/mod") } - // GO111MODULE does not appear in `go env` output until Go 1.13. - if goversion < 13 { - envVars.go111module = go111module - } return envVars, env, err } From 2592a854ecc2cbb00419ca9691925d963afe25fa Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 17 Nov 2022 12:02:44 -0500 Subject: [PATCH 441/723] gopls/internal/lsp: simplify KnownPackages This change simplifies the two functions called KnownPackages, and renames one of them. The first, source.KnownPackages, is a standalone function called in exactly one place: ListKnownPackages. It has been renamed KnownPackagePaths to distinguish from the other and to make clear that it returns only metadata. Its implementation could be greatly simplified in a follow-up, as noted. In the meantime, one obviously wrong use of ImportSpec.Path.Value (a quoted string literal!) as an package (not import!) path was fixed, and the package name was obtained directly, not via CompiledGoFiles. The second, Snapshot.KnownPackagePaths, is called from two places. This CL eliminates one of them: stubMethods used it apparently only to populate a field of missingInterface (pkg) that was unused. The other call (from 'implementations') is something that should eventually go away as we incrementalize; in any case it doesn't rely on the undocumented ordering invariant established by the implementation. This change removes the ordering invariant, documents the lack of order, and removes the ordering logic. Change-Id: Ia515a62c2d78276b3344f2880c22746b2c06ff64 Reviewed-on: https://go-review.googlesource.com/c/tools/+/451675 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/snapshot.go | 9 +---- gopls/internal/lsp/command.go | 2 +- .../lsp/source/completion/completion.go | 3 ++ gopls/internal/lsp/source/known_packages.go | 40 +++++++++---------- gopls/internal/lsp/source/stub.go | 36 ++--------------- gopls/internal/lsp/source/view.go | 7 +++- internal/imports/fix.go | 3 ++ 7 files changed, 35 insertions(+), 65 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 634afdff713..281c5d9b083 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1150,19 +1150,14 @@ func (s *snapshot) KnownPackages(ctx context.Context) ([]source.Package, error) return nil, err } - // The WorkspaceSymbols implementation relies on this function returning - // workspace packages first. - ids := s.workspacePackageIDs() + ids := make([]source.PackageID, 0, len(s.meta.metadata)) s.mu.Lock() for id := range s.meta.metadata { - if _, ok := s.workspacePackages[id]; ok { - continue - } ids = append(ids, id) } s.mu.Unlock() - var pkgs []source.Package + pkgs := make([]source.Package, 0, len(ids)) for _, id := range ids { pkg, err := s.checkedPackage(ctx, id, s.workspaceParseMode(id)) if err != nil { diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index a3d93e265cb..28cf3471a75 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -735,7 +735,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.KnownPackages(ctx, deps.snapshot, deps.fh) + pkgs, err := source.KnownPackagePaths(ctx, deps.snapshot, deps.fh) for _, pkg := range pkgs { result.Packages = append(result.Packages, string(pkg)) } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 1260e0e7c3b..144d6587da0 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -1475,6 +1475,9 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru count := 0 + // TODO(adonovan): strength-reduce to a metadata query. + // All that's needed below is Package.{Name,Path}. + // Presumably that can be answered more thoroughly more quickly. known, err := c.snapshot.CachedImportPaths(ctx) if err != nil { return err diff --git a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go index efaaee57aed..6f3bf8c9028 100644 --- a/gopls/internal/lsp/source/known_packages.go +++ b/gopls/internal/lsp/source/known_packages.go @@ -16,21 +16,24 @@ import ( "golang.org/x/tools/internal/imports" ) -// KnownPackages returns a list of all known packages -// in the package graph that could potentially be imported -// by the given file. -func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle) ([]PackagePath, error) { +// KnownPackagePaths returns a new list of package paths of all known +// packages in the package graph that could potentially be imported by +// the given file. The list is ordered lexicographically, except that +// all dot-free paths (standard packages) appear before dotful ones. +func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle) ([]PackagePath, error) { + // TODO(adonovan): this whole algorithm could be more + // simply expressed in terms of Metadata, not Packages. + // All we need below is: + // - for fh: Metadata.{DepsByPkgPath,Path,Name} + // - for all cached packages: Metadata.{Path,Name,ForTest,DepsByPkgPath}. pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return nil, fmt.Errorf("GetParsedFile: %w", err) } alreadyImported := map[PackagePath]struct{}{} - for _, imp := range pgf.File.Imports { - // TODO(adonovan): the correct PackagePath might need a "vendor/" prefix. - alreadyImported[PackagePath(imp.Path.Value)] = struct{}{} + for _, imp := range pkg.Imports() { + alreadyImported[imp.PkgPath()] = struct{}{} } - // TODO(adonovan): this whole algorithm could be more - // simply expressed in terms of Metadata, not Packages. pkgs, err := snapshot.CachedImportPaths(ctx) if err != nil { return nil, err @@ -40,13 +43,8 @@ func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandl paths []PackagePath ) for path, knownPkg := range pkgs { - gofiles := knownPkg.CompiledGoFiles() - if len(gofiles) == 0 || gofiles[0].File.Name == nil { - continue - } - pkgName := gofiles[0].File.Name.Name // package main cannot be imported - if pkgName == "main" { + if knownPkg.Name() == "main" { continue } // test packages cannot be imported @@ -57,7 +55,7 @@ func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandl if _, ok := alreadyImported[path]; ok { continue } - // snapshot.KnownPackages could have multiple versions of a pkg + // snapshot.CachedImportPaths could have multiple versions of a pkg if _, ok := seen[path]; ok { continue } @@ -86,7 +84,8 @@ func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandl return } paths = append(paths, path) - }, "", pgf.URI.Filename(), pkg.GetTypes().Name(), o.Env) + seen[path] = struct{}{} + }, "", pgf.URI.Filename(), string(pkg.Name()), o.Env) }) if err != nil { // if an error occurred, we still have a decent list we can @@ -97,11 +96,8 @@ func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandl importI, importJ := paths[i], paths[j] iHasDot := strings.Contains(string(importI), ".") jHasDot := strings.Contains(string(importJ), ".") - if iHasDot && !jHasDot { - return false - } - if jHasDot && !iHasDot { - return true + if iHasDot != jHasDot { + return jHasDot // dot-free paths (standard packages) compare less } return importI < importJ }) diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 3dcdb0ec6ba..9e4b704c224 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -98,13 +98,8 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi // stubMethods returns the Go code of all methods // that implement the given interface func stubMethods(ctx context.Context, concreteFile *ast.File, si *stubmethods.StubInfo, snapshot Snapshot) ([]byte, []*stubImport, error) { - ifacePkg, err := deducePkgFromTypes(ctx, snapshot, si.Interface) - if err != nil { - return nil, nil, err - } - si.Concrete.Obj().Type() concMS := types.NewMethodSet(types.NewPointer(si.Concrete.Obj().Type())) - missing, err := missingMethods(ctx, snapshot, concMS, si.Concrete.Obj().Pkg(), si.Interface, ifacePkg, map[string]struct{}{}) + missing, err := missingMethods(ctx, snapshot, concMS, si.Concrete.Obj().Pkg(), si.Interface, map[string]struct{}{}) if err != nil { return nil, nil, fmt.Errorf("missingMethods: %w", err) } @@ -188,19 +183,6 @@ func printStubMethod(md methodData) []byte { return b.Bytes() } -func deducePkgFromTypes(ctx context.Context, snapshot Snapshot, ifaceObj types.Object) (Package, error) { - pkgs, err := snapshot.KnownPackages(ctx) - if err != nil { - return nil, err - } - for _, p := range pkgs { - if p.PkgPath() == PackagePath(ifaceObj.Pkg().Path()) { - return p, nil - } - } - return nil, fmt.Errorf("pkg %q not found", ifaceObj.Pkg().Path()) -} - func deduceIfaceName(concretePkg, ifacePkg *types.Package, ifaceObj types.Object) string { if concretePkg.Path() == ifacePkg.Path() { return ifaceObj.Name() @@ -245,7 +227,7 @@ returns }, } */ -func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.MethodSet, concPkg *types.Package, ifaceObj types.Object, ifacePkg Package, visited map[string]struct{}) ([]*missingInterface, error) { +func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.MethodSet, concPkg *types.Package, ifaceObj types.Object, visited map[string]struct{}) ([]*missingInterface, error) { iface, ok := ifaceObj.Type().Underlying().(*types.Interface) if !ok { return nil, fmt.Errorf("expected %v to be an interface but got %T", iface, ifaceObj.Type().Underlying()) @@ -253,17 +235,7 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method missing := []*missingInterface{} for i := 0; i < iface.NumEmbeddeds(); i++ { eiface := iface.Embedded(i).Obj() - depPkg := ifacePkg - if path := PackagePath(eiface.Pkg().Path()); path != ifacePkg.PkgPath() { - // TODO(adonovan): I'm not sure what this is trying to do, but it - // looks wrong the in case of type aliases. - var err error - depPkg, err = ifacePkg.DirectDep(path) - if err != nil { - return nil, err - } - } - em, err := missingMethods(ctx, snapshot, concMS, concPkg, eiface, depPkg, visited) + em, err := missingMethods(ctx, snapshot, concMS, concPkg, eiface, visited) if err != nil { return nil, err } @@ -274,7 +246,6 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method return nil, fmt.Errorf("error getting iface file: %w", err) } mi := &missingInterface{ - pkg: ifacePkg, iface: iface, file: parsedFile.File, } @@ -322,7 +293,6 @@ func getStubFile(ctx context.Context, obj types.Object, snapshot Snapshot) (*Par type missingInterface struct { iface *types.Interface file *ast.File - pkg Package missing []*types.Func } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 47d642d95f3..4178197e488 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -182,8 +182,11 @@ type Snapshot interface { // and checked in TypecheckWorkspace mode. CachedImportPaths(ctx context.Context) (map[PackagePath]Package, error) - // KnownPackages returns all the packages loaded in this snapshot, checked - // in TypecheckWorkspace mode. + // KnownPackages returns a new unordered list of all packages + // loaded in this snapshot, checked in TypecheckWorkspace mode. + // + // TODO(adonovan): opt: rewrite 'implementations' to avoid the + // need ever to "load everything at once" using this function. KnownPackages(ctx context.Context) ([]Package, error) // ActivePackages returns the packages considered 'active' in the workspace. diff --git a/internal/imports/fix.go b/internal/imports/fix.go index 9b7b106fde1..646455802ba 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -697,6 +697,9 @@ func candidateImportName(pkg *pkg) string { // GetAllCandidates calls wrapped for each package whose name starts with // searchPrefix, and can be imported from filename with the package name filePkg. +// +// Beware that the wrapped function may be called multiple times concurrently. +// TODO(adonovan): encapsulate the concurrency. func GetAllCandidates(ctx context.Context, wrapped func(ImportFix), searchPrefix, filename, filePkg string, env *ProcessEnv) error { callback := &scanCallback{ rootFound: func(gopathwalk.Root) bool { From 85bf7a8fb4750610858ce72f1ebd39fb94013b27 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 18 Nov 2022 12:03:11 -0500 Subject: [PATCH 442/723] gopls/internal/lsp/source: eliminate Metadata interface This change merges the source.Metadata interface with its sole implementation, cache.Metadata. One possible concern: the struct cannot have private fields used only by the cache-package logic that constructs these structs. We are ok with that. Change-Id: I93c112f92dc812bd0da07d36e7244d5d77978312 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452035 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/check.go | 12 +-- gopls/internal/lsp/cache/graph.go | 6 +- gopls/internal/lsp/cache/load.go | 18 ++-- gopls/internal/lsp/cache/metadata.go | 88 ------------------- gopls/internal/lsp/cache/pkg.go | 10 ++- gopls/internal/lsp/cache/snapshot.go | 12 +-- gopls/internal/lsp/source/format.go | 2 +- gopls/internal/lsp/source/rename.go | 54 ++++++------ gopls/internal/lsp/source/view.go | 67 +++++++++++--- gopls/internal/lsp/source/workspace_symbol.go | 32 +++---- 10 files changed, 130 insertions(+), 171 deletions(-) delete mode 100644 gopls/internal/lsp/cache/metadata.go diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 1f3622a73d1..a22e285b6a1 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -46,7 +46,7 @@ type packageHandle struct { promise *memoize.Promise // [typeCheckResult] // m is the metadata associated with the package. - m *Metadata + m *source.Metadata // key is the hashed key for the package. // @@ -184,7 +184,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // readGoFiles reads the content of Metadata.GoFiles and // Metadata.CompiledGoFiles, in parallel. -func readGoFiles(ctx context.Context, s *snapshot, m *Metadata) (goFiles, compiledGoFiles []source.FileHandle, err error) { +func readGoFiles(ctx context.Context, s *snapshot, m *source.Metadata) (goFiles, compiledGoFiles []source.FileHandle, err error) { var group errgroup.Group getFileHandles := func(files []span.URI) []source.FileHandle { fhs := make([]source.FileHandle, len(files)) @@ -221,7 +221,7 @@ func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { // computePackageKey returns a key representing the act of type checking // a package named id containing the specified files, metadata, and // combined dependency hash. -func computePackageKey(id PackageID, files []source.FileHandle, m *Metadata, depsKey source.Hash, mode source.ParseMode, experimentalKey bool) packageHandleKey { +func computePackageKey(id PackageID, files []source.FileHandle, m *source.Metadata, depsKey source.Hash, mode source.ParseMode, experimentalKey bool) packageHandleKey { // TODO(adonovan): opt: no need to materalize the bytes; hash them directly. // Also, use field separators to avoid spurious collisions. b := bytes.NewBuffer(nil) @@ -304,7 +304,7 @@ func (ph *packageHandle) cached() (*pkg, error) { // typeCheckImpl type checks the parsed source files in compiledGoFiles. // (The resulting pkg also holds the parsed but not type-checked goFiles.) // deps holds the future results of type-checking the direct dependencies. -func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle) (*pkg, error) { +func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *source.Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle) (*pkg, error) { // Start type checking of direct dependencies, // in parallel and asynchronously. // As the type checker imports each of these @@ -439,7 +439,7 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) -func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { +func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *source.Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { ctx, done := event.Start(ctx, "cache.typeCheck", tag.Package.Of(string(m.ID))) defer done() @@ -628,7 +628,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost // Select packages that can't be found, and were imported in non-workspace packages. // Workspace packages already show their own errors. var relevantErrors []*packagesinternal.PackageError - for _, depsError := range pkg.m.depsErrors { + for _, depsError := range pkg.m.DepsErrors { // Up to Go 1.15, the missing package was included in the stack, which // was presumably a bug. We want the next one up. directImporterIdx := len(depsError.ImportStack) - 1 diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index be5a0618caa..709f1abfb2f 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -15,7 +15,7 @@ import ( // graph of Go packages, as obtained from go/packages. type metadataGraph struct { // metadata maps package IDs to their associated metadata. - metadata map[PackageID]*Metadata + metadata map[PackageID]*source.Metadata // importedBy maps package IDs to the list of packages that import them. importedBy map[PackageID][]PackageID @@ -27,12 +27,12 @@ type metadataGraph struct { // Clone creates a new metadataGraph, applying the given updates to the // receiver. -func (g *metadataGraph) Clone(updates map[PackageID]*Metadata) *metadataGraph { +func (g *metadataGraph) Clone(updates map[PackageID]*source.Metadata) *metadataGraph { if len(updates) == 0 { // Optimization: since the graph is immutable, we can return the receiver. return g } - result := &metadataGraph{metadata: make(map[PackageID]*Metadata, len(g.metadata))} + result := &metadataGraph{metadata: make(map[PackageID]*source.Metadata, len(g.metadata))} // Copy metadata. for id, m := range g.metadata { result.metadata[id] = m diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 46e2e891ac9..5f31958a15e 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -157,7 +157,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc moduleErrs := make(map[string][]packages.Error) // module path -> errors filterer := buildFilterer(s.view.rootURI.Filename(), s.view.gomodcache, s.view.Options()) - newMetadata := make(map[PackageID]*Metadata) + newMetadata := make(map[PackageID]*source.Metadata) for _, pkg := range pkgs { // The Go command returns synthetic list results for module queries that // encountered module errors. @@ -222,7 +222,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc // // TODO(rfindley): perform a sanity check that metadata matches here. If not, // we have an invalidation bug elsewhere. - updates := make(map[PackageID]*Metadata) + updates := make(map[PackageID]*source.Metadata) var updatedIDs []PackageID for _, m := range newMetadata { if existing := s.meta.metadata[m.ID]; existing == nil { @@ -475,7 +475,7 @@ func makeWorkspaceDir(ctx context.Context, workspace *workspace, fs source.FileS // buildMetadata populates the updates map with metadata updates to // apply, based on the given pkg. It recurs through pkg.Imports to ensure that // metadata exists for all dependencies. -func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*Metadata, path []PackageID) error { +func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*source.Metadata, path []PackageID) error { // Allow for multiple ad-hoc packages in the workspace (see #47584). pkgPath := PackagePath(pkg.PkgPath) id := PackageID(pkg.ID) @@ -507,7 +507,7 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con } // Recreate the metadata rather than reusing it to avoid locking. - m := &Metadata{ + m := &source.Metadata{ ID: id, PkgPath: pkgPath, Name: PackageName(pkg.Name), @@ -515,7 +515,7 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con TypesSizes: pkg.TypesSizes, Config: cfg, Module: pkg.Module, - depsErrors: packagesinternal.GetDepsErrors(pkg), + DepsErrors: packagesinternal.GetDepsErrors(pkg), } updates[id] = m @@ -606,7 +606,7 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con // snapshot s. // // s.mu must be held while calling this function. -func containsPackageLocked(s *snapshot, m *Metadata) bool { +func containsPackageLocked(s *snapshot, m *source.Metadata) bool { // In legacy workspace mode, or if a package does not have an associated // module, a package is considered inside the workspace if any of its files // are under the workspace root (and not excluded). @@ -647,7 +647,7 @@ func containsPackageLocked(s *snapshot, m *Metadata) bool { // the snapshot s. // // s.mu must be held while calling this function. -func containsOpenFileLocked(s *snapshot, m *Metadata) bool { +func containsOpenFileLocked(s *snapshot, m *source.Metadata) bool { uris := map[span.URI]struct{}{} for _, uri := range m.CompiledGoFiles { uris[uri] = struct{}{} @@ -668,7 +668,7 @@ func containsOpenFileLocked(s *snapshot, m *Metadata) bool { // workspace of the snapshot s. // // s.mu must be held while calling this function. -func containsFileInWorkspaceLocked(s *snapshot, m *Metadata) bool { +func containsFileInWorkspaceLocked(s *snapshot, m *source.Metadata) bool { uris := map[span.URI]struct{}{} for _, uri := range m.CompiledGoFiles { uris[uri] = struct{}{} @@ -738,7 +738,7 @@ func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[Packag // function returns false. // // If m is not a command-line-arguments package, this is trivially true. -func allFilesHaveRealPackages(g *metadataGraph, m *Metadata) bool { +func allFilesHaveRealPackages(g *metadataGraph, m *source.Metadata) bool { n := len(m.CompiledGoFiles) checkURIs: for _, uri := range append(m.CompiledGoFiles[0:n:n], m.GoFiles...) { diff --git a/gopls/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go deleted file mode 100644 index 135623fc53b..00000000000 --- a/gopls/internal/lsp/cache/metadata.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cache - -import ( - "go/types" - - "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/packagesinternal" -) - -type ( - PackageID = source.PackageID - PackagePath = source.PackagePath - PackageName = source.PackageName - ImportPath = source.ImportPath -) - -// Metadata holds package Metadata extracted from a call to packages.Load. -type Metadata struct { - ID PackageID - PkgPath PackagePath - Name PackageName - GoFiles []span.URI - CompiledGoFiles []span.URI - ForTest PackagePath // package path under test, or "" - TypesSizes types.Sizes - Errors []packages.Error - DepsByImpPath map[ImportPath]PackageID // may contain dups; empty ID => missing - DepsByPkgPath map[PackagePath]PackageID // values are unique and non-empty - Module *packages.Module - depsErrors []*packagesinternal.PackageError - - // Config is the *packages.Config associated with the loaded package. - Config *packages.Config -} - -// PackageID implements the source.Metadata interface. -func (m *Metadata) PackageID() PackageID { return m.ID } - -// Name implements the source.Metadata interface. -func (m *Metadata) PackageName() PackageName { return m.Name } - -// PkgPath implements the source.Metadata interface. -func (m *Metadata) PackagePath() PackagePath { return m.PkgPath } - -// IsIntermediateTestVariant reports whether the given package is an -// intermediate test variant, e.g. "net/http [net/url.test]". -// -// Such test variants arise when an x_test package (in this case net/url_test) -// imports a package (in this case net/http) that itself imports the the -// non-x_test package (in this case net/url). -// -// This is done so that the forward transitive closure of net/url_test has -// only one package for the "net/url" import. -// The intermediate test variant exists to hold the test variant import: -// -// net/url_test [net/url.test] -// -// | "net/http" -> net/http [net/url.test] -// | "net/url" -> net/url [net/url.test] -// | ... -// -// net/http [net/url.test] -// -// | "net/url" -> net/url [net/url.test] -// | ... -// -// This restriction propagates throughout the import graph of net/http: for -// every package imported by net/http that imports net/url, there must be an -// intermediate test variant that instead imports "net/url [net/url.test]". -// -// As one can see from the example of net/url and net/http, intermediate test -// variants can result in many additional packages that are essentially (but -// not quite) identical. For this reason, we filter these variants wherever -// possible. -func (m *Metadata) IsIntermediateTestVariant() bool { - return m.ForTest != "" && m.ForTest != m.PkgPath && m.ForTest+"_test" != m.PkgPath -} - -// ModuleInfo implements the source.Metadata interface. -func (m *Metadata) ModuleInfo() *packages.Module { - return m.Module -} diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 6e1233a37b4..5c43a82ad5c 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -18,9 +18,17 @@ import ( "golang.org/x/tools/internal/memoize" ) +// Convenient local aliases for typed strings. +type ( + PackageID = source.PackageID + PackagePath = source.PackagePath + PackageName = source.PackageName + ImportPath = source.ImportPath +) + // pkg contains the type information needed by the source package. type pkg struct { - m *Metadata + m *source.Metadata mode source.ParseMode fset *token.FileSet // for now, same as the snapshot's FileSet goFiles []*source.ParsedGoFile diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 281c5d9b083..ae5fe28825c 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1128,12 +1128,12 @@ func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { return result } -func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]source.Metadata, error) { +func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) { knownIDs, err := s.getOrLoadIDsForURI(ctx, uri) if err != nil { return nil, err } - var mds []source.Metadata + var mds []*source.Metadata for _, id := range knownIDs { md := s.getMetadata(id) // TODO(rfindley): knownIDs and metadata should be in sync, but existing @@ -1168,7 +1168,7 @@ func (s *snapshot) KnownPackages(ctx context.Context) ([]source.Package, error) return pkgs, nil } -func (s *snapshot) AllValidMetadata(ctx context.Context) ([]source.Metadata, error) { +func (s *snapshot) AllValidMetadata(ctx context.Context) ([]*source.Metadata, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } @@ -1176,7 +1176,7 @@ func (s *snapshot) AllValidMetadata(ctx context.Context) ([]source.Metadata, err s.mu.Lock() defer s.mu.Unlock() - var meta []source.Metadata + var meta []*source.Metadata for _, m := range s.meta.metadata { meta = append(meta, m) } @@ -1233,7 +1233,7 @@ func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { return match } -func (s *snapshot) getMetadata(id PackageID) *Metadata { +func (s *snapshot) getMetadata(id PackageID) *source.Metadata { s.mu.Lock() defer s.mu.Unlock() @@ -1922,7 +1922,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Compute which metadata updates are required. We only need to invalidate // packages directly containing the affected file, and only if it changed in // a relevant way. - metadataUpdates := make(map[PackageID]*Metadata) + metadataUpdates := make(map[PackageID]*source.Metadata) for k, v := range s.meta.metadata { invalidateMetadata := idsToInvalidate[k] diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index a83c9e471e2..2605cbb0a85 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -74,7 +74,7 @@ func Format(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.T var langVersion, modulePath string mds, err := snapshot.MetadataForFile(ctx, fh.URI()) if err == nil && len(mds) > 0 { - if mi := mds[0].ModuleInfo(); mi != nil { + if mi := mds[0].Module; mi != nil { langVersion = mi.GoVersion modulePath = mi.Path } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 9586d8418cf..51555b0ba1f 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -87,27 +87,27 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot meta := fileMeta[0] - if meta.PackageName() == "main" { + if meta.Name == "main" { err := errors.New("can't rename package \"main\"") return nil, err, err } - if strings.HasSuffix(string(meta.PackageName()), "_test") { + if strings.HasSuffix(string(meta.Name), "_test") { err := errors.New("can't rename x_test packages") return nil, err, err } - if meta.ModuleInfo() == nil { - err := fmt.Errorf("can't rename package: missing module information for package %q", meta.PackagePath()) + if meta.Module == nil { + err := fmt.Errorf("can't rename package: missing module information for package %q", meta.PkgPath) return nil, err, err } - if meta.ModuleInfo().Path == string(meta.PackagePath()) { - err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PackagePath(), meta.ModuleInfo().Path) + if meta.Module.Path == string(meta.PkgPath) { + err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path) return nil, err, err } // TODO(rfindley): we should not need the package here. - pkg, err := snapshot.WorkspacePackageByID(ctx, meta.PackageID()) + pkg, err := snapshot.WorkspacePackageByID(ctx, meta.ID) if err != nil { err = fmt.Errorf("error building package to rename: %v", err) return nil, err, err @@ -200,10 +200,10 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, // TODO(rfindley): we mix package path and import path here haphazardly. // Fix this. meta := fileMeta[0] - oldPath := meta.PackagePath() + oldPath := meta.PkgPath var modulePath PackagePath - if mi := meta.ModuleInfo(); mi == nil { - return nil, true, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PackagePath()) + if mi := meta.Module; mi == nil { + return nil, true, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PkgPath) } else { modulePath = PackagePath(mi.Path) } @@ -244,7 +244,7 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, // It updates package clauses and import paths for the renamed package as well // as any other packages affected by the directory renaming among packages // described by allMetadata. -func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath PackagePath, newName PackageName, allMetadata []Metadata) (map[span.URI][]protocol.TextEdit, error) { +func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath PackagePath, newName PackageName, allMetadata []*Metadata) (map[span.URI][]protocol.TextEdit, error) { if modulePath == oldPath { return nil, fmt.Errorf("cannot rename package: module path %q is the same as the package path, so renaming the package directory would have no effect", modulePath) } @@ -259,7 +259,7 @@ func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath PackageP // Special case: x_test packages for the renamed package will not have the // package path as as a dir prefix, but still need their package clauses // renamed. - if m.PackagePath() == oldPath+"_test" { + if m.PkgPath == oldPath+"_test" { newTestName := newName + "_test" if err := renamePackageClause(ctx, m, s, newTestName, seen, edits); err != nil { @@ -271,24 +271,24 @@ func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath PackageP // Subtle: check this condition before checking for valid module info // below, because we should not fail this operation if unrelated packages // lack module info. - if !strings.HasPrefix(string(m.PackagePath())+"/", string(oldPath)+"/") { + if !strings.HasPrefix(string(m.PkgPath)+"/", string(oldPath)+"/") { continue // not affected by the package renaming } - if m.ModuleInfo() == nil { - return nil, fmt.Errorf("cannot rename package: missing module information for package %q", m.PackagePath()) + if m.Module == nil { + return nil, fmt.Errorf("cannot rename package: missing module information for package %q", m.PkgPath) } - if modulePath != PackagePath(m.ModuleInfo().Path) { + if modulePath != PackagePath(m.Module.Path) { continue // don't edit imports if nested package and renaming package have different module paths } // Renaming a package consists of changing its import path and package name. - suffix := strings.TrimPrefix(string(m.PackagePath()), string(oldPath)) + suffix := strings.TrimPrefix(string(m.PkgPath), string(oldPath)) newPath := newPathPrefix + suffix - pkgName := m.PackageName() - if m.PackagePath() == PackagePath(oldPath) { + pkgName := m.Name + if m.PkgPath == PackagePath(oldPath) { pkgName = newName if err := renamePackageClause(ctx, m, s, newName, seen, edits); err != nil { @@ -336,15 +336,15 @@ func (s seenPackageRename) add(uri span.URI, path PackagePath) bool { // package clause has already been updated, to prevent duplicate edits. // // Edits are written into the edits map. -func renamePackageClause(ctx context.Context, m Metadata, s Snapshot, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { - pkg, err := s.WorkspacePackageByID(ctx, m.PackageID()) +func renamePackageClause(ctx context.Context, m *Metadata, s Snapshot, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { + pkg, err := s.WorkspacePackageByID(ctx, m.ID) if err != nil { return err } // Rename internal references to the package in the renaming package. for _, f := range pkg.CompiledGoFiles() { - if seen.add(f.URI, m.PackagePath()) { + if seen.add(f.URI, m.PkgPath) { continue } @@ -370,11 +370,11 @@ func renamePackageClause(ctx context.Context, m Metadata, s Snapshot, newName Pa // newPath and name newName. // // Edits are written into the edits map. -func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath ImportPath, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { +func renameImports(ctx context.Context, s Snapshot, m *Metadata, newPath ImportPath, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { // TODO(rfindley): we should get reverse dependencies as metadata first, // rather then building the package immediately. We don't need reverse // dependencies if they are intermediate test variants. - rdeps, err := s.GetReverseDependencies(ctx, m.PackageID()) + rdeps, err := s.GetReverseDependencies(ctx, m.ID) if err != nil { return err } @@ -394,13 +394,13 @@ func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath ImportPa } for _, f := range dep.CompiledGoFiles() { - if seen.add(f.URI, m.PackagePath()) { + if seen.add(f.URI, m.PkgPath) { continue } for _, imp := range f.File.Imports { // TODO(adonovan): what if RHS has "vendor/" prefix? - if UnquoteImportPath(imp) != ImportPath(m.PackagePath()) { + if UnquoteImportPath(imp) != ImportPath(m.PkgPath) { continue // not the import we're looking for } @@ -418,7 +418,7 @@ func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath ImportPa // If the package name of an import has not changed or if its import // path already has a local package name, then we don't need to update // the local package name. - if newName == m.PackageName() || imp.Name != nil { + if newName == m.Name || imp.Name != nil { continue } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 4178197e488..7466484952f 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -28,6 +28,7 @@ import ( "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/packagesinternal" ) // A GlobalSnapshotID uniquely identifies a snapshot within this process and @@ -196,7 +197,7 @@ type Snapshot interface { ActivePackages(ctx context.Context) ([]Package, error) // AllValidMetadata returns all valid metadata loaded for the snapshot. - AllValidMetadata(ctx context.Context) ([]Metadata, error) + AllValidMetadata(ctx context.Context) ([]*Metadata, error) // WorkspacePackageByID returns the workspace package with id, type checked // in 'workspace' mode. @@ -206,7 +207,7 @@ type Snapshot interface { Symbols(ctx context.Context) map[span.URI][]Symbol // Metadata returns package metadata associated with the given file URI. - MetadataForFile(ctx context.Context, uri span.URI) ([]Metadata, error) + MetadataForFile(ctx context.Context, uri span.URI) ([]*Metadata, error) // GetCriticalError returns any critical errors in the workspace. GetCriticalError(ctx context.Context) *CriticalError @@ -373,18 +374,56 @@ type TidiedModule struct { } // Metadata represents package metadata retrieved from go/packages. -type Metadata interface { - // PackageID is the unique package id. - PackageID() PackageID - - // PackageName is the package name. - PackageName() PackageName - - // PackagePath is the package path. - PackagePath() PackagePath - - // ModuleInfo returns the go/packages module information for the given package. - ModuleInfo() *packages.Module +type Metadata struct { + ID PackageID + PkgPath PackagePath + Name PackageName + GoFiles []span.URI + CompiledGoFiles []span.URI + ForTest PackagePath // package path under test, or "" + TypesSizes types.Sizes + Errors []packages.Error + DepsByImpPath map[ImportPath]PackageID // may contain dups; empty ID => missing + DepsByPkgPath map[PackagePath]PackageID // values are unique and non-empty + Module *packages.Module + DepsErrors []*packagesinternal.PackageError + + // Config is the *packages.Config associated with the loaded package. + Config *packages.Config +} + +// IsIntermediateTestVariant reports whether the given package is an +// intermediate test variant, e.g. "net/http [net/url.test]". +// +// Such test variants arise when an x_test package (in this case net/url_test) +// imports a package (in this case net/http) that itself imports the the +// non-x_test package (in this case net/url). +// +// This is done so that the forward transitive closure of net/url_test has +// only one package for the "net/url" import. +// The intermediate test variant exists to hold the test variant import: +// +// net/url_test [net/url.test] +// +// | "net/http" -> net/http [net/url.test] +// | "net/url" -> net/url [net/url.test] +// | ... +// +// net/http [net/url.test] +// +// | "net/url" -> net/url [net/url.test] +// | ... +// +// This restriction propagates throughout the import graph of net/http: for +// every package imported by net/http that imports net/url, there must be an +// intermediate test variant that instead imports "net/url [net/url.test]". +// +// As one can see from the example of net/url and net/http, intermediate test +// variants can result in many additional packages that are essentially (but +// not quite) identical. For this reason, we filter these variants wherever +// possible. +func (m *Metadata) IsIntermediateTestVariant() bool { + return m.ForTest != "" && m.ForTest != m.PkgPath && m.ForTest+"_test" != m.PkgPath } var ErrViewExists = errors.New("view already exists for session") diff --git a/gopls/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go index dfb5f39dc15..dc5abd211f0 100644 --- a/gopls/internal/lsp/source/workspace_symbol.go +++ b/gopls/internal/lsp/source/workspace_symbol.go @@ -88,17 +88,17 @@ type matcherFunc func(chunks []string) (int, float64) // // The space argument is an empty slice with spare capacity that may be used // to allocate the result. -type symbolizer func(space []string, name string, pkg Metadata, m matcherFunc) ([]string, float64) +type symbolizer func(space []string, name string, pkg *Metadata, m matcherFunc) ([]string, float64) -func fullyQualifiedSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { +func fullyQualifiedSymbolMatch(space []string, name string, pkg *Metadata, matcher matcherFunc) ([]string, float64) { if _, score := dynamicSymbolMatch(space, name, pkg, matcher); score > 0 { - return append(space, string(pkg.PackagePath()), ".", name), score + return append(space, string(pkg.PkgPath), ".", name), score } return nil, 0 } -func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { - if IsCommandLineArguments(pkg.PackageID()) { +func dynamicSymbolMatch(space []string, name string, pkg *Metadata, matcher matcherFunc) ([]string, float64) { + if IsCommandLineArguments(pkg.ID) { // command-line-arguments packages have a non-sensical package path, so // just use their package name. return packageSymbolMatch(space, name, pkg, matcher) @@ -106,14 +106,14 @@ func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher match var score float64 - endsInPkgName := strings.HasSuffix(string(pkg.PackagePath()), string(pkg.PackageName())) + endsInPkgName := strings.HasSuffix(string(pkg.PkgPath), string(pkg.Name)) // If the package path does not end in the package name, we need to check the // package-qualified symbol as an extra pass first. if !endsInPkgName { - pkgQualified := append(space, string(pkg.PackageName()), ".", name) + pkgQualified := append(space, string(pkg.Name), ".", name) idx, score := matcher(pkgQualified) - nameStart := len(pkg.PackageName()) + 1 + nameStart := len(pkg.Name) + 1 if score > 0 { // If our match is contained entirely within the unqualified portion, // just return that. @@ -126,11 +126,11 @@ func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher match } // Now try matching the fully qualified symbol. - fullyQualified := append(space, string(pkg.PackagePath()), ".", name) + fullyQualified := append(space, string(pkg.PkgPath), ".", name) idx, score := matcher(fullyQualified) // As above, check if we matched just the unqualified symbol name. - nameStart := len(pkg.PackagePath()) + 1 + nameStart := len(pkg.PkgPath) + 1 if idx >= nameStart { return append(space, name), score } @@ -139,9 +139,9 @@ func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher match // initial pass above, so check if we matched just the package-qualified // name. if endsInPkgName && idx >= 0 { - pkgStart := len(pkg.PackagePath()) - len(pkg.PackageName()) + pkgStart := len(pkg.PkgPath) - len(pkg.Name) if idx >= pkgStart { - return append(space, string(pkg.PackageName()), ".", name), score + return append(space, string(pkg.Name), ".", name), score } } @@ -150,8 +150,8 @@ func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher match return fullyQualified, score * 0.6 } -func packageSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { - qualified := append(space, string(pkg.PackageName()), ".", name) +func packageSymbolMatch(space []string, name string, pkg *Metadata, matcher matcherFunc) ([]string, float64) { + qualified := append(space, string(pkg.Name), ".", name) if _, s := matcher(qualified); s > 0 { return qualified, s } @@ -441,7 +441,7 @@ func convertFilterToRegexp(filter string) *regexp.Regexp { // symbolFile holds symbol information for a single file. type symbolFile struct { uri span.URI - md Metadata + md *Metadata syms []Symbol } @@ -526,7 +526,7 @@ func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, r kind: sym.Kind, uri: i.uri, rng: sym.Range, - container: string(i.md.PackagePath()), + container: string(i.md.PkgPath), } store.store(si) } From 36a5c6a8a6d3dc45fbccd63e2fb38ef6911f33ce Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 25 Aug 2022 11:09:24 -0400 Subject: [PATCH 443/723] go/ssa: build generic function bodies ssa now always builds the bodies of generic functions and methods. Within the bodies of generic functions, parameterized types may appear in Instructions. When ssa.InstantiateGenerics is on, calls to generic functions with ground (non-parameterized) types are built as instantiations of the generic function bodies. Otherwise, calls to generic functions are built as a wrapper function that delegates to the generic function body. The ChangeType instruction can now represent a coercion to or from a parameterized type to an instance of the type. *ssa.Const values may now represent the zero value of any type. The build mode of go/analysis/passes/buildssa is again ssa.BuilderMode(0) (i.e. ssa.InstantiateGenerics is off). This change is a stack of already reviewed CLs. Fixes golang/go#48525 Change-Id: Ib516eba43963674c804a63e3dcdae6d4116353c9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/425496 TryBot-Result: Gopher Robot Reviewed-by: Zvonimir Pavlinovic Run-TryBot: Tim King gopls-CI: kokoro Reviewed-by: Alan Donovan --- go/analysis/passes/buildssa/buildssa.go | 3 +- go/analysis/passes/nilness/nilness.go | 5 +- .../passes/nilness/testdata/src/a/a.go | 7 + .../passes/nilness/testdata/src/c/c.go | 2 +- go/callgraph/callgraph.go | 2 + go/callgraph/cha/cha.go | 2 + go/callgraph/cha/testdata/generics.go | 4 + go/callgraph/static/static.go | 2 + go/callgraph/vta/utils.go | 29 +- go/callgraph/vta/vta.go | 2 + go/pointer/analysis.go | 20 +- go/pointer/api.go | 4 + go/pointer/doc.go | 8 + go/pointer/gen.go | 20 +- go/pointer/pointer_test.go | 16 +- go/pointer/reflect.go | 5 +- go/pointer/util.go | 5 +- go/ssa/TODO | 16 + go/ssa/builder.go | 236 ++++--- go/ssa/builder_generic_test.go | 664 ++++++++++++++++++ go/ssa/builder_go120_test.go | 4 + go/ssa/builder_test.go | 12 + go/ssa/const.go | 143 +++- go/ssa/const_test.go | 104 +++ go/ssa/coretype.go | 256 +++++++ go/ssa/coretype_test.go | 105 +++ go/ssa/create.go | 36 +- go/ssa/emit.go | 176 +++-- go/ssa/func.go | 23 +- go/ssa/instantiate.go | 81 ++- go/ssa/instantiate_test.go | 283 +++++++- go/ssa/interp/interp.go | 25 +- go/ssa/interp/interp_test.go | 4 +- go/ssa/interp/ops.go | 9 +- go/ssa/interp/testdata/boundmeth.go | 3 +- go/ssa/interp/testdata/slice2array.go | 40 +- go/ssa/interp/testdata/typeassert.go | 32 + go/ssa/interp/testdata/zeros.go | 45 ++ go/ssa/lift.go | 7 +- go/ssa/lvalue.go | 10 +- go/ssa/methods.go | 12 +- go/ssa/parameterized.go | 9 + go/ssa/print.go | 4 +- go/ssa/sanity.go | 22 +- go/ssa/ssa.go | 138 ++-- go/ssa/stdlib_test.go | 18 - go/ssa/subst.go | 18 +- go/ssa/subst_test.go | 6 +- go/ssa/testdata/valueforexpr.go | 1 + go/ssa/util.go | 69 +- go/ssa/wrappers.go | 76 +- 51 files changed, 2405 insertions(+), 418 deletions(-) create mode 100644 go/ssa/TODO create mode 100644 go/ssa/builder_generic_test.go create mode 100644 go/ssa/const_test.go create mode 100644 go/ssa/coretype.go create mode 100644 go/ssa/coretype_test.go create mode 100644 go/ssa/interp/testdata/typeassert.go create mode 100644 go/ssa/interp/testdata/zeros.go diff --git a/go/analysis/passes/buildssa/buildssa.go b/go/analysis/passes/buildssa/buildssa.go index 4ec0e73ff2c..02b7b18b3f5 100644 --- a/go/analysis/passes/buildssa/buildssa.go +++ b/go/analysis/passes/buildssa/buildssa.go @@ -48,8 +48,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // Some Analyzers may need GlobalDebug, in which case we'll have // to set it globally, but let's wait till we need it. - // Monomorphize at least until type parameters are available. - mode := ssa.InstantiateGenerics + mode := ssa.BuilderMode(0) prog := ssa.NewProgram(pass.Fset, mode) diff --git a/go/analysis/passes/nilness/nilness.go b/go/analysis/passes/nilness/nilness.go index 8db18c73ade..61fa30a5237 100644 --- a/go/analysis/passes/nilness/nilness.go +++ b/go/analysis/passes/nilness/nilness.go @@ -62,7 +62,6 @@ var Analyzer = &analysis.Analyzer{ func run(pass *analysis.Pass) (interface{}, error) { ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) - // TODO(48525): ssainput.SrcFuncs is missing fn._Instances(). runFunc will be skipped. for _, fn := range ssainput.SrcFuncs { runFunc(pass, fn) } @@ -307,9 +306,9 @@ func nilnessOf(stack []fact, v ssa.Value) nilness { return isnonnil case *ssa.Const: if v.IsNil() { - return isnil + return isnil // nil or zero value of a pointer-like type } else { - return isnonnil + return unknown // non-pointer } } diff --git a/go/analysis/passes/nilness/testdata/src/a/a.go b/go/analysis/passes/nilness/testdata/src/a/a.go index aa7f9a8f859..0629e08d89e 100644 --- a/go/analysis/passes/nilness/testdata/src/a/a.go +++ b/go/analysis/passes/nilness/testdata/src/a/a.go @@ -209,3 +209,10 @@ func f13() { var d *Y print(d.value) // want "nil dereference in field selection" } + +func f14() { + var x struct{ f string } + if x == struct{ f string }{} { // we don't catch this tautology as we restrict to reference types + print(x) + } +} diff --git a/go/analysis/passes/nilness/testdata/src/c/c.go b/go/analysis/passes/nilness/testdata/src/c/c.go index 2b2036595a6..c9a05a714ff 100644 --- a/go/analysis/passes/nilness/testdata/src/c/c.go +++ b/go/analysis/passes/nilness/testdata/src/c/c.go @@ -2,7 +2,7 @@ package c func instantiated[X any](x *X) int { if x == nil { - print(*x) // not reported until _Instances are added to SrcFuncs + print(*x) // want "nil dereference in load" } return 1 } diff --git a/go/callgraph/callgraph.go b/go/callgraph/callgraph.go index 352ce0c76ed..905623753d6 100644 --- a/go/callgraph/callgraph.go +++ b/go/callgraph/callgraph.go @@ -37,6 +37,8 @@ package callgraph // import "golang.org/x/tools/go/callgraph" // More generally, we could eliminate "uninteresting" nodes such as // nodes from packages we don't care about. +// TODO(zpavlinovic): decide how callgraphs handle calls to and from generic function bodies. + import ( "fmt" "go/token" diff --git a/go/callgraph/cha/cha.go b/go/callgraph/cha/cha.go index 170040426b8..7075a73cbe8 100644 --- a/go/callgraph/cha/cha.go +++ b/go/callgraph/cha/cha.go @@ -22,6 +22,8 @@ // partial programs, such as libraries without a main or test function. package cha // import "golang.org/x/tools/go/callgraph/cha" +// TODO(zpavlinovic): update CHA for how it handles generic function bodies. + import ( "go/types" diff --git a/go/callgraph/cha/testdata/generics.go b/go/callgraph/cha/testdata/generics.go index 79250a56ca1..0323c7582b6 100644 --- a/go/callgraph/cha/testdata/generics.go +++ b/go/callgraph/cha/testdata/generics.go @@ -41,5 +41,9 @@ func f(h func(), g func(I), k func(A), a A, b B) { // f --> instantiated[main.A] // f --> instantiated[main.A] // f --> instantiated[main.B] +// instantiated --> (*A).Foo +// instantiated --> (*B).Foo +// instantiated --> (A).Foo +// instantiated --> (B).Foo // instantiated[main.A] --> (A).Foo // instantiated[main.B] --> (B).Foo diff --git a/go/callgraph/static/static.go b/go/callgraph/static/static.go index c7fae75bbde..62d2364bf2c 100644 --- a/go/callgraph/static/static.go +++ b/go/callgraph/static/static.go @@ -6,6 +6,8 @@ // only static call edges. package static // import "golang.org/x/tools/go/callgraph/static" +// TODO(zpavlinovic): update static for how it handles generic function bodies. + import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" diff --git a/go/callgraph/vta/utils.go b/go/callgraph/vta/utils.go index c0b5775907f..d1831983ad6 100644 --- a/go/callgraph/vta/utils.go +++ b/go/callgraph/vta/utils.go @@ -9,6 +9,7 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/typeparams" ) func canAlias(n1, n2 node) bool { @@ -117,19 +118,27 @@ func functionUnderPtr(t types.Type) types.Type { } // sliceArrayElem returns the element type of type `t` that is -// expected to be a (pointer to) array or slice, consistent with +// expected to be a (pointer to) array, slice or string, consistent with // the ssa.Index and ssa.IndexAddr instructions. Panics otherwise. func sliceArrayElem(t types.Type) types.Type { - u := t.Underlying() - - if p, ok := u.(*types.Pointer); ok { - u = p.Elem().Underlying() - } - - if a, ok := u.(*types.Array); ok { - return a.Elem() + switch u := t.Underlying().(type) { + case *types.Pointer: + return u.Elem().Underlying().(*types.Array).Elem() + case *types.Array: + return u.Elem() + case *types.Slice: + return u.Elem() + case *types.Basic: + return types.Typ[types.Byte] + case *types.Interface: // type param. + terms, err := typeparams.InterfaceTermSet(u) + if err != nil || len(terms) == 0 { + panic(t) + } + return sliceArrayElem(terms[0].Type()) // Element types must match. + default: + panic(t) } - return u.(*types.Slice).Elem() } // siteCallees computes a set of callees for call site `c` given program `callgraph`. diff --git a/go/callgraph/vta/vta.go b/go/callgraph/vta/vta.go index 9839bd3f3cd..58393600337 100644 --- a/go/callgraph/vta/vta.go +++ b/go/callgraph/vta/vta.go @@ -54,6 +54,8 @@ // reaching the node representing the call site to create a set of callees. package vta +// TODO(zpavlinovic): update VTA for how it handles generic function bodies and instantiation wrappers. + import ( "go/types" diff --git a/go/pointer/analysis.go b/go/pointer/analysis.go index 35ad8abdb12..e3c85ede4f7 100644 --- a/go/pointer/analysis.go +++ b/go/pointer/analysis.go @@ -16,6 +16,7 @@ import ( "runtime" "runtime/debug" "sort" + "strings" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" @@ -377,12 +378,27 @@ func (a *analysis) callEdge(caller *cgnode, site *callsite, calleeid nodeid) { fmt.Fprintf(a.log, "\tcall edge %s -> %s\n", site, callee) } - // Warn about calls to non-intrinsic external functions. + // Warn about calls to functions that are handled unsoundly. // TODO(adonovan): de-dup these messages. - if fn := callee.fn; fn.Blocks == nil && a.findIntrinsic(fn) == nil { + fn := callee.fn + + // Warn about calls to non-intrinsic external functions. + if fn.Blocks == nil && a.findIntrinsic(fn) == nil { a.warnf(site.pos(), "unsound call to unknown intrinsic: %s", fn) a.warnf(fn.Pos(), " (declared here)") } + + // Warn about calls to generic function bodies. + if fn.TypeParams().Len() > 0 && len(fn.TypeArgs()) == 0 { + a.warnf(site.pos(), "unsound call to generic function body: %s (build with ssa.InstantiateGenerics)", fn) + a.warnf(fn.Pos(), " (declared here)") + } + + // Warn about calls to instantiation wrappers of generics functions. + if fn.Origin() != nil && strings.HasPrefix(fn.Synthetic, "instantiation wrapper ") { + a.warnf(site.pos(), "unsound call to instantiation wrapper of generic: %s (build with ssa.InstantiateGenerics)", fn) + a.warnf(fn.Pos(), " (declared here)") + } } // dumpSolution writes the PTS solution to the specified file. diff --git a/go/pointer/api.go b/go/pointer/api.go index 9a4cc0af4a2..64de1100351 100644 --- a/go/pointer/api.go +++ b/go/pointer/api.go @@ -28,7 +28,11 @@ type Config struct { // dependencies of any main package may still affect the // analysis result, because they contribute runtime types and // thus methods. + // // TODO(adonovan): investigate whether this is desirable. + // + // Calls to generic functions will be unsound unless packages + // are built using the ssa.InstantiateGenerics builder mode. Mains []*ssa.Package // Reflection determines whether to handle reflection diff --git a/go/pointer/doc.go b/go/pointer/doc.go index d41346e699f..aca343b88e3 100644 --- a/go/pointer/doc.go +++ b/go/pointer/doc.go @@ -358,6 +358,14 @@ A. Control-flow joins would merge interfaces ({T1}, {V1}) and ({T2}, type-unsafe combination (T1,V2). Treating the value and its concrete type as inseparable makes the analysis type-safe.) +Type parameters: + +Type parameters are not directly supported by the analysis. +Calls to generic functions will be left as if they had empty bodies. +Users of the package are expected to use the ssa.InstantiateGenerics +builder mode when building code that uses or depends on code +containing generics. + reflect.Value: A reflect.Value is modelled very similar to an interface{}, i.e. as diff --git a/go/pointer/gen.go b/go/pointer/gen.go index 09705948d9c..bee656b6237 100644 --- a/go/pointer/gen.go +++ b/go/pointer/gen.go @@ -14,9 +14,11 @@ import ( "fmt" "go/token" "go/types" + "strings" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/typeparams" ) var ( @@ -978,7 +980,10 @@ func (a *analysis) genInstr(cgn *cgnode, instr ssa.Instruction) { a.sizeof(instr.Type())) case *ssa.Index: - a.copy(a.valueNode(instr), 1+a.valueNode(instr.X), a.sizeof(instr.Type())) + _, isstring := typeparams.CoreType(instr.X.Type()).(*types.Basic) + if !isstring { + a.copy(a.valueNode(instr), 1+a.valueNode(instr.X), a.sizeof(instr.Type())) + } case *ssa.Select: recv := a.valueOffsetNode(instr, 2) // instr : (index, recvOk, recv0, ... recv_n-1) @@ -1202,6 +1207,19 @@ func (a *analysis) genFunc(cgn *cgnode) { return } + if fn.TypeParams().Len() > 0 && len(fn.TypeArgs()) == 0 { + // Body of generic function. + // We'll warn about calls to such functions at the end. + return + } + + if strings.HasPrefix(fn.Synthetic, "instantiation wrapper ") { + // instantiation wrapper of a generic function. + // These may contain type coercions which are not currently supported. + // We'll warn about calls to such functions at the end. + return + } + if a.log != nil { fmt.Fprintln(a.log, "; Creating nodes for local values") } diff --git a/go/pointer/pointer_test.go b/go/pointer/pointer_test.go index 05fe981f86e..1fa54f6e8f7 100644 --- a/go/pointer/pointer_test.go +++ b/go/pointer/pointer_test.go @@ -240,9 +240,14 @@ func doOneInput(t *testing.T, input, fpath string) bool { // Find all calls to the built-in print(x). Analytically, // print is a no-op, but it's a convenient hook for testing // the PTS of an expression, so our tests use it. + // Exclude generic bodies as these should be dead code for pointer. + // Instance of generics are included. probes := make(map[*ssa.CallCommon]bool) for fn := range ssautil.AllFunctions(prog) { - // TODO(taking): Switch to a more principled check like fn.declaredPackage() == mainPkg if _Origin is exported. + if isGenericBody(fn) { + continue // skip generic bodies + } + // TODO(taking): Switch to a more principled check like fn.declaredPackage() == mainPkg if Origin is exported. if fn.Pkg == mainpkg || (fn.Pkg == nil && mainFiles[prog.Fset.File(fn.Pos())]) { for _, b := range fn.Blocks { for _, instr := range b.Instrs { @@ -656,6 +661,15 @@ func TestInput(t *testing.T) { } } +// isGenericBody returns true if fn is the body of a generic function. +func isGenericBody(fn *ssa.Function) bool { + sig := fn.Signature + if typeparams.ForSignature(sig).Len() > 0 || typeparams.RecvTypeParams(sig).Len() > 0 { + return fn.Synthetic == "" + } + return false +} + // join joins the elements of multiset with " | "s. func join(set map[string]int) string { var buf bytes.Buffer diff --git a/go/pointer/reflect.go b/go/pointer/reflect.go index efb11b00096..3762dd8d401 100644 --- a/go/pointer/reflect.go +++ b/go/pointer/reflect.go @@ -1024,7 +1024,7 @@ func ext۰reflect۰ChanOf(a *analysis, cgn *cgnode) { var dir reflect.ChanDir // unknown if site := cgn.callersite; site != nil { if c, ok := site.instr.Common().Args[0].(*ssa.Const); ok { - v, _ := constant.Int64Val(c.Value) + v := c.Int64() if 0 <= v && v <= int64(reflect.BothDir) { dir = reflect.ChanDir(v) } @@ -1751,8 +1751,7 @@ func ext۰reflect۰rtype۰InOut(a *analysis, cgn *cgnode, out bool) { index := -1 if site := cgn.callersite; site != nil { if c, ok := site.instr.Common().Args[0].(*ssa.Const); ok { - v, _ := constant.Int64Val(c.Value) - index = int(v) + index = int(c.Int64()) } } a.addConstraint(&rtypeInOutConstraint{ diff --git a/go/pointer/util.go b/go/pointer/util.go index 5fec1fc4ed5..17728aa06ac 100644 --- a/go/pointer/util.go +++ b/go/pointer/util.go @@ -8,12 +8,13 @@ import ( "bytes" "fmt" "go/types" - exec "golang.org/x/sys/execabs" "log" "os" "runtime" "time" + exec "golang.org/x/sys/execabs" + "golang.org/x/tools/container/intsets" ) @@ -125,7 +126,7 @@ func (a *analysis) flatten(t types.Type) []*fieldInfo { // Debuggability hack: don't remove // the named type from interfaces as // they're very verbose. - fl = append(fl, &fieldInfo{typ: t}) + fl = append(fl, &fieldInfo{typ: t}) // t may be a type param } else { fl = a.flatten(u) } diff --git a/go/ssa/TODO b/go/ssa/TODO new file mode 100644 index 00000000000..6c35253c73c --- /dev/null +++ b/go/ssa/TODO @@ -0,0 +1,16 @@ +-*- text -*- + +SSA Generics to-do list +=========================== + +DOCUMENTATION: +- Read me for internals + +TYPE PARAMETERIZED GENERIC FUNCTIONS: +- sanity.go updates. +- Check source functions going to generics. +- Tests, tests, tests... + +USAGE: +- Back fill users for handling ssa.InstantiateGenerics being off. + diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 8ec8f6e310b..04deb7b0633 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -101,6 +101,9 @@ package ssa // // This is a low level operation for creating functions that do not exist in // the source. Use with caution. +// +// TODO(taking): Use consistent terminology for "concrete". +// TODO(taking): Use consistent terminology for "monomorphization"/"instantiate"/"expand". import ( "fmt" @@ -272,7 +275,7 @@ func (b *builder) exprN(fn *Function, e ast.Expr) Value { return fn.emit(&c) case *ast.IndexExpr: - mapt := fn.typeOf(e.X).Underlying().(*types.Map) + mapt := coreType(fn.typeOf(e.X)).(*types.Map) // ,ok must be a map. lookup := &Lookup{ X: b.expr(fn, e.X), Index: emitConv(fn, b.expr(fn, e.Index), mapt.Key()), @@ -309,7 +312,7 @@ func (b *builder) builtin(fn *Function, obj *types.Builtin, args []ast.Expr, typ typ = fn.typ(typ) switch obj.Name() { case "make": - switch typ.Underlying().(type) { + switch ct := coreType(typ).(type) { case *types.Slice: n := b.expr(fn, args[1]) m := n @@ -319,7 +322,7 @@ func (b *builder) builtin(fn *Function, obj *types.Builtin, args []ast.Expr, typ if m, ok := m.(*Const); ok { // treat make([]T, n, m) as new([m]T)[:n] cap := m.Int64() - at := types.NewArray(typ.Underlying().(*types.Slice).Elem(), cap) + at := types.NewArray(ct.Elem(), cap) alloc := emitNew(fn, at, pos) alloc.Comment = "makeslice" v := &Slice{ @@ -370,6 +373,8 @@ func (b *builder) builtin(fn *Function, obj *types.Builtin, args []ast.Expr, typ // We must still evaluate the value, though. (If it // was side-effect free, the whole call would have // been constant-folded.) + // + // Type parameters are always non-constant so use Underlying. t := deref(fn.typeOf(args[0])).Underlying() if at, ok := t.(*types.Array); ok { b.expr(fn, args[0]) // for effects only @@ -465,27 +470,27 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { return &lazyAddress{addr: emit, t: fld.Type(), pos: e.Sel.Pos(), expr: e.Sel} case *ast.IndexExpr: + xt := fn.typeOf(e.X) + elem, mode := indexType(xt) var x Value var et types.Type - switch t := fn.typeOf(e.X).Underlying().(type) { - case *types.Array: + switch mode { + case ixArrVar: // array, array|slice, array|*array, or array|*array|slice. x = b.addr(fn, e.X, escaping).address(fn) - et = types.NewPointer(t.Elem()) - case *types.Pointer: // *array - x = b.expr(fn, e.X) - et = types.NewPointer(t.Elem().Underlying().(*types.Array).Elem()) - case *types.Slice: + et = types.NewPointer(elem) + case ixVar: // *array, slice, *array|slice x = b.expr(fn, e.X) - et = types.NewPointer(t.Elem()) - case *types.Map: + et = types.NewPointer(elem) + case ixMap: + mt := coreType(xt).(*types.Map) return &element{ m: b.expr(fn, e.X), - k: emitConv(fn, b.expr(fn, e.Index), t.Key()), - t: t.Elem(), + k: emitConv(fn, b.expr(fn, e.Index), mt.Key()), + t: mt.Elem(), pos: e.Lbrack, } default: - panic("unexpected container type in IndexExpr: " + t.String()) + panic("unexpected container type in IndexExpr: " + xt.String()) } index := b.expr(fn, e.Index) if isUntyped(index.Type()) { @@ -562,7 +567,7 @@ func (b *builder) assign(fn *Function, loc lvalue, e ast.Expr, isZero bool, sb * } if _, ok := loc.(*address); ok { - if isInterface(loc.typ()) { + if isNonTypeParamInterface(loc.typ()) { // e.g. var x interface{} = T{...} // Can't in-place initialize an interface value. // Fall back to copying. @@ -632,18 +637,19 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { case *ast.FuncLit: fn2 := &Function{ - name: fmt.Sprintf("%s$%d", fn.Name(), 1+len(fn.AnonFuncs)), - Signature: fn.typeOf(e.Type).Underlying().(*types.Signature), - pos: e.Type.Func, - parent: fn, - Pkg: fn.Pkg, - Prog: fn.Prog, - syntax: e, - _Origin: nil, // anon funcs do not have an origin. - _TypeParams: fn._TypeParams, // share the parent's type parameters. - _TypeArgs: fn._TypeArgs, // share the parent's type arguments. - info: fn.info, - subst: fn.subst, // share the parent's type substitutions. + name: fmt.Sprintf("%s$%d", fn.Name(), 1+len(fn.AnonFuncs)), + Signature: fn.typeOf(e.Type).(*types.Signature), + pos: e.Type.Func, + parent: fn, + anonIdx: int32(len(fn.AnonFuncs)), + Pkg: fn.Pkg, + Prog: fn.Prog, + syntax: e, + topLevelOrigin: nil, // use anonIdx to lookup an anon instance's origin. + typeparams: fn.typeparams, // share the parent's type parameters. + typeargs: fn.typeargs, // share the parent's type arguments. + info: fn.info, + subst: fn.subst, // share the parent's type substitutions. } fn.AnonFuncs = append(fn.AnonFuncs, fn2) b.created.Add(fn2) @@ -745,14 +751,20 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { case *ast.SliceExpr: var low, high, max Value var x Value - switch fn.typeOf(e.X).Underlying().(type) { + xtyp := fn.typeOf(e.X) + switch coreType(xtyp).(type) { case *types.Array: // Potentially escaping. x = b.addr(fn, e.X, true).address(fn) case *types.Basic, *types.Slice, *types.Pointer: // *array x = b.expr(fn, e.X) default: - panic("unreachable") + // coreType exception? + if isBytestring(xtyp) { + x = b.expr(fn, e.X) // bytestring is handled as string and []byte. + } else { + panic("unexpected sequence type in SliceExpr") + } } if e.Low != nil { low = b.expr(fn, e.Low) @@ -780,7 +792,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { case *types.Builtin: return &Builtin{name: obj.Name(), sig: fn.instanceType(e).(*types.Signature)} case *types.Nil: - return nilConst(fn.instanceType(e)) + return zeroConst(fn.instanceType(e)) } // Package-level func or var? if v := fn.Prog.packageLevelMember(obj); v != nil { @@ -788,7 +800,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { return emitLoad(fn, g) // var (address) } callee := v.(*Function) // (func) - if len(callee._TypeParams) > 0 { + if callee.typeparams.Len() > 0 { targs := fn.subst.types(instanceArgs(fn.info, e)) callee = fn.Prog.needsInstance(callee, targs, b.created) } @@ -822,11 +834,32 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { wantAddr := isPointer(rt) escaping := true v := b.receiver(fn, e.X, wantAddr, escaping, sel) - if isInterface(rt) { - // If v has interface type I, + + if types.IsInterface(rt) { + // If v may be an interface type I (after instantiating), // we must emit a check that v is non-nil. - // We use: typeassert v.(I). - emitTypeAssert(fn, v, rt, token.NoPos) + if recv, ok := sel.recv.(*typeparams.TypeParam); ok { + // Emit a nil check if any possible instantiation of the + // type parameter is an interface type. + if len(typeSetOf(recv)) > 0 { + // recv has a concrete term its typeset. + // So it cannot be instantiated as an interface. + // + // Example: + // func _[T interface{~int; Foo()}] () { + // var v T + // _ = v.Foo // <-- MethodVal + // } + } else { + // rt may be instantiated as an interface. + // Emit nil check: typeassert (any(v)).(any). + emitTypeAssert(fn, emitConv(fn, v, tEface), tEface, token.NoPos) + } + } else { + // non-type param interface + // Emit nil check: typeassert v.(I). + emitTypeAssert(fn, v, rt, token.NoPos) + } } if targs := receiverTypeArgs(obj); len(targs) > 0 { // obj is generic. @@ -863,9 +896,17 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { return b.expr(fn, e.X) // Handle instantiation within the *Ident or *SelectorExpr cases. } // not a generic instantiation. - switch t := fn.typeOf(e.X).Underlying().(type) { - case *types.Array: - // Non-addressable array (in a register). + xt := fn.typeOf(e.X) + switch et, mode := indexType(xt); mode { + case ixVar: + // Addressable slice/array; use IndexAddr and Load. + return b.addr(fn, e, false).load(fn) + + case ixArrVar, ixValue: + // An array in a register, a string or a combined type that contains + // either an [_]array (ixArrVar) or string (ixValue). + + // Note: for ixArrVar and coreType(xt)==nil can be IndexAddr and Load. index := b.expr(fn, e.Index) if isUntyped(index.Type()) { index = emitConv(fn, index, tInt) @@ -875,38 +916,20 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { Index: index, } v.setPos(e.Lbrack) - v.setType(t.Elem()) - return fn.emit(v) - - case *types.Map: - v := &Lookup{ - X: b.expr(fn, e.X), - Index: emitConv(fn, b.expr(fn, e.Index), t.Key()), - } - v.setPos(e.Lbrack) - v.setType(t.Elem()) + v.setType(et) return fn.emit(v) - case *types.Basic: // => string - // Strings are not addressable. - index := b.expr(fn, e.Index) - if isUntyped(index.Type()) { - index = emitConv(fn, index, tInt) - } + case ixMap: + ct := coreType(xt).(*types.Map) v := &Lookup{ X: b.expr(fn, e.X), - Index: index, + Index: emitConv(fn, b.expr(fn, e.Index), ct.Key()), } v.setPos(e.Lbrack) - v.setType(tByte) + v.setType(ct.Elem()) return fn.emit(v) - - case *types.Slice, *types.Pointer: // *array - // Addressable slice/array; use IndexAddr and Load. - return b.addr(fn, e, false).load(fn) - default: - panic("unexpected container type in IndexExpr: " + t.String()) + panic("unexpected container type in IndexExpr: " + xt.String()) } case *ast.CompositeLit, *ast.StarExpr: @@ -967,14 +990,14 @@ func (b *builder) setCallFunc(fn *Function, e *ast.CallExpr, c *CallCommon) { wantAddr := isPointer(recv) escaping := true v := b.receiver(fn, selector.X, wantAddr, escaping, sel) - if isInterface(recv) { + if types.IsInterface(recv) { // Invoke-mode call. - c.Value = v + c.Value = v // possibly type param c.Method = obj } else { // "Call"-mode call. callee := fn.Prog.originFunc(obj) - if len(callee._TypeParams) > 0 { + if callee.typeparams.Len() > 0 { callee = fn.Prog.needsInstance(callee, receiverTypeArgs(obj), b.created) } c.Value = callee @@ -1065,7 +1088,7 @@ func (b *builder) emitCallArgs(fn *Function, sig *types.Signature, e *ast.CallEx st := sig.Params().At(np).Type().(*types.Slice) vt := st.Elem() if len(varargs) == 0 { - args = append(args, nilConst(st)) + args = append(args, zeroConst(st)) } else { // Replace a suffix of args with a slice containing it. at := types.NewArray(vt, int64(len(varargs))) @@ -1097,7 +1120,7 @@ func (b *builder) setCall(fn *Function, e *ast.CallExpr, c *CallCommon) { b.setCallFunc(fn, e, c) // Then append the other actual parameters. - sig, _ := fn.typeOf(e.Fun).Underlying().(*types.Signature) + sig, _ := coreType(fn.typeOf(e.Fun)).(*types.Signature) if sig == nil { panic(fmt.Sprintf("no signature for call of %s", e.Fun)) } @@ -1230,8 +1253,32 @@ func (b *builder) arrayLen(fn *Function, elts []ast.Expr) int64 { // literal has type *T behaves like &T{}. // In that case, addr must hold a T, not a *T. func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero bool, sb *storebuf) { - typ := deref(fn.typeOf(e)) - switch t := typ.Underlying().(type) { + typ := deref(fn.typeOf(e)) // type with name [may be type param] + t := deref(coreType(typ)).Underlying() // core type for comp lit case + // Computing typ and t is subtle as these handle pointer types. + // For example, &T{...} is valid even for maps and slices. + // Also typ should refer to T (not *T) while t should be the core type of T. + // + // To show the ordering to take into account, consider the composite literal + // expressions `&T{f: 1}` and `{f: 1}` within the expression `[]S{{f: 1}}` here: + // type N struct{f int} + // func _[T N, S *N]() { + // _ = &T{f: 1} + // _ = []S{{f: 1}} + // } + // For `&T{f: 1}`, we compute `typ` and `t` as: + // typeOf(&T{f: 1}) == *T + // deref(*T) == T (typ) + // coreType(T) == N + // deref(N) == N + // N.Underlying() == struct{f int} (t) + // For `{f: 1}` in `[]S{{f: 1}}`, we compute `typ` and `t` as: + // typeOf({f: 1}) == S + // deref(S) == S (typ) + // coreType(S) == *N + // deref(*N) == N + // N.Underlying() == struct{f int} (t) + switch t := t.(type) { case *types.Struct: if !isZero && len(e.Elts) != t.NumFields() { // memclear @@ -1259,6 +1306,7 @@ func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero X: addr, Field: fieldIndex, } + faddr.setPos(pos) faddr.setType(types.NewPointer(sf.Type())) fn.emit(faddr) b.assign(fn, &address{addr: faddr, pos: pos, expr: e}, e, isZero, sb) @@ -1529,7 +1577,7 @@ func (b *builder) typeSwitchStmt(fn *Function, s *ast.TypeSwitchStmt, label *lbl casetype = fn.typeOf(cond) var condv Value if casetype == tUntypedNil { - condv = emitCompare(fn, token.EQL, x, nilConst(x.Type()), cond.Pos()) + condv = emitCompare(fn, token.EQL, x, zeroConst(x.Type()), cond.Pos()) ti = x } else { yok := emitTypeTest(fn, x, casetype, cc.Case) @@ -1612,7 +1660,7 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { case *ast.SendStmt: // ch<- i ch := b.expr(fn, comm.Chan) - chtyp := fn.typ(ch.Type()).Underlying().(*types.Chan) + chtyp := coreType(fn.typ(ch.Type())).(*types.Chan) st = &SelectState{ Dir: types.SendOnly, Chan: ch, @@ -1669,9 +1717,8 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { vars = append(vars, varIndex, varOk) for _, st := range states { if st.Dir == types.RecvOnly { - chtyp := fn.typ(st.Chan.Type()).Underlying().(*types.Chan) - tElem := chtyp.Elem() - vars = append(vars, anonVar(tElem)) + chtyp := coreType(fn.typ(st.Chan.Type())).(*types.Chan) + vars = append(vars, anonVar(chtyp.Elem())) } } sel.setType(types.NewTuple(vars...)) @@ -1835,6 +1882,8 @@ func (b *builder) rangeIndexed(fn *Function, x Value, tv types.Type, pos token.P // elimination if x is pure, static unrolling, etc. // Ranging over a nil *array may have >0 iterations. // We still generate code for x, in case it has effects. + // + // TypeParams do not have constant length. Use underlying instead of core type. length = intConst(arr.Len()) } else { // length = len(x). @@ -1867,7 +1916,7 @@ func (b *builder) rangeIndexed(fn *Function, x Value, tv types.Type, pos token.P k = emitLoad(fn, index) if tv != nil { - switch t := x.Type().Underlying().(type) { + switch t := coreType(x.Type()).(type) { case *types.Array: instr := &Index{ X: x, @@ -1937,11 +1986,9 @@ func (b *builder) rangeIter(fn *Function, x Value, tk, tv types.Type, pos token. emitJump(fn, loop) fn.currentBlock = loop - _, isString := x.Type().Underlying().(*types.Basic) - okv := &Next{ Iter: it, - IsString: isString, + IsString: isBasic(coreType(x.Type())), } okv.setType(types.NewTuple( varOk, @@ -1991,7 +2038,7 @@ func (b *builder) rangeChan(fn *Function, x Value, tk types.Type, pos token.Pos) } recv.setPos(pos) recv.setType(types.NewTuple( - newVar("k", x.Type().Underlying().(*types.Chan).Elem()), + newVar("k", coreType(x.Type()).(*types.Chan).Elem()), varOk, )) ko := fn.emit(recv) @@ -2035,7 +2082,7 @@ func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { var k, v Value var loop, done *BasicBlock - switch rt := x.Type().Underlying().(type) { + switch rt := coreType(x.Type()).(type) { case *types.Slice, *types.Array, *types.Pointer: // *array k, v, loop, done = b.rangeIndexed(fn, x, tv, s.For) @@ -2113,11 +2160,11 @@ start: b.expr(fn, s.X) case *ast.SendStmt: + chtyp := coreType(fn.typeOf(s.Chan)).(*types.Chan) fn.emit(&Send{ Chan: b.expr(fn, s.Chan), - X: emitConv(fn, b.expr(fn, s.Value), - fn.typeOf(s.Chan).Underlying().(*types.Chan).Elem()), - pos: s.Arrow, + X: emitConv(fn, b.expr(fn, s.Value), chtyp.Elem()), + pos: s.Arrow, }) case *ast.IncDecStmt: @@ -2295,11 +2342,9 @@ func (b *builder) buildFunctionBody(fn *Function) { var functype *ast.FuncType switch n := fn.syntax.(type) { case nil: - // TODO(taking): Temporarily this can be the body of a generic function. if fn.Params != nil { return // not a Go source function. (Synthetic, or from object file.) } - // fn.Params == nil is handled within body == nil case. case *ast.FuncDecl: functype = n.Type recvField = n.Recv @@ -2331,6 +2376,13 @@ func (b *builder) buildFunctionBody(fn *Function) { } return } + + // Build instantiation wrapper around generic body? + if fn.topLevelOrigin != nil && fn.subst == nil { + buildInstantiationWrapper(fn) + return + } + if fn.Prog.mode&LogSource != 0 { defer logStack("build function %s @ %s", fn, fn.Prog.Fset.Position(fn.pos))() } @@ -2435,7 +2487,17 @@ func (p *Package) build() { // TODO(adonovan): ideally belongs in memberFromObject, but // that would require package creation in topological order. for name, mem := range p.Members { - if ast.IsExported(name) && !isGeneric(mem) { + isGround := func(m Member) bool { + switch m := m.(type) { + case *Type: + named, _ := m.Type().(*types.Named) + return named == nil || typeparams.ForNamed(named) == nil + case *Function: + return m.typeparams.Len() == 0 + } + return true // *NamedConst, *Global + } + if ast.IsExported(name) && isGround(mem) { p.Prog.needMethodsOf(mem.Type(), &p.created) } } diff --git a/go/ssa/builder_generic_test.go b/go/ssa/builder_generic_test.go new file mode 100644 index 00000000000..dda53e1541f --- /dev/null +++ b/go/ssa/builder_generic_test.go @@ -0,0 +1,664 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ssa_test + +import ( + "fmt" + "go/parser" + "go/token" + "reflect" + "sort" + "testing" + + "golang.org/x/tools/go/expect" + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/typeparams" +) + +// TestGenericBodies tests that bodies of generic functions and methods containing +// different constructs can be built in BuilderMode(0). +// +// Each test specifies the contents of package containing a single go file. +// Each call print(arg0, arg1, ...) to the builtin print function +// in ssa is correlated a comment at the end of the line of the form: +// +// //@ types(a, b, c) +// +// where a, b and c are the types of the arguments to the print call +// serialized using go/types.Type.String(). +// See x/tools/go/expect for details on the syntax. +func TestGenericBodies(t *testing.T) { + if !typeparams.Enabled { + t.Skip("TestGenericBodies requires type parameters") + } + for _, test := range []struct { + pkg string // name of the package. + contents string // contents of the Go package. + }{ + { + pkg: "p", + contents: ` + package p + + func f(x int) { + var i interface{} + print(i, 0) //@ types("interface{}", int) + print() //@ types() + print(x) //@ types(int) + } + `, + }, + { + pkg: "q", + contents: ` + package q + + func f[T any](x T) { + print(x) //@ types(T) + } + `, + }, + { + pkg: "r", + contents: ` + package r + + func f[T ~int]() { + var x T + print(x) //@ types(T) + } + `, + }, + { + pkg: "s", + contents: ` + package s + + func a[T ~[4]byte](x T) { + for k, v := range x { + print(x, k, v) //@ types(T, int, byte) + } + } + func b[T ~*[4]byte](x T) { + for k, v := range x { + print(x, k, v) //@ types(T, int, byte) + } + } + func c[T ~[]byte](x T) { + for k, v := range x { + print(x, k, v) //@ types(T, int, byte) + } + } + func d[T ~string](x T) { + for k, v := range x { + print(x, k, v) //@ types(T, int, rune) + } + } + func e[T ~map[int]string](x T) { + for k, v := range x { + print(x, k, v) //@ types(T, int, string) + } + } + func f[T ~chan string](x T) { + for v := range x { + print(x, v) //@ types(T, string) + } + } + + func From() { + type A [4]byte + print(a[A]) //@ types("func(x s.A)") + + type B *[4]byte + print(b[B]) //@ types("func(x s.B)") + + type C []byte + print(c[C]) //@ types("func(x s.C)") + + type D string + print(d[D]) //@ types("func(x s.D)") + + type E map[int]string + print(e[E]) //@ types("func(x s.E)") + + type F chan string + print(f[F]) //@ types("func(x s.F)") + } + `, + }, + { + pkg: "t", + contents: ` + package t + + func f[S any, T ~chan S](x T) { + for v := range x { + print(x, v) //@ types(T, S) + } + } + + func From() { + type F chan string + print(f[string, F]) //@ types("func(x t.F)") + } + `, + }, + { + pkg: "u", + contents: ` + package u + + func fibonacci[T ~chan int](c, quit T) { + x, y := 0, 1 + for { + select { + case c <- x: + x, y = y, x+y + case <-quit: + print(c, quit, x, y) //@ types(T, T, int, int) + return + } + } + } + func start[T ~chan int](c, quit T) { + go func() { + for i := 0; i < 10; i++ { + print(<-c) //@ types(int) + } + quit <- 0 + }() + } + func From() { + type F chan int + c := make(F) + quit := make(F) + print(start[F], c, quit) //@ types("func(c u.F, quit u.F)", "u.F", "u.F") + print(fibonacci[F], c, quit) //@ types("func(c u.F, quit u.F)", "u.F", "u.F") + } + `, + }, + { + pkg: "v", + contents: ` + package v + + func f[T ~struct{ x int; y string }](i int) T { + u := []T{ T{0, "lorem"}, T{1, "ipsum"}} + return u[i] + } + func From() { + type S struct{ x int; y string } + print(f[S]) //@ types("func(i int) v.S") + } + `, + }, + { + pkg: "w", + contents: ` + package w + + func f[T ~[4]int8](x T, l, h int) []int8 { + return x[l:h] + } + func g[T ~*[4]int16](x T, l, h int) []int16 { + return x[l:h] + } + func h[T ~[]int32](x T, l, h int) T { + return x[l:h] + } + func From() { + type F [4]int8 + type G *[4]int16 + type H []int32 + print(f[F](F{}, 0, 0)) //@ types("[]int8") + print(g[G](nil, 0, 0)) //@ types("[]int16") + print(h[H](nil, 0, 0)) //@ types("w.H") + } + `, + }, + { + pkg: "x", + contents: ` + package x + + func h[E any, T ~[]E](x T, l, h int) []E { + s := x[l:h] + print(s) //@ types("T") + return s + } + func From() { + type H []int32 + print(h[int32, H](nil, 0, 0)) //@ types("[]int32") + } + `, + }, + { + pkg: "y", + contents: ` + package y + + // Test "make" builtin with different forms on core types and + // when capacities are constants or variable. + func h[E any, T ~[]E](m, n int) { + print(make(T, 3)) //@ types(T) + print(make(T, 3, 5)) //@ types(T) + print(make(T, m)) //@ types(T) + print(make(T, m, n)) //@ types(T) + } + func i[K comparable, E any, T ~map[K]E](m int) { + print(make(T)) //@ types(T) + print(make(T, 5)) //@ types(T) + print(make(T, m)) //@ types(T) + } + func j[E any, T ~chan E](m int) { + print(make(T)) //@ types(T) + print(make(T, 6)) //@ types(T) + print(make(T, m)) //@ types(T) + } + func From() { + type H []int32 + h[int32, H](3, 4) + type I map[int8]H + i[int8, H, I](5) + type J chan I + j[I, J](6) + } + `, + }, + { + pkg: "z", + contents: ` + package z + + func h[T ~[4]int](x T) { + print(len(x), cap(x)) //@ types(int, int) + } + func i[T ~[4]byte | []int | ~chan uint8](x T) { + print(len(x), cap(x)) //@ types(int, int) + } + func j[T ~[4]int | any | map[string]int]() { + print(new(T)) //@ types("*T") + } + func k[T ~[4]int | any | map[string]int](x T) { + print(x) //@ types(T) + panic(x) + } + `, + }, + { + pkg: "a", + contents: ` + package a + + func f[E any, F ~func() E](x F) { + print(x, x()) //@ types(F, E) + } + func From() { + type T func() int + f[int, T](func() int { return 0 }) + f[int, func() int](func() int { return 1 }) + } + `, + }, + { + pkg: "b", + contents: ` + package b + + func f[E any, M ~map[string]E](m M) { + y, ok := m["lorem"] + print(m, y, ok) //@ types(M, E, bool) + } + func From() { + type O map[string][]int + f(O{"lorem": []int{0, 1, 2, 3}}) + } + `, + }, + { + pkg: "c", + contents: ` + package c + + func a[T interface{ []int64 | [5]int64 }](x T) int64 { + print(x, x[2], x[3]) //@ types(T, int64, int64) + x[2] = 5 + return x[3] + } + func b[T interface{ []byte | string }](x T) byte { + print(x, x[3]) //@ types(T, byte) + return x[3] + } + func c[T interface{ []byte }](x T) byte { + print(x, x[2], x[3]) //@ types(T, byte, byte) + x[2] = 'b' + return x[3] + } + func d[T interface{ map[int]int64 }](x T) int64 { + print(x, x[2], x[3]) //@ types(T, int64, int64) + x[2] = 43 + return x[3] + } + func e[T ~string](t T) { + print(t, t[0]) //@ types(T, uint8) + } + func f[T ~string|[]byte](t T) { + print(t, t[0]) //@ types(T, uint8) + } + func g[T []byte](t T) { + print(t, t[0]) //@ types(T, byte) + } + func h[T ~[4]int|[]int](t T) { + print(t, t[0]) //@ types(T, int) + } + func i[T ~[4]int|*[4]int|[]int](t T) { + print(t, t[0]) //@ types(T, int) + } + func j[T ~[4]int|*[4]int|[]int](t T) { + print(t, &t[0]) //@ types(T, "*int") + } + `, + }, + { + pkg: "d", + contents: ` + package d + + type MyInt int + type Other int + type MyInterface interface{ foo() } + + // ChangeType tests + func ct0(x int) { v := MyInt(x); print(x, v) /*@ types(int, "d.MyInt")*/ } + func ct1[T MyInt | Other, S int ](x S) { v := T(x); print(x, v) /*@ types(S, T)*/ } + func ct2[T int, S MyInt | int ](x S) { v := T(x); print(x, v) /*@ types(S, T)*/ } + func ct3[T MyInt | Other, S MyInt | int ](x S) { v := T(x) ; print(x, v) /*@ types(S, T)*/ } + + // Convert tests + func co0[T int | int8](x MyInt) { v := T(x); print(x, v) /*@ types("d.MyInt", T)*/} + func co1[T int | int8](x T) { v := MyInt(x); print(x, v) /*@ types(T, "d.MyInt")*/ } + func co2[S, T int | int8](x T) { v := S(x); print(x, v) /*@ types(T, S)*/ } + + // MakeInterface tests + func mi0[T MyInterface](x T) { v := MyInterface(x); print(x, v) /*@ types(T, "d.MyInterface")*/ } + + // NewConst tests + func nc0[T any]() { v := (*T)(nil); print(v) /*@ types("*T")*/} + + // SliceToArrayPointer + func sl0[T *[4]int | *[2]int](x []int) { v := T(x); print(x, v) /*@ types("[]int", T)*/ } + func sl1[T *[4]int | *[2]int, S []int](x S) { v := T(x); print(x, v) /*@ types(S, T)*/ } + `, + }, + { + pkg: "e", + contents: ` + package e + + func c[T interface{ foo() string }](x T) { + print(x, x.foo, x.foo()) /*@ types(T, "func() string", string)*/ + } + `, + }, + { + pkg: "f", + contents: `package f + + func eq[T comparable](t T, i interface{}) bool { + return t == i + } + `, + }, + { + pkg: "g", + contents: `package g + type S struct{ f int } + func c[P *S]() []P { return []P{{f: 1}} } + `, + }, + { + pkg: "h", + contents: `package h + func sign[bytes []byte | string](s bytes) (bool, bool) { + neg := false + if len(s) > 0 && (s[0] == '-' || s[0] == '+') { + neg = s[0] == '-' + s = s[1:] + } + return !neg, len(s) > 0 + }`, + }, + { + pkg: "i", + contents: `package i + func digits[bytes []byte | string](s bytes) bool { + for _, c := range []byte(s) { + if c < '0' || '9' < c { + return false + } + } + return true + }`, + }, + } { + test := test + t.Run(test.pkg, func(t *testing.T) { + // Parse + conf := loader.Config{ParserMode: parser.ParseComments} + fname := test.pkg + ".go" + f, err := conf.ParseFile(fname, test.contents) + if err != nil { + t.Fatalf("parse: %v", err) + } + conf.CreateFromFiles(test.pkg, f) + + // Load + lprog, err := conf.Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + + // Create and build SSA + prog := ssa.NewProgram(lprog.Fset, ssa.SanityCheckFunctions) + for _, info := range lprog.AllPackages { + if info.TransitivelyErrorFree { + prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) + } + } + p := prog.Package(lprog.Package(test.pkg).Pkg) + p.Build() + + // Collect calls to the builtin print function. + probes := make(map[*ssa.CallCommon]bool) + for _, mem := range p.Members { + if fn, ok := mem.(*ssa.Function); ok { + for _, bb := range fn.Blocks { + for _, i := range bb.Instrs { + if i, ok := i.(ssa.CallInstruction); ok { + call := i.Common() + if b, ok := call.Value.(*ssa.Builtin); ok && b.Name() == "print" { + probes[i.Common()] = true + } + } + } + } + } + } + + // Collect all notes in f, i.e. comments starting with "//@ types". + notes, err := expect.ExtractGo(prog.Fset, f) + if err != nil { + t.Errorf("expect.ExtractGo: %v", err) + } + + // Matches each probe with a note that has the same line. + sameLine := func(x, y token.Pos) bool { + xp := prog.Fset.Position(x) + yp := prog.Fset.Position(y) + return xp.Filename == yp.Filename && xp.Line == yp.Line + } + expectations := make(map[*ssa.CallCommon]*expect.Note) + for call := range probes { + var match *expect.Note + for _, note := range notes { + if note.Name == "types" && sameLine(call.Pos(), note.Pos) { + match = note // first match is good enough. + break + } + } + if match != nil { + expectations[call] = match + } else { + t.Errorf("Unmatched probe: %v", call) + } + } + + // Check each expectation. + for call, note := range expectations { + var args []string + for _, a := range call.Args { + args = append(args, a.Type().String()) + } + if got, want := fmt.Sprint(args), fmt.Sprint(note.Args); got != want { + t.Errorf("Arguments to print() were expected to be %q. got %q", want, got) + } + } + }) + } +} + +// TestInstructionString tests serializing instructions via Instruction.String(). +func TestInstructionString(t *testing.T) { + if !typeparams.Enabled { + t.Skip("TestInstructionString requires type parameters") + } + // Tests (ssa.Instruction).String(). Instructions are from a single go file. + // The Instructions tested are those that match a comment of the form: + // + // //@ instrs(f, kind, strs...) + // + // where f is the name of the function, kind is the type of the instructions matched + // within the function, and tests that the String() value for all of the instructions + // matched of String() is strs (in some order). + // See x/tools/go/expect for details on the syntax. + + const contents = ` + package p + + //@ instrs("f", "*ssa.TypeAssert") + //@ instrs("f", "*ssa.Call", "print(nil:interface{}, 0:int)") + func f(x int) { // non-generic smoke test. + var i interface{} + print(i, 0) + } + + //@ instrs("h", "*ssa.Alloc", "local T (u)") + //@ instrs("h", "*ssa.FieldAddr", "&t0.x [#0]") + func h[T ~struct{ x string }]() T { + u := T{"lorem"} + return u + } + + //@ instrs("c", "*ssa.TypeAssert", "typeassert t0.(interface{})") + //@ instrs("c", "*ssa.Call", "invoke x.foo()") + func c[T interface{ foo() string }](x T) { + _ = x.foo + _ = x.foo() + } + + //@ instrs("d", "*ssa.TypeAssert", "typeassert t0.(interface{})") + //@ instrs("d", "*ssa.Call", "invoke x.foo()") + func d[T interface{ foo() string; comparable }](x T) { + _ = x.foo + _ = x.foo() + } + ` + + // Parse + conf := loader.Config{ParserMode: parser.ParseComments} + const fname = "p.go" + f, err := conf.ParseFile(fname, contents) + if err != nil { + t.Fatalf("parse: %v", err) + } + conf.CreateFromFiles("p", f) + + // Load + lprog, err := conf.Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + + // Create and build SSA + prog := ssa.NewProgram(lprog.Fset, ssa.SanityCheckFunctions) + for _, info := range lprog.AllPackages { + if info.TransitivelyErrorFree { + prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) + } + } + p := prog.Package(lprog.Package("p").Pkg) + p.Build() + + // Collect all notes in f, i.e. comments starting with "//@ instr". + notes, err := expect.ExtractGo(prog.Fset, f) + if err != nil { + t.Errorf("expect.ExtractGo: %v", err) + } + + // Expectation is a {function, type string} -> {want, matches} + // where matches is all Instructions.String() that match the key. + // Each expecation is that some permutation of matches is wants. + type expKey struct { + function string + kind string + } + type expValue struct { + wants []string + matches []string + } + expectations := make(map[expKey]*expValue) + for _, note := range notes { + if note.Name == "instrs" { + if len(note.Args) < 2 { + t.Error("Had @instrs annotation without at least 2 arguments") + continue + } + fn, kind := fmt.Sprint(note.Args[0]), fmt.Sprint(note.Args[1]) + var wants []string + for _, arg := range note.Args[2:] { + wants = append(wants, fmt.Sprint(arg)) + } + expectations[expKey{fn, kind}] = &expValue{wants, nil} + } + } + + // Collect all Instructions that match the expectations. + for _, mem := range p.Members { + if fn, ok := mem.(*ssa.Function); ok { + for _, bb := range fn.Blocks { + for _, i := range bb.Instrs { + kind := fmt.Sprintf("%T", i) + if e := expectations[expKey{fn.Name(), kind}]; e != nil { + e.matches = append(e.matches, i.String()) + } + } + } + } + } + + // Check each expectation. + for key, value := range expectations { + if _, ok := p.Members[key.function]; !ok { + t.Errorf("Expectation on %s does not match a member in %s", key.function, p.Pkg.Name()) + } + got, want := value.matches, value.wants + sort.Strings(got) + sort.Strings(want) + if !reflect.DeepEqual(want, got) { + t.Errorf("Within %s wanted instructions of kind %s: %q. got %q", key.function, key.kind, want, got) + } + } +} diff --git a/go/ssa/builder_go120_test.go b/go/ssa/builder_go120_test.go index 84bdd4c41ab..04fb11a2d22 100644 --- a/go/ssa/builder_go120_test.go +++ b/go/ssa/builder_go120_test.go @@ -25,6 +25,10 @@ func TestBuildPackageGo120(t *testing.T) { importer types.Importer }{ {"slice to array", "package p; var s []byte; var _ = ([4]byte)(s)", nil}, + {"slice to zero length array", "package p; var s []byte; var _ = ([0]byte)(s)", nil}, + {"slice to zero length array type parameter", "package p; var s []byte; func f[T ~[0]byte]() { tmp := (T)(s); var z T; _ = tmp == z}", nil}, + {"slice to non-zero length array type parameter", "package p; var s []byte; func h[T ~[1]byte | [4]byte]() { tmp := T(s); var z T; _ = tmp == z}", nil}, + {"slice to maybe-zero length array type parameter", "package p; var s []byte; func g[T ~[0]byte | [4]byte]() { tmp := T(s); var z T; _ = tmp == z}", nil}, } for _, tc := range tests { diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index 6fc844187db..a80d8d5ab73 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -226,6 +226,18 @@ func TestRuntimeTypes(t *testing.T) { nil, }, } + + if typeparams.Enabled { + tests = append(tests, []struct { + input string + want []string + }{ + // MakeInterface does not create runtime type for parameterized types. + {`package N; var g interface{}; func f[S any]() { var v []S; g = v }; `, + nil, + }, + }...) + } for _, test := range tests { // Parse the file. fset := token.NewFileSet() diff --git a/go/ssa/const.go b/go/ssa/const.go index dc182d9616c..3468eac7e13 100644 --- a/go/ssa/const.go +++ b/go/ssa/const.go @@ -12,65 +12,73 @@ import ( "go/token" "go/types" "strconv" + "strings" + + "golang.org/x/tools/internal/typeparams" ) // NewConst returns a new constant of the specified value and type. // val must be valid according to the specification of Const.Value. func NewConst(val constant.Value, typ types.Type) *Const { + if val == nil { + switch soleTypeKind(typ) { + case types.IsBoolean: + val = constant.MakeBool(false) + case types.IsInteger: + val = constant.MakeInt64(0) + case types.IsString: + val = constant.MakeString("") + } + } return &Const{typ, val} } +// soleTypeKind returns a BasicInfo for which constant.Value can +// represent all zero values for the types in the type set. +// +// types.IsBoolean for false is a representative. +// types.IsInteger for 0 +// types.IsString for "" +// 0 otherwise. +func soleTypeKind(typ types.Type) types.BasicInfo { + // State records the set of possible zero values (false, 0, ""). + // Candidates (perhaps all) are eliminated during the type-set + // iteration, which executes at least once. + state := types.IsBoolean | types.IsInteger | types.IsString + typeSetOf(typ).underIs(func(t types.Type) bool { + var c types.BasicInfo + if t, ok := t.(*types.Basic); ok { + c = t.Info() + } + if c&types.IsNumeric != 0 { // int/float/complex + c = types.IsInteger + } + state = state & c + return state != 0 + }) + return state +} + // intConst returns an 'int' constant that evaluates to i. // (i is an int64 in case the host is narrower than the target.) func intConst(i int64) *Const { return NewConst(constant.MakeInt64(i), tInt) } -// nilConst returns a nil constant of the specified type, which may -// be any reference type, including interfaces. -func nilConst(typ types.Type) *Const { - return NewConst(nil, typ) -} - // stringConst returns a 'string' constant that evaluates to s. func stringConst(s string) *Const { return NewConst(constant.MakeString(s), tString) } -// zeroConst returns a new "zero" constant of the specified type, -// which must not be an array or struct type: the zero values of -// aggregates are well-defined but cannot be represented by Const. +// zeroConst returns a new "zero" constant of the specified type. func zeroConst(t types.Type) *Const { - switch t := t.(type) { - case *types.Basic: - switch { - case t.Info()&types.IsBoolean != 0: - return NewConst(constant.MakeBool(false), t) - case t.Info()&types.IsNumeric != 0: - return NewConst(constant.MakeInt64(0), t) - case t.Info()&types.IsString != 0: - return NewConst(constant.MakeString(""), t) - case t.Kind() == types.UnsafePointer: - fallthrough - case t.Kind() == types.UntypedNil: - return nilConst(t) - default: - panic(fmt.Sprint("zeroConst for unexpected type:", t)) - } - case *types.Pointer, *types.Slice, *types.Interface, *types.Chan, *types.Map, *types.Signature: - return nilConst(t) - case *types.Named: - return NewConst(zeroConst(t.Underlying()).Value, t) - case *types.Array, *types.Struct, *types.Tuple: - panic(fmt.Sprint("zeroConst applied to aggregate:", t)) - } - panic(fmt.Sprint("zeroConst: unexpected ", t)) + return NewConst(nil, t) } func (c *Const) RelString(from *types.Package) string { var s string if c.Value == nil { - s = "nil" + s = zeroString(c.typ, from) } else if c.Value.Kind() == constant.String { s = constant.StringVal(c.Value) const max = 20 @@ -85,6 +93,44 @@ func (c *Const) RelString(from *types.Package) string { return s + ":" + relType(c.Type(), from) } +// zeroString returns the string representation of the "zero" value of the type t. +func zeroString(t types.Type, from *types.Package) string { + switch t := t.(type) { + case *types.Basic: + switch { + case t.Info()&types.IsBoolean != 0: + return "false" + case t.Info()&types.IsNumeric != 0: + return "0" + case t.Info()&types.IsString != 0: + return `""` + case t.Kind() == types.UnsafePointer: + fallthrough + case t.Kind() == types.UntypedNil: + return "nil" + default: + panic(fmt.Sprint("zeroString for unexpected type:", t)) + } + case *types.Pointer, *types.Slice, *types.Interface, *types.Chan, *types.Map, *types.Signature: + return "nil" + case *types.Named: + return zeroString(t.Underlying(), from) + case *types.Array, *types.Struct: + return relType(t, from) + "{}" + case *types.Tuple: + // Tuples are not normal values. + // We are currently format as "(t[0], ..., t[n])". Could be something else. + components := make([]string, t.Len()) + for i := 0; i < t.Len(); i++ { + components[i] = zeroString(t.At(i).Type(), from) + } + return "(" + strings.Join(components, ", ") + ")" + case *typeparams.TypeParam: + return "*new(" + relType(t, from) + ")" + } + panic(fmt.Sprint("zeroString: unexpected ", t)) +} + func (c *Const) Name() string { return c.RelString(nil) } @@ -107,9 +153,26 @@ func (c *Const) Pos() token.Pos { return token.NoPos } -// IsNil returns true if this constant represents a typed or untyped nil value. +// IsNil returns true if this constant represents a typed or untyped nil value +// with an underlying reference type: pointer, slice, chan, map, function, or +// *basic* interface. +// +// Note: a type parameter whose underlying type is a basic interface is +// considered a reference type. func (c *Const) IsNil() bool { - return c.Value == nil + return c.Value == nil && nillable(c.typ) +} + +// nillable reports whether *new(T) == nil is legal for type T. +func nillable(t types.Type) bool { + switch t := t.Underlying().(type) { + case *types.Pointer, *types.Slice, *types.Chan, *types.Map, *types.Signature: + return true + case *types.Interface: + return len(typeSetOf(t)) == 0 // basic interface. + default: + return false + } } // TODO(adonovan): move everything below into golang.org/x/tools/go/ssa/interp. @@ -149,14 +212,16 @@ func (c *Const) Uint64() uint64 { // Float64 returns the numeric value of this constant truncated to fit // a float64. func (c *Const) Float64() float64 { - f, _ := constant.Float64Val(c.Value) + x := constant.ToFloat(c.Value) // (c.Value == nil) => x.Kind() == Unknown + f, _ := constant.Float64Val(x) return f } // Complex128 returns the complex value of this constant truncated to // fit a complex128. func (c *Const) Complex128() complex128 { - re, _ := constant.Float64Val(constant.Real(c.Value)) - im, _ := constant.Float64Val(constant.Imag(c.Value)) + x := constant.ToComplex(c.Value) // (c.Value == nil) => x.Kind() == Unknown + re, _ := constant.Float64Val(constant.Real(x)) + im, _ := constant.Float64Val(constant.Imag(x)) return complex(re, im) } diff --git a/go/ssa/const_test.go b/go/ssa/const_test.go new file mode 100644 index 00000000000..131fe1aced2 --- /dev/null +++ b/go/ssa/const_test.go @@ -0,0 +1,104 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ssa_test + +import ( + "go/ast" + "go/constant" + "go/parser" + "go/token" + "go/types" + "math/big" + "strings" + "testing" + + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/typeparams" +) + +func TestConstString(t *testing.T) { + if !typeparams.Enabled { + t.Skip("TestConstString requires type parameters.") + } + + const source = ` + package P + + type Named string + + func fn() (int, bool, string) + func gen[T int]() {} + ` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.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) + } + + for _, test := range []struct { + expr string // type expression + constant interface{} // constant value + want string // expected String() value + }{ + {"int", int64(0), "0:int"}, + {"int64", int64(0), "0:int64"}, + {"float32", int64(0), "0:float32"}, + {"float32", big.NewFloat(1.5), "1.5:float32"}, + {"bool", false, "false:bool"}, + {"string", "", `"":string`}, + {"Named", "", `"":P.Named`}, + {"struct{x string}", nil, "struct{x string}{}:struct{x string}"}, + {"[]int", nil, "nil:[]int"}, + {"[3]int", nil, "[3]int{}:[3]int"}, + {"*int", nil, "nil:*int"}, + {"interface{}", nil, "nil:interface{}"}, + {"interface{string}", nil, `"":interface{string}`}, + {"interface{int|int64}", nil, "0:interface{int|int64}"}, + {"interface{bool}", nil, "false:interface{bool}"}, + {"interface{bool|int}", nil, "nil:interface{bool|int}"}, + {"interface{int|string}", nil, "nil:interface{int|string}"}, + {"interface{bool|string}", nil, "nil:interface{bool|string}"}, + {"interface{struct{x string}}", nil, "nil:interface{struct{x string}}"}, + {"interface{int|int64}", int64(1), "1:interface{int|int64}"}, + {"interface{~bool}", true, "true:interface{~bool}"}, + {"interface{Named}", "lorem ipsum", `"lorem ipsum":interface{P.Named}`}, + {"func() (int, bool, string)", nil, "nil:func() (int, bool, string)"}, + } { + // Eval() expr for its type. + tv, err := types.Eval(fset, pkg, 0, test.expr) + if err != nil { + t.Fatalf("Eval(%s) failed: %v", test.expr, err) + } + var val constant.Value + if test.constant != nil { + val = constant.Make(test.constant) + } + c := ssa.NewConst(val, tv.Type) + got := strings.ReplaceAll(c.String(), " | ", "|") // Accept both interface{a | b} and interface{a|b}. + if got != test.want { + t.Errorf("ssa.NewConst(%v, %s).String() = %v, want %v", val, tv.Type, got, test.want) + } + } + + // Test tuples + fn := pkg.Scope().Lookup("fn") + tup := fn.Type().(*types.Signature).Results() + if got, want := ssa.NewConst(nil, tup).String(), `(0, false, ""):(int, bool, string)`; got != want { + t.Errorf("ssa.NewConst(%v, %s).String() = %v, want %v", nil, tup, got, want) + } + + // Test type-param + gen := pkg.Scope().Lookup("gen") + tp := typeparams.ForSignature(gen.Type().(*types.Signature)).At(0) + if got, want := ssa.NewConst(nil, tp).String(), "0:T"; got != want { + t.Errorf("ssa.NewConst(%v, %s).String() = %v, want %v", nil, tup, got, want) + } +} diff --git a/go/ssa/coretype.go b/go/ssa/coretype.go new file mode 100644 index 00000000000..54bc4a8e6d5 --- /dev/null +++ b/go/ssa/coretype.go @@ -0,0 +1,256 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ssa + +import ( + "go/types" + + "golang.org/x/tools/internal/typeparams" +) + +// Utilities for dealing with core types. + +// coreType returns the core type of T or nil if T does not have a core type. +// +// See https://go.dev/ref/spec#Core_types for the definition of a core type. +func coreType(T types.Type) types.Type { + U := T.Underlying() + if _, ok := U.(*types.Interface); !ok { + return U // for non-interface types, + } + + terms, err := _NormalTerms(U) + if len(terms) == 0 || err != nil { + // len(terms) -> empty type set of interface. + // err != nil => U is invalid, exceeds complexity bounds, or has an empty type set. + return nil // no core type. + } + + U = terms[0].Type().Underlying() + var identical int // i in [0,identical) => Identical(U, terms[i].Type().Underlying()) + for identical = 1; identical < len(terms); identical++ { + if !types.Identical(U, terms[identical].Type().Underlying()) { + break + } + } + + if identical == len(terms) { + // https://go.dev/ref/spec#Core_types + // "There is a single type U which is the underlying type of all types in the type set of T" + return U + } + ch, ok := U.(*types.Chan) + if !ok { + return nil // no core type as identical < len(terms) and U is not a channel. + } + // https://go.dev/ref/spec#Core_types + // "the type chan E if T contains only bidirectional channels, or the type chan<- E or + // <-chan E depending on the direction of the directional channels present." + for chans := identical; chans < len(terms); chans++ { + curr, ok := terms[chans].Type().Underlying().(*types.Chan) + if !ok { + return nil + } + if !types.Identical(ch.Elem(), curr.Elem()) { + return nil // channel elements are not identical. + } + if ch.Dir() == types.SendRecv { + // ch is bidirectional. We can safely always use curr's direction. + ch = curr + } else if curr.Dir() != types.SendRecv && ch.Dir() != curr.Dir() { + // ch and curr are not bidirectional and not the same direction. + return nil + } + } + return ch +} + +// isBytestring returns true if T has the same terms as interface{[]byte | string}. +// These act like a coreType for some operations: slice expressions, append and copy. +// +// See https://go.dev/ref/spec#Core_types for the details on bytestring. +func isBytestring(T types.Type) bool { + U := T.Underlying() + if _, ok := U.(*types.Interface); !ok { + return false + } + + tset := typeSetOf(U) + if len(tset) != 2 { + return false + } + hasBytes, hasString := false, false + tset.underIs(func(t types.Type) bool { + switch { + case isString(t): + hasString = true + case isByteSlice(t): + hasBytes = true + } + return hasBytes || hasString + }) + return hasBytes && hasString +} + +// _NormalTerms returns a slice of terms representing the normalized structural +// type restrictions of a type, if any. +// +// For all types other than *types.TypeParam, *types.Interface, and +// *types.Union, this is just a single term with Tilde() == false and +// Type() == typ. For *types.TypeParam, *types.Interface, and *types.Union, see +// below. +// +// Structural type restrictions of a type parameter are created via +// non-interface types embedded in its constraint interface (directly, or via a +// chain of interface embeddings). For example, in the declaration type +// T[P interface{~int; m()}] int the structural restriction of the type +// parameter P is ~int. +// +// With interface embedding and unions, the specification of structural type +// restrictions may be arbitrarily complex. For example, consider the +// following: +// +// type A interface{ ~string|~[]byte } +// +// type B interface{ int|string } +// +// type C interface { ~string|~int } +// +// type T[P interface{ A|B; C }] int +// +// In this example, the structural type restriction of P is ~string|int: A|B +// expands to ~string|~[]byte|int|string, which reduces to ~string|~[]byte|int, +// which when intersected with C (~string|~int) yields ~string|int. +// +// _NormalTerms computes these expansions and reductions, producing a +// "normalized" form of the embeddings. A structural restriction is normalized +// if it is a single union containing no interface terms, and is minimal in the +// sense that removing any term changes the set of types satisfying the +// constraint. It is left as a proof for the reader that, modulo sorting, there +// is exactly one such normalized form. +// +// Because the minimal representation always takes this form, _NormalTerms +// returns a slice of tilde terms corresponding to the terms of the union in +// the normalized structural restriction. An error is returned if the type is +// invalid, exceeds complexity bounds, or has an empty type set. In the latter +// case, _NormalTerms returns ErrEmptyTypeSet. +// +// _NormalTerms makes no guarantees about the order of terms, except that it +// is deterministic. +// +// This is a copy of x/exp/typeparams.NormalTerms which x/tools cannot depend on. +// TODO(taking): Remove this copy when possible. +func _NormalTerms(typ types.Type) ([]*typeparams.Term, error) { + switch typ := typ.(type) { + case *typeparams.TypeParam: + return typeparams.StructuralTerms(typ) + case *typeparams.Union: + return typeparams.UnionTermSet(typ) + case *types.Interface: + return typeparams.InterfaceTermSet(typ) + default: + return []*typeparams.Term{typeparams.NewTerm(false, typ)}, nil + } +} + +// typeSetOf returns the type set of typ. Returns an empty typeset on an error. +func typeSetOf(typ types.Type) typeSet { + terms, err := _NormalTerms(typ) + if err != nil { + return nil + } + return terms +} + +type typeSet []*typeparams.Term // type terms of the type set + +// underIs calls f with the underlying types of the specific type terms +// of s and reports whether all calls to f returned true. If there are +// no specific terms, underIs returns the result of f(nil). +func (s typeSet) underIs(f func(types.Type) bool) bool { + if len(s) == 0 { + return f(nil) + } + for _, t := range s { + u := t.Type().Underlying() + if !f(u) { + return false + } + } + return true +} + +// indexType returns the element type and index mode of a IndexExpr over a type. +// It returns (nil, invalid) if the type is not indexable; this should never occur in a well-typed program. +func indexType(typ types.Type) (types.Type, indexMode) { + switch U := typ.Underlying().(type) { + case *types.Array: + return U.Elem(), ixArrVar + case *types.Pointer: + if arr, ok := U.Elem().Underlying().(*types.Array); ok { + return arr.Elem(), ixVar + } + case *types.Slice: + return U.Elem(), ixVar + case *types.Map: + return U.Elem(), ixMap + case *types.Basic: + return tByte, ixValue // must be a string + case *types.Interface: + terms, err := _NormalTerms(U) + if len(terms) == 0 || err != nil { + return nil, ixInvalid // no underlying terms or error is empty. + } + + elem, mode := indexType(terms[0].Type()) + for i := 1; i < len(terms) && mode != ixInvalid; i++ { + e, m := indexType(terms[i].Type()) + if !types.Identical(elem, e) { // if type checked, just a sanity check + return nil, ixInvalid + } + // Update the mode to the most constrained address type. + mode = mode.meet(m) + } + if mode != ixInvalid { + return elem, mode + } + } + return nil, ixInvalid +} + +// An indexMode specifies the (addressing) mode of an index operand. +// +// Addressing mode of an index operation is based on the set of +// underlying types. +// Hasse diagram of the indexMode meet semi-lattice: +// +// ixVar ixMap +// | | +// ixArrVar | +// | | +// ixValue | +// \ / +// ixInvalid +type indexMode byte + +const ( + ixInvalid indexMode = iota // index is invalid + ixValue // index is a computed value (not addressable) + ixArrVar // like ixVar, but index operand contains an array + ixVar // index is an addressable variable + ixMap // index is a map index expression (acts like a variable on lhs, commaok on rhs of an assignment) +) + +// meet is the address type that is constrained by both x and y. +func (x indexMode) meet(y indexMode) indexMode { + if (x == ixMap || y == ixMap) && x != y { + return ixInvalid + } + // Use int representation and return min. + if x < y { + return y + } + return x +} diff --git a/go/ssa/coretype_test.go b/go/ssa/coretype_test.go new file mode 100644 index 00000000000..c4ed290fd85 --- /dev/null +++ b/go/ssa/coretype_test.go @@ -0,0 +1,105 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ssa + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/tools/internal/typeparams" +) + +func TestCoreType(t *testing.T) { + if !typeparams.Enabled { + t.Skip("TestCoreType requires type parameters.") + } + + const source = ` + package P + + type Named int + + type A any + type B interface{~int} + type C interface{int} + type D interface{Named} + type E interface{~int|interface{Named}} + type F interface{~int|~float32} + type G interface{chan int|interface{chan int}} + type H interface{chan int|chan float32} + type I interface{chan<- int|chan int} + type J interface{chan int|chan<- int} + type K interface{<-chan int|chan int} + type L interface{chan int|<-chan int} + type M interface{chan int|chan Named} + type N interface{<-chan int|chan<- int} + type O interface{chan int|bool} + type P struct{ Named } + type Q interface{ Foo() } + type R interface{ Foo() ; Named } + type S interface{ Foo() ; ~int } + + type T interface{chan int|interface{chan int}|<-chan 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) + } + + for _, test := range []struct { + expr string // type expression of Named type + want string // expected core type (or "" if none) + }{ + {"Named", "int"}, // Underlying type is not interface. + {"A", ""}, // Interface has no terms. + {"B", "int"}, // Tilde term. + {"C", "int"}, // Non-tilde term. + {"D", "int"}, // Named term. + {"E", "int"}, // Identical underlying types. + {"F", ""}, // Differing underlying types. + {"G", "chan int"}, // Identical Element types. + {"H", ""}, // Element type int has differing underlying type to float32. + {"I", "chan<- int"}, // SendRecv followed by SendOnly + {"J", "chan<- int"}, // SendOnly followed by SendRecv + {"K", "<-chan int"}, // RecvOnly followed by SendRecv + {"L", "<-chan int"}, // SendRecv followed by RecvOnly + {"M", ""}, // Element type int is not *identical* to Named. + {"N", ""}, // Differing channel directions + {"O", ""}, // A channel followed by a non-channel. + {"P", "struct{P.Named}"}, // Embedded type. + {"Q", ""}, // interface type with no terms and functions + {"R", "int"}, // interface type with both terms and functions. + {"S", "int"}, // interface type with a tilde term + {"T", "<-chan int"}, // Prefix of 2 terms that are identical before switching to channel. + } { + // Eval() expr for its type. + tv, err := types.Eval(fset, pkg, 0, test.expr) + if err != nil { + t.Fatalf("Eval(%s) failed: %v", test.expr, err) + } + + ct := coreType(tv.Type) + var got string + if ct == nil { + got = "" + } else { + got = ct.String() + } + if got != test.want { + t.Errorf("coreType(%s) = %v, want %v", test.expr, got, test.want) + } + } +} diff --git a/go/ssa/create.go b/go/ssa/create.go index 345d9acfbbd..ccb20e79683 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -91,37 +91,31 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) { } // Collect type parameters if this is a generic function/method. - var tparams []*typeparams.TypeParam - for i, rtparams := 0, typeparams.RecvTypeParams(sig); i < rtparams.Len(); i++ { - tparams = append(tparams, rtparams.At(i)) - } - for i, sigparams := 0, typeparams.ForSignature(sig); i < sigparams.Len(); i++ { - tparams = append(tparams, sigparams.At(i)) + var tparams *typeparams.TypeParamList + if rtparams := typeparams.RecvTypeParams(sig); rtparams.Len() > 0 { + tparams = rtparams + } else if sigparams := typeparams.ForSignature(sig); sigparams.Len() > 0 { + tparams = sigparams } fn := &Function{ - name: name, - object: obj, - Signature: sig, - syntax: syntax, - pos: obj.Pos(), - Pkg: pkg, - Prog: pkg.Prog, - _TypeParams: tparams, - info: pkg.info, + name: name, + object: obj, + Signature: sig, + syntax: syntax, + pos: obj.Pos(), + Pkg: pkg, + Prog: pkg.Prog, + typeparams: tparams, + info: pkg.info, } pkg.created.Add(fn) if syntax == nil { fn.Synthetic = "loaded from gc object file" } - if len(tparams) > 0 { + if tparams.Len() > 0 { fn.Prog.createInstanceSet(fn) } - if len(tparams) > 0 && syntax != nil { - fn.Synthetic = "generic function" - // TODO(taking): Allow for the function to be built once type params are supported. - fn.syntax = nil // Treating as an external function temporarily. - } pkg.objects[obj] = fn if sig.Recv() == nil { diff --git a/go/ssa/emit.go b/go/ssa/emit.go index f6537acc97f..b041491b6e8 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -121,9 +121,9 @@ func emitCompare(f *Function, op token.Token, x, y Value, pos token.Pos) Value { if types.Identical(xt, yt) { // no conversion necessary - } else if _, ok := xt.(*types.Interface); ok { + } else if isNonTypeParamInterface(x.Type()) { y = emitConv(f, y, x.Type()) - } else if _, ok := yt.(*types.Interface); ok { + } else if isNonTypeParamInterface(y.Type()) { x = emitConv(f, x, y.Type()) } else if _, ok := x.(*Const); ok { x = emitConv(f, x, y.Type()) @@ -166,6 +166,32 @@ func isValuePreserving(ut_src, ut_dst types.Type) bool { return false } +// isSliceToArrayPointer reports whether ut_src is a slice type +// that can be converted to a pointer to an array type ut_dst. +// Precondition: neither argument is a named type. +func isSliceToArrayPointer(ut_src, ut_dst types.Type) bool { + if slice, ok := ut_src.(*types.Slice); ok { + if ptr, ok := ut_dst.(*types.Pointer); ok { + if arr, ok := ptr.Elem().Underlying().(*types.Array); ok { + return types.Identical(slice.Elem(), arr.Elem()) + } + } + } + return false +} + +// isSliceToArray reports whether ut_src is a slice type +// that can be converted to an array type ut_dst. +// Precondition: neither argument is a named type. +func isSliceToArray(ut_src, ut_dst types.Type) bool { + if slice, ok := ut_src.(*types.Slice); ok { + if arr, ok := ut_dst.(*types.Array); ok { + return types.Identical(slice.Elem(), arr.Elem()) + } + } + return false +} + // emitConv emits to f code to convert Value val to exactly type typ, // and returns the converted value. Implicit conversions are required // by language assignability rules in assignments, parameter passing, @@ -180,17 +206,25 @@ func emitConv(f *Function, val Value, typ types.Type) Value { ut_dst := typ.Underlying() ut_src := t_src.Underlying() + dst_types := typeSetOf(ut_dst) + src_types := typeSetOf(ut_src) + // Just a change of type, but not value or representation? - if isValuePreserving(ut_src, ut_dst) { + preserving := src_types.underIs(func(s types.Type) bool { + return dst_types.underIs(func(d types.Type) bool { + return s != nil && d != nil && isValuePreserving(s, d) // all (s -> d) are value preserving. + }) + }) + if preserving { c := &ChangeType{X: val} c.setType(typ) return f.emit(c) } // Conversion to, or construction of a value of, an interface type? - if _, ok := ut_dst.(*types.Interface); ok { + if isNonTypeParamInterface(typ) { // Assignment from one interface type to another? - if _, ok := ut_src.(*types.Interface); ok { + if isNonTypeParamInterface(t_src) { c := &ChangeInterface{X: val} c.setType(typ) return f.emit(c) @@ -198,7 +232,7 @@ func emitConv(f *Function, val Value, typ types.Type) Value { // Untyped nil constant? Return interface-typed nil constant. if ut_src == tUntypedNil { - return nilConst(typ) + return zeroConst(typ) } // Convert (non-nil) "untyped" literals to their default type. @@ -213,7 +247,7 @@ func emitConv(f *Function, val Value, typ types.Type) Value { // Conversion of a compile-time constant value? if c, ok := val.(*Const); ok { - if _, ok := ut_dst.(*types.Basic); ok || c.IsNil() { + if isBasic(ut_dst) || c.Value == nil { // Conversion of a compile-time constant to // another constant type results in a new // constant of the destination type and @@ -227,41 +261,30 @@ func emitConv(f *Function, val Value, typ types.Type) Value { } // Conversion from slice to array pointer? - if slice, ok := ut_src.(*types.Slice); ok { - switch t := ut_dst.(type) { - case *types.Pointer: - ptr := t - if arr, ok := ptr.Elem().Underlying().(*types.Array); ok && types.Identical(slice.Elem(), arr.Elem()) { - c := &SliceToArrayPointer{X: val} - // TODO(taking): Check if this should be ut_dst or ptr. - c.setType(ptr) - return f.emit(c) - } - case *types.Array: - arr := t - if arr.Len() == 0 { - return zeroValue(f, arr) - } - if types.Identical(slice.Elem(), arr.Elem()) { - c := &SliceToArrayPointer{X: val} - c.setType(types.NewPointer(arr)) - x := f.emit(c) - unOp := &UnOp{ - Op: token.MUL, - X: x, - CommaOk: false, - } - unOp.setType(typ) - return f.emit(unOp) - } - } + slice2ptr := src_types.underIs(func(s types.Type) bool { + return dst_types.underIs(func(d types.Type) bool { + return s != nil && d != nil && isSliceToArrayPointer(s, d) // all (s->d) are slice to array pointer conversion. + }) + }) + if slice2ptr { + c := &SliceToArrayPointer{X: val} + c.setType(typ) + return f.emit(c) } + + // Conversion from slice to array? + slice2array := src_types.underIs(func(s types.Type) bool { + return dst_types.underIs(func(d types.Type) bool { + return s != nil && d != nil && isSliceToArray(s, d) // all (s->d) are slice to array conversion. + }) + }) + if slice2array { + return emitSliceToArray(f, val, typ) + } + // A representation-changing conversion? - // At least one of {ut_src,ut_dst} must be *Basic. - // (The other may be []byte or []rune.) - _, ok1 := ut_src.(*types.Basic) - _, ok2 := ut_dst.(*types.Basic) - if ok1 || ok2 { + // All of ut_src or ut_dst is basic, byte slice, or rune slice? + if isBasicConvTypes(src_types) || isBasicConvTypes(dst_types) { c := &Convert{X: val} c.setType(typ) return f.emit(c) @@ -270,6 +293,33 @@ func emitConv(f *Function, val Value, typ types.Type) Value { panic(fmt.Sprintf("in %s: cannot convert %s (%s) to %s", f, val, val.Type(), typ)) } +// emitTypeCoercion emits to f code to coerce the type of a +// Value v to exactly type typ, and returns the coerced value. +// +// Requires that coercing v.Typ() to typ is a value preserving change. +// +// Currently used only when v.Type() is a type instance of typ or vice versa. +// A type v is a type instance of a type t if there exists a +// type parameter substitution σ s.t. σ(v) == t. Example: +// +// σ(func(T) T) == func(int) int for σ == [T ↦ int] +// +// This happens in instantiation wrappers for conversion +// from an instantiation to a parameterized type (and vice versa) +// with σ substituting f.typeparams by f.typeargs. +func emitTypeCoercion(f *Function, v Value, typ types.Type) Value { + if types.Identical(v.Type(), typ) { + return v // no coercion needed + } + // TODO(taking): for instances should we record which side is the instance? + c := &ChangeType{ + X: v, + } + c.setType(typ) + f.emit(c) + return c +} + // emitStore emits to f an instruction to store value val at location // addr, applying implicit conversions as required by assignability rules. func emitStore(f *Function, addr, val Value, pos token.Pos) *Store { @@ -378,7 +428,7 @@ func emitTailCall(f *Function, call *Call) { // value of a field. func emitImplicitSelections(f *Function, v Value, indices []int, pos token.Pos) Value { for _, index := range indices { - fld := deref(v.Type()).Underlying().(*types.Struct).Field(index) + fld := coreType(deref(v.Type())).(*types.Struct).Field(index) if isPointer(v.Type()) { instr := &FieldAddr{ @@ -412,7 +462,7 @@ func emitImplicitSelections(f *Function, v Value, indices []int, pos token.Pos) // field's value. // Ident id is used for position and debug info. func emitFieldSelection(f *Function, v Value, index int, wantAddr bool, id *ast.Ident) Value { - fld := deref(v.Type()).Underlying().(*types.Struct).Field(index) + fld := coreType(deref(v.Type())).(*types.Struct).Field(index) if isPointer(v.Type()) { instr := &FieldAddr{ X: v, @@ -438,6 +488,48 @@ func emitFieldSelection(f *Function, v Value, index int, wantAddr bool, id *ast. return v } +// emitSliceToArray emits to f code to convert a slice value to an array value. +// +// Precondition: all types in type set of typ are arrays and convertible to all +// types in the type set of val.Type(). +func emitSliceToArray(f *Function, val Value, typ types.Type) Value { + // Emit the following: + // if val == nil && len(typ) == 0 { + // ptr = &[0]T{} + // } else { + // ptr = SliceToArrayPointer(val) + // } + // v = *ptr + + ptype := types.NewPointer(typ) + p := &SliceToArrayPointer{X: val} + p.setType(ptype) + ptr := f.emit(p) + + nilb := f.newBasicBlock("slicetoarray.nil") + nonnilb := f.newBasicBlock("slicetoarray.nonnil") + done := f.newBasicBlock("slicetoarray.done") + + cond := emitCompare(f, token.EQL, ptr, zeroConst(ptype), token.NoPos) + emitIf(f, cond, nilb, nonnilb) + f.currentBlock = nilb + + zero := f.addLocal(typ, token.NoPos) + emitJump(f, done) + f.currentBlock = nonnilb + + emitJump(f, done) + f.currentBlock = done + + phi := &Phi{Edges: []Value{zero, ptr}, Comment: "slicetoarray"} + phi.pos = val.Pos() + phi.setType(typ) + x := f.emit(phi) + unOp := &UnOp{Op: token.MUL, X: x} + unOp.setType(typ) + return f.emit(unOp) +} + // zeroValue emits to f code to produce a zero value of type t, // and returns it. func zeroValue(f *Function, t types.Type) Value { diff --git a/go/ssa/func.go b/go/ssa/func.go index c598ff836d3..57f5f718f73 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -251,7 +251,10 @@ func buildReferrers(f *Function) { } // mayNeedRuntimeTypes returns all of the types in the body of fn that might need runtime types. +// +// EXCLUSIVE_LOCKS_ACQUIRED(meth.Prog.methodsMu) func mayNeedRuntimeTypes(fn *Function) []types.Type { + // Collect all types that may need rtypes, i.e. those that flow into an interface. var ts []types.Type for _, bb := range fn.Blocks { for _, instr := range bb.Instrs { @@ -260,7 +263,21 @@ func mayNeedRuntimeTypes(fn *Function) []types.Type { } } } - return ts + + // Types that contain a parameterized type are considered to not be runtime types. + if fn.typeparams.Len() == 0 { + return ts // No potentially parameterized types. + } + // Filter parameterized types, in place. + fn.Prog.methodsMu.Lock() + defer fn.Prog.methodsMu.Unlock() + filtered := ts[:0] + for _, t := range ts { + if !fn.Prog.parameterized.isParameterized(t) { + filtered = append(filtered, t) + } + } + return filtered } // finishBody() finalizes the contents of the function after SSA code generation of its body. @@ -518,8 +535,8 @@ func (fn *Function) declaredPackage() *Package { switch { case fn.Pkg != nil: return fn.Pkg // non-generic function - case fn._Origin != nil: - return fn._Origin.Pkg // instance of a named generic function + case fn.topLevelOrigin != nil: + return fn.topLevelOrigin.Pkg // instance of a named generic function case fn.parent != nil: return fn.parent.declaredPackage() // instance of an anonymous [generic] function default: diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go index 049b53487d5..f73594bb41b 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go @@ -18,7 +18,7 @@ import ( // // This is an experimental interface! It may change without warning. func (prog *Program) _Instances(fn *Function) []*Function { - if len(fn._TypeParams) == 0 { + if fn.typeparams.Len() == 0 || len(fn.typeargs) > 0 { return nil } @@ -29,7 +29,7 @@ func (prog *Program) _Instances(fn *Function) []*Function { // A set of instantiations of a generic function fn. type instanceSet struct { - fn *Function // len(fn._TypeParams) > 0 and len(fn._TypeArgs) == 0. + fn *Function // fn.typeparams.Len() > 0 and len(fn.typeargs) == 0. instances map[*typeList]*Function // canonical type arguments to an instance. syntax *ast.FuncDecl // fn.syntax copy for instantiating after fn is done. nil on synthetic packages. info *types.Info // fn.pkg.info copy for building after fn is done.. nil on synthetic packages. @@ -56,7 +56,7 @@ func (insts *instanceSet) list() []*Function { // // EXCLUSIVE_LOCKS_ACQUIRED(prog.methodMu) func (prog *Program) createInstanceSet(fn *Function) { - assert(len(fn._TypeParams) > 0 && len(fn._TypeArgs) == 0, "Can only create instance sets for generic functions") + assert(fn.typeparams.Len() > 0 && len(fn.typeargs) == 0, "Can only create instance sets for generic functions") prog.methodsMu.Lock() defer prog.methodsMu.Unlock() @@ -73,7 +73,7 @@ func (prog *Program) createInstanceSet(fn *Function) { } } -// needsInstance returns an Function that that is the instantiation of fn with the type arguments targs. +// needsInstance returns a Function that is the instantiation of fn with the type arguments targs. // // Any CREATEd instance is added to cr. // @@ -82,41 +82,45 @@ func (prog *Program) needsInstance(fn *Function, targs []types.Type, cr *creator prog.methodsMu.Lock() defer prog.methodsMu.Unlock() - return prog.instances[fn].lookupOrCreate(targs, cr) + return prog.lookupOrCreateInstance(fn, targs, cr) +} + +// lookupOrCreateInstance returns a Function that is the instantiation of fn with the type arguments targs. +// +// Any CREATEd instance is added to cr. +// +// EXCLUSIVE_LOCKS_REQUIRED(prog.methodMu) +func (prog *Program) lookupOrCreateInstance(fn *Function, targs []types.Type, cr *creator) *Function { + return prog.instances[fn].lookupOrCreate(targs, &prog.parameterized, cr) } // lookupOrCreate returns the instantiation of insts.fn using targs. -// If the instantiation is reported, this is added to cr. -func (insts *instanceSet) lookupOrCreate(targs []types.Type, cr *creator) *Function { +// If the instantiation is created, this is added to cr. +func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWalker, cr *creator) *Function { if insts.instances == nil { insts.instances = make(map[*typeList]*Function) } + fn := insts.fn + prog := fn.Prog + // canonicalize on a tuple of targs. Sig is not unique. // // func A[T any]() { // var x T // fmt.Println("%T", x) // } - key := insts.fn.Prog.canon.List(targs) + key := prog.canon.List(targs) if inst, ok := insts.instances[key]; ok { return inst } + // CREATE instance/instantiation wrapper var syntax ast.Node if insts.syntax != nil { syntax = insts.syntax } - instance := createInstance(insts.fn, targs, insts.info, syntax, cr) - insts.instances[key] = instance - return instance -} -// createInstance returns an CREATEd instantiation of fn using targs. -// -// Function is added to cr. -func createInstance(fn *Function, targs []types.Type, info *types.Info, syntax ast.Node, cr *creator) *Function { - prog := fn.Prog var sig *types.Signature var obj *types.Func if recv := fn.Signature.Recv(); recv != nil { @@ -137,25 +141,36 @@ func createInstance(fn *Function, targs []types.Type, info *types.Info, syntax a sig = prog.canon.Type(instance).(*types.Signature) } + var synthetic string + var subst *subster + + concrete := !parameterized.anyParameterized(targs) + + if prog.mode&InstantiateGenerics != 0 && concrete { + synthetic = fmt.Sprintf("instance of %s", fn.Name()) + subst = makeSubster(prog.ctxt, fn.typeparams, targs, false) + } else { + synthetic = fmt.Sprintf("instantiation wrapper of %s", fn.Name()) + } + name := fmt.Sprintf("%s%s", fn.Name(), targs) // may not be unique - synthetic := fmt.Sprintf("instantiation of %s", fn.Name()) instance := &Function{ - name: name, - object: obj, - Signature: sig, - Synthetic: synthetic, - _Origin: fn, - pos: obj.Pos(), - Pkg: nil, - Prog: fn.Prog, - _TypeParams: fn._TypeParams, - _TypeArgs: targs, - info: info, // on synthetic packages info is nil. - subst: makeSubster(prog.ctxt, fn._TypeParams, targs, false), - } - if prog.mode&InstantiateGenerics != 0 { - instance.syntax = syntax // otherwise treat instance as an external function. + name: name, + object: obj, + Signature: sig, + Synthetic: synthetic, + syntax: syntax, + topLevelOrigin: fn, + pos: obj.Pos(), + Pkg: nil, + Prog: fn.Prog, + typeparams: fn.typeparams, // share with origin + typeargs: targs, + info: insts.info, // on synthetic packages info is nil. + subst: subst, } + cr.Add(instance) + insts.instances[key] = instance return instance } diff --git a/go/ssa/instantiate_test.go b/go/ssa/instantiate_test.go index 0da8c63042e..cd33e7e659e 100644 --- a/go/ssa/instantiate_test.go +++ b/go/ssa/instantiate_test.go @@ -4,19 +4,52 @@ package ssa -// Note: Tests use unexported functions. +// Note: Tests use unexported method _Instances. import ( "bytes" + "fmt" "go/types" "reflect" "sort" + "strings" "testing" "golang.org/x/tools/go/loader" "golang.org/x/tools/internal/typeparams" ) +// loadProgram creates loader.Program out of p. +func loadProgram(p string) (*loader.Program, error) { + // Parse + var conf loader.Config + f, err := conf.ParseFile("", p) + if err != nil { + return nil, fmt.Errorf("parse: %v", err) + } + conf.CreateFromFiles("p", f) + + // Load + lprog, err := conf.Load() + if err != nil { + return nil, fmt.Errorf("Load: %v", err) + } + return lprog, nil +} + +// buildPackage builds and returns ssa representation of package pkg of lprog. +func buildPackage(lprog *loader.Program, pkg string, mode BuilderMode) *Package { + prog := NewProgram(lprog.Fset, mode) + + for _, info := range lprog.AllPackages { + prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) + } + + p := prog.Package(lprog.Package(pkg).Pkg) + p.Build() + return p +} + // TestNeedsInstance ensures that new method instances can be created via needsInstance, // that TypeArgs are as expected, and can be accessed via _Instances. func TestNeedsInstance(t *testing.T) { @@ -45,30 +78,15 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) // func init func() // var init$guard bool - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) + lprog, err := loadProgram(input) + if err != err { + t.Fatal(err) } for _, mode := range []BuilderMode{BuilderMode(0), InstantiateGenerics} { // Create and build SSA - prog := NewProgram(lprog.Fset, mode) - - for _, info := range lprog.AllPackages { - prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) - } - - p := prog.Package(lprog.Package("p").Pkg) - p.Build() + p := buildPackage(lprog, "p", mode) + prog := p.Prog ptr := p.Type("Pointer").Type().(*types.Named) if ptr.NumMethods() != 1 { @@ -88,11 +106,11 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) if len(cr) != 1 { t.Errorf("Expected first instance to create a function. got %d created functions", len(cr)) } - if instance._Origin != meth { - t.Errorf("Expected Origin of %s to be %s. got %s", instance, meth, instance._Origin) + if instance.Origin() != meth { + t.Errorf("Expected Origin of %s to be %s. got %s", instance, meth, instance.Origin()) } - if len(instance._TypeArgs) != 1 || !types.Identical(instance._TypeArgs[0], intSliceTyp) { - t.Errorf("Expected TypeArgs of %s to be %v. got %v", instance, []types.Type{intSliceTyp}, instance._TypeArgs) + if len(instance.TypeArgs()) != 1 || !types.Identical(instance.TypeArgs()[0], intSliceTyp) { + t.Errorf("Expected TypeArgs of %s to be %v. got %v", instance, []types.Type{intSliceTyp}, instance.typeargs) } instances := prog._Instances(meth) if want := []*Function{instance}; !reflect.DeepEqual(instances, want) { @@ -126,3 +144,218 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) } } } + +// TestCallsToInstances checks that calles of calls to generic functions, +// without monomorphization, are wrappers around the origin generic function. +func TestCallsToInstances(t *testing.T) { + if !typeparams.Enabled { + return + } + const input = ` +package p + +type I interface { + Foo() +} + +type A int +func (a A) Foo() {} + +type J[T any] interface{ Bar() T } +type K[T any] struct{ J[T] } + +func Id[T any] (t T) T { + return t +} + +func Lambda[T I]() func() func(T) { + return func() func(T) { + return T.Foo + } +} + +func NoOp[T any]() {} + +func Bar[T interface { Foo(); ~int | ~string }, U any] (t T, u U) { + Id[U](u) + Id[T](t) +} + +func Make[T any]() interface{} { + NoOp[K[T]]() + return nil +} + +func entry(i int, a A) int { + Lambda[A]()()(a) + + x := Make[int]() + if j, ok := x.(interface{ Bar() int }); ok { + print(j) + } + + Bar[A, int](a, i) + + return Id[int](i) +} +` + lprog, err := loadProgram(input) + if err != err { + t.Fatal(err) + } + + p := buildPackage(lprog, "p", SanityCheckFunctions) + prog := p.Prog + + for _, ti := range []struct { + orig string + instance string + tparams string + targs string + chTypeInstrs int // number of ChangeType instructions in f's body + }{ + {"Id", "Id[int]", "[T]", "[int]", 2}, + {"Lambda", "Lambda[p.A]", "[T]", "[p.A]", 1}, + {"Make", "Make[int]", "[T]", "[int]", 0}, + {"NoOp", "NoOp[p.K[T]]", "[T]", "[p.K[T]]", 0}, + } { + test := ti + t.Run(test.instance, func(t *testing.T) { + f := p.Members[test.orig].(*Function) + if f == nil { + t.Fatalf("origin function not found") + } + + i := instanceOf(f, test.instance, prog) + if i == nil { + t.Fatalf("instance not found") + } + + // for logging on failures + var body strings.Builder + i.WriteTo(&body) + t.Log(body.String()) + + if len(i.Blocks) != 1 { + t.Fatalf("body has more than 1 block") + } + + if instrs := changeTypeInstrs(i.Blocks[0]); instrs != test.chTypeInstrs { + t.Errorf("want %v instructions; got %v", test.chTypeInstrs, instrs) + } + + if test.tparams != tparams(i) { + t.Errorf("want %v type params; got %v", test.tparams, tparams(i)) + } + + if test.targs != targs(i) { + t.Errorf("want %v type arguments; got %v", test.targs, targs(i)) + } + }) + } +} + +func instanceOf(f *Function, name string, prog *Program) *Function { + for _, i := range prog._Instances(f) { + if i.Name() == name { + return i + } + } + return nil +} + +func tparams(f *Function) string { + tplist := f.TypeParams() + var tps []string + for i := 0; i < tplist.Len(); i++ { + tps = append(tps, tplist.At(i).String()) + } + return fmt.Sprint(tps) +} + +func targs(f *Function) string { + var tas []string + for _, ta := range f.TypeArgs() { + tas = append(tas, ta.String()) + } + return fmt.Sprint(tas) +} + +func changeTypeInstrs(b *BasicBlock) int { + cnt := 0 + for _, i := range b.Instrs { + if _, ok := i.(*ChangeType); ok { + cnt++ + } + } + return cnt +} + +func TestInstanceUniqueness(t *testing.T) { + if !typeparams.Enabled { + return + } + const input = ` +package p + +func H[T any](t T) { + print(t) +} + +func F[T any](t T) { + H[T](t) + H[T](t) + H[T](t) +} + +func G[T any](t T) { + H[T](t) + H[T](t) +} + +func Foo[T any, S any](t T, s S) { + Foo[S, T](s, t) + Foo[T, S](t, s) +} +` + lprog, err := loadProgram(input) + if err != err { + t.Fatal(err) + } + + p := buildPackage(lprog, "p", SanityCheckFunctions) + prog := p.Prog + + for _, test := range []struct { + orig string + instances string + }{ + {"H", "[p.H[T] p.H[T]]"}, + {"Foo", "[p.Foo[S T] p.Foo[T S]]"}, + } { + t.Run(test.orig, func(t *testing.T) { + f := p.Members[test.orig].(*Function) + if f == nil { + t.Fatalf("origin function not found") + } + + instances := prog._Instances(f) + sort.Slice(instances, func(i, j int) bool { return instances[i].Name() < instances[j].Name() }) + + if got := fmt.Sprintf("%v", instances); !reflect.DeepEqual(got, test.instances) { + t.Errorf("got %v instances, want %v", got, test.instances) + } + }) + } +} + +// instancesStr returns a sorted slice of string +// representation of instances. +func instancesStr(instances []*Function) []string { + var is []string + for _, i := range instances { + is = append(is, fmt.Sprintf("%v", i)) + } + sort.Strings(is) + return is +} diff --git a/go/ssa/interp/interp.go b/go/ssa/interp/interp.go index 2b21aad708b..58cac464241 100644 --- a/go/ssa/interp/interp.go +++ b/go/ssa/interp/interp.go @@ -51,7 +51,6 @@ import ( "os" "reflect" "runtime" - "strings" "sync/atomic" "golang.org/x/tools/go/ssa" @@ -335,7 +334,17 @@ func visitInstr(fr *frame, instr ssa.Instruction) continuation { } case *ssa.Index: - fr.env[instr] = fr.get(instr.X).(array)[asInt64(fr.get(instr.Index))] + x := fr.get(instr.X) + idx := fr.get(instr.Index) + + switch x := x.(type) { + case array: + fr.env[instr] = x[asInt64(idx)] + case string: + fr.env[instr] = x[asInt64(idx)] + default: + panic(fmt.Sprintf("unexpected x type in Index: %T", x)) + } case *ssa.Lookup: fr.env[instr] = lookup(instr, fr.get(instr.X), fr.get(instr.Index)) @@ -506,13 +515,15 @@ func callSSA(i *interpreter, caller *frame, callpos token.Pos, fn *ssa.Function, return ext(fr, args) } if fn.Blocks == nil { - var reason string // empty by default - if strings.HasPrefix(fn.Synthetic, "instantiation") { - reason = " (interp requires ssa.BuilderMode to include InstantiateGenerics on generics)" - } - panic("no code for function: " + name + reason) + panic("no code for function: " + name) } } + + // generic function body? + if fn.TypeParams().Len() > 0 && len(fn.TypeArgs()) == 0 { + panic("interp requires ssa.BuilderMode to include InstantiateGenerics to execute generics") + } + fr.env = make(map[ssa.Value]value) fr.block = fn.Blocks[0] fr.locals = make([]value, len(fn.Locals)) diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go index 51a74015c95..c893d83e753 100644 --- a/go/ssa/interp/interp_test.go +++ b/go/ssa/interp/interp_test.go @@ -127,12 +127,14 @@ var testdataTests = []string{ "width32.go", "fixedbugs/issue52342.go", - "fixedbugs/issue55086.go", } func init() { if typeparams.Enabled { testdataTests = append(testdataTests, "fixedbugs/issue52835.go") + testdataTests = append(testdataTests, "fixedbugs/issue55086.go") + testdataTests = append(testdataTests, "typeassert.go") + testdataTests = append(testdataTests, "zeros.go") } } diff --git a/go/ssa/interp/ops.go b/go/ssa/interp/ops.go index 8f031384f03..188899d69f3 100644 --- a/go/ssa/interp/ops.go +++ b/go/ssa/interp/ops.go @@ -34,9 +34,10 @@ type exitPanic int // constValue returns the value of the constant with the // dynamic type tag appropriate for c.Type(). func constValue(c *ssa.Const) value { - if c.IsNil() { - return zero(c.Type()) // typed nil + if c.Value == nil { + return zero(c.Type()) // typed zero } + // c is not a type parameter so it's underlying type is basic. if t, ok := c.Type().Underlying().(*types.Basic); ok { // TODO(adonovan): eliminate untyped constants from SSA form. @@ -307,7 +308,7 @@ func slice(x, lo, hi, max value) value { panic(fmt.Sprintf("slice: unexpected X type: %T", x)) } -// lookup returns x[idx] where x is a map or string. +// lookup returns x[idx] where x is a map. func lookup(instr *ssa.Lookup, x, idx value) value { switch x := x.(type) { // map or string case map[value]value, *hashmap: @@ -327,8 +328,6 @@ func lookup(instr *ssa.Lookup, x, idx value) value { v = tuple{v, ok} } return v - case string: - return x[asInt64(idx)] } panic(fmt.Sprintf("unexpected x type in Lookup: %T", x)) } diff --git a/go/ssa/interp/testdata/boundmeth.go b/go/ssa/interp/testdata/boundmeth.go index 69937f9d3c7..47b94068591 100644 --- a/go/ssa/interp/testdata/boundmeth.go +++ b/go/ssa/interp/testdata/boundmeth.go @@ -123,7 +123,8 @@ func nilInterfaceMethodValue() { r := fmt.Sprint(recover()) // runtime panic string varies across toolchains if r != "interface conversion: interface is nil, not error" && - r != "runtime error: invalid memory address or nil pointer dereference" { + r != "runtime error: invalid memory address or nil pointer dereference" && + r != "method value: interface is nil" { panic("want runtime panic from nil interface method value, got " + r) } }() diff --git a/go/ssa/interp/testdata/slice2array.go b/go/ssa/interp/testdata/slice2array.go index 43c0543eabf..84e6b733008 100644 --- a/go/ssa/interp/testdata/slice2array.go +++ b/go/ssa/interp/testdata/slice2array.go @@ -19,7 +19,7 @@ func main() { { var s []int - a:= ([0]int)(s) + a := ([0]int)(s) if a != [0]int{} { panic("zero len array is not equal") } @@ -31,6 +31,20 @@ func main() { if !threeToFourDoesPanic() { panic("panic expected from threeToFourDoesPanic()") } + + if !fourPanicsWhileOneDoesNot[[4]int]() { + panic("panic expected from fourPanicsWhileOneDoesNot[[4]int]()") + } + if fourPanicsWhileOneDoesNot[[1]int]() { + panic("no panic expected from fourPanicsWhileOneDoesNot[[1]int]()") + } + + if !fourPanicsWhileZeroDoesNot[[4]int]() { + panic("panic expected from fourPanicsWhileZeroDoesNot[[4]int]()") + } + if fourPanicsWhileZeroDoesNot[[0]int]() { + panic("no panic expected from fourPanicsWhileZeroDoesNot[[0]int]()") + } } func emptyToEmptyDoesNotPanic() (raised bool) { @@ -53,4 +67,26 @@ func threeToFourDoesPanic() (raised bool) { s := make([]int, 3, 5) _ = ([4]int)(s) return false -} \ No newline at end of file +} + +func fourPanicsWhileOneDoesNot[T [1]int | [4]int]() (raised bool) { + defer func() { + if e := recover(); e != nil { + raised = true + } + }() + s := make([]int, 3, 5) + _ = T(s) + return false +} + +func fourPanicsWhileZeroDoesNot[T [0]int | [4]int]() (raised bool) { + defer func() { + if e := recover(); e != nil { + raised = true + } + }() + var s []int + _ = T(s) + return false +} diff --git a/go/ssa/interp/testdata/typeassert.go b/go/ssa/interp/testdata/typeassert.go new file mode 100644 index 00000000000..792a7558f61 --- /dev/null +++ b/go/ssa/interp/testdata/typeassert.go @@ -0,0 +1,32 @@ +// Tests of type asserts. +// Requires type parameters. +package typeassert + +type fooer interface{ foo() string } + +type X int + +func (_ X) foo() string { return "x" } + +func f[T fooer](x T) func() string { + return x.foo +} + +func main() { + if f[X](0)() != "x" { + panic("f[X]() != 'x'") + } + + p := false + func() { + defer func() { + if recover() != nil { + p = true + } + }() + f[fooer](nil) // panics on x.foo when T is an interface and nil. + }() + if !p { + panic("f[fooer] did not panic") + } +} diff --git a/go/ssa/interp/testdata/zeros.go b/go/ssa/interp/testdata/zeros.go new file mode 100644 index 00000000000..509c78a36ec --- /dev/null +++ b/go/ssa/interp/testdata/zeros.go @@ -0,0 +1,45 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Test interpretation on zero values with type params. +package zeros + +func assert(cond bool, msg string) { + if !cond { + panic(msg) + } +} + +func tp0[T int | string | float64]() T { return T(0) } + +func tpFalse[T ~bool]() T { return T(false) } + +func tpEmptyString[T string | []byte]() T { return T("") } + +func tpNil[T *int | []byte]() T { return T(nil) } + +func main() { + // zero values + var zi int + var zf float64 + var zs string + + assert(zi == int(0), "zero value of int is int(0)") + assert(zf == float64(0), "zero value of float64 is float64(0)") + assert(zs != string(0), "zero value of string is not string(0)") + + assert(zi == tp0[int](), "zero value of int is int(0)") + assert(zf == tp0[float64](), "zero value of float64 is float64(0)") + assert(zs != tp0[string](), "zero value of string is not string(0)") + + assert(zf == -0.0, "constant -0.0 is converted to 0.0") + + assert(!tpFalse[bool](), "zero value of bool is false") + + assert(tpEmptyString[string]() == zs, `zero value of string is string("")`) + assert(len(tpEmptyString[[]byte]()) == 0, `[]byte("") is empty`) + + assert(tpNil[*int]() == nil, "nil is nil") + assert(tpNil[[]byte]() == nil, "nil is nil") +} diff --git a/go/ssa/lift.go b/go/ssa/lift.go index c350481db76..945536bbbf4 100644 --- a/go/ssa/lift.go +++ b/go/ssa/lift.go @@ -44,6 +44,8 @@ import ( "go/types" "math/big" "os" + + "golang.org/x/tools/internal/typeparams" ) // If true, show diagnostic information at each step of lifting. @@ -381,10 +383,9 @@ type newPhiMap map[*BasicBlock][]newPhi // // fresh is a source of fresh ids for phi nodes. func liftAlloc(df domFrontier, alloc *Alloc, newPhis newPhiMap, fresh *int) bool { - // Don't lift aggregates into registers, because we don't have - // a way to express their zero-constants. + // TODO(taking): zero constants of aggregated types can now be lifted. switch deref(alloc.Type()).Underlying().(type) { - case *types.Array, *types.Struct: + case *types.Array, *types.Struct, *typeparams.TypeParam: return false } diff --git a/go/ssa/lvalue.go b/go/ssa/lvalue.go index 455b1e50fa4..51122b8e85e 100644 --- a/go/ssa/lvalue.go +++ b/go/ssa/lvalue.go @@ -56,12 +56,12 @@ func (a *address) typ() types.Type { } // An element is an lvalue represented by m[k], the location of an -// element of a map or string. These locations are not addressable +// element of a map. These locations are not addressable // since pointers cannot be formed from them, but they do support -// load(), and in the case of maps, store(). +// load() and store(). type element struct { - m, k Value // map or string - t types.Type // map element type or string byte type + m, k Value // map + t types.Type // map element type pos token.Pos // source position of colon ({k:v}) or lbrack (m[k]=v) } @@ -86,7 +86,7 @@ func (e *element) store(fn *Function, v Value) { } func (e *element) address(fn *Function) Value { - panic("map/string elements are not addressable") + panic("map elements are not addressable") } func (e *element) typ() types.Type { diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 6954e17b772..4185618cdd6 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -27,8 +27,8 @@ func (prog *Program) MethodValue(sel *types.Selection) *Function { panic(fmt.Sprintf("MethodValue(%s) kind != MethodVal", sel)) } T := sel.Recv() - if isInterface(T) { - return nil // abstract method (interface) + if types.IsInterface(T) { + return nil // abstract method (interface, possibly type param) } if prog.mode&LogSource != 0 { defer logStack("MethodValue %s %v", T, sel)() @@ -76,7 +76,7 @@ type methodSet struct { // EXCLUSIVE_LOCKS_REQUIRED(prog.methodsMu) func (prog *Program) createMethodSet(T types.Type) *methodSet { if prog.mode&SanityCheckFunctions != 0 { - if isInterface(T) || prog.parameterized.isParameterized(T) { + if types.IsInterface(T) || prog.parameterized.isParameterized(T) { panic("type is interface or parameterized") } } @@ -107,9 +107,9 @@ func (prog *Program) addMethod(mset *methodSet, sel *types.Selection, cr *creato fn = makeWrapper(prog, sel, cr) } else { fn = prog.originFunc(obj) - if len(fn._TypeParams) > 0 { // instantiate + if fn.typeparams.Len() > 0 { // instantiate targs := receiverTypeArgs(obj) - fn = prog.instances[fn].lookupOrCreate(targs, cr) + fn = prog.lookupOrCreateInstance(fn, targs, cr) } } if fn.Signature.Recv() == nil { @@ -190,7 +190,7 @@ func (prog *Program) needMethods(T types.Type, skip bool, cr *creator) { tmset := prog.MethodSets.MethodSet(T) - if !skip && !isInterface(T) && tmset.Len() > 0 { + if !skip && !types.IsInterface(T) && tmset.Len() > 0 { // Create methods of T. mset := prog.createMethodSet(T) if !mset.complete { diff --git a/go/ssa/parameterized.go b/go/ssa/parameterized.go index 956718cd723..b11413c8184 100644 --- a/go/ssa/parameterized.go +++ b/go/ssa/parameterized.go @@ -111,3 +111,12 @@ func (w *tpWalker) isParameterized(typ types.Type) (res bool) { return false } + +func (w *tpWalker) anyParameterized(ts []types.Type) bool { + for _, t := range ts { + if w.isParameterized(t) { + return true + } + } + return false +} diff --git a/go/ssa/print.go b/go/ssa/print.go index b8e53923a17..9aa6809789e 100644 --- a/go/ssa/print.go +++ b/go/ssa/print.go @@ -232,7 +232,7 @@ func (v *MakeChan) String() string { } func (v *FieldAddr) String() string { - st := deref(v.X.Type()).Underlying().(*types.Struct) + st := coreType(deref(v.X.Type())).(*types.Struct) // Be robust against a bad index. name := "?" if 0 <= v.Field && v.Field < st.NumFields() { @@ -242,7 +242,7 @@ func (v *FieldAddr) String() string { } func (v *Field) String() string { - st := v.X.Type().Underlying().(*types.Struct) + st := coreType(v.X.Type()).(*types.Struct) // Be robust against a bad index. name := "?" if 0 <= v.Field && v.Field < st.NumFields() { diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 7d71302756e..3fb3f394e87 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -132,9 +132,9 @@ func (s *sanity) checkInstr(idx int, instr Instruction) { case *ChangeType: case *SliceToArrayPointer: case *Convert: - if _, ok := instr.X.Type().Underlying().(*types.Basic); !ok { - if _, ok := instr.Type().Underlying().(*types.Basic); !ok { - s.errorf("convert %s -> %s: at least one type must be basic", instr.X.Type(), instr.Type()) + if from := instr.X.Type(); !isBasicConvTypes(typeSetOf(from)) { + if to := instr.Type(); !isBasicConvTypes(typeSetOf(to)) { + s.errorf("convert %s -> %s: at least one type must be basic (or all basic, []byte, or []rune)", from, to) } } @@ -403,7 +403,7 @@ func (s *sanity) checkFunction(fn *Function) bool { // - check transient fields are nil // - warn if any fn.Locals do not appear among block instructions. - // TODO(taking): Sanity check _Origin, _TypeParams, and _TypeArgs. + // TODO(taking): Sanity check origin, typeparams, and typeargs. s.fn = fn if fn.Prog == nil { s.errorf("nil Prog") @@ -420,16 +420,19 @@ func (s *sanity) checkFunction(fn *Function) bool { strings.HasPrefix(fn.Synthetic, "bound ") || strings.HasPrefix(fn.Synthetic, "thunk ") || strings.HasSuffix(fn.name, "Error") || - strings.HasPrefix(fn.Synthetic, "instantiation") || - (fn.parent != nil && len(fn._TypeArgs) > 0) /* anon fun in instance */ { + strings.HasPrefix(fn.Synthetic, "instance ") || + strings.HasPrefix(fn.Synthetic, "instantiation ") || + (fn.parent != nil && len(fn.typeargs) > 0) /* anon fun in instance */ { // ok } else { s.errorf("nil Pkg") } } if src, syn := fn.Synthetic == "", fn.Syntax() != nil; src != syn { - if strings.HasPrefix(fn.Synthetic, "instantiation") && fn.Prog.mode&InstantiateGenerics != 0 { - // ok + if len(fn.typeargs) > 0 && fn.Prog.mode&InstantiateGenerics != 0 { + // ok (instantiation with InstantiateGenerics on) + } else if fn.topLevelOrigin != nil && len(fn.typeargs) > 0 { + // ok (we always have the syntax set for instantiation) } else { s.errorf("got fromSource=%t, hasSyntax=%t; want same values", src, syn) } @@ -494,6 +497,9 @@ func (s *sanity) checkFunction(fn *Function) bool { if anon.Parent() != fn { s.errorf("AnonFuncs[%d]=%s but %s.Parent()=%s", i, anon, anon, anon.Parent()) } + if i != int(anon.anonIdx) { + s.errorf("AnonFuncs[%d]=%s but %s.anonIdx=%d", i, anon, anon, anon.anonIdx) + } } s.fn = nil return !s.insane diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index cbc638c81a8..698cb16507f 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -294,16 +294,15 @@ type Node interface { // // Type() returns the function's Signature. // -// A function is generic iff it has a non-empty TypeParams list and an -// empty TypeArgs list. TypeParams lists the type parameters of the -// function's Signature or the receiver's type parameters for a method. -// -// The instantiation of a generic function is a concrete function. These -// are a list of n>0 TypeParams and n TypeArgs. An instantiation will -// have a generic Origin function. There is at most one instantiation -// of each origin type per Identical() type list. Instantiations do not -// belong to any Pkg. The generic function and the instantiations will -// share the same source Pos for the functions and the instructions. +// A generic function is a function or method that has uninstantiated type +// parameters (TypeParams() != nil). Consider a hypothetical generic +// method, (*Map[K,V]).Get. It may be instantiated with all ground +// (non-parameterized) types as (*Map[string,int]).Get or with +// parameterized types as (*Map[string,U]).Get, where U is a type parameter. +// In both instantiations, Origin() refers to the instantiated generic +// method, (*Map[K,V]).Get, TypeParams() refers to the parameters [K,V] of +// the generic method. TypeArgs() refers to [string,U] or [string,int], +// respectively, and is nil in the generic method. type Function struct { name string object types.Object // a declared *types.Func or one of its wrappers @@ -324,10 +323,11 @@ type Function struct { AnonFuncs []*Function // anonymous functions directly beneath this one referrers []Instruction // referring instructions (iff Parent() != nil) built bool // function has completed both CREATE and BUILD phase. + anonIdx int32 // position of a nested function in parent's AnonFuncs. fn.Parent()!=nil => fn.Parent().AnonFunc[fn.anonIdx] == fn. - _Origin *Function // the origin function if this the instantiation of a generic function. nil if Parent() != nil. - _TypeParams []*typeparams.TypeParam // the type paramaters of this function. len(TypeParams) == len(_TypeArgs) => runtime function - _TypeArgs []types.Type // type arguments for for an instantiation. len(_TypeArgs) != 0 => instantiation + typeparams *typeparams.TypeParamList // type parameters of this function. typeparams.Len() > 0 => generic or instance of generic function + typeargs []types.Type // type arguments that instantiated typeparams. len(typeargs) > 0 => instance of generic function + topLevelOrigin *Function // the origin function if this is an instance of a source function. nil if Parent()!=nil. // The following fields are set transiently during building, // then cleared. @@ -337,7 +337,7 @@ type Function struct { targets *targets // linked stack of branch targets lblocks map[types.Object]*lblock // labelled blocks info *types.Info // *types.Info to build from. nil for wrappers. - subst *subster // type substitution cache + subst *subster // non-nil => expand generic body using this type substitution of ground types } // BasicBlock represents an SSA basic block. @@ -409,26 +409,28 @@ type Parameter struct { referrers []Instruction } -// A Const represents the value of a constant expression. +// A Const represents a value known at build time. // -// The underlying type of a constant may be any boolean, numeric, or -// string type. In addition, a Const may represent the nil value of -// any reference type---interface, map, channel, pointer, slice, or -// function---but not "untyped nil". +// Consts include true constants of boolean, numeric, and string types, as +// defined by the Go spec; these are represented by a non-nil Value field. // -// All source-level constant expressions are represented by a Const -// of the same type and value. -// -// Value holds the value of the constant, independent of its Type(), -// using go/constant representation, or nil for a typed nil value. +// Consts also include the "zero" value of any type, of which the nil values +// of various pointer-like types are a special case; these are represented +// by a nil Value field. // // Pos() returns token.NoPos. // -// Example printed form: -// -// 42:int -// "hello":untyped string -// 3+4i:MyComplex +// Example printed forms: +// +// 42:int +// "hello":untyped string +// 3+4i:MyComplex +// nil:*int +// nil:[]string +// [3]int{}:[3]int +// struct{x string}{}:struct{x string} +// 0:interface{int|int64} +// nil:interface{bool|int} // no go/constant representation type Const struct { typ types.Type Value constant.Value @@ -603,9 +605,17 @@ type UnOp struct { // - between (possibly named) pointers to identical base types. // - from a bidirectional channel to a read- or write-channel, // optionally adding/removing a name. +// - between a type (t) and an instance of the type (tσ), i.e. +// Type() == σ(X.Type()) (or X.Type()== σ(Type())) where +// σ is the type substitution of Parent().TypeParams by +// Parent().TypeArgs. // // This operation cannot fail dynamically. // +// Type changes may to be to or from a type parameter (or both). All +// types in the type set of X.Type() have a value-preserving type +// change to all types in the type set of Type(). +// // Pos() returns the ast.CallExpr.Lparen, if the instruction arose // from an explicit conversion in the source. // @@ -631,6 +641,10 @@ type ChangeType struct { // // A conversion may imply a type name change also. // +// Conversions may to be to or from a type parameter. All types in +// the type set of X.Type() can be converted to all types in the type +// set of Type(). +// // This operation cannot fail dynamically. // // Conversions of untyped string/number/bool constants to a specific @@ -670,6 +684,11 @@ type ChangeInterface struct { // Pos() returns the ast.CallExpr.Lparen, if the instruction arose // from an explicit conversion in the source. // +// Conversion may to be to or from a type parameter. All types in +// the type set of X.Type() must be a slice types that can be converted to +// all types in the type set of Type() which must all be pointer to array +// types. +// // Example printed form: // // t1 = slice to array pointer *[4]byte <- []byte (t0) @@ -809,7 +828,9 @@ type Slice struct { // // Pos() returns the position of the ast.SelectorExpr.Sel for the // field, if explicit in the source. For implicit selections, returns -// the position of the inducing explicit selection. +// the position of the inducing explicit selection. If produced for a +// struct literal S{f: e}, it returns the position of the colon; for +// S{e} it returns the start of expression e. // // Example printed form: // @@ -817,7 +838,7 @@ type Slice struct { type FieldAddr struct { register X Value // *struct - Field int // field is X.Type().Underlying().(*types.Pointer).Elem().Underlying().(*types.Struct).Field(Field) + Field int // field is typeparams.CoreType(X.Type().Underlying().(*types.Pointer).Elem()).(*types.Struct).Field(Field) } // The Field instruction yields the Field of struct X. @@ -836,14 +857,14 @@ type FieldAddr struct { type Field struct { register X Value // struct - Field int // index into X.Type().(*types.Struct).Fields + Field int // index into typeparams.CoreType(X.Type()).(*types.Struct).Fields } // The IndexAddr instruction yields the address of the element at // index Index of collection X. Index is an integer expression. // -// The elements of maps and strings are not addressable; use Lookup or -// MapUpdate instead. +// The elements of maps and strings are not addressable; use Lookup (map), +// Index (string), or MapUpdate instead. // // Dynamically, this instruction panics if X evaluates to a nil *array // pointer. @@ -858,11 +879,13 @@ type Field struct { // t2 = &t0[t1] type IndexAddr struct { register - X Value // slice or *array, + X Value // *array, slice or type parameter with types array, *array, or slice. Index Value // numeric index } -// The Index instruction yields element Index of array X. +// The Index instruction yields element Index of collection X, an array, +// string or type parameter containing an array, a string, a pointer to an, +// array or a slice. // // Pos() returns the ast.IndexExpr.Lbrack for the index operation, if // explicit in the source. @@ -872,13 +895,12 @@ type IndexAddr struct { // t2 = t0[t1] type Index struct { register - X Value // array + X Value // array, string or type parameter with types array, *array, slice, or string. Index Value // integer index } -// The Lookup instruction yields element Index of collection X, a map -// or string. Index is an integer expression if X is a string or the -// appropriate key type if X is a map. +// The Lookup instruction yields element Index of collection map X. +// Index is the appropriate key type. // // If CommaOk, the result is a 2-tuple of the value above and a // boolean indicating the result of a map membership test for the key. @@ -892,8 +914,8 @@ type Index struct { // t5 = t3[t4],ok type Lookup struct { register - X Value // string or map - Index Value // numeric or key-typed index + X Value // map + Index Value // key-typed index CommaOk bool // return a value,ok pair } @@ -1337,9 +1359,10 @@ type anInstruction struct { // 2. "invoke" mode: when Method is non-nil (IsInvoke), a CallCommon // represents a dynamically dispatched call to an interface method. // In this mode, Value is the interface value and Method is the -// interface's abstract method. Note: an abstract method may be -// shared by multiple interfaces due to embedding; Value.Type() -// provides the specific interface used for this call. +// interface's abstract method. The interface value may be a type +// parameter. Note: an abstract method may be shared by multiple +// interfaces due to embedding; Value.Type() provides the specific +// interface used for this call. // // Value is implicitly supplied to the concrete method implementation // as the receiver parameter; in other words, Args[0] holds not the @@ -1378,7 +1401,7 @@ func (c *CallCommon) Signature() *types.Signature { if c.Method != nil { return c.Method.Type().(*types.Signature) } - return c.Value.Type().Underlying().(*types.Signature) + return coreType(c.Value.Type()).(*types.Signature) } // StaticCallee returns the callee if this is a trivially static @@ -1469,6 +1492,29 @@ func (v *Function) Referrers() *[]Instruction { return nil } +// TypeParams are the function's type parameters if generic or the +// type parameters that were instantiated if fn is an instantiation. +// +// TODO(taking): declare result type as *types.TypeParamList +// after we drop support for go1.17. +func (fn *Function) TypeParams() *typeparams.TypeParamList { + return fn.typeparams +} + +// TypeArgs are the types that TypeParams() were instantiated by to create fn +// from fn.Origin(). +func (fn *Function) TypeArgs() []types.Type { return fn.typeargs } + +// Origin is the function fn is an instantiation of. Returns nil if fn is not +// an instantiation. +func (fn *Function) Origin() *Function { + if fn.parent != nil && len(fn.typeargs) > 0 { + // Nested functions are BUILT at a different time than there instances. + return fn.parent.Origin().AnonFuncs[fn.anonIdx] + } + return fn.topLevelOrigin +} + func (v *Parameter) Type() types.Type { return v.typ } func (v *Parameter) Name() string { return v.name } func (v *Parameter) Object() types.Object { return v.object } diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go index 7e02f97a7ed..8b9f4238da8 100644 --- a/go/ssa/stdlib_test.go +++ b/go/ssa/stdlib_test.go @@ -21,12 +21,10 @@ import ( "testing" "time" - "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/typeparams/genericfeatures" ) func bytesAllocated() uint64 { @@ -51,22 +49,6 @@ func TestStdlib(t *testing.T) { if err != nil { t.Fatal(err) } - var nonGeneric int - for i := 0; i < len(pkgs); i++ { - pkg := pkgs[i] - inspect := inspector.New(pkg.Syntax) - features := genericfeatures.ForPackage(inspect, pkg.TypesInfo) - // Skip standard library packages that use generics. This won't be - // sufficient if any standard library packages start _importing_ packages - // that use generics. - if features != 0 { - t.Logf("skipping package %q which uses generics", pkg.PkgPath) - continue - } - pkgs[nonGeneric] = pkg - nonGeneric++ - } - pkgs = pkgs[:nonGeneric] t1 := time.Now() alloc1 := bytesAllocated() diff --git a/go/ssa/subst.go b/go/ssa/subst.go index b29130ea0cb..396626befca 100644 --- a/go/ssa/subst.go +++ b/go/ssa/subst.go @@ -18,6 +18,8 @@ import ( // // Not concurrency-safe. type subster struct { + // TODO(zpavlinovic): replacements can contain type params + // when generating instances inside of a generic function body. replacements map[*typeparams.TypeParam]types.Type // values should contain no type params cache map[types.Type]types.Type // cache of subst results ctxt *typeparams.Context @@ -27,17 +29,17 @@ type subster struct { // Returns a subster that replaces tparams[i] with targs[i]. Uses ctxt as a cache. // targs should not contain any types in tparams. -func makeSubster(ctxt *typeparams.Context, tparams []*typeparams.TypeParam, targs []types.Type, debug bool) *subster { - assert(len(tparams) == len(targs), "makeSubster argument count must match") +func makeSubster(ctxt *typeparams.Context, tparams *typeparams.TypeParamList, targs []types.Type, debug bool) *subster { + assert(tparams.Len() == len(targs), "makeSubster argument count must match") subst := &subster{ - replacements: make(map[*typeparams.TypeParam]types.Type, len(tparams)), + replacements: make(map[*typeparams.TypeParam]types.Type, tparams.Len()), cache: make(map[types.Type]types.Type), ctxt: ctxt, debug: debug, } - for i, tpar := range tparams { - subst.replacements[tpar] = targs[i] + for i := 0; i < tparams.Len(); i++ { + subst.replacements[tparams.At(i)] = targs[i] } if subst.debug { if err := subst.wellFormed(); err != nil { @@ -331,9 +333,9 @@ func (subst *subster) named(t *types.Named) types.Type { // type N[A any] func() A // func Foo[T](g N[T]) {} // To instantiate Foo[string], one goes through {T->string}. To get the type of g - // one subsitutes T with string in {N with TypeArgs == {T} and TypeParams == {A} } - // to get {N with TypeArgs == {string} and TypeParams == {A} }. - assert(targs.Len() == tparams.Len(), "TypeArgs().Len() must match TypeParams().Len() if present") + // one subsitutes T with string in {N with typeargs == {T} and typeparams == {A} } + // to get {N with TypeArgs == {string} and typeparams == {A} }. + assert(targs.Len() == tparams.Len(), "typeargs.Len() must match typeparams.Len() if present") for i, n := 0, targs.Len(); i < n; i++ { inst := subst.typ(targs.At(i)) // TODO(generic): Check with rfindley for mutual recursion insts[i] = inst diff --git a/go/ssa/subst_test.go b/go/ssa/subst_test.go index fe84adcc3da..5fa88270004 100644 --- a/go/ssa/subst_test.go +++ b/go/ssa/subst_test.go @@ -99,12 +99,8 @@ var _ L[int] = Fn0[L[int]](nil) } T := tv.Type.(*types.Named) - var tparams []*typeparams.TypeParam - for i, l := 0, typeparams.ForNamed(T); i < l.Len(); i++ { - tparams = append(tparams, l.At(i)) - } - subst := makeSubster(typeparams.NewContext(), tparams, targs, true) + subst := makeSubster(typeparams.NewContext(), typeparams.ForNamed(T), targs, true) sub := subst.typ(T.Underlying()) if got := sub.String(); got != test.want { t.Errorf("subst{%v->%v}.typ(%s) = %v, want %v", test.expr, test.args, T.Underlying(), got, test.want) diff --git a/go/ssa/testdata/valueforexpr.go b/go/ssa/testdata/valueforexpr.go index da76f13a392..243ec614f64 100644 --- a/go/ssa/testdata/valueforexpr.go +++ b/go/ssa/testdata/valueforexpr.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main diff --git a/go/ssa/util.go b/go/ssa/util.go index 80c7d5cbec0..c30e74c7fc9 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -49,7 +49,56 @@ func isPointer(typ types.Type) bool { return ok } -func isInterface(T types.Type) bool { return types.IsInterface(T) } +// isNonTypeParamInterface reports whether t is an interface type but not a type parameter. +func isNonTypeParamInterface(t types.Type) bool { + return !typeparams.IsTypeParam(t) && types.IsInterface(t) +} + +// isBasic reports whether t is a basic type. +func isBasic(t types.Type) bool { + _, ok := t.(*types.Basic) + return ok +} + +// isString reports whether t is exactly a string type. +func isString(t types.Type) bool { + return isBasic(t) && t.(*types.Basic).Info()&types.IsString != 0 +} + +// isByteSlice reports whether t is []byte. +func isByteSlice(t types.Type) bool { + if b, ok := t.(*types.Slice); ok { + e, _ := b.Elem().(*types.Basic) + return e != nil && e.Kind() == types.Byte + } + return false +} + +// isRuneSlice reports whether t is []rune. +func isRuneSlice(t types.Type) bool { + if b, ok := t.(*types.Slice); ok { + e, _ := b.Elem().(*types.Basic) + return e != nil && e.Kind() == types.Rune + } + return false +} + +// isBasicConvType returns true when a type set can be +// one side of a Convert operation. This is when: +// - All are basic, []byte, or []rune. +// - At least 1 is basic. +// - At most 1 is []byte or []rune. +func isBasicConvTypes(tset typeSet) bool { + basics := 0 + all := tset.underIs(func(t types.Type) bool { + if isBasic(t) { + basics++ + return true + } + return isByteSlice(t) || isRuneSlice(t) + }) + return all && basics >= 1 && len(tset)-basics <= 1 +} // deref returns a pointer's element type; otherwise it returns typ. func deref(typ types.Type) types.Type { @@ -113,7 +162,7 @@ func nonbasicTypes(ts []types.Type) []types.Type { added := make(map[types.Type]bool) // additionally filter duplicates var filtered []types.Type for _, T := range ts { - if _, basic := T.(*types.Basic); !basic { + if !isBasic(T) { if !added[T] { added[T] = true filtered = append(filtered, T) @@ -123,22 +172,6 @@ func nonbasicTypes(ts []types.Type) []types.Type { return filtered } -// isGeneric returns true if a package-level member is generic. -func isGeneric(m Member) bool { - switch m := m.(type) { - case *NamedConst, *Global: - return false - case *Type: - // lifted from types.isGeneric. - named, _ := m.Type().(*types.Named) - return named != nil && named.Obj() != nil && typeparams.NamedTypeArgs(named) == nil && typeparams.ForNamed(named) != nil - case *Function: - return len(m._TypeParams) != len(m._TypeArgs) - default: - panic("unreachable") - } -} - // receiverTypeArgs returns the type arguments to a function's reciever. // Returns an empty list if obj does not have a reciever or its reciever does not have type arguments. func receiverTypeArgs(obj *types.Func) []types.Type { diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index 3f2267c8a1b..228daf6158a 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go @@ -120,19 +120,19 @@ func makeWrapper(prog *Program, sel *selection, cr *creator) *Function { // address of implicit C field. var c Call - if r := recvType(obj); !isInterface(r) { // concrete method + if r := recvType(obj); !types.IsInterface(r) { // concrete method if !isPointer(r) { v = emitLoad(fn, v) } callee := prog.originFunc(obj) - if len(callee._TypeParams) > 0 { - callee = prog.instances[callee].lookupOrCreate(receiverTypeArgs(obj), cr) + if callee.typeparams.Len() > 0 { + callee = prog.lookupOrCreateInstance(callee, receiverTypeArgs(obj), cr) } c.Call.Value = callee c.Call.Args = append(c.Call.Args, v) } else { c.Call.Method = obj - c.Call.Value = emitLoad(fn, v) + c.Call.Value = emitLoad(fn, v) // interface (possibly a typeparam) } for _, arg := range fn.Params[1:] { c.Call.Args = append(c.Call.Args, arg) @@ -208,16 +208,16 @@ func makeBound(prog *Program, obj *types.Func, cr *creator) *Function { createParams(fn, 0) var c Call - if !isInterface(recvType(obj)) { // concrete + if !types.IsInterface(recvType(obj)) { // concrete callee := prog.originFunc(obj) - if len(callee._TypeParams) > 0 { - callee = prog.instances[callee].lookupOrCreate(targs, cr) + if callee.typeparams.Len() > 0 { + callee = prog.lookupOrCreateInstance(callee, targs, cr) } c.Call.Value = callee c.Call.Args = []Value{fv} } else { - c.Call.Value = fv c.Call.Method = obj + c.Call.Value = fv // interface (possibly a typeparam) } for _, arg := range fn.Params { c.Call.Args = append(c.Call.Args, arg) @@ -324,3 +324,63 @@ func toSelection(sel *types.Selection) *selection { indirect: sel.Indirect(), } } + +// -- instantiations -------------------------------------------------- + +// buildInstantiationWrapper creates a body for an instantiation +// wrapper fn. The body calls the original generic function, +// bracketed by ChangeType conversions on its arguments and results. +func buildInstantiationWrapper(fn *Function) { + orig := fn.topLevelOrigin + sig := fn.Signature + + fn.startBody() + if sig.Recv() != nil { + fn.addParamObj(sig.Recv()) + } + createParams(fn, 0) + + // Create body. Add a call to origin generic function + // and make type changes between argument and parameters, + // as well as return values. + var c Call + c.Call.Value = orig + if res := orig.Signature.Results(); res.Len() == 1 { + c.typ = res.At(0).Type() + } else { + c.typ = res + } + + // parameter of instance becomes an argument to the call + // to the original generic function. + argOffset := 0 + for i, arg := range fn.Params { + var typ types.Type + if i == 0 && sig.Recv() != nil { + typ = orig.Signature.Recv().Type() + argOffset = 1 + } else { + typ = orig.Signature.Params().At(i - argOffset).Type() + } + c.Call.Args = append(c.Call.Args, emitTypeCoercion(fn, arg, typ)) + } + + results := fn.emit(&c) + var ret Return + switch res := sig.Results(); res.Len() { + case 0: + // no results, do nothing. + case 1: + ret.Results = []Value{emitTypeCoercion(fn, results, res.At(0).Type())} + default: + for i := 0; i < sig.Results().Len(); i++ { + v := emitExtract(fn, results, i) + ret.Results = append(ret.Results, emitTypeCoercion(fn, v, res.At(i).Type())) + } + } + + fn.emit(&ret) + fn.currentBlock = nil + + fn.finishBody() +} From c099dff179b302e0d67ea8b88e199cac1f1b8ff5 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Fri, 18 Nov 2022 14:15:01 -0500 Subject: [PATCH 444/723] gopls/internal/vulncheck: log progress Log before/after package loading and govulncheck.Source calls. All stderr output of `gopls vulncheck` command is forwarded to the client as LSP progress reports and visible to users. go/packages.Load can be traced further if GOPACKAGESDEBUG env var is set. Instrumentation in govulncheck.Source is still under discussion in upstream. For golang/go#56795 Change-Id: I8244930494aed17b319887bf96c2523f3215fa50 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452055 Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller gopls-CI: kokoro --- gopls/internal/vulncheck/command.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 510dd844960..646e622bef0 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -222,11 +222,14 @@ func init() { cfg.Mode = packages.NeedName | packages.NeedImports | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps | packages.NeedModule - + logf := log.New(os.Stderr, "", log.Ltime).Printf + logf("Loading packages...") pkgs, err := packages.Load(&cfg, patterns...) if err != nil { + logf("Failed to load packages: %v", err) return err } + logf("Loaded %d packages and their dependencies", len(pkgs)) cli, err := client.NewClient(findGOVULNDB(&cfg), client.Options{ HTTPCache: gvc.DefaultCache(), }) @@ -240,6 +243,7 @@ func init() { if err != nil { return err } + logf("Found %d vulnerabilities", len(res.Vulns)) if err := json.NewEncoder(os.Stdout).Encode(res); err != nil { return err } From fc039936a9d1d54d8b8583f7d6fb3e8d76f42c73 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Mon, 14 Nov 2022 07:28:05 -0500 Subject: [PATCH 445/723] internal/lsp/mod: adjust vulncheck diagnostics suppression logic Vulnerabilities associated with a module should be suppressed after the module is updated. Previously, we checked whether the module version in go.mod 'require' matches the FoundVersion reported by the vulncheck. However, we realized that we cannot always assume the module version in require is the actually used module version (due to how minimal version selection works, and how exclude/replace affects). Instead check whether the module version is newer or equals to the suggested fixed version and if this go.mod require is newer, assume that the user updated the module already and suppress diagnostics about the module. This is not perfect but a heuristic to reduce confusion from the stale vulncheck report right after applying the quick fixes and upgrading modules. Change-Id: I40f4c3e70b19af3f6edd98f30de3ccb7a6bd7498 Reviewed-on: https://go-review.googlesource.com/c/tools/+/450277 Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/mod/diagnostics.go | 31 +++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index a51b1e3e5f5..2bebf6f229e 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -215,13 +215,23 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, var warning, info []string for _, mv := range vulns { mod, vuln := mv.mod, mv.vuln - // Only show the diagnostic if the vulnerability was calculated - // for the module at the current version. - // TODO(hyangah): this prevents from surfacing vulnerable modules - // since module version selection is affected by dependency module - // requirements and replace/exclude/go.work use. Drop this check - // and annotate only the module path. - if semver.IsValid(mod.FoundVersion) && semver.Compare(req.Mod.Version, mod.FoundVersion) != 0 { + // It is possible that the source code was changed since the last + // govulncheck run and information in the `vulns` info is stale. + // For example, imagine that a user is in the middle of updating + // problematic modules detected by the govulncheck run by applying + // quick fixes. Stale diagnostics can be confusing and prevent the + // user from quickly locating the next module to fix. + // Ideally we should rerun the analysis with the updated module + // dependencies or any other code changes, but we are not yet + // in the position of automatically triggerring the analysis + // (govulncheck can take a while). We also don't know exactly what + // part of source code was changed since `vulns` was computed. + // As a heuristic, we assume that a user upgrades the affecting + // module to the version with the fix or the latest one, and if the + // version in the require statement is equal to or higher than the + // fixed version, skip generating a diagnostic about the vulnerability. + // Eventually, the user has to rerun govulncheck. + if mod.FixedVersion != "" && semver.IsValid(req.Mod.Version) && semver.Compare(mod.FixedVersion, req.Mod.Version) <= 0 { continue } if !vuln.IsCalled() { @@ -233,15 +243,14 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, if fixedVersion := mod.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { cmd, err := getUpgradeCodeAction(fh, req, fixedVersion) if err != nil { - return nil, err + return nil, err // TODO: bug report } // Add an upgrade for module@latest. // TODO(suzmue): verify if latest is the same as fixedVersion. latest, err := getUpgradeCodeAction(fh, req, "latest") if err != nil { - return nil, err + return nil, err // TODO: bug report } - fixes = []source.SuggestedFix{ source.SuggestedFixFromCommand(cmd, protocol.QuickFix), source.SuggestedFixFromCommand(latest, protocol.QuickFix), @@ -250,7 +259,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, } if len(warning) == 0 && len(info) == 0 { - return nil, nil + continue } severity := protocol.SeverityInformation if len(warning) > 0 { From 128f61d438af442db4e5a489ccf8d06899c031cf Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Fri, 11 Nov 2022 22:34:29 -0500 Subject: [PATCH 446/723] gopls/internal/lsp/mod: add related info Related info in module vulncheck diagnostics carries the location to the entry stack frame found from some of the call paths. Change-Id: I99eb60ab1c2cb7cc356eae8d2ba35a2167cb179e Reviewed-on: https://go-review.googlesource.com/c/tools/+/450278 TryBot-Result: Gopher Robot Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Robert Findley gopls-CI: kokoro Reviewed-by: Suzy Mueller --- gopls/internal/lsp/mod/diagnostics.go | 46 +++++++++++- gopls/internal/regtest/misc/vuln_test.go | 89 ++++++++++++++++++------ 2 files changed, 112 insertions(+), 23 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 2bebf6f229e..3b74bd8835d 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -18,6 +18,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/vuln/osv" ) @@ -213,6 +214,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, // Fixes will include only the upgrades for warning level diagnostics. var fixes []source.SuggestedFix var warning, info []string + var relatedInfo []source.RelatedInformation for _, mv := range vulns { mod, vuln := mv.mod, mv.vuln // It is possible that the source code was changed since the last @@ -238,6 +240,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, info = append(info, vuln.OSV.ID) } else { warning = append(warning, vuln.OSV.ID) + relatedInfo = append(relatedInfo, listRelatedInfo(ctx, snapshot, vuln)...) } // Upgrade to the exact version we offer the user, not the most recent. if fixedVersion := mod.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { @@ -282,7 +285,6 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, default: fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", ")) } - vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ URI: fh.URI(), Range: rng, @@ -290,12 +292,54 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, Source: source.Vulncheck, Message: b.String(), SuggestedFixes: fixes, + Related: relatedInfo, }) } return vulnDiagnostics, nil } +func listRelatedInfo(ctx context.Context, snapshot source.Snapshot, vuln *govulncheck.Vuln) []source.RelatedInformation { + var ri []source.RelatedInformation + for _, m := range vuln.Modules { + for _, p := range m.Packages { + for _, c := range p.CallStacks { + if len(c.Frames) == 0 { + continue + } + entry := c.Frames[0] + pos := entry.Position + if pos.Filename == "" { + continue // token.Position Filename is an optional field. + } + uri := span.URIFromPath(pos.Filename) + startPos := protocol.Position{ + Line: uint32(pos.Line) - 1, + // We need to read the file contents to precisesly map + // token.Position (pos) to the UTF16-based column offset + // protocol.Position requires. That can be expensive. + // We need this related info to just help users to open + // the entry points of the callstack and once the file is + // open, we will compute the precise location based on the + // open file contents. So, use the beginning of the line + // as the position here instead of precise UTF16-based + // position computation. + Character: 0, + } + ri = append(ri, source.RelatedInformation{ + URI: uri, + Range: protocol.Range{ + Start: startPos, + End: startPos, + }, + Message: fmt.Sprintf("[%v] %v -> %v.%v", vuln.OSV.ID, entry.Name(), p.Path, c.Symbol), + }) + } + } + } + return ri +} + func formatMessage(v *govulncheck.Vuln) string { details := []byte(v.OSV.Details) // Remove any new lines that are not preceded or followed by a new line. diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index b38b184a610..b511eb7e432 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -9,9 +9,12 @@ package misc import ( "context" + "path/filepath" + "sort" "strings" "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/lsp/regtest" @@ -329,6 +332,10 @@ func TestRunVulncheckWarning(t *testing.T) { ReadDiagnostics("go.mod", gotDiagnostics), ), ) + env.OpenFile("x/x.go") + lineX := env.RegexpSearch("x/x.go", `c\.C1\(\)\.Vuln1\(\)`) + env.OpenFile("y/y.go") + lineY := env.RegexpSearch("y/y.go", `c\.C2\(\)\(\)`) wantDiagnostics := map[string]vulnDiagExpectation{ "golang.org/amod": { applyAction: "Upgrade to v1.0.4", @@ -340,6 +347,10 @@ func TestRunVulncheckWarning(t *testing.T) { "Upgrade to latest", "Upgrade to v1.0.4", }, + relatedInfo: []vulnRelatedInfo{ + {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1 + {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln2 + }, }, }, codeActions: []string{ @@ -353,6 +364,9 @@ func TestRunVulncheckWarning(t *testing.T) { { msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.", severity: protocol.SeverityWarning, + relatedInfo: []vulnRelatedInfo{ + {"y.go", uint32(lineY.Line), "[GO-2022-02]"}, // bvuln.Vuln + }, }, }, hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."}, @@ -364,8 +378,8 @@ func TestRunVulncheckWarning(t *testing.T) { // Check that the actions we get when including all diagnostics at a location return the same result gotActions := env.CodeAction("go.mod", modPathDiagnostics) - if !sameCodeActions(gotActions, want.codeActions) { - t.Errorf("code actions for %q do not match, expected %v, got %v\n", mod, want.codeActions, gotActions) + if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { + t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff) continue } @@ -399,20 +413,12 @@ require ( }) } -func sameCodeActions(gotActions []protocol.CodeAction, want []string) bool { - gotTitles := make([]string, len(gotActions)) - for i, ca := range gotActions { - gotTitles[i] = ca.Title - } - if len(gotTitles) != len(want) { - return false - } - for i := range want { - if gotTitles[i] != want[i] { - return false - } +func diffCodeActions(gotActions []protocol.CodeAction, want []string) string { + var gotTitles []string + for _, ca := range gotActions { + gotTitles = append(gotTitles, ca.Title) } - return true + return cmp.Diff(want, gotTitles) } const workspace2 = ` @@ -483,8 +489,8 @@ func TestRunVulncheckInfo(t *testing.T) { modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) // Check that the actions we get when including all diagnostics at a location return the same result gotActions := env.CodeAction("go.mod", modPathDiagnostics) - if !sameCodeActions(gotActions, want.codeActions) { - t.Errorf("code actions for %q do not match, expected %v, got %v\n", mod, want.codeActions, gotActions) + if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { + t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff) continue } } @@ -513,12 +519,16 @@ func testVulnDiagnostics(t *testing.T, env *Env, requireMod string, want vulnDia continue } if diag.Severity != w.severity { - t.Errorf("incorrect severity for %q, expected %s got %s\n", w.msg, w.severity, diag.Severity) + t.Errorf("incorrect severity for %q, want %s got %s\n", w.msg, w.severity, diag.Severity) } - + sort.Slice(w.relatedInfo, func(i, j int) bool { return w.relatedInfo[i].less(w.relatedInfo[j]) }) + if got, want := summarizeRelatedInfo(diag.RelatedInformation), w.relatedInfo; !cmp.Equal(got, want) { + t.Errorf("related info for %q do not match, want %v, got %v\n", w.msg, want, got) + } + // Check expected code actions appear. gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag}) - if !sameCodeActions(gotActions, w.codeActions) { - t.Errorf("code actions for %q do not match, expected %v, got %v\n", w.msg, w.codeActions, gotActions) + if diff := diffCodeActions(gotActions, w.codeActions); diff != "" { + t.Errorf("code actions for %q do not match, want %v, got %v\n%v\n", w.msg, w.codeActions, gotActions, diff) continue } @@ -527,7 +537,7 @@ func testVulnDiagnostics(t *testing.T, env *Env, requireMod string, want vulnDia hover, _ := env.Hover("go.mod", pos) for _, part := range want.hover { if !strings.Contains(hover.Value, part) { - t.Errorf("hover contents for %q do not match, expected %v, got %v\n", w.msg, strings.Join(want.hover, ","), hover.Value) + t.Errorf("hover contents for %q do not match, want %v, got %v\n", w.msg, strings.Join(want.hover, ","), hover.Value) break } } @@ -536,12 +546,47 @@ func testVulnDiagnostics(t *testing.T, env *Env, requireMod string, want vulnDia return modPathDiagnostics } +// summarizeRelatedInfo converts protocol.DiagnosticRelatedInformation to vulnRelatedInfo +// that captures only the part that we want to test. +func summarizeRelatedInfo(rinfo []protocol.DiagnosticRelatedInformation) []vulnRelatedInfo { + var res []vulnRelatedInfo + for _, r := range rinfo { + filename := filepath.Base(r.Location.URI.SpanURI().Filename()) + message, _, _ := strings.Cut(r.Message, " ") + line := r.Location.Range.Start.Line + res = append(res, vulnRelatedInfo{filename, line, message}) + } + sort.Slice(res, func(i, j int) bool { + return res[i].less(res[j]) + }) + return res +} + +type vulnRelatedInfo struct { + Filename string + Line uint32 + Message string +} + type vulnDiag struct { msg string severity protocol.DiagnosticSeverity // codeActions is a list titles of code actions that we get with this // diagnostics as the context. codeActions []string + // relatedInfo is related info message prefixed by the file base. + // See summarizeRelatedInfo. + relatedInfo []vulnRelatedInfo +} + +func (i vulnRelatedInfo) less(j vulnRelatedInfo) bool { + if i.Filename != j.Filename { + return i.Filename < j.Filename + } + if i.Line != j.Line { + return i.Line < j.Line + } + return i.Message < j.Message } // wantVulncheckModDiagnostics maps a module path in the require From 8ba9a370eed74583cae5c33e386c53f4af039c90 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 16 Nov 2022 19:27:06 -0500 Subject: [PATCH 447/723] gopls/internal/lsp/mod: highlight when there is no fix for vuln Change-Id: If53455159b8578901ff728640e9c1ec671c61cdb Reviewed-on: https://go-review.googlesource.com/c/tools/+/451695 Reviewed-by: Suzy Mueller Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/mod/hover.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 547f25270b7..520a029924a 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -179,13 +179,13 @@ func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulnchec } func fixedVersionInfo(v *govulncheck.Vuln, modPath string) string { - fix := "No fix is available." + fix := "\n**No fix is available.**" for _, m := range v.Modules { if m.Path != modPath { continue } if m.FixedVersion != "" { - fix = "Fixed in " + m.FixedVersion + "." + fix = "\nFixed in " + m.FixedVersion + "." } break } From 4ce4f93a92fba791168c16f327d01e9abb86c4c6 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 17 Nov 2022 10:07:16 -0500 Subject: [PATCH 448/723] gopls/internal/lsp: add gopls.fetch_vulncheck_result This CL changes the signature of View's Vulnerabilities and SetVulnerabilities, to use govulncheck.Result instead of []*govulncheck.Vuln. That allows us to hold extra information about the analysis in addition to the list of vulnerabilities. Also, instead of aliasing x/vuln/exp/govulncheck.Result define our own gopls/internal/govulncheck.Result type so the gopls documentation is less confusing. Change-Id: I7a18da9bf047b9ebed6fc0264b5e0f66c04ba3f3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/451696 Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Suzy Mueller Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/doc/commands.md | 20 +++++++++++ gopls/internal/govulncheck/types.go | 12 +++++++ gopls/internal/govulncheck/types_118.go | 7 ++-- gopls/internal/govulncheck/types_not118.go | 7 ---- gopls/internal/lsp/cache/session.go | 2 +- gopls/internal/lsp/cache/view.go | 20 +++++++---- gopls/internal/lsp/command.go | 17 ++++++++-- gopls/internal/lsp/command/command_gen.go | 20 +++++++++++ gopls/internal/lsp/command/interface.go | 6 ++++ gopls/internal/lsp/mod/diagnostics.go | 9 +++-- gopls/internal/lsp/mod/hover.go | 9 +++-- gopls/internal/lsp/source/api_json.go | 7 ++++ gopls/internal/lsp/source/view.go | 4 +-- gopls/internal/regtest/misc/vuln_test.go | 39 ++++++++++++++++++++++ 14 files changed, 150 insertions(+), 29 deletions(-) create mode 100644 gopls/internal/govulncheck/types.go diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index c942be48fda..b6eee96f881 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -100,6 +100,26 @@ Args: } ``` +### **Get known vulncheck result** +Identifier: `gopls.fetch_vulncheck_result` + +Fetch the result of latest vulnerability check (`govulncheck`). + +Args: + +``` +{ + // The file URI. + "URI": string, +} +``` + +Result: + +``` +map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/govulncheck.Result +``` + ### **Toggle gc_details** Identifier: `gopls.gc_details` diff --git a/gopls/internal/govulncheck/types.go b/gopls/internal/govulncheck/types.go new file mode 100644 index 00000000000..6cf0415a242 --- /dev/null +++ b/gopls/internal/govulncheck/types.go @@ -0,0 +1,12 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package govulncheck + +// Result is the result of vulnerability scanning. +type Result struct { + // Vulns contains all vulnerabilities that are called or imported by + // the analyzed module. + Vulns []*Vuln +} diff --git a/gopls/internal/govulncheck/types_118.go b/gopls/internal/govulncheck/types_118.go index 74109631950..7b354d622a8 100644 --- a/gopls/internal/govulncheck/types_118.go +++ b/gopls/internal/govulncheck/types_118.go @@ -8,7 +8,9 @@ // Package govulncheck provides an experimental govulncheck API. package govulncheck -import "golang.org/x/vuln/exp/govulncheck" +import ( + "golang.org/x/vuln/exp/govulncheck" +) var ( // Source reports vulnerabilities that affect the analyzed packages. @@ -22,9 +24,6 @@ type ( // Config is the configuration for Main. Config = govulncheck.Config - // Result is the result of executing Source. - Result = govulncheck.Result - // Vuln represents a single OSV entry. Vuln = govulncheck.Vuln diff --git a/gopls/internal/govulncheck/types_not118.go b/gopls/internal/govulncheck/types_not118.go index ae92caa5eac..faf5a7055b5 100644 --- a/gopls/internal/govulncheck/types_not118.go +++ b/gopls/internal/govulncheck/types_not118.go @@ -13,13 +13,6 @@ import ( "golang.org/x/vuln/osv" ) -// Result is the result of executing Source or Binary. -type Result struct { - // Vulns contains all vulnerabilities that are called or imported by - // the analyzed module. - Vulns []*Vuln -} - // Vuln represents a single OSV entry. type Vuln struct { // OSV contains all data from the OSV entry for this vulnerability. diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 9b4355331a6..4916f4235f5 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -239,7 +239,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, name: name, folder: folder, moduleUpgrades: map[span.URI]map[string]string{}, - vulns: map[span.URI][]*govulncheck.Vuln{}, + vulns: map[span.URI]*govulncheck.Result{}, filesByURI: map[span.URI]*fileBase{}, filesByBase: map[string][]*fileBase{}, rootURI: root, diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index f373c00508b..8198353ecab 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -61,7 +61,7 @@ type View struct { // Each modfile has a map of module name to upgrade version. moduleUpgrades map[span.URI]map[string]string - vulns map[span.URI][]*govulncheck.Vuln + vulns map[span.URI]*govulncheck.Result // keep track of files by uri and by basename, a single file may be mapped // to multiple uris, and the same basename may map to multiple files @@ -1044,16 +1044,24 @@ func (v *View) ClearModuleUpgrades(modfile span.URI) { delete(v.moduleUpgrades, modfile) } -func (v *View) Vulnerabilities(modfile span.URI) []*govulncheck.Vuln { +func (v *View) Vulnerabilities(modfile ...span.URI) map[span.URI]*govulncheck.Result { + m := make(map[span.URI]*govulncheck.Result) v.mu.Lock() defer v.mu.Unlock() - vulns := make([]*govulncheck.Vuln, len(v.vulns[modfile])) - copy(vulns, v.vulns[modfile]) - return vulns + if len(modfile) == 0 { + for k, v := range v.vulns { + m[k] = v + } + return m + } + for _, f := range modfile { + m[f] = v.vulns[f] + } + return m } -func (v *View) SetVulnerabilities(modfile span.URI, vulns []*govulncheck.Vuln) { +func (v *View) SetVulnerabilities(modfile span.URI, vulns *govulncheck.Result) { v.mu.Lock() defer v.mu.Unlock() diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 28cf3471a75..90b8408dcad 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -833,6 +833,17 @@ type pkgLoadConfig struct { Tests bool } +func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*govulncheck.Result, error) { + ret := map[protocol.DocumentURI]*govulncheck.Result{} + err := c.run(ctx, commandConfig{forURI: arg.URI}, func(ctx context.Context, deps commandDeps) error { + for modfile, result := range deps.snapshot.View().Vulnerabilities() { + ret[protocol.URIFromSpanURI(modfile)] = result + } + return nil + }) + return ret, err +} + func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) error { if args.URI == "" { return errors.New("VulncheckArgs is missing URI field") @@ -887,10 +898,10 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc // TODO: for easy debugging, log the failed stdout somewhere? return fmt.Errorf("failed to parse govulncheck output: %v", err) } - vulns := result.Vulns - deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), vulns) - c.s.diagnoseSnapshot(deps.snapshot, nil, false) + deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), &result) + c.s.diagnoseSnapshot(deps.snapshot, nil, false) + vulns := result.Vulns affecting := make([]string, 0, len(vulns)) for _, v := range vulns { if v.IsCalled() { diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go index 615f9509c8c..5111e673eeb 100644 --- a/gopls/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -24,6 +24,7 @@ const ( ApplyFix Command = "apply_fix" CheckUpgrades Command = "check_upgrades" EditGoDirective Command = "edit_go_directive" + FetchVulncheckResult Command = "fetch_vulncheck_result" GCDetails Command = "gc_details" Generate Command = "generate" GenerateGoplsMod Command = "generate_gopls_mod" @@ -50,6 +51,7 @@ var Commands = []Command{ ApplyFix, CheckUpgrades, EditGoDirective, + FetchVulncheckResult, GCDetails, Generate, GenerateGoplsMod, @@ -102,6 +104,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.EditGoDirective(ctx, a0) + case "gopls.fetch_vulncheck_result": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return s.FetchVulncheckResult(ctx, a0) case "gopls.gc_details": var a0 protocol.DocumentURI if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -276,6 +284,18 @@ func NewEditGoDirectiveCommand(title string, a0 EditGoDirectiveArgs) (protocol.C }, nil } +func NewFetchVulncheckResultCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.fetch_vulncheck_result", + Arguments: args, + }, nil +} + func NewGCDetailsCommand(title string, a0 protocol.DocumentURI) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index 23b9f655046..e6ab13da47b 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -17,6 +17,7 @@ package command import ( "context" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" ) @@ -153,6 +154,11 @@ type Interface interface { // // Run vulnerability check (`govulncheck`). RunVulncheckExp(context.Context, VulncheckArgs) error + + // FetchVulncheckResult: Get known vulncheck result + // + // Fetch the result of latest vulnerability check (`govulncheck`). + FetchVulncheckResult(context.Context, URIArg) (map[protocol.DocumentURI]*govulncheck.Result, error) } type RunTestsArgs struct { diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 3b74bd8835d..ec0993fc9ee 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -180,14 +180,17 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return nil, err } - vs := snapshot.View().Vulnerabilities(fh.URI()) - // TODO(suzmue): should we just store the vulnerabilities like this? + vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()] + if vs == nil || len(vs.Vulns) == 0 { + return nil, nil + } + type modVuln struct { mod *govulncheck.Module vuln *govulncheck.Vuln } vulnsByModule := make(map[string][]modVuln) - for _, vuln := range vs { + for _, vuln := range vs.Vulns { for _, mod := range vuln.Modules { vulnsByModule[mod.Path] = append(vulnsByModule[mod.Path], modVuln{mod, vuln}) } diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 520a029924a..7706262fbfb 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -71,7 +71,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Get the vulnerability info. - affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI()), req.Mod.Path) + affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI())[fh.URI()], req.Mod.Path) // Get the `go mod why` results for the given file. why, err := snapshot.ModWhy(ctx, fh) @@ -117,8 +117,11 @@ func formatHeader(modpath string, options *source.Options) string { return b.String() } -func lookupVulns(vulns []*govulncheck.Vuln, modpath string) (affecting, nonaffecting []*govulncheck.Vuln) { - for _, vuln := range vulns { +func lookupVulns(vulns *govulncheck.Result, modpath string) (affecting, nonaffecting []*govulncheck.Vuln) { + if vulns == nil { + return nil, nil + } + for _, vuln := range vulns.Vulns { for _, mod := range vuln.Modules { if mod.Path != modpath { continue diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index e283c3b35c7..fdc46486b18 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -689,6 +689,13 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Runs `go mod edit -go=version` for a module.", ArgDoc: "{\n\t// Any document URI within the relevant module.\n\t\"URI\": string,\n\t// The version to pass to `go mod edit -go`.\n\t\"Version\": string,\n}", }, + { + Command: "gopls.fetch_vulncheck_result", + Title: "Get known vulncheck result", + Doc: "Fetch the result of latest vulnerability check (`govulncheck`).", + ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", + ResultDoc: "map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/govulncheck.Result", + }, { Command: "gopls.gc_details", Title: "Toggle gc_details", diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 7466484952f..aed3f54938c 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -314,11 +314,11 @@ type View interface { // Vulnerabilites returns known vulnerabilities for the given modfile. // TODO(suzmue): replace command.Vuln with a different type, maybe // https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary? - Vulnerabilities(modfile span.URI) []*govulncheck.Vuln + Vulnerabilities(modfile ...span.URI) map[span.URI]*govulncheck.Result // SetVulnerabilities resets the list of vulnerabilites that exists for the given modules // required by modfile. - SetVulnerabilities(modfile span.URI, vulnerabilities []*govulncheck.Vuln) + SetVulnerabilities(modfile span.URI, vulncheckResult *govulncheck.Result) // FileKind returns the type of a file FileKind(FileHandle) FileKind diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index b511eb7e432..a9587c2f440 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" @@ -180,9 +181,43 @@ func main() { // TODO(hyangah): once the diagnostics are published, wait for diagnostics. ShownMessage("Found GOSTDLIB"), ) + testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GOSTDLIB"}}) }) } +func testFetchVulncheckResult(t *testing.T, env *Env, want map[string][]string) { + t.Helper() + + var result map[protocol.DocumentURI]*govulncheck.Result + fetchCmd, err := command.NewFetchVulncheckResultCommand("fetch", command.URIArg{ + URI: env.Sandbox.Workdir.URI("go.mod"), + }) + if err != nil { + t.Fatal(err) + } + env.ExecuteCommand(&protocol.ExecuteCommandParams{ + Command: fetchCmd.Command, + Arguments: fetchCmd.Arguments, + }, &result) + + for _, v := range want { + sort.Strings(v) + } + got := map[string][]string{} + for k, r := range result { + var osv []string + for _, v := range r.Vulns { + osv = append(osv, v.OSV.ID) + } + sort.Strings(osv) + modfile := env.Sandbox.Workdir.RelPath(k.SpanURI().Filename()) + got[modfile] = osv + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("fetch vulnchheck result = got %v, want %v: diff %v", got, want, diff) + } +} + const workspace1 = ` -- go.mod -- module golang.org/entry @@ -332,6 +367,9 @@ func TestRunVulncheckWarning(t *testing.T) { ReadDiagnostics("go.mod", gotDiagnostics), ), ) + testFetchVulncheckResult(t, env, map[string][]string{ + "go.mod": {"GO-2022-01", "GO-2022-02", "GO-2022-03"}, + }) env.OpenFile("x/x.go") lineX := env.RegexpSearch("x/x.go", `c\.C1\(\)\.Vuln1\(\)`) env.OpenFile("y/y.go") @@ -470,6 +508,7 @@ func TestRunVulncheckInfo(t *testing.T) { ReadDiagnostics("go.mod", gotDiagnostics)), ShownMessage("No vulnerabilities found")) // only count affecting vulnerabilities. + testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GO-2022-02"}}) // wantDiagnostics maps a module path in the require // section of a go.mod to diagnostics that will be returned // when running vulncheck. From 19fb30d1d24fd3c7ab2f8e46efa08d1bf17e5397 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 18 Nov 2022 16:29:11 -0500 Subject: [PATCH 449/723] gopls/internal/lsp/cache: fix data race in Symbols We forgot to acquire a lock while accessing snapshot.files. Also, upgrade errgroup and use its new SetLimit feature. Change-Id: Iff1449fd727f0766f6c81391acccaa21951f59be Reviewed-on: https://go-review.googlesource.com/c/tools/+/452058 Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/snapshot.go | 29 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index ae5fe28825c..c2cb946afaa 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1094,32 +1094,37 @@ func (s *snapshot) activePackageHandles(ctx context.Context) ([]*packageHandle, // Symbols extracts and returns the symbols for each file in all the snapshot's views. func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { + // Read the set of Go files out of the snapshot. + var goFiles []source.VersionedFileHandle + s.mu.Lock() + s.files.Range(func(uri span.URI, f source.VersionedFileHandle) { + if s.View().FileKind(f) == source.Go { + goFiles = append(goFiles, f) + } + }) + s.mu.Unlock() + + // Symbolize them in parallel. var ( group errgroup.Group - nprocs = 2 * runtime.GOMAXPROCS(-1) // symbolize is a mix of I/O and CPU - iolimit = make(chan struct{}, nprocs) // I/O limiting counting semaphore + nprocs = 2 * runtime.GOMAXPROCS(-1) // symbolize is a mix of I/O and CPU resultMu sync.Mutex result = make(map[span.URI][]source.Symbol) ) - s.files.Range(func(uri span.URI, f source.VersionedFileHandle) { - if s.View().FileKind(f) != source.Go { - return // workspace symbols currently supports only Go files. - } - - // TODO(adonovan): upgrade errgroup and use group.SetLimit(nprocs). - iolimit <- struct{}{} // acquire token + group.SetLimit(nprocs) + for _, f := range goFiles { + f := f group.Go(func() error { - defer func() { <-iolimit }() // release token symbols, err := s.symbolize(ctx, f) if err != nil { return err } resultMu.Lock() - result[uri] = symbols + result[f.URI()] = symbols resultMu.Unlock() return nil }) - }) + } // Keep going on errors, but log the first failure. // Partial results are better than no symbol results. if err := group.Wait(); err != nil { From 932ec22a59ae8f56b4dfcfcd32954f3977732366 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 11 Jan 2022 22:37:02 -0500 Subject: [PATCH 450/723] internal/testenv: add a Command function that replaces exec.Command The function is derived from the testenv.Command function in the main repo as of CL 446875, with a couple of modifications to allow it to build (with more limited functionality) with Go versions as old as 1.16 (currently needed in order to test gopls with such versions). testenv.Command sets up an exec.Cmd with more useful termination behavior in the context of a test: namely, it is terminated with SIGQUIT (to get a goroutine dump from the subprocess) shortly before the test would otherwise time out. Assuming that the test logs the output from the command appropriately, this should make deadlocks and unexpectedly slow operations easier to diagnose in the builders. For golang/go#50014. Updates golang/go#50436. Change-Id: I872d4b24e63951bf9b7811189e672973d366fb78 Reviewed-on: https://go-review.googlesource.com/c/tools/+/377835 Reviewed-by: Dmitri Shuralyov Reviewed-by: Joedian Reid TryBot-Result: Gopher Robot Auto-Submit: Bryan Mills Run-TryBot: Bryan Mills Reviewed-by: Robert Findley gopls-CI: kokoro --- go/analysis/analysistest/analysistest.go | 3 +- internal/testenv/exec.go | 149 +++++++++++++++++++++++ internal/testenv/testenv.go | 49 +++----- internal/testenv/testenv_notunix.go | 14 +++ internal/testenv/testenv_unix.go | 14 +++ 5 files changed, 193 insertions(+), 36 deletions(-) create mode 100644 internal/testenv/exec.go create mode 100644 internal/testenv/testenv_notunix.go create mode 100644 internal/testenv/testenv_unix.go diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index abef7b44729..a3a53ba9f20 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -19,6 +19,7 @@ import ( "sort" "strconv" "strings" + "testing" "text/scanner" "golang.org/x/tools/go/analysis" @@ -278,7 +279,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns // attempted, even if unsuccessful. It is safe for a test to ignore all // the results, but a test may use it to perform additional checks. func Run(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result { - if t, ok := t.(testenv.Testing); ok { + if t, ok := t.(testing.TB); ok { testenv.NeedsGoPackages(t) } diff --git a/internal/testenv/exec.go b/internal/testenv/exec.go new file mode 100644 index 00000000000..f103ad9d82e --- /dev/null +++ b/internal/testenv/exec.go @@ -0,0 +1,149 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testenv + +import ( + "context" + "os" + "os/exec" + "reflect" + "runtime" + "strconv" + "testing" + "time" +) + +// HasExec reports whether the current system can start new processes +// using os.StartProcess or (more commonly) exec.Command. +func HasExec() bool { + switch runtime.GOOS { + case "js", "ios": + return false + } + return true +} + +// NeedsExec checks that the current system can start new processes +// using os.StartProcess or (more commonly) exec.Command. +// If not, NeedsExec calls t.Skip with an explanation. +func NeedsExec(t testing.TB) { + if !HasExec() { + t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH) + } +} + +// CommandContext is like exec.CommandContext, but: +// - skips t if the platform does not support os/exec, +// - if supported, sends SIGQUIT instead of SIGKILL in its Cancel function +// - if the test has a deadline, adds a Context timeout and (if supported) WaitDelay +// for an arbitrary grace period before the test's deadline expires, +// - if Cmd has the Cancel field, fails the test if the command is canceled +// due to the test's deadline, and +// - if supported, sets a Cleanup function that verifies that the test did not +// leak a subprocess. +func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { + t.Helper() + NeedsExec(t) + + var ( + cancelCtx context.CancelFunc + gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging) + ) + + if td, ok := Deadline(t); ok { + // Start with a minimum grace period, just long enough to consume the + // output of a reasonable program after it terminates. + gracePeriod = 100 * time.Millisecond + if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { + scale, err := strconv.Atoi(s) + if err != nil { + t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) + } + gracePeriod *= time.Duration(scale) + } + + // If time allows, increase the termination grace period to 5% of the + // test's remaining time. + testTimeout := time.Until(td) + if gp := testTimeout / 20; gp > gracePeriod { + gracePeriod = gp + } + + // When we run commands that execute subprocesses, we want to reserve two + // grace periods to clean up: one for the delay between the first + // termination signal being sent (via the Cancel callback when the Context + // expires) and the process being forcibly terminated (via the WaitDelay + // field), and a second one for the delay becween the process being + // terminated and and the test logging its output for debugging. + // + // (We want to ensure that the test process itself has enough time to + // log the output before it is also terminated.) + cmdTimeout := testTimeout - 2*gracePeriod + + if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout { + // Either ctx doesn't have a deadline, or its deadline would expire + // after (or too close before) the test has already timed out. + // Add a shorter timeout so that the test will produce useful output. + ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout) + } + } + + cmd := exec.CommandContext(ctx, name, args...) + + // Use reflection to set the Cancel and WaitDelay fields, if present. + // TODO(bcmills): When we no longer support Go versions below 1.20, + // remove the use of reflect and assume that the fields are always present. + rc := reflect.ValueOf(cmd).Elem() + + if rCancel := rc.FieldByName("Cancel"); rCancel.IsValid() { + rCancel.Set(reflect.ValueOf(func() error { + if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded { + // The command timed out due to running too close to the test's deadline + // (because we specifically set a shorter Context deadline for that + // above). There is no way the test did that intentionally — it's too + // close to the wire! — so mark it as a test failure. That way, if the + // test expects the command to fail for some other reason, it doesn't + // have to distinguish between that reason and a timeout. + t.Errorf("test timed out while running command: %v", cmd) + } else { + // The command is being terminated due to ctx being canceled, but + // apparently not due to an explicit test deadline that we added. + // Log that information in case it is useful for diagnosing a failure, + // but don't actually fail the test because of it. + t.Logf("%v: terminating command: %v", ctx.Err(), cmd) + } + return cmd.Process.Signal(Sigquit) + })) + } + + if rWaitDelay := rc.FieldByName("WaitDelay"); rWaitDelay.IsValid() { + rWaitDelay.Set(reflect.ValueOf(gracePeriod)) + } + + // t.Cleanup was added in Go 1.14; for earlier Go versions, + // we just let the Context leak. + type Cleanupper interface { + Cleanup(func()) + } + if ct, ok := t.(Cleanupper); ok { + ct.Cleanup(func() { + if cancelCtx != nil { + cancelCtx() + } + if cmd.Process != nil && cmd.ProcessState == nil { + t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) + } + }) + } + + return cmd +} + +// Command is like exec.Command, but applies the same changes as +// testenv.CommandContext (with a default Context). +func Command(t testing.TB, name string, args ...string) *exec.Cmd { + t.Helper() + return CommandContext(t, context.Background(), name, args...) +} diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index 22df96a858c..ff38498e568 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -24,16 +24,6 @@ import ( exec "golang.org/x/sys/execabs" ) -// Testing is an abstraction of a *testing.T. -type Testing interface { - Skipf(format string, args ...interface{}) - Fatalf(format string, args ...interface{}) -} - -type helperer interface { - Helper() -} - // packageMainIsDevel reports whether the module containing package main // is a development version (if module information is available). func packageMainIsDevel() bool { @@ -192,14 +182,13 @@ func allowMissingTool(tool string) bool { // NeedsTool skips t if the named tool is not present in the path. // As a special case, "cgo" means "go" is present and can compile cgo programs. -func NeedsTool(t Testing, tool string) { - if t, ok := t.(helperer); ok { - t.Helper() - } +func NeedsTool(t testing.TB, tool string) { err := hasTool(tool) if err == nil { return } + + t.Helper() if allowMissingTool(tool) { t.Skipf("skipping because %s tool not available: %v", tool, err) } else { @@ -209,10 +198,8 @@ func NeedsTool(t Testing, tool string) { // NeedsGoPackages skips t if the go/packages driver (or 'go' tool) implied by // the current process environment is not present in the path. -func NeedsGoPackages(t Testing) { - if t, ok := t.(helperer); ok { - t.Helper() - } +func NeedsGoPackages(t testing.TB) { + t.Helper() tool := os.Getenv("GOPACKAGESDRIVER") switch tool { @@ -232,10 +219,8 @@ func NeedsGoPackages(t Testing) { // NeedsGoPackagesEnv skips t if the go/packages driver (or 'go' tool) implied // by env is not present in the path. -func NeedsGoPackagesEnv(t Testing, env []string) { - if t, ok := t.(helperer); ok { - t.Helper() - } +func NeedsGoPackagesEnv(t testing.TB, env []string) { + t.Helper() for _, v := range env { if strings.HasPrefix(v, "GOPACKAGESDRIVER=") { @@ -256,10 +241,8 @@ func NeedsGoPackagesEnv(t Testing, env []string) { // and then run them with os.StartProcess or exec.Command. // Android doesn't have the userspace go build needs to run, // and js/wasm doesn't support running subprocesses. -func NeedsGoBuild(t Testing) { - if t, ok := t.(helperer); ok { - t.Helper() - } +func NeedsGoBuild(t testing.TB) { + t.Helper() // This logic was derived from internal/testing.HasGoBuild and // may need to be updated as that function evolves. @@ -318,29 +301,25 @@ func Go1Point() int { // NeedsGo1Point skips t if the Go version used to run the test is older than // 1.x. -func NeedsGo1Point(t Testing, x int) { - if t, ok := t.(helperer); ok { - t.Helper() - } +func NeedsGo1Point(t testing.TB, x int) { if Go1Point() < x { + t.Helper() t.Skipf("running Go version %q is version 1.%d, older than required 1.%d", runtime.Version(), Go1Point(), x) } } // SkipAfterGo1Point skips t if the Go version used to run the test is newer than // 1.x. -func SkipAfterGo1Point(t Testing, x int) { - if t, ok := t.(helperer); ok { - t.Helper() - } +func SkipAfterGo1Point(t testing.TB, x int) { if Go1Point() > x { + t.Helper() t.Skipf("running Go version %q is version 1.%d, newer than maximum 1.%d", runtime.Version(), Go1Point(), x) } } // Deadline returns the deadline of t, if known, // using the Deadline method added in Go 1.15. -func Deadline(t Testing) (time.Time, bool) { +func Deadline(t testing.TB) (time.Time, bool) { td, ok := t.(interface { Deadline() (time.Time, bool) }) diff --git a/internal/testenv/testenv_notunix.go b/internal/testenv/testenv_notunix.go new file mode 100644 index 00000000000..74de6f0a8e2 --- /dev/null +++ b/internal/testenv/testenv_notunix.go @@ -0,0 +1,14 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows || plan9 || (js && wasm) +// +build windows plan9 js,wasm + +package testenv + +import "os" + +// Sigquit is the signal to send to kill a hanging subprocess. +// On Unix we send SIGQUIT, but on non-Unix we only have os.Kill. +var Sigquit = os.Kill diff --git a/internal/testenv/testenv_unix.go b/internal/testenv/testenv_unix.go new file mode 100644 index 00000000000..bc6af1ff81d --- /dev/null +++ b/internal/testenv/testenv_unix.go @@ -0,0 +1,14 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix || aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build unix aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package testenv + +import "syscall" + +// Sigquit is the signal to send to kill a hanging subprocess. +// Send SIGQUIT to get a stack trace. +var Sigquit = syscall.SIGQUIT From b978661c6cbaacda12e290b1e88c7675e49452dd Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 11 Jan 2022 22:39:53 -0500 Subject: [PATCH 451/723] cmd/godoc: streamline test subprocesses - Use testenv.Command to obtain Cmd instances that terminate (with useful goroutine dumps!) before the test's timeout, and remove arbitrary hard-coded timeouts. - Execute the test binary itself as cmd/godoc instead of invoking (and cleaning up after) 'go build'. - Use context cancellation to reduce the number of ad-hoc goroutines and channels needed by the tests and to provide stronger invariants on process cleanup. For golang/go#50014 Change-Id: I19ae4d10da691db233c79734799ae074ffdf6a03 Reviewed-on: https://go-review.googlesource.com/c/tools/+/377836 Run-TryBot: Bryan Mills Reviewed-by: Joedian Reid Reviewed-by: Dmitri Shuralyov TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Bryan Mills --- cmd/godoc/godoc_test.go | 189 ++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 102 deletions(-) diff --git a/cmd/godoc/godoc_test.go b/cmd/godoc/godoc_test.go index 4eb341af1e1..3e91ac6f94c 100644 --- a/cmd/godoc/godoc_test.go +++ b/cmd/godoc/godoc_test.go @@ -2,10 +2,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package main_test +package main import ( "bytes" + "context" "fmt" "go/build" "io/ioutil" @@ -13,10 +14,10 @@ import ( "net/http" "os" "os/exec" - "path/filepath" "regexp" "runtime" "strings" + "sync" "testing" "time" @@ -24,42 +25,39 @@ import ( "golang.org/x/tools/internal/testenv" ) -// buildGodoc builds the godoc executable. -// It returns its path, and a cleanup function. -// -// TODO(adonovan): opt: do this at most once, and do the cleanup -// exactly once. How though? There's no atexit. -func buildGodoc(t *testing.T) (bin string, cleanup func()) { - t.Helper() - - if runtime.GOARCH == "arm" { - t.Skip("skipping test on arm platforms; too slow") - } - if runtime.GOOS == "android" { - t.Skipf("the dependencies are not available on android") +func TestMain(m *testing.M) { + if os.Getenv("GODOC_TEST_IS_GODOC") != "" { + main() + os.Exit(0) } - testenv.NeedsTool(t, "go") - tmp, err := ioutil.TempDir("", "godoc-regtest-") - if err != nil { - t.Fatal(err) - } - defer func() { - if cleanup == nil { // probably, go build failed. - os.RemoveAll(tmp) - } - }() + // Inform subprocesses that they should run the cmd/godoc main instead of + // running tests. It's a close approximation to building and running the real + // command, and much less complicated and expensive to build and clean up. + os.Setenv("GODOC_TEST_IS_GODOC", "1") - bin = filepath.Join(tmp, "godoc") - if runtime.GOOS == "windows" { - bin += ".exe" - } - cmd := exec.Command("go", "build", "-o", bin) - if err := cmd.Run(); err != nil { - t.Fatalf("Building godoc: %v", err) + os.Exit(m.Run()) +} + +var exe struct { + path string + err error + once sync.Once +} + +func godocPath(t *testing.T) string { + switch runtime.GOOS { + case "js", "ios": + t.Skipf("skipping test that requires exec") } - return bin, func() { os.RemoveAll(tmp) } + exe.once.Do(func() { + exe.path, exe.err = os.Executable() + }) + if exe.err != nil { + t.Fatal(exe.err) + } + return exe.path } func serverAddress(t *testing.T) string { @@ -74,60 +72,42 @@ func serverAddress(t *testing.T) string { return ln.Addr().String() } -func waitForServerReady(t *testing.T, cmd *exec.Cmd, addr string) { - ch := make(chan error, 1) - go func() { ch <- fmt.Errorf("server exited early: %v", cmd.Wait()) }() - go waitForServer(t, ch, +func waitForServerReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) { + waitForServer(t, ctx, fmt.Sprintf("http://%v/", addr), "Go Documentation Server", - 15*time.Second, false) - if err := <-ch; err != nil { - t.Skipf("skipping due to https://go.dev/issue/50014: %v", err) - } } -func waitForSearchReady(t *testing.T, cmd *exec.Cmd, addr string) { - ch := make(chan error, 1) - go func() { ch <- fmt.Errorf("server exited early: %v", cmd.Wait()) }() - go waitForServer(t, ch, +func waitForSearchReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) { + waitForServer(t, ctx, fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr), "The list of tokens.", - 2*time.Minute, false) - if err := <-ch; err != nil { - t.Skipf("skipping due to https://go.dev/issue/50014: %v", err) - } } -func waitUntilScanComplete(t *testing.T, addr string) { - ch := make(chan error) - go waitForServer(t, ch, +func waitUntilScanComplete(t *testing.T, ctx context.Context, addr string) { + waitForServer(t, ctx, fmt.Sprintf("http://%v/pkg", addr), "Scan is not yet complete", - 2*time.Minute, // setting reverse as true, which means this waits // until the string is not returned in the response anymore - true, - ) - if err := <-ch; err != nil { - t.Skipf("skipping due to https://go.dev/issue/50014: %v", err) - } + true) } -const pollInterval = 200 * time.Millisecond +const pollInterval = 50 * time.Millisecond -// waitForServer waits for server to meet the required condition. -// It sends a single error value to ch, unless the test has failed. -// The error value is nil if the required condition was met within -// timeout, or non-nil otherwise. -func waitForServer(t *testing.T, ch chan<- error, url, match string, timeout time.Duration, reverse bool) { - deadline := time.Now().Add(timeout) - for time.Now().Before(deadline) { - time.Sleep(pollInterval) - if t.Failed() { - return +// waitForServer waits for server to meet the required condition, +// failing the test if ctx is canceled before that occurs. +func waitForServer(t *testing.T, ctx context.Context, url, match string, reverse bool) { + start := time.Now() + for { + if ctx.Err() != nil { + t.Helper() + t.Fatalf("server failed to respond in %v", time.Since(start)) } + + time.Sleep(pollInterval) res, err := http.Get(url) if err != nil { continue @@ -140,11 +120,9 @@ func waitForServer(t *testing.T, ch chan<- error, url, match string, timeout tim switch { case !reverse && bytes.Contains(body, []byte(match)), reverse && !bytes.Contains(body, []byte(match)): - ch <- nil return } } - ch <- fmt.Errorf("server failed to respond in %v", timeout) } // hasTag checks whether a given release tag is contained in the current version @@ -158,24 +136,18 @@ func hasTag(t string) bool { return false } -func killAndWait(cmd *exec.Cmd) { - cmd.Process.Kill() - cmd.Process.Wait() -} - func TestURL(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping on plan9; fails to start up quickly enough") } - bin, cleanup := buildGodoc(t) - defer cleanup() + bin := godocPath(t) testcase := func(url string, contents string) func(t *testing.T) { return func(t *testing.T) { stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) args := []string{fmt.Sprintf("-url=%s", url)} - cmd := exec.Command(bin, args...) + cmd := testenv.Command(t, bin, args...) cmd.Stdout = stdout cmd.Stderr = stderr cmd.Args[0] = "godoc" @@ -205,8 +177,8 @@ func TestURL(t *testing.T) { // Basic integration test for godoc HTTP interface. func TestWeb(t *testing.T) { - bin, cleanup := buildGodoc(t) - defer cleanup() + bin := godocPath(t) + for _, x := range packagestest.All { t.Run(x.Name(), func(t *testing.T) { testWeb(t, x, bin, false) @@ -217,17 +189,19 @@ func TestWeb(t *testing.T) { // Basic integration test for godoc HTTP interface. func TestWebIndex(t *testing.T) { if testing.Short() { - t.Skip("skipping test in -short mode") + t.Skip("skipping slow test in -short mode") } - bin, cleanup := buildGodoc(t) - defer cleanup() + bin := godocPath(t) testWeb(t, packagestest.GOPATH, bin, true) } // Basic integration test for godoc HTTP interface. func testWeb(t *testing.T, x packagestest.Exporter, bin string, withIndex bool) { - if runtime.GOOS == "plan9" { - t.Skip("skipping on plan9; fails to start up quickly enough") + switch runtime.GOOS { + case "plan9": + t.Skip("skipping on plan9: fails to start up quickly enough") + case "android", "ios": + t.Skip("skipping on mobile: lacks GOROOT/api in test environment") } // Write a fake GOROOT/GOPATH with some third party packages. @@ -256,23 +230,39 @@ package a; import _ "godoc.test/repo2/a"; const Name = "repo1a"`, if withIndex { args = append(args, "-index", "-index_interval=-1s") } - cmd := exec.Command(bin, args...) + cmd := testenv.Command(t, bin, args...) cmd.Dir = e.Config.Dir cmd.Env = e.Config.Env - cmd.Stdout = os.Stderr - cmd.Stderr = os.Stderr + cmdOut := new(strings.Builder) + cmd.Stdout = cmdOut + cmd.Stderr = cmdOut cmd.Args[0] = "godoc" if err := cmd.Start(); err != nil { t.Fatalf("failed to start godoc: %s", err) } - defer killAndWait(cmd) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + err := cmd.Wait() + t.Logf("%v: %v", cmd, err) + cancel() + }() + defer func() { + // Shut down the server cleanly if possible. + if runtime.GOOS == "windows" { + cmd.Process.Kill() // Windows doesn't support os.Interrupt. + } else { + cmd.Process.Signal(os.Interrupt) + } + <-ctx.Done() + t.Logf("server output:\n%s", cmdOut) + }() if withIndex { - waitForSearchReady(t, cmd, addr) + waitForSearchReady(t, ctx, cmd, addr) } else { - waitForServerReady(t, cmd, addr) - waitUntilScanComplete(t, addr) + waitForServerReady(t, ctx, cmd, addr) + waitUntilScanComplete(t, ctx, addr) } tests := []struct { @@ -454,22 +444,17 @@ func TestNoMainModule(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping on plan9; for consistency with other tests that build godoc binary") } - bin, cleanup := buildGodoc(t) - defer cleanup() - tempDir, err := ioutil.TempDir("", "godoc-test-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) + bin := godocPath(t) + tempDir := t.TempDir() // Run godoc in an empty directory with module mode explicitly on, // so that 'go env GOMOD' reports os.DevNull. - cmd := exec.Command(bin, "-url=/") + cmd := testenv.Command(t, bin, "-url=/") cmd.Dir = tempDir cmd.Env = append(os.Environ(), "GO111MODULE=on") var stderr bytes.Buffer cmd.Stderr = &stderr - err = cmd.Run() + err := cmd.Run() if err != nil { t.Fatalf("godoc command failed: %v\nstderr=%q", err, stderr.String()) } From db5eae21789c24629aaad3552348d4effec7135d Mon Sep 17 00:00:00 2001 From: Martin Zacho Date: Fri, 18 Nov 2022 15:39:38 +0100 Subject: [PATCH 452/723] analysis: correct go/analysis/passes/findcall path in docs Change-Id: I6c2305b1b52dceff78e289b24f3ad1a5b69c8f7e Reviewed-on: https://go-review.googlesource.com/c/tools/+/451955 Reviewed-by: Martin Zacho gopls-CI: kokoro Reviewed-by: Tim King TryBot-Result: Gopher Robot Reviewed-by: Joedian Reid Run-TryBot: Tim King --- go/analysis/doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/analysis/doc.go b/go/analysis/doc.go index 2c49e335892..b5a301c205e 100644 --- a/go/analysis/doc.go +++ b/go/analysis/doc.go @@ -297,7 +297,7 @@ singlechecker and multichecker subpackages. The singlechecker package provides the main function for a command that runs one analyzer. By convention, each analyzer such as -go/passes/findcall should be accompanied by a singlechecker-based +go/analysis/passes/findcall should be accompanied by a singlechecker-based command such as go/analysis/passes/findcall/cmd/findcall, defined in its entirety as: From 41c70c91bcffeec650018664141451be12048277 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 21 Nov 2022 14:14:23 -0500 Subject: [PATCH 453/723] internal/lsp/source: avoid using go/types.Implements with Typ[Invalid] go/types predicates are undefined on types.Typ[types.Invalid]. Avoid using types.Implements with invalid types in one place. For golang/go##53595 Change-Id: I48cff1681dc7d4f59ea815e5a0d58e0160990c82 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452475 Run-TryBot: Robert Findley Reviewed-by: Robert Griesemer TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Robert Findley --- gopls/internal/lsp/source/completion/postfix_snippets.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gopls/internal/lsp/source/completion/postfix_snippets.go b/gopls/internal/lsp/source/completion/postfix_snippets.go index 07d07235df0..0737ec2461f 100644 --- a/gopls/internal/lsp/source/completion/postfix_snippets.go +++ b/gopls/internal/lsp/source/completion/postfix_snippets.go @@ -271,7 +271,8 @@ func (a *postfixTmplArgs) VarName(t types.Type, nonNamedDefault string) string { } var name string - if types.Implements(t, errorIntf) { + // 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 { name = nonNamedDefault From 57f56abcb06a73575f280dd88c01bd8665883fa9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 21 Nov 2022 14:45:14 -0500 Subject: [PATCH 454/723] gopls/internal/lsp/source: avoid panic(nil) The previous code used panic(nil) as a longjump. However, if a library function such as Node.Pos() were to panic(nil), we would catch it spuriously instead of reporting the panic by crashing the program. This change uses a new type, found, as a sentinal for the non-local jump. Updates golang/go#25448 Change-Id: I2439432a7ca477d0c25c9c848a98308a378b089b Reviewed-on: https://go-review.googlesource.com/c/tools/+/452515 Reviewed-by: Russ Cox Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/source/hover.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 26d5e23ad34..3d09e102f7a 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -895,10 +895,14 @@ func anyNonEmpty(x []string) bool { // // It returns (nil, nil) if no Field or Decl is found at pos. func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *ast.Field) { - // panic(nil) breaks off the traversal and + // panic(found{}) breaks off the traversal and // causes the function to return normally. + type found struct{} defer func() { - if x := recover(); x != nil { + switch x := recover().(type) { + case nil: + case found: + default: panic(x) } }() @@ -930,7 +934,7 @@ func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *a break } } - panic(nil) // found + panic(found{}) } } @@ -953,7 +957,7 @@ func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *a case *ast.FuncDecl: if n.Name.Pos() == pos { decl = n - panic(nil) // found + panic(found{}) } case *ast.GenDecl: @@ -962,13 +966,13 @@ func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *a case *ast.TypeSpec: if spec.Name.Pos() == pos { decl = n - panic(nil) // found + panic(found{}) } case *ast.ValueSpec: for _, id := range spec.Names { if id.Pos() == pos { decl = n - panic(nil) // found + panic(found{}) } } } From 2ad3c3337f520388f019a5ed8eb4646ed1375449 Mon Sep 17 00:00:00 2001 From: Michael Matloob Date: Fri, 18 Nov 2022 17:17:53 -0500 Subject: [PATCH 455/723] go/packages: change workaround for issue 28749 to not require NeedFiles Before this change, the workaround for golang/go#28749 filtered out the files from OtherFiles from CompiledGoFiles to remove non-go files that are appearing in CompiledGoFiles. But OtherFiles requires NeedFiles, so the value of NeedFiles is affecting CompiledGoFiles which should only be controlled by NeedCompliedGoFiles. Instead remove all files that don't look like Go files. Fixes golang/go#56632 Change-Id: Ib5e310f39f2470ee3b3488b6c4b74b70e3164d4c Reviewed-on: https://go-review.googlesource.com/c/tools/+/452059 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Reviewed-by: Cherry Mui Run-TryBot: Michael Matloob gopls-CI: kokoro --- go/packages/golist.go | 13 ++++------ go/packages/packages_test.go | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/go/packages/golist.go b/go/packages/golist.go index d9a7915bab0..6bb7168d2e3 100644 --- a/go/packages/golist.go +++ b/go/packages/golist.go @@ -604,17 +604,12 @@ func (state *golistState) createDriverResponse(words ...string) (*driverResponse // Work around https://golang.org/issue/28749: // cmd/go puts assembly, C, and C++ files in CompiledGoFiles. - // Filter out any elements of CompiledGoFiles that are also in OtherFiles. - // We have to keep this workaround in place until go1.12 is a distant memory. - if len(pkg.OtherFiles) > 0 { - other := make(map[string]bool, len(pkg.OtherFiles)) - for _, f := range pkg.OtherFiles { - other[f] = true - } - + // Remove files from CompiledGoFiles that are non-go files + // (or are not files that look like they are from the cache). + if len(pkg.CompiledGoFiles) > 0 { out := pkg.CompiledGoFiles[:0] for _, f := range pkg.CompiledGoFiles { - if other[f] { + if ext := filepath.Ext(f); ext != ".go" && ext != "" { // ext == "" means the file is from the cache, so probably cgo-processed file continue } out = append(out, f) diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 647f3a366df..8180969d1ab 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -2471,6 +2471,52 @@ func testIssue37098(t *testing.T, exporter packagestest.Exporter) { } } +// TestIssue56632 checks that CompiledGoFiles does not contain non-go files regardless of +// whether the NeedFiles mode bit is set. +func TestIssue56632(t *testing.T) { + t.Parallel() + testenv.NeedsGoBuild(t) + testenv.NeedsTool(t, "cgo") + + exported := packagestest.Export(t, packagestest.GOPATH, []packagestest.Module{{ + Name: "golang.org/issue56632", + Files: map[string]interface{}{ + "a/a.go": `package a`, + "a/a_cgo.go": `package a + +import "C"`, + "a/a.s": ``, + "a/a.c": ``, + }}}) + defer exported.Cleanup() + + modes := []packages.LoadMode{packages.NeedCompiledGoFiles, packages.NeedCompiledGoFiles | packages.NeedFiles, packages.NeedImports | packages.NeedCompiledGoFiles, packages.NeedImports | packages.NeedFiles | packages.NeedCompiledGoFiles} + for _, mode := range modes { + exported.Config.Mode = mode + + initial, err := packages.Load(exported.Config, "golang.org/issue56632/a") + if err != nil { + t.Fatalf("failed to load package: %v", err) + } + + if len(initial) != 1 { + t.Errorf("expected 3 packages, got %d", len(initial)) + } + + p := initial[0] + + if len(p.Errors) != 0 { + t.Errorf("expected no errors, got %v", p.Errors) + } + + for _, f := range p.CompiledGoFiles { + if strings.HasSuffix(f, ".s") || strings.HasSuffix(f, ".c") { + t.Errorf("expected no non-Go CompiledGoFiles, got file %q in CompiledGoFiles", f) + } + } + } +} + // TestInvalidFilesInXTest checks the fix for golang/go#37971 in Go 1.15. func TestInvalidFilesInXTest(t *testing.T) { testAllOrModulesParallel(t, testInvalidFilesInXTest) } func testInvalidFilesInXTest(t *testing.T, exporter packagestest.Exporter) { From 3b9d20c52192f5c16e801afcbc23756327155865 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Fri, 18 Nov 2022 11:32:17 -0500 Subject: [PATCH 456/723] internal/gcimporter: in TestStdlib, only check x/tools packages if we expect to have their source The go_android_exec and go_ios_exec wrappers (found in GOROOT/misc/android and GOROOT/misc/ios, respectively) only copy over the source code for the package under test, its parent directories, and the 'testdata' subdirectories of those parents. That does not necessarily include the transitive closure of packages imported by those packages. This may fix the failing tests on the android-amd64-emu builder on release-branch.go1.19. (I do not understand why the test is not already being skipped due to the call to testenv.NeedsGoPackages, but I do not intend to investigate further.) Change-Id: I6bd32fd7e7e9f56e85b2e03baae59da5d9ba0ed9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/451995 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Bryan Mills --- internal/gcimporter/stdlib_test.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/gcimporter/stdlib_test.go b/internal/gcimporter/stdlib_test.go index ec1be3ea031..34ddb306f14 100644 --- a/internal/gcimporter/stdlib_test.go +++ b/internal/gcimporter/stdlib_test.go @@ -9,6 +9,7 @@ import ( "fmt" "go/token" "go/types" + "runtime" "testing" "unsafe" @@ -30,9 +31,18 @@ func TestStdlib(t *testing.T) { t.Skip("skipping test on 32-bit machine") } - // Load, parse and type-check the standard library and x/tools. + // Load, parse and type-check the standard library. + // If we have the full source code for x/tools, also load and type-check that. cfg := &packages.Config{Mode: packages.LoadAllSyntax} - pkgs, err := packages.Load(cfg, "std", "golang.org/x/tools/...") + patterns := []string{"std"} + switch runtime.GOOS { + case "android", "ios": + // The go_.*_exec script for mobile builders only copies over the source tree + // for the package under test. + default: + patterns = append(patterns, "golang.org/x/tools/...") + } + pkgs, err := packages.Load(cfg, patterns...) if err != nil { t.Fatalf("failed to load/parse/type-check: %v", err) } From b797704ca3061ceee5a58e30419219d73a7da91b Mon Sep 17 00:00:00 2001 From: thepudds Date: Mon, 21 Nov 2022 21:33:32 +0000 Subject: [PATCH 457/723] go/analysis/passes/loopclosure: recursively check last statements in statements like if, switch, and for In golang/go#16520, there was a suggestion to extend the current loopclosure check to check more statements. The current loopclosure flags patterns like: for k, v := range seq { go/defer func() { ... k, v ... }() } For this CL, the motivating example from golang/go#16520 is: var wg sync.WaitGroup for i := 0; i < 10; i++ { for j := 0; j < 1; j++ { wg.Add(1) go func() { fmt.Printf("%d ", i) wg.Done() }() } } wg.Wait() The current loopclosure check does not flag this because of the inner for loop, and the checker looks only at the last statement in the outer loop body. The suggestion is we redefine "last" recursively. For example, if the last statement is an if, then we examine the last statements in both of its branches. Or if the last statement is a nested loop, then we examine the last statement of that loop's body, and so on. A few years ago, Alan Donovan sent a sketch in CL 184537. This CL attempts to complete Alan's sketch, as well as integrates with the ensuing changes from golang/go#55972 to check errgroup.Group.Go, which with this CL can now be recursively "last". Updates golang/go#16520 Updates golang/go#55972 Fixes golang/go#30649 Fixes golang/go#32876 Change-Id: If66c6707025c20f32a2a781f6d11c4901f15742a GitHub-Last-Rev: 04980e02b7213dcca2e23c535a69dffaf19f314f GitHub-Pull-Request: golang/tools#415 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452155 Reviewed-by: Tim King Run-TryBot: Tim King Reviewed-by: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- go/analysis/passes/loopclosure/loopclosure.go | 212 +++++++++++++----- .../passes/loopclosure/testdata/src/a/a.go | 106 +++++++++ gopls/doc/analyzers.md | 72 ++++-- gopls/internal/lsp/source/api_json.go | 4 +- 4 files changed, 317 insertions(+), 77 deletions(-) diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index bb0715c02b5..5291d1b2cd0 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -18,24 +18,60 @@ import ( const Doc = `check references to loop variables from within nested functions -This analyzer checks for references to loop variables from within a function -literal inside the loop body. It checks for patterns where access to a loop -variable is known to escape the current loop iteration: - 1. a call to go or defer at the end of the loop body - 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body - 3. a call testing.T.Run where the subtest body invokes t.Parallel() - -In the case of (1) and (2), the analyzer only considers references in the last -statement of the loop body as it is not deep enough to understand the effects -of subsequent statements which might render the reference benign. - -For example: - - for i, v := range s { - go func() { - println(i, v) // not what you might expect - }() - } +This analyzer reports places where a function literal references the +iteration variable of an enclosing loop, and the loop calls the function +in such a way (e.g. with go or defer) that it may outlive the loop +iteration and possibly observe the wrong value of the variable. + +In this example, all the deferred functions run after the loop has +completed, so all observe the final value of v. + + for _, v := range list { + defer func() { + use(v) // incorrect + }() + } + +One fix is to create a new variable for each iteration of the loop: + + for _, v := range list { + v := v // new var per iteration + defer func() { + use(v) // ok + }() + } + +The next example uses a go statement and has a similar problem. +In addition, it has a data race because the loop updates v +concurrent with the goroutines accessing it. + + for _, v := range elem { + go func() { + use(v) // incorrect, and a data race + }() + } + +A fix is the same as before. The checker also reports problems +in goroutines started by golang.org/x/sync/errgroup.Group. +A hard-to-spot variant of this form is common in parallel tests: + + func Test(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + use(test) // incorrect, and a data race + }) + } + } + +The t.Parallel() call causes the rest of the function to execute +concurrent with the loop. + +The analyzer reports references only in the last statement, +as it is not deep enough to understand the effects of subsequent +statements that might render the reference benign. +("Last statement" is defined recursively in compound +statements such as if, switch, and select.) See: https://golang.org/doc/go_faq.html#closures_and_goroutines` @@ -91,59 +127,121 @@ func run(pass *analysis.Pass) (interface{}, error) { // // For go, defer, and errgroup.Group.Go, we ignore all but the last // statement, because it's hard to prove go isn't followed by wait, or - // defer by return. + // defer by return. "Last" is defined recursively. // + // TODO: consider allowing the "last" go/defer/Go statement to be followed by + // N "trivial" statements, possibly under a recursive definition of "trivial" + // so that that checker could, for example, conclude that a go statement is + // followed by an if statement made of only trivial statements and trivial expressions, + // and hence the go statement could still be checked. + forEachLastStmt(body.List, func(last ast.Stmt) { + var stmts []ast.Stmt + switch s := last.(type) { + case *ast.GoStmt: + stmts = litStmts(s.Call.Fun) + case *ast.DeferStmt: + stmts = litStmts(s.Call.Fun) + case *ast.ExprStmt: // check for errgroup.Group.Go + if call, ok := s.X.(*ast.CallExpr); ok { + stmts = litStmts(goInvoke(pass.TypesInfo, call)) + } + } + for _, stmt := range stmts { + reportCaptured(pass, vars, stmt) + } + }) + + // Also check for testing.T.Run (with T.Parallel). // We consider every t.Run statement in the loop body, because there is - // no such commonly used mechanism for synchronizing parallel subtests. + // no commonly used mechanism for synchronizing parallel subtests. // It is of course theoretically possible to synchronize parallel subtests, // though such a pattern is likely to be exceedingly rare as it would be // fighting against the test runner. - lastStmt := len(body.List) - 1 - for i, s := range body.List { - var stmts []ast.Stmt // statements that must be checked for escaping references + for _, s := range body.List { switch s := s.(type) { - case *ast.GoStmt: - if i == lastStmt { - stmts = litStmts(s.Call.Fun) - } - - case *ast.DeferStmt: - if i == lastStmt { - stmts = litStmts(s.Call.Fun) - } - - case *ast.ExprStmt: // check for errgroup.Group.Go and testing.T.Run (with T.Parallel) + case *ast.ExprStmt: if call, ok := s.X.(*ast.CallExpr); ok { - if i == lastStmt { - stmts = litStmts(goInvoke(pass.TypesInfo, call)) - } - if stmts == nil { - stmts = parallelSubtest(pass.TypesInfo, call) + for _, stmt := range parallelSubtest(pass.TypesInfo, call) { + reportCaptured(pass, vars, stmt) } + } } + } + }) + return nil, nil +} - for _, stmt := range stmts { - ast.Inspect(stmt, func(n ast.Node) bool { - id, ok := n.(*ast.Ident) - if !ok { - return true - } - obj := pass.TypesInfo.Uses[id] - if obj == nil { - return true - } - for _, v := range vars { - if v == obj { - pass.ReportRangef(id, "loop variable %s captured by func literal", id.Name) - } - } - return true - }) +// reportCaptured reports a diagnostic stating a loop variable +// has been captured by a func literal if checkStmt has escaping +// references to vars. vars is expected to be variables updated by a loop statement, +// and checkStmt is expected to be a statements from the body of a func literal in the loop. +func reportCaptured(pass *analysis.Pass, vars []types.Object, checkStmt ast.Stmt) { + ast.Inspect(checkStmt, func(n ast.Node) bool { + id, ok := n.(*ast.Ident) + if !ok { + return true + } + obj := pass.TypesInfo.Uses[id] + if obj == nil { + return true + } + for _, v := range vars { + if v == obj { + pass.ReportRangef(id, "loop variable %s captured by func literal", id.Name) } } + return true }) - return nil, nil +} + +// forEachLastStmt calls onLast on each "last" statement in a list of statements. +// "Last" is defined recursively so, for example, if the last statement is +// a switch statement, then each switch case is also visited to examine +// its last statements. +func forEachLastStmt(stmts []ast.Stmt, onLast func(last ast.Stmt)) { + if len(stmts) == 0 { + return + } + + s := stmts[len(stmts)-1] + switch s := s.(type) { + case *ast.IfStmt: + loop: + for { + forEachLastStmt(s.Body.List, onLast) + switch e := s.Else.(type) { + case *ast.BlockStmt: + forEachLastStmt(e.List, onLast) + break loop + case *ast.IfStmt: + s = e + case nil: + break loop + } + } + case *ast.ForStmt: + forEachLastStmt(s.Body.List, onLast) + case *ast.RangeStmt: + forEachLastStmt(s.Body.List, onLast) + case *ast.SwitchStmt: + for _, c := range s.Body.List { + cc := c.(*ast.CaseClause) + forEachLastStmt(cc.Body, onLast) + } + case *ast.TypeSwitchStmt: + for _, c := range s.Body.List { + cc := c.(*ast.CaseClause) + forEachLastStmt(cc.Body, onLast) + } + case *ast.SelectStmt: + for _, c := range s.Body.List { + cc := c.(*ast.CommClause) + forEachLastStmt(cc.Body, onLast) + } + default: + onLast(s) + } } // litStmts returns all statements from the function body of a function diff --git a/go/analysis/passes/loopclosure/testdata/src/a/a.go b/go/analysis/passes/loopclosure/testdata/src/a/a.go index 2a17d16bc86..7a7f05f663f 100644 --- a/go/analysis/passes/loopclosure/testdata/src/a/a.go +++ b/go/analysis/passes/loopclosure/testdata/src/a/a.go @@ -7,6 +7,8 @@ package testdata import ( + "sync" + "golang.org/x/sync/errgroup" ) @@ -108,6 +110,70 @@ func _() { } } +// Cases that rely on recursively checking for last statements. +func _() { + + for i := range "outer" { + for j := range "inner" { + if j < 1 { + defer func() { + print(i) // want "loop variable i captured by func literal" + }() + } else if j < 2 { + go func() { + print(i) // want "loop variable i captured by func literal" + }() + } else { + go func() { + print(i) + }() + println("we don't catch the error above because of this statement") + } + } + } + + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + if j < 1 { + switch j { + case 0: + defer func() { + print(i) // want "loop variable i captured by func literal" + }() + default: + go func() { + print(i) // want "loop variable i captured by func literal" + }() + } + } else if j < 2 { + var a interface{} = j + switch a.(type) { + case int: + defer func() { + print(i) // want "loop variable i captured by func literal" + }() + default: + go func() { + print(i) // want "loop variable i captured by func literal" + }() + } + } else { + ch := make(chan string) + select { + case <-ch: + defer func() { + print(i) // want "loop variable i captured by func literal" + }() + default: + go func() { + print(i) // want "loop variable i captured by func literal" + }() + } + } + } + } +} + // Group is used to test that loopclosure only matches Group.Go when Group is // from the golang.org/x/sync/errgroup package. type Group struct{} @@ -125,6 +191,21 @@ func _() { return nil }) } + + for i, v := range s { + if i > 0 { + g.Go(func() error { + print(i) // want "loop variable i captured by func literal" + return nil + }) + } else { + g.Go(func() error { + print(v) // want "loop variable v captured by func literal" + return nil + }) + } + } + // Do not match other Group.Go cases g1 := new(Group) for i, v := range s { @@ -135,3 +216,28 @@ func _() { }) } } + +// Real-world example from #16520, slightly simplified +func _() { + var nodes []interface{} + + critical := new(errgroup.Group) + others := sync.WaitGroup{} + + isCritical := func(node interface{}) bool { return false } + run := func(node interface{}) error { return nil } + + for _, node := range nodes { + if isCritical(node) { + critical.Go(func() error { + return run(node) // want "loop variable node captured by func literal" + }) + } else { + others.Add(1) + go func() { + _ = run(node) // want "loop variable node captured by func literal" + others.Done() + }() + } + } +} diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 176c32f1ba4..28bf1deae83 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -218,24 +218,60 @@ inferred from function arguments, or from other type arguments: check references to loop variables from within nested functions -This analyzer checks for references to loop variables from within a function -literal inside the loop body. It checks for patterns where access to a loop -variable is known to escape the current loop iteration: - 1. a call to go or defer at the end of the loop body - 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body - 3. a call testing.T.Run where the subtest body invokes t.Parallel() - -In the case of (1) and (2), the analyzer only considers references in the last -statement of the loop body as it is not deep enough to understand the effects -of subsequent statements which might render the reference benign. - -For example: - - for i, v := range s { - go func() { - println(i, v) // not what you might expect - }() - } +This analyzer reports places where a function literal references the +iteration variable of an enclosing loop, and the loop calls the function +in such a way (e.g. with go or defer) that it may outlive the loop +iteration and possibly observe the wrong value of the variable. + +In this example, all the deferred functions run after the loop has +completed, so all observe the final value of v. + + for _, v := range list { + defer func() { + use(v) // incorrect + }() + } + +One fix is to create a new variable for each iteration of the loop: + + for _, v := range list { + v := v // new var per iteration + defer func() { + use(v) // ok + }() + } + +The next example uses a go statement and has a similar problem. +In addition, it has a data race because the loop updates v +concurrent with the goroutines accessing it. + + for _, v := range elem { + go func() { + use(v) // incorrect, and a data race + }() + } + +A fix is the same as before. The checker also reports problems +in goroutines started by golang.org/x/sync/errgroup.Group. +A hard-to-spot variant of this form is common in parallel tests: + + func Test(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + use(test) // incorrect, and a data race + }) + } + } + +The t.Parallel() call causes the rest of the function to execute +concurrent with the loop. + +The analyzer reports references only in the last statement, +as it is not deep enough to understand the effects of subsequent +statements that might render the reference benign. +("Last statement" is defined recursively in compound +statements such as if, switch, and select.) See: https://golang.org/doc/go_faq.html#closures_and_goroutines diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index fdc46486b18..707c0ee2a15 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -299,7 +299,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "\"loopclosure\"", - Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n 3. a call testing.T.Run where the subtest body invokes t.Parallel()\n\nIn the case of (1) and (2), the analyzer only considers references in the last\nstatement of the loop body as it is not deep enough to understand the effects\nof subsequent statements which might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", + 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\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v.\n\n for _, v := range list {\n defer func() {\n use(v) // incorrect\n }()\n }\n\nOne fix is to create a new variable for each iteration of the loop:\n\n for _, v := range list {\n v := v // new var per iteration\n defer func() {\n use(v) // ok\n }()\n }\n\nThe next example uses a go statement and has a similar problem.\nIn addition, it has a data race because the loop updates v\nconcurrent with the goroutines accessing it.\n\n for _, v := range elem {\n go func() {\n use(v) // incorrect, and a data race\n }()\n }\n\nA fix is the same as before. The checker also reports problems\nin goroutines started by golang.org/x/sync/errgroup.Group.\nA hard-to-spot variant of this form is common in parallel tests:\n\n func Test(t *testing.T) {\n for _, test := range tests {\n t.Run(test.name, func(t *testing.T) {\n t.Parallel()\n use(test) // incorrect, and a data race\n })\n }\n }\n\nThe t.Parallel() call causes the rest of the function to execute\nconcurrent with the loop.\n\nThe analyzer reports references only in the last statement,\nas it is not deep enough to understand the effects of subsequent\nstatements that might render the reference benign.\n(\"Last statement\" is defined recursively in compound\nstatements such as if, switch, and select.)\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", Default: "true", }, { @@ -932,7 +932,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "loopclosure", - Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n 3. a call testing.T.Run where the subtest body invokes t.Parallel()\n\nIn the case of (1) and (2), the analyzer only considers references in the last\nstatement of the loop body as it is not deep enough to understand the effects\nof subsequent statements which might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", + 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\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v.\n\n for _, v := range list {\n defer func() {\n use(v) // incorrect\n }()\n }\n\nOne fix is to create a new variable for each iteration of the loop:\n\n for _, v := range list {\n v := v // new var per iteration\n defer func() {\n use(v) // ok\n }()\n }\n\nThe next example uses a go statement and has a similar problem.\nIn addition, it has a data race because the loop updates v\nconcurrent with the goroutines accessing it.\n\n for _, v := range elem {\n go func() {\n use(v) // incorrect, and a data race\n }()\n }\n\nA fix is the same as before. The checker also reports problems\nin goroutines started by golang.org/x/sync/errgroup.Group.\nA hard-to-spot variant of this form is common in parallel tests:\n\n func Test(t *testing.T) {\n for _, test := range tests {\n t.Run(test.name, func(t *testing.T) {\n t.Parallel()\n use(test) // incorrect, and a data race\n })\n }\n }\n\nThe t.Parallel() call causes the rest of the function to execute\nconcurrent with the loop.\n\nThe analyzer reports references only in the last statement,\nas it is not deep enough to understand the effects of subsequent\nstatements that might render the reference benign.\n(\"Last statement\" is defined recursively in compound\nstatements such as if, switch, and select.)\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", Default: true, }, { From 6eafd96cb20c3c228afb70bcd1d99b6413057488 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Mon, 21 Nov 2022 15:13:39 -0500 Subject: [PATCH 458/723] gopls: fix formatting for vulncheck results in hover The "Fixed in version" message for each vulnerability appears in an unindented new line when the description already contains new lines. This change puts this message an indented new line for both multiline descriptions and descriptions rendered as a single line. Another option would be to only do this fix for messages that already have this message rendered on a new line. Change-Id: I23c46356599fe3d76f1fc6d4cc076248a8bce9ef Reviewed-on: https://go-review.googlesource.com/c/tools/+/452517 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim Run-TryBot: Suzy Mueller --- gopls/internal/lsp/mod/hover.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 7706262fbfb..55735b45854 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -182,13 +182,13 @@ func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulnchec } func fixedVersionInfo(v *govulncheck.Vuln, modPath string) string { - fix := "\n**No fix is available.**" + fix := "\n\n **No fix is available.**" for _, m := range v.Modules { if m.Path != modPath { continue } if m.FixedVersion != "" { - fix = "\nFixed in " + m.FixedVersion + "." + fix = "\n\n Fixed in " + m.FixedVersion + "." } break } From 060c049c4674911187753a9d919629fc696127a7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 21 Nov 2022 18:21:06 -0500 Subject: [PATCH 459/723] go/packages: fix doc typo Fixes golang/go#34806 Change-Id: I21dea6def53030360890e888e29b33dfc9e4eed4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452578 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- go/packages/packages.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/packages/packages.go b/go/packages/packages.go index 046212347c0..9df20919ba2 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -1021,7 +1021,7 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { tc := &types.Config{ Importer: importer, - // Type-check bodies of functions only in non-initial packages. + // Type-check bodies of functions only in initial packages. // Example: for import graph A->B->C and initial packages {A,C}, // we can ignore function bodies in B. IgnoreFuncBodies: ld.Mode&NeedDeps == 0 && !lpkg.initial, From fb7be641d995d25275526612e5efaa4fea881e32 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 21 Nov 2022 16:43:40 -0500 Subject: [PATCH 460/723] gopls/internal/lsp/command: return the vulncheck progress token Return the token used for reporting progress on vulncheck invocations, so that clients can fetch results after the vulncheck invocation completes. Change-Id: If07e9fac01bee4709143cfd0b0dfb2183b229dd7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452575 gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/doc/commands.md | 10 ++++ gopls/internal/lsp/command.go | 30 +++++++++- gopls/internal/lsp/command/command_gen.go | 2 +- gopls/internal/lsp/command/interface.go | 10 +++- gopls/internal/lsp/regtest/env.go | 60 ++++++++++++++----- gopls/internal/lsp/regtest/env_test.go | 10 ++-- gopls/internal/lsp/regtest/expectation.go | 35 +++++++++-- gopls/internal/lsp/regtest/wrappers.go | 4 +- gopls/internal/lsp/source/api_json.go | 9 +-- .../regtest/codelens/codelens_test.go | 10 ++-- .../regtest/codelens/gcdetails_test.go | 4 +- gopls/internal/regtest/misc/vuln_test.go | 42 +++++++++---- 12 files changed, 172 insertions(+), 54 deletions(-) diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index b6eee96f881..667db225ef6 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -315,6 +315,16 @@ Args: } ``` +Result: + +``` +{ + // Token holds the progress token for LSP workDone reporting of the vulncheck + // invocation. + "Token": interface{}, +} +``` + ### **Start the gopls debug server** Identifier: `gopls.start_debugging` diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 90b8408dcad..4a537728197 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -76,6 +76,13 @@ type commandDeps struct { type commandFunc func(context.Context, commandDeps) error +// run performs command setup for command execution, and invokes the given run +// function. If cfg.async is set, run executes the given func in a separate +// goroutine, and returns as soon as setup is complete and the goroutine is +// scheduled. +// +// Invariant: if the resulting error is non-nil, the given run func will +// (eventually) be executed exactly once. func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run commandFunc) (err error) { if cfg.requireSave { var unsaved []string @@ -844,16 +851,25 @@ func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.U return ret, err } -func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) error { +func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) (command.RunVulncheckResult, error) { if args.URI == "" { - return errors.New("VulncheckArgs is missing URI field") + return command.RunVulncheckResult{}, errors.New("VulncheckArgs is missing URI field") } + + // Return the workdone token so that clients can identify when this + // vulncheck invocation is complete. + // + // Since the run function executes asynchronously, we use a channel to + // synchronize the start of the run and return the token. + tokenChan := make(chan protocol.ProgressToken, 1) err := c.run(ctx, commandConfig{ async: true, // need to be async to be cancellable progress: "govulncheck", requireSave: true, forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { + tokenChan <- deps.work.Token() + view := deps.snapshot.View() opts := view.Options() // quickly test if gopls is compiled to support govulncheck @@ -920,5 +936,13 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc Message: fmt.Sprintf("Found %v", strings.Join(affecting, ", ")), }) }) - return err + if err != nil { + return command.RunVulncheckResult{}, err + } + select { + case <-ctx.Done(): + return command.RunVulncheckResult{}, ctx.Err() + case token := <-tokenChan: + return command.RunVulncheckResult{Token: token}, nil + } } diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go index 5111e673eeb..7fa02001aa7 100644 --- a/gopls/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -175,7 +175,7 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte if err := UnmarshalArgs(params.Arguments, &a0); err != nil { return nil, err } - return nil, s.RunVulncheckExp(ctx, a0) + return s.RunVulncheckExp(ctx, a0) case "gopls.start_debugging": var a0 DebuggingArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index e6ab13da47b..4cd19ca6b48 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -153,7 +153,7 @@ type Interface interface { // RunVulncheckExp: Run vulncheck (experimental) // // Run vulnerability check (`govulncheck`). - RunVulncheckExp(context.Context, VulncheckArgs) error + RunVulncheckExp(context.Context, VulncheckArgs) (RunVulncheckResult, error) // FetchVulncheckResult: Get known vulncheck result // @@ -319,6 +319,14 @@ type VulncheckArgs struct { // TODO: -tests } +// RunVulncheckResult holds the result of asynchronously starting the vulncheck +// command. +type RunVulncheckResult struct { + // Token holds the progress token for LSP workDone reporting of the vulncheck + // invocation. + Token protocol.ProgressToken +} + type VulncheckResult struct { Vuln []Vuln diff --git a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go index 2374dab19ef..96c3db38a2b 100644 --- a/gopls/internal/lsp/regtest/env.go +++ b/gopls/internal/lsp/regtest/env.go @@ -55,10 +55,8 @@ func NewAwaiter(workdir *fake.Workdir) *Awaiter { return &Awaiter{ workdir: workdir, state: State{ - diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), - outstandingWork: make(map[protocol.ProgressToken]*workProgress), - startedWork: make(map[string]uint64), - completedWork: make(map[string]uint64), + diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), + work: make(map[protocol.ProgressToken]*workProgress), }, waiters: make(map[int]*condition), } @@ -92,16 +90,48 @@ type State struct { // outstandingWork is a map of token->work summary. All tokens are assumed to // be string, though the spec allows for numeric tokens as well. When work // completes, it is deleted from this map. - outstandingWork map[protocol.ProgressToken]*workProgress - startedWork map[string]uint64 - completedWork map[string]uint64 + work map[protocol.ProgressToken]*workProgress +} + +// outstandingWork counts started but not complete work items by title. +func (s State) outstandingWork() map[string]uint64 { + outstanding := make(map[string]uint64) + for _, work := range s.work { + if !work.complete { + outstanding[work.title]++ + } + } + return outstanding +} + +// completedWork counts complete work items by title. +func (s State) completedWork() map[string]uint64 { + completed := make(map[string]uint64) + for _, work := range s.work { + if work.complete { + completed[work.title]++ + } + } + return completed +} + +// startedWork counts started (and possibly complete) work items. +func (s State) startedWork() map[string]uint64 { + started := make(map[string]uint64) + for _, work := range s.work { + started[work.title]++ + } + return started } type workProgress struct { title, msg string percent float64 + complete bool } +// This method, provided for debugging, accesses mutable fields without a lock, +// so it must not be called concurrent with any State mutation. func (s State) String() string { var b strings.Builder b.WriteString("#### log messages (see RPC logs for full text):\n") @@ -124,7 +154,10 @@ func (s State) String() string { } b.WriteString("\n") b.WriteString("#### outstanding work:\n") - for token, state := range s.outstandingWork { + for token, state := range s.work { + if state.complete { + continue + } name := state.title if name == "" { name = fmt.Sprintf("!NO NAME(token: %s)", token) @@ -132,7 +165,7 @@ func (s State) String() string { fmt.Fprintf(&b, "\t%s: %.2f\n", name, state.percent) } b.WriteString("#### completed work:\n") - for name, count := range s.completedWork { + for name, count := range s.completedWork() { fmt.Fprintf(&b, "\t%s: %d\n", name, count) } return b.String() @@ -187,14 +220,14 @@ func (a *Awaiter) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDo a.mu.Lock() defer a.mu.Unlock() - a.state.outstandingWork[m.Token] = &workProgress{} + a.state.work[m.Token] = &workProgress{} return nil } func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) error { a.mu.Lock() defer a.mu.Unlock() - work, ok := a.state.outstandingWork[m.Token] + work, ok := a.state.work[m.Token] if !ok { panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m)) } @@ -202,7 +235,6 @@ func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) erro switch kind := v["kind"]; kind { case "begin": work.title = v["title"].(string) - a.state.startedWork[work.title] = a.state.startedWork[work.title] + 1 if msg, ok := v["message"]; ok { work.msg = msg.(string) } @@ -214,9 +246,7 @@ func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) erro work.msg = msg.(string) } case "end": - title := a.state.outstandingWork[m.Token].title - a.state.completedWork[title] = a.state.completedWork[title] + 1 - delete(a.state.outstandingWork, m.Token) + work.complete = true } a.checkConditionsLocked() return nil diff --git a/gopls/internal/lsp/regtest/env_test.go b/gopls/internal/lsp/regtest/env_test.go index 824c602df4d..e334faa905c 100644 --- a/gopls/internal/lsp/regtest/env_test.go +++ b/gopls/internal/lsp/regtest/env_test.go @@ -15,9 +15,7 @@ import ( func TestProgressUpdating(t *testing.T) { a := &Awaiter{ state: State{ - outstandingWork: make(map[protocol.ProgressToken]*workProgress), - startedWork: make(map[string]uint64), - completedWork: make(map[string]uint64), + work: make(map[protocol.ProgressToken]*workProgress), }, } ctx := context.Background() @@ -57,10 +55,10 @@ func TestProgressUpdating(t *testing.T) { t.Fatal(err) } } - if _, ok := a.state.outstandingWork["foo"]; ok { - t.Error("got work entry for \"foo\", want none") + if !a.state.work["foo"].complete { + t.Error("work entry \"foo\" is incomplete, want complete") } - got := *a.state.outstandingWork["bar"] + got := *a.state.work["bar"] want := workProgress{title: "bar work", percent: 42} if got != want { t.Errorf("work progress for \"bar\": %v, want %v", got, want) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index e5beb042713..c365ae97bbd 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -179,7 +179,7 @@ func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) * // $/progress API that has not completed. func NoOutstandingWork() SimpleExpectation { check := func(s State) Verdict { - if len(s.outstandingWork) == 0 { + if len(s.outstandingWork()) == 0 { return Met } return Unmet @@ -347,7 +347,7 @@ func (e *Env) DoneWithClose() Expectation { // See CompletedWork. func StartedWork(title string, atLeast uint64) SimpleExpectation { check := func(s State) Verdict { - if s.startedWork[title] >= atLeast { + if s.startedWork()[title] >= atLeast { return Met } return Unmet @@ -364,7 +364,8 @@ func StartedWork(title string, atLeast uint64) SimpleExpectation { // progress notification title to identify the work we expect to be completed. func CompletedWork(title string, count uint64, atLeast bool) SimpleExpectation { check := func(s State) Verdict { - if s.completedWork[title] == count || atLeast && s.completedWork[title] > count { + completed := s.completedWork() + if completed[title] == count || atLeast && completed[title] > count { return Met } return Unmet @@ -379,12 +380,38 @@ func CompletedWork(title string, count uint64, atLeast bool) SimpleExpectation { } } +// CompletedProgress expects that workDone progress is complete for the given +// progress token. +// +// If the token is not a progress token that the client has seen, this +// expectation is Unmeetable. +func CompletedProgress(token protocol.ProgressToken) SimpleExpectation { + check := func(s State) Verdict { + work, ok := s.work[token] + if !ok { + return Unmeetable // TODO(rfindley): refactor to allow the verdict to explain this result + } + if work.complete { + return Met + } + return Unmet + } + desc := fmt.Sprintf("completed work for token %v", token) + return SimpleExpectation{ + check: check, + description: desc, + } +} + // OutstandingWork expects a work item to be outstanding. The given title must // be an exact match, whereas the given msg must only be contained in the work // item's message. func OutstandingWork(title, msg string) SimpleExpectation { check := func(s State) Verdict { - for _, work := range s.outstandingWork { + for _, work := range s.work { + if work.complete { + continue + } if work.title == title && strings.Contains(work.msg, msg) { return Met } diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index 13e5f7bc819..e2d62e62141 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -335,7 +335,7 @@ func (e *Env) CodeLens(path string) []protocol.CodeLens { // ExecuteCodeLensCommand executes the command for the code lens matching the // given command name. -func (e *Env) ExecuteCodeLensCommand(path string, cmd command.Command) { +func (e *Env) ExecuteCodeLensCommand(path string, cmd command.Command, result interface{}) { e.T.Helper() lenses := e.CodeLens(path) var lens protocol.CodeLens @@ -352,7 +352,7 @@ func (e *Env) ExecuteCodeLensCommand(path string, cmd command.Command) { e.ExecuteCommand(&protocol.ExecuteCommandParams{ Command: lens.Command.Command, Arguments: lens.Command.Arguments, - }, nil) + }, result) } func (e *Env) ExecuteCommand(params *protocol.ExecuteCommandParams, result interface{}) { diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 707c0ee2a15..fc691a1fbb3 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -759,10 +759,11 @@ var GeneratedAPIJSON = &APIJSON{ ArgDoc: "{\n\t// The test file containing the tests to run.\n\t\"URI\": string,\n\t// Specific test names to run, e.g. TestFoo.\n\t\"Tests\": []string,\n\t// Specific benchmarks to run, e.g. BenchmarkFoo.\n\t\"Benchmarks\": []string,\n}", }, { - Command: "gopls.run_vulncheck_exp", - Title: "Run vulncheck (experimental)", - Doc: "Run vulnerability check (`govulncheck`).", - ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", + Command: "gopls.run_vulncheck_exp", + Title: "Run vulncheck (experimental)", + Doc: "Run vulnerability check (`govulncheck`).", + ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", + ResultDoc: "{\n\t// Token holds the progress token for LSP workDone reporting of the vulncheck\n\t// invocation.\n\t\"Token\": interface{},\n}", }, { Command: "gopls.start_debugging", diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 096d572f906..b79f5a3fe0a 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -204,7 +204,7 @@ require golang.org/x/hello v1.2.3 env.Await(env.DoneWithChangeWatchedFiles()) env.OpenFile("a/go.mod") env.OpenFile("b/go.mod") - env.ExecuteCodeLensCommand("a/go.mod", command.CheckUpgrades) + env.ExecuteCodeLensCommand("a/go.mod", command.CheckUpgrades, nil) d := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( @@ -218,9 +218,9 @@ require golang.org/x/hello v1.2.3 ), ) // Check for upgrades in b/go.mod and then clear them. - env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades) + env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades, nil) env.Await(env.DiagnosticAtRegexpWithMessage("b/go.mod", `require`, "can be upgraded")) - env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics) + env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics, nil) env.Await(EmptyDiagnostics("b/go.mod")) // Apply the diagnostics to a/go.mod. @@ -282,7 +282,7 @@ func main() { ` WithOptions(ProxyFiles(proxy)).Run(t, shouldRemoveDep, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - env.ExecuteCodeLensCommand("go.mod", command.Tidy) + env.ExecuteCodeLensCommand("go.mod", command.Tidy, nil) env.Await(env.DoneWithChangeWatchedFiles()) got := env.Editor.BufferText("go.mod") const wantGoMod = `module mod.com @@ -332,7 +332,7 @@ func Foo() { )) // Regenerate cgo, fixing the diagnostic. - env.ExecuteCodeLensCommand("cgo.go", command.RegenerateCgo) + env.ExecuteCodeLensCommand("cgo.go", command.RegenerateCgo, nil) env.Await(EmptyDiagnostics("cgo.go")) }) } diff --git a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go index 762694ac718..497b9730703 100644 --- a/gopls/internal/regtest/codelens/gcdetails_test.go +++ b/gopls/internal/regtest/codelens/gcdetails_test.go @@ -46,7 +46,7 @@ func main() { }, ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") - env.ExecuteCodeLensCommand("main.go", command.GCDetails) + env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) d := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( @@ -79,7 +79,7 @@ func main() { env.Await(DiagnosticAt("main.go", 5, 13)) // Toggle the GC details code lens again so now it should be off. - env.ExecuteCodeLensCommand("main.go", command.GCDetails) + env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) env.Await( EmptyDiagnostics("main.go"), ) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index a9587c2f440..df1b666b24d 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -172,14 +172,18 @@ func main() { t.Fatal("got no vulncheck codelens") } // Run Command included in the codelens. + var result command.RunVulncheckResult env.ExecuteCommand(&protocol.ExecuteCommandParams{ Command: lens.Command.Command, Arguments: lens.Command.Arguments, - }, nil) + }, &result) + env.Await( - CompletedWork("govulncheck", 1, true), - // TODO(hyangah): once the diagnostics are published, wait for diagnostics. - ShownMessage("Found GOSTDLIB"), + OnceMet( + CompletedProgress(result.Token), + // TODO(hyangah): once the diagnostics are published, wait for diagnostics. + ShownMessage("Found GOSTDLIB"), + ), ) testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GOSTDLIB"}}) }) @@ -357,16 +361,23 @@ func TestRunVulncheckWarning(t *testing.T) { WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp) + var result command.RunVulncheckResult + env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp, &result) gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.Await( - CompletedWork("govulncheck", 1, true), - ShownMessage("Found"), + OnceMet( + CompletedProgress(result.Token), + ShownMessage("Found"), + ), + ) + // Vulncheck diagnostics asynchronous to the vulncheck command. + env.Await( OnceMet( env.DiagnosticAtRegexp("go.mod", `golang.org/amod`), ReadDiagnostics("go.mod", gotDiagnostics), ), ) + testFetchVulncheckResult(t, env, map[string][]string{ "go.mod": {"GO-2022-01", "GO-2022-02", "GO-2022-03"}, }) @@ -499,14 +510,23 @@ func TestRunVulncheckInfo(t *testing.T) { defer db.Clean() WithOptions(opts...).Run(t, workspace2, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp) + var result command.RunVulncheckResult + env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp, &result) gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.Await( - CompletedWork("govulncheck", 1, true), + OnceMet( + CompletedProgress(result.Token), + ShownMessage("No vulnerabilities found"), // only count affecting vulnerabilities. + ), + ) + + // Vulncheck diagnostics asynchronous to the vulncheck command. + env.Await( OnceMet( env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"), - ReadDiagnostics("go.mod", gotDiagnostics)), - ShownMessage("No vulnerabilities found")) // only count affecting vulnerabilities. + ReadDiagnostics("go.mod", gotDiagnostics), + ), + ) testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GO-2022-02"}}) // wantDiagnostics maps a module path in the require From 5c35617670abc72469228d78a2fe093508559dde Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Mon, 21 Nov 2022 17:11:23 -0500 Subject: [PATCH 461/723] gopls: add quick fixes for all vulnerabilities Fix a bug that was causing only the upgrade quick fix for the last vulnerability to be shown as code actions. Change-Id: If4ad57ae6ddbb077a7e01e6bb7271066a3d800ef Reviewed-on: https://go-review.googlesource.com/c/tools/+/452576 gopls-CI: kokoro Run-TryBot: Suzy Mueller Reviewed-by: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot --- gopls/internal/lsp/mod/diagnostics.go | 32 +++++++++++++----------- gopls/internal/regtest/misc/vuln_test.go | 18 ++++++++++--- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index ec0993fc9ee..72029114790 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -251,16 +251,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, if err != nil { return nil, err // TODO: bug report } - // Add an upgrade for module@latest. - // TODO(suzmue): verify if latest is the same as fixedVersion. - latest, err := getUpgradeCodeAction(fh, req, "latest") - if err != nil { - return nil, err // TODO: bug report - } - fixes = []source.SuggestedFix{ - source.SuggestedFixFromCommand(cmd, protocol.QuickFix), - source.SuggestedFixFromCommand(latest, protocol.QuickFix), - } + fixes = append(fixes, source.SuggestedFixFromCommand(cmd, protocol.QuickFix)) } } @@ -271,6 +262,15 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, if len(warning) > 0 { severity = protocol.SeverityWarning } + if len(fixes) > 0 { + // Add an upgrade for module@latest. + // TODO(suzmue): verify if latest is the same as fixedVersion. + latest, err := getUpgradeCodeAction(fh, req, "latest") + if err != nil { + return nil, err // TODO: bug report + } + fixes = append([]source.SuggestedFix{source.SuggestedFixFromCommand(latest, protocol.QuickFix)}, fixes...) + } sort.Strings(warning) sort.Strings(info) @@ -392,9 +392,9 @@ func upgradeTitle(fixedVersion string) string { // required module and returns a more selective list of upgrade code actions, // where the code actions have been deduped. func SelectUpgradeCodeActions(actions []protocol.CodeAction) []protocol.CodeAction { - // TODO(suzmue): we can further limit the code actions to only return the most - // recent version that will fix all the vulnerabilities. - + if len(actions) <= 1 { + return actions // return early if no sorting necessary + } set := make(map[string]protocol.CodeAction) for _, action := range actions { set[action.Command.Title] = action @@ -409,7 +409,11 @@ func SelectUpgradeCodeActions(actions []protocol.CodeAction) []protocol.CodeActi vi, vj := getUpgradeVersion(result[i]), getUpgradeVersion(result[j]) return vi == "latest" || (vj != "latest" && semver.Compare(vi, vj) > 0) }) - return result + // Choose at most one specific version and the latest. + if getUpgradeVersion(result[0]) == "latest" { + return result[:2] + } + return result[:1] } func getUpgradeVersion(p protocol.CodeAction) string { diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index df1b666b24d..f2a5be5ecf1 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -325,6 +325,16 @@ package bvuln func Vuln() { // something evil } +-- golang.org/amod@v1.0.6/go.mod -- +module golang.org/amod + +go 1.14 +-- golang.org/amod@v1.0.6/avuln/avuln.go -- +package avuln + +type VulnData struct {} +func (v VulnData) Vuln1() {} +func (v VulnData) Vuln2() {} ` func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) { @@ -387,14 +397,14 @@ func TestRunVulncheckWarning(t *testing.T) { lineY := env.RegexpSearch("y/y.go", `c\.C2\(\)\(\)`) wantDiagnostics := map[string]vulnDiagExpectation{ "golang.org/amod": { - applyAction: "Upgrade to v1.0.4", + applyAction: "Upgrade to v1.0.6", diagnostics: []vulnDiag{ { msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.", severity: protocol.SeverityWarning, codeActions: []string{ "Upgrade to latest", - "Upgrade to v1.0.4", + "Upgrade to v1.0.6", }, relatedInfo: []vulnRelatedInfo{ {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1 @@ -404,7 +414,7 @@ func TestRunVulncheckWarning(t *testing.T) { }, codeActions: []string{ "Upgrade to latest", - "Upgrade to v1.0.4", + "Upgrade to v1.0.6", }, hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, }, @@ -452,7 +462,7 @@ go 1.18 require golang.org/cmod v1.1.3 require ( - golang.org/amod v1.0.4 // indirect + golang.org/amod v1.0.6 // indirect golang.org/bmod v0.5.0 // indirect ) ` From 7cda55e58b08b34fab25f7f7710a167794765dcf Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 22 Nov 2022 12:09:11 -0500 Subject: [PATCH 462/723] gopls/internal/lsp/cache: wire in mod vulnerability analysis Add a stub implementation of incremental go.mod vulnerability analysis, following the pattern of mod tidy analysis. Change-Id: Iaaea71f6a8a7e735f8b595289a528e78b19e2560 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452770 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/cache/mod_vuln.go | 72 ++++++++++++++++++++++++ gopls/internal/lsp/cache/session.go | 1 + gopls/internal/lsp/cache/snapshot.go | 6 +- gopls/internal/vulncheck/command.go | 12 ++-- gopls/internal/vulncheck/command_test.go | 17 +++--- gopls/internal/vulncheck/vulncheck.go | 6 ++ 6 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 gopls/internal/lsp/cache/mod_vuln.go diff --git a/gopls/internal/lsp/cache/mod_vuln.go b/gopls/internal/lsp/cache/mod_vuln.go new file mode 100644 index 00000000000..d783f5dc1f5 --- /dev/null +++ b/gopls/internal/lsp/cache/mod_vuln.go @@ -0,0 +1,72 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "context" + "os" + + "golang.org/x/tools/gopls/internal/govulncheck" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/gopls/internal/vulncheck" + "golang.org/x/tools/internal/memoize" +) + +// ModVuln returns import vulnerability analysis for the given go.mod URI. +// Concurrent requests are combined into a single command. +func (s *snapshot) ModVuln(ctx context.Context, modURI span.URI) (*govulncheck.Result, error) { + s.mu.Lock() + entry, hit := s.modVulnHandles.Get(modURI) + s.mu.Unlock() + + type modVuln struct { + result *govulncheck.Result + err error + } + + // Cache miss? + if !hit { + // If the file handle is an overlay, it may not be written to disk. + // The go.mod file has to be on disk for vulncheck to work. + // + // TODO(hyangah): use overlays for vulncheck. + fh, err := s.GetFile(ctx, modURI) + if err != nil { + return nil, err + } + if _, ok := fh.(*overlay); ok { + if info, _ := os.Stat(modURI.Filename()); info == nil { + return nil, source.ErrNoModOnDisk + } + } + + handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg interface{}) interface{} { + result, err := modVulnImpl(ctx, arg.(*snapshot), modURI) + return modVuln{result, err} + }) + + entry = handle + s.mu.Lock() + s.modVulnHandles.Set(modURI, entry, nil) + s.mu.Unlock() + } + + // Await result. + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) + if err != nil { + return nil, err + } + res := v.(modVuln) + return res.result, res.err +} + +func modVulnImpl(ctx context.Context, s *snapshot, uri span.URI) (*govulncheck.Result, error) { + fh, err := s.GetFile(ctx, uri) + if err != nil { + return nil, err + } + return vulncheck.AnalyzeVulnerableImports(ctx, s, fh) +} diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 4916f4235f5..28acd5d98dd 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -283,6 +283,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, parseModHandles: persistent.NewMap(uriLessInterface), parseWorkHandles: persistent.NewMap(uriLessInterface), modTidyHandles: persistent.NewMap(uriLessInterface), + modVulnHandles: persistent.NewMap(uriLessInterface), modWhyHandles: persistent.NewMap(uriLessInterface), knownSubdirs: newKnownDirsSet(), workspace: workspace, diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index c2cb946afaa..9a4c5b37811 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -140,6 +140,7 @@ type snapshot struct { // the view's go.mod file. modTidyHandles *persistent.Map // from span.URI to *memoize.Promise[modTidyResult] modWhyHandles *persistent.Map // from span.URI to *memoize.Promise[modWhyResult] + modVulnHandles *persistent.Map // from span.URI to *memoize.Promise[modVulnResult] workspace *workspace // (not guarded by mu) @@ -230,6 +231,7 @@ func (s *snapshot) destroy(destroyedBy string) { s.parseModHandles.Destroy() s.parseWorkHandles.Destroy() s.modTidyHandles.Destroy() + s.modVulnHandles.Destroy() s.modWhyHandles.Destroy() if s.workspaceDir != "" { @@ -1713,6 +1715,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC parseWorkHandles: s.parseWorkHandles.Clone(), modTidyHandles: s.modTidyHandles.Clone(), modWhyHandles: s.modWhyHandles.Clone(), + modVulnHandles: s.modVulnHandles.Clone(), knownSubdirs: s.knownSubdirs.Clone(), workspace: newWorkspace, } @@ -1752,6 +1755,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Invalidate go.mod-related handles. result.modTidyHandles.Delete(uri) result.modWhyHandles.Delete(uri) + result.modVulnHandles.Delete(uri) // Invalidate handles for cached symbols. result.symbolizeHandles.Delete(uri) @@ -1815,9 +1819,9 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // TODO(maybe): Only delete mod handles for // which the withoutURI is relevant. // Requires reverse-engineering the go command. (!) - result.modTidyHandles.Clear() result.modWhyHandles.Clear() + result.modVulnHandles.Clear() } result.parseModHandles.Delete(uri) diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 646e622bef0..a45290a88fe 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -26,10 +26,10 @@ import ( ) func init() { - Govulncheck = govulncheck + Govulncheck = govulncheckFunc } -func govulncheck(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) { +func govulncheckFunc(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) { if patterns == "" { patterns = "." } @@ -39,7 +39,7 @@ func govulncheck(ctx context.Context, cfg *packages.Config, patterns string) (re return res, err } - c := cmd{Client: dbClient} + c := Cmd{Client: dbClient} vulns, err := c.Run(ctx, cfg, patterns) if err != nil { return res, err @@ -65,14 +65,14 @@ type Vuln = command.Vuln type CallStack = command.CallStack type StackEntry = command.StackEntry -// cmd is an in-process govulncheck command runner +// Cmd is an in-process govulncheck command runner // that uses the provided client.Client. -type cmd struct { +type Cmd struct { Client client.Client } // Run runs the govulncheck after loading packages using the provided packages.Config. -func (c *cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) (_ []Vuln, err error) { +func (c *Cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) (_ []Vuln, err error) { logger := log.New(log.Default().Writer(), "", 0) cfg.Mode |= packages.NeedModule | packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index 36fd76d7858..b7a270e35aa 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -5,7 +5,7 @@ //go:build go1.18 // +build go1.18 -package vulncheck +package vulncheck_test import ( "bytes" @@ -24,6 +24,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/gopls/internal/vulncheck/vulntest" "golang.org/x/tools/internal/testenv" ) @@ -40,7 +41,7 @@ func TestCmd_Run(t *testing.T) { t.Fatal(err) } - cmd := &cmd{Client: cli} + cmd := &vulncheck.Cmd{Client: cli} cfg := packagesCfg(ctx, snapshot) result, err := cmd.Run(ctx, cfg, "./...") if err != nil { @@ -63,7 +64,7 @@ func TestCmd_Run(t *testing.T) { var want = []report{ { - Vuln: Vuln{ + Vuln: vulncheck.Vuln{ ID: "GO-2022-01", Details: "Something.\n", Symbol: "VulnData.Vuln1", @@ -86,7 +87,7 @@ func TestCmd_Run(t *testing.T) { }, }, { - Vuln: Vuln{ + Vuln: vulncheck.Vuln{ ID: "GO-2022-02", Symbol: "Vuln", PkgPath: "golang.org/bmod/bvuln", @@ -101,7 +102,7 @@ func TestCmd_Run(t *testing.T) { }, }, { - Vuln: Vuln{ + Vuln: vulncheck.Vuln{ ID: "GO-2022-03", Details: "unaffecting vulnerability.\n", ModPath: "golang.org/amod", @@ -131,12 +132,12 @@ func TestCmd_Run(t *testing.T) { } type report struct { - Vuln + vulncheck.Vuln // Trace is stringified Vuln.CallStacks CallStacksStr []string } -func toReport(v Vuln) report { +func toReport(v vulncheck.Vuln) report { var r = report{Vuln: v} for _, s := range v.CallStacks { r.CallStacksStr = append(r.CallStacksStr, CallStackString(s)) @@ -144,7 +145,7 @@ func toReport(v Vuln) report { return r } -func CallStackString(callstack CallStack) string { +func CallStackString(callstack vulncheck.CallStack) string { var b bytes.Buffer for _, entry := range callstack { fname := filepath.Base(entry.URI.SpanURI().Filename()) diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go index 1cbf18e86db..c4dfe511c03 100644 --- a/gopls/internal/vulncheck/vulncheck.go +++ b/gopls/internal/vulncheck/vulncheck.go @@ -12,7 +12,9 @@ import ( "context" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/source" ) // Govulncheck runs the in-process govulncheck implementation. @@ -20,3 +22,7 @@ import ( var Govulncheck func(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) = nil var Main func(cfg packages.Config, patterns ...string) error = nil + +func AnalyzeVulnerableImports(ctx context.Context, snapshot source.Snapshot, modfile source.FileHandle) (*govulncheck.Result, error) { + panic("not implemented") +} From 50df761c64795b48492fca3e7437c7324749ae47 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 22 Nov 2022 16:13:26 -0500 Subject: [PATCH 463/723] gopls: skip vulnerabilities that are probably fixed in hover If the version in the go.mod >= the fixed version for a vulnerability, it is probably fixed. Remove the information about the vulnerability to reduce noise. Change-Id: Ie2f02f3c358f3e7782eb47979d52efe58fc9508a Reviewed-on: https://go-review.googlesource.com/c/tools/+/452895 TryBot-Result: Gopher Robot Run-TryBot: Suzy Mueller gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/mod/hover.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 55735b45854..70686851ff3 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -12,6 +12,7 @@ import ( "strings" "golang.org/x/mod/modfile" + "golang.org/x/mod/semver" "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" @@ -71,7 +72,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Get the vulnerability info. - affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI())[fh.URI()], req.Mod.Path) + affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI())[fh.URI()], req.Mod.Path, req.Mod.Version) // Get the `go mod why` results for the given file. why, err := snapshot.ModWhy(ctx, fh) @@ -117,7 +118,7 @@ func formatHeader(modpath string, options *source.Options) string { return b.String() } -func lookupVulns(vulns *govulncheck.Result, modpath string) (affecting, nonaffecting []*govulncheck.Vuln) { +func lookupVulns(vulns *govulncheck.Result, modpath, version string) (affecting, nonaffecting []*govulncheck.Vuln) { if vulns == nil { return nil, nil } @@ -126,6 +127,25 @@ func lookupVulns(vulns *govulncheck.Result, modpath string) (affecting, nonaffec if mod.Path != modpath { continue } + // It is possible that the source code was changed since the last + // govulncheck run and information in the `vulns` info is stale. + // For example, imagine that a user is in the middle of updating + // problematic modules detected by the govulncheck run by applying + // quick fixes. Stale diagnostics can be confusing and prevent the + // user from quickly locating the next module to fix. + // Ideally we should rerun the analysis with the updated module + // dependencies or any other code changes, but we are not yet + // in the position of automatically triggerring the analysis + // (govulncheck can take a while). We also don't know exactly what + // part of source code was changed since `vulns` was computed. + // As a heuristic, we assume that a user upgrades the affecting + // module to the version with the fix or the latest one, and if the + // version in the require statement is equal to or higher than the + // fixed version, skip the vulnerability information in the hover. + // Eventually, the user has to rerun govulncheck. + if mod.FixedVersion != "" && semver.IsValid(version) && semver.Compare(mod.FixedVersion, version) <= 0 { + continue + } if vuln.IsCalled() { affecting = append(affecting, vuln) } else { From 2b56641c370d3ca1748a985a68f0e4d68763b2b8 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 22 Nov 2022 16:48:56 -0500 Subject: [PATCH 464/723] internal/gcimporter: adjust the number of expected packages in TestStdlib CL 451995 omitted the 'x/tools/...' pattern on Android, which allowed the packages to load successfully but left the count lower than expected (not all packages in 'std' build on Android). This is the only failure mode I continue to see regularly on the android-amd64-emu builder. Change-Id: I0b4fac647935c745a9894621a6ebcfb5f2b0620e Reviewed-on: https://go-review.googlesource.com/c/tools/+/452778 Reviewed-by: Russ Cox TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills Auto-Submit: Bryan Mills --- internal/gcimporter/stdlib_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/gcimporter/stdlib_test.go b/internal/gcimporter/stdlib_test.go index 34ddb306f14..33ff7958118 100644 --- a/internal/gcimporter/stdlib_test.go +++ b/internal/gcimporter/stdlib_test.go @@ -35,12 +35,14 @@ func TestStdlib(t *testing.T) { // If we have the full source code for x/tools, also load and type-check that. cfg := &packages.Config{Mode: packages.LoadAllSyntax} patterns := []string{"std"} + minPkgs := 225 // 'GOOS=plan9 go1.18 list std | wc -l' reports 228; most other platforms have more. switch runtime.GOOS { case "android", "ios": // The go_.*_exec script for mobile builders only copies over the source tree // for the package under test. default: patterns = append(patterns, "golang.org/x/tools/...") + minPkgs += 160 // At the time of writing, 'GOOS=plan9 go list ./... | wc -l' reports 188. } pkgs, err := packages.Load(cfg, patterns...) if err != nil { @@ -49,7 +51,7 @@ func TestStdlib(t *testing.T) { if packages.PrintErrors(pkgs) > 0 { t.Fatal("there were errors during loading") } - if len(pkgs) < 240 { + if len(pkgs) < minPkgs { t.Errorf("too few packages (%d) were loaded", len(pkgs)) } From 7cfde84b7fdacbfdd420421ad8c919957c628812 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 22 Nov 2022 15:34:02 -0500 Subject: [PATCH 465/723] gopls/internal/vulncheck: add import info based vuln scanning This vulnerability check mode reports the vulnerabilities that affect any of the packages known to gopls. This is not yet wired into diagnostics and hover handlers. Tests will be added after cl/452516 and diagnostics/hover handler change are merged. Change-Id: Id9b30ccd58780f5fd66eb72f2a74b0ed98bfbaff Reviewed-on: https://go-review.googlesource.com/c/tools/+/452516 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Hyang-Ah Hana Kim --- gopls/internal/govulncheck/util.go | 5 +- gopls/internal/lsp/cache/mod_vuln.go | 5 +- gopls/internal/vulncheck/command.go | 289 ++++++++++++++++++++++++-- gopls/internal/vulncheck/vulncheck.go | 7 +- 4 files changed, 287 insertions(+), 19 deletions(-) diff --git a/gopls/internal/govulncheck/util.go b/gopls/internal/govulncheck/util.go index fc63d5678ad..9bcb9b4aaed 100644 --- a/gopls/internal/govulncheck/util.go +++ b/gopls/internal/govulncheck/util.go @@ -19,9 +19,12 @@ import ( // LatestFixed returns the latest fixed version in the list of affected ranges, // or the empty string if there are no fixed versions. -func LatestFixed(as []osv.Affected) string { +func LatestFixed(modulePath string, as []osv.Affected) string { v := "" for _, a := range as { + if a.Package.Name != modulePath { + continue + } for _, r := range a.Ranges { if r.Type == osv.TypeSemver { for _, e := range r.Events { diff --git a/gopls/internal/lsp/cache/mod_vuln.go b/gopls/internal/lsp/cache/mod_vuln.go index d783f5dc1f5..b16c8c57ba7 100644 --- a/gopls/internal/lsp/cache/mod_vuln.go +++ b/gopls/internal/lsp/cache/mod_vuln.go @@ -64,9 +64,12 @@ func (s *snapshot) ModVuln(ctx context.Context, modURI span.URI) (*govulncheck.R } func modVulnImpl(ctx context.Context, s *snapshot, uri span.URI) (*govulncheck.Result, error) { + if vulncheck.VulnerablePackages == nil { + return &govulncheck.Result{}, nil + } fh, err := s.GetFile(ctx, uri) if err != nil { return nil, err } - return vulncheck.AnalyzeVulnerableImports(ctx, s, fh) + return vulncheck.VulnerablePackages(ctx, s, fh) } diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index a45290a88fe..004541f9040 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -13,12 +13,17 @@ import ( "fmt" "log" "os" + "regexp" "sort" "strings" + "sync" + "golang.org/x/mod/semver" + "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" - gvc "golang.org/x/tools/gopls/internal/govulncheck" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/vuln/client" gvcapi "golang.org/x/vuln/exp/govulncheck" "golang.org/x/vuln/osv" @@ -27,6 +32,8 @@ import ( func init() { Govulncheck = govulncheckFunc + + VulnerablePackages = vulnerablePackages } func govulncheckFunc(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) { @@ -34,7 +41,7 @@ func govulncheckFunc(ctx context.Context, cfg *packages.Config, patterns string) patterns = "." } - dbClient, err := client.NewClient(findGOVULNDB(cfg), client.Options{HTTPCache: gvc.DefaultCache()}) + dbClient, err := client.NewClient(findGOVULNDB(cfg.Env), client.Options{HTTPCache: govulncheck.DefaultCache()}) if err != nil { return res, err } @@ -49,8 +56,8 @@ func govulncheckFunc(ctx context.Context, cfg *packages.Config, patterns string) return res, err } -func findGOVULNDB(cfg *packages.Config) []string { - for _, kv := range cfg.Env { +func findGOVULNDB(env []string) []string { + for _, kv := range env { if strings.HasPrefix(kv, "GOVULNDB=") { return strings.Split(kv[len("GOVULNDB="):], ",") } @@ -79,7 +86,7 @@ func (c *Cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps logger.Println("loading packages...") - loadedPkgs, err := gvc.LoadPackages(cfg, patterns...) + loadedPkgs, err := govulncheck.LoadPackages(cfg, patterns...) if err != nil { logger.Printf("%v", err) return nil, fmt.Errorf("package load failed") @@ -97,7 +104,7 @@ func (c *Cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) r.Vulns = filterCalled(r) logger.Printf("found %d vulnerabilities.\n", len(r.Vulns)) - callInfo := gvc.GetCallInfo(r, loadedPkgs) + callInfo := govulncheck.GetCallInfo(r, loadedPkgs) return toVulns(callInfo, unaffectedMods) // TODO: add import graphs. } @@ -148,15 +155,15 @@ func filterUnaffected(vulns []*vulncheck.Vuln) map[string][]*osv.Entry { return output } -func fixed(v *osv.Entry) string { - lf := gvc.LatestFixed(v.Affected) +func fixed(modPath string, v *osv.Entry) string { + lf := govulncheck.LatestFixed(modPath, v.Affected) if lf != "" && lf[0] != 'v' { lf = "v" + lf } return lf } -func toVulns(ci *gvc.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, error) { +func toVulns(ci *govulncheck.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, error) { var vulns []Vuln for _, vg := range ci.VulnGroups { @@ -165,7 +172,7 @@ func toVulns(ci *gvc.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, ID: v0.OSV.ID, PkgPath: v0.PkgPath, CurrentVersion: ci.ModuleVersions[v0.ModPath], - FixedVersion: fixed(v0.OSV), + FixedVersion: fixed(v0.ModPath, v0.OSV), Details: v0.OSV.Details, Aliases: v0.OSV.Aliases, @@ -180,7 +187,7 @@ func toVulns(ci *gvc.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, vuln.CallStacks = append(vuln.CallStacks, toCallStack(css[0])) // TODO(hyangah): https://go-review.googlesource.com/c/vuln/+/425183 added position info // in the summary but we don't need the info. Allow SummarizeCallStack to skip it optionally. - sum := trimPosPrefix(gvc.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath)) + sum := trimPosPrefix(govulncheck.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath)) vuln.CallStackSummaries = append(vuln.CallStackSummaries, sum) } } @@ -195,7 +202,7 @@ func toVulns(ci *gvc.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, ModPath: m, URL: href(v0), CurrentVersion: "", - FixedVersion: fixed(v0), + FixedVersion: fixed(m, v0), } vulns = append(vulns, vuln) } @@ -230,8 +237,8 @@ func init() { return err } logf("Loaded %d packages and their dependencies", len(pkgs)) - cli, err := client.NewClient(findGOVULNDB(&cfg), client.Options{ - HTTPCache: gvc.DefaultCache(), + cli, err := client.NewClient(findGOVULNDB(cfg.Env), client.Options{ + HTTPCache: govulncheck.DefaultCache(), }) if err != nil { return err @@ -250,3 +257,257 @@ func init() { return nil } } + +var ( + // Regexp for matching go tags. The groups are: + // 1 the major.minor version + // 2 the patch version, or empty if none + // 3 the entire prerelease, if present + // 4 the prerelease type ("beta" or "rc") + // 5 the prerelease number + tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc|-pre)(\d+))?$`) +) + +// This is a modified copy of pkgsite/internal/stdlib:VersionForTag. +func GoTagToSemver(tag string) string { + if tag == "" { + return "" + } + + tag = strings.Fields(tag)[0] + // Special cases for go1. + if tag == "go1" { + return "v1.0.0" + } + if tag == "go1.0" { + return "" + } + m := tagRegexp.FindStringSubmatch(tag) + if m == nil { + return "" + } + version := "v" + m[1] + if m[2] != "" { + version += m[2] + } else { + version += ".0" + } + if m[3] != "" { + if !strings.HasPrefix(m[4], "-") { + version += "-" + } + version += m[4] + "." + m[5] + } + return version +} + +// semverToGoTag returns the Go standard library repository tag corresponding +// to semver, a version string without the initial "v". +// Go tags differ from standard semantic versions in a few ways, +// such as beginning with "go" instead of "v". +func semverToGoTag(v string) string { + if strings.HasPrefix(v, "v0.0.0") { + return "master" + } + // Special case: v1.0.0 => go1. + if v == "v1.0.0" { + return "go1" + } + if !semver.IsValid(v) { + return fmt.Sprintf("", v) + } + goVersion := semver.Canonical(v) + prerelease := semver.Prerelease(goVersion) + versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease) + patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".") + if patch == "0" { + versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0") + } + goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v")) + if prerelease != "" { + // Go prereleases look like "beta1" instead of "beta.1". + // "beta1" is bad for sorting (since beta10 comes before beta9), so + // require the dot form. + i := finalDigitsIndex(prerelease) + if i >= 1 { + if prerelease[i-1] != '.' { + return fmt.Sprintf("", v) + } + // Remove the dot. + prerelease = prerelease[:i-1] + prerelease[i:] + } + goVersion += strings.TrimPrefix(prerelease, "-") + } + return goVersion +} + +// finalDigitsIndex returns the index of the first digit in the sequence of digits ending s. +// If s doesn't end in digits, it returns -1. +func finalDigitsIndex(s string) int { + // Assume ASCII (since the semver package does anyway). + var i int + for i = len(s) - 1; i >= 0; i-- { + if s[i] < '0' || s[i] > '9' { + break + } + } + if i == len(s)-1 { + return -1 + } + return i + 1 +} + +// vulnerablePackages queries the vulndb and reports which vulnerabilities +// apply to this snapshot. The result contains a set of packages, +// grouped by vuln ID and by module. +func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile source.FileHandle) (*govulncheck.Result, error) { + metadata, err := snapshot.AllValidMetadata(ctx) + if err != nil { + return nil, err + } + + // Group packages by modules since vuln db is keyed by module. + metadataByModule := map[source.PackagePath][]*source.Metadata{} + for _, md := range metadata { + // TODO(hyangah): delete after go.dev/cl/452057 is merged. + // After the cl, this becomes an impossible condition. + if md == nil { + continue + } + mi := md.Module + if mi == nil { + continue + } + modulePath := source.PackagePath(mi.Path) + metadataByModule[modulePath] = append(metadataByModule[modulePath], md) + } + + // Request vuln entries from remote service. + cli, err := client.NewClient( + findGOVULNDB(snapshot.View().Options().EnvSlice()), + client.Options{HTTPCache: govulncheck.DefaultCache()}) + if err != nil { + return nil, err + } + // Keys are osv.Entry.IDs + vulnsResult := map[string]*govulncheck.Vuln{} + var ( + group errgroup.Group + mu sync.Mutex + ) + + group.SetLimit(10) + for path, mds := range metadataByModule { + path, mds := path, mds + group.Go(func() error { + + effectiveModule := mds[0].Module + for effectiveModule.Replace != nil { + effectiveModule = effectiveModule.Replace + } + ver := effectiveModule.Version + + // TODO(go.dev/issues/56312): batch these requests for efficiency. + vulns, err := cli.GetByModule(ctx, effectiveModule.Path) + if err != nil { + return err + } + if len(vulns) == 0 { // No known vulnerability. + return nil + } + + // set of packages in this module known to gopls. + // This will be lazily initialized when we need it. + var knownPkgs map[source.PackagePath]bool + + // Report vulnerabilities that affect packages of this module. + for _, entry := range vulns { + var vulnerablePkgs []*govulncheck.Package + + for _, a := range entry.Affected { + if a.Package.Ecosystem != osv.GoEcosystem || a.Package.Name != effectiveModule.Path { + continue + } + if !a.Ranges.AffectsSemver(ver) { + continue + } + for _, imp := range a.EcosystemSpecific.Imports { + if knownPkgs == nil { + knownPkgs = toPackagePathSet(mds) + } + if knownPkgs[source.PackagePath(imp.Path)] { + vulnerablePkgs = append(vulnerablePkgs, &govulncheck.Package{ + Path: imp.Path, + }) + } + } + } + if len(vulnerablePkgs) == 0 { + continue + } + mu.Lock() + vuln, ok := vulnsResult[entry.ID] + if !ok { + vuln = &govulncheck.Vuln{OSV: entry} + vulnsResult[entry.ID] = vuln + } + vuln.Modules = append(vuln.Modules, &govulncheck.Module{ + Path: string(path), + FoundVersion: ver, + FixedVersion: fixedVersion(effectiveModule.Path, entry.Affected), + Packages: vulnerablePkgs, + }) + mu.Unlock() + } + return nil + }) + } + if err := group.Wait(); err != nil { + return nil, err + } + + vulns := make([]*govulncheck.Vuln, 0, len(vulnsResult)) + for _, v := range vulnsResult { + vulns = append(vulns, v) + } + // Sort so the results are deterministic. + sort.Slice(vulns, func(i, j int) bool { + return vulns[i].OSV.ID < vulns[j].OSV.ID + }) + ret := &govulncheck.Result{ + Vulns: vulns, + } + return ret, nil +} + +// toPackagePathSet transforms the metadata to a set of package paths. +func toPackagePathSet(mds []*source.Metadata) map[source.PackagePath]bool { + pkgPaths := make(map[source.PackagePath]bool, len(mds)) + for _, md := range mds { + pkgPaths[md.PkgPath] = true + } + return pkgPaths +} + +func fixedVersion(modulePath string, affected []osv.Affected) string { + fixed := govulncheck.LatestFixed(modulePath, affected) + if fixed != "" { + fixed = versionString(modulePath, fixed) + } + return fixed +} + +// versionString prepends a version string prefix (`v` or `go` +// depending on the modulePath) to the given semver-style version string. +func versionString(modulePath, version string) string { + if version == "" { + return "" + } + v := "v" + version + // These are internal Go module paths used by the vuln DB + // when listing vulns in standard library and the go command. + if modulePath == "stdlib" || modulePath == "toolchain" { + return semverToGoTag(v) + } + return v +} diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go index c4dfe511c03..71ac117c947 100644 --- a/gopls/internal/vulncheck/vulncheck.go +++ b/gopls/internal/vulncheck/vulncheck.go @@ -23,6 +23,7 @@ var Govulncheck func(ctx context.Context, cfg *packages.Config, patterns string) var Main func(cfg packages.Config, patterns ...string) error = nil -func AnalyzeVulnerableImports(ctx context.Context, snapshot source.Snapshot, modfile source.FileHandle) (*govulncheck.Result, error) { - panic("not implemented") -} +// VulnerablePackages queries the vulndb and reports which vulnerabilities +// apply to this snapshot. The result contains a set of packages, +// grouped by vuln ID and by module. +var VulnerablePackages func(ctx context.Context, snapshot source.Snapshot, modfile source.FileHandle) (*govulncheck.Result, error) = nil From d39685aa6c0cf4458cf61d15db68a71f5b677543 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 22 Nov 2022 13:18:01 -0500 Subject: [PATCH 466/723] gopls/internal/lsp/source: Package clarifications Some preparatory simplifications: - Group the fields of Package into metadata, parse info, type info. - Derive TypesSizes from metadata; eliminate field. - Remove MissingDependencies method by specializing it for its sole use, avoiding the need to invert the go/types heuristic. Also, clarify sort in codelens. Change-Id: Iecd5b49046207034c64acd5b0240a9fed7761538 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452835 gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/check.go | 1 - gopls/internal/lsp/cache/pkg.go | 39 +------------------ gopls/internal/lsp/cache/snapshot.go | 32 ++++++++++++++- gopls/internal/lsp/code_lens.go | 8 ++-- .../lsp/source/completion/completion.go | 7 +--- gopls/internal/lsp/source/view.go | 28 +++++++------ 6 files changed, 53 insertions(+), 62 deletions(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index a22e285b6a1..663cf25b184 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -457,7 +457,6 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil Selections: make(map[*ast.SelectorExpr]*types.Selection), Scopes: make(map[ast.Node]*types.Scope), }, - typesSizes: m.TypesSizes, } typeparams.InitInstanceInfo(pkg.typesInfo) diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 5c43a82ad5c..6466b8ee1dd 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -10,7 +10,6 @@ import ( "go/scanner" "go/token" "go/types" - "sort" "golang.org/x/mod/module" "golang.org/x/tools/gopls/internal/lsp/source" @@ -40,7 +39,6 @@ type pkg struct { typeErrors []types.Error types *types.Package typesInfo *types.Info - typesSizes types.Sizes hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors analyses memoize.Store // maps analyzer.Name to Promise[actionResult] @@ -113,7 +111,7 @@ func (p *pkg) GetTypesInfo() *types.Info { } func (p *pkg) GetTypesSizes() types.Sizes { - return p.typesSizes + return p.m.TypesSizes } func (p *pkg) ForTest() string { @@ -144,41 +142,6 @@ func (p *pkg) ResolveImportPath(importPath ImportPath) (source.Package, error) { return nil, fmt.Errorf("package does not import %s", importPath) } -func (p *pkg) MissingDependencies() []ImportPath { - // We don't invalidate metadata for import deletions, - // so check the package imports via the *types.Package. - // - // rfindley says: it looks like this is intending to implement - // a heuristic "if go list couldn't resolve import paths to - // packages, then probably you're not in GOPATH or a module". - // This is used to determine if we need to show a warning diagnostic. - // It looks like this logic is implementing the heuristic that - // "even if the metadata has a MissingDep, if the types.Package - // doesn't need that dep anymore we shouldn't show the warning". - // But either we're outside of GOPATH/Module, or we're not... - // - // adonovan says: I think this effectively reverses the - // heuristic used by the type checker when Importer.Import - // returns an error---go/types synthesizes a package whose - // Path is the import path (sans "vendor/")---hence the - // dubious ImportPath() conversion. A blank DepsByImpPath - // entry means a missing import. - // - // If we invalidate the metadata for import deletions (which - // should be fast) then we can simply return the blank entries - // in DepsByImpPath. (They are PackageIDs not PackagePaths, - // but the caller only cares whether the set is empty!) - var missing []ImportPath - for _, pkg := range p.types.Imports() { - importPath := ImportPath(pkg.Path()) - if id, ok := p.m.DepsByImpPath[importPath]; ok && id == "" { - missing = append(missing, importPath) - } - } - sort.Slice(missing, func(i, j int) bool { return missing[i] < missing[j] }) - return missing -} - func (p *pkg) Imports() []source.Package { var result []source.Package // unordered for _, dep := range p.deps { diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 9a4c5b37811..369235fcd70 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1430,13 +1430,43 @@ func shouldShowAdHocPackagesWarning(snapshot source.Snapshot, pkgs []source.Pack return "" } for _, pkg := range pkgs { - if len(pkg.MissingDependencies()) > 0 { + if hasMissingDependencies(pkg) { return adHocPackagesWarning } } return "" } +func hasMissingDependencies(pkg source.Package) bool { + // We don't invalidate metadata for import deletions, + // so check the package imports via the syntax tree + // (not via types.Package.Imports, since it contains packages + // synthesized under the vendoring-hostile assumption that + // ImportPath equals PackagePath). + // + // rfindley says: it looks like this is intending to implement + // a heuristic "if go list couldn't resolve import paths to + // packages, then probably you're not in GOPATH or a module". + // This is used to determine if we need to show a warning diagnostic. + // It looks like this logic is implementing the heuristic that + // "even if the metadata has a MissingDep, if the types.Package + // doesn't need that dep anymore we shouldn't show the warning". + // But either we're outside of GOPATH/Module, or we're not... + // + // If we invalidate the metadata for import deletions (which + // should be fast) then we can simply return the blank entries + // in DepsByImpPath. + for _, f := range pkg.GetSyntax() { + for _, imp := range f.Imports { + importPath := source.UnquoteImportPath(imp) + if _, err := pkg.ResolveImportPath(importPath); err != nil { + return true + } + } + } + return false +} + func containsCommandLineArguments(pkgs []source.Package) bool { for _, pkg := range pkgs { if source.IsCommandLineArguments(pkg.ID()) { diff --git a/gopls/internal/lsp/code_lens.go b/gopls/internal/lsp/code_lens.go index 4bbe2bb34c6..f554e798c3c 100644 --- a/gopls/internal/lsp/code_lens.go +++ b/gopls/internal/lsp/code_lens.go @@ -9,11 +9,11 @@ import ( "fmt" "sort" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/mod" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" ) func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) { @@ -48,10 +48,10 @@ func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) } sort.Slice(result, func(i, j int) bool { a, b := result[i], result[j] - if protocol.CompareRange(a.Range, b.Range) == 0 { - return a.Command.Command < b.Command.Command + if cmp := protocol.CompareRange(a.Range, b.Range); cmp != 0 { + return cmp < 0 } - return protocol.CompareRange(a.Range, b.Range) < 0 + return a.Command.Command < b.Command.Command }) return result, nil } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 144d6587da0..1eeabc9873c 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -1083,12 +1083,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { // Is sel a qualified identifier? if id, ok := sel.X.(*ast.Ident); ok { if pkgName, ok := c.pkg.GetTypesInfo().Uses[id].(*types.PkgName); ok { - var pkg source.Package - for _, imp := range c.pkg.Imports() { - if imp.PkgPath() == source.PackagePath(pkgName.Imported().Path()) { - pkg = imp - } - } + pkg, _ := c.pkg.DirectDep(source.PackagePath(pkgName.Imported().Path())) // If the package is not imported, try searching for unimported // completions. if pkg == nil && c.opts.unimported { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index aed3f54938c..ad1012994ff 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -676,28 +676,32 @@ type ( ImportPath string // path that appears in an import declaration (e.g. "example.com/foo") ) -// Package represents a Go package that has been type-checked. It maintains -// only the relevant fields of a *go/packages.Package. +// Package represents a Go package that has been parsed and type-checked. +// It maintains only the relevant fields of a *go/packages.Package. type Package interface { + // Metadata: ID() PackageID Name() PackageName PkgPath() PackagePath - CompiledGoFiles() []*ParsedGoFile - File(uri span.URI) (*ParsedGoFile, error) + GetTypesSizes() types.Sizes + ForTest() string + Version() *module.Version + + // Results of parsing: FileSet() *token.FileSet - GetSyntax() []*ast.File + ParseMode() ParseMode + CompiledGoFiles() []*ParsedGoFile // (borrowed) + File(uri span.URI) (*ParsedGoFile, error) + GetSyntax() []*ast.File // (borrowed) + HasListOrParseErrors() bool + + // Results of type checking: GetTypes() *types.Package GetTypesInfo() *types.Info - GetTypesSizes() types.Sizes - ForTest() string DirectDep(path PackagePath) (Package, error) ResolveImportPath(path ImportPath) (Package, error) - MissingDependencies() []ImportPath // unordered - Imports() []Package - Version() *module.Version - HasListOrParseErrors() bool + Imports() []Package // new slice of all direct dependencies, unordered HasTypeErrors() bool - ParseMode() ParseMode } // A CriticalError is a workspace-wide error that generally prevents gopls from From 50ccef5d0e76d2aaf35def413043c04447d6334b Mon Sep 17 00:00:00 2001 From: Dung Le Date: Mon, 24 Oct 2022 18:11:33 -0400 Subject: [PATCH 467/723] internal/regtest/bench: add benchmark tests for gopls Change-Id: Ic25fdfde6a7236eee488299e24d8fe440fffc4ac Reviewed-on: https://go-review.googlesource.com/c/tools/+/450555 gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Dylan Le TryBot-Result: Gopher Robot --- .../regtest/bench/editor_features_test.go | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 gopls/internal/regtest/bench/editor_features_test.go diff --git a/gopls/internal/regtest/bench/editor_features_test.go b/gopls/internal/regtest/bench/editor_features_test.go new file mode 100644 index 00000000000..92df3446695 --- /dev/null +++ b/gopls/internal/regtest/bench/editor_features_test.go @@ -0,0 +1,55 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bench + +import ( + "fmt" + "testing" +) + +func BenchmarkGoToDefinition(b *testing.B) { + env := benchmarkEnv(b) + + env.OpenFile("internal/imports/mod.go") + pos := env.RegexpSearch("internal/imports/mod.go", "ModuleJSON") + env.GoToDefinition("internal/imports/mod.go", pos) + env.Await(env.DoneWithOpen()) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + env.GoToDefinition("internal/imports/mod.go", pos) + } +} + +func BenchmarkFindAllReferences(b *testing.B) { + env := benchmarkEnv(b) + + env.OpenFile("internal/imports/mod.go") + pos := env.RegexpSearch("internal/imports/mod.go", "gopathwalk") + env.References("internal/imports/mod.go", pos) + env.Await(env.DoneWithOpen()) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + env.References("internal/imports/mod.go", pos) + } +} + +func BenchmarkRename(b *testing.B) { + env := benchmarkEnv(b) + + env.OpenFile("internal/imports/mod.go") + env.Await(env.DoneWithOpen()) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + pos := env.RegexpSearch("internal/imports/mod.go", "gopathwalk") + newName := fmt.Sprintf("%s%d", "gopathwalk", i) + env.Rename("internal/imports/mod.go", pos, newName) + } +} From 4b7d1b3118e6ab94b430c43c9ee61ee556a59985 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 22 Nov 2022 14:54:57 -0500 Subject: [PATCH 468/723] gopls: add diagnostics for both vulns used and not used Add separate diagnostics with different severity levels in order to capture both reachable and unreachable vulnerabilites in the diagnostics. Change-Id: Ib7fa422d9ef7f84e85a5da30b6cf766618cb583c Reviewed-on: https://go-review.googlesource.com/c/tools/+/452837 TryBot-Result: Gopher Robot Run-TryBot: Suzy Mueller gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/mod/diagnostics.go | 81 +++++++++++++++--------- gopls/internal/regtest/misc/vuln_test.go | 29 ++++++--- 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 72029114790..e3385205cd3 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -215,7 +215,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, // Map affecting vulns to 'warning' level diagnostics, // others to 'info' level diagnostics. // Fixes will include only the upgrades for warning level diagnostics. - var fixes []source.SuggestedFix + var warningFixes, infoFixes []source.SuggestedFix var warning, info []string var relatedInfo []source.RelatedInformation for _, mv := range vulns { @@ -251,52 +251,71 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, if err != nil { return nil, err // TODO: bug report } - fixes = append(fixes, source.SuggestedFixFromCommand(cmd, protocol.QuickFix)) + sf := source.SuggestedFixFromCommand(cmd, protocol.QuickFix) + if !vuln.IsCalled() { + infoFixes = append(infoFixes, sf) + } else { + warningFixes = append(warningFixes, sf) + } } } if len(warning) == 0 && len(info) == 0 { continue } - severity := protocol.SeverityInformation - if len(warning) > 0 { - severity = protocol.SeverityWarning + // Add an upgrade for module@latest. + // TODO(suzmue): verify if latest is the same as fixedVersion. + latest, err := getUpgradeCodeAction(fh, req, "latest") + if err != nil { + return nil, err // TODO: bug report } - if len(fixes) > 0 { - // Add an upgrade for module@latest. - // TODO(suzmue): verify if latest is the same as fixedVersion. - latest, err := getUpgradeCodeAction(fh, req, "latest") - if err != nil { - return nil, err // TODO: bug report - } - fixes = append([]source.SuggestedFix{source.SuggestedFixFromCommand(latest, protocol.QuickFix)}, fixes...) + sf := source.SuggestedFixFromCommand(latest, protocol.QuickFix) + if len(warningFixes) > 0 { + warningFixes = append(warningFixes, sf) + } + if len(infoFixes) > 0 { + infoFixes = append(infoFixes, sf) } sort.Strings(warning) sort.Strings(info) - var b strings.Builder - switch len(warning) { - case 0: - if n := len(info); n == 1 { + if len(warning) > 0 { + var b strings.Builder + switch len(warning) { + case 1: + fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", req.Mod.Path, warning[0]) + default: + fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", ")) + } + vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityWarning, + Source: source.Vulncheck, + Message: b.String(), + SuggestedFixes: warningFixes, + Related: relatedInfo, + }) + } + if len(info) > 0 { + var b strings.Builder + switch len(info) { + case 1: fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", req.Mod.Path, info[0]) - } else if n > 1 { + default: fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", req.Mod.Path, strings.Join(info, ", ")) } - case 1: - fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", req.Mod.Path, warning[0]) - default: - fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", ")) + vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityInformation, + Source: source.Vulncheck, + Message: b.String(), + SuggestedFixes: infoFixes, + Related: relatedInfo, + }) } - vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: severity, - Source: source.Vulncheck, - Message: b.String(), - SuggestedFixes: fixes, - Related: relatedInfo, - }) } return vulnDiagnostics, nil diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index f2a5be5ecf1..e50c38f2408 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -402,6 +402,18 @@ func TestRunVulncheckWarning(t *testing.T) { { msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.", severity: protocol.SeverityWarning, + codeActions: []string{ + "Upgrade to latest", + "Upgrade to v1.0.4", + }, + relatedInfo: []vulnRelatedInfo{ + {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1 + {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln2 + }, + }, + { + msg: "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.", + severity: protocol.SeverityInformation, codeActions: []string{ "Upgrade to latest", "Upgrade to v1.0.6", @@ -600,15 +612,14 @@ func testVulnDiagnostics(t *testing.T, env *Env, requireMod string, want vulnDia t.Errorf("code actions for %q do not match, want %v, got %v\n%v\n", w.msg, w.codeActions, gotActions, diff) continue } - - // Check that useful info is supplemented as hover. - if len(want.hover) > 0 { - hover, _ := env.Hover("go.mod", pos) - for _, part := range want.hover { - if !strings.Contains(hover.Value, part) { - t.Errorf("hover contents for %q do not match, want %v, got %v\n", w.msg, strings.Join(want.hover, ","), hover.Value) - break - } + } + // Check that useful info is supplemented as hover. + if len(want.hover) > 0 { + hover, _ := env.Hover("go.mod", pos) + for _, part := range want.hover { + if !strings.Contains(hover.Value, part) { + t.Errorf("hover contents for %q do not match, want %v, got %v\n", requireMod, strings.Join(want.hover, ","), hover.Value) + break } } } From 1ccccf8daa815cd16240099ae6e77cbfa642046f Mon Sep 17 00:00:00 2001 From: Tim King Date: Fri, 18 Nov 2022 15:12:26 -0800 Subject: [PATCH 469/723] go/ssa: deprecate coreType and _NormalTerms Removes coreType and _NormalTerms. Uses typeparams.CoreType instead. Change-Id: Ie7b82142c9ec5a0799784edc0917a2dd511658eb Reviewed-on: https://go-review.googlesource.com/c/tools/+/452156 Run-TryBot: Tim King Reviewed-by: Zvonimir Pavlinovic Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- go/ssa/builder.go | 40 +++++----- go/ssa/const.go | 4 +- go/ssa/coretype.go | 159 ++++++++-------------------------------- go/ssa/coretype_test.go | 6 +- go/ssa/emit.go | 18 +++-- go/ssa/print.go | 5 +- go/ssa/ssa.go | 2 +- go/ssa/util.go | 6 +- 8 files changed, 73 insertions(+), 167 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 04deb7b0633..be8d36a6eeb 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -275,7 +275,7 @@ func (b *builder) exprN(fn *Function, e ast.Expr) Value { return fn.emit(&c) case *ast.IndexExpr: - mapt := coreType(fn.typeOf(e.X)).(*types.Map) // ,ok must be a map. + mapt := typeparams.CoreType(fn.typeOf(e.X)).(*types.Map) // ,ok must be a map. lookup := &Lookup{ X: b.expr(fn, e.X), Index: emitConv(fn, b.expr(fn, e.Index), mapt.Key()), @@ -312,7 +312,7 @@ func (b *builder) builtin(fn *Function, obj *types.Builtin, args []ast.Expr, typ typ = fn.typ(typ) switch obj.Name() { case "make": - switch ct := coreType(typ).(type) { + switch ct := typeparams.CoreType(typ).(type) { case *types.Slice: n := b.expr(fn, args[1]) m := n @@ -482,7 +482,7 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { x = b.expr(fn, e.X) et = types.NewPointer(elem) case ixMap: - mt := coreType(xt).(*types.Map) + mt := typeparams.CoreType(xt).(*types.Map) return &element{ m: b.expr(fn, e.X), k: emitConv(fn, b.expr(fn, e.Index), mt.Key()), @@ -752,14 +752,14 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { var low, high, max Value var x Value xtyp := fn.typeOf(e.X) - switch coreType(xtyp).(type) { + switch typeparams.CoreType(xtyp).(type) { case *types.Array: // Potentially escaping. x = b.addr(fn, e.X, true).address(fn) case *types.Basic, *types.Slice, *types.Pointer: // *array x = b.expr(fn, e.X) default: - // coreType exception? + // core type exception? if isBytestring(xtyp) { x = b.expr(fn, e.X) // bytestring is handled as string and []byte. } else { @@ -841,7 +841,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { if recv, ok := sel.recv.(*typeparams.TypeParam); ok { // Emit a nil check if any possible instantiation of the // type parameter is an interface type. - if len(typeSetOf(recv)) > 0 { + if typeSetOf(recv).Len() > 0 { // recv has a concrete term its typeset. // So it cannot be instantiated as an interface. // @@ -906,7 +906,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { // An array in a register, a string or a combined type that contains // either an [_]array (ixArrVar) or string (ixValue). - // Note: for ixArrVar and coreType(xt)==nil can be IndexAddr and Load. + // Note: for ixArrVar and CoreType(xt)==nil can be IndexAddr and Load. index := b.expr(fn, e.Index) if isUntyped(index.Type()) { index = emitConv(fn, index, tInt) @@ -920,7 +920,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { return fn.emit(v) case ixMap: - ct := coreType(xt).(*types.Map) + ct := typeparams.CoreType(xt).(*types.Map) v := &Lookup{ X: b.expr(fn, e.X), Index: emitConv(fn, b.expr(fn, e.Index), ct.Key()), @@ -1120,7 +1120,7 @@ func (b *builder) setCall(fn *Function, e *ast.CallExpr, c *CallCommon) { b.setCallFunc(fn, e, c) // Then append the other actual parameters. - sig, _ := coreType(fn.typeOf(e.Fun)).(*types.Signature) + sig, _ := typeparams.CoreType(fn.typeOf(e.Fun)).(*types.Signature) if sig == nil { panic(fmt.Sprintf("no signature for call of %s", e.Fun)) } @@ -1253,8 +1253,8 @@ func (b *builder) arrayLen(fn *Function, elts []ast.Expr) int64 { // literal has type *T behaves like &T{}. // In that case, addr must hold a T, not a *T. func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero bool, sb *storebuf) { - typ := deref(fn.typeOf(e)) // type with name [may be type param] - t := deref(coreType(typ)).Underlying() // core type for comp lit case + typ := deref(fn.typeOf(e)) // type with name [may be type param] + t := deref(typeparams.CoreType(typ)).Underlying() // core type for comp lit case // Computing typ and t is subtle as these handle pointer types. // For example, &T{...} is valid even for maps and slices. // Also typ should refer to T (not *T) while t should be the core type of T. @@ -1269,13 +1269,13 @@ func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero // For `&T{f: 1}`, we compute `typ` and `t` as: // typeOf(&T{f: 1}) == *T // deref(*T) == T (typ) - // coreType(T) == N + // CoreType(T) == N // deref(N) == N // N.Underlying() == struct{f int} (t) // For `{f: 1}` in `[]S{{f: 1}}`, we compute `typ` and `t` as: // typeOf({f: 1}) == S // deref(S) == S (typ) - // coreType(S) == *N + // CoreType(S) == *N // deref(*N) == N // N.Underlying() == struct{f int} (t) switch t := t.(type) { @@ -1660,7 +1660,7 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { case *ast.SendStmt: // ch<- i ch := b.expr(fn, comm.Chan) - chtyp := coreType(fn.typ(ch.Type())).(*types.Chan) + chtyp := typeparams.CoreType(fn.typ(ch.Type())).(*types.Chan) st = &SelectState{ Dir: types.SendOnly, Chan: ch, @@ -1717,7 +1717,7 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { vars = append(vars, varIndex, varOk) for _, st := range states { if st.Dir == types.RecvOnly { - chtyp := coreType(fn.typ(st.Chan.Type())).(*types.Chan) + chtyp := typeparams.CoreType(fn.typ(st.Chan.Type())).(*types.Chan) vars = append(vars, anonVar(chtyp.Elem())) } } @@ -1916,7 +1916,7 @@ func (b *builder) rangeIndexed(fn *Function, x Value, tv types.Type, pos token.P k = emitLoad(fn, index) if tv != nil { - switch t := coreType(x.Type()).(type) { + switch t := typeparams.CoreType(x.Type()).(type) { case *types.Array: instr := &Index{ X: x, @@ -1988,7 +1988,7 @@ func (b *builder) rangeIter(fn *Function, x Value, tk, tv types.Type, pos token. okv := &Next{ Iter: it, - IsString: isBasic(coreType(x.Type())), + IsString: isBasic(typeparams.CoreType(x.Type())), } okv.setType(types.NewTuple( varOk, @@ -2038,7 +2038,7 @@ func (b *builder) rangeChan(fn *Function, x Value, tk types.Type, pos token.Pos) } recv.setPos(pos) recv.setType(types.NewTuple( - newVar("k", coreType(x.Type()).(*types.Chan).Elem()), + newVar("k", typeparams.CoreType(x.Type()).(*types.Chan).Elem()), varOk, )) ko := fn.emit(recv) @@ -2082,7 +2082,7 @@ func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { var k, v Value var loop, done *BasicBlock - switch rt := coreType(x.Type()).(type) { + switch rt := typeparams.CoreType(x.Type()).(type) { case *types.Slice, *types.Array, *types.Pointer: // *array k, v, loop, done = b.rangeIndexed(fn, x, tv, s.For) @@ -2160,7 +2160,7 @@ start: b.expr(fn, s.X) case *ast.SendStmt: - chtyp := coreType(fn.typeOf(s.Chan)).(*types.Chan) + chtyp := typeparams.CoreType(fn.typeOf(s.Chan)).(*types.Chan) fn.emit(&Send{ Chan: b.expr(fn, s.Chan), X: emitConv(fn, b.expr(fn, s.Value), chtyp.Elem()), diff --git a/go/ssa/const.go b/go/ssa/const.go index 3468eac7e13..a1bac7b914b 100644 --- a/go/ssa/const.go +++ b/go/ssa/const.go @@ -45,7 +45,7 @@ func soleTypeKind(typ types.Type) types.BasicInfo { // Candidates (perhaps all) are eliminated during the type-set // iteration, which executes at least once. state := types.IsBoolean | types.IsInteger | types.IsString - typeSetOf(typ).underIs(func(t types.Type) bool { + underIs(typeSetOf(typ), func(t types.Type) bool { var c types.BasicInfo if t, ok := t.(*types.Basic); ok { c = t.Info() @@ -169,7 +169,7 @@ func nillable(t types.Type) bool { case *types.Pointer, *types.Slice, *types.Chan, *types.Map, *types.Signature: return true case *types.Interface: - return len(typeSetOf(t)) == 0 // basic interface. + return typeSetOf(t).Len() == 0 // basic interface. default: return false } diff --git a/go/ssa/coretype.go b/go/ssa/coretype.go index 54bc4a8e6d5..128d61e4267 100644 --- a/go/ssa/coretype.go +++ b/go/ssa/coretype.go @@ -12,63 +12,8 @@ import ( // Utilities for dealing with core types. -// coreType returns the core type of T or nil if T does not have a core type. -// -// See https://go.dev/ref/spec#Core_types for the definition of a core type. -func coreType(T types.Type) types.Type { - U := T.Underlying() - if _, ok := U.(*types.Interface); !ok { - return U // for non-interface types, - } - - terms, err := _NormalTerms(U) - if len(terms) == 0 || err != nil { - // len(terms) -> empty type set of interface. - // err != nil => U is invalid, exceeds complexity bounds, or has an empty type set. - return nil // no core type. - } - - U = terms[0].Type().Underlying() - var identical int // i in [0,identical) => Identical(U, terms[i].Type().Underlying()) - for identical = 1; identical < len(terms); identical++ { - if !types.Identical(U, terms[identical].Type().Underlying()) { - break - } - } - - if identical == len(terms) { - // https://go.dev/ref/spec#Core_types - // "There is a single type U which is the underlying type of all types in the type set of T" - return U - } - ch, ok := U.(*types.Chan) - if !ok { - return nil // no core type as identical < len(terms) and U is not a channel. - } - // https://go.dev/ref/spec#Core_types - // "the type chan E if T contains only bidirectional channels, or the type chan<- E or - // <-chan E depending on the direction of the directional channels present." - for chans := identical; chans < len(terms); chans++ { - curr, ok := terms[chans].Type().Underlying().(*types.Chan) - if !ok { - return nil - } - if !types.Identical(ch.Elem(), curr.Elem()) { - return nil // channel elements are not identical. - } - if ch.Dir() == types.SendRecv { - // ch is bidirectional. We can safely always use curr's direction. - ch = curr - } else if curr.Dir() != types.SendRecv && ch.Dir() != curr.Dir() { - // ch and curr are not bidirectional and not the same direction. - return nil - } - } - return ch -} - // isBytestring returns true if T has the same terms as interface{[]byte | string}. -// These act like a coreType for some operations: slice expressions, append and copy. +// These act like a core type for some operations: slice expressions, append and copy. // // See https://go.dev/ref/spec#Core_types for the details on bytestring. func isBytestring(T types.Type) bool { @@ -78,11 +23,11 @@ func isBytestring(T types.Type) bool { } tset := typeSetOf(U) - if len(tset) != 2 { + if tset.Len() != 2 { return false } hasBytes, hasString := false, false - tset.underIs(func(t types.Type) bool { + underIs(tset, func(t types.Type) bool { switch { case isString(t): hasString = true @@ -94,87 +39,45 @@ func isBytestring(T types.Type) bool { return hasBytes && hasString } -// _NormalTerms returns a slice of terms representing the normalized structural -// type restrictions of a type, if any. -// -// For all types other than *types.TypeParam, *types.Interface, and -// *types.Union, this is just a single term with Tilde() == false and -// Type() == typ. For *types.TypeParam, *types.Interface, and *types.Union, see -// below. -// -// Structural type restrictions of a type parameter are created via -// non-interface types embedded in its constraint interface (directly, or via a -// chain of interface embeddings). For example, in the declaration type -// T[P interface{~int; m()}] int the structural restriction of the type -// parameter P is ~int. -// -// With interface embedding and unions, the specification of structural type -// restrictions may be arbitrarily complex. For example, consider the -// following: -// -// type A interface{ ~string|~[]byte } -// -// type B interface{ int|string } -// -// type C interface { ~string|~int } -// -// type T[P interface{ A|B; C }] int -// -// In this example, the structural type restriction of P is ~string|int: A|B -// expands to ~string|~[]byte|int|string, which reduces to ~string|~[]byte|int, -// which when intersected with C (~string|~int) yields ~string|int. -// -// _NormalTerms computes these expansions and reductions, producing a -// "normalized" form of the embeddings. A structural restriction is normalized -// if it is a single union containing no interface terms, and is minimal in the -// sense that removing any term changes the set of types satisfying the -// constraint. It is left as a proof for the reader that, modulo sorting, there -// is exactly one such normalized form. -// -// Because the minimal representation always takes this form, _NormalTerms -// returns a slice of tilde terms corresponding to the terms of the union in -// the normalized structural restriction. An error is returned if the type is -// invalid, exceeds complexity bounds, or has an empty type set. In the latter -// case, _NormalTerms returns ErrEmptyTypeSet. -// -// _NormalTerms makes no guarantees about the order of terms, except that it -// is deterministic. -// -// This is a copy of x/exp/typeparams.NormalTerms which x/tools cannot depend on. -// TODO(taking): Remove this copy when possible. -func _NormalTerms(typ types.Type) ([]*typeparams.Term, error) { +// termList is a list of types. +type termList []*typeparams.Term // type terms of the type set +func (s termList) Len() int { return len(s) } +func (s termList) At(i int) types.Type { return s[i].Type() } + +// typeSetOf returns the type set of typ. Returns an empty typeset on an error. +func typeSetOf(typ types.Type) termList { + // This is a adaptation of x/exp/typeparams.NormalTerms which x/tools cannot depend on. + var terms []*typeparams.Term + var err error switch typ := typ.(type) { case *typeparams.TypeParam: - return typeparams.StructuralTerms(typ) + terms, err = typeparams.StructuralTerms(typ) case *typeparams.Union: - return typeparams.UnionTermSet(typ) + terms, err = typeparams.UnionTermSet(typ) case *types.Interface: - return typeparams.InterfaceTermSet(typ) + terms, err = typeparams.InterfaceTermSet(typ) default: - return []*typeparams.Term{typeparams.NewTerm(false, typ)}, nil + // Common case. + // Specializing the len=1 case to avoid a slice + // had no measurable space/time benefit. + terms = []*typeparams.Term{typeparams.NewTerm(false, typ)} } -} -// typeSetOf returns the type set of typ. Returns an empty typeset on an error. -func typeSetOf(typ types.Type) typeSet { - terms, err := _NormalTerms(typ) if err != nil { - return nil + return termList(nil) } - return terms + return termList(terms) } -type typeSet []*typeparams.Term // type terms of the type set - // underIs calls f with the underlying types of the specific type terms // of s and reports whether all calls to f returned true. If there are // no specific terms, underIs returns the result of f(nil). -func (s typeSet) underIs(f func(types.Type) bool) bool { - if len(s) == 0 { +func underIs(s termList, f func(types.Type) bool) bool { + if s.Len() == 0 { return f(nil) } - for _, t := range s { - u := t.Type().Underlying() + for i := 0; i < s.Len(); i++ { + u := s.At(i).Underlying() if !f(u) { return false } @@ -199,14 +102,14 @@ func indexType(typ types.Type) (types.Type, indexMode) { case *types.Basic: return tByte, ixValue // must be a string case *types.Interface: - terms, err := _NormalTerms(U) - if len(terms) == 0 || err != nil { + tset := typeSetOf(U) + if tset.Len() == 0 { return nil, ixInvalid // no underlying terms or error is empty. } - elem, mode := indexType(terms[0].Type()) - for i := 1; i < len(terms) && mode != ixInvalid; i++ { - e, m := indexType(terms[i].Type()) + elem, mode := indexType(tset.At(0)) + for i := 1; i < tset.Len() && mode != ixInvalid; i++ { + e, m := indexType(tset.At(i)) if !types.Identical(elem, e) { // if type checked, just a sanity check return nil, ixInvalid } diff --git a/go/ssa/coretype_test.go b/go/ssa/coretype_test.go index c4ed290fd85..74fe4db1667 100644 --- a/go/ssa/coretype_test.go +++ b/go/ssa/coretype_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 ssa +package ssa_test import ( "go/ast" @@ -91,7 +91,7 @@ func TestCoreType(t *testing.T) { t.Fatalf("Eval(%s) failed: %v", test.expr, err) } - ct := coreType(tv.Type) + ct := typeparams.CoreType(tv.Type) var got string if ct == nil { got = "" @@ -99,7 +99,7 @@ func TestCoreType(t *testing.T) { got = ct.String() } if got != test.want { - t.Errorf("coreType(%s) = %v, want %v", test.expr, got, test.want) + t.Errorf("CoreType(%s) = %v, want %v", test.expr, got, test.want) } } } diff --git a/go/ssa/emit.go b/go/ssa/emit.go index b041491b6e8..3754325048f 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -11,6 +11,8 @@ import ( "go/ast" "go/token" "go/types" + + "golang.org/x/tools/internal/typeparams" ) // emitNew emits to f a new (heap Alloc) instruction allocating an @@ -210,8 +212,8 @@ func emitConv(f *Function, val Value, typ types.Type) Value { src_types := typeSetOf(ut_src) // Just a change of type, but not value or representation? - preserving := src_types.underIs(func(s types.Type) bool { - return dst_types.underIs(func(d types.Type) bool { + preserving := underIs(src_types, func(s types.Type) bool { + return underIs(dst_types, func(d types.Type) bool { return s != nil && d != nil && isValuePreserving(s, d) // all (s -> d) are value preserving. }) }) @@ -261,8 +263,8 @@ func emitConv(f *Function, val Value, typ types.Type) Value { } // Conversion from slice to array pointer? - slice2ptr := src_types.underIs(func(s types.Type) bool { - return dst_types.underIs(func(d types.Type) bool { + slice2ptr := underIs(src_types, func(s types.Type) bool { + return underIs(dst_types, func(d types.Type) bool { return s != nil && d != nil && isSliceToArrayPointer(s, d) // all (s->d) are slice to array pointer conversion. }) }) @@ -273,8 +275,8 @@ func emitConv(f *Function, val Value, typ types.Type) Value { } // Conversion from slice to array? - slice2array := src_types.underIs(func(s types.Type) bool { - return dst_types.underIs(func(d types.Type) bool { + slice2array := underIs(src_types, func(s types.Type) bool { + return underIs(dst_types, func(d types.Type) bool { return s != nil && d != nil && isSliceToArray(s, d) // all (s->d) are slice to array conversion. }) }) @@ -428,7 +430,7 @@ func emitTailCall(f *Function, call *Call) { // value of a field. func emitImplicitSelections(f *Function, v Value, indices []int, pos token.Pos) Value { for _, index := range indices { - fld := coreType(deref(v.Type())).(*types.Struct).Field(index) + fld := typeparams.CoreType(deref(v.Type())).(*types.Struct).Field(index) if isPointer(v.Type()) { instr := &FieldAddr{ @@ -462,7 +464,7 @@ func emitImplicitSelections(f *Function, v Value, indices []int, pos token.Pos) // field's value. // Ident id is used for position and debug info. func emitFieldSelection(f *Function, v Value, index int, wantAddr bool, id *ast.Ident) Value { - fld := coreType(deref(v.Type())).(*types.Struct).Field(index) + fld := typeparams.CoreType(deref(v.Type())).(*types.Struct).Field(index) if isPointer(v.Type()) { instr := &FieldAddr{ X: v, diff --git a/go/ssa/print.go b/go/ssa/print.go index 9aa6809789e..e40bbfa2d21 100644 --- a/go/ssa/print.go +++ b/go/ssa/print.go @@ -17,6 +17,7 @@ import ( "strings" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typeparams" ) // relName returns the name of v relative to i. @@ -232,7 +233,7 @@ func (v *MakeChan) String() string { } func (v *FieldAddr) String() string { - st := coreType(deref(v.X.Type())).(*types.Struct) + st := typeparams.CoreType(deref(v.X.Type())).(*types.Struct) // Be robust against a bad index. name := "?" if 0 <= v.Field && v.Field < st.NumFields() { @@ -242,7 +243,7 @@ func (v *FieldAddr) String() string { } func (v *Field) String() string { - st := coreType(v.X.Type()).(*types.Struct) + st := typeparams.CoreType(v.X.Type()).(*types.Struct) // Be robust against a bad index. name := "?" if 0 <= v.Field && v.Field < st.NumFields() { diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 698cb16507f..3108de2e4a8 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -1401,7 +1401,7 @@ func (c *CallCommon) Signature() *types.Signature { if c.Method != nil { return c.Method.Type().(*types.Signature) } - return coreType(c.Value.Type()).(*types.Signature) + return typeparams.CoreType(c.Value.Type()).(*types.Signature) } // StaticCallee returns the callee if this is a trivially static diff --git a/go/ssa/util.go b/go/ssa/util.go index c30e74c7fc9..6b0aada9e8a 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -88,16 +88,16 @@ func isRuneSlice(t types.Type) bool { // - All are basic, []byte, or []rune. // - At least 1 is basic. // - At most 1 is []byte or []rune. -func isBasicConvTypes(tset typeSet) bool { +func isBasicConvTypes(tset termList) bool { basics := 0 - all := tset.underIs(func(t types.Type) bool { + all := underIs(tset, func(t types.Type) bool { if isBasic(t) { basics++ return true } return isByteSlice(t) || isRuneSlice(t) }) - return all && basics >= 1 && len(tset)-basics <= 1 + return all && basics >= 1 && tset.Len()-basics <= 1 } // deref returns a pointer's element type; otherwise it returns typ. From 7d9f45139f4a37b6e3a0425947d50b0df7368347 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 23 Nov 2022 11:59:34 -0500 Subject: [PATCH 470/723] gopls/internal/govulncheck: add in-memory cache for vulndb We used govulncheck.DefaultCache that is a cache in file system. This helps clients from hitting the network excessively. Mod diagnostic is computed very frequently, however, even file read & unmarshal contents for every module for every diagnostic computation can become slow. This CL adds an additional layer of caching that holds the information in memory. To avoid network access across gopls restarts, this cache is backed by the file system cache where the cached data is persisted on disk. Change-Id: Ib4e33a51b88453c09f424e944be2f6fc578eb1ea Reviewed-on: https://go-review.googlesource.com/c/tools/+/453155 Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/govulncheck/vulncache.go | 105 ++++++++++++++++++++++++ gopls/internal/vulncheck/command.go | 2 +- 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 gopls/internal/govulncheck/vulncache.go diff --git a/gopls/internal/govulncheck/vulncache.go b/gopls/internal/govulncheck/vulncache.go new file mode 100644 index 00000000000..a259f027336 --- /dev/null +++ b/gopls/internal/govulncheck/vulncache.go @@ -0,0 +1,105 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package govulncheck + +import ( + "sync" + "time" + + vulnc "golang.org/x/vuln/client" + "golang.org/x/vuln/osv" +) + +// inMemoryCache is an implementation of the [client.Cache] interface +// that "decorates" another instance of that interface to provide +// an additional layer of (memory-based) caching. +type inMemoryCache struct { + mu sync.Mutex + underlying vulnc.Cache + db map[string]*db +} + +var _ vulnc.Cache = &inMemoryCache{} + +type db struct { + retrieved time.Time + index vulnc.DBIndex + entry map[string][]*osv.Entry +} + +// NewInMemoryCache returns a new memory-based cache that decorates +// the provided cache (file-based, perhaps). +func NewInMemoryCache(underlying vulnc.Cache) *inMemoryCache { + return &inMemoryCache{ + underlying: underlying, + db: make(map[string]*db), + } +} + +func (c *inMemoryCache) lookupDBLocked(dbName string) *db { + cached := c.db[dbName] + if cached == nil { + cached = &db{entry: make(map[string][]*osv.Entry)} + c.db[dbName] = cached + } + return cached +} + +// ReadIndex returns the index for dbName from the cache, or returns zero values +// if it is not present. +func (c *inMemoryCache) ReadIndex(dbName string) (vulnc.DBIndex, time.Time, error) { + c.mu.Lock() + defer c.mu.Unlock() + cached := c.lookupDBLocked(dbName) + + if cached.retrieved.IsZero() { + // First time ReadIndex is called. + index, retrieved, err := c.underlying.ReadIndex(dbName) + if err != nil { + return index, retrieved, err + } + cached.index, cached.retrieved = index, retrieved + } + return cached.index, cached.retrieved, nil +} + +// WriteIndex puts the index and retrieved time into the cache. +func (c *inMemoryCache) WriteIndex(dbName string, index vulnc.DBIndex, retrieved time.Time) error { + c.mu.Lock() + defer c.mu.Unlock() + cached := c.lookupDBLocked(dbName) + cached.index, cached.retrieved = index, retrieved + // TODO(hyangah): shouldn't we invalidate all cached entries? + return c.underlying.WriteIndex(dbName, index, retrieved) +} + +// ReadEntries returns the vulndb entries for path from the cache. +func (c *inMemoryCache) ReadEntries(dbName, path string) ([]*osv.Entry, error) { + c.mu.Lock() + defer c.mu.Unlock() + cached := c.lookupDBLocked(dbName) + entries, ok := cached.entry[path] + if !ok { + // cache miss + entries, err := c.underlying.ReadEntries(dbName, path) + if err != nil { + return entries, err + } + cached.entry[path] = entries + } + return entries, nil +} + +// WriteEntries puts the entries for path into the cache. +func (c *inMemoryCache) WriteEntries(dbName, path string, entries []*osv.Entry) error { + c.mu.Lock() + defer c.mu.Unlock() + cached := c.lookupDBLocked(dbName) + cached.entry[path] = entries + return c.underlying.WriteEntries(dbName, path, entries) +} diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 004541f9040..86db0a3bdb7 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -385,7 +385,7 @@ func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile s // Request vuln entries from remote service. cli, err := client.NewClient( findGOVULNDB(snapshot.View().Options().EnvSlice()), - client.Options{HTTPCache: govulncheck.DefaultCache()}) + client.Options{HTTPCache: govulncheck.NewInMemoryCache(govulncheck.DefaultCache())}) if err != nil { return nil, err } From 9519368a491771888d4c56af67a8519b48b0105f Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 22 Nov 2022 22:19:30 -0500 Subject: [PATCH 471/723] gopls/internal/lsp/mod: add the vulncheck diagnostics mode When user sets `"ui.vulncheck": "Imports"`, gopls will run the vulnerability scanning on the modules used in the project as part of the go.mod diagnostics. This scanning mode is less expensive than the govulncheck callgraph analysis so it can run almost in real time, but it is less precise than the govulncheck callgraph analysis. In the follow up change, we will add a code action that triggers the more precise govulncheck callgraph analysis. Change-Id: Ibf479c733c7e1ff98a3e74854c0f77ac6a6b5445 Reviewed-on: https://go-review.googlesource.com/c/tools/+/453156 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim --- gopls/doc/settings.md | 14 +++ gopls/internal/lsp/diagnostics.go | 16 +-- gopls/internal/lsp/mod/diagnostics.go | 27 ++++- gopls/internal/lsp/mod/hover.go | 21 +++- gopls/internal/lsp/source/api_json.go | 18 ++++ gopls/internal/lsp/source/options.go | 24 +++++ gopls/internal/lsp/source/options_test.go | 22 ++++ gopls/internal/lsp/source/view.go | 4 + gopls/internal/regtest/misc/vuln_test.go | 124 +++++++++++++++++++++- 9 files changed, 252 insertions(+), 18 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 4c3c7a8182a..8d32b4f4088 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -343,6 +343,20 @@ Can contain any of: Default: `{"bounds":true,"escape":true,"inline":true,"nil":true}`. +##### **vulncheck** *enum* + +**This setting is experimental and may be deleted.** + +vulncheck enables vulnerability scanning. + +Must be one of: + +* `"Imports"`: In Imports mode, `gopls` will report vulnerabilities that affect packages +directly and indirectly used by the analyzed main module. +* `"Off"`: Disable vulnerability analysis. + +Default: `"Off"`. + ##### **diagnosticsDelay** *time.Duration* **This is an advanced setting and should not be configured by most `gopls` users.** diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index e6e644de245..43097892840 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -260,14 +260,6 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn } store(modCheckUpgradesSource, "diagnosing go.mod upgrades", upgradeReports, upgradeErr) - // Diagnose vulnerabilities. - vulnReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) - if ctx.Err() != nil { - log.Trace.Log(ctx, "diagnose cancelled") - return - } - store(modVulncheckSource, "diagnosing vulnerabilities", vulnReports, vulnErr) - // Diagnose go.work file. workReports, workErr := work.Diagnostics(ctx, snapshot) if ctx.Err() != nil { @@ -291,6 +283,14 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn } store(modSource, "diagnosing go.mod file", modReports, modErr) + // Diagnose vulnerabilities. + vulnReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) + if ctx.Err() != nil { + log.Trace.Log(ctx, "diagnose cancelled") + return + } + store(modVulncheckSource, "diagnosing vulnerabilities", vulnReports, vulnErr) + if s.shouldIgnoreError(ctx, snapshot, activeErr) { return } diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index e3385205cd3..dc369ad7bfb 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -180,7 +180,15 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return nil, err } + fromGovulncheck := true vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()] + if vs == nil && snapshot.View().Options().Vulncheck == source.ModeVulncheckImports { + vs, err = snapshot.ModVuln(ctx, fh.URI()) + if err != nil { + return nil, err + } + fromGovulncheck = false + } if vs == nil || len(vs.Vulns) == 0 { return nil, nil } @@ -300,11 +308,20 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, } if len(info) > 0 { var b strings.Builder - switch len(info) { - case 1: - fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", req.Mod.Path, info[0]) - default: - fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", req.Mod.Path, strings.Join(info, ", ")) + if fromGovulncheck { + switch len(info) { + case 1: + fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", req.Mod.Path, info[0]) + default: + fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", req.Mod.Path, strings.Join(info, ", ")) + } + } else { + switch len(info) { + case 1: + fmt.Fprintf(&b, "%v has a vulnerability %v.", req.Mod.Path, info[0]) + default: + fmt.Fprintf(&b, "%v has known vulnerabilities %v.", req.Mod.Path, strings.Join(info, ", ")) + } } vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ URI: fh.URI(), diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 70686851ff3..71ffa9282d4 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -72,7 +72,16 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Get the vulnerability info. - affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI())[fh.URI()], req.Mod.Path, req.Mod.Version) + fromGovulncheck := true + vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()] + if vs == nil && snapshot.View().Options().Vulncheck == source.ModeVulncheckImports { + vs, err = snapshot.ModVuln(ctx, fh.URI()) + if err != nil { + return nil, err + } + fromGovulncheck = false + } + affecting, nonaffecting := lookupVulns(vs, req.Mod.Path, req.Mod.Version) // Get the `go mod why` results for the given file. why, err := snapshot.ModWhy(ctx, fh) @@ -95,7 +104,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path) header := formatHeader(req.Mod.Path, options) explanation = formatExplanation(explanation, req, options, isPrivate) - vulns := formatVulnerabilities(req.Mod.Path, affecting, nonaffecting, options) + vulns := formatVulnerabilities(req.Mod.Path, affecting, nonaffecting, options, fromGovulncheck) return &protocol.Hover{ Contents: protocol.MarkupContent{ @@ -158,7 +167,7 @@ func lookupVulns(vulns *govulncheck.Result, modpath, version string) (affecting, return affecting, nonaffecting } -func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulncheck.Vuln, options *source.Options) string { +func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulncheck.Vuln, options *source.Options, fromGovulncheck bool) string { if len(affecting) == 0 && len(nonaffecting) == 0 { return "" } @@ -187,7 +196,11 @@ func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulnchec } } if len(nonaffecting) > 0 { - fmt.Fprintf(&b, "\n**FYI:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n") + if fromGovulncheck { + fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n") + } else { + fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities. Use `govulncheck` to check if the project uses vulnerable symbols.\n") + } } for _, v := range nonaffecting { fix := fixedVersionInfo(v, modPath) diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index fc691a1fbb3..91b676a5426 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -498,6 +498,24 @@ var GeneratedAPIJSON = &APIJSON{ Status: "experimental", Hierarchy: "ui.diagnostic", }, + { + Name: "vulncheck", + Type: "enum", + Doc: "vulncheck enables vulnerability scanning.\n", + EnumValues: []EnumValue{ + { + Value: "\"Imports\"", + Doc: "`\"Imports\"`: In Imports mode, `gopls` will report vulnerabilities that affect packages\ndirectly and indirectly used by the analyzed main module.\n", + }, + { + Value: "\"Off\"", + Doc: "`\"Off\"`: Disable vulnerability analysis.\n", + }, + }, + Default: "\"Off\"", + Status: "experimental", + Hierarchy: "ui.diagnostic", + }, { Name: "diagnosticsDelay", Type: "time.Duration", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 20c8f85ed6d..d6458cc4fd0 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -131,6 +131,7 @@ func DefaultOptions() *Options { Inline: true, Nil: true, }, + Vulncheck: ModeVulncheckOff, }, InlayHintOptions: InlayHintOptions{}, DocumentationOptions: DocumentationOptions{ @@ -430,6 +431,9 @@ type DiagnosticOptions struct { // that should be reported by the gc_details command. Annotations map[Annotation]bool `status:"experimental"` + // Vulncheck enables vulnerability scanning. + Vulncheck VulncheckMode `status:"experimental"` + // DiagnosticsDelay controls the amount of time that gopls waits // after the most recent file modification before computing deep diagnostics. // Simple diagnostics (parsing and type-checking) are always run immediately @@ -692,6 +696,18 @@ const ( ModeDegradeClosed MemoryMode = "DegradeClosed" ) +type VulncheckMode string + +const ( + // Disable vulnerability analysis. + ModeVulncheckOff VulncheckMode = "Off" + // In Imports mode, `gopls` will report vulnerabilities that affect packages + // directly and indirectly used by the analyzed main module. + ModeVulncheckImports VulncheckMode = "Imports" + + // TODO: VulncheckRequire, VulncheckCallgraph +) + type OptionResults []OptionResult type OptionResult struct { @@ -1006,6 +1022,14 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "annotations": result.setAnnotationMap(&o.Annotations) + case "vulncheck": + if s, ok := result.asOneOf( + string(ModeVulncheckOff), + string(ModeVulncheckImports), + ); ok { + o.Vulncheck = VulncheckMode(s) + } + case "codelenses", "codelens": var lensOverrides map[string]bool result.setBoolMap(&lensOverrides) diff --git a/gopls/internal/lsp/source/options_test.go b/gopls/internal/lsp/source/options_test.go index dfc464e8c31..4fa6ecf15df 100644 --- a/gopls/internal/lsp/source/options_test.go +++ b/gopls/internal/lsp/source/options_test.go @@ -167,6 +167,28 @@ func TestSetOption(t *testing.T) { return !o.Annotations[Nil] && !o.Annotations[Bounds] }, }, + { + name: "vulncheck", + value: []interface{}{"invalid"}, + wantError: true, + check: func(o Options) bool { + return o.Vulncheck == "" // For invalid value, default to 'off'. + }, + }, + { + name: "vulncheck", + value: "Imports", + check: func(o Options) bool { + return o.Vulncheck == ModeVulncheckImports // For invalid value, default to 'off'. + }, + }, + { + name: "vulncheck", + value: "imports", + check: func(o Options) bool { + return o.Vulncheck == ModeVulncheckImports + }, + }, } for _, test := range tests { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index ad1012994ff..a68c3c13358 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -148,6 +148,10 @@ type Snapshot interface { // the given go.mod file. ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule, error) + // ModVuln returns import vulnerability analysis for the given go.mod URI. + // Concurrent requests are combined into a single command. + ModVuln(ctx context.Context, modURI span.URI) (*govulncheck.Result, error) + // GoModForFile returns the URI of the go.mod file for the given URI. GoModForFile(uri span.URI) span.URI diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index e50c38f2408..c37d4041672 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -9,6 +9,7 @@ package misc import ( "context" + "encoding/json" "path/filepath" "sort" "strings" @@ -98,6 +99,17 @@ description: | of this vulnerability. references: - href: pkg.go.dev/vuln/GO-2022-03 +-- GO-2022-04.yaml -- +modules: + - module: golang.org/bmod + packages: + - package: golang.org/bmod/unused + symbols: + - Vuln +description: | + vuln in bmod/somtrhingelse +references: + - href: pkg.go.dev/vuln/GO-2022-04 -- GOSTDLIB.yaml -- modules: - module: stdlib @@ -237,7 +249,7 @@ require ( -- go.sum -- golang.org/amod v1.0.0 h1:EUQOI2m5NhQZijXZf8WimSnnWubaFNrrKUH/PopTN8k= golang.org/amod v1.0.0/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E= -golang.org/bmod v0.5.0 h1:0kt1EI53298Ta9w4RPEAzNUQjtDoHUA6cc0c7Rwxhlk= +golang.org/bmod v0.5.0 h1:KgvUulMyMiYRB7suKA0x+DfWRVdeyPgVJvcishTH+ng= golang.org/bmod v0.5.0/go.mod h1:f6o+OhF66nz/0BBc/sbCsshyPRKMSxZIlG50B/bsM4c= golang.org/cmod v1.1.3 h1:PJ7rZFTk7xGAunBRDa0wDe7rZjZ9R/vr1S2QkVVCngQ= golang.org/cmod v1.1.3/go.mod h1:eCR8dnmvLYQomdeAZRCPgS5JJihXtqOQrpEkNj5feQA= @@ -322,6 +334,12 @@ go 1.14 -- golang.org/bmod@v0.5.0/bvuln/bvuln.go -- package bvuln +func Vuln() { + // something evil +} +-- golang.org/bmod@v0.5.0/unused/unused.go -- +package unused + func Vuln() { // something evil } @@ -360,6 +378,110 @@ func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) { return db, []RunOption{ProxyFiles(proxyData), ev, settings}, nil } +func TestRunVulncheckPackageDiagnostics(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + + db, opts0, err := vulnTestEnv(vulnsData, proxy1) + if err != nil { + t.Fatal(err) + } + defer db.Clean() + + checkVulncheckDiagnostics := func(env *Env, t *testing.T) { + env.OpenFile("go.mod") + + gotDiagnostics := &protocol.PublishDiagnosticsParams{} + env.AfterChange( + env.DiagnosticAtRegexp("go.mod", `golang.org/amod`), + ReadDiagnostics("go.mod", gotDiagnostics), + ) + + testFetchVulncheckResult(t, env, map[string][]string{}) + + wantVulncheckDiagnostics := map[string]vulnDiagExpectation{ + "golang.org/amod": { + diagnostics: []vulnDiag{ + { + msg: "golang.org/amod has known vulnerabilities GO-2022-01, GO-2022-03.", + severity: protocol.SeverityInformation, + codeActions: []string{ + "Upgrade to latest", + "Upgrade to v1.0.6", + }, + }, + }, + codeActions: []string{ + "Upgrade to latest", + "Upgrade to v1.0.6", + }, + hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, + }, + "golang.org/bmod": { + diagnostics: []vulnDiag{ + { + msg: "golang.org/bmod has a vulnerability GO-2022-02.", + severity: protocol.SeverityInformation, + }, + }, + hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."}, + }, + } + + for mod, want := range wantVulncheckDiagnostics { + modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) + + gotActions := env.CodeAction("go.mod", modPathDiagnostics) + if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { + t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", mod, gotActions, want.codeActions, diff) + continue + } + } + } + + wantNoVulncheckDiagnostics := func(env *Env, t *testing.T) { + env.OpenFile("go.mod") + + gotDiagnostics := &protocol.PublishDiagnosticsParams{} + env.AfterChange( + ReadDiagnostics("go.mod", gotDiagnostics), + ) + + if len(gotDiagnostics.Diagnostics) > 0 { + t.Errorf("Unexpected diagnostics: %v", stringify(gotDiagnostics)) + } + testFetchVulncheckResult(t, env, map[string][]string{}) + } + + for _, tc := range []struct { + name string + setting Settings + wantDiagnostics bool + }{ + {"imports", Settings{"ui.diagnostic.vulncheck": "Imports"}, true}, + {"default", Settings{}, false}, + {"invalid", Settings{"ui.diagnostic.vulncheck": "invalid"}, false}, + } { + t.Run(tc.name, func(t *testing.T) { + // override the settings options to enable diagnostics + opts := append(opts0, tc.setting) + WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) { + // TODO(hyangah): implement it, so we see GO-2022-01, GO-2022-02, and GO-2022-03. + // Check that the actions we get when including all diagnostics at a location return the same result + if tc.wantDiagnostics { + checkVulncheckDiagnostics(env, t) + } else { + wantNoVulncheckDiagnostics(env, t) + } + }) + }) + } +} + +func stringify(a interface{}) string { + data, _ := json.Marshal(a) + return string(data) +} + func TestRunVulncheckWarning(t *testing.T) { testenv.NeedsGo1Point(t, 18) From f718365b8d687e81808426c2c5eb05c404bd9491 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 23 Nov 2022 12:31:55 -0500 Subject: [PATCH 472/723] gopls/internal/lsp: include all vulns info to fetch_vulncheck_result In addition to the govulncheck analysis result, gopls now has its own vulnerability scanning that reports vulnerabilities on known packages in the workspace. While we are still debating on the right name for this analysis mode, let's call it 'light-weight' vuln scanning because this is less expensive than the govulncheck analysis (but with less precise result). fetch_vulncheck_result now includes the vulnerability analysis results from the light-weight vuln scanning. We distinguish the analysis source by adding Analysis field in the govulncheck.Result type. For each go.mod, if govulncheck result exists, return it. Otherwise, return the light-weight vuln scanning result if it is available in the snapshot. Change-Id: I8d53db474137cecc1138ae432207bf508c09ca6f Reviewed-on: https://go-review.googlesource.com/c/tools/+/453195 Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/govulncheck/types.go | 19 +++++++++++++ gopls/internal/lsp/command.go | 11 ++++++++ gopls/internal/regtest/misc/vuln_test.go | 36 ++++++++++++++++-------- gopls/internal/vulncheck/command.go | 1 + 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/gopls/internal/govulncheck/types.go b/gopls/internal/govulncheck/types.go index 6cf0415a242..75240aa7d35 100644 --- a/gopls/internal/govulncheck/types.go +++ b/gopls/internal/govulncheck/types.go @@ -9,4 +9,23 @@ type Result struct { // Vulns contains all vulnerabilities that are called or imported by // the analyzed module. Vulns []*Vuln + + // Mode contains the source of the vulnerability info. + // Clients of the gopls.fetch_vulncheck_result command may need + // to interprete the vulnerabilities differently based on the + // analysis mode. For example, Vuln without callstack traces + // indicate a vulnerability that is not used if the result was + // from 'govulncheck' analysis mode. On the other hand, Vuln + // without callstack traces just implies the package with the + // vulnerability is known to the workspace and we do not know + // whether the vulnerable symbols are actually used or not. + Mode AnalysisMode } + +type AnalysisMode string + +const ( + ModeInvalid AnalysisMode = "" // zero value + ModeGovulncheck AnalysisMode = "govulncheck" + ModeImports AnalysisMode = "imports" +) diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 4a537728197..7f3652dce28 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -843,6 +843,16 @@ type pkgLoadConfig struct { func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*govulncheck.Result, error) { ret := map[protocol.DocumentURI]*govulncheck.Result{} err := c.run(ctx, commandConfig{forURI: arg.URI}, func(ctx context.Context, deps commandDeps) error { + if deps.snapshot.View().Options().Vulncheck == source.ModeVulncheckImports { + for _, modfile := range deps.snapshot.ModFiles() { + res, err := deps.snapshot.ModVuln(ctx, modfile) + if err != nil { + return err + } + ret[protocol.URIFromSpanURI(modfile)] = res + } + } + // Overwrite if there is any govulncheck-based result. for modfile, result := range deps.snapshot.View().Vulnerabilities() { ret[protocol.URIFromSpanURI(modfile)] = result } @@ -914,6 +924,7 @@ func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.Vulnc // TODO: for easy debugging, log the failed stdout somewhere? return fmt.Errorf("failed to parse govulncheck output: %v", err) } + result.Mode = govulncheck.ModeGovulncheck deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), &result) c.s.diagnoseSnapshot(deps.snapshot, nil, false) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index c37d4041672..734a87353be 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -197,11 +197,17 @@ func main() { ShownMessage("Found GOSTDLIB"), ), ) - testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GOSTDLIB"}}) + testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ + "go.mod": {IDs: []string{"GOSTDLIB"}, Mode: govulncheck.ModeGovulncheck}}) }) } -func testFetchVulncheckResult(t *testing.T, env *Env, want map[string][]string) { +type fetchVulncheckResult struct { + IDs []string + Mode govulncheck.AnalysisMode +} + +func testFetchVulncheckResult(t *testing.T, env *Env, want map[string]fetchVulncheckResult) { t.Helper() var result map[protocol.DocumentURI]*govulncheck.Result @@ -217,9 +223,9 @@ func testFetchVulncheckResult(t *testing.T, env *Env, want map[string][]string) }, &result) for _, v := range want { - sort.Strings(v) + sort.Strings(v.IDs) } - got := map[string][]string{} + got := map[string]fetchVulncheckResult{} for k, r := range result { var osv []string for _, v := range r.Vulns { @@ -227,7 +233,10 @@ func testFetchVulncheckResult(t *testing.T, env *Env, want map[string][]string) } sort.Strings(osv) modfile := env.Sandbox.Workdir.RelPath(k.SpanURI().Filename()) - got[modfile] = osv + got[modfile] = fetchVulncheckResult{ + IDs: osv, + Mode: r.Mode, + } } if diff := cmp.Diff(want, got); diff != "" { t.Errorf("fetch vulnchheck result = got %v, want %v: diff %v", got, want, diff) @@ -396,7 +405,12 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { ReadDiagnostics("go.mod", gotDiagnostics), ) - testFetchVulncheckResult(t, env, map[string][]string{}) + testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ + "go.mod": { + IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, + Mode: govulncheck.ModeImports, + }, + }) wantVulncheckDiagnostics := map[string]vulnDiagExpectation{ "golang.org/amod": { @@ -449,7 +463,7 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { if len(gotDiagnostics.Diagnostics) > 0 { t.Errorf("Unexpected diagnostics: %v", stringify(gotDiagnostics)) } - testFetchVulncheckResult(t, env, map[string][]string{}) + testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{}) } for _, tc := range []struct { @@ -510,8 +524,8 @@ func TestRunVulncheckWarning(t *testing.T) { ), ) - testFetchVulncheckResult(t, env, map[string][]string{ - "go.mod": {"GO-2022-01", "GO-2022-02", "GO-2022-03"}, + testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ + "go.mod": {IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, Mode: govulncheck.ModeGovulncheck}, }) env.OpenFile("x/x.go") lineX := env.RegexpSearch("x/x.go", `c\.C1\(\)\.Vuln1\(\)`) @@ -672,7 +686,7 @@ func TestRunVulncheckInfo(t *testing.T) { ), ) - testFetchVulncheckResult(t, env, map[string][]string{"go.mod": {"GO-2022-02"}}) + testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{"go.mod": {IDs: []string{"GO-2022-02"}, Mode: govulncheck.ModeGovulncheck}}) // wantDiagnostics maps a module path in the require // section of a go.mod to diagnostics that will be returned // when running vulncheck. @@ -791,7 +805,7 @@ func (i vulnRelatedInfo) less(j vulnRelatedInfo) bool { return i.Message < j.Message } -// wantVulncheckModDiagnostics maps a module path in the require +// vulnDiagExpectation maps a module path in the require // section of a go.mod to diagnostics that will be returned // when running vulncheck. type vulnDiagExpectation struct { diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 86db0a3bdb7..d29318efecc 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -476,6 +476,7 @@ func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile s }) ret := &govulncheck.Result{ Vulns: vulns, + Mode: govulncheck.ModeImports, } return ret, nil } From e29d1ed0069496ea52660754e0ce66b23bd56f63 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 22 Nov 2022 14:15:25 -0500 Subject: [PATCH 473/723] gopls: add diagnostic for standard library Add a diagnostic on module statement for the go standard library vulnerabilities. Change-Id: I95700810f7f05cac9bf643798ff88d4c3ef8576f Reviewed-on: https://go-review.googlesource.com/c/tools/+/452836 Run-TryBot: Suzy Mueller gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot --- gopls/internal/lsp/mod/diagnostics.go | 101 +++++++++++++++++------ gopls/internal/regtest/misc/vuln_test.go | 2 +- 2 files changed, 77 insertions(+), 26 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index dc369ad7bfb..5c80c55e104 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -289,55 +289,106 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, sort.Strings(info) if len(warning) > 0 { - var b strings.Builder - switch len(warning) { - case 1: - fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", req.Mod.Path, warning[0]) - default: - fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", ")) - } vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ URI: fh.URI(), Range: rng, Severity: protocol.SeverityWarning, Source: source.Vulncheck, - Message: b.String(), + Message: getVulnMessage(req.Mod.Path, warning, true, fromGovulncheck), SuggestedFixes: warningFixes, Related: relatedInfo, }) } if len(info) > 0 { - var b strings.Builder - if fromGovulncheck { - switch len(info) { - case 1: - fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", req.Mod.Path, info[0]) - default: - fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", req.Mod.Path, strings.Join(info, ", ")) - } - } else { - switch len(info) { - case 1: - fmt.Fprintf(&b, "%v has a vulnerability %v.", req.Mod.Path, info[0]) - default: - fmt.Fprintf(&b, "%v has known vulnerabilities %v.", req.Mod.Path, strings.Join(info, ", ")) - } - } vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ URI: fh.URI(), Range: rng, Severity: protocol.SeverityInformation, Source: source.Vulncheck, - Message: b.String(), + Message: getVulnMessage(req.Mod.Path, info, false, fromGovulncheck), SuggestedFixes: infoFixes, Related: relatedInfo, }) } } + // Add standard library vulnerabilities. + stdlibVulns := vulnsByModule["stdlib"] + if len(stdlibVulns) == 0 { + return vulnDiagnostics, nil + } + + // Put the standard library diagnostic on the module declaration. + rng, err := pm.Mapper.OffsetRange(pm.File.Module.Syntax.Start.Byte, pm.File.Module.Syntax.End.Byte) + if err != nil { + return vulnDiagnostics, nil // TODO: bug report + } + + stdlib := stdlibVulns[0].mod.FoundVersion + var warning, info []string + var relatedInfo []source.RelatedInformation + for _, mv := range stdlibVulns { + vuln := mv.vuln + stdlib = mv.mod.FoundVersion + if !vuln.IsCalled() { + info = append(info, vuln.OSV.ID) + } else { + warning = append(warning, vuln.OSV.ID) + relatedInfo = append(relatedInfo, listRelatedInfo(ctx, snapshot, vuln)...) + } + } + if len(warning) > 0 { + vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityWarning, + Source: source.Vulncheck, + Message: getVulnMessage(stdlib, warning, true, fromGovulncheck), + Related: relatedInfo, + }) + } + if len(info) > 0 { + vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityInformation, + Source: source.Vulncheck, + Message: getVulnMessage(stdlib, info, false, fromGovulncheck), + Related: relatedInfo, + }) + } return vulnDiagnostics, nil } +func getVulnMessage(mod string, vulns []string, used, fromGovulncheck bool) string { + var b strings.Builder + if used { + switch len(vulns) { + case 1: + fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", mod, vulns[0]) + default: + fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", mod, strings.Join(vulns, ", ")) + } + } else { + if fromGovulncheck { + switch len(vulns) { + case 1: + fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", mod, vulns[0]) + default: + fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", mod, strings.Join(vulns, ", ")) + } + } else { + switch len(vulns) { + case 1: + fmt.Fprintf(&b, "%v has a vulnerability %v.", mod, vulns[0]) + default: + fmt.Fprintf(&b, "%v has known vulnerabilities %v.", mod, strings.Join(vulns, ", ")) + } + } + } + return b.String() +} + func listRelatedInfo(ctx context.Context, snapshot source.Snapshot, vuln *govulncheck.Vuln) []source.RelatedInformation { var ri []source.RelatedInformation for _, m := range vuln.Modules { diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 734a87353be..bf2f077a486 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -193,8 +193,8 @@ func main() { env.Await( OnceMet( CompletedProgress(result.Token), - // TODO(hyangah): once the diagnostics are published, wait for diagnostics. ShownMessage("Found GOSTDLIB"), + env.DiagnosticAtRegexpWithMessage("go.mod", `module`, `go1.18 has a vulnerability used in the code: GOSTDLIB.`), ), ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ From 4c3cb1e658a6790fa073c0309f7870eb45ee4ecf Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Sun, 27 Nov 2022 16:13:28 -0500 Subject: [PATCH 474/723] internal/gocommand: add GoVersionString Unlike the existing GoVersion that returns the minor version number of the go command, GoVersionString returns the part that indicates the major/minor/patch version from `go version` output. The full go version info is necessary when analyzing vulnerabilities. Change-Id: I5ea624e877e02009148ad906e6475cb1aba40b3a Reviewed-on: https://go-review.googlesource.com/c/tools/+/453595 Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/view.go | 12 ++++++++++++ gopls/internal/lsp/source/view.go | 4 ++++ internal/gocommand/version.go | 21 ++++++++++++++++++++ internal/gocommand/version_test.go | 31 ++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 internal/gocommand/version_test.go diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 8198353ecab..db7cbb7f204 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -119,6 +119,9 @@ type workspaceInformation struct { // The Go version in use: X in Go 1.X. goversion int + // The Go version reported by go version command. (e.g. go1.19.1, go1.20-rc.1, go1.21-abcdef01) + goversionString string + // hasGopackagesDriver is true if the user has a value set for the // GOPACKAGESDRIVER environment variable or a gopackagesdriver binary on // their machine. @@ -836,6 +839,10 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, if err != nil { return nil, err } + goversionString, err := gocommand.GoVersionString(ctx, inv, s.gocmdRunner) + if err != nil { + return nil, err + } // Make sure to get the `go env` before continuing with initialization. envVars, env, err := s.getGoEnv(ctx, folder.Filename(), goversion, options.EnvSlice()) @@ -863,6 +870,7 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, return &workspaceInformation{ hasGopackagesDriver: hasGopackagesDriver, goversion: goversion, + goversionString: goversionString, environmentVariables: envVars, goEnv: env, }, nil @@ -1072,6 +1080,10 @@ func (v *View) GoVersion() int { return v.workspaceInformation.goversion } +func (v *View) GoVersionString() string { + return v.workspaceInformation.goversionString +} + // Copied from // https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/str/path.go;l=58;drc=2910c5b4a01a573ebc97744890a07c1a3122c67a func globsMatchPath(globs, target string) bool { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index a68c3c13358..99918504c6d 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -329,6 +329,10 @@ type View interface { // GoVersion returns the configured Go version for this view. GoVersion() int + + // GoVersionString returns the go version string configured for this view. + // Unlike [GoVersion], this encodes the minor version and commit hash information. + GoVersionString() string } // A FileSource maps uris to FileHandles. This abstraction exists both for diff --git a/internal/gocommand/version.go b/internal/gocommand/version.go index 8db5ceb9d51..98e834f74d3 100644 --- a/internal/gocommand/version.go +++ b/internal/gocommand/version.go @@ -7,6 +7,7 @@ package gocommand import ( "context" "fmt" + "regexp" "strings" ) @@ -56,3 +57,23 @@ func GoVersion(ctx context.Context, inv Invocation, r *Runner) (int, error) { } return 0, fmt.Errorf("no parseable ReleaseTags in %v", tags) } + +// GoVersionString reports the go version string as shown in `go version` command output. +// When `go version` outputs in non-standard form, this returns an empty string. +func GoVersionString(ctx context.Context, inv Invocation, r *Runner) (string, error) { + inv.Verb = "version" + goVersion, err := r.Run(ctx, inv) + if err != nil { + return "", err + } + return parseGoVersionOutput(goVersion.Bytes()), nil +} + +func parseGoVersionOutput(data []byte) string { + re := regexp.MustCompile(`^go version (go\S+|devel \S+)`) + m := re.FindSubmatch(data) + if len(m) != 2 { + return "" // unrecognized version + } + return string(m[1]) +} diff --git a/internal/gocommand/version_test.go b/internal/gocommand/version_test.go new file mode 100644 index 00000000000..d4d02259e08 --- /dev/null +++ b/internal/gocommand/version_test.go @@ -0,0 +1,31 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gocommand + +import ( + "strconv" + "testing" +) + +func TestParseGoVersionOutput(t *testing.T) { + tests := []struct { + args string + want string + }{ + {"go version go1.12 linux/amd64", "go1.12"}, + {"go version go1.18.1 darwin/amd64", "go1.18.1"}, + {"go version go1.19.rc1 windows/arm64", "go1.19.rc1"}, + {"go version devel d5de62df152baf4de6e9fe81933319b86fd95ae4 linux/386", "devel d5de62df152baf4de6e9fe81933319b86fd95ae4"}, + {"go version devel go1.20-1f068f0dc7 Tue Oct 18 20:58:37 2022 +0000 darwin/amd64", "devel go1.20-1f068f0dc7"}, + {"v1.19.1 foo/bar", ""}, + } + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + if got := parseGoVersionOutput([]byte(tt.args)); got != tt.want { + t.Errorf("parseGoVersionOutput() = %v, want %v", got, tt.want) + } + }) + } +} From 3881046b05857f4b86f1c9603bbe6128e5d9fb8b Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Mon, 28 Nov 2022 20:36:49 -0500 Subject: [PATCH 475/723] gopls: add a go:generate rule for doc generation Change-Id: I31ade9b6eec5ff814fc803a82a182d0f92237d59 Reviewed-on: https://go-review.googlesource.com/c/tools/+/369775 Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gopls/main.go b/gopls/main.go index 837a01d40b5..bdbe3615429 100644 --- a/gopls/main.go +++ b/gopls/main.go @@ -11,6 +11,8 @@ // for the most up-to-date documentation. package main // import "golang.org/x/tools/gopls" +//go:generate go run doc/generate.go + import ( "context" "golang.org/x/tools/internal/analysisinternal" From ff22fab9324c4ef0a092e88fd0fdce98d3833698 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 18 Nov 2022 14:47:36 -0500 Subject: [PATCH 476/723] gopls/internal/lsp/cache: eliminate obsolete invalidation step This change removes the call to invalidatePackagesLocked after buildMetadata in the load method; now that we no longer permit invalid metadata, it is unnecessary. (The sole remaining call to invalidatePackagesLocked was inlined into Clone.) Also: - check the invalidation invariant in load before Clone. - merge MetadataForFile and getOrLoadIDsForURI since all callers want the Metadata (not just PackageID) and the defensive check is no longer needed. - simplify awaitLoadedAllErrors by calling getInitializationError. - reduce allocation and critical sections in various snapshot methods that retrieve the metadataGraph. - flag various places where we can avoid type-checking. - various other doc comments. Updates golang/go#54180 Change-Id: I82dca203b2520259630b2fd9d08e120030d44a96 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452057 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/load.go | 41 ++----- gopls/internal/lsp/cache/pkg.go | 4 +- gopls/internal/lsp/cache/snapshot.go | 144 ++++++++++-------------- gopls/internal/lsp/code_action.go | 5 +- gopls/internal/lsp/command.go | 2 + gopls/internal/lsp/link.go | 2 + gopls/internal/lsp/source/add_import.go | 3 +- gopls/internal/lsp/source/code_lens.go | 3 + gopls/internal/lsp/source/fix.go | 1 + gopls/internal/lsp/source/hover.go | 1 + gopls/internal/lsp/source/references.go | 4 + gopls/internal/lsp/source/stub.go | 1 + gopls/internal/lsp/source/util.go | 3 + gopls/internal/lsp/source/view.go | 4 +- gopls/internal/vulncheck/command.go | 13 +-- 15 files changed, 98 insertions(+), 133 deletions(-) diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 5f31958a15e..321ff5f04a0 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -21,6 +21,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" @@ -213,48 +214,30 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc s.mu.Lock() - // Only update metadata where we don't already have valid metadata. - // - // We want to preserve an invariant that s.packages.Get(id).m.Metadata - // matches s.meta.metadata[id].Metadata. By avoiding overwriting valid - // metadata, we minimize the amount of invalidation required to preserve this - // invariant. - // - // TODO(rfindley): perform a sanity check that metadata matches here. If not, - // we have an invalidation bug elsewhere. + // Compute the minimal metadata updates (for Clone) + // required to preserve this invariant: + // for all id, s.packages.Get(id).m == s.meta.metadata[id]. updates := make(map[PackageID]*source.Metadata) - var updatedIDs []PackageID for _, m := range newMetadata { if existing := s.meta.metadata[m.ID]; existing == nil { updates[m.ID] = m - updatedIDs = append(updatedIDs, m.ID) delete(s.shouldLoad, m.ID) } } + // Assert the invariant. + s.packages.Range(func(k, v interface{}) { + pk, ph := k.(packageKey), v.(*packageHandle) + if s.meta.metadata[pk.id] != ph.m { + // TODO(adonovan): upgrade to unconditional panic after Jan 2023. + bug.Reportf("inconsistent metadata") + } + }) event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates))) - // Invalidate the reverse transitive closure of packages that have changed. - // - // Note that the original metadata is being invalidated here, so we use the - // original metadata graph to compute the reverse closure. - invalidatedPackages := s.meta.reverseTransitiveClosure(updatedIDs...) - s.meta = s.meta.Clone(updates) s.resetIsActivePackageLocked() - // Invalidate any packages and analysis results we may have associated with - // this metadata. - // - // Generally speaking we should have already invalidated these results in - // snapshot.clone, but with experimentalUseInvalidMetadata is may be possible - // that we have re-computed stale results before the reload completes. In - // this case, we must re-invalidate here. - // - // TODO(golang/go#54180): if we decide to make experimentalUseInvalidMetadata - // obsolete, we should avoid this invalidation. - s.invalidatePackagesLocked(invalidatedPackages) - s.workspacePackages = computeWorkspacePackagesLocked(s, s.meta) s.dumpWorkspace("load") s.mu.Unlock() diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 6466b8ee1dd..5b927a5f6d2 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -25,7 +25,7 @@ type ( ImportPath = source.ImportPath ) -// pkg contains the type information needed by the source package. +// pkg contains parse trees and type information for a package. type pkg struct { m *source.Metadata mode source.ParseMode @@ -34,7 +34,7 @@ type pkg struct { compiledGoFiles []*source.ParsedGoFile diagnostics []*source.Diagnostic deps map[PackageID]*pkg // use m.DepsBy{Pkg,Imp}Path to look up ID - version *module.Version + version *module.Version // may be nil; may differ from m.Module.Version parseErrors []scanner.ErrorList typeErrors []types.Error types *types.Package diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 369235fcd70..1f738c6c22f 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -100,10 +100,10 @@ type snapshot struct { // It may be invalidated when a file's content changes. // // Invariants to preserve: - // - packages.Get(id).m.Metadata == meta.metadata[id].Metadata for all ids + // - packages.Get(id).meta == meta.metadata[id] for all ids // - if a package is in packages, then all of its dependencies should also // be in packages, unless there is a missing import - packages *persistent.Map // from packageKey to *memoize.Promise[*packageHandle] + packages *persistent.Map // from packageKey to *packageHandle // isActivePackageCache maps package ID to the cached value if it is active or not. // It may be invalidated when metadata changes or a new file is opened or closed. @@ -655,6 +655,8 @@ func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode sourc func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, pkgPolicy source.PackageFilter) (source.Package, error) { ctx = event.Label(ctx, tag.URI.Of(uri)) + // TODO(adonovan): opt: apply pkgPolicy to the list of + // Metadatas before initiating loading of any package. phs, err := s.packageHandlesForFile(ctx, uri, mode, false) if err != nil { return nil, err @@ -699,24 +701,24 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode if kind := s.view.FileKind(fh); kind != source.Go { return nil, fmt.Errorf("no packages for non-Go file %s (%v)", uri, kind) } - knownIDs, err := s.getOrLoadIDsForURI(ctx, uri) + metas, err := s.MetadataForFile(ctx, uri) if err != nil { return nil, err } var phs []*packageHandle - for _, id := range knownIDs { + for _, m := range metas { // Filter out any intermediate test variants. We typically aren't // interested in these packages for file= style queries. - if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant() && !withIntermediateTestVariants { + if m.IsIntermediateTestVariant() && !withIntermediateTestVariants { continue } parseMode := source.ParseFull if mode == source.TypecheckWorkspace { - parseMode = s.workspaceParseMode(id) + parseMode = s.workspaceParseMode(m.ID) } - ph, err := s.buildPackageHandle(ctx, id, parseMode) + ph, err := s.buildPackageHandle(ctx, m.ID, parseMode) if err != nil { return nil, err } @@ -725,12 +727,10 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode return phs, nil } -// getOrLoadIDsForURI returns package IDs associated with the file uri. If no -// such packages exist or if they are known to be stale, it reloads the file. -// -// If experimentalUseInvalidMetadata is set, this function may return package -// IDs with invalid metadata. -func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]PackageID, error) { +// MetadataForFile returns the Metadata for each package associated +// with the file uri. If no such package exists or if they are known +// to be stale, it reloads metadata for the file. +func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) { s.mu.Lock() // Start with the set of package associations derived from the last load. @@ -772,23 +772,31 @@ func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]Pack s.clearShouldLoad(scope) // Don't return an error here, as we may still return stale IDs. - // Furthermore, the result of getOrLoadIDsForURI should be consistent upon + // Furthermore, the result of MetadataForFile should be consistent upon // subsequent calls, even if the file is marked as unloadable. if err != nil && !errors.Is(err, errNoPackages) { - event.Error(ctx, "getOrLoadIDsForURI", err) + event.Error(ctx, "MetadataForFile", err) } } + // Retrieve the metadata. s.mu.Lock() + defer s.mu.Unlock() ids = s.meta.ids[uri] - // metadata is only ever added by loading, so if we get here and still have - // no ids, uri is unloadable. + metas := make([]*source.Metadata, len(ids)) + for i, id := range ids { + metas[i] = s.meta.metadata[id] + if metas[i] == nil { + panic("nil metadata") + } + } + // Metadata is only ever added by loading, + // so if we get here and still have + // no IDs, uri is unloadable. if !unloadable && len(ids) == 0 { s.unloadableFiles[uri] = struct{}{} } - s.mu.Unlock() - - return ids, nil + return metas, nil } func (s *snapshot) GetReverseDependencies(ctx context.Context, id PackageID) ([]source.Package, error) { @@ -1135,37 +1143,17 @@ func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { return result } -func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) { - knownIDs, err := s.getOrLoadIDsForURI(ctx, uri) - if err != nil { - return nil, err - } - var mds []*source.Metadata - for _, id := range knownIDs { - md := s.getMetadata(id) - // TODO(rfindley): knownIDs and metadata should be in sync, but existing - // code is defensive of nil metadata. - if md != nil { - mds = append(mds, md) - } - } - return mds, nil -} - func (s *snapshot) KnownPackages(ctx context.Context) ([]source.Package, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } - ids := make([]source.PackageID, 0, len(s.meta.metadata)) s.mu.Lock() - for id := range s.meta.metadata { - ids = append(ids, id) - } + g := s.meta s.mu.Unlock() - pkgs := make([]source.Package, 0, len(ids)) - for _, id := range ids { + pkgs := make([]source.Package, 0, len(g.metadata)) + for id := range g.metadata { pkg, err := s.checkedPackage(ctx, id, s.workspaceParseMode(id)) if err != nil { return nil, err @@ -1181,10 +1169,11 @@ func (s *snapshot) AllValidMetadata(ctx context.Context) ([]*source.Metadata, er } s.mu.Lock() - defer s.mu.Unlock() + g := s.meta + s.mu.Unlock() - var meta []*source.Metadata - for _, m := range s.meta.metadata { + meta := make([]*source.Metadata, 0, len(g.metadata)) + for _, m := range g.metadata { meta = append(meta, m) } return meta, nil @@ -1281,11 +1270,7 @@ func (s *snapshot) clearShouldLoad(scopes ...loadScope) { // noValidMetadataForURILocked reports whether there is any valid metadata for // the given URI. func (s *snapshot) noValidMetadataForURILocked(uri span.URI) bool { - ids, ok := s.meta.ids[uri] - if !ok { - return true - } - for _, id := range ids { + for _, id := range s.meta.ids[uri] { if _, ok := s.meta.metadata[id]; ok { return false } @@ -1483,12 +1468,8 @@ func (s *snapshot) awaitLoadedAllErrors(ctx context.Context) *source.CriticalErr // TODO(rfindley): Should we be more careful about returning the // initialization error? Is it possible for the initialization error to be // corrected without a successful reinitialization? - s.mu.Lock() - initializedErr := s.initializedErr - s.mu.Unlock() - - if initializedErr != nil { - return initializedErr + if err := s.getInitializationError(); err != nil { + return err } // TODO(rfindley): revisit this handling. Calling reloadWorkspace with a @@ -1928,7 +1909,25 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC addRevDeps(id, invalidateMetadata) } - result.invalidatePackagesLocked(idsToInvalidate) + // Delete invalidated package type information. + for id := range idsToInvalidate { + for _, mode := range source.AllParseModes { + key := packageKey{mode, id} + result.packages.Delete(key) + } + } + + // Delete invalidated analysis actions. + var actionsToDelete []actionKey + result.actions.Range(func(k, _ interface{}) { + key := k.(actionKey) + if _, ok := idsToInvalidate[key.pkgid]; ok { + actionsToDelete = append(actionsToDelete, key) + } + }) + for _, key := range actionsToDelete { + result.actions.Delete(key) + } // If a file has been deleted, we must delete metadata for all packages // containing that file. @@ -2080,35 +2079,6 @@ func invalidatedPackageIDs(uri span.URI, known map[span.URI][]PackageID, package return invalidated } -// invalidatePackagesLocked deletes data associated with the given package IDs. -// -// Note: all keys in the ids map are invalidated, regardless of the -// corresponding value. -// -// s.mu must be held while calling this function. -func (s *snapshot) invalidatePackagesLocked(ids map[PackageID]bool) { - // Delete invalidated package type information. - for id := range ids { - for _, mode := range source.AllParseModes { - key := packageKey{mode, id} - s.packages.Delete(key) - } - } - - // Copy actions. - // TODO(adonovan): opt: avoid iteration over s.actions. - var actionsToDelete []actionKey - s.actions.Range(func(k, _ interface{}) { - key := k.(actionKey) - if _, ok := ids[key.pkgid]; ok { - actionsToDelete = append(actionsToDelete, key) - } - }) - for _, key := range actionsToDelete { - s.actions.Delete(key) - } -} - // fileWasSaved reports whether the FileHandle passed in has been saved. It // accomplishes this by checking to see if the original and current FileHandles // are both overlays, and if the current FileHandle is saved while the original diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index d19cafc0704..275348baca2 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -212,7 +212,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara } if wanted[protocol.RefactorExtract] { - fixes, err := extractionFixes(ctx, snapshot, pkg, uri, params.Range) + fixes, err := extractionFixes(ctx, snapshot, uri, params.Range) if err != nil { return nil, err } @@ -305,7 +305,7 @@ func importDiagnostics(fix *imports.ImportFix, diagnostics []protocol.Diagnostic return results } -func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) { +func extractionFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) { if rng.Start == rng.End { return nil, nil } @@ -313,6 +313,7 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.P if err != nil { return nil, err } + // TODO(adonovan): opt: avoid package loading; only parsing is needed. _, pgf, err := source.GetParsedFile(ctx, snapshot, fh, source.NarrowestPackage) if err != nil { return nil, fmt.Errorf("getting file for Identifier: %w", err) diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 7f3652dce28..81e29bdc77e 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -690,6 +690,7 @@ func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIAr progress: "Toggling GC Details", forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { + // TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed. pkg, err := deps.snapshot.PackageForFile(ctx, deps.fh.URI(), source.TypecheckWorkspace, source.NarrowestPackage) if err != nil { return err @@ -756,6 +757,7 @@ func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) ( err := c.run(ctx, commandConfig{ forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { + // TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed. pkg, err := deps.snapshot.PackageForFile(ctx, args.URI.SpanURI(), source.TypecheckWorkspace, source.NarrowestPackage) if err != nil { return err diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go index 7eb561fa00d..ea1ff5ad0ab 100644 --- a/gopls/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -103,6 +103,7 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) { view := snapshot.View() // We don't actually need type information, so any typecheck mode is fine. + // TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed. pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckWorkspace, source.WidestPackage) if err != nil { return nil, err @@ -174,6 +175,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle } func moduleAtVersion(targetImportPath source.ImportPath, pkg source.Package) (string, string, bool) { + // TODO(adonovan): opt: avoid need for package; use Metadata.DepsByImportPath only. impPkg, err := pkg.ResolveImportPath(targetImportPath) if err != nil { return "", "", false diff --git a/gopls/internal/lsp/source/add_import.go b/gopls/internal/lsp/source/add_import.go index 2fc03e5d758..29be1484288 100644 --- a/gopls/internal/lsp/source/add_import.go +++ b/gopls/internal/lsp/source/add_import.go @@ -7,12 +7,13 @@ package source import ( "context" - "golang.org/x/tools/internal/imports" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/imports" ) // AddImport adds a single import statement to the given file func AddImport(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, importPath string) ([]protocol.TextEdit, error) { + // TODO(adonovan): opt: avoid loading type checked package; only parsing is needed. _, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go index 83ffcc28d29..afca98d06f7 100644 --- a/gopls/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -62,6 +62,7 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p } if len(fns.Benchmarks) > 0 { + // TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed. _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) if err != nil { return nil, err @@ -100,6 +101,7 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) ( if !strings.HasSuffix(fh.URI().Filename(), "_test.go") { return out, nil } + // TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed. pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) if err != nil { return out, err @@ -227,6 +229,7 @@ func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([ } func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { + // TODO(adonovan): avoid loading type-checked package; only Metadata is needed. _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 64498e22b83..b220a6bf846 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -109,6 +109,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi } editsPerFile[fh.URI()] = te } + // TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed. _, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 3d09e102f7a..c2522293571 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -139,6 +139,7 @@ var ErrNoRuneFound = errors.New("no rune found") // findRune returns rune information for a position in a file. func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, MappedRange, error) { + // TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed. pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return 0, MappedRange{}, err diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index d0310560c10..f147108e31e 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -78,6 +78,10 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit for _, dep := range rdeps { for _, f := range dep.CompiledGoFiles() { for _, imp := range f.File.Imports { + // TODO(adonovan): using UnquoteImport() here would + // reveal that there's an ImportPath==PackagePath + // comparison that doesn't account for vendoring. + // Use dep.Metadata().DepsByPkgPath instead. if path, err := strconv.Unquote(imp.Path.Value); err == nil && path == string(renamingPkg.PkgPath()) { refs = append(refs, &ReferenceInfo{ Name: packageName, diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 9e4b704c224..4cdfc7124af 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -280,6 +280,7 @@ func getStubFile(ctx context.Context, obj types.Object, snapshot Snapshot) (*Par objPos := snapshot.FileSet().Position(obj.Pos()) objFile := span.URIFromPath(objPos.Filename) objectFH := snapshot.FindFile(objFile) + // TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed. _, goFile, err := GetParsedFile(ctx, snapshot, objectFH, WidestPackage) if err != nil { return nil, nil, fmt.Errorf("GetParsedFile: %w", err) diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index d7ab5c579b7..55267c95ad4 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -94,6 +94,9 @@ func (s MappedRange) URI() span.URI { // GetParsedFile is a convenience function that extracts the Package and // ParsedGoFile for a file in a Snapshot. pkgPolicy is one of NarrowestPackage/ // WidestPackage. +// +// Type-checking is expensive. Call cheaper methods of Snapshot if all +// you need is Metadata or parse trees. func GetParsedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgPolicy PackageFilter) (Package, *ParsedGoFile, error) { pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckWorkspace, pkgPolicy) if err != nil { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 99918504c6d..0e9dcd2a92a 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -210,7 +210,7 @@ type Snapshot interface { // Symbols returns all symbols in the snapshot. Symbols(ctx context.Context) map[span.URI][]Symbol - // Metadata returns package metadata associated with the given file URI. + // Metadata returns metadata for each package associated with the given file URI. MetadataForFile(ctx context.Context, uri span.URI) ([]*Metadata, error) // GetCriticalError returns any critical errors in the workspace. @@ -693,7 +693,7 @@ type Package interface { PkgPath() PackagePath GetTypesSizes() types.Sizes ForTest() string - Version() *module.Version + Version() *module.Version // may differ from Metadata.Module.Version // Results of parsing: FileSet() *token.FileSet diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index d29318efecc..a6f0f1c9bea 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -369,17 +369,10 @@ func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile s // Group packages by modules since vuln db is keyed by module. metadataByModule := map[source.PackagePath][]*source.Metadata{} for _, md := range metadata { - // TODO(hyangah): delete after go.dev/cl/452057 is merged. - // After the cl, this becomes an impossible condition. - if md == nil { - continue + if md.Module != nil { + modulePath := source.PackagePath(md.Module.Path) + metadataByModule[modulePath] = append(metadataByModule[modulePath], md) } - mi := md.Module - if mi == nil { - continue - } - modulePath := source.PackagePath(mi.Path) - metadataByModule[modulePath] = append(metadataByModule[modulePath], md) } // Request vuln entries from remote service. From e8a70a5e3daccbec8c73a46a38db5713577acd28 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 28 Nov 2022 14:58:06 -0500 Subject: [PATCH 477/723] gopls/internal/lsp: remove access to mutable session state from the view To avoid concurrency bugs and/or operation on inconsistent session state, remove visibility into the session from views. In a few places, this refactoring highlighted bugs (such as incorrect usage of view.SetOptions). This broke one of the debug templates, for which it was simply easier to remove the back-link from view to session. Change-Id: I4dbce0dcebab6f25a9c70524310ae1e0e04e2d97 Reviewed-on: https://go-review.googlesource.com/c/tools/+/453815 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley --- gopls/internal/lsp/cache/cache.go | 4 +- gopls/internal/lsp/cache/load.go | 5 -- gopls/internal/lsp/cache/mod.go | 2 +- gopls/internal/lsp/cache/mod_tidy.go | 2 +- gopls/internal/lsp/cache/session.go | 78 +++++++++++------------- gopls/internal/lsp/cache/snapshot.go | 12 ++-- gopls/internal/lsp/cache/view.go | 29 ++++----- gopls/internal/lsp/completion_test.go | 4 +- gopls/internal/lsp/debug/serve.go | 7 +-- gopls/internal/lsp/lsp_test.go | 12 ++-- gopls/internal/lsp/server.go | 4 +- gopls/internal/lsp/source/source_test.go | 17 ++++-- gopls/internal/lsp/source/view.go | 9 --- gopls/internal/lsp/workspace.go | 4 +- gopls/internal/lsp/workspace_symbol.go | 13 +++- gopls/internal/vulncheck/command_test.go | 2 +- 16 files changed, 93 insertions(+), 111 deletions(-) diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go index 0eb00f23201..8c06b6d5ccf 100644 --- a/gopls/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -173,11 +173,11 @@ func (c *Cache) NewSession(ctx context.Context) *Session { c.options(options) } s := &Session{ - cache: c, id: strconv.FormatInt(index, 10), + cache: c, + gocmdRunner: &gocommand.Runner{}, options: options, overlays: make(map[span.URI]*overlay), - gocmdRunner: &gocommand.Runner{}, } event.Log(ctx, "New session", KeyCreateSession.Of(s)) return s diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 321ff5f04a0..ccba4e8c3f0 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -105,11 +105,6 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc } sort.Strings(query) // for determinism - if s.view.Options().VerboseWorkDoneProgress { - work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading query=%s", query), nil, nil) - defer work.End(ctx, "Done.") - } - ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query)) defer done() diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index 97bdb2f019a..757bb5e8fca 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -186,7 +186,7 @@ func (s *snapshot) goSum(ctx context.Context, modURI span.URI) []byte { var sumFH source.FileHandle = s.FindFile(sumURI) if sumFH == nil { var err error - sumFH, err = s.view.session.cache.getFile(ctx, sumURI) + sumFH, err = s.view.cache.getFile(ctx, sumURI) if err != nil { return nil } diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index fc97ee6b25e..f4332119c80 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -106,7 +106,7 @@ func modTidyImpl(ctx context.Context, snapshot *snapshot, filename string, pm *s // Keep the temporary go.mod file around long enough to parse it. defer cleanup() - if _, err := snapshot.view.session.gocmdRunner.Run(ctx, *inv); err != nil { + if _, err := snapshot.view.gocmdRunner.Run(ctx, *inv); err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 28acd5d98dd..5eca7c832dc 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -14,9 +14,9 @@ import ( "sync/atomic" "golang.org/x/tools/gopls/internal/govulncheck" - "golang.org/x/tools/gopls/internal/lsp/progress" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" @@ -25,8 +25,12 @@ import ( ) type Session struct { - cache *Cache - id string + // Unique identifier for this session. + id string + + // Immutable attributes shared across views. + cache *Cache // shared cache + gocmdRunner *gocommand.Runner // limits go command concurrency optionsMu sync.Mutex options *source.Options @@ -37,11 +41,6 @@ type Session struct { overlayMu sync.Mutex overlays map[span.URI]*overlay - - // gocmdRunner guards go command calls from concurrency errors. - gocmdRunner *gocommand.Runner - - progress *progress.Tracker } type overlay struct { @@ -139,12 +138,6 @@ func (s *Session) SetOptions(options *source.Options) { s.options = options } -// SetProgressTracker sets the progress tracker for the session. -func (s *Session) SetProgressTracker(tracker *progress.Tracker) { - // The progress tracker should be set before any view is initialized. - s.progress = tracker -} - // Shutdown the session and all views it has created. func (s *Session) Shutdown(ctx context.Context) { var views []*View @@ -154,7 +147,7 @@ func (s *Session) Shutdown(ctx context.Context) { s.viewMap = nil s.viewMu.Unlock() for _, view := range views { - view.shutdown(ctx) + view.shutdown() } event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s)) } @@ -169,7 +162,7 @@ func (s *Session) Cache() *Cache { // of its gopls workspace module in that directory, so that client tooling // can execute in the same main module. On success it also returns a release // function that must be called when the Snapshot is no longer needed. -func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (source.View, source.Snapshot, func(), error) { +func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (*View, source.Snapshot, func(), error) { s.viewMu.Lock() defer s.viewMu.Unlock() for _, view := range s.views { @@ -230,10 +223,11 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, backgroundCtx, cancel := context.WithCancel(baseCtx) v := &View{ - session: s, + id: strconv.FormatInt(index, 10), + cache: s.cache, + gocmdRunner: s.gocmdRunner, initialWorkspaceLoad: make(chan struct{}), initializationSema: make(chan struct{}, 1), - id: strconv.FormatInt(index, 10), options: options, baseCtx: baseCtx, name: name, @@ -308,7 +302,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, } // View returns a view with a matching name, if the session has one. -func (s *Session) View(name string) source.View { +func (s *Session) View(name string) *View { s.viewMu.RLock() defer s.viewMu.RUnlock() for _, view := range s.views { @@ -321,11 +315,7 @@ func (s *Session) View(name string) source.View { // ViewOf returns a view corresponding to the given URI. // If the file is not already associated with a view, pick one using some heuristics. -func (s *Session) ViewOf(uri span.URI) (source.View, error) { - return s.viewOf(uri) -} - -func (s *Session) viewOf(uri span.URI) (*View, error) { +func (s *Session) ViewOf(uri span.URI) (*View, error) { s.viewMu.RLock() defer s.viewMu.RUnlock() // Check if we already know this file. @@ -340,13 +330,11 @@ func (s *Session) viewOf(uri span.URI) (*View, error) { return s.viewMap[uri], nil } -func (s *Session) Views() []source.View { +func (s *Session) Views() []*View { s.viewMu.RLock() defer s.viewMu.RUnlock() - result := make([]source.View, len(s.views)) - for i, v := range s.views { - result[i] = v - } + result := make([]*View, len(s.views)) + copy(result, s.views) return result } @@ -378,19 +366,19 @@ func bestViewForURI(uri span.URI, views []*View) *View { return views[0] } -func (s *Session) removeView(ctx context.Context, view *View) error { +// RemoveView removes the view v from the session +func (s *Session) RemoveView(view *View) { s.viewMu.Lock() defer s.viewMu.Unlock() - i, err := s.dropView(ctx, view) - if err != nil { - return err + i := s.dropView(view) + if i == -1 { // error reported elsewhere + return } // delete this view... we don't care about order but we do want to make // sure we can garbage collect the view s.views[i] = s.views[len(s.views)-1] s.views[len(s.views)-1] = nil s.views = s.views[:len(s.views)-1] - return nil } func (s *Session) updateView(ctx context.Context, view *View, options *source.Options) (*View, error) { @@ -406,9 +394,9 @@ func (s *Session) updateView(ctx context.Context, view *View, options *source.Op seqID := view.snapshot.sequenceID // Preserve sequence IDs when updating a view in place. view.snapshotMu.Unlock() - i, err := s.dropView(ctx, view) - if err != nil { - return nil, err + i := s.dropView(view) + if i == -1 { + return nil, fmt.Errorf("view %q not found", view.id) } v, _, release, err := s.createView(ctx, view.name, view.folder, options, seqID) @@ -428,18 +416,24 @@ func (s *Session) updateView(ctx context.Context, view *View, options *source.Op return v, nil } -func (s *Session) dropView(ctx context.Context, v *View) (int, error) { +// dropView removes v from the set of views for the receiver s and calls +// v.shutdown, returning the index of v in s.views (if found), or -1 if v was +// not found. s.viewMu must be held while calling this function. +func (s *Session) dropView(v *View) int { // we always need to drop the view map s.viewMap = make(map[span.URI]*View) for i := range s.views { if v == s.views[i] { // we found the view, drop it and return the index it was found at s.views[i] = nil - v.shutdown(ctx) - return i, nil + v.shutdown() + return i } } - return -1, fmt.Errorf("view %s for %v not found", v.Name(), v.Folder()) + // TODO(rfindley): it looks wrong that we don't shutdown v in this codepath. + // We should never get here. + bug.Reportf("tried to drop nonexistent view %q", v.id) + return -1 } func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModification) error { @@ -498,7 +492,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif if c.OnDisk { continue } - bestView, err := s.viewOf(c.URI) + bestView, err := s.ViewOf(c.URI) if err != nil { return nil, nil, err } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 1f738c6c22f..5a15641a5f0 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -258,7 +258,7 @@ func (s *snapshot) BackgroundContext() context.Context { } func (s *snapshot) FileSet() *token.FileSet { - return s.view.session.cache.fset + return s.view.cache.fset } func (s *snapshot) ModFiles() []span.URI { @@ -376,7 +376,7 @@ func (s *snapshot) config(ctx context.Context, inv *gocommand.Invocation) *packa if typesinternal.SetUsesCgo(&types.Config{}) { cfg.Mode |= packages.LoadMode(packagesinternal.TypecheckCgo) } - packagesinternal.SetGoCmdRunner(cfg, s.view.session.gocmdRunner) + packagesinternal.SetGoCmdRunner(cfg, s.view.gocmdRunner) return cfg } @@ -387,7 +387,7 @@ func (s *snapshot) RunGoCommandDirect(ctx context.Context, mode source.Invocatio } defer cleanup() - return s.view.session.gocmdRunner.Run(ctx, *inv) + return s.view.gocmdRunner.Run(ctx, *inv) } func (s *snapshot) RunGoCommandPiped(ctx context.Context, mode source.InvocationFlags, inv *gocommand.Invocation, stdout, stderr io.Writer) error { @@ -396,7 +396,7 @@ func (s *snapshot) RunGoCommandPiped(ctx context.Context, mode source.Invocation return err } defer cleanup() - return s.view.session.gocmdRunner.RunPiped(ctx, *inv, stdout, stderr) + return s.view.gocmdRunner.RunPiped(ctx, *inv, stdout, stderr) } func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd string, run func(invoke func(...string) (*bytes.Buffer, error)) error) (bool, []byte, []byte, error) { @@ -417,7 +417,7 @@ func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd stri invoke := func(args ...string) (*bytes.Buffer, error) { inv.Verb = args[0] inv.Args = args[1:] - return s.view.session.gocmdRunner.Run(ctx, *inv) + return s.view.gocmdRunner.Run(ctx, *inv) } if err := run(invoke); err != nil { return false, nil, nil, err @@ -1319,7 +1319,7 @@ func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.Versi return fh, nil } - fh, err := s.view.session.cache.getFile(ctx, f.URI()) // read the file + fh, err := s.view.cache.getFile(ctx, f.URI()) // read the file if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index db7cbb7f204..4adcfb11662 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -36,8 +36,10 @@ import ( ) type View struct { - session *Session - id string + id string + + cache *Cache // shared cache + gocmdRunner *gocommand.Runner // limits go command concurrency optionsMu sync.Mutex options *source.Options @@ -343,7 +345,11 @@ func minorOptionsChange(a, b *source.Options) bool { return reflect.DeepEqual(aBuildFlags, bBuildFlags) } -func (v *View) SetOptions(ctx context.Context, options *source.Options) (source.View, error) { +// SetViewOptions sets the options of the given view to new values. Calling +// this may cause the view to be invalidated and a replacement view added to +// the session. If so the new view will be returned, otherwise the original one +// will be returned. +func (s *Session) SetViewOptions(ctx context.Context, v *View, options *source.Options) (*View, error) { // no need to rebuild the view if the options were not materially changed v.optionsMu.Lock() if minorOptionsChange(v.options, options) { @@ -352,7 +358,7 @@ func (v *View) SetOptions(ctx context.Context, options *source.Options) (source. return v, nil } v.optionsMu.Unlock() - newView, err := v.session.updateView(ctx, v, options) + newView, err := s.updateView(ctx, v, options) return newView, err } @@ -375,7 +381,7 @@ func (s *snapshot) WriteEnv(ctx context.Context, w io.Writer) error { fullEnv[s[0]] = s[1] } } - goVersion, err := s.view.session.gocmdRunner.Run(ctx, gocommand.Invocation{ + goVersion, err := s.view.gocmdRunner.Run(ctx, gocommand.Invocation{ Verb: "version", Env: env, WorkingDir: s.view.rootURI.Filename(), @@ -584,16 +590,9 @@ func (v *View) findFile(uri span.URI) (*fileBase, error) { return nil, nil } -func (v *View) Shutdown(ctx context.Context) { - v.session.removeView(ctx, v) -} - // shutdown releases resources associated with the view, and waits for ongoing // work to complete. -// -// TODO(rFindley): probably some of this should also be one in View.Shutdown -// above? -func (v *View) shutdown(ctx context.Context) { +func (v *View) shutdown() { // Cancel the initial workspace load if it is still running. v.initCancelFirstAttempt() @@ -610,10 +609,6 @@ func (v *View) shutdown(ctx context.Context) { v.snapshotWG.Wait() } -func (v *View) Session() *Session { - return v.session -} - func (s *snapshot) IgnoredFile(uri span.URI) bool { filename := uri.Filename() var prefixes []string diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index 23d69ed1269..22578467dbf 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -129,12 +129,12 @@ func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*sourc original := view.Options() modified := view.Options().Clone() options(modified) - view, err = view.SetOptions(r.ctx, modified) + view, err = r.server.session.SetViewOptions(r.ctx, view, modified) if err != nil { t.Error(err) return nil } - defer view.SetOptions(r.ctx, original) + defer r.server.session.SetViewOptions(r.ctx, view, original) list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go index 248041ed68a..abc939eff7e 100644 --- a/gopls/internal/lsp/debug/serve.go +++ b/gopls/internal/lsp/debug/serve.go @@ -130,11 +130,7 @@ func (st *State) Session(id string) *cache.Session { func (st *State) Views() []*cache.View { var views []*cache.View for _, s := range st.Sessions() { - for _, v := range s.Views() { - if cv, ok := v.(*cache.View); ok { - views = append(views, cv) - } - } + views = append(views, s.Views()...) } return views } @@ -897,7 +893,6 @@ var ViewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` {{define "body"}} Name: {{.Name}}
    Folder: {{.Folder}}
    -From: {{template "sessionlink" .Session.ID}}

    Environment

      {{range .Options.Env}}
    • {{.}}
    • {{end}}
    {{end}} diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index d966d94397d..5a29cd8b64b 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -70,12 +70,12 @@ func testLSP(t *testing.T, datum *tests.Data) { t.Fatal(err) } - defer view.Shutdown(ctx) + defer session.RemoveView(view) // Enable type error analyses for tests. // TODO(golang/go#38212): Delete this once they are enabled by default. tests.EnableAllAnalyzers(view, options) - view.SetOptions(ctx, options) + session.SetViewOptions(ctx, view, options) // Enable all inlay hints for tests. tests.EnableAllInlayHints(view, options) @@ -233,10 +233,11 @@ func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { } original := view.Options() modified := original + defer r.server.session.SetViewOptions(r.ctx, view, original) // Test all folding ranges. modified.LineFoldingOnly = false - view, err = view.SetOptions(r.ctx, modified) + view, err = r.server.session.SetViewOptions(r.ctx, view, modified) if err != nil { t.Error(err) return @@ -254,7 +255,7 @@ func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { // Test folding ranges with lineFoldingOnly = true. modified.LineFoldingOnly = true - view, err = view.SetOptions(r.ctx, modified) + view, err = r.server.session.SetViewOptions(r.ctx, view, modified) if err != nil { t.Error(err) return @@ -269,7 +270,6 @@ func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { return } r.foldingRanges(t, "foldingRange-lineFolding", uri, ranges) - view.SetOptions(r.ctx, original) } func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) { @@ -1328,7 +1328,7 @@ func TestBytesOffset(t *testing.T) { } } -func (r *runner) collectDiagnostics(view source.View) { +func (r *runner) collectDiagnostics(view *cache.View) { if r.diagnostics != nil { return } diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go index 9f7059141d5..6d1775ff46b 100644 --- a/gopls/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -23,8 +23,6 @@ const concurrentAnalyses = 1 // NewServer creates an LSP server and binds it to handle incoming client // messages on on the supplied stream. func NewServer(session *cache.Session, client protocol.ClientCloser) *Server { - tracker := progress.NewTracker(client) - session.SetProgressTracker(tracker) return &Server{ diagnostics: map[span.URI]*fileReports{}, gcOptimizationDetails: make(map[source.PackageID]struct{}), @@ -33,7 +31,7 @@ func NewServer(session *cache.Session, client protocol.ClientCloser) *Server { session: session, client: client, diagnosticsSema: make(chan struct{}, concurrentAnalyses), - progress: tracker, + progress: progress.NewTracker(client), diagDebouncer: newDebouncer(), watchedFileDebouncer: newDebouncer(), } diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index d7dc77c6a1c..e5405301952 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -38,8 +38,9 @@ func TestSource(t *testing.T) { } type runner struct { + session *cache.Session + view *cache.View snapshot source.Snapshot - view source.View data *tests.Data ctx context.Context normalizers []tests.Normalizer @@ -58,12 +59,15 @@ func testSource(t *testing.T, datum *tests.Data) { t.Fatal(err) } release() - defer view.Shutdown(ctx) + defer session.RemoveView(view) // Enable type error analyses for tests. // TODO(golang/go#38212): Delete this once they are enabled by default. tests.EnableAllAnalyzers(view, options) - view.SetOptions(ctx, options) + view, err = session.SetViewOptions(ctx, view, options) + if err != nil { + t.Fatal(err) + } var modifications []source.FileModification for filename, content := range datum.Config.Overlay { @@ -84,6 +88,7 @@ func testSource(t *testing.T, datum *tests.Data) { snapshot, release := view.Snapshot(ctx) defer release() r := &runner{ + session: session, view: view, snapshot: snapshot, data: datum, @@ -283,14 +288,14 @@ func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*sourc original := r.view.Options() modified := original.Clone() options(modified) - newView, err := r.view.SetOptions(r.ctx, modified) - if newView != r.view { + view, err := r.session.SetViewOptions(r.ctx, r.view, modified) + if view != r.view { t.Fatalf("options change unexpectedly created new view") } if err != nil { t.Fatal(err) } - defer r.view.SetOptions(r.ctx, original) + defer r.session.SetViewOptions(r.ctx, view, original) list, surrounding, err := completion.Completion(r.ctx, r.snapshot, fh, protocol.Position{ Line: uint32(src.Start().Line() - 1), diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 0e9dcd2a92a..fa55cdeecd5 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -283,18 +283,9 @@ type View interface { // Folder returns the folder with which this view was created. Folder() span.URI - // Shutdown closes this view, and detaches it from its session. - Shutdown(ctx context.Context) - // Options returns a copy of the Options for this view. Options() *Options - // SetOptions sets the options of this view to new values. - // Calling this may cause the view to be invalidated and a replacement view - // added to the session. If so the new view will be returned, otherwise the - // original one will be. - SetOptions(context.Context, *Options) (View, error) - // Snapshot returns the current snapshot for the view, and a // release function that must be called when the Snapshot is // no longer needed. diff --git a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go index 4f9948ec9d3..e4326b1fbce 100644 --- a/gopls/internal/lsp/workspace.go +++ b/gopls/internal/lsp/workspace.go @@ -18,7 +18,7 @@ func (s *Server) didChangeWorkspaceFolders(ctx context.Context, params *protocol for _, folder := range event.Removed { view := s.session.View(folder.Name) if view != nil { - view.Shutdown(ctx) + s.session.RemoveView(view) } else { return fmt.Errorf("view %s for %v not found", folder.Name, folder.URI) } @@ -58,7 +58,7 @@ func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChan if err := s.fetchConfig(ctx, view.Name(), view.Folder(), options); err != nil { return err } - view, err := view.SetOptions(ctx, options) + view, err := s.session.SetViewOptions(ctx, view, options) if err != nil { return err } diff --git a/gopls/internal/lsp/workspace_symbol.go b/gopls/internal/lsp/workspace_symbol.go index 9101a3e7d11..88b3e8865ae 100644 --- a/gopls/internal/lsp/workspace_symbol.go +++ b/gopls/internal/lsp/workspace_symbol.go @@ -7,9 +7,9 @@ package lsp import ( "context" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" ) func (s *Server) symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) { @@ -19,5 +19,14 @@ func (s *Server) symbol(ctx context.Context, params *protocol.WorkspaceSymbolPar views := s.session.Views() matcher := s.session.Options().SymbolMatcher style := s.session.Options().SymbolStyle - return source.WorkspaceSymbols(ctx, matcher, style, views, params.Query) + // TODO(rfindley): it looks wrong that we need to pass views here. + // + // Evidence: + // - this is the only place we convert views to []source.View + // - workspace symbols is the only place where we call source.View.Snapshot + var sourceViews []source.View + for _, v := range views { + sourceViews = append(sourceViews, v) + } + return source.WorkspaceSymbols(ctx, matcher, style, sourceViews, params.Query) } diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index b7a270e35aa..4032693f9f4 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -319,7 +319,7 @@ func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Co // The snapshot must be released before calling view.Shutdown, to avoid a // deadlock. release() - view.Shutdown(ctx) + session.RemoveView(view) }() test(ctx, snapshot) From f1b76aef9342e5d3e99751b0dfdacf930b5ab0a6 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Sun, 27 Nov 2022 20:54:19 -0500 Subject: [PATCH 478/723] gopls/internal/lsp: add an option to overwrite previous diagnostics Diagnostics for a file in a snapshot are organized by the diagnostics source, and storeDiagnostics had been merging the new diagnostics into the existing diagnostics if any. This works only if we can assume diagnostics on the same diagnosticSource and the same snapshot don't change or at least are cumulative. Since any file changes or setting changes result in new snapshots, this assumption had been true. With vulncheck diagnostics that may switch between diagnostics snapshot.ModVuln output and govulncheck output stored in snapshot.View for the same snapshot, this assumption no longer holds. For example, assume that we computed snapshot.ModVuln and stored and published the diagnostics on a snapshot. Later the user initiated "Run govulncheck" and gopls had to recompute the diagnostics. The snapshot version is same, but the new diagnostics can be different (i.e. different messaging, severity, related info, etc). Previously storeDiagnostics just added these as additional diagnostics, and as a result, gopls published both snapshot.ModVuln diagnostics and snapshot.View.Vulnerabilities diagnostics. That is confusing for users. This change makes storeDiagnostics take an extra "merge" flag. If set, this works as before. If not set, this resets the diagnostics map for the diagnosticsSource. For vulncheck diagnostics, we store diagnostics with merge=false so either snapshot.ModVuln diagnostics or snapshot.View.Vulnerabilities diagnostics are shown, not both. This can cause an extra allocation for each storeDiagnostics call when called with modVulncheckSource. An alternative is to increase the global id when govulncheck result is set or reset. Global id is changed through invalidation and that seems rather heavy weight. Change-Id: I1133b01f5032c17dc976a163b08228a1706997e6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/453596 Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/diagnostics.go | 31 ++++++++++++----------- gopls/internal/regtest/misc/vuln_test.go | 32 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 43097892840..19bc3ce124e 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -239,7 +239,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn }() // common code for dispatching diagnostics - store := func(dsource diagnosticSource, operation string, diagsByFileID map[source.VersionedFileIdentity][]*source.Diagnostic, err error) { + store := func(dsource diagnosticSource, operation string, diagsByFileID map[source.VersionedFileIdentity][]*source.Diagnostic, err error, merge bool) { if err != nil { event.Error(ctx, "warning: while "+operation, err, source.SnapshotLabels(snapshot)...) } @@ -248,7 +248,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn event.Error(ctx, "missing URI while "+operation, fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) continue } - s.storeDiagnostics(snapshot, id.URI, dsource, diags) + s.storeDiagnostics(snapshot, id.URI, dsource, diags, merge) } } @@ -258,7 +258,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn log.Trace.Log(ctx, "diagnose cancelled") return } - store(modCheckUpgradesSource, "diagnosing go.mod upgrades", upgradeReports, upgradeErr) + store(modCheckUpgradesSource, "diagnosing go.mod upgrades", upgradeReports, upgradeErr, true) // Diagnose go.work file. workReports, workErr := work.Diagnostics(ctx, snapshot) @@ -266,7 +266,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn log.Trace.Log(ctx, "diagnose cancelled") return } - store(workSource, "diagnosing go.work file", workReports, workErr) + store(workSource, "diagnosing go.work file", workReports, workErr, true) // All subsequent steps depend on the completion of // type-checking of the all active packages in the workspace. @@ -281,7 +281,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn log.Trace.Log(ctx, "diagnose cancelled") return } - store(modSource, "diagnosing go.mod file", modReports, modErr) + store(modSource, "diagnosing go.mod file", modReports, modErr, true) // Diagnose vulnerabilities. vulnReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) @@ -289,7 +289,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn log.Trace.Log(ctx, "diagnose cancelled") return } - store(modVulncheckSource, "diagnosing vulnerabilities", vulnReports, vulnErr) + store(modVulncheckSource, "diagnosing vulnerabilities", vulnReports, vulnErr, false) if s.shouldIgnoreError(ctx, snapshot, activeErr) { return @@ -305,7 +305,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn // Diagnose template (.tmpl) files. for _, f := range snapshot.Templates() { diags := template.Diagnose(f) - s.storeDiagnostics(snapshot, f.URI(), typeCheckSource, diags) + s.storeDiagnostics(snapshot, f.URI(), typeCheckSource, diags, true) } // If there are no workspace packages, there is nothing to diagnose and @@ -345,7 +345,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn if diagnostic == nil { continue } - s.storeDiagnostics(snapshot, o.URI(), orphanedSource, []*source.Diagnostic{diagnostic}) + s.storeDiagnostics(snapshot, o.URI(), orphanedSource, []*source.Diagnostic{diagnostic}, true) } } @@ -372,7 +372,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg // builtin.go exists only for documentation purposes, and is not valid Go code. // Don't report distracting errors if !snapshot.IsBuiltin(ctx, cgf.URI) { - s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, pkgDiagnostics[cgf.URI]) + s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, pkgDiagnostics[cgf.URI], true) } } if includeAnalysis && !pkg.HasListOrParseErrors() { @@ -382,7 +382,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg return } for _, cgf := range pkg.CompiledGoFiles() { - s.storeDiagnostics(snapshot, cgf.URI, analysisSource, reports[cgf.URI]) + s.storeDiagnostics(snapshot, cgf.URI, analysisSource, reports[cgf.URI], true) } } @@ -411,7 +411,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg if fh == nil || !fh.Saved() { continue } - s.storeDiagnostics(snapshot, id.URI, gcDetailsSource, diags) + s.storeDiagnostics(snapshot, id.URI, gcDetailsSource, diags, true) } } s.gcOptimizationDetailsMu.Unlock() @@ -438,7 +438,10 @@ func (s *Server) mustPublishDiagnostics(uri span.URI) { // storeDiagnostics stores results from a single diagnostic source. If merge is // true, it merges results into any existing results for this snapshot. -func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsource diagnosticSource, diags []*source.Diagnostic) { +// +// TODO(hyangah): investigate whether we can unconditionally overwrite previous report.diags +// with the new diags and eliminate the need for the `merge` flag. +func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsource diagnosticSource, diags []*source.Diagnostic, merge bool) { // Safeguard: ensure that the file actually exists in the snapshot // (see golang.org/issues/38602). fh := snapshot.FindFile(uri) @@ -459,7 +462,7 @@ func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsourc if report.snapshotID > snapshot.GlobalID() { return } - if report.diags == nil || report.snapshotID != snapshot.GlobalID() { + if report.diags == nil || report.snapshotID != snapshot.GlobalID() || !merge { report.diags = map[string]*source.Diagnostic{} } report.snapshotID = snapshot.GlobalID() @@ -494,7 +497,7 @@ func (s *Server) showCriticalErrorStatus(ctx context.Context, snapshot source.Sn if err != nil { event.Error(ctx, "errors loading workspace", err.MainError, source.SnapshotLabels(snapshot)...) for _, d := range err.Diagnostics { - s.storeDiagnostics(snapshot, d.URI, modSource, []*source.Diagnostic{d}) + s.storeDiagnostics(snapshot, d.URI, modSource, []*source.Diagnostic{d}, true) } errMsg = strings.ReplaceAll(err.MainError.Error(), "\n", " ") } diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index bf2f077a486..e07abb96706 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -486,6 +486,38 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { } else { wantNoVulncheckDiagnostics(env, t) } + + if tc.name == "imports" && tc.wantDiagnostics { + // test we get only govulncheck-based diagnostics after "run govulncheck". + var result command.RunVulncheckResult + env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp, &result) + gotDiagnostics := &protocol.PublishDiagnosticsParams{} + env.Await( + OnceMet( + CompletedProgress(result.Token), + ShownMessage("Found"), + ), + ) + env.Await( + OnceMet( + env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"), + ReadDiagnostics("go.mod", gotDiagnostics), + ), + ) + // We expect only one diagnostic for GO-2022-02. + count := 0 + for _, diag := range gotDiagnostics.Diagnostics { + if strings.Contains(diag.Message, "GO-2022-02") { + count++ + if got, want := diag.Severity, protocol.SeverityWarning; got != want { + t.Errorf("Diagnostic for GO-2022-02 = %v, want %v", got, want) + } + } + } + if count != 1 { + t.Errorf("Unexpected number of diagnostics about GO-2022-02 = %v, want 1:\n%+v", count, stringify(gotDiagnostics)) + } + } }) }) } From 9a54670c13aa3e92dc7949d67aefd64c949cfce1 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Mon, 28 Nov 2022 09:02:07 -0500 Subject: [PATCH 479/723] gopls/internal/lsp/mod: add "Run govulncheck" codeaction For each diagnostic generated from the vulncheck diagnostics, add the "Run govulncheck" code action so users can conveniently run govulncheck to inspect whether the vulnerability is actually used. Change-Id: I931110e15e72d8f6c27e373e95daaaef5d1bf7e4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/453597 Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Suzy Mueller --- gopls/internal/lsp/mod/diagnostics.go | 42 +++++++++++++++++------- gopls/internal/regtest/misc/vuln_test.go | 8 +++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 5c80c55e104..b48f39c3640 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -193,6 +193,16 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return nil, nil } + vulncheck, err := command.NewRunVulncheckExpCommand("Run govulncheck", command.VulncheckArgs{ + URI: protocol.DocumentURI(fh.URI()), + Pattern: "./...", + }) + if err != nil { + // must not happen + return nil, err // TODO: bug report + } + suggestVulncheck := source.SuggestedFixFromCommand(vulncheck, protocol.QuickFix) + type modVuln struct { mod *govulncheck.Module vuln *govulncheck.Vuln @@ -284,6 +294,9 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, if len(infoFixes) > 0 { infoFixes = append(infoFixes, sf) } + if !fromGovulncheck { + infoFixes = append(infoFixes, suggestVulncheck) + } sort.Strings(warning) sort.Strings(info) @@ -475,32 +488,39 @@ func upgradeTitle(fixedVersion string) string { return title } -// SelectUpgradeCodeActions takes a list of upgrade code actions for a -// required module and returns a more selective list of upgrade code actions, -// where the code actions have been deduped. +// SelectUpgradeCodeActions takes a list of code actions for a required module +// and returns a more selective list of upgrade code actions, +// where the code actions have been deduped. Code actions unrelated to upgrade +// remain untouched. func SelectUpgradeCodeActions(actions []protocol.CodeAction) []protocol.CodeAction { if len(actions) <= 1 { return actions // return early if no sorting necessary } + var others []protocol.CodeAction + set := make(map[string]protocol.CodeAction) for _, action := range actions { - set[action.Command.Title] = action + if strings.HasPrefix(action.Title, upgradeCodeActionPrefix) { + set[action.Command.Title] = action + } else { + others = append(others, action) + } } - var result []protocol.CodeAction + var upgrades []protocol.CodeAction for _, action := range set { - result = append(result, action) + upgrades = append(upgrades, action) } // Sort results by version number, latest first. // There should be no duplicates at this point. - sort.Slice(result, func(i, j int) bool { - vi, vj := getUpgradeVersion(result[i]), getUpgradeVersion(result[j]) + sort.Slice(upgrades, func(i, j int) bool { + vi, vj := getUpgradeVersion(upgrades[i]), getUpgradeVersion(upgrades[j]) return vi == "latest" || (vj != "latest" && semver.Compare(vi, vj) > 0) }) // Choose at most one specific version and the latest. - if getUpgradeVersion(result[0]) == "latest" { - return result[:2] + if getUpgradeVersion(upgrades[0]) == "latest" { + return append(upgrades[:2], others...) } - return result[:1] + return append(upgrades[:1], others...) } func getUpgradeVersion(p protocol.CodeAction) string { diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index e07abb96706..a261c22611f 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -421,12 +421,14 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { codeActions: []string{ "Upgrade to latest", "Upgrade to v1.0.6", + "Run govulncheck", }, }, }, codeActions: []string{ "Upgrade to latest", "Upgrade to v1.0.6", + "Run govulncheck", }, hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, }, @@ -435,8 +437,14 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { { msg: "golang.org/bmod has a vulnerability GO-2022-02.", severity: protocol.SeverityInformation, + codeActions: []string{ + "Run govulncheck", + }, }, }, + codeActions: []string{ + "Run govulncheck", + }, hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."}, }, } From 7a1b5707a6c9bf6b6e5df881927cb2bb266c436c Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Sun, 27 Nov 2022 16:57:51 -0500 Subject: [PATCH 480/723] gopls/internal/vulncheck: check vulnerabilities in standard library Metadata for packages from standard library have no Module field set. In that case, associate it with a fake Module that represent the pseudo module (the module name is stdlib and the version is the go command version). That will make the existing detection logic to continue to work for standard library packages. Change-Id: I99a6a33c416405341e3098c47809d05a9a774040 Reviewed-on: https://go-review.googlesource.com/c/tools/+/453598 Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/regtest/misc/vuln_test.go | 73 ++++++++++++++++++++++++ gopls/internal/vulncheck/command.go | 22 +++++-- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index a261c22611f..5668c9d3c8e 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -202,6 +202,79 @@ func main() { }) } +func TestRunVulncheckDiagnosticsStd(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- main.go -- +package main + +import ( + "archive/zip" + "fmt" +) + +func main() { + _, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB + fmt.Println(err) +} +` + + db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) + if err != nil { + t.Fatal(err) + } + defer db.Clean() + WithOptions( + EnvVars{ + // Let the analyzer read vulnerabilities data from the testdata/vulndb. + "GOVULNDB": db.URI(), + // When fetchinging stdlib package vulnerability info, + // behave as if our go version is go1.18 for this testing. + // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). + vulncheck.GoVersionForVulnTest: "go1.18", + "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. + }, + Settings{"ui.diagnostic.vulncheck": "Imports"}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("go.mod") + gotDiagnostics := &protocol.PublishDiagnosticsParams{} + env.AfterChange( + env.DiagnosticAtRegexp("go.mod", `module mod.com`), + ReadDiagnostics("go.mod", gotDiagnostics), + ) + testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ + "go.mod": { + IDs: []string{"GOSTDLIB"}, + Mode: govulncheck.ModeImports, + }, + }) + + wantVulncheckDiagnostics := map[string]vulnDiagExpectation{ + "module mod.com": { + diagnostics: []vulnDiag{ + { + msg: "go1.18 has a vulnerability GOSTDLIB.", + severity: protocol.SeverityInformation, + }, + }, + }, + } + + for pattern, want := range wantVulncheckDiagnostics { + modPathDiagnostics := testVulnDiagnostics(t, env, pattern, want, gotDiagnostics) + gotActions := env.CodeAction("go.mod", modPathDiagnostics) + if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { + t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", pattern, gotActions, want.codeActions, diff) + continue + } + } + }) +} + type fetchVulncheckResult struct { IDs []string Mode govulncheck.AnalysisMode diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index a6f0f1c9bea..3006d619241 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -369,10 +369,12 @@ func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile s // Group packages by modules since vuln db is keyed by module. metadataByModule := map[source.PackagePath][]*source.Metadata{} for _, md := range metadata { - if md.Module != nil { - modulePath := source.PackagePath(md.Module.Path) - metadataByModule[modulePath] = append(metadataByModule[modulePath], md) + mi := md.Module + modulePath := source.PackagePath("stdlib") + if mi != nil { + modulePath = source.PackagePath(mi.Path) } + metadataByModule[modulePath] = append(metadataByModule[modulePath], md) } // Request vuln entries from remote service. @@ -389,12 +391,22 @@ func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile s mu sync.Mutex ) + goVersion := snapshot.View().Options().Env[GoVersionForVulnTest] + if goVersion == "" { + goVersion = snapshot.View().GoVersionString() + } group.SetLimit(10) + stdlibModule := &packages.Module{ + Path: "stdlib", + Version: goVersion, + } for path, mds := range metadataByModule { path, mds := path, mds group.Go(func() error { - - effectiveModule := mds[0].Module + effectiveModule := stdlibModule + if m := mds[0].Module; m != nil { + effectiveModule = m + } for effectiveModule.Replace != nil { effectiveModule = effectiveModule.Replace } From bedad5a8e7c8ed8fd4b78fcfeaca247990860bd4 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Sun, 27 Nov 2022 23:39:15 -0500 Subject: [PATCH 481/723] gopls/internal/lsp/mod: add hover for stdlib vulnerabilities Change-Id: I296bcc9b1c9d4c3abd67d3465e8848e3bfc0999a Reviewed-on: https://go-review.googlesource.com/c/tools/+/453599 Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim --- gopls/internal/lsp/mod/hover.go | 81 ++++++++++++++++++++++-- gopls/internal/regtest/misc/vuln_test.go | 18 +++--- 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 71ffa9282d4..34173e8d89f 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -46,6 +46,14 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, return nil, fmt.Errorf("computing cursor position: %w", err) } + // If the cursor position is on a module statement + if hover, ok := hoverOnModuleStatement(ctx, pm, offset, snapshot, fh); ok { + return hover, nil + } + return hoverOnRequireStatement(ctx, pm, offset, snapshot, fh) +} + +func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh source.FileHandle) (*protocol.Hover, error) { // Confirm that the cursor is at the position of a require statement. var req *modfile.Require var startPos, endPos int @@ -75,6 +83,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, fromGovulncheck := true vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()] if vs == nil && snapshot.View().Options().Vulncheck == source.ModeVulncheckImports { + var err error vs, err = snapshot.ModVuln(ctx, fh.URI()) if err != nil { return nil, err @@ -115,6 +124,40 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, }, nil } +func hoverOnModuleStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh source.FileHandle) (*protocol.Hover, bool) { + if offset < pm.File.Module.Syntax.Start.Byte || offset > pm.File.Module.Syntax.End.Byte { + return nil, false + } + + rng, err := pm.Mapper.OffsetRange(pm.File.Module.Syntax.Start.Byte, pm.File.Module.Syntax.End.Byte) + if err != nil { + return nil, false + } + fromGovulncheck := true + vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()] + + if vs == nil && snapshot.View().Options().Vulncheck == source.ModeVulncheckImports { + vs, err = snapshot.ModVuln(ctx, fh.URI()) + if err != nil { + return nil, false + } + fromGovulncheck = false + } + modpath := "stdlib" + goVersion := snapshot.View().GoVersionString() + affecting, nonaffecting := lookupVulns(vs, modpath, goVersion) + options := snapshot.View().Options() + vulns := formatVulnerabilities(modpath, affecting, nonaffecting, options, fromGovulncheck) + + return &protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: options.PreferredContentFormat, + Value: vulns, + }, + Range: rng, + }, true +} + func formatHeader(modpath string, options *source.Options) string { var b strings.Builder // Write the heading as an H3. @@ -188,11 +231,12 @@ func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulnchec } for _, v := range affecting { fix := fixedVersionInfo(v, modPath) + pkgs := vulnerablePkgsInfo(v, modPath, useMarkdown) if useMarkdown { - fmt.Fprintf(&b, "- [**%v**](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) + fmt.Fprintf(&b, "- [**%v**](%v) %v%v%v\n", v.OSV.ID, href(v.OSV), formatMessage(v), pkgs, fix) } else { - fmt.Fprintf(&b, " - [%v] %v (%v) %v\n", v.OSV.ID, formatMessage(v), href(v.OSV), fix) + fmt.Fprintf(&b, " - [%v] %v (%v) %v%v\n", v.OSV.ID, formatMessage(v), href(v.OSV), pkgs, fix) } } if len(nonaffecting) > 0 { @@ -204,16 +248,45 @@ func formatVulnerabilities(modPath string, affecting, nonaffecting []*govulnchec } for _, v := range nonaffecting { fix := fixedVersionInfo(v, modPath) + pkgs := vulnerablePkgsInfo(v, modPath, useMarkdown) if useMarkdown { - fmt.Fprintf(&b, "- [%v](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) + fmt.Fprintf(&b, "- [%v](%v) %v%v%v\n", v.OSV.ID, href(v.OSV), formatMessage(v), pkgs, fix) } else { - fmt.Fprintf(&b, " - [%v] %v %v (%v)\n", v.OSV.ID, formatMessage(v), fix, href(v.OSV)) + fmt.Fprintf(&b, " - [%v] %v (%v) %v%v\n", v.OSV.ID, formatMessage(v), href(v.OSV), pkgs, fix) } } b.WriteString("\n") return b.String() } +func vulnerablePkgsInfo(v *govulncheck.Vuln, modPath string, useMarkdown bool) string { + var b bytes.Buffer + for _, m := range v.Modules { + if m.Path != modPath { + continue + } + if c := len(m.Packages); c == 1 { + b.WriteString("\n Vulnerable package is:") + } else if c > 1 { + b.WriteString("\n Vulnerable packages are:") + } + for _, pkg := range m.Packages { + if useMarkdown { + b.WriteString("\n * `") + } else { + b.WriteString("\n ") + } + b.WriteString(pkg.Path) + if useMarkdown { + b.WriteString("`") + } + } + } + if b.Len() == 0 { + return "" + } + return b.String() +} func fixedVersionInfo(v *govulncheck.Vuln, modPath string) string { fix := "\n\n **No fix is available.**" for _, m := range v.Modules { diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 5668c9d3c8e..9a7d2ff860b 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -234,7 +234,6 @@ func main() { "GOVULNDB": db.URI(), // When fetchinging stdlib package vulnerability info, // behave as if our go version is go1.18 for this testing. - // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). vulncheck.GoVersionForVulnTest: "go1.18", "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. }, @@ -261,6 +260,7 @@ func main() { severity: protocol.SeverityInformation, }, }, + hover: []string{"GOSTDLIB", "No fix is available", "GOSTDLIB"}, }, } @@ -522,12 +522,12 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { }, } - for mod, want := range wantVulncheckDiagnostics { - modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) + for pattern, want := range wantVulncheckDiagnostics { + modPathDiagnostics := testVulnDiagnostics(t, env, pattern, want, gotDiagnostics) gotActions := env.CodeAction("go.mod", modPathDiagnostics) if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { - t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", mod, gotActions, want.codeActions, diff) + t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", pattern, gotActions, want.codeActions, diff) continue } } @@ -827,11 +827,11 @@ func TestRunVulncheckInfo(t *testing.T) { }) } -// testVulnDiagnostics finds the require statement line for the requireMod in go.mod file +// testVulnDiagnostics finds the require or module statement line for the requireMod in go.mod file // and runs checks if diagnostics and code actions associated with the line match expectation. -func testVulnDiagnostics(t *testing.T, env *Env, requireMod string, want vulnDiagExpectation, got *protocol.PublishDiagnosticsParams) []protocol.Diagnostic { +func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagExpectation, got *protocol.PublishDiagnosticsParams) []protocol.Diagnostic { t.Helper() - pos := env.RegexpSearch("go.mod", requireMod) + pos := env.RegexpSearch("go.mod", pattern) var modPathDiagnostics []protocol.Diagnostic for _, w := range want.diagnostics { // Find the diagnostics at pos. @@ -845,7 +845,7 @@ func testVulnDiagnostics(t *testing.T, env *Env, requireMod string, want vulnDia } } if diag == nil { - t.Errorf("no diagnostic at %q matching %q found\n", requireMod, w.msg) + t.Errorf("no diagnostic at %q matching %q found\n", pattern, w.msg) continue } if diag.Severity != w.severity { @@ -867,7 +867,7 @@ func testVulnDiagnostics(t *testing.T, env *Env, requireMod string, want vulnDia hover, _ := env.Hover("go.mod", pos) for _, part := range want.hover { if !strings.Contains(hover.Value, part) { - t.Errorf("hover contents for %q do not match, want %v, got %v\n", requireMod, strings.Join(want.hover, ","), hover.Value) + t.Errorf("hover contents for %q do not match, want %v, got %v\n", pattern, strings.Join(want.hover, ","), hover.Value) break } } From 0a6aa90a519294294a361c9effe8af8dc8dafabc Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Mon, 28 Nov 2022 09:16:55 -0500 Subject: [PATCH 482/723] gopls/internal/lsp/command: rename run_vulncheck_exp to run_govulncheck The command literally runs the govulncheck command. Change-Id: I2d1316e2841778237d15d50f0d986cb89f60c680 Reviewed-on: https://go-review.googlesource.com/c/tools/+/453600 Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Hyang-Ah Hana Kim --- gopls/doc/commands.md | 40 +++++++++++------------ gopls/doc/settings.md | 4 +-- gopls/internal/lsp/command.go | 2 +- gopls/internal/lsp/command/command_gen.go | 24 +++++++------- gopls/internal/lsp/command/interface.go | 4 +-- gopls/internal/lsp/mod/code_lens.go | 4 +-- gopls/internal/lsp/mod/diagnostics.go | 2 +- gopls/internal/lsp/source/api_json.go | 20 ++++++------ gopls/internal/lsp/source/options.go | 2 +- gopls/internal/regtest/misc/vuln_test.go | 22 ++++++------- 10 files changed, 62 insertions(+), 62 deletions(-) diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 667db225ef6..86cc41e4891 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -281,26 +281,8 @@ Args: } ``` -### **Run test(s)** -Identifier: `gopls.run_tests` - -Runs `go test` for a specific set of test or benchmark functions. - -Args: - -``` -{ - // The test file containing the tests to run. - "URI": string, - // Specific test names to run, e.g. TestFoo. - "Tests": []string, - // Specific benchmarks to run, e.g. BenchmarkFoo. - "Benchmarks": []string, -} -``` - -### **Run vulncheck (experimental)** -Identifier: `gopls.run_vulncheck_exp` +### **Run govulncheck.** +Identifier: `gopls.run_govulncheck` Run vulnerability check (`govulncheck`). @@ -325,6 +307,24 @@ Result: } ``` +### **Run test(s)** +Identifier: `gopls.run_tests` + +Runs `go test` for a specific set of test or benchmark functions. + +Args: + +``` +{ + // The test file containing the tests to run. + "URI": string, + // Specific test names to run, e.g. TestFoo. + "Tests": []string, + // Specific benchmarks to run, e.g. BenchmarkFoo. + "Benchmarks": []string, +} +``` + ### **Start the gopls debug server** Identifier: `gopls.start_debugging` diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 8d32b4f4088..6816967ee21 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -542,9 +542,9 @@ Runs `go generate` for a given directory. Identifier: `regenerate_cgo` Regenerates cgo definitions. -### **Run vulncheck (experimental)** +### **Run govulncheck.** -Identifier: `run_vulncheck_exp` +Identifier: `run_govulncheck` Run vulnerability check (`govulncheck`). ### **Run test(s) (legacy)** diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 81e29bdc77e..01dc4918573 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -863,7 +863,7 @@ func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.U return ret, err } -func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) (command.RunVulncheckResult, error) { +func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.VulncheckArgs) (command.RunVulncheckResult, error) { if args.URI == "" { return command.RunVulncheckResult{}, errors.New("VulncheckArgs is missing URI field") } diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go index 7fa02001aa7..1c76fb0ec85 100644 --- a/gopls/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -34,8 +34,8 @@ const ( RegenerateCgo Command = "regenerate_cgo" RemoveDependency Command = "remove_dependency" ResetGoModDiagnostics Command = "reset_go_mod_diagnostics" + RunGovulncheck Command = "run_govulncheck" RunTests Command = "run_tests" - RunVulncheckExp Command = "run_vulncheck_exp" StartDebugging Command = "start_debugging" Test Command = "test" Tidy Command = "tidy" @@ -61,8 +61,8 @@ var Commands = []Command{ RegenerateCgo, RemoveDependency, ResetGoModDiagnostics, + RunGovulncheck, RunTests, - RunVulncheckExp, StartDebugging, Test, Tidy, @@ -164,18 +164,18 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.ResetGoModDiagnostics(ctx, a0) - case "gopls.run_tests": - var a0 RunTestsArgs + case "gopls.run_govulncheck": + var a0 VulncheckArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { return nil, err } - return nil, s.RunTests(ctx, a0) - case "gopls.run_vulncheck_exp": - var a0 VulncheckArgs + return s.RunGovulncheck(ctx, a0) + case "gopls.run_tests": + var a0 RunTestsArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { return nil, err } - return s.RunVulncheckExp(ctx, a0) + return nil, s.RunTests(ctx, a0) case "gopls.start_debugging": var a0 DebuggingArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -404,26 +404,26 @@ func NewResetGoModDiagnosticsCommand(title string, a0 URIArg) (protocol.Command, }, nil } -func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) { +func NewRunGovulncheckCommand(title string, a0 VulncheckArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { return protocol.Command{}, err } return protocol.Command{ Title: title, - Command: "gopls.run_tests", + Command: "gopls.run_govulncheck", Arguments: args, }, nil } -func NewRunVulncheckExpCommand(title string, a0 VulncheckArgs) (protocol.Command, error) { +func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { return protocol.Command{}, err } return protocol.Command{ Title: title, - Command: "gopls.run_vulncheck_exp", + Command: "gopls.run_tests", Arguments: args, }, nil } diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index 4cd19ca6b48..ebc967c8f38 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -150,10 +150,10 @@ type Interface interface { // address. StartDebugging(context.Context, DebuggingArgs) (DebuggingResult, error) - // RunVulncheckExp: Run vulncheck (experimental) + // RunGovulncheck: Run govulncheck. // // Run vulnerability check (`govulncheck`). - RunVulncheckExp(context.Context, VulncheckArgs) (RunVulncheckResult, error) + RunGovulncheck(context.Context, VulncheckArgs) (RunVulncheckResult, error) // FetchVulncheckResult: Get known vulncheck result // diff --git a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go index 01d75d92a20..e61bb04aa60 100644 --- a/gopls/internal/lsp/mod/code_lens.go +++ b/gopls/internal/lsp/mod/code_lens.go @@ -22,7 +22,7 @@ func LensFuncs() map[command.Command]source.LensFunc { command.UpgradeDependency: upgradeLenses, command.Tidy: tidyLens, command.Vendor: vendorLens, - command.RunVulncheckExp: vulncheckLenses, + command.RunGovulncheck: vulncheckLenses, } } @@ -172,7 +172,7 @@ func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.Fi return nil, err } - vulncheck, err := command.NewRunVulncheckExpCommand("Run govulncheck", command.VulncheckArgs{ + vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ URI: uri, Pattern: "./...", }) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index b48f39c3640..cce0725f047 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -193,7 +193,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return nil, nil } - vulncheck, err := command.NewRunVulncheckExpCommand("Run govulncheck", command.VulncheckArgs{ + vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ URI: protocol.DocumentURI(fh.URI()), Pattern: "./...", }) diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 91b676a5426..c8a6e19a340 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -600,7 +600,7 @@ var GeneratedAPIJSON = &APIJSON{ Default: "true", }, { - Name: "\"run_vulncheck_exp\"", + Name: "\"run_govulncheck\"", Doc: "Run vulnerability check (`govulncheck`).", Default: "false", }, @@ -770,19 +770,19 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Reset diagnostics in the go.mod file of a module.", ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", }, + { + Command: "gopls.run_govulncheck", + Title: "Run govulncheck.", + Doc: "Run vulnerability check (`govulncheck`).", + ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", + ResultDoc: "{\n\t// Token holds the progress token for LSP workDone reporting of the vulncheck\n\t// invocation.\n\t\"Token\": interface{},\n}", + }, { Command: "gopls.run_tests", Title: "Run test(s)", Doc: "Runs `go test` for a specific set of test or benchmark functions.", ArgDoc: "{\n\t// The test file containing the tests to run.\n\t\"URI\": string,\n\t// Specific test names to run, e.g. TestFoo.\n\t\"Tests\": []string,\n\t// Specific benchmarks to run, e.g. BenchmarkFoo.\n\t\"Benchmarks\": []string,\n}", }, - { - Command: "gopls.run_vulncheck_exp", - Title: "Run vulncheck (experimental)", - Doc: "Run vulnerability check (`govulncheck`).", - ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", - ResultDoc: "{\n\t// Token holds the progress token for LSP workDone reporting of the vulncheck\n\t// invocation.\n\t\"Token\": interface{},\n}", - }, { Command: "gopls.start_debugging", Title: "Start the gopls debug server", @@ -844,8 +844,8 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Regenerates cgo definitions.", }, { - Lens: "run_vulncheck_exp", - Title: "Run vulncheck (experimental)", + Lens: "run_govulncheck", + Title: "Run govulncheck.", Doc: "Run vulnerability check (`govulncheck`).", }, { diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index d6458cc4fd0..cad7f28654a 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -156,7 +156,7 @@ func DefaultOptions() *Options { string(command.GCDetails): false, string(command.UpgradeDependency): true, string(command.Vendor): true, - // TODO(hyangah): enable command.RunVulncheckExp. + // TODO(hyangah): enable command.RunGovulncheck. }, }, }, diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 9a7d2ff860b..aac107fbecf 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -26,7 +26,7 @@ import ( "golang.org/x/tools/internal/testenv" ) -func TestRunVulncheckExpError(t *testing.T) { +func TestRunGovulncheckError(t *testing.T) { const files = ` -- go.mod -- module mod.com @@ -36,7 +36,7 @@ go 1.12 package foo ` Run(t, files, func(t *testing.T, env *Env) { - cmd, err := command.NewRunVulncheckExpCommand("Run Vulncheck Exp", command.VulncheckArgs{ + cmd, err := command.NewRunGovulncheckCommand("Run Vulncheck Exp", command.VulncheckArgs{ URI: "/invalid/file/url", // invalid arg }) if err != nil { @@ -44,7 +44,7 @@ package foo } params := &protocol.ExecuteCommandParams{ - Command: command.RunVulncheckExp.ID(), + Command: command.RunGovulncheck.ID(), Arguments: cmd.Arguments, } @@ -123,7 +123,7 @@ references: - href: pkg.go.dev/vuln/GOSTDLIB ` -func TestRunVulncheckExpStd(t *testing.T) { +func TestRunGovulncheckStd(t *testing.T) { testenv.NeedsGo1Point(t, 18) const files = ` -- go.mod -- @@ -161,7 +161,7 @@ func main() { }, Settings{ "codelenses": map[string]bool{ - "run_vulncheck_exp": true, + "run_govulncheck": true, }, }, ).Run(t, files, func(t *testing.T, env *Env) { @@ -170,7 +170,7 @@ func main() { // Test CodeLens is present. lenses := env.CodeLens("go.mod") - const wantCommand = "gopls." + string(command.RunVulncheckExp) + const wantCommand = "gopls." + string(command.RunGovulncheck) var gotCodelens = false var lens protocol.CodeLens for _, l := range lenses { @@ -444,7 +444,7 @@ func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) { } settings := Settings{ "codelenses": map[string]bool{ - "run_vulncheck_exp": true, + "run_govulncheck": true, }, } ev := EnvVars{ @@ -571,7 +571,7 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { if tc.name == "imports" && tc.wantDiagnostics { // test we get only govulncheck-based diagnostics after "run govulncheck". var result command.RunVulncheckResult - env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp, &result) + env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( @@ -621,7 +621,7 @@ func TestRunVulncheckWarning(t *testing.T) { env.OpenFile("go.mod") var result command.RunVulncheckResult - env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp, &result) + env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( @@ -771,7 +771,7 @@ func Vuln() {} // vulnerable. func OK() {} // ok. ` -func TestRunVulncheckInfo(t *testing.T) { +func TestGovulncheckInfo(t *testing.T) { testenv.NeedsGo1Point(t, 18) db, opts, err := vulnTestEnv(vulnsData, proxy2) @@ -782,7 +782,7 @@ func TestRunVulncheckInfo(t *testing.T) { WithOptions(opts...).Run(t, workspace2, func(t *testing.T, env *Env) { env.OpenFile("go.mod") var result command.RunVulncheckResult - env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp, &result) + env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( From 88ee30bcc8bdcd946a243c3c4f2ba99649e8a1d1 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 29 Nov 2022 17:04:19 -0500 Subject: [PATCH 483/723] gopls/internal/lsp/source: enable run_govulncheck codelens in exp mode This enables "Run govulncheck" codelens for Nightly. Change-Id: I2620d6ed2d5e29e894c92b1e0b6c18c0dedd5f45 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454115 Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot --- gopls/internal/lsp/source/options.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index cad7f28654a..fb596a82a27 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -858,6 +858,9 @@ func (o *Options) enableAllExperimentMaps() { if _, ok := o.Codelenses[string(command.GCDetails)]; !ok { o.Codelenses[string(command.GCDetails)] = true } + 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 } From fe83ddb67c9cd0501b72d59d9849fd300248c141 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 28 Nov 2022 16:52:57 -0500 Subject: [PATCH 484/723] gopls/internal/lsp: move options off of the cache Invert the control for constructing an LSP session: there is no reason for the cache to know about source.Options, or how to create a session. Change-Id: Id96152641e6db751acecac9c66ea8fc2535939db Reviewed-on: https://go-review.googlesource.com/c/tools/+/453837 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/cache.go | 28 ++++++++------------- gopls/internal/lsp/cache/session.go | 9 ++++--- gopls/internal/lsp/cmd/capabilities_test.go | 2 +- gopls/internal/lsp/cmd/cmd.go | 2 +- gopls/internal/lsp/cmd/serve.go | 6 ++--- gopls/internal/lsp/cmd/test/cmdtest.go | 4 +-- gopls/internal/lsp/lsp_test.go | 3 +-- gopls/internal/lsp/lsprpc/lsprpc.go | 18 +++++++------ gopls/internal/lsp/lsprpc/lsprpc_test.go | 8 +++--- gopls/internal/lsp/mod/mod_test.go | 3 +-- gopls/internal/lsp/regtest/runner.go | 6 ++--- gopls/internal/lsp/source/source_test.go | 3 +-- gopls/internal/regtest/bench/bench_test.go | 2 +- gopls/internal/regtest/bench/stress_test.go | 2 +- gopls/internal/vulncheck/command_test.go | 3 +-- 15 files changed, 46 insertions(+), 53 deletions(-) diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go index 8c06b6d5ccf..46b2e26a331 100644 --- a/gopls/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -31,11 +31,10 @@ import ( // New Creates a new cache for gopls operation results, using the given file // set, shared store, and session options. // -// All of the fset, store and options may be nil, but if store is non-nil so -// must be fset (and they must always be used together), otherwise it may be -// possible to get cached data referencing token.Pos values not mapped by the -// FileSet. -func New(fset *token.FileSet, store *memoize.Store, options func(*source.Options)) *Cache { +// Both the fset and store may be nil, but if store is non-nil so must be fset +// (and they must always be used together), otherwise it may be possible to get +// cached data referencing token.Pos values not mapped by the FileSet. +func New(fset *token.FileSet, store *memoize.Store) *Cache { index := atomic.AddInt64(&cacheIndex, 1) if store != nil && fset == nil { @@ -51,7 +50,6 @@ func New(fset *token.FileSet, store *memoize.Store, options func(*source.Options c := &Cache{ id: strconv.FormatInt(index, 10), fset: fset, - options: options, store: store, fileContent: map[span.URI]*fileHandle{}, } @@ -62,11 +60,6 @@ type Cache struct { id string fset *token.FileSet - // TODO(rfindley): it doesn't make sense that cache accepts LSP options, just - // so that it can create a session: the cache does not (and should not) - // depend on options. Invert this relationship to remove options from Cache. - options func(*source.Options) - store *memoize.Store fileMu sync.Mutex @@ -166,11 +159,14 @@ func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, e }, nil } -func (c *Cache) NewSession(ctx context.Context) *Session { +// NewSession creates a new gopls session with the given cache and options overrides. +// +// The provided optionsOverrides may be nil. +func NewSession(ctx context.Context, c *Cache, optionsOverrides func(*source.Options)) *Session { index := atomic.AddInt64(&sessionIndex, 1) options := source.DefaultOptions().Clone() - if c.options != nil { - c.options(options) + if optionsOverrides != nil { + optionsOverrides(options) } s := &Session{ id: strconv.FormatInt(index, 10), @@ -183,10 +179,6 @@ func (c *Cache) NewSession(ctx context.Context) *Session { return s } -func (c *Cache) FileSet() *token.FileSet { - return c.fset -} - func (h *fileHandle) URI() span.URI { return h.uri } diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 5eca7c832dc..514411177d3 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -29,8 +29,9 @@ type Session struct { id string // Immutable attributes shared across views. - cache *Cache // shared cache - gocmdRunner *gocommand.Runner // limits go command concurrency + cache *Cache // shared cache + gocmdRunner *gocommand.Runner // limits go command concurrency + optionsOverrides func(*source.Options) // transformation to apply on top of all options optionsMu sync.Mutex options *source.Options @@ -183,8 +184,8 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, seqID uint64) (*View, *snapshot, func(), error) { index := atomic.AddInt64(&viewIndex, 1) - if s.cache.options != nil { - s.cache.options(options) + if s.optionsOverrides != nil { + s.optionsOverrides(options) } // Get immutable workspace configuration. diff --git a/gopls/internal/lsp/cmd/capabilities_test.go b/gopls/internal/lsp/cmd/capabilities_test.go index bd209639420..4b38db751a4 100644 --- a/gopls/internal/lsp/cmd/capabilities_test.go +++ b/gopls/internal/lsp/cmd/capabilities_test.go @@ -43,7 +43,7 @@ func TestCapabilities(t *testing.T) { params.Capabilities.Workspace.Configuration = true // Send an initialize request to the server. - c.Server = lsp.NewServer(cache.New(nil, nil, app.options).NewSession(ctx), c.Client) + c.Server = lsp.NewServer(cache.NewSession(ctx, cache.New(nil, nil), app.options), c.Client) result, err := c.Server.Initialize(ctx, params) if err != nil { t.Fatal(err) diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index 5c64e108668..3aa74d067ae 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -286,7 +286,7 @@ func (app *Application) connect(ctx context.Context) (*connection, error) { switch { case app.Remote == "": connection := newConnection(app) - connection.Server = lsp.NewServer(cache.New(nil, nil, app.options).NewSession(ctx), connection.Client) + connection.Server = lsp.NewServer(cache.NewSession(ctx, cache.New(nil, nil), app.options), connection.Client) ctx = protocol.WithClient(ctx, connection.Client) return connection, connection.initialize(ctx, app.options) case strings.HasPrefix(app.Remote, "internal@"): diff --git a/gopls/internal/lsp/cmd/serve.go b/gopls/internal/lsp/cmd/serve.go index 8a4de5eac6c..44d4b1d1d6b 100644 --- a/gopls/internal/lsp/cmd/serve.go +++ b/gopls/internal/lsp/cmd/serve.go @@ -14,12 +14,12 @@ import ( "os" "time" - "golang.org/x/tools/internal/fakenet" - "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/debug" "golang.org/x/tools/gopls/internal/lsp/lsprpc" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/fakenet" + "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/tool" ) @@ -101,7 +101,7 @@ func (s *Serve) Run(ctx context.Context, args ...string) error { return fmt.Errorf("creating forwarder: %w", err) } } else { - ss = lsprpc.NewStreamServer(cache.New(nil, nil, s.app.options), isDaemon) + ss = lsprpc.NewStreamServer(cache.New(nil, nil), isDaemon, s.app.options) } var network, addr string diff --git a/gopls/internal/lsp/cmd/test/cmdtest.go b/gopls/internal/lsp/cmd/test/cmdtest.go index 167631a3cab..fed47497576 100644 --- a/gopls/internal/lsp/cmd/test/cmdtest.go +++ b/gopls/internal/lsp/cmd/test/cmdtest.go @@ -51,8 +51,8 @@ func TestCommandLine(t *testing.T, testdata string, options func(*source.Options func NewTestServer(ctx context.Context, options func(*source.Options)) *servertest.TCPServer { ctx = debug.WithInstance(ctx, "", "") - cache := cache.New(nil, nil, options) - ss := lsprpc.NewStreamServer(cache, false) + cache := cache.New(nil, nil) + ss := lsprpc.NewStreamServer(cache, false, options) return servertest.NewTCPServer(ctx, ss, nil) } diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 5a29cd8b64b..e9da7e3ee27 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -59,8 +59,7 @@ type runner struct { func testLSP(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) - cache := cache.New(nil, nil, nil) - session := cache.NewSession(ctx) + session := cache.NewSession(ctx, cache.New(nil, nil), nil) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) session.SetOptions(options) diff --git a/gopls/internal/lsp/lsprpc/lsprpc.go b/gopls/internal/lsp/lsprpc/lsprpc.go index f0fe53dcf59..d43e68ec0d2 100644 --- a/gopls/internal/lsp/lsprpc/lsprpc.go +++ b/gopls/internal/lsp/lsprpc/lsprpc.go @@ -19,14 +19,15 @@ import ( "sync/atomic" "time" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/gopls/internal/lsp" "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/debug" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/jsonrpc2" ) // Unique identifiers for client/server. @@ -39,6 +40,9 @@ type StreamServer struct { // daemon controls whether or not to log new connections. daemon bool + // optionsOverrides is passed to newly created sessions. + optionsOverrides func(*source.Options) + // serverForTest may be set to a test fake for testing. serverForTest protocol.Server } @@ -46,13 +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) *StreamServer { - return &StreamServer{cache: cache, daemon: daemon} +func NewStreamServer(cache *cache.Cache, daemon bool, optionsFunc func(*source.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 := s.cache.NewSession(ctx) + session := cache.NewSession(ctx, s.cache, s.optionsOverrides) server := s.serverForTest if server == nil { server = lsp.NewServer(session, client) @@ -69,7 +73,7 @@ func (s *StreamServer) Binder() *ServerBinder { // incoming streams using a new lsp server. func (s *StreamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error { client := protocol.ClientDispatcher(conn) - session := s.cache.NewSession(ctx) + session := cache.NewSession(ctx, s.cache, s.optionsOverrides) server := s.serverForTest if server == nil { server = lsp.NewServer(session, client) diff --git a/gopls/internal/lsp/lsprpc/lsprpc_test.go b/gopls/internal/lsp/lsprpc/lsprpc_test.go index 1bf20b521cb..5fa1340948d 100644 --- a/gopls/internal/lsp/lsprpc/lsprpc_test.go +++ b/gopls/internal/lsp/lsprpc/lsprpc_test.go @@ -58,7 +58,7 @@ func TestClientLogging(t *testing.T) { client := FakeClient{Logs: make(chan string, 10)} ctx = debug.WithInstance(ctx, "", "") - ss := NewStreamServer(cache.New(nil, nil, nil), false) + ss := NewStreamServer(cache.New(nil, nil), false, nil) ss.serverForTest = server ts := servertest.NewPipeServer(ss, nil) defer checkClose(t, ts.Close) @@ -121,7 +121,7 @@ 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, nil, nil), false) + ss := NewStreamServer(cache.New(nil, nil), false, nil) ss.serverForTest = s tsDirect := servertest.NewTCPServer(serveCtx, ss, nil) @@ -216,8 +216,8 @@ func TestDebugInfoLifecycle(t *testing.T) { clientCtx := debug.WithInstance(baseCtx, "", "") serverCtx := debug.WithInstance(baseCtx, "", "") - cache := cache.New(nil, nil, nil) - ss := NewStreamServer(cache, false) + cache := cache.New(nil, nil) + ss := NewStreamServer(cache, false, nil) tsBackend := servertest.NewTCPServer(serverCtx, ss, nil) forwarder, err := NewForwarder("tcp;"+tsBackend.Addr, nil) diff --git a/gopls/internal/lsp/mod/mod_test.go b/gopls/internal/lsp/mod/mod_test.go index 767ec44a7d8..55cbc11ee7c 100644 --- a/gopls/internal/lsp/mod/mod_test.go +++ b/gopls/internal/lsp/mod/mod_test.go @@ -26,8 +26,7 @@ func TestModfileRemainsUnchanged(t *testing.T) { testenv.NeedsGo1Point(t, 14) ctx := tests.Context(t) - cache := cache.New(nil, nil, nil) - session := cache.NewSession(ctx) + session := cache.NewSession(ctx, cache.New(nil, nil), nil) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) options.TempModfile = true diff --git a/gopls/internal/lsp/regtest/runner.go b/gopls/internal/lsp/regtest/runner.go index 02be6fa40ee..a006164a076 100644 --- a/gopls/internal/lsp/regtest/runner.go +++ b/gopls/internal/lsp/regtest/runner.go @@ -336,7 +336,7 @@ func (s *loggingFramer) printBuffers(testname string, w io.Writer) { // defaultServer handles the Default execution mode. func (r *Runner) defaultServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { - return lsprpc.NewStreamServer(cache.New(r.fset, r.store, optsHook), false) + return lsprpc.NewStreamServer(cache.New(r.fset, r.store), false, optsHook) } // experimentalServer handles the Experimental execution mode. @@ -348,7 +348,7 @@ func (r *Runner) experimentalServer(optsHook func(*source.Options)) jsonrpc2.Str // source.Options.EnableAllExperiments, but we want to test it. o.ExperimentalWorkspaceModule = true } - return lsprpc.NewStreamServer(cache.New(nil, nil, options), false) + return lsprpc.NewStreamServer(cache.New(nil, nil), false, options) } // forwardedServer handles the Forwarded execution mode. @@ -356,7 +356,7 @@ func (r *Runner) forwardedServer(optsHook func(*source.Options)) jsonrpc2.Stream r.tsOnce.Do(func() { ctx := context.Background() ctx = debug.WithInstance(ctx, "", "off") - ss := lsprpc.NewStreamServer(cache.New(nil, nil, optsHook), false) + ss := lsprpc.NewStreamServer(cache.New(nil, nil), false, optsHook) r.ts = servertest.NewTCPServer(ctx, ss, nil) }) return newForwarder("tcp", r.ts.Addr) diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index e5405301952..3ca62c341e0 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -49,8 +49,7 @@ type runner struct { func testSource(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) - cache := cache.New(nil, nil, nil) - session := cache.NewSession(ctx) + session := cache.NewSession(ctx, cache.New(nil, nil), nil) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) options.SetEnvSlice(datum.Config.Env) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index 98d783c67c9..2048e9667b6 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -212,7 +212,7 @@ func getServer() servertest.Connector { path := getInstalledGopls() return &SidecarServer{path} } - server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false) + server := lsprpc.NewStreamServer(cache.New(nil, nil), false, hooks.Options) return servertest.NewPipeServer(server, jsonrpc2.NewRawStream) } diff --git a/gopls/internal/regtest/bench/stress_test.go b/gopls/internal/regtest/bench/stress_test.go index b1198b43338..11c511f1780 100644 --- a/gopls/internal/regtest/bench/stress_test.go +++ b/gopls/internal/regtest/bench/stress_test.go @@ -45,7 +45,7 @@ func TestPilosaStress(t *testing.T) { t.Fatal(err) } - server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false) + server := lsprpc.NewStreamServer(cache.New(nil, nil), false, hooks.Options) ts := servertest.NewPipeServer(server, jsonrpc2.NewRawStream) ctx := context.Background() diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index 4032693f9f4..4905fcd383d 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -298,8 +298,7 @@ func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Co t.Fatal(err) } - cache := cache.New(nil, nil, nil) - session := cache.NewSession(ctx) + session := cache.NewSession(ctx, cache.New(nil, nil), nil) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) session.SetOptions(options) From 81b6bef4d10196e4ad701b21a814b489d7e2b7cc Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 29 Nov 2022 15:03:04 -0500 Subject: [PATCH 485/723] gopls/internal/lsp: add run vulncheck fix for stdlib vulns Add the `Run Vulncheck` code action for vulns found in the standard library. Change-Id: I8b3a7f969e5fab6fd081a926f6f69bac50978fce Reviewed-on: https://go-review.googlesource.com/c/tools/+/453958 TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim Run-TryBot: Suzy Mueller gopls-CI: kokoro --- gopls/internal/lsp/mod/diagnostics.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index cce0725f047..47065035384 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -361,15 +361,21 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, }) } if len(info) > 0 { + var fixes []source.SuggestedFix + if !fromGovulncheck { + fixes = append(fixes, suggestVulncheck) + } vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityInformation, - Source: source.Vulncheck, - Message: getVulnMessage(stdlib, info, false, fromGovulncheck), - Related: relatedInfo, + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityInformation, + Source: source.Vulncheck, + Message: getVulnMessage(stdlib, info, false, fromGovulncheck), + SuggestedFixes: fixes, + Related: relatedInfo, }) } + return vulnDiagnostics, nil } From 9b8d87b59383c4c62811fe42912e2c71c71691be Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 30 Nov 2022 12:37:01 -0500 Subject: [PATCH 486/723] gopls/internal/regtest: fix TestRunGovulncheckStd The stdlib vulnerable package diagnostic should expect a `Run govulncheck` code action. Change-Id: If567c1b9912531e8601cb51b89bf5597fa565060 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454117 gopls-CI: kokoro Auto-Submit: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim Run-TryBot: Suzy Mueller Auto-Submit: Robert Findley --- gopls/internal/regtest/misc/vuln_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index aac107fbecf..daeb73656d3 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -256,11 +256,13 @@ func main() { "module mod.com": { diagnostics: []vulnDiag{ { - msg: "go1.18 has a vulnerability GOSTDLIB.", - severity: protocol.SeverityInformation, + msg: "go1.18 has a vulnerability GOSTDLIB.", + severity: protocol.SeverityInformation, + codeActions: []string{"Run govulncheck"}, }, }, - hover: []string{"GOSTDLIB", "No fix is available", "GOSTDLIB"}, + hover: []string{"GOSTDLIB", "No fix is available", "GOSTDLIB"}, + codeActions: []string{"Run govulncheck"}, }, } From 1a0053c954a62b7533dd0e2b5887a4a7274741d3 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 29 Nov 2022 14:47:05 -0500 Subject: [PATCH 487/723] gopls: move reset go.mod diagnostic codelens to module statement If there is no require statement, then there is no require statement to attach the codelens to. Move the codelens to the module statement instead. Change-Id: I8f77f2cbf73d410ffdebb25f7c426379d06ad400 Reviewed-on: https://go-review.googlesource.com/c/tools/+/453957 TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim gopls-CI: kokoro Run-TryBot: Suzy Mueller --- gopls/internal/lsp/mod/code_lens.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go index e61bb04aa60..4e08f4a2c7c 100644 --- a/gopls/internal/lsp/mod/code_lens.go +++ b/gopls/internal/lsp/mod/code_lens.go @@ -31,15 +31,25 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File if err != nil || pm.File == nil { return nil, err } + uri := protocol.URIFromSpanURI(fh.URI()) + reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.URIArg{URI: uri}) + if err != nil { + return nil, err + } + // Put the `Reset go.mod diagnostics` codelens on the module statement. + modrng, err := moduleStmtRange(fh, pm) + if err != nil { + return nil, err + } + lenses := []protocol.CodeLens{{Range: modrng, Command: reset}} if len(pm.File.Require) == 0 { // Nothing to upgrade. - return nil, nil + return lenses, nil } var requires []string for _, req := range pm.File.Require { requires = append(requires, req.Mod.Path) } - uri := protocol.URIFromSpanURI(fh.URI()) checkUpgrade, err := command.NewCheckUpgradesCommand("Check for upgrades", command.CheckUpgradesArgs{ URI: uri, Modules: requires, @@ -63,22 +73,18 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File if err != nil { return nil, err } - reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.URIArg{URI: uri}) - if err != nil { - return nil, err - } + // Put the upgrade code lenses above the first require block or statement. rng, err := firstRequireRange(fh, pm) if err != nil { return nil, err } - return []protocol.CodeLens{ + return append(lenses, []protocol.CodeLens{ {Range: rng, Command: checkUpgrade}, {Range: rng, Command: upgradeTransitive}, {Range: rng, Command: upgradeDirect}, - {Range: rng, Command: reset}, - }, nil + }...), nil } func tidyLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { From 78c1861cf9bb2754482beac3ddf98042609cfb12 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 30 Nov 2022 14:15:58 -0500 Subject: [PATCH 488/723] gopls/internal/vulncheck: clarify the log message Some vulnerabilities are informational, so "Found X vulnerabilities" message can be misleading. Print affecting/unaffecting vulns counts instead. Change-Id: I5f6508beac7ee13313defe727a8c8089dbde24b8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454119 Reviewed-by: Suzy Mueller gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot --- gopls/internal/vulncheck/command.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 3006d619241..b1bdfba1a9d 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -250,7 +250,13 @@ func init() { if err != nil { return err } - logf("Found %d vulnerabilities", len(res.Vulns)) + affecting := 0 + for _, v := range res.Vulns { + if v.IsCalled() { + affecting++ + } + } + logf("Found %d affecting vulns and %d unaffecting vulns in imported packages", affecting, len(res.Vulns)-affecting) if err := json.NewEncoder(os.Stdout).Encode(res); err != nil { return err } From c5ce806930b9cd8c2d097ecbf6005fcc36ec477b Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 1 Dec 2022 12:43:15 -0500 Subject: [PATCH 489/723] gopls/internal/lsp/mod: disable the diagnostics on stdlib vulns Updates golang/vscode-go#2551 Change-Id: I4ae7c696202fd3b073aef81b0e18e3e501c1b0fc Reviewed-on: https://go-review.googlesource.com/c/tools/+/454436 Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Suzy Mueller --- gopls/internal/lsp/mod/diagnostics.go | 97 +++++++++++++----------- gopls/internal/regtest/misc/vuln_test.go | 32 +------- 2 files changed, 55 insertions(+), 74 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 47065035384..7ee92bdc332 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -325,55 +325,60 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, } } - // Add standard library vulnerabilities. - stdlibVulns := vulnsByModule["stdlib"] - if len(stdlibVulns) == 0 { - return vulnDiagnostics, nil - } - - // Put the standard library diagnostic on the module declaration. - rng, err := pm.Mapper.OffsetRange(pm.File.Module.Syntax.Start.Byte, pm.File.Module.Syntax.End.Byte) - if err != nil { - return vulnDiagnostics, nil // TODO: bug report - } + // TODO(hyangah): place this diagnostic on the `go` directive or `toolchain` directive + // after https://go.dev/issue/57001. + const diagnoseStdLib = false + if diagnoseStdLib { + // Add standard library vulnerabilities. + stdlibVulns := vulnsByModule["stdlib"] + if len(stdlibVulns) == 0 { + return vulnDiagnostics, nil + } + + // Put the standard library diagnostic on the module declaration. + rng, err := pm.Mapper.OffsetRange(pm.File.Module.Syntax.Start.Byte, pm.File.Module.Syntax.End.Byte) + if err != nil { + return vulnDiagnostics, nil // TODO: bug report + } - stdlib := stdlibVulns[0].mod.FoundVersion - var warning, info []string - var relatedInfo []source.RelatedInformation - for _, mv := range stdlibVulns { - vuln := mv.vuln - stdlib = mv.mod.FoundVersion - if !vuln.IsCalled() { - info = append(info, vuln.OSV.ID) - } else { - warning = append(warning, vuln.OSV.ID) - relatedInfo = append(relatedInfo, listRelatedInfo(ctx, snapshot, vuln)...) + stdlib := stdlibVulns[0].mod.FoundVersion + var warning, info []string + var relatedInfo []source.RelatedInformation + for _, mv := range stdlibVulns { + vuln := mv.vuln + stdlib = mv.mod.FoundVersion + if !vuln.IsCalled() { + info = append(info, vuln.OSV.ID) + } else { + warning = append(warning, vuln.OSV.ID) + relatedInfo = append(relatedInfo, listRelatedInfo(ctx, snapshot, vuln)...) + } } - } - if len(warning) > 0 { - vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityWarning, - Source: source.Vulncheck, - Message: getVulnMessage(stdlib, warning, true, fromGovulncheck), - Related: relatedInfo, - }) - } - if len(info) > 0 { - var fixes []source.SuggestedFix - if !fromGovulncheck { - fixes = append(fixes, suggestVulncheck) + if len(warning) > 0 { + vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityWarning, + Source: source.Vulncheck, + Message: getVulnMessage(stdlib, warning, true, fromGovulncheck), + Related: relatedInfo, + }) + } + if len(info) > 0 { + var fixes []source.SuggestedFix + if !fromGovulncheck { + fixes = append(fixes, suggestVulncheck) + } + vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityInformation, + Source: source.Vulncheck, + Message: getVulnMessage(stdlib, info, false, fromGovulncheck), + SuggestedFixes: fixes, + Related: relatedInfo, + }) } - vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityInformation, - Source: source.Vulncheck, - Message: getVulnMessage(stdlib, info, false, fromGovulncheck), - SuggestedFixes: fixes, - Related: relatedInfo, - }) } return vulnDiagnostics, nil diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index daeb73656d3..377e9a2df02 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -194,7 +194,7 @@ func main() { OnceMet( CompletedProgress(result.Token), ShownMessage("Found GOSTDLIB"), - env.DiagnosticAtRegexpWithMessage("go.mod", `module`, `go1.18 has a vulnerability used in the code: GOSTDLIB.`), + EmptyOrNoDiagnostics("go.mod"), ), ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ @@ -202,7 +202,7 @@ func main() { }) } -func TestRunVulncheckDiagnosticsStd(t *testing.T) { +func TestFetchVulncheckResultStd(t *testing.T) { testenv.NeedsGo1Point(t, 18) const files = ` -- go.mod -- @@ -240,10 +240,9 @@ func main() { Settings{"ui.diagnostic.vulncheck": "Imports"}, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.AfterChange( - env.DiagnosticAtRegexp("go.mod", `module mod.com`), - ReadDiagnostics("go.mod", gotDiagnostics), + EmptyOrNoDiagnostics("go.mod"), + // we don't publish diagnostics for standard library vulnerability yet. ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ "go.mod": { @@ -251,29 +250,6 @@ func main() { Mode: govulncheck.ModeImports, }, }) - - wantVulncheckDiagnostics := map[string]vulnDiagExpectation{ - "module mod.com": { - diagnostics: []vulnDiag{ - { - msg: "go1.18 has a vulnerability GOSTDLIB.", - severity: protocol.SeverityInformation, - codeActions: []string{"Run govulncheck"}, - }, - }, - hover: []string{"GOSTDLIB", "No fix is available", "GOSTDLIB"}, - codeActions: []string{"Run govulncheck"}, - }, - } - - for pattern, want := range wantVulncheckDiagnostics { - modPathDiagnostics := testVulnDiagnostics(t, env, pattern, want, gotDiagnostics) - gotActions := env.CodeAction("go.mod", modPathDiagnostics) - if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { - t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", pattern, gotActions, want.codeActions, diff) - continue - } - } }) } From e79e423fe340678b9798731d055761f080169d94 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 1 Dec 2022 14:23:03 -0500 Subject: [PATCH 490/723] gopls/internal/vulncheck: handle package errors x/tools/go/packages.Load can succeed even when the packages have errors. vulncheck analysis requires clean build for the analyzed code. So, check returned packages and stop if any of them has errors. Change-Id: I25a272631aa80f0c22abb00cf52776db2640cb80 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454437 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot --- gopls/internal/regtest/misc/vuln_test.go | 34 ++++++++++++++++++++++++ gopls/internal/vulncheck/command.go | 7 ++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 377e9a2df02..b043e0cc77c 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -56,6 +56,40 @@ package foo }) } +func TestRunGovulncheckError2(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- foo.go -- +package foo + +func F() { // build error incomplete +` + WithOptions( + EnvVars{ + "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. + }, + Settings{ + "codelenses": map[string]bool{ + "run_govulncheck": true, + }, + }, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("go.mod") + var result command.RunVulncheckResult + env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) + env.Await( + OnceMet( + CompletedProgress(result.Token), + // TODO(hyangah): find a way to inspect $/progress 'report' message. + LogMatching(protocol.Info, "failed to load packages due to errors", 1, false), + ), + ) + }) +} + const vulnsData = ` -- GO-2022-01.yaml -- modules: diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index b1bdfba1a9d..54c9fe5c9e3 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -10,6 +10,7 @@ package vulncheck import ( "context" "encoding/json" + "errors" "fmt" "log" "os" @@ -91,7 +92,6 @@ func (c *Cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) logger.Printf("%v", err) return nil, fmt.Errorf("package load failed") } - logger.Printf("analyzing %d packages...\n", len(loadedPkgs)) r, err := vulncheck.Source(ctx, loadedPkgs, &vulncheck.Config{Client: c.Client, SourceGoVersion: goVersion()}) @@ -236,6 +236,11 @@ func init() { logf("Failed to load packages: %v", err) return err } + if n := packages.PrintErrors(pkgs); n > 0 { + err := errors.New("failed to load packages due to errors") + logf("%v", err) + return err + } logf("Loaded %d packages and their dependencies", len(pkgs)) cli, err := client.NewClient(findGOVULNDB(cfg.Env), client.Options{ HTTPCache: govulncheck.DefaultCache(), From 0379b73aefe056f088ad2d8727c4caff859503d4 Mon Sep 17 00:00:00 2001 From: Michael Matloob Date: Wed, 30 Nov 2022 13:11:15 -0500 Subject: [PATCH 491/723] internal/gcimporter: fix TestImportStdLib The test attempted to find all stdlib packages by scanning pkg/$GOOS_$GOARCH for .a files and then tried to import all of them. Now that .a files are no longer being placed there, the test is a noop. Fix this by using go list std (and filtering out testonly packages) and trying to import all of those to recreate what the test intended to do. This also removes a dependency on the pkg/$GOOS_$GOARCH directory which will stop being produced by dist in CL 453496. For golang/go#47257 Change-Id: Idfa0cbb21093776183ce193eb5363a9727bf77ef Reviewed-on: https://go-review.googlesource.com/c/tools/+/454118 Run-TryBot: Michael Matloob Reviewed-by: Michael Matloob TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Bryan Mills --- internal/gcimporter/gcimporter_test.go | 61 +++++++++++--------------- internal/testenv/testenv.go | 45 +++++++++++++++++++ 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index 56f39182786..d04668e6312 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -20,6 +20,7 @@ import ( "io/ioutil" "os" "os/exec" + "path" "path/filepath" "runtime" "strings" @@ -88,37 +89,6 @@ func testPath(t *testing.T, path, srcDir string) *types.Package { return pkg } -const maxTime = 30 * time.Second - -func testDir(t *testing.T, dir string, endTime time.Time) (nimports int) { - dirname := filepath.Join(runtime.GOROOT(), "pkg", runtime.GOOS+"_"+runtime.GOARCH, dir) - list, err := ioutil.ReadDir(dirname) - if err != nil { - t.Fatalf("testDir(%s): %s", dirname, err) - } - for _, f := range list { - if time.Now().After(endTime) { - t.Log("testing time used up") - return - } - switch { - case !f.IsDir(): - // try extensions - for _, ext := range pkgExts { - if strings.HasSuffix(f.Name(), ext) { - name := f.Name()[0 : len(f.Name())-len(ext)] // remove extension - if testPath(t, filepath.Join(dir, name), dir) != nil { - nimports++ - } - } - } - case f.IsDir(): - nimports += testDir(t, filepath.Join(dir, f.Name()), endTime) - } - } - return -} - func mktmpdir(t *testing.T) string { tmpdir, err := ioutil.TempDir("", "gcimporter_test") if err != nil { @@ -371,15 +341,36 @@ func TestVersionHandling(t *testing.T) { } func TestImportStdLib(t *testing.T) { + if testing.Short() { + t.Skip("the imports can be expensive, and this test is especially slow when the build cache is empty") + } // This package only handles gc export data. needsCompiler(t, "gc") testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache - dt := maxTime - if testing.Short() && os.Getenv("GO_BUILDER_NAME") == "" { - dt = 10 * time.Millisecond + // Get list of packages in stdlib. Filter out test-only packages with {{if .GoFiles}} check. + var stderr bytes.Buffer + cmd := exec.Command("go", "list", "-f", "{{if .GoFiles}}{{.ImportPath}}{{end}}", "std") + cmd.Stderr = &stderr + out, err := cmd.Output() + if err != nil { + t.Fatalf("failed to run go list to determine stdlib packages: %v\nstderr:\n%v", err, stderr.String()) } - nimports := testDir(t, "", time.Now().Add(dt)) // installed packages + pkgs := strings.Fields(string(out)) + + var nimports int + for _, pkg := range pkgs { + t.Run(pkg, func(t *testing.T) { + if testPath(t, pkg, filepath.Join(testenv.GOROOT(t), "src", path.Dir(pkg))) != nil { + nimports++ + } + }) + } + const minPkgs = 225 // 'GOOS=plan9 go1.18 list std | wc -l' reports 228; most other platforms have more. + if len(pkgs) < minPkgs { + t.Fatalf("too few packages (%d) were imported", nimports) + } + t.Logf("tested %d imports", nimports) } diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index ff38498e568..8184db0ba40 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -345,3 +345,48 @@ func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[str t.Fatalf("writing the importcfg failed: %s", err) } } + +var ( + gorootOnce sync.Once + gorootPath string + gorootErr error +) + +func findGOROOT() (string, error) { + gorootOnce.Do(func() { + gorootPath = runtime.GOROOT() + if gorootPath != "" { + // If runtime.GOROOT() is non-empty, assume that it is valid. (It might + // not be: for example, the user may have explicitly set GOROOT + // to the wrong directory.) + return + } + + cmd := exec.Command("go", "env", "GOROOT") + out, err := cmd.Output() + if err != nil { + gorootErr = fmt.Errorf("%v: %v", cmd, err) + } + gorootPath = strings.TrimSpace(string(out)) + }) + + return gorootPath, gorootErr +} + +// GOROOT reports the path to the directory containing the root of the Go +// project source tree. This is normally equivalent to runtime.GOROOT, but +// works even if the test binary was built with -trimpath. +// +// If GOROOT cannot be found, GOROOT skips t if t is non-nil, +// or panics otherwise. +func GOROOT(t testing.TB) string { + path, err := findGOROOT() + if err != nil { + if t == nil { + panic(err) + } + t.Helper() + t.Skip(err) + } + return path +} From d54e12bfc5fbc3d948088c5f869a5dcd960f1e81 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 30 Nov 2022 18:11:33 -0500 Subject: [PATCH 492/723] gopls/internal/lsp: avoid I/O in URI comparison operations Previously, URI comparison would look at both the text and then the inodes of the files, calling stat(2). This was a muddling of two concerns, and caused I/O operations in surprsing places, often when locks are held. Most callers need only a stable sort order for determinism; they can use a simple textual comparison. Of the remainder: - workspace.Clone uses a forked copy of the original, out of unrationalized fear. - Session.NewView uses span.SameExistingFile, the pure I/O-based check. - ColumnMapper.Range uses a weakened syntactic check; it just just an assertion (bug.Report). Also, commit to honorSymlink=false and simplify the code so that InDir and InDirLex become identical. Closes golang/go#42833 (won't fix) Change-Id: I984656a96144991908089eec101fd0df3c0e2477 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454355 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan --- gopls/internal/lsp/cache/session.go | 2 +- gopls/internal/lsp/cache/view.go | 2 +- gopls/internal/lsp/cache/workspace.go | 13 ++++-- gopls/internal/lsp/cmd/highlight.go | 5 +-- gopls/internal/lsp/fake/editor.go | 2 +- gopls/internal/lsp/lsp_test.go | 14 ++---- gopls/internal/lsp/protocol/span.go | 14 +++++- gopls/internal/lsp/source/references.go | 9 ++-- gopls/internal/lsp/source/source_test.go | 13 ++---- gopls/internal/lsp/source/util.go | 54 +----------------------- gopls/internal/span/parse.go | 4 +- gopls/internal/span/span.go | 25 ++++++++--- gopls/internal/span/uri.go | 27 ++---------- 13 files changed, 64 insertions(+), 120 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 514411177d3..c4d289a7786 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -167,7 +167,7 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt s.viewMu.Lock() defer s.viewMu.Unlock() for _, view := range s.views { - if span.CompareURI(view.folder, folder) == 0 { + if span.SameExistingFile(view.folder, folder) { return nil, nil, nil, source.ErrViewExists } } diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 4adcfb11662..54b4122db4f 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -486,7 +486,7 @@ func (v *View) filterFunc() func(span.URI) bool { filterer := buildFilterer(v.rootURI.Filename(), v.gomodcache, v.Options()) return func(uri span.URI) bool { // Only filter relative to the configured root directory. - if source.InDirLex(v.folder.Filename(), uri.Filename()) { + if source.InDir(v.folder.Filename(), uri.Filename()) { return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), filterer) } return false diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go index b280ef369a8..a97894ab5ba 100644 --- a/gopls/internal/lsp/cache/workspace.go +++ b/gopls/internal/lsp/cache/workspace.go @@ -309,9 +309,7 @@ func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI { for d := range w.wsDirs { dirs = append(dirs, d) } - sort.Slice(dirs, func(i, j int) bool { - return source.CompareURI(dirs[i], dirs[j]) < 0 - }) + sort.Slice(dirs, func(i, j int) bool { return dirs[i] < dirs[j] }) return dirs } @@ -349,6 +347,13 @@ func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, result.activeModFiles[k] = v } + equalURI := func(a, b span.URI) (r bool) { + // This query is a strange mix of syntax and file system state: + // deletion of a file causes a false result if the name doesn't change. + // Our tests exercise only the first clause. + return a == b || span.SameExistingFile(a, b) + } + // First handle changes to the go.work or gopls.mod file. This must be // considered before any changes to go.mod or go.sum files, as these files // determine which modules we care about. If go.work/gopls.mod has changed @@ -362,7 +367,7 @@ func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, continue } changed = true - active := result.moduleSource != legacyWorkspace || source.CompareURI(modURI(w.root), uri) == 0 + active := result.moduleSource != legacyWorkspace || equalURI(modURI(w.root), uri) needReinit = needReinit || (active && change.fileHandle.Saved()) // Don't mess with the list of mod files if using go.work or gopls.mod. if result.moduleSource == goplsModWorkspace || result.moduleSource == goWorkWorkspace { diff --git a/gopls/internal/lsp/cmd/highlight.go b/gopls/internal/lsp/cmd/highlight.go index 0737e9c424a..d8b3d226e0b 100644 --- a/gopls/internal/lsp/cmd/highlight.go +++ b/gopls/internal/lsp/cmd/highlight.go @@ -8,7 +8,6 @@ import ( "context" "flag" "fmt" - "sort" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/span" @@ -78,9 +77,7 @@ func (r *highlight) Run(ctx context.Context, args ...string) error { results = append(results, s) } // Sort results to make tests deterministic since DocumentHighlight uses a map. - sort.SliceStable(results, func(i, j int) bool { - return span.Compare(results[i], results[j]) == -1 - }) + span.SortSpans(results) for _, s := range results { fmt.Println(s) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index f73301d674c..df2f9751849 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -1274,7 +1274,7 @@ func (e *Editor) renameBuffers(ctx context.Context, oldPath, newPath string) (cl for path := range e.buffers { abs := e.sandbox.Workdir.AbsPath(path) - if oldAbs == abs || source.InDirLex(oldAbs, abs) { + if oldAbs == abs || source.InDir(oldAbs, abs) { rel, err := filepath.Rel(oldAbs, abs) if err != nil { return nil, nil, fmt.Errorf("filepath.Rel(%q, %q): %v", oldAbs, abs, err) diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index e9da7e3ee27..23941be0f6e 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -783,13 +783,9 @@ func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) } results = append(results, imp) } - // Sort results and expected to make tests deterministic. - sort.SliceStable(results, func(i, j int) bool { - return span.Compare(results[i], results[j]) == -1 - }) - sort.SliceStable(impls, func(i, j int) bool { - return span.Compare(impls[i], impls[j]) == -1 - }) + span.SortSpans(results) // to make tests + span.SortSpans(impls) // deterministic + for i := range results { if results[i] != impls[i] { t.Errorf("for %dth implementation of %v got %v want %v", i, spn, results[i], impls[i]) @@ -830,9 +826,7 @@ func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) { results = append(results, h) } // Sort results to make tests deterministic since DocumentHighlight uses a map. - sort.SliceStable(results, func(i, j int) bool { - return span.Compare(results[i], results[j]) == -1 - }) + span.SortSpans(results) // Check to make sure all the expected highlights are found. for i := range results { if results[i] != locations[i] { diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 2d87c081274..86b14c2caeb 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -38,10 +38,13 @@ import ( "bytes" "fmt" "go/token" + "path/filepath" + "strings" "unicode/utf8" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" ) // A ColumnMapper maps between UTF-8 oriented positions (e.g. token.Pos, @@ -93,9 +96,16 @@ func (m *ColumnMapper) Location(s span.Span) (Location, error) { } func (m *ColumnMapper) Range(s span.Span) (Range, error) { - if span.CompareURI(m.URI, s.URI()) != 0 { - return Range{}, fmt.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI()) + // Assert that we aren't using the wrong mapper. + // We check only the base name, and case insensitively, + // because we can't assume clean paths, no symbolic links, + // case-sensitive directories. The authoritative answer + // requires querying the file system, and we don't want + // to do that. + if !strings.EqualFold(filepath.Base(string(m.URI)), filepath.Base(string(s.URI()))) { + return Range{}, bug.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI()) } + s, err := s.WithOffset(m.TokFile) if err != nil { return Range{}, err diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index f147108e31e..bf3784bd634 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "go/ast" + "strings" "go/token" "go/types" @@ -122,11 +123,11 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit toSort = refs[1:] } sort.Slice(toSort, func(i, j int) bool { - x := CompareURI(toSort[i].URI(), toSort[j].URI()) - if x == 0 { - return toSort[i].ident.Pos() < toSort[j].ident.Pos() + x, y := toSort[i], toSort[j] + if cmp := strings.Compare(string(x.URI()), string(y.URI())); cmp != 0 { + return cmp < 0 } - return x < 0 + return x.ident.Pos() < y.ident.Pos() }) return refs, nil } diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 3ca62c341e0..98bac6df894 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -614,13 +614,8 @@ func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) } results = append(results, imp) } - // Sort results and expected to make tests deterministic. - sort.SliceStable(results, func(i, j int) bool { - return span.Compare(results[i], results[j]) == -1 - }) - sort.SliceStable(impls, func(i, j int) bool { - return span.Compare(impls[i], impls[j]) == -1 - }) + span.SortSpans(results) // to make tests + span.SortSpans(impls) // deterministic for i := range results { if results[i] != impls[i] { t.Errorf("for %dth implementation of %v got %v want %v", i, spn, results[i], impls[i]) @@ -655,9 +650,7 @@ func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) { results = append(results, h) } // Sort results to make tests deterministic since DocumentHighlight uses a map. - sort.SliceStable(results, func(i, j int) bool { - return span.Compare(results[i], results[j]) == -1 - }) + span.SortSpans(results) // Check to make sure all the expected highlights are found. for i := range results { if results[i] != locations[i] { diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 55267c95ad4..32c690f92da 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -445,62 +445,12 @@ func isDirective(c string) bool { return true } -// honorSymlinks toggles whether or not we consider symlinks when comparing -// file or directory URIs. -const honorSymlinks = false - -func CompareURI(left, right span.URI) int { - if honorSymlinks { - return span.CompareURI(left, right) - } - if left == right { - return 0 - } - if left < right { - return -1 - } - return 1 -} - // InDir checks whether path is in the file tree rooted at dir. -// InDir makes some effort to succeed even in the presence of symbolic links. -// -// Copied and slightly adjusted from go/src/cmd/go/internal/search/search.go. -func InDir(dir, path string) bool { - if InDirLex(dir, path) { - return true - } - if !honorSymlinks { - return false - } - xpath, err := filepath.EvalSymlinks(path) - if err != nil || xpath == path { - xpath = "" - } else { - if InDirLex(dir, xpath) { - return true - } - } - - xdir, err := filepath.EvalSymlinks(dir) - if err == nil && xdir != dir { - if InDirLex(xdir, path) { - return true - } - if xpath != "" { - if InDirLex(xdir, xpath) { - return true - } - } - } - return false -} - -// InDirLex is like inDir but only checks the lexical form of the file names. +// It checks only the lexical form of the file names. // It does not consider symbolic links. // // Copied from go/src/cmd/go/internal/search/search.go. -func InDirLex(dir, path string) bool { +func InDir(dir, path string) bool { pv := strings.ToUpper(filepath.VolumeName(path)) dv := strings.ToUpper(filepath.VolumeName(dir)) path = path[len(pv):] diff --git a/gopls/internal/span/parse.go b/gopls/internal/span/parse.go index c4cec16e90d..715d5fe44fd 100644 --- a/gopls/internal/span/parse.go +++ b/gopls/internal/span/parse.go @@ -92,8 +92,9 @@ func rstripSuffix(input string) suffix { return suffix{"", "", -1} } remains := input + + // Remove optional trailing decimal number. num := -1 - // first see if we have a number at the end last := strings.LastIndexFunc(remains, func(r rune) bool { return r < '0' || r > '9' }) if last >= 0 && last < len(remains)-1 { number, err := strconv.ParseInt(remains[last+1:], 10, 64) @@ -104,6 +105,7 @@ func rstripSuffix(input string) suffix { } // now see if we have a trailing separator r, w := utf8.DecodeLastRuneInString(remains) + // TODO(adonovan): this condition is clearly wrong. Should the third byte be '-'? if r != ':' && r != '#' && r == '#' { return suffix{input, "", -1} } diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go index 333048ae8c6..00c7b3f2a43 100644 --- a/gopls/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -11,6 +11,8 @@ import ( "fmt" "go/token" "path" + "sort" + "strings" ) // Span represents a source code range in standardized form. @@ -54,12 +56,23 @@ func NewPoint(line, col, offset int) Point { return p } -func Compare(a, b Span) int { - if r := CompareURI(a.URI(), b.URI()); r != 0 { - return r - } - if r := comparePoint(a.v.Start, b.v.Start); r != 0 { - return r +// SortSpans sorts spans into a stable but unspecified order. +func SortSpans(spans []Span) { + sort.SliceStable(spans, func(i, j int) bool { + return compare(spans[i], spans[j]) < 0 + }) +} + +// compare implements a three-valued ordered comparison of Spans. +func compare(a, b Span) int { + // This is a textual comparison. It does not peform path + // cleaning, case folding, resolution of symbolic links, + // testing for existence, or any I/O. + if cmp := strings.Compare(string(a.URI()), string(b.URI())); cmp != 0 { + return cmp + } + if cmp := comparePoint(a.v.Start, b.v.Start); cmp != 0 { + return cmp } return comparePoint(a.v.End, b.v.End) } diff --git a/gopls/internal/span/uri.go b/gopls/internal/span/uri.go index adcc54a7797..8f6b033f41a 100644 --- a/gopls/internal/span/uri.go +++ b/gopls/internal/span/uri.go @@ -8,7 +8,6 @@ import ( "fmt" "net/url" "os" - "path" "path/filepath" "runtime" "strings" @@ -101,28 +100,9 @@ func URIFromURI(s string) URI { return URI(u.String()) } -// CompareURI performs a three-valued comparison of two URIs. -// Lexically unequal URIs may compare equal if they are "file:" URIs -// that share the same base name (ignoring case) and denote the same -// file device/inode, according to stat(2). -func CompareURI(a, b URI) int { - if equalURI(a, b) { - return 0 - } - if a < b { - return -1 - } - return 1 -} - -func equalURI(a, b URI) bool { - if a == b { - return true - } - // If we have the same URI basename, we may still have the same file URIs. - if !strings.EqualFold(path.Base(string(a)), path.Base(string(b))) { - return false - } +// SameExistingFile reports whether two spans denote the +// same existing file by querying the file system. +func SameExistingFile(a, b URI) bool { fa, err := filename(a) if err != nil { return false @@ -131,7 +111,6 @@ func equalURI(a, b URI) bool { if err != nil { return false } - // Stat the files to check if they are equal. infoa, err := os.Stat(filepath.FromSlash(fa)) if err != nil { return false From 47a82463d369ea966c8b000b8068b22fe23107c7 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 1 Dec 2022 17:35:23 -0500 Subject: [PATCH 493/723] gopls/internal/regtest/misc: skip TestRunGovulncheckError2 Skipping while investigating a better option to test error handling behavior. Updates golang/go#57032 Change-Id: Ifa7924a013ed092ac6acfc85f06aff48b2035721 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454638 Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Auto-Submit: Hyang-Ah Hana Kim --- gopls/internal/regtest/misc/vuln_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index b043e0cc77c..18e500c44ca 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -57,6 +57,7 @@ package foo } func TestRunGovulncheckError2(t *testing.T) { + t.Skip("skipping due to go.dev/issues/57032") const files = ` -- go.mod -- module mod.com From 8c78b306b8b8f8a3cb067d717e3c76860d76340f Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 1 Dec 2022 17:07:48 -0500 Subject: [PATCH 494/723] gopls/internal/vulncheck: always use pkg.go.dev/vuln for urls Change-Id: Id740cf5d0387b7358f4fac2c7aa1300354453944 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454636 Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Jamal Carvalho --- gopls/internal/lsp/mod/diagnostics.go | 17 ++++------------- gopls/internal/vulncheck/util.go | 17 ++++------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 7ee92bdc332..6b7a1a6fefa 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -465,20 +465,11 @@ func formatMessage(v *govulncheck.Vuln) string { return strings.TrimSpace(strings.Replace(string(details), "\n\n", "\n\n ", -1)) } -// href returns a URL embedded in the entry if any. -// If no suitable URL is found, it returns a default entry in -// pkg.go.dev/vuln. +// href returns the url for the vulnerability information. +// Eventually we should retrieve the url embedded in the osv.Entry. +// While vuln.go.dev is under development, this always returns +// the page in pkg.go.dev. func href(vuln *osv.Entry) string { - for _, affected := range vuln.Affected { - if url := affected.DatabaseSpecific.URL; url != "" { - return url - } - } - for _, r := range vuln.References { - if r.Type == "WEB" { - return r.URL - } - } return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vuln.ID) } diff --git a/gopls/internal/vulncheck/util.go b/gopls/internal/vulncheck/util.go index d21d30517a4..fe65c6eb4de 100644 --- a/gopls/internal/vulncheck/util.go +++ b/gopls/internal/vulncheck/util.go @@ -48,20 +48,11 @@ func toStackEntry(src vulncheck.StackEntry) StackEntry { } } -// href returns a URL embedded in the entry if any. -// If no suitable URL is found, it returns a default entry in -// pkg.go.dev/vuln. +// href returns the url for the vulnerability information. +// Eventually we should retrieve the url embedded in the osv.Entry. +// While vuln.go.dev is under development, this always returns +// the page in pkg.go.dev. func href(vuln *osv.Entry) string { - for _, affected := range vuln.Affected { - if url := affected.DatabaseSpecific.URL; url != "" { - return url - } - } - for _, r := range vuln.References { - if r.Type == "WEB" { - return r.URL - } - } return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vuln.ID) } From 31d5843335c1b43d94ec439c246662623d5b00ba Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 1 Dec 2022 15:10:20 -0500 Subject: [PATCH 495/723] gopls/internal/lsp/cache: fix re-entrant session locking The session used re-entrant locking of its RWMutex viewMu, a latent bug only avoided due to jsonrpc2 serialization. Furthermore, the read locking in ViewOf is not even correct: ViewOf may write to viewMap for unmapped files. Fix this by switching to a Mutex, and delegating the ViewOf logic to a viewOfLocked method. Change-Id: I73f4a7b6b6569c2565c8be0dcf8f05d9c88c141c Reviewed-on: https://go-review.googlesource.com/c/tools/+/454635 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/session.go | 38 +++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index c4d289a7786..3b0958e592b 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -36,7 +36,7 @@ type Session struct { optionsMu sync.Mutex options *source.Options - viewMu sync.RWMutex + viewMu sync.Mutex views []*View viewMap map[span.URI]*View // map of URI->best view @@ -304,8 +304,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, // View returns a view with a matching name, if the session has one. func (s *Session) View(name string) *View { - s.viewMu.RLock() - defer s.viewMu.RUnlock() + s.viewMu.Lock() + defer s.viewMu.Unlock() for _, view := range s.views { if view.Name() == name { return view @@ -317,8 +317,13 @@ func (s *Session) View(name string) *View { // ViewOf returns a view corresponding to the given URI. // If the file is not already associated with a view, pick one using some heuristics. func (s *Session) ViewOf(uri span.URI) (*View, error) { - s.viewMu.RLock() - defer s.viewMu.RUnlock() + s.viewMu.Lock() + defer s.viewMu.Unlock() + return s.viewOfLocked(uri) +} + +// Precondition: caller holds s.viewMu lock. +func (s *Session) viewOfLocked(uri span.URI) (*View, error) { // Check if we already know this file. if v, found := s.viewMap[uri]; found { return v, nil @@ -332,8 +337,8 @@ func (s *Session) ViewOf(uri span.URI) (*View, error) { } func (s *Session) Views() []*View { - s.viewMu.RLock() - defer s.viewMu.RUnlock() + s.viewMu.Lock() + defer s.viewMu.Unlock() result := make([]*View, len(s.views)) copy(result, s.views) return result @@ -462,8 +467,8 @@ type fileChange struct { // On success, it returns a release function that // must be called when the snapshots are no longer needed. func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]span.URI, func(), error) { - s.viewMu.RLock() - defer s.viewMu.RUnlock() + s.viewMu.Lock() + defer s.viewMu.Unlock() views := make(map[*View]map[span.URI]*fileChange) affectedViews := map[span.URI][]*View{} @@ -493,7 +498,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif if c.OnDisk { continue } - bestView, err := s.ViewOf(c.URI) + bestView, err := s.viewOfLocked(c.URI) if err != nil { return nil, nil, err } @@ -575,15 +580,15 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // directory changes removed and expanded to include all of the files in // the directory. func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []source.FileModification) []source.FileModification { - s.viewMu.RLock() - defer s.viewMu.RUnlock() var snapshots []*snapshot + + s.viewMu.Lock() for _, v := range s.views { snapshot, release := v.getSnapshot() defer release() snapshots = append(snapshots, snapshot) } - // TODO(adonovan): opt: release lock here. + s.viewMu.Unlock() knownDirs := knownDirectories(ctx, snapshots) defer knownDirs.Destroy() @@ -640,6 +645,7 @@ func knownFilesInDir(ctx context.Context, snapshots []*snapshot, dir span.URI) m return files } +// Precondition: caller holds s.viewMu lock. func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModification) (map[span.URI]*overlay, error) { s.overlayMu.Lock() defer s.overlayMu.Unlock() @@ -726,7 +732,7 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif // When opening files, ensure that we actually have a well-defined view and file kind. if c.Action == source.Open { - view, err := s.ViewOf(o.uri) + view, err := s.viewOfLocked(o.uri) if err != nil { return nil, fmt.Errorf("updateOverlays: finding view for %s: %v", o.uri, err) } @@ -784,8 +790,8 @@ func (s *Session) Overlays() []source.Overlay { // known by the view. For views within a module, this is the module root, // any directory in the module root, and any replace targets. func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} { - s.viewMu.RLock() - defer s.viewMu.RUnlock() + s.viewMu.Lock() + defer s.viewMu.Unlock() patterns := map[string]struct{}{} for _, view := range s.views { snapshot, release := view.getSnapshot() From e0b516bc0a4e63e59ad664d4dd495b588a301e11 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 28 Nov 2022 23:41:43 -0500 Subject: [PATCH 496/723] gopls/internal/lsp/cache: invalidate metadata after vendor change This change causes the snapshot to watch the vendor/modules.txt file and to invalidate the metadata if it changes, since it could cause modules to move. Also, add a regression test for the bug in which running go mod vendor causes a module to move, but the Definitions operation reports the old location. (And the reverse case of deleting the vendor tree.) Also, add persistent.Map.String method, for debugging. Fixes golang/go#55995 Change-Id: I48302416586c763b4a4de7d67aaa88fde52ea400 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454315 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/session.go | 1 - gopls/internal/lsp/cache/snapshot.go | 42 +++++---- gopls/internal/lsp/cache/view.go | 22 +++-- gopls/internal/lsp/cache/view_test.go | 17 ++-- .../internal/regtest/misc/definition_test.go | 86 +++++++++++++++++++ internal/persistent/map.go | 14 +++ 6 files changed, 142 insertions(+), 40 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 3b0958e592b..51c8bb70641 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -581,7 +581,6 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // the directory. func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []source.FileModification) []source.FileModification { var snapshots []*snapshot - s.viewMu.Lock() for _, v := range s.views { snapshot, release := v.getSnapshot() diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 5a15641a5f0..80de11a341b 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -917,7 +917,7 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru dirName := dir.Filename() // If the directory is within the view's folder, we're already watching - // it with the pattern above. + // it with the first pattern above. if source.InDir(s.view.folder.Filename(), dirName) { continue } @@ -1654,16 +1654,18 @@ func (s *snapshot) orphanedFiles() []source.VersionedFileHandle { // Most likely, each call site of inVendor needs to be reconsidered to // understand and correctly implement the desired behavior. func inVendor(uri span.URI) bool { - if !strings.Contains(string(uri), "/vendor/") { - return false - } - // Only packages in _subdirectories_ of /vendor/ are considered vendored + _, after, found := cut(string(uri), "/vendor/") + // Only subdirectories of /vendor/ are considered vendored // (/vendor/a/foo.go is vendored, /vendor/foo.go is not). - split := strings.Split(string(uri), "/vendor/") - if len(split) < 2 { - return false + return found && strings.Contains(after, "/") +} + +// TODO(adonovan): replace with strings.Cut when we can assume go1.18. +func cut(s, sep string) (before, after string, found bool) { + if i := strings.Index(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true } - return strings.Contains(split[1], "/") + return s, "", false } // unappliedChanges is a file source that handles an uncloned snapshot. @@ -1691,14 +1693,16 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC s.mu.Lock() defer s.mu.Unlock() - // If there is an initialization error and a vendor directory changed, try to - // reinit. - if s.initializedErr != nil { - for uri := range changes { - if inVendor(uri) { - reinit = true - break - } + // Changes to vendor tree may require reinitialization, + // either because of an initialization error + // (e.g. "inconsistent vendoring detected"), or because + // one or more modules may have moved into or out of the + // vendor tree after 'go mod vendor' or 'rm -fr vendor/'. + for uri := range changes { + if inVendor(uri) && s.initializedErr != nil || + strings.HasSuffix(string(uri), "/vendor/modules.txt") { + reinit = true + break } } @@ -1782,7 +1786,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // directIDs keeps track of package IDs that have directly changed. - // It maps id->invalidateMetadata. + // Note: this is not a set, it's a map from id to invalidateMetadata. directIDs := map[PackageID]bool{} // Invalidate all package metadata if the workspace module has changed. @@ -1821,7 +1825,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Mark all of the package IDs containing the given file. filePackageIDs := invalidatedPackageIDs(uri, s.meta.ids, pkgFileChanged) for id := range filePackageIDs { - directIDs[id] = directIDs[id] || invalidateMetadata + directIDs[id] = directIDs[id] || invalidateMetadata // may insert 'false' } // Invalidate the previous modTidyHandle if any of the files have been diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 54b4122db4f..bcb6f3a6b9e 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -1128,28 +1128,34 @@ var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`) // FileHandle from the cache for temporary files is problematic, since we // cannot delete it. func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modContent []byte) (bool, error) { + // Legacy GOPATH workspace? if s.workspaceMode()&moduleMode == 0 { return false, nil } + + // Explicit -mod flag? matches := modFlagRegexp.FindStringSubmatch(s.view.goEnv["GOFLAGS"]) - var modFlag string if len(matches) != 0 { - modFlag = matches[1] - } - if modFlag != "" { - // Don't override an explicit '-mod=vendor' argument. - // We do want to override '-mod=readonly': it would break various module code lenses, - // and on 1.16 we know -modfile is available, so we won't mess with go.mod anyway. - return modFlag == "vendor", nil + modFlag := matches[1] + if modFlag != "" { + // Don't override an explicit '-mod=vendor' argument. + // We do want to override '-mod=readonly': it would break various module code lenses, + // and on 1.16 we know -modfile is available, so we won't mess with go.mod anyway. + return modFlag == "vendor", nil + } } modFile, err := modfile.Parse(modURI.Filename(), modContent, nil) if err != nil { return false, err } + + // No vendor directory? if fi, err := os.Stat(filepath.Join(s.view.rootURI.Filename(), "vendor")); err != nil || !fi.IsDir() { return false, nil } + + // Vendoring enabled by default by go declaration in go.mod? vendorEnabled := modFile.Go != nil && modFile.Go.Version != "" && semver.Compare("v"+modFile.Go.Version, "v1.14") >= 0 return vendorEnabled, nil } diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go index 99daff13b01..f57fc808e80 100644 --- a/gopls/internal/lsp/cache/view_test.go +++ b/gopls/internal/lsp/cache/view_test.go @@ -111,18 +111,11 @@ func TestInVendor(t *testing.T) { path string inVendor bool }{ - { - path: "foo/vendor/x.go", - inVendor: false, - }, - { - path: "foo/vendor/x/x.go", - inVendor: true, - }, - { - path: "foo/x.go", - inVendor: false, - }, + {"foo/vendor/x.go", false}, + {"foo/vendor/x/x.go", true}, + {"foo/x.go", false}, + {"foo/vendor/foo.txt", false}, + {"foo/vendor/modules.txt", false}, } { if got := inVendor(span.URIFromPath(tt.path)); got != tt.inVendor { t.Errorf("expected %s inVendor %v, got %v", tt.path, tt.inVendor, got) diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index a5030d71fb3..8f8619e3cb1 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -5,7 +5,9 @@ package misc import ( + "os" "path" + "path/filepath" "strings" "testing" @@ -286,3 +288,87 @@ func TestGoToCrashingDefinition_Issue49223(t *testing.T) { env.Editor.Server.Definition(env.Ctx, params) }) } + +// TestVendoringInvalidatesMetadata ensures that gopls uses the +// correct metadata even after an external 'go mod vendor' command +// causes packages to move; see issue #55995. +func TestVendoringInvalidatesMetadata(t *testing.T) { + const proxy = ` +-- other.com/b@v1.0.0/go.mod -- +module other.com/b +go 1.14 + +-- other.com/b@v1.0.0/b.go -- +package b +const K = 0 +` + const src = ` +-- go.mod -- +module example.com/a +go 1.14 +require other.com/b v1.0.0 + +-- go.sum -- +other.com/b v1.0.0 h1:1wb3PMGdet5ojzrKl+0iNksRLnOM9Jw+7amBNqmYwqk= +other.com/b v1.0.0/go.mod h1:TgHQFucl04oGT+vrUm/liAzukYHNxCwKNkQZEyn3m9g= + +-- a.go -- +package a +import "other.com/b" +const _ = b.K + +` + WithOptions( + ProxyFiles(proxy), + Modes(Default), // fails in 'experimental' mode + ).Run(t, src, func(t *testing.T, env *Env) { + // Enable to debug go.sum mismatch, which may appear as + // "module lookup disabled by GOPROXY=off", confusingly. + if false { + env.DumpGoSum(".") + } + + env.OpenFile("a.go") + refPos := env.RegexpSearch("a.go", "K") // find "b.K" reference + + // Initially, b.K is defined in the module cache. + gotFile, _ := env.GoToDefinition("a.go", refPos) + wantCache := filepath.ToSlash(env.Sandbox.GOPATH()) + "/pkg/mod/other.com/b@v1.0.0/b.go" + if gotFile != wantCache { + t.Errorf("GoToDefinition, before: got file %q, want %q", gotFile, wantCache) + } + + // Run 'go mod vendor' outside the editor. + if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, true); err != nil { + t.Fatalf("go mod vendor: %v", err) + } + + // Synchronize changes to watched files. + env.Await(env.DoneWithChangeWatchedFiles()) + + // Now, b.K is defined in the vendor tree. + gotFile, _ = env.GoToDefinition("a.go", refPos) + wantVendor := "vendor/other.com/b/b.go" + if gotFile != wantVendor { + t.Errorf("GoToDefinition, after go mod vendor: got file %q, want %q", gotFile, wantVendor) + } + + // Delete the vendor tree. + if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil { + t.Fatal(err) + } + // Notify the server of the deletion. + if err := env.Sandbox.Workdir.CheckForFileChanges(env.Ctx); err != nil { + t.Fatal(err) + } + + // Synchronize again. + env.Await(env.DoneWithChangeWatchedFiles()) + + // b.K is once again defined in the module cache. + gotFile, _ = env.GoToDefinition("a.go", refPos) + if gotFile != wantCache { + t.Errorf("GoToDefinition, after rm -rf vendor: got file %q, want %q", gotFile, wantCache) + } + }) +} diff --git a/internal/persistent/map.go b/internal/persistent/map.go index f5dd10206b8..b29cfe41943 100644 --- a/internal/persistent/map.go +++ b/internal/persistent/map.go @@ -8,7 +8,9 @@ package persistent import ( + "fmt" "math/rand" + "strings" "sync/atomic" ) @@ -41,6 +43,18 @@ type Map struct { root *mapNode } +func (m *Map) String() string { + var buf strings.Builder + buf.WriteByte('{') + var sep string + m.Range(func(k, v interface{}) { + fmt.Fprintf(&buf, "%s%v: %v", sep, k, v) + sep = ", " + }) + buf.WriteByte('}') + return buf.String() +} + type mapNode struct { key interface{} value *refValue From 25fdb81eb104ce5f95ad9a8262491f8eb20a79f8 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 2 Dec 2022 11:21:38 -0500 Subject: [PATCH 497/723] gopls/internal/regtest/misc: skip vendor test on go1.13 To investigate why changing the minimum go version to go1.13 (and updating the sums) isn't a fix would be a waste of time since our support for 1.13 is about to go away. Change-Id: I12ee02faf617318da4733e600ef6312c3ee2865e Reviewed-on: https://go-review.googlesource.com/c/tools/+/454795 Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/internal/regtest/misc/definition_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index 8f8619e3cb1..db1cb83bb13 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -293,6 +293,7 @@ func TestGoToCrashingDefinition_Issue49223(t *testing.T) { // correct metadata even after an external 'go mod vendor' command // causes packages to move; see issue #55995. func TestVendoringInvalidatesMetadata(t *testing.T) { + testenv.NeedsGo1Point(t, 14) const proxy = ` -- other.com/b@v1.0.0/go.mod -- module other.com/b From d444fa3b3280d9d760669a7a1ed817bf5b02707f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 1 Dec 2022 11:17:42 -0500 Subject: [PATCH 498/723] gopls/internal/lsp/cache: simplify canonical URI cache The View's URI canonicalization cache entry, fileBase, used to hold a list of URIs even though only the first is ever needed. This change simplifies it to a pair of (URI, filename). This should reduce allocation slightly. Also: - Eliminate the unnecessary pointer type to further reduce allocation. - Use a dedicated mutex for the two maps. - Update the names to better reflect the purpose. - Document various failed approaches to optimization and two problems in the existing logic, one of correctness, the other of performance. Change-Id: Ic74ae4bae8864de292fe97d26c5f9aacf2367961 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454435 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/cache/session.go | 8 +- gopls/internal/lsp/cache/snapshot.go | 25 ++--- gopls/internal/lsp/cache/view.go | 147 +++++++++++---------------- 3 files changed, 77 insertions(+), 103 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 51c8bb70641..cb2570e8d46 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -235,8 +235,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, folder: folder, moduleUpgrades: map[span.URI]map[string]string{}, vulns: map[span.URI]*govulncheck.Result{}, - filesByURI: map[span.URI]*fileBase{}, - filesByBase: map[string][]*fileBase{}, + filesByURI: make(map[span.URI]span.URI), + filesByBase: make(map[string][]canonicalURI), rootURI: root, explicitGowork: goworkURI, workspaceInformation: *wsInfo, @@ -510,8 +510,8 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // Apply the changes to all affected views. for _, view := range changedViews { - // Make sure that the file is added to the view. - _ = view.getFile(c.URI) + // Make sure that the file is added to the view's knownFiles set. + view.canonicalURI(c.URI, true) // ignore result if _, ok := views[view]; !ok { views[view] = make(map[span.URI]*fileChange) } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 80de11a341b..1ef462207f2 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1287,12 +1287,12 @@ func (s *snapshot) isWorkspacePackage(id PackageID) bool { } func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle { - f := s.view.getFile(uri) + uri, _ = s.view.canonicalURI(uri, true) s.mu.Lock() defer s.mu.Unlock() - result, _ := s.files.Get(f.URI()) + result, _ := s.files.Get(uri) return result } @@ -1302,32 +1302,29 @@ func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle { // GetVersionedFile succeeds even if the file does not exist. A non-nil error return // indicates some type of internal error, for example if ctx is cancelled. func (s *snapshot) GetVersionedFile(ctx context.Context, uri span.URI) (source.VersionedFileHandle, error) { - f := s.view.getFile(uri) + uri, _ = s.view.canonicalURI(uri, true) s.mu.Lock() defer s.mu.Unlock() - return s.getFileLocked(ctx, f) -} - -// GetFile implements the fileSource interface by wrapping GetVersionedFile. -func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { - return s.GetVersionedFile(ctx, uri) -} -func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.VersionedFileHandle, error) { - if fh, ok := s.files.Get(f.URI()); ok { + if fh, ok := s.files.Get(uri); ok { return fh, nil } - fh, err := s.view.cache.getFile(ctx, f.URI()) // read the file + fh, err := s.view.cache.getFile(ctx, uri) // read the file if err != nil { return nil, err } closed := &closedFile{fh} - s.files.Set(f.URI(), closed) + s.files.Set(uri, closed) return closed, nil } +// GetFile implements the fileSource interface by wrapping GetVersionedFile. +func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { + return s.GetVersionedFile(ctx, uri) +} + func (s *snapshot) IsOpen(uri span.URI) bool { s.mu.Lock() defer s.mu.Unlock() diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index bcb6f3a6b9e..bc623ff16f3 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -65,10 +65,12 @@ type View struct { vulns map[span.URI]*govulncheck.Result - // keep track of files by uri and by basename, a single file may be mapped - // to multiple uris, and the same basename may map to multiple files - filesByURI map[span.URI]*fileBase - filesByBase map[string][]*fileBase + // filesByURI maps URIs to the canonical URI for the file it denotes. + // We also keep a set of candidates for a given basename + // to reduce the set of pairs that need to be tested for sameness. + filesByMu sync.Mutex + filesByURI map[span.URI]span.URI // key is noncanonical URI (alias) + filesByBase map[string][]canonicalURI // key is basename // initCancelFirstAttempt can be used to terminate the view's first // attempt at initialization. @@ -205,28 +207,6 @@ const ( tempModfile ) -// fileBase holds the common functionality for all files. -// It is intended to be embedded in the file implementations -type fileBase struct { - uris []span.URI - fname string - - view *View -} - -func (f *fileBase) URI() span.URI { - return f.uris[0] -} - -func (f *fileBase) filename() string { - return f.fname -} - -func (f *fileBase) addURI(uri span.URI) int { - f.uris = append(f.uris, uri) - return len(f.uris) -} - func (v *View) ID() string { return v.id } // tempModFile creates a temporary go.mod file based on the contents @@ -493,18 +473,6 @@ func (v *View) filterFunc() func(span.URI) bool { } } -func (v *View) mapFile(uri span.URI, f *fileBase) { - v.filesByURI[uri] = f - if f.addURI(uri) == 1 { - basename := basename(f.filename()) - v.filesByBase[basename] = append(v.filesByBase[basename], f) - } -} - -func basename(filename string) string { - return strings.ToLower(filepath.Base(filename)) -} - func (v *View) relevantChange(c source.FileModification) bool { // If the file is known to the view, the change is relevant. if v.knownFile(c.URI) { @@ -530,64 +498,73 @@ func (v *View) relevantChange(c source.FileModification) bool { return v.contains(c.URI) } +// knownFile reports whether the specified valid URI (or an alias) is known to the view. func (v *View) knownFile(uri span.URI) bool { - v.mu.Lock() - defer v.mu.Unlock() - - f, err := v.findFile(uri) - return f != nil && err == nil + _, known := v.canonicalURI(uri, false) + return known } -// getFile returns a file for the given URI. -func (v *View) getFile(uri span.URI) *fileBase { - v.mu.Lock() - defer v.mu.Unlock() - - f, _ := v.findFile(uri) - if f != nil { - return f - } - f = &fileBase{ - view: v, - fname: uri.Filename(), - } - v.mapFile(uri, f) - return f +// TODO(adonovan): opt: eliminate 'filename' optimization. I doubt the +// cost of allocation is significant relative to the +// stat/open/fstat/close operations that follow on Windows. +type canonicalURI struct { + uri span.URI + filename string // = uri.Filename(), an optimization (on Windows) } -// findFile checks the cache for any file matching the given uri. +// canonicalURI returns the canonical URI that denotes the same file +// as uri, which may differ due to case insensitivity, unclean paths, +// soft or hard links, and so on. If no previous alias was found, or +// the file is missing, insert determines whether to make uri the +// canonical representative of the file or to return false. // -// An error is only returned for an irreparable failure, for example, if the -// filename in question does not exist. -func (v *View) findFile(uri span.URI) (*fileBase, error) { - if f := v.filesByURI[uri]; f != nil { - // a perfect match - return f, nil - } - // no exact match stored, time to do some real work - // check for any files with the same basename - fname := uri.Filename() - basename := basename(fname) +// The cache grows indefinitely without invalidation: file system +// operations may cause two URIs that used to denote the same file to +// no longer to do so. Also, the basename cache grows without bound. +// TODO(adonovan): fix both bugs. +func (v *View) canonicalURI(uri span.URI, insert bool) (span.URI, bool) { + v.filesByMu.Lock() + defer v.filesByMu.Unlock() + + // Have we seen this exact URI before? + if canonical, ok := v.filesByURI[uri]; ok { + return canonical, true + } + + // Inspect all candidates with the same lowercase basename. + // This heuristic is easily defeated by symbolic links to files. + // Files with some basenames (e.g. doc.go) are very numerous. + // + // The set of candidates grows without bound, and incurs a + // linear sequence of SameFile queries to the file system. + // + // It is tempting to fetch the device/inode pair that + // uniquely identifies a file once, and then compare those + // pairs, but that would cause us to cache stale file system + // state (in addition to the filesByURI staleness). + filename := uri.Filename() + basename := strings.ToLower(filepath.Base(filename)) if candidates := v.filesByBase[basename]; candidates != nil { - pathStat, err := os.Stat(fname) - if os.IsNotExist(err) { - return nil, err - } - if err != nil { - return nil, nil // the file may exist, return without an error - } - for _, c := range candidates { - if cStat, err := os.Stat(c.filename()); err == nil { - if os.SameFile(pathStat, cStat) { - // same file, map it - v.mapFile(uri, c) - return c, nil + if pathStat, _ := os.Stat(filename); pathStat != nil { + for _, c := range candidates { + if cStat, _ := os.Stat(c.filename); cStat != nil { + // On Windows, SameFile is more expensive as it must + // open the file and use the equivalent of fstat(2). + if os.SameFile(pathStat, cStat) { + v.filesByURI[uri] = c.uri + return c.uri, true + } } } } } - // no file with a matching name was found, it wasn't in our cache - return nil, nil + + // No candidates, stat failed, or no candidate matched. + if insert { + v.filesByURI[uri] = uri + v.filesByBase[basename] = append(v.filesByBase[basename], canonicalURI{uri, filename}) + } + return uri, insert } // shutdown releases resources associated with the view, and waits for ongoing From f540ee6b175a7971fd009ac8bc072f972cf9490a Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Thu, 1 Dec 2022 17:03:08 -0500 Subject: [PATCH 499/723] internal/gcimporter: load cached export data for packages individually In short tests, also avoid creating export data for all of std. This change applies the same improvements made to the equivalent std packages in CL 454497 and CL 454498. (It may even fix the reverse builders that are currently timing out in x/tools, although I suspect there is still a bit of work to do for those.) For golang/go#56967. Updates golang/go#47257. Change-Id: I82e72557da5f917203637513122932c7942a98e9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454499 Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot Reviewed-by: Michael Matloob Run-TryBot: Bryan Mills gopls-CI: kokoro --- internal/gcimporter/gcimporter.go | 64 +++++++++++++++++--------- internal/gcimporter/gcimporter_test.go | 46 ++++++++++++++++-- internal/goroot/importcfg.go | 8 ++-- 3 files changed, 88 insertions(+), 30 deletions(-) diff --git a/internal/gcimporter/gcimporter.go b/internal/gcimporter/gcimporter.go index f8369cdc52e..1faaa365260 100644 --- a/internal/gcimporter/gcimporter.go +++ b/internal/gcimporter/gcimporter.go @@ -13,6 +13,7 @@ package gcimporter // import "golang.org/x/tools/internal/gcimporter" import ( "bufio" + "bytes" "errors" "fmt" "go/build" @@ -22,14 +23,13 @@ import ( "io" "io/ioutil" "os" - "path" + "os/exec" "path/filepath" "sort" "strconv" "strings" + "sync" "text/scanner" - - "golang.org/x/tools/internal/goroot" ) const ( @@ -41,23 +41,45 @@ const ( trace = false ) -func lookupGorootExport(pkgpath, srcRoot, srcDir string) (string, bool) { - pkgpath = filepath.ToSlash(pkgpath) - m, err := goroot.PkgfileMap() - if err != nil { - return "", false - } - if export, ok := m[pkgpath]; ok { - return export, true - } - vendorPrefix := "vendor" - if strings.HasPrefix(srcDir, filepath.Join(srcRoot, "cmd")) { - vendorPrefix = path.Join("cmd", vendorPrefix) +var exportMap sync.Map // package dir → func() (string, bool) + +// lookupGorootExport returns the location of the export data +// (normally found in the build cache, but located in GOROOT/pkg +// in prior Go releases) for the package located in pkgDir. +// +// (We use the package's directory instead of its import path +// mainly to simplify handling of the packages in src/vendor +// and cmd/vendor.) +func lookupGorootExport(pkgDir string) (string, bool) { + f, ok := exportMap.Load(pkgDir) + if !ok { + var ( + listOnce sync.Once + exportPath string + ) + f, _ = exportMap.LoadOrStore(pkgDir, func() (string, bool) { + listOnce.Do(func() { + cmd := exec.Command("go", "list", "-export", "-f", "{{.Export}}", pkgDir) + cmd.Dir = build.Default.GOROOT + var output []byte + output, err := cmd.Output() + if err != nil { + return + } + + exports := strings.Split(string(bytes.TrimSpace(output)), "\n") + if len(exports) != 1 { + return + } + + exportPath = exports[0] + }) + + return exportPath, exportPath != "" + }) } - pkgpath = path.Join(vendorPrefix, pkgpath) - fmt.Fprintln(os.Stderr, "looking up ", pkgpath) - export, ok := m[pkgpath] - return export, ok + + return f.(func() (string, bool))() } var pkgExts = [...]string{".a", ".o"} @@ -83,8 +105,8 @@ func FindPkg(path, srcDir string) (filename, id string) { bp, _ := build.Import(path, srcDir, build.FindOnly|build.AllowBinary) if bp.PkgObj == "" { var ok bool - if bp.Goroot { - filename, ok = lookupGorootExport(path, bp.SrcRoot, srcDir) + if bp.Goroot && bp.Dir != "" { + filename, ok = lookupGorootExport(bp.Dir) } if !ok { id = path // make sure we have an id to print in error message diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index d04668e6312..1e1dd358453 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -27,6 +27,7 @@ import ( "testing" "time" + "golang.org/x/tools/internal/goroot" "golang.org/x/tools/internal/testenv" ) @@ -65,8 +66,20 @@ func compilePkg(t *testing.T, dirname, filename, outdirname string, packagefiles } objname := basename + ".o" outname := filepath.Join(outdirname, objname) - importcfgfile := filepath.Join(outdirname, basename) + ".importcfg" - testenv.WriteImportcfg(t, importcfgfile, packagefiles) + + importcfgfile := os.DevNull + if len(packagefiles) > 0 { + importcfgfile = filepath.Join(outdirname, basename) + ".importcfg" + importcfg := new(bytes.Buffer) + fmt.Fprintf(importcfg, "# import config") + for k, v := range packagefiles { + fmt.Fprintf(importcfg, "\npackagefile %s=%s\n", k, v) + } + if err := os.WriteFile(importcfgfile, importcfg.Bytes(), 0655); err != nil { + t.Fatal(err) + } + } + importreldir := strings.ReplaceAll(outdirname, string(os.PathSeparator), "/") cmd := exec.Command("go", "tool", "compile", "-p", pkg, "-D", importreldir, "-importcfg", importcfgfile, "-o", outname, filename) cmd.Dir = dirname @@ -109,7 +122,16 @@ func TestImportTestdata(t *testing.T) { tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) - compile(t, "testdata", testfile, filepath.Join(tmpdir, "testdata"), nil) + packageFiles := map[string]string{} + for _, pkg := range []string{"go/ast", "go/token"} { + export, _ := FindPkg(pkg, "testdata") + if export == "" { + t.Fatalf("no export data found for %s", pkg) + } + packageFiles[pkg] = export + } + + compile(t, "testdata", testfile, filepath.Join(tmpdir, "testdata"), packageFiles) // filename should end with ".go" filename := testfile[:len(testfile)-3] @@ -137,6 +159,10 @@ func TestImportTestdata(t *testing.T) { } func TestImportTypeparamTests(t *testing.T) { + if testing.Short() { + t.Skipf("in short mode, skipping test that requires export data for all of std") + } + testenv.NeedsGo1Point(t, 18) // requires generics // This package only handles gc export data. @@ -191,7 +217,11 @@ func TestImportTypeparamTests(t *testing.T) { // Compile and import, and compare the resulting package with the package // that was type-checked directly. - compile(t, rootDir, entry.Name(), filepath.Join(tmpdir, "testdata"), nil) + pkgFiles, err := goroot.PkgfileMap() + if err != nil { + t.Fatal(err) + } + compile(t, rootDir, entry.Name(), filepath.Join(tmpdir, "testdata"), pkgFiles) pkgName := strings.TrimSuffix(entry.Name(), ".go") imported := importPkg(t, "./testdata/"+pkgName, tmpdir) checked := checkFile(t, filename, src) @@ -589,7 +619,13 @@ func TestIssue13566(t *testing.T) { if err != nil { t.Fatal(err) } - compilePkg(t, "testdata", "a.go", testoutdir, nil, apkg(testoutdir)) + + jsonExport, _ := FindPkg("encoding/json", "testdata") + if jsonExport == "" { + t.Fatalf("no export data found for encoding/json") + } + + compilePkg(t, "testdata", "a.go", testoutdir, map[string]string{"encoding/json": jsonExport}, apkg(testoutdir)) compile(t, testoutdir, bpath, testoutdir, map[string]string{apkg(testoutdir): filepath.Join(testoutdir, "a.o")}) // import must succeed (test for issue at hand) diff --git a/internal/goroot/importcfg.go b/internal/goroot/importcfg.go index 6575cfb9df6..f1cd28e2ec3 100644 --- a/internal/goroot/importcfg.go +++ b/internal/goroot/importcfg.go @@ -29,9 +29,7 @@ func Importcfg() (string, error) { } fmt.Fprintf(&icfg, "# import config") for importPath, export := range m { - if importPath != "unsafe" && export != "" { // unsafe - fmt.Fprintf(&icfg, "\npackagefile %s=%s", importPath, export) - } + fmt.Fprintf(&icfg, "\npackagefile %s=%s", importPath, export) } s := icfg.String() return s, nil @@ -63,7 +61,9 @@ func PkgfileMap() (map[string]string, error) { return } importPath, export := sp[0], sp[1] - m[importPath] = export + if export != "" { + m[importPath] = export + } } stdlibPkgfileMap = m }) From 6002d6ea51c0b879256e7822030f9ad18c9e4a7f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 1 Dec 2022 17:20:25 -0500 Subject: [PATCH 500/723] gopls/internal/regtest/misc: test Implementations + vendor This change adds a test for golang/go#56169, whose cause and fix were the same as for golang/go#55995, which was fixed in CL 454315. Updates golang/go#55995 Fixes golang/go#56169 Change-Id: I6d2cf964be77606de3e945c2e5ecdee82892ee99 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454637 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- .../internal/regtest/misc/definition_test.go | 1 + .../internal/regtest/misc/references_test.go | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index db1cb83bb13..e9e56da58bd 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -292,6 +292,7 @@ func TestGoToCrashingDefinition_Issue49223(t *testing.T) { // TestVendoringInvalidatesMetadata ensures that gopls uses the // correct metadata even after an external 'go mod vendor' command // causes packages to move; see issue #55995. +// See also TestImplementationsInVendor, which tests the same fix. func TestVendoringInvalidatesMetadata(t *testing.T) { testenv.NeedsGo1Point(t, 14) const proxy = ` diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index 2cd4359d313..c4f4c75fe51 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -6,6 +6,7 @@ package misc import ( "fmt" + "os" "sort" "strings" "testing" @@ -13,6 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" ) func TestStdlibReferences(t *testing.T) { @@ -288,3 +290,88 @@ func _() { } }) } + +// This is a regression test for Issue #56169, in which interface +// implementations in vendored modules were not found. The actual fix +// was the same as for #55995; see TestVendoringInvalidatesMetadata. +func TestImplementationsInVendor(t *testing.T) { + testenv.NeedsGo1Point(t, 14) + const proxy = ` +-- other.com/b@v1.0.0/go.mod -- +module other.com/b +go 1.14 + +-- other.com/b@v1.0.0/b.go -- +package b +type B int +func (B) F() {} +` + const src = ` +-- go.mod -- +module example.com/a +go 1.14 +require other.com/b v1.0.0 + +-- go.sum -- +other.com/b v1.0.0 h1:9WyCKS+BLAMRQM0CegP6zqP2beP+ShTbPaARpNY31II= +other.com/b v1.0.0/go.mod h1:TgHQFucl04oGT+vrUm/liAzukYHNxCwKNkQZEyn3m9g= + +-- a.go -- +package a +import "other.com/b" +type I interface { F() } +var _ b.B + +` + WithOptions( + ProxyFiles(proxy), + Modes(Default), // fails in 'experimental' mode + ).Run(t, src, func(t *testing.T, env *Env) { + // Enable to debug go.sum mismatch, which may appear as + // "module lookup disabled by GOPROXY=off", confusingly. + if false { + env.DumpGoSum(".") + } + + checkVendor := func(locs []protocol.Location, wantVendor bool) { + if len(locs) != 1 { + t.Errorf("got %d locations, want 1", len(locs)) + } else if strings.Contains(string(locs[0].URI), "/vendor/") != wantVendor { + t.Errorf("got location %s, wantVendor=%t", locs[0], wantVendor) + } + } + + env.OpenFile("a.go") + refPos := env.RegexpSearch("a.go", "I") // find "I" reference + + // Initially, a.I has one implementation b.B in + // the module cache, not the vendor tree. + checkVendor(env.Implementations("a.go", refPos), false) + + // Run 'go mod vendor' outside the editor. + if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, true); err != nil { + t.Fatalf("go mod vendor: %v", err) + } + + // Synchronize changes to watched files. + env.Await(env.DoneWithChangeWatchedFiles()) + + // Now, b.B is found in the vendor tree. + checkVendor(env.Implementations("a.go", refPos), true) + + // Delete the vendor tree. + if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil { + t.Fatal(err) + } + // Notify the server of the deletion. + if err := env.Sandbox.Workdir.CheckForFileChanges(env.Ctx); err != nil { + t.Fatal(err) + } + + // Synchronize again. + env.Await(env.DoneWithChangeWatchedFiles()) + + // b.B is once again defined in the module cache. + checkVendor(env.Implementations("a.go", refPos), false) + }) +} From 4f69bf3eb3beddcf2b4b1ae17855da12b1c1013a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 2 Dec 2022 13:01:36 -0500 Subject: [PATCH 501/723] gopls/internal/lsp/cache: narrow reloadOrphanedFiles to open files This CL implements a narrower fix for #57053 than completely removing orphaned file reloading, as done in CL 454116. One side-effect of orphaned file reloading that we still depend on is to ensure we at least try to load each open file. Elsewhere in the code (for example, in GetCriticalError) we assume that we've tried loading all open files. For golang/go#57053 Change-Id: I33b77859b3c2bf1437558b64b8262985730a7215 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454559 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Auto-Submit: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/cache/snapshot.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 1ef462207f2..0b561206bff 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1390,6 +1390,10 @@ func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { // ID "command-line-arguments" are usually a symptom of a bad workspace // configuration. // + // This heuristic is path-dependent: we only get command-line-arguments + // packages when we've loaded using file scopes, which only occurs + // on-demand or via orphaned file reloading. + // // TODO(rfindley): re-evaluate this heuristic. if containsCommandLineArguments(wsPkgs) { return s.workspaceLayoutError(ctx) @@ -1492,7 +1496,7 @@ func (s *snapshot) awaitLoadedAllErrors(ctx context.Context) *source.CriticalErr } } - if err := s.reloadOrphanedFiles(ctx); err != nil { + if err := s.reloadOrphanedOpenFiles(ctx); err != nil { diags := s.extractGoCommandErrors(ctx, err) return &source.CriticalError{ MainError: err, @@ -1560,12 +1564,12 @@ func (s *snapshot) reloadWorkspace(ctx context.Context) error { return err } -func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { +func (s *snapshot) reloadOrphanedOpenFiles(ctx context.Context) error { // When we load ./... or a package path directly, we may not get packages // that exist only in overlays. As a workaround, we search all of the files // available in the snapshot and reload their metadata individually using a // file= query if the metadata is unavailable. - files := s.orphanedFiles() + files := s.orphanedOpenFiles() // Files without a valid package declaration can't be loaded. Don't try. var scopes []loadScope @@ -1612,12 +1616,16 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { return nil } -func (s *snapshot) orphanedFiles() []source.VersionedFileHandle { +func (s *snapshot) orphanedOpenFiles() []source.VersionedFileHandle { s.mu.Lock() defer s.mu.Unlock() var files []source.VersionedFileHandle s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { + // Only consider open files, which will be represented as overlays. + if _, isOverlay := fh.(*overlay); !isOverlay { + return + } // Don't try to reload metadata for go.mod files. if s.view.FileKind(fh) != source.Go { return @@ -1627,11 +1635,6 @@ func (s *snapshot) orphanedFiles() []source.VersionedFileHandle { if !source.InDir(s.view.folder.Filename(), uri.Filename()) { return } - // If the file is not open and is in a vendor directory, don't treat it - // like a workspace package. - if _, ok := fh.(*overlay); !ok && inVendor(uri) { - return - } // Don't reload metadata for files we've already deemed unloadable. if _, ok := s.unloadableFiles[uri]; ok { return From 52c7b88fe89b3ef23d2641bca2f18b6ec5882720 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Fri, 2 Dec 2022 13:58:51 -0500 Subject: [PATCH 502/723] gopls/internal/robustio: only define ERROR_SHARING_VIOLATION on Windows The type syscall.Errno is not defined on plan9, so defining a constant of that type in an unconstrained file causes build errors on the plan9 builders. The ERROR_SHARING_VIOLATION constant is only needed for Windows and only makes sense on that platform, so we move it to a Windows-only file. Change-Id: I4fbad994a71f746523557f82c72c08ddd5fbcb2e Reviewed-on: https://go-review.googlesource.com/c/tools/+/454501 Reviewed-by: Robert Findley Run-TryBot: Bryan Mills gopls-CI: kokoro TryBot-Result: Gopher Robot Auto-Submit: Bryan Mills --- gopls/internal/robustio/{gopls.go => gopls_windows.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename gopls/internal/robustio/{gopls.go => gopls_windows.go} (100%) diff --git a/gopls/internal/robustio/gopls.go b/gopls/internal/robustio/gopls_windows.go similarity index 100% rename from gopls/internal/robustio/gopls.go rename to gopls/internal/robustio/gopls_windows.go From 2ad6325d9080398a9a71b69effd71191c00838b4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 2 Dec 2022 16:58:59 -0500 Subject: [PATCH 503/723] gopls/internal/lsp/cache: expand ImportPath!=PackagePath comment Change-Id: I2ea748a6a434bada2a310581538ac5d8dcefa01e Reviewed-on: https://go-review.googlesource.com/c/tools/+/454562 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan --- gopls/internal/lsp/cache/load.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index ccba4e8c3f0..19b21c6bb63 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -552,6 +552,30 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con // (Beware that, for historical reasons, go list uses // the JSON field "ImportPath" for the package's // path--effectively the linker symbol prefix.) + // + // The example above is slightly special to go list + // because it's in the std module. Otherwise, + // vendored modules are simply modules whose directory + // is vendor/ instead of GOMODCACHE, and the + // import path equals the package path. + // + // But in GOPATH (non-module) mode, it's possible for + // package vendoring to cause a non-identity ImportMap, + // as in this example: + // + // $ cd $HOME/src + // $ find . -type f + // ./b/b.go + // ./vendor/example.com/a/a.go + // $ cat ./b/b.go + // package b + // import _ "example.com/a" + // $ cat ./vendor/example.com/a/a.go + // package a + // $ GOPATH=$HOME GO111MODULE=off go list -json ./b | grep -A2 ImportMap + // "ImportMap": { + // "example.com/a": "vendor/example.com/a" + // }, // Don't remember any imports with significant errors. // From bdcd08225250423bf7f5f70d1dad7b2f96c380f0 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Fri, 2 Dec 2022 13:52:51 -0500 Subject: [PATCH 504/723] internal/gcimporter: skip tests earlier when 'go build' is not available This fixes tests failing on the Android builders on Go 1.20 after CL 454499. Previously the tests were skipped in the 'compile' helper function, but as of that CL they fail before reaching that point due to missing export data for packages in std. Updates golang/go#56967. Updates golang/go#47257. Change-Id: Ief953b6dbc54c8e0b1f71fc18a0d6ab212caf308 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454500 gopls-CI: kokoro Reviewed-by: Jamal Carvalho TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills Auto-Submit: Bryan Mills --- internal/gcimporter/gcimporter_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index 1e1dd358453..ab9a4db4690 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -118,6 +118,7 @@ const testfile = "exports.go" func TestImportTestdata(t *testing.T) { needsCompiler(t, "gc") + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) @@ -164,6 +165,7 @@ func TestImportTypeparamTests(t *testing.T) { } testenv.NeedsGo1Point(t, 18) // requires generics + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache // This package only handles gc export data. if runtime.Compiler != "gc" { @@ -601,6 +603,7 @@ func TestCorrectMethodPackage(t *testing.T) { func TestIssue13566(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache // On windows, we have to set the -D option for the compiler to avoid having a drive // letter and an illegal ':' in the import path - just skip it (see also issue #3483). From aa9f4b2f3d575daef809d1fc76c01a33b13e7bce Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 2 Dec 2022 17:05:57 -0500 Subject: [PATCH 505/723] go/analysis: document that facts are gob encoded in one gulp Fixes golang/go#54661 Change-Id: Iddae4d2ab54fc0527e060922351c52eec07c78c0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454564 TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- go/analysis/doc.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go/analysis/doc.go b/go/analysis/doc.go index b5a301c205e..c5429c9e239 100644 --- a/go/analysis/doc.go +++ b/go/analysis/doc.go @@ -244,6 +244,9 @@ if the default encoding is unsuitable. Facts should be stateless. Because serialized facts may appear within build outputs, the gob encoding of a fact must be deterministic, to avoid spurious cache misses in build systems that use content-addressable caches. +The driver makes a single call to the gob encoder for all facts +exported by a given analysis pass, so that the topology of +shared data structures referenced by multiple facts is preserved. The Pass type has functions to import and export facts, associated either with an object or with a package: From bf5db8100143206bf73fe78418bfb95467b4753e Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 2 Dec 2022 17:02:23 -0500 Subject: [PATCH 506/723] gopls/internal/lsp/cache: improve ad-hoc warning for nested modules Our warning for the specific (and somewhat arbitrary) case of opening a nested module was stale. Update it for workspaces. This is a very weak distillation of CL 448695, targeted at improving this one error message. Change-Id: Ic78e2f8ff8004a78ac2a0650b40767de9dfe153c Reviewed-on: https://go-review.googlesource.com/c/tools/+/454563 Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/load.go | 72 +++++++++++++++++----------- gopls/internal/lsp/cache/mod_tidy.go | 3 ++ gopls/internal/lsp/cache/snapshot.go | 24 +++++++++- gopls/internal/lsp/cache/view.go | 34 ++++++++----- gopls/internal/lsp/diagnostics.go | 3 ++ gopls/internal/lsp/source/view.go | 2 + 6 files changed, 95 insertions(+), 43 deletions(-) diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 19b21c6bb63..f79109a31d1 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -275,26 +275,34 @@ func (m *moduleErrorMap) Error() string { return buf.String() } -// workspaceLayoutErrors returns a diagnostic for every open file, as well as -// an error message if there are no open files. -func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError { +// workspaceLayoutErrors returns an error decribing a misconfiguration of the +// workspace, along with related diagnostic. +// +// The unusual argument ordering of results is intentional: if the resulting +// error is nil, so must be the resulting diagnostics. +// +// If ctx is cancelled, it may return ctx.Err(), nil. +// +// TODO(rfindley): separate workspace diagnostics from critical workspace +// errors. +func (s *snapshot) workspaceLayoutError(ctx context.Context) (error, []*source.Diagnostic) { // TODO(rfindley): do we really not want to show a critical error if the user // has no go.mod files? if len(s.workspace.getKnownModFiles()) == 0 { - return nil + return nil, nil } // TODO(rfindley): both of the checks below should be delegated to the workspace. if s.view.effectiveGO111MODULE() == off { - return nil + return nil, nil } if s.workspace.moduleSource != legacyWorkspace { - return nil + return nil, nil } // If the user has one module per view, there is nothing to warn about. if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 { - return nil + return nil, nil } // Apply diagnostics about the workspace configuration to relevant open @@ -320,10 +328,7 @@ go workspaces (go.work files). See the documentation for more information on setting up your workspace: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` } - return &source.CriticalError{ - MainError: fmt.Errorf(msg), - Diagnostics: s.applyCriticalErrorToFiles(ctx, msg, openFiles), - } + return fmt.Errorf(msg), s.applyCriticalErrorToFiles(ctx, msg, openFiles) } // If the user has one active go.mod file, they may still be editing files @@ -331,40 +336,49 @@ https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` // that the nested module must be opened as a workspace folder. if len(s.workspace.ActiveModFiles()) == 1 { // Get the active root go.mod file to compare against. - var rootModURI span.URI + var rootMod string for uri := range s.workspace.ActiveModFiles() { - rootModURI = uri + rootMod = uri.Filename() } - nestedModules := map[string][]source.VersionedFileHandle{} + rootDir := filepath.Dir(rootMod) + nestedModules := make(map[string][]source.VersionedFileHandle) for _, fh := range openFiles { - modURI := moduleForURI(s.workspace.knownModFiles, fh.URI()) - if modURI != rootModURI { - modDir := filepath.Dir(modURI.Filename()) + mod, err := findRootPattern(ctx, filepath.Dir(fh.URI().Filename()), "go.mod", s) + if err != nil { + if ctx.Err() != nil { + return ctx.Err(), nil + } + continue + } + if mod == "" { + continue + } + if mod != rootMod && source.InDir(rootDir, mod) { + modDir := filepath.Dir(mod) nestedModules[modDir] = append(nestedModules[modDir], fh) } } + var multiModuleMsg string + if s.view.goversion >= 18 { + multiModuleMsg = `To work on multiple modules at once, please use a go.work file. +See https://github.com/golang/tools/blob/master/gopls/doc/workspace.md for more information on using workspaces.` + } else { + multiModuleMsg = `To work on multiple modules at once, please upgrade to Go 1.18 and use a go.work file. +See https://github.com/golang/tools/blob/master/gopls/doc/workspace.md for more information on using workspaces.` + } // Add a diagnostic to each file in a nested module to mark it as // "orphaned". Don't show a general diagnostic in the progress bar, // because the user may still want to edit a file in a nested module. var srcDiags []*source.Diagnostic for modDir, uris := range nestedModules { - msg := fmt.Sprintf(`This file is in %s, which is a nested module in the %s module. -gopls currently requires one module per workspace folder. -Please open %s as a separate workspace folder. -You can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md. -`, modDir, filepath.Dir(rootModURI.Filename()), modDir) + msg := fmt.Sprintf("This file is in %s, which is a nested module in the %s module.\n%s", modDir, rootMod, multiModuleMsg) srcDiags = append(srcDiags, s.applyCriticalErrorToFiles(ctx, msg, uris)...) } if len(srcDiags) != 0 { - return &source.CriticalError{ - MainError: fmt.Errorf(`You are working in a nested module. -Please open it as a separate workspace folder. Learn more: -https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`), - Diagnostics: srcDiags, - } + return fmt.Errorf("You have opened a nested module.\n%s", multiModuleMsg), srcDiags } } - return nil + return nil, nil } func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.VersionedFileHandle) []*source.Diagnostic { diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index f4332119c80..0a4b88e7a4d 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -63,6 +63,9 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc Diagnostics: criticalErr.Diagnostics, }, nil } + if ctx.Err() != nil { // must check ctx after GetCriticalError + return nil, ctx.Err() + } if err := s.awaitLoaded(ctx); err != nil { return nil, err diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 0b561206bff..965065558af 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1212,6 +1212,8 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[PackagePath]sourc return results, nil } +// TODO(rfindley): clarify that this is only active modules. Or update to just +// use findRootPattern. func (s *snapshot) GoModForFile(uri span.URI) span.URI { return moduleForURI(s.workspace.activeModFiles, uri) } @@ -1396,13 +1398,31 @@ func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { // // TODO(rfindley): re-evaluate this heuristic. if containsCommandLineArguments(wsPkgs) { - return s.workspaceLayoutError(ctx) + err, diags := s.workspaceLayoutError(ctx) + if err != nil { + if ctx.Err() != nil { + return nil // see the API documentation for source.Snapshot + } + return &source.CriticalError{ + MainError: err, + Diagnostics: diags, + } + } } return nil } if errMsg := loadErr.MainError.Error(); strings.Contains(errMsg, "cannot find main module") || strings.Contains(errMsg, "go.mod file not found") { - return s.workspaceLayoutError(ctx) + err, diags := s.workspaceLayoutError(ctx) + if err != nil { + if ctx.Err() != nil { + return nil // see the API documentation for source.Snapshot + } + return &source.CriticalError{ + MainError: err, + Diagnostics: diags, + } + } } return loadErr } diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index bc623ff16f3..4fad654f243 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -859,33 +859,38 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, // Otherwise, it returns folder. // TODO (rFindley): move this to workspace.go // TODO (rFindley): simplify this once workspace modules are enabled by default. -func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSource, excludePath func(string) bool, experimental bool) (span.URI, error) { +func findWorkspaceRoot(ctx context.Context, folderURI span.URI, fs source.FileSource, excludePath func(string) bool, experimental bool) (span.URI, error) { patterns := []string{"go.work", "go.mod"} if experimental { patterns = []string{"go.work", "gopls.mod", "go.mod"} } + folder := folderURI.Filename() for _, basename := range patterns { - dir, err := findRootPattern(ctx, folder, basename, fs) + match, err := findRootPattern(ctx, folder, basename, fs) if err != nil { - return "", fmt.Errorf("finding %s: %w", basename, err) + if ctxErr := ctx.Err(); ctxErr != nil { + return "", ctxErr + } + return "", err } - if dir != "" { + if match != "" { + dir := span.URIFromPath(filepath.Dir(match)) return dir, nil } } // The experimental workspace can handle nested modules at this point... if experimental { - return folder, nil + return folderURI, nil } // ...else we should check if there's exactly one nested module. - all, err := findModules(folder, excludePath, 2) + all, err := findModules(folderURI, excludePath, 2) if err == errExhausted { // Fall-back behavior: if we don't find any modules after searching 10000 // files, assume there are none. event.Log(ctx, fmt.Sprintf("stopped searching for modules after %d files", fileLimit)) - return folder, nil + return folderURI, nil } if err != nil { return "", err @@ -896,19 +901,24 @@ func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSourc return dirURI(uri), nil } } - return folder, nil + return folderURI, nil } -func findRootPattern(ctx context.Context, folder span.URI, basename string, fs source.FileSource) (span.URI, error) { - dir := folder.Filename() +// findRootPattern looks for files with the given basename in dir or any parent +// directory of dir, using the provided FileSource. It returns the first match, +// starting from dir and search parents. +// +// The resulting string is either the file path of a matching file with the +// given basename, or "" if none was found. +func findRootPattern(ctx context.Context, dir, basename string, fs source.FileSource) (string, error) { for dir != "" { target := filepath.Join(dir, basename) exists, err := fileExists(ctx, span.URIFromPath(target), fs) if err != nil { - return "", err + return "", err // not readable or context cancelled } if exists { - return span.URIFromPath(dir), nil + return target, nil } // Trailing separators must be trimmed, otherwise filepath.Split is a noop. next, _ := filepath.Split(strings.TrimRight(dir, string(filepath.Separator))) diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 19bc3ce124e..6b83cf862ca 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -295,6 +295,9 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn return } criticalErr := snapshot.GetCriticalError(ctx) + if ctx.Err() != nil { // must check ctx after GetCriticalError + return + } // Show the error as a progress error report so that it appears in the // status bar. If a client doesn't support progress reports, the error diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index fa55cdeecd5..e82a89ad27f 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -214,6 +214,8 @@ type Snapshot interface { MetadataForFile(ctx context.Context, uri span.URI) ([]*Metadata, error) // GetCriticalError returns any critical errors in the workspace. + // + // A nil result may mean success, or context cancellation. GetCriticalError(ctx context.Context) *CriticalError // BuildGoplsMod generates a go.mod file for all modules in the workspace. From c9ea9a72c54987217e6af00e7a6a29f26a316d0a Mon Sep 17 00:00:00 2001 From: Dung Le Date: Tue, 15 Nov 2022 03:31:39 -0500 Subject: [PATCH 507/723] gopls/internal/regtest: add a test for the case when the renaming package's path contains "internal" as a segment For golang/go#56184. Change-Id: Id33650adb5befb0a2cdead28425ad6ec8f7d9e0d Reviewed-on: https://go-review.googlesource.com/c/tools/+/450556 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/regtest/misc/rename_test.go | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index a9fd920317f..a772770562e 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -865,6 +865,65 @@ const A = 1 + nested.B }) } +func TestRenamePackage_InternalPackage(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +import ( + "fmt" + "mod.com/lib/internal/x" +) + +const A = 1 + +func print() { + fmt.Println(x.B) +} + +-- lib/internal/x/a.go -- +package x + +const B = 1 + +-- main.go -- +package main + +import "mod.com/lib" + +func main() { + lib.print() +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/internal/x/a.go") + pos := env.RegexpSearch("lib/internal/x/a.go", "x") + env.Rename("lib/internal/x/a.go", pos, "utils") + + // Check if the new package name exists. + env.RegexpSearch("lib/a.go", "mod.com/lib/internal/utils") + env.RegexpSearch("lib/a.go", "utils.B") + + // Check if the test package is renamed + env.RegexpSearch("lib/internal/utils/a.go", "package utils") + + env.OpenFile("lib/a.go") + pos = env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "lib1") + + // Check if the new package name exists. + env.RegexpSearch("lib1/a.go", "package lib1") + env.RegexpSearch("lib1/a.go", "mod.com/lib1/internal/utils") + env.RegexpSearch("main.go", `import "mod.com/lib1"`) + env.RegexpSearch("main.go", "lib1.print()") + }) +} + // checkTestdata checks that current buffer contents match their corresponding // expected content in the testdata directory. func checkTestdata(t *testing.T, env *Env) { From fe60148df7654055de5513e31850d131fd91c1a5 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Tue, 6 Dec 2022 04:22:04 +0000 Subject: [PATCH 508/723] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Once this CL is submitted, and post-submit testing succeeds on all first-class ports across all supported Go versions, this repository will be tagged with its next minor version. Change-Id: I4009ac1fde369a228a4cb5a64718a300be1620ff Reviewed-on: https://go-review.googlesource.com/c/tools/+/455437 Reviewed-by: Heschi Kreinick Reviewed-by: Carlos Amedee gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Gopher Robot Auto-Submit: Gopher Robot --- go.mod | 4 ++-- go.sum | 12 ++++++------ gopls/go.mod | 4 ++-- gopls/go.sum | 11 ++++++----- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index b46da5396a5..72bd7ea2ec8 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.18 // tagx:compat 1.16 require ( github.com/yuin/goldmark v1.4.13 golang.org/x/mod v0.7.0 - golang.org/x/net v0.2.0 - golang.org/x/sys v0.2.0 + golang.org/x/net v0.3.0 + golang.org/x/sys v0.3.0 ) require golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index 92f0a74888d..7bb017175e0 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -19,15 +19,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/gopls/go.mod b/gopls/go.mod index a36bccdea18..e5c35a6abea 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -9,8 +9,8 @@ require ( github.com/sergi/go-diff v1.1.0 golang.org/x/mod v0.7.0 golang.org/x/sync v0.1.0 - golang.org/x/sys v0.2.0 - golang.org/x/text v0.4.0 + golang.org/x/sys v0.3.0 + golang.org/x/text v0.5.0 golang.org/x/tools v0.2.1-0.20221108172846-9474ca31d0df golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe gopkg.in/yaml.v3 v3.0.1 diff --git a/gopls/go.sum b/gopls/go.sum index ccc7756fb9c..0e4b9523d60 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -59,7 +59,7 @@ golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -71,14 +71,15 @@ golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe h1:qptQiQwEpETwDiz85LKtChqif9xhVkAm8Nhxs0xnTww= golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe/go.mod h1:8nFLBv8KFyZ2VuczUYssYKh+fcBR3BuXDG/HIWcxlwM= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From aee3994bd5f840a71b7b3fd8ce9fa21176e0a9e1 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Tue, 6 Dec 2022 09:24:35 -0500 Subject: [PATCH 509/723] gopls/internal/lsp/fake: in (*Workdir).RenameFile, fall back to read + write os.Rename cannot portably rename files across directories; notably, that operation fails with "invalid argument" on some Plan 9 filesystems. Fixes golang/go#57111. Change-Id: Ifd108bb58fa968fc2c8a21b99211a904d6d3d4e6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/455515 Run-TryBot: Bryan Mills Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Auto-Submit: Bryan Mills --- gopls/internal/lsp/fake/workdir.go | 81 ++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index 2b426e40c3b..4c8aa127d9a 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -330,12 +330,87 @@ func (w *Workdir) fileEvent(path string, changeType protocol.FileChangeType) Fil // RenameFile performs an on disk-renaming of the workdir-relative oldPath to // workdir-relative newPath. +// +// oldPath must either be a regular file or in the same directory as newPath. func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error { oldAbs := w.AbsPath(oldPath) newAbs := w.AbsPath(newPath) - if err := robustio.Rename(oldAbs, newAbs); err != nil { - return err + w.fileMu.Lock() + defer w.fileMu.Unlock() + + // For os.Rename, “OS-specific restrictions may apply when oldpath and newpath + // are in different directories.” If that applies here, we may fall back to + // ReadFile, WriteFile, and RemoveFile to perform the rename non-atomically. + // + // However, the fallback path only works for regular files: renaming a + // directory would be much more complex and isn't needed for our tests. + fallbackOk := false + if filepath.Dir(oldAbs) != filepath.Dir(newAbs) { + fi, err := os.Stat(oldAbs) + if err == nil && !fi.Mode().IsRegular() { + return &os.PathError{ + Op: "RenameFile", + Path: oldPath, + Err: fmt.Errorf("%w: file is not regular and not in the same directory as %s", os.ErrInvalid, newPath), + } + } + fallbackOk = true + } + + var renameErr error + const debugFallback = false + if fallbackOk && debugFallback { + renameErr = fmt.Errorf("%w: debugging fallback path", os.ErrInvalid) + } else { + renameErr = robustio.Rename(oldAbs, newAbs) + } + if renameErr != nil { + if !fallbackOk { + return renameErr // The OS-specific Rename restrictions do not apply. + } + + content, err := w.ReadFile(oldPath) + if err != nil { + // If we can't even read the file, the error from Rename may be accurate. + return renameErr + } + fi, err := os.Stat(newAbs) + if err == nil { + if fi.IsDir() { + // “If newpath already exists and is not a directory, Rename replaces it.” + // But if it is a directory, maybe not? + return renameErr + } + // On most platforms, Rename replaces the named file with a new file, + // rather than overwriting the existing file it in place. Mimic that + // behavior here. + if err := robustio.RemoveAll(newAbs); err != nil { + // Maybe we don't have permission to replace newPath? + return renameErr + } + } else if !os.IsNotExist(err) { + // If the destination path already exists or there is some problem with it, + // the error from Rename may be accurate. + return renameErr + } + if writeErr := WriteFileData(newPath, []byte(content), w.RelativeTo); writeErr != nil { + // At this point we have tried to actually write the file. + // If it still doesn't exist, assume that the error from Rename was accurate: + // for example, maybe we don't have permission to create the new path. + // Otherwise, return the error from the write, which may indicate some + // other problem (such as a full disk). + if _, statErr := os.Stat(newAbs); !os.IsNotExist(statErr) { + return writeErr + } + return renameErr + } + if err := robustio.RemoveAll(oldAbs); err != nil { + // If we failed to remove the old file, that may explain the Rename error too. + // Make a best effort to back out the write to the new path. + robustio.RemoveAll(newAbs) + return renameErr + } } // Send synthetic file events for the renaming. Renamed files are handled as @@ -346,7 +421,7 @@ func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error w.fileEvent(newPath, protocol.Created), } w.sendEvents(ctx, events) - + delete(w.files, oldPath) return nil } From 6b505011c34b18863129095dd233c8e59496afad Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 5 Dec 2022 16:25:31 -0500 Subject: [PATCH 510/723] gopls/release: fix the release script when go.work is not used Run go/packages.Load from the gopls module. Also improve error handling so that we would have gotten more useful output in the failure mode of golang/go#57094. Fixes golang/go#57094 Change-Id: Ida57b37440ea280370979e20905b728e093123ea Reviewed-on: https://go-review.googlesource.com/c/tools/+/455167 TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/release/release.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/gopls/release/release.go b/gopls/release/release.go index fb456b6aa0e..e78a2674710 100644 --- a/gopls/release/release.go +++ b/gopls/release/release.go @@ -15,7 +15,6 @@ import ( "flag" "fmt" "go/types" - exec "golang.org/x/sys/execabs" "io/ioutil" "log" "os" @@ -24,6 +23,8 @@ import ( "strconv" "strings" + exec "golang.org/x/sys/execabs" + "golang.org/x/mod/modfile" "golang.org/x/mod/semver" "golang.org/x/tools/go/packages" @@ -71,7 +72,7 @@ func main() { log.Fatal(err) } // Confirm that they have updated the hardcoded version. - if err := validateHardcodedVersion(wd, *versionFlag); err != nil { + if err := validateHardcodedVersion(*versionFlag); err != nil { log.Fatal(err) } // Confirm that the versions in the go.mod file are correct. @@ -129,13 +130,13 @@ func validateBranchName(version string) error { // validateHardcodedVersion reports whether the version hardcoded in the gopls // binary is equivalent to the version being published. It reports an error if // not. -func validateHardcodedVersion(wd string, version string) error { +func validateHardcodedVersion(version string) error { + const debugPkg = "golang.org/x/tools/gopls/internal/lsp/debug" pkgs, err := packages.Load(&packages.Config{ - Dir: filepath.Dir(wd), Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes, - }, "golang.org/x/tools/gopls/internal/lsp/debug") + }, debugPkg) if err != nil { return err } @@ -143,6 +144,9 @@ func validateHardcodedVersion(wd string, version string) error { return fmt.Errorf("expected 1 package, got %v", len(pkgs)) } pkg := pkgs[0] + if len(pkg.Errors) > 0 { + return fmt.Errorf("failed to load %q: first error: %w", debugPkg, pkg.Errors[0]) + } obj := pkg.Types.Scope().Lookup("Version") c, ok := obj.(*types.Const) if !ok { @@ -164,8 +168,8 @@ func validateHardcodedVersion(wd string, version string) error { return nil } -func validateGoModFile(wd string) error { - filename := filepath.Join(wd, "go.mod") +func validateGoModFile(goplsDir string) error { + filename := filepath.Join(goplsDir, "go.mod") data, err := ioutil.ReadFile(filename) if err != nil { return err From 1dcc423c1279d4335d9e29f9b85767de5addf83d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 6 Dec 2022 13:27:13 -0500 Subject: [PATCH 511/723] gopls/internal/lsp/cache: split metadata and loading in PackagesForFile This change factors PackagesForFile{,s} so that they query the metadata, perform filtering, then load the remaining packages, rather than load all the packages before filtering. This is another small step towards operating on metadata instead of fully type-checked packages where possible. Change-Id: I4c67c93df8012a8a7233703c7fac04991502dc62 Reviewed-on: https://go-review.googlesource.com/c/tools/+/455615 gopls-CI: kokoro Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/snapshot.go | 112 ++++++++++++++++----------- gopls/internal/lsp/cache/view.go | 4 + gopls/internal/lsp/command.go | 1 + gopls/internal/lsp/diagnostics.go | 1 + gopls/internal/lsp/source/view.go | 13 +++- 5 files changed, 82 insertions(+), 49 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 965065558af..28d6fff135c 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -637,99 +637,111 @@ func (s *snapshot) buildOverlay() map[string][]byte { func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]source.Package, error) { ctx = event.Label(ctx, tag.URI.Of(uri)) - phs, err := s.packageHandlesForFile(ctx, uri, mode, includeTestVariants) + metas, err := s.containingPackages(ctx, uri) if err != nil { return nil, err } - var pkgs []source.Package - for _, ph := range phs { - pkg, err := ph.await(ctx, s) - if err != nil { - return nil, err + + var ids []PackageID + for _, m := range metas { + // Filter out any intermediate test variants. + // We typically aren't interested in these + // packages for file= style queries. + if m.IsIntermediateTestVariant() && !includeTestVariants { + continue } - pkgs = append(pkgs, pkg) + ids = append(ids, m.ID) } - return pkgs, nil + + return s.loadPackages(ctx, mode, ids...) } func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, pkgPolicy source.PackageFilter) (source.Package, error) { ctx = event.Label(ctx, tag.URI.Of(uri)) - - // TODO(adonovan): opt: apply pkgPolicy to the list of - // Metadatas before initiating loading of any package. - phs, err := s.packageHandlesForFile(ctx, uri, mode, false) + metas, err := s.containingPackages(ctx, uri) if err != nil { return nil, err } - - if len(phs) < 1 { - return nil, fmt.Errorf("no packages") + if len(metas) == 0 { + return nil, fmt.Errorf("no package metadata for file %s", uri) } - - ph := phs[0] - for _, handle := range phs[1:] { - switch pkgPolicy { - case source.WidestPackage: - if ph == nil || len(handle.CompiledGoFiles()) > len(ph.CompiledGoFiles()) { - ph = handle - } - case source.NarrowestPackage: - if ph == nil || len(handle.CompiledGoFiles()) < len(ph.CompiledGoFiles()) { - ph = handle - } - } + switch pkgPolicy { + case source.NarrowestPackage: + metas = metas[:1] + case source.WidestPackage: + metas = metas[len(metas)-1:] } - if ph == nil { - return nil, fmt.Errorf("no packages in input") + pkgs, err := s.loadPackages(ctx, mode, metas[0].ID) + if err != nil { + return nil, err } - - return ph.await(ctx, s) + return pkgs[0], err } -func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, withIntermediateTestVariants bool) ([]*packageHandle, error) { +// containingPackages returns a new slice of metadata records for +// packages that contain the Go file specified by uri. The results are +// ordered "narrowest" to "widest". +func (s *snapshot) containingPackages(ctx context.Context, uri span.URI) ([]*source.Metadata, error) { // TODO(rfindley): why can't/shouldn't we awaitLoaded here? It seems that if - // we ask for package handles for a file, we should wait for pending loads. + // the caller of this function is going to + // ask for package handles for a file, we should wait for pending loads. // Else we will reload orphaned files before the initial load completes. // Check if we should reload metadata for the file. We don't invalidate IDs // (though we should), so the IDs will be a better source of truth than the // metadata. If there are no IDs for the file, then we should also reload. + // + // TODO(adonovan): I can't relate the GetFile call to the + // previous comment, but tests rely on its effects. + // It does more than provide fh to the FileKind check. But what? fh, err := s.GetFile(ctx, uri) if err != nil { return nil, err } + + // Check that the file kind is Go (not, say, C or assembly). + // (This check is perhaps superfluous since the go/packages "file=" + // metadata query in MetadataForFile requires .go files files.) if kind := s.view.FileKind(fh); kind != source.Go { return nil, fmt.Errorf("no packages for non-Go file %s (%v)", uri, kind) } - metas, err := s.MetadataForFile(ctx, uri) + + metas, err := s.MetadataForFile(ctx, uri) // ordered narrowest to widest if err != nil { return nil, err } + return metas, err +} +// loadPackages type-checks the specified packages in the given mode. +func (s *snapshot) loadPackages(ctx context.Context, mode source.TypecheckMode, ids ...PackageID) ([]source.Package, error) { + // Build all the handles... var phs []*packageHandle - for _, m := range metas { - // Filter out any intermediate test variants. We typically aren't - // interested in these packages for file= style queries. - if m.IsIntermediateTestVariant() && !withIntermediateTestVariants { - continue - } + for _, id := range ids { parseMode := source.ParseFull if mode == source.TypecheckWorkspace { - parseMode = s.workspaceParseMode(m.ID) + parseMode = s.workspaceParseMode(id) } - ph, err := s.buildPackageHandle(ctx, m.ID, parseMode) + ph, err := s.buildPackageHandle(ctx, id, parseMode) if err != nil { return nil, err } phs = append(phs, ph) } - return phs, nil + + // ...then await them all. + var pkgs []source.Package + for _, ph := range phs { + pkg, err := ph.await(ctx, s) + if err != nil { + return nil, err + } + pkgs = append(pkgs, pkg) + } + return pkgs, nil } -// MetadataForFile returns the Metadata for each package associated -// with the file uri. If no such package exists or if they are known -// to be stale, it reloads metadata for the file. func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) { s.mu.Lock() @@ -796,6 +808,12 @@ func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source if !unloadable && len(ids) == 0 { s.unloadableFiles[uri] = struct{}{} } + + // Sort packages "narrowed" to "widest" (in practice: non-tests before tests). + sort.Slice(metas, func(i, j int) bool { + return len(metas[i].CompiledGoFiles) < len(metas[j].CompiledGoFiles) + }) + return metas, nil } diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 4fad654f243..0f4272ef87a 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -275,11 +275,15 @@ func (v *View) Options() *source.Options { } func (v *View) FileKind(fh source.FileHandle) source.FileKind { + // The kind of an unsaved buffer comes from the + // TextDocumentItem.LanguageID field in the didChange event, + // not from the file name. They may differ. if o, ok := fh.(source.Overlay); ok { if o.Kind() != source.UnknownKind { return o.Kind() } } + fext := filepath.Ext(fh.URI().Filename()) switch fext { case ".go": diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 01dc4918573..e09925a40e3 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -426,6 +426,7 @@ func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, work *progress.WorkDone, uri protocol.DocumentURI, tests, benchmarks []string) error { // TODO: fix the error reporting when this runs async. + // TODO(adonovan): opt: request only metadata, not type checking. pkgs, err := snapshot.PackagesForFile(ctx, uri.SpanURI(), source.TypecheckWorkspace, false) if err != nil { return err diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 6b83cf862ca..e3fef660780 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -541,6 +541,7 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps if snapshot.IsBuiltin(ctx, fh.URI()) { return nil } + // TODO(rfindley): opt: request metadata, not type-checking. pkgs, _ := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace, false) if len(pkgs) > 0 { return nil diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index e82a89ad27f..52d99789144 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -210,7 +210,10 @@ type Snapshot interface { // Symbols returns all symbols in the snapshot. Symbols(ctx context.Context) map[span.URI][]Symbol - // Metadata returns metadata for each package associated with the given file URI. + // Metadata returns a new slice containing metadata for each + // package containing the Go file identified by uri, ordered by the + // number of CompiledGoFiles (i.e. "narrowest" to "widest" package). + // It returns an error if the context was cancelled. MetadataForFile(ctx context.Context, uri span.URI) ([]*Metadata, error) // GetCriticalError returns any critical errors in the workspace. @@ -317,7 +320,13 @@ type View interface { // required by modfile. SetVulnerabilities(modfile span.URI, vulncheckResult *govulncheck.Result) - // FileKind returns the type of a file + // FileKind returns the type of a file. + // + // We can't reliably deduce the kind from the file name alone, + // as some editors can be told to interpret a buffer as + // language different from the file name heuristic, e.g. that + // an .html file actually contains Go "html/template" syntax, + // or even that a .go file contains Python. FileKind(FileHandle) FileKind // GoVersion returns the configured Go version for this view. From 09ae2d5afea85a6939c6bf50e456d33c51846913 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 18 Nov 2022 15:44:00 -0500 Subject: [PATCH 512/723] gopls/internal/lsp/source: KnownPackagePaths: avoid loading This operation is logically just about metadata. This change makes it avoid package loading. (This is a warm-up exercise for more invasive variants in many other places.) Change-Id: I408c754f10665d3959ee0f4ff6cf95c54ec94f3e Reviewed-on: https://go-review.googlesource.com/c/tools/+/452056 Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/check.go | 2 +- gopls/internal/lsp/cache/mod_tidy.go | 2 +- gopls/internal/lsp/cache/snapshot.go | 3 +- gopls/internal/lsp/source/known_packages.go | 125 ++++++++++++-------- gopls/internal/lsp/source/view.go | 9 +- 5 files changed, 84 insertions(+), 57 deletions(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 663cf25b184..8adec22a17d 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -721,7 +721,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost for _, depErr := range relevantErrors { for i := len(depErr.ImportStack) - 1; i >= 0; i-- { item := depErr.ImportStack[i] - m := s.getMetadata(PackageID(item)) + m := s.Metadata(PackageID(item)) if m == nil || m.Module == nil { continue } diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index 0a4b88e7a4d..9495f3fa3cf 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -193,7 +193,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.Pars // workspace. // TODO(adonovan): opt: opportunities for parallelism abound. for _, id := range snapshot.workspacePackageIDs() { - m := snapshot.getMetadata(id) + m := snapshot.Metadata(id) if m == nil { return nil, fmt.Errorf("no metadata for %s", id) } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 28d6fff135c..8b02dea585b 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1249,10 +1249,9 @@ func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { return match } -func (s *snapshot) getMetadata(id PackageID) *source.Metadata { +func (s *snapshot) Metadata(id PackageID) *source.Metadata { s.mu.Lock() defer s.mu.Unlock() - return s.meta.metadata[id] } diff --git a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go index 6f3bf8c9028..19abf8f6578 100644 --- a/gopls/internal/lsp/source/known_packages.go +++ b/gopls/internal/lsp/source/known_packages.go @@ -7,6 +7,8 @@ package source import ( "context" "fmt" + "go/parser" + "go/token" "sort" "strings" "sync" @@ -20,78 +22,96 @@ import ( // packages in the package graph that could potentially be imported by // the given file. The list is ordered lexicographically, except that // all dot-free paths (standard packages) appear before dotful ones. +// +// It is part of the gopls.list_known_packages command. func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle) ([]PackagePath, error) { - // TODO(adonovan): this whole algorithm could be more - // simply expressed in terms of Metadata, not Packages. - // All we need below is: - // - for fh: Metadata.{DepsByPkgPath,Path,Name} - // - for all cached packages: Metadata.{Path,Name,ForTest,DepsByPkgPath}. - pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + // This algorithm is expressed in terms of Metadata, not Packages, + // so it doesn't cause or wait for type checking. + + // Find a Metadata containing the file. + metas, err := snapshot.MetadataForFile(ctx, fh.URI()) if err != nil { - return nil, fmt.Errorf("GetParsedFile: %w", err) + return nil, err // e.g. context cancelled } - alreadyImported := map[PackagePath]struct{}{} - for _, imp := range pkg.Imports() { - alreadyImported[imp.PkgPath()] = struct{}{} + if len(metas) == 0 { + return nil, fmt.Errorf("no loaded package contain file %s", fh.URI()) } - pkgs, err := snapshot.CachedImportPaths(ctx) + current := metas[0] // pick one arbitrarily (they should all have the same package path) + + // Parse the file's imports so we can compute which + // PackagePaths are imported by this specific file. + src, err := fh.Read() if err != nil { return nil, err } - var ( - seen = make(map[PackagePath]struct{}) - paths []PackagePath - ) - for path, knownPkg := range pkgs { + file, err := parser.ParseFile(token.NewFileSet(), fh.URI().Filename(), src, parser.ImportsOnly) + if err != nil { + return nil, err + } + imported := make(map[PackagePath]bool) + for _, imp := range file.Imports { + if id := current.DepsByImpPath[UnquoteImportPath(imp)]; id != "" { + if m := snapshot.Metadata(id); m != nil { + imported[m.PkgPath] = true + } + } + } + + // Now find candidates among known packages. + knownPkgs, err := snapshot.AllValidMetadata(ctx) + if err != nil { + return nil, err + } + seen := make(map[PackagePath]bool) + for _, knownPkg := range knownPkgs { // package main cannot be imported - if knownPkg.Name() == "main" { + if knownPkg.Name == "main" { continue } // test packages cannot be imported - if knownPkg.ForTest() != "" { - continue - } - // no need to import what the file already imports - if _, ok := alreadyImported[path]; ok { + if knownPkg.ForTest != "" { continue } - // snapshot.CachedImportPaths could have multiple versions of a pkg - if _, ok := seen[path]; ok { + // No need to import what the file already imports. + // This check is based on PackagePath, not PackageID, + // so that all test variants are filtered out too. + if imported[knownPkg.PkgPath] { continue } - seen[path] = struct{}{} // make sure internal packages are importable by the file - if !IsValidImport(pkg.PkgPath(), path) { + if !IsValidImport(current.PkgPath, knownPkg.PkgPath) { continue } // naive check on cyclical imports - if isDirectlyCyclical(pkg, knownPkg) { + if isDirectlyCyclical(current, knownPkg) { continue } - paths = append(paths, path) - seen[path] = struct{}{} + // AllValidMetadata may have multiple variants of a pkg. + seen[knownPkg.PkgPath] = true } - err = snapshot.RunProcessEnvFunc(ctx, func(o *imports.Options) error { - var mu sync.Mutex + + // Augment the set by invoking the goimports algorithm. + if err := snapshot.RunProcessEnvFunc(ctx, func(o *imports.Options) error { ctx, cancel := context.WithTimeout(ctx, time.Millisecond*80) defer cancel() - return imports.GetAllCandidates(ctx, func(ifix imports.ImportFix) { - mu.Lock() - defer mu.Unlock() + var seenMu sync.Mutex + wrapped := func(ifix imports.ImportFix) { + seenMu.Lock() + defer seenMu.Unlock() // TODO(adonovan): what if the actual package path has a vendor/ prefix? - path := PackagePath(ifix.StmtInfo.ImportPath) - if _, ok := seen[path]; ok { - return - } - paths = append(paths, path) - seen[path] = struct{}{} - }, "", pgf.URI.Filename(), string(pkg.Name()), o.Env) - }) - if err != nil { - // if an error occurred, we still have a decent list we can - // show to the user through snapshot.CachedImportPaths + seen[PackagePath(ifix.StmtInfo.ImportPath)] = true + } + return imports.GetAllCandidates(ctx, wrapped, "", fh.URI().Filename(), string(current.Name), o.Env) + }); err != nil { + // If goimports failed, proceed with just the candidates from the metadata. event.Error(ctx, "imports.GetAllCandidates", err) } + + // Sort lexicographically, but with std before non-std packages. + paths := make([]PackagePath, 0, len(seen)) + for path := range seen { + paths = append(paths, path) + } sort.Slice(paths, func(i, j int) bool { importI, importJ := paths[i], paths[j] iHasDot := strings.Contains(string(importI), ".") @@ -101,6 +121,7 @@ func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh VersionedFileH } return importI < importJ }) + return paths, nil } @@ -109,11 +130,11 @@ func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh VersionedFileH // a list of importable packages already generates a very large list // and having a few false positives in there could be worth the // performance snappiness. -func isDirectlyCyclical(pkg, imported Package) bool { - for _, imp := range imported.Imports() { - if imp.PkgPath() == pkg.PkgPath() { - return true - } - } - return false +// +// TODO(adonovan): ensure that metadata graph is always cyclic! +// Many algorithms will get confused or even stuck in the +// presence of cycles. Then replace this function by 'false'. +func isDirectlyCyclical(pkg, imported *Metadata) bool { + _, ok := imported.DepsByPkgPath[pkg.PkgPath] + return ok } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 52d99789144..1084c744976 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -185,6 +185,9 @@ type Snapshot interface { // CachedImportPaths returns all the imported packages loaded in this // snapshot, indexed by their package path (not import path, despite the name) // and checked in TypecheckWorkspace mode. + // + // To reduce latency, it does not wait for type-checking to complete. + // It is intended for use only in completions. CachedImportPaths(ctx context.Context) (map[PackagePath]Package, error) // KnownPackages returns a new unordered list of all packages @@ -210,7 +213,11 @@ type Snapshot interface { // Symbols returns all symbols in the snapshot. Symbols(ctx context.Context) map[span.URI][]Symbol - // Metadata returns a new slice containing metadata for each + // Metadata returns the metadata for the specified package, + // or nil if it was not found. + Metadata(id PackageID) *Metadata + + // MetadataForFile returns a new slice containing metadata for each // package containing the Go file identified by uri, ordered by the // number of CompiledGoFiles (i.e. "narrowest" to "widest" package). // It returns an error if the context was cancelled. From 09fb680ddf877effdf076354c021b41ac4743ac9 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 7 Dec 2022 09:39:02 -0500 Subject: [PATCH 513/723] gopls/internal/lsp/fake: eliminate the unnecessary ChangeFilesOnDisk API This API is redundant with individual sandbox file operations, and is one of the few places where we expose the (possibly unnecessary) FileEvent type. Change-Id: I7b06d0c3eb99b75e3f63611559b7aeb60b9b120b Reviewed-on: https://go-review.googlesource.com/c/tools/+/455855 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Auto-Submit: Robert Findley --- gopls/internal/lsp/fake/workdir.go | 20 ---------- gopls/internal/lsp/regtest/wrappers.go | 7 ---- .../regtest/diagnostics/diagnostics_test.go | 5 +-- gopls/internal/regtest/watch/watch_test.go | 38 +++---------------- 4 files changed, 7 insertions(+), 63 deletions(-) diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index 4c8aa127d9a..faba51701d1 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -221,26 +221,6 @@ func (w *Workdir) RegexpSearch(path string, re string) (Pos, error) { return start, err } -// ChangeFilesOnDisk executes the given on-disk file changes in a batch, -// simulating the action of changing branches outside of an editor. -func (w *Workdir) ChangeFilesOnDisk(ctx context.Context, events []FileEvent) error { - for _, e := range events { - switch e.ProtocolEvent.Type { - case protocol.Deleted: - fp := w.AbsPath(e.Path) - if err := os.Remove(fp); err != nil { - return fmt.Errorf("removing %q: %w", e.Path, err) - } - case protocol.Changed, protocol.Created: - if _, err := w.writeFile(ctx, e.Path, e.Content); err != nil { - return err - } - } - } - w.sendEvents(ctx, events) - return nil -} - // RemoveFile removes a workdir-relative file path. func (w *Workdir) RemoveFile(ctx context.Context, path string) error { fp := w.AbsPath(path) diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index e2d62e62141..71939660b6c 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -13,13 +13,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" ) -func (e *Env) ChangeFilesOnDisk(events []fake.FileEvent) { - e.T.Helper() - if err := e.Sandbox.Workdir.ChangeFilesOnDisk(e.Ctx, events); err != nil { - e.T.Fatal(err) - } -} - // RemoveWorkspaceFile deletes a file on disk but does nothing in the // editor. It calls t.Fatal on any error. func (e *Env) RemoveWorkspaceFile(name string) { diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 45bf60267df..fb858f6ab5d 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -11,12 +11,11 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/bug" - "golang.org/x/tools/gopls/internal/lsp" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 1be766b8689..32fe418228d 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -487,39 +487,11 @@ package a func _() {} ` Run(t, pkg, func(t *testing.T, env *Env) { - env.ChangeFilesOnDisk([]fake.FileEvent{ - { - Path: "a/a3.go", - Content: `package a - -var Hello int -`, - ProtocolEvent: protocol.FileEvent{ - URI: env.Sandbox.Workdir.URI("a/a3.go"), - Type: protocol.Created, - }, - }, - { - Path: "a/a1.go", - ProtocolEvent: protocol.FileEvent{ - URI: env.Sandbox.Workdir.URI("a/a1.go"), - Type: protocol.Deleted, - }, - }, - { - Path: "a/a2.go", - Content: `package a; func _() {};`, - ProtocolEvent: protocol.FileEvent{ - URI: env.Sandbox.Workdir.URI("a/a2.go"), - Type: protocol.Changed, - }, - }, - }) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - NoDiagnostics("main.go"), - ), + env.WriteWorkspaceFile("a/a3.go", "package a\n\nvar Hello int\n") + env.RemoveWorkspaceFile("a/a1.go") + env.WriteWorkspaceFile("a/a2.go", "package a; func _() {};") + env.AfterChange( + NoDiagnostics("main.go"), ) }) } From 838a165e480e740195fec58d126e8f44542ac8d4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 7 Dec 2022 09:48:12 -0500 Subject: [PATCH 514/723] gopls/internal/lsp/fake: eliminate the unnecessary fake.FileEvent There's no need to wrap protocol.FileEvent just to capture a relative path, when it is always available through the sandbox. Change-Id: I03747cf71bbe8466b9357fc8fdd7bda6b8b61d9a Reviewed-on: https://go-review.googlesource.com/c/tools/+/455856 gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- gopls/internal/lsp/fake/editor.go | 16 +++--- gopls/internal/lsp/fake/workdir.go | 69 +++++++++---------------- gopls/internal/lsp/fake/workdir_test.go | 24 +++++---- 3 files changed, 45 insertions(+), 64 deletions(-) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index df2f9751849..4c974bde4c6 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -323,7 +323,7 @@ func (e *Editor) makeWorkspaceFoldersLocked() (folders []protocol.WorkspaceFolde // onFileChanges is registered to be called by the Workdir on any writes that // go through the Workdir API. It is called synchronously by the Workdir. -func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) { +func (e *Editor) onFileChanges(ctx context.Context, evts []protocol.FileEvent) { if e.Server == nil { return } @@ -339,19 +339,17 @@ func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) { go func() { e.mu.Lock() defer e.mu.Unlock() - var lspevts []protocol.FileEvent for _, evt := range evts { // Always send an on-disk change, even for events that seem useless // because they're shadowed by an open buffer. - lspevts = append(lspevts, evt.ProtocolEvent) - - if buf, ok := e.buffers[evt.Path]; ok { + path := e.sandbox.Workdir.URIToPath(evt.URI) + if buf, ok := e.buffers[path]; ok { // Following VS Code, don't honor deletions or changes to dirty buffers. - if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted { + if buf.dirty || evt.Type == protocol.Deleted { continue } - content, err := e.sandbox.Workdir.ReadFile(evt.Path) + content, err := e.sandbox.Workdir.ReadFile(path) if err != nil { continue // A race with some other operation. } @@ -360,11 +358,11 @@ func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) { continue } // During shutdown, this call will fail. Ignore the error. - _ = e.setBufferContentLocked(ctx, evt.Path, false, lines(content), nil) + _ = e.setBufferContentLocked(ctx, path, false, lines(content), nil) } } e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{ - Changes: lspevts, + Changes: evts, }) }() } diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index faba51701d1..4538ce78d0f 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -23,13 +23,6 @@ import ( "golang.org/x/tools/gopls/internal/span" ) -// FileEvent wraps the protocol.FileEvent so that it can be associated with a -// workdir-relative path. -type FileEvent struct { - Path, Content string - ProtocolEvent protocol.FileEvent -} - // RelativeTo is a helper for operations relative to a given directory. type RelativeTo string @@ -88,7 +81,7 @@ type Workdir struct { RelativeTo watcherMu sync.Mutex - watchers []func(context.Context, []FileEvent) + watchers []func(context.Context, []protocol.FileEvent) fileMu sync.Mutex // File identities we know about, for the purpose of detecting changes. @@ -161,7 +154,7 @@ func (w *Workdir) RootURI() protocol.DocumentURI { } // AddWatcher registers the given func to be called on any file change. -func (w *Workdir) AddWatcher(watcher func(context.Context, []FileEvent)) { +func (w *Workdir) AddWatcher(watcher func(context.Context, []protocol.FileEvent)) { w.watcherMu.Lock() w.watchers = append(w.watchers, watcher) w.watcherMu.Unlock() @@ -230,24 +223,21 @@ func (w *Workdir) RemoveFile(ctx context.Context, path string) error { w.fileMu.Lock() defer w.fileMu.Unlock() - evts := []FileEvent{{ - Path: path, - ProtocolEvent: protocol.FileEvent{ - URI: w.URI(path), - Type: protocol.Deleted, - }, + evts := []protocol.FileEvent{{ + URI: w.URI(path), + Type: protocol.Deleted, }} w.sendEvents(ctx, evts) delete(w.files, path) return nil } -func (w *Workdir) sendEvents(ctx context.Context, evts []FileEvent) { +func (w *Workdir) sendEvents(ctx context.Context, evts []protocol.FileEvent) { if len(evts) == 0 { return } w.watcherMu.Lock() - watchers := make([]func(context.Context, []FileEvent), len(w.watchers)) + watchers := make([]func(context.Context, []protocol.FileEvent), len(w.watchers)) copy(watchers, w.watchers) w.watcherMu.Unlock() for _, w := range watchers { @@ -258,7 +248,7 @@ func (w *Workdir) sendEvents(ctx context.Context, evts []FileEvent) { // WriteFiles writes the text file content to workdir-relative paths. // It batches notifications rather than sending them consecutively. func (w *Workdir) WriteFiles(ctx context.Context, files map[string]string) error { - var evts []FileEvent + var evts []protocol.FileEvent for filename, content := range files { evt, err := w.writeFile(ctx, filename, content) if err != nil { @@ -276,15 +266,15 @@ func (w *Workdir) WriteFile(ctx context.Context, path, content string) error { if err != nil { return err } - w.sendEvents(ctx, []FileEvent{evt}) + w.sendEvents(ctx, []protocol.FileEvent{evt}) return nil } -func (w *Workdir) writeFile(ctx context.Context, path, content string) (FileEvent, error) { +func (w *Workdir) writeFile(ctx context.Context, path, content string) (protocol.FileEvent, error) { fp := w.AbsPath(path) _, err := os.Stat(fp) if err != nil && !os.IsNotExist(err) { - return FileEvent{}, fmt.Errorf("checking if %q exists: %w", path, err) + return protocol.FileEvent{}, fmt.Errorf("checking if %q exists: %w", path, err) } var changeType protocol.FileChangeType if os.IsNotExist(err) { @@ -293,18 +283,15 @@ func (w *Workdir) writeFile(ctx context.Context, path, content string) (FileEven changeType = protocol.Changed } if err := WriteFileData(path, []byte(content), w.RelativeTo); err != nil { - return FileEvent{}, err + return protocol.FileEvent{}, err } return w.fileEvent(path, changeType), nil } -func (w *Workdir) fileEvent(path string, changeType protocol.FileChangeType) FileEvent { - return FileEvent{ - Path: path, - ProtocolEvent: protocol.FileEvent{ - URI: w.URI(path), - Type: changeType, - }, +func (w *Workdir) fileEvent(path string, changeType protocol.FileChangeType) protocol.FileEvent { + return protocol.FileEvent{ + URI: w.URI(path), + Type: changeType, } } @@ -396,7 +383,7 @@ func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error // Send synthetic file events for the renaming. Renamed files are handled as // Delete+Create events: // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#fileChangeType - events := []FileEvent{ + events := []protocol.FileEvent{ w.fileEvent(oldPath, protocol.Deleted), w.fileEvent(newPath, protocol.Created), } @@ -466,7 +453,7 @@ func (w *Workdir) CheckForFileChanges(ctx context.Context) error { // pollFiles updates w.files and calculates FileEvents corresponding to file // state changes since the last poll. It does not call sendEvents. -func (w *Workdir) pollFiles() ([]FileEvent, error) { +func (w *Workdir) pollFiles() ([]protocol.FileEvent, error) { w.fileMu.Lock() defer w.fileMu.Unlock() @@ -474,7 +461,7 @@ func (w *Workdir) pollFiles() ([]FileEvent, error) { if err != nil { return nil, err } - var evts []FileEvent + var evts []protocol.FileEvent // Check which files have been added or modified. for path, id := range files { oldID, ok := w.files[path] @@ -488,22 +475,16 @@ func (w *Workdir) pollFiles() ([]FileEvent, error) { default: continue } - evts = append(evts, FileEvent{ - Path: path, - ProtocolEvent: protocol.FileEvent{ - URI: w.URI(path), - Type: typ, - }, + evts = append(evts, protocol.FileEvent{ + URI: w.URI(path), + Type: typ, }) } // Any remaining files must have been deleted. for path := range w.files { - evts = append(evts, FileEvent{ - Path: path, - ProtocolEvent: protocol.FileEvent{ - URI: w.URI(path), - Type: protocol.Deleted, - }, + evts = append(evts, protocol.FileEvent{ + URI: w.URI(path), + Type: protocol.Deleted, }) } w.files = files diff --git a/gopls/internal/lsp/fake/workdir_test.go b/gopls/internal/lsp/fake/workdir_test.go index 77c6684556c..81155a64806 100644 --- a/gopls/internal/lsp/fake/workdir_test.go +++ b/gopls/internal/lsp/fake/workdir_test.go @@ -22,7 +22,7 @@ go 1.12 Hello World! ` -func newWorkdir(t *testing.T) (*Workdir, <-chan []FileEvent, func()) { +func newWorkdir(t *testing.T) (*Workdir, <-chan []protocol.FileEvent, func()) { t.Helper() tmpdir, err := ioutil.TempDir("", "goplstest-workdir-") @@ -39,8 +39,8 @@ func newWorkdir(t *testing.T) (*Workdir, <-chan []FileEvent, func()) { } } - fileEvents := make(chan []FileEvent) - watch := func(_ context.Context, events []FileEvent) { + fileEvents := make(chan []protocol.FileEvent) + watch := func(_ context.Context, events []protocol.FileEvent) { go func() { fileEvents <- events }() @@ -84,11 +84,12 @@ func TestWorkdir_WriteFile(t *testing.T) { if got := len(es); got != 1 { t.Fatalf("len(events) = %d, want 1", got) } - if es[0].Path != test.path { - t.Errorf("event.Path = %q, want %q", es[0].Path, test.path) + path := wd.URIToPath(es[0].URI) + if path != test.path { + t.Errorf("event path = %q, want %q", path, test.path) } - if es[0].ProtocolEvent.Type != test.wantType { - t.Errorf("event type = %v, want %v", es[0].ProtocolEvent.Type, test.wantType) + if es[0].Type != test.wantType { + t.Errorf("event type = %v, want %v", es[0].Type, test.wantType) } got, err := wd.ReadFile(test.path) if err != nil { @@ -137,20 +138,21 @@ func TestWorkdir_CheckForFileChanges(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - checkChange := func(path string, typ protocol.FileChangeType) { + checkChange := func(wantPath string, wantType protocol.FileChangeType) { if err := wd.CheckForFileChanges(ctx); err != nil { t.Fatal(err) } - var gotEvt FileEvent + var gotEvt protocol.FileEvent select { case <-ctx.Done(): t.Fatal(ctx.Err()) case ev := <-events: gotEvt = ev[0] } + gotPath := wd.URIToPath(gotEvt.URI) // Only check relative path and Type - if gotEvt.Path != path || gotEvt.ProtocolEvent.Type != typ { - t.Errorf("file events: got %v, want {Path: %s, Type: %v}", gotEvt, path, typ) + if gotPath != wantPath || gotEvt.Type != wantType { + t.Errorf("file events: got %v, want {Path: %s, Type: %v}", gotEvt, wantPath, wantType) } } // Sleep some positive amount of time to ensure a distinct mtime. From 1e5efed1b8575fb1f00aaca5e7e156af237a6301 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 7 Dec 2022 10:01:55 -0500 Subject: [PATCH 515/723] gopls/internal/lsp/fake: simply use polling to simulate file watching Initially, the fake.Workdir type simulated file watching by sending file events as a result of explicit operations on individual files (such as WriteFile or RemoveFile). Subsequently, a polling mechanism was added for computing file events as a result of some external operation, such as running the Go command. Since the latter operation is based on actual changes to filesystem state, it should reproduce the simulated events exactly (and if it doesn't, there is a bug). Therefore, simplify file watching to only use filesystem polling. Change-Id: If8e8523637e8d41d09e2e996c3bd6577352393c6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/455859 Reviewed-by: Alan Donovan Reviewed-by: Bryan Mills gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/fake/sandbox.go | 2 +- gopls/internal/lsp/fake/workdir.go | 111 +++++++---------------- gopls/internal/lsp/fake/workdir_test.go | 112 ++++++++++++++++++------ 3 files changed, 119 insertions(+), 106 deletions(-) diff --git a/gopls/internal/lsp/fake/sandbox.go b/gopls/internal/lsp/fake/sandbox.go index 206a3de1ba6..e4da6289d1c 100644 --- a/gopls/internal/lsp/fake/sandbox.go +++ b/gopls/internal/lsp/fake/sandbox.go @@ -152,7 +152,7 @@ func Tempdir(files map[string][]byte) (string, error) { return "", err } for name, data := range files { - if err := WriteFileData(name, data, RelativeTo(dir)); err != nil { + if err := writeFileData(name, data, RelativeTo(dir)); err != nil { return "", fmt.Errorf("writing to tempdir: %w", err) } } diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index 4538ce78d0f..a76a3b5391d 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -45,9 +45,10 @@ func (r RelativeTo) RelPath(fp string) string { return filepath.ToSlash(fp) } -// WriteFileData writes content to the relative path, replacing the special -// token $SANDBOX_WORKDIR with the relative root given by rel. -func WriteFileData(path string, content []byte, rel RelativeTo) error { +// writeFileData writes content to the relative path, replacing the special +// token $SANDBOX_WORKDIR with the relative root given by rel. It does not +// trigger any file events. +func writeFileData(path string, content []byte, rel RelativeTo) error { content = bytes.ReplaceAll(content, []byte("$SANDBOX_WORKDIR"), []byte(rel)) fp := rel.AbsPath(path) if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil { @@ -122,7 +123,7 @@ func hashFile(data []byte) string { func (w *Workdir) writeInitialFiles(files map[string][]byte) error { w.files = map[string]fileID{} for name, data := range files { - if err := WriteFileData(name, data, w.RelativeTo); err != nil { + if err := writeFileData(name, data, w.RelativeTo); err != nil { return fmt.Errorf("writing to workdir: %w", err) } fp := w.AbsPath(name) @@ -214,78 +215,37 @@ func (w *Workdir) RegexpSearch(path string, re string) (Pos, error) { return start, err } -// RemoveFile removes a workdir-relative file path. +// RemoveFile removes a workdir-relative file path and notifies watchers of the +// change. func (w *Workdir) RemoveFile(ctx context.Context, path string) error { fp := w.AbsPath(path) if err := os.RemoveAll(fp); err != nil { return fmt.Errorf("removing %q: %w", path, err) } - w.fileMu.Lock() - defer w.fileMu.Unlock() - evts := []protocol.FileEvent{{ - URI: w.URI(path), - Type: protocol.Deleted, - }} - w.sendEvents(ctx, evts) - delete(w.files, path) - return nil + return w.CheckForFileChanges(ctx) } -func (w *Workdir) sendEvents(ctx context.Context, evts []protocol.FileEvent) { - if len(evts) == 0 { - return - } - w.watcherMu.Lock() - watchers := make([]func(context.Context, []protocol.FileEvent), len(w.watchers)) - copy(watchers, w.watchers) - w.watcherMu.Unlock() - for _, w := range watchers { - w(ctx, evts) - } -} - -// WriteFiles writes the text file content to workdir-relative paths. -// It batches notifications rather than sending them consecutively. +// WriteFiles writes the text file content to workdir-relative paths and +// notifies watchers of the changes. func (w *Workdir) WriteFiles(ctx context.Context, files map[string]string) error { - var evts []protocol.FileEvent - for filename, content := range files { - evt, err := w.writeFile(ctx, filename, content) - if err != nil { + for path, content := range files { + fp := w.AbsPath(path) + _, err := os.Stat(fp) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("checking if %q exists: %w", path, err) + } + if err := writeFileData(path, []byte(content), w.RelativeTo); err != nil { return err } - evts = append(evts, evt) } - w.sendEvents(ctx, evts) - return nil + return w.CheckForFileChanges(ctx) } -// WriteFile writes text file content to a workdir-relative path. +// WriteFile writes text file content to a workdir-relative path and notifies +// watchers of the change. func (w *Workdir) WriteFile(ctx context.Context, path, content string) error { - evt, err := w.writeFile(ctx, path, content) - if err != nil { - return err - } - w.sendEvents(ctx, []protocol.FileEvent{evt}) - return nil -} - -func (w *Workdir) writeFile(ctx context.Context, path, content string) (protocol.FileEvent, error) { - fp := w.AbsPath(path) - _, err := os.Stat(fp) - if err != nil && !os.IsNotExist(err) { - return protocol.FileEvent{}, fmt.Errorf("checking if %q exists: %w", path, err) - } - var changeType protocol.FileChangeType - if os.IsNotExist(err) { - changeType = protocol.Created - } else { - changeType = protocol.Changed - } - if err := WriteFileData(path, []byte(content), w.RelativeTo); err != nil { - return protocol.FileEvent{}, err - } - return w.fileEvent(path, changeType), nil + return w.WriteFiles(ctx, map[string]string{path: content}) } func (w *Workdir) fileEvent(path string, changeType protocol.FileChangeType) protocol.FileEvent { @@ -296,16 +256,13 @@ func (w *Workdir) fileEvent(path string, changeType protocol.FileChangeType) pro } // RenameFile performs an on disk-renaming of the workdir-relative oldPath to -// workdir-relative newPath. +// workdir-relative newPath, and notifies watchers of the changes. // // oldPath must either be a regular file or in the same directory as newPath. func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error { oldAbs := w.AbsPath(oldPath) newAbs := w.AbsPath(newPath) - w.fileMu.Lock() - defer w.fileMu.Unlock() - // For os.Rename, “OS-specific restrictions may apply when oldpath and newpath // are in different directories.” If that applies here, we may fall back to // ReadFile, WriteFile, and RemoveFile to perform the rename non-atomically. @@ -361,7 +318,7 @@ func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error // the error from Rename may be accurate. return renameErr } - if writeErr := WriteFileData(newPath, []byte(content), w.RelativeTo); writeErr != nil { + if writeErr := writeFileData(newPath, []byte(content), w.RelativeTo); writeErr != nil { // At this point we have tried to actually write the file. // If it still doesn't exist, assume that the error from Rename was accurate: // for example, maybe we don't have permission to create the new path. @@ -380,16 +337,7 @@ func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error } } - // Send synthetic file events for the renaming. Renamed files are handled as - // Delete+Create events: - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#fileChangeType - events := []protocol.FileEvent{ - w.fileEvent(oldPath, protocol.Deleted), - w.fileEvent(newPath, protocol.Created), - } - w.sendEvents(ctx, events) - delete(w.files, oldPath) - return nil + return w.CheckForFileChanges(ctx) } // ListFiles returns a new sorted list of the relative paths of files in dir, @@ -447,7 +395,16 @@ func (w *Workdir) CheckForFileChanges(ctx context.Context) error { if err != nil { return err } - w.sendEvents(ctx, evts) + if len(evts) == 0 { + return nil + } + w.watcherMu.Lock() + watchers := make([]func(context.Context, []protocol.FileEvent), len(w.watchers)) + copy(watchers, w.watchers) + w.watcherMu.Unlock() + for _, w := range watchers { + w(ctx, evts) + } return nil } diff --git a/gopls/internal/lsp/fake/workdir_test.go b/gopls/internal/lsp/fake/workdir_test.go index 81155a64806..c660a405cda 100644 --- a/gopls/internal/lsp/fake/workdir_test.go +++ b/gopls/internal/lsp/fake/workdir_test.go @@ -9,20 +9,28 @@ import ( "io/ioutil" "os" "sort" + "sync" "testing" - "time" + "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/lsp/protocol" ) -const data = ` +const sharedData = ` -- go.mod -- go 1.12 -- nested/README.md -- Hello World! ` -func newWorkdir(t *testing.T) (*Workdir, <-chan []protocol.FileEvent, func()) { +// newWorkdir sets up a temporary Workdir with the given txtar-encoded content. +// It also configures an eventBuffer to receive file event notifications. These +// notifications are sent synchronously for each operation, such that once a +// workdir file operation has returned the caller can expect that any relevant +// file notifications are present in the buffer. +// +// It is the caller's responsibility to call the returned cleanup function. +func newWorkdir(t *testing.T, txt string) (*Workdir, *eventBuffer, func()) { t.Helper() tmpdir, err := ioutil.TempDir("", "goplstest-workdir-") @@ -30,7 +38,7 @@ func newWorkdir(t *testing.T) (*Workdir, <-chan []protocol.FileEvent, func()) { t.Fatal(err) } wd := NewWorkdir(tmpdir) - if err := wd.writeInitialFiles(UnpackTxt(data)); err != nil { + if err := wd.writeInitialFiles(UnpackTxt(txt)); err != nil { t.Fatal(err) } cleanup := func() { @@ -39,18 +47,37 @@ func newWorkdir(t *testing.T) (*Workdir, <-chan []protocol.FileEvent, func()) { } } - fileEvents := make(chan []protocol.FileEvent) - watch := func(_ context.Context, events []protocol.FileEvent) { - go func() { - fileEvents <- events - }() - } - wd.AddWatcher(watch) - return wd, fileEvents, cleanup + buf := new(eventBuffer) + wd.AddWatcher(buf.onEvents) + return wd, buf, cleanup +} + +// eventBuffer collects events from a file watcher. +type eventBuffer struct { + mu sync.Mutex + events []protocol.FileEvent +} + +// onEvents collects adds events to the buffer; to be used with Workdir.AddWatcher. +func (c *eventBuffer) onEvents(_ context.Context, events []protocol.FileEvent) { + c.mu.Lock() + defer c.mu.Unlock() + + c.events = append(c.events, events...) +} + +// take empties the buffer, returning its previous contents. +func (c *eventBuffer) take() []protocol.FileEvent { + c.mu.Lock() + defer c.mu.Unlock() + + evts := c.events + c.events = nil + return evts } func TestWorkdir_ReadFile(t *testing.T) { - wd, _, cleanup := newWorkdir(t) + wd, _, cleanup := newWorkdir(t, sharedData) defer cleanup() got, err := wd.ReadFile("nested/README.md") @@ -64,7 +91,7 @@ func TestWorkdir_ReadFile(t *testing.T) { } func TestWorkdir_WriteFile(t *testing.T) { - wd, events, cleanup := newWorkdir(t) + wd, events, cleanup := newWorkdir(t, sharedData) defer cleanup() ctx := context.Background() @@ -80,7 +107,7 @@ func TestWorkdir_WriteFile(t *testing.T) { if err := wd.WriteFile(ctx, test.path, "42"); err != nil { t.Fatal(err) } - es := <-events + es := events.take() if got := len(es); got != 1 { t.Fatalf("len(events) = %d, want 1", got) } @@ -102,8 +129,41 @@ func TestWorkdir_WriteFile(t *testing.T) { } } +// Test for file notifications following file operations. +func TestWorkdir_FileWatching(t *testing.T) { + wd, events, cleanup := newWorkdir(t, "") + defer cleanup() + ctx := context.Background() + + must := func(err error) { + if err != nil { + t.Fatal(err) + } + } + + type changeMap map[string]protocol.FileChangeType + checkEvent := func(wantChanges changeMap) { + gotChanges := make(changeMap) + for _, e := range events.take() { + gotChanges[wd.URIToPath(e.URI)] = e.Type + } + if diff := cmp.Diff(wantChanges, gotChanges); diff != "" { + t.Errorf("mismatching file events (-want +got):\n%s", diff) + } + } + + must(wd.WriteFile(ctx, "foo.go", "package foo")) + checkEvent(changeMap{"foo.go": protocol.Created}) + + must(wd.RenameFile(ctx, "foo.go", "bar.go")) + checkEvent(changeMap{"foo.go": protocol.Deleted, "bar.go": protocol.Created}) + + must(wd.RemoveFile(ctx, "bar.go")) + checkEvent(changeMap{"bar.go": protocol.Deleted}) +} + func TestWorkdir_ListFiles(t *testing.T) { - wd, _, cleanup := newWorkdir(t) + wd, _, cleanup := newWorkdir(t, sharedData) defer cleanup() checkFiles := func(dir string, want []string) { @@ -133,22 +193,19 @@ func TestWorkdir_ListFiles(t *testing.T) { func TestWorkdir_CheckForFileChanges(t *testing.T) { t.Skip("broken on darwin-amd64-10_12") - wd, events, cleanup := newWorkdir(t) + wd, events, cleanup := newWorkdir(t, sharedData) defer cleanup() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + ctx := context.Background() checkChange := func(wantPath string, wantType protocol.FileChangeType) { if err := wd.CheckForFileChanges(ctx); err != nil { t.Fatal(err) } - var gotEvt protocol.FileEvent - select { - case <-ctx.Done(): - t.Fatal(ctx.Err()) - case ev := <-events: - gotEvt = ev[0] + ev := events.take() + if len(ev) == 0 { + t.Fatal("no file events received") } + gotEvt := ev[0] gotPath := wd.URIToPath(gotEvt.URI) // Only check relative path and Type if gotPath != wantPath || gotEvt.Type != wantType { @@ -156,12 +213,11 @@ func TestWorkdir_CheckForFileChanges(t *testing.T) { } } // Sleep some positive amount of time to ensure a distinct mtime. - time.Sleep(100 * time.Millisecond) - if err := WriteFileData("go.mod", []byte("module foo.test\n"), wd.RelativeTo); err != nil { + if err := writeFileData("go.mod", []byte("module foo.test\n"), wd.RelativeTo); err != nil { t.Fatal(err) } checkChange("go.mod", protocol.Changed) - if err := WriteFileData("newFile", []byte("something"), wd.RelativeTo); err != nil { + if err := writeFileData("newFile", []byte("something"), wd.RelativeTo); err != nil { t.Fatal(err) } checkChange("newFile", protocol.Created) From d0f184db7258194a672f34d0b673bddeb8100c0d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 7 Dec 2022 16:24:10 -0500 Subject: [PATCH 516/723] gopls/internal/lsp/source: avoid unnecessary calls to GetTypedFile GetTypedFile is the new name for GetParsedFile, to make obvious that it involves type-checking, which is expensive. All remaining calls to this function that do not actually require type-checking have been downgraded into parser calls. Also, downgrade missingImports.ifaceFile to a slice of ImportSpec. Change-Id: Ie960252f482c6dbffab749dd904eb24bd3b3a25c Reviewed-on: https://go-review.googlesource.com/c/tools/+/456036 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- .../lsp/analysis/stubmethods/stubmethods.go | 30 ++++++++++--------- gopls/internal/lsp/code_action.go | 3 +- gopls/internal/lsp/source/add_import.go | 3 +- gopls/internal/lsp/source/code_lens.go | 9 ++---- .../lsp/source/completion/completion.go | 2 +- gopls/internal/lsp/source/diagnostics.go | 2 +- gopls/internal/lsp/source/fix.go | 5 ++-- gopls/internal/lsp/source/highlight.go | 2 +- gopls/internal/lsp/source/hover.go | 2 +- gopls/internal/lsp/source/inlay_hint.go | 4 +-- gopls/internal/lsp/source/signature_help.go | 2 +- gopls/internal/lsp/source/stub.go | 27 ++++++++--------- gopls/internal/lsp/source/util.go | 13 ++++---- 13 files changed, 49 insertions(+), 55 deletions(-) diff --git a/gopls/internal/lsp/analysis/stubmethods/stubmethods.go b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go index effeb8a405d..9092f8b1276 100644 --- a/gopls/internal/lsp/analysis/stubmethods/stubmethods.go +++ b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go @@ -269,19 +269,21 @@ func fromAssignStmt(ti *types.Info, as *ast.AssignStmt, pos token.Pos) *StubInfo } } -// RelativeToFiles returns a types.Qualifier that formats package names -// according to the files where the concrete and interface types are defined. +// RelativeToFiles returns a types.Qualifier that formats package +// names according to the import environments of the files that define +// the concrete type and the interface type. (Only the imports of the +// latter file are provided.) // // This is similar to types.RelativeTo except if a file imports the package with a different name, // then it will use it. And if the file does import the package but it is ignored, -// then it will return the original name. It also prefers package names in ifaceFile in case -// an import is missing from concFile but is present in ifaceFile. +// then it will return the original name. It also prefers package names in importEnv in case +// an import is missing from concFile but is present among importEnv. // // Additionally, if missingImport is not nil, the function will be called whenever the concFile // is presented with a package that is not imported. This is useful so that as types.TypeString is // formatting a function signature, it is identifying packages that will need to be imported when // stubbing an interface. -func RelativeToFiles(concPkg *types.Package, concFile, ifaceFile *ast.File, missingImport func(name, path string)) types.Qualifier { +func RelativeToFiles(concPkg *types.Package, concFile *ast.File, ifaceImports []*ast.ImportSpec, missingImport func(name, path string)) types.Qualifier { return func(other *types.Package) string { if other == concPkg { return "" @@ -292,6 +294,7 @@ func RelativeToFiles(concPkg *types.Package, concFile, ifaceFile *ast.File, miss for _, imp := range concFile.Imports { impPath, _ := strconv.Unquote(imp.Path.Value) isIgnored := imp.Name != nil && (imp.Name.Name == "." || imp.Name.Name == "_") + // TODO(adonovan): this comparison disregards a vendor prefix in 'other'. if impPath == other.Path() && !isIgnored { importName := other.Name() if imp.Name != nil { @@ -304,16 +307,15 @@ func RelativeToFiles(concPkg *types.Package, concFile, ifaceFile *ast.File, miss // If the concrete file does not have the import, check if the package // is renamed in the interface file and prefer that. var importName string - if ifaceFile != nil { - for _, imp := range ifaceFile.Imports { - impPath, _ := strconv.Unquote(imp.Path.Value) - isIgnored := imp.Name != nil && (imp.Name.Name == "." || imp.Name.Name == "_") - if impPath == other.Path() && !isIgnored { - if imp.Name != nil && imp.Name.Name != concPkg.Name() { - importName = imp.Name.Name - } - break + for _, imp := range ifaceImports { + impPath, _ := strconv.Unquote(imp.Path.Value) + isIgnored := imp.Name != nil && (imp.Name.Name == "." || imp.Name.Name == "_") + // TODO(adonovan): this comparison disregards a vendor prefix in 'other'. + if impPath == other.Path() && !isIgnored { + if imp.Name != nil && imp.Name.Name != concPkg.Name() { + importName = imp.Name.Name } + break } } diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 275348baca2..1cf58f9ddf2 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -313,8 +313,7 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI if err != nil { return nil, err } - // TODO(adonovan): opt: avoid package loading; only parsing is needed. - _, pgf, err := source.GetParsedFile(ctx, snapshot, fh, source.NarrowestPackage) + pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) if err != nil { return nil, fmt.Errorf("getting file for Identifier: %w", err) } diff --git a/gopls/internal/lsp/source/add_import.go b/gopls/internal/lsp/source/add_import.go index 29be1484288..c50c1554165 100644 --- a/gopls/internal/lsp/source/add_import.go +++ b/gopls/internal/lsp/source/add_import.go @@ -13,8 +13,7 @@ import ( // AddImport adds a single import statement to the given file func AddImport(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, importPath string) ([]protocol.TextEdit, error) { - // TODO(adonovan): opt: avoid loading type checked package; only parsing is needed. - _, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go index afca98d06f7..cb7bbc8578b 100644 --- a/gopls/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -62,8 +62,7 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p } if len(fns.Benchmarks) > 0 { - // TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed. - _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, err } @@ -101,8 +100,7 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) ( if !strings.HasSuffix(fh.URI().Filename(), "_test.go") { return out, nil } - // TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed. - pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) + pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, WidestPackage) if err != nil { return out, err } @@ -229,8 +227,7 @@ func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([ } func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { - // TODO(adonovan): avoid loading type-checked package; only Metadata is needed. - _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 1eeabc9873c..9d11d7088cd 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -427,7 +427,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan startTime := time.Now() - pkg, pgf, err := source.GetParsedFile(ctx, snapshot, fh, source.NarrowestPackage) + pkg, pgf, err := source.GetTypedFile(ctx, snapshot, fh, source.NarrowestPackage) 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 diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index c292f257989..8a382043506 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -64,7 +64,7 @@ func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (Vers if err != nil { return VersionedFileIdentity{}, nil, err } - pkg, _, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, _, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return VersionedFileIdentity{}, nil, err } diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index b220a6bf846..537692ef677 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -109,8 +109,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi } editsPerFile[fh.URI()] = te } - // TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed. - _, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, err } @@ -133,7 +132,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi // getAllSuggestedFixInputs is a helper function to collect all possible needed // inputs for an AppliesFunc or SuggestedFixFunc. func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, span.Range, []byte, *ast.File, *types.Package, *types.Info, error) { - pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return nil, span.Range{}, nil, nil, nil, nil, fmt.Errorf("getting file for Identifier: %w", err) } diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index 166dc3cd46b..2f0766b9a82 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -21,7 +21,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p ctx, done := event.Start(ctx, "source.Highlight") defer done() - // Don't use GetParsedFile because it uses TypecheckWorkspace, and we + // Don't use GetTypedFile because it uses TypecheckWorkspace, and we // always want fully parsed files for highlight, regardless of whether // the file belongs to a workspace package. pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckFull, WidestPackage) diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index c2522293571..bbea8949bca 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -140,7 +140,7 @@ var ErrNoRuneFound = errors.New("no rune found") // findRune returns rune information for a position in a file. func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, MappedRange, error) { // TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed. - pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return 0, MappedRange{}, err } diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go index 9d9152f87e8..039a1df1f8a 100644 --- a/gopls/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -13,9 +13,9 @@ import ( "go/types" "strings" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/lsppos" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" ) @@ -83,7 +83,7 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng proto ctx, done := event.Start(ctx, "source.InlayHint") defer done() - pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return nil, fmt.Errorf("getting file for InlayHint: %w", err) } diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index 8c62a779619..ad0e69bc59b 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -20,7 +20,7 @@ func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, positi ctx, done := event.Start(ctx, "source.SignatureHelp") defer done() - pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { return nil, 0, fmt.Errorf("getting file for SignatureHelp: %w", err) } diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 4cdfc7124af..4d5101ed668 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -25,9 +25,9 @@ import ( ) func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { - pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) if err != nil { - return nil, nil, fmt.Errorf("GetParsedFile: %w", err) + return nil, nil, fmt.Errorf("GetTypedFile: %w", err) } nodes, pos, err := getStubNodes(pgf, rng) if err != nil { @@ -114,7 +114,7 @@ func stubMethods(ctx context.Context, concreteFile *ast.File, si *stubmethods.St for _, m := range mi.missing { // TODO(marwan-at-work): this should share the same logic with source.FormatVarType // as it also accounts for type aliases. - sig := types.TypeString(m.Type(), stubmethods.RelativeToFiles(si.Concrete.Obj().Pkg(), concreteFile, mi.file, func(name, path string) { + sig := types.TypeString(m.Type(), stubmethods.RelativeToFiles(si.Concrete.Obj().Pkg(), concreteFile, mi.imports, func(name, path string) { for _, imp := range stubImports { if imp.Name == name && imp.Path == path { return @@ -245,13 +245,13 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method if err != nil { return nil, fmt.Errorf("error getting iface file: %w", err) } - mi := &missingInterface{ - iface: iface, - file: parsedFile.File, - } - if mi.file == nil { + if parsedFile.File == nil { return nil, fmt.Errorf("could not find ast.File for %v", ifaceObj.Name()) } + mi := &missingInterface{ + iface: iface, + imports: parsedFile.File.Imports, + } for i := 0; i < iface.NumExplicitMethods(); i++ { method := iface.ExplicitMethod(i) // if the concrete type does not have the interface method @@ -276,16 +276,13 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method } // Token position information for obj.Pos and the ParsedGoFile result is in Snapshot.FileSet. +// The returned file does not have type information. func getStubFile(ctx context.Context, obj types.Object, snapshot Snapshot) (*ParsedGoFile, VersionedFileHandle, error) { objPos := snapshot.FileSet().Position(obj.Pos()) objFile := span.URIFromPath(objPos.Filename) objectFH := snapshot.FindFile(objFile) - // TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed. - _, goFile, err := GetParsedFile(ctx, snapshot, objectFH, WidestPackage) - if err != nil { - return nil, nil, fmt.Errorf("GetParsedFile: %w", err) - } - return goFile, objectFH, nil + goFile, err := snapshot.ParseGo(ctx, objectFH, ParseFull) + return goFile, objectFH, err } // missingInterface represents an interface @@ -293,7 +290,7 @@ func getStubFile(ctx context.Context, obj types.Object, snapshot Snapshot) (*Par // from the destination concrete type type missingInterface struct { iface *types.Interface - file *ast.File + imports []*ast.ImportSpec // the interface's import environment missing []*types.Func } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 32c690f92da..cc9c9e44665 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -91,13 +91,14 @@ func (s MappedRange) URI() span.URI { return s.m.URI } -// GetParsedFile is a convenience function that extracts the Package and -// ParsedGoFile for a file in a Snapshot. pkgPolicy is one of NarrowestPackage/ -// WidestPackage. +// GetTypedFile is a convenience function that reads, parses, and +// type-checks the package containing a file in the given +// Snapshot. pkgPolicy is one of NarrowestPackage or WidestPackage. // -// Type-checking is expensive. Call cheaper methods of Snapshot if all -// you need is Metadata or parse trees. -func GetParsedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgPolicy PackageFilter) (Package, *ParsedGoFile, error) { +// Type-checking is expensive. Call snapshot.ParseGo if all you need +// is a parse tree, or snapshot.MetadataForFile if all you only need +// metadata. +func GetTypedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgPolicy PackageFilter) (Package, *ParsedGoFile, error) { pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckWorkspace, pkgPolicy) if err != nil { return nil, nil, err From cb701f7c70bf148430745c1fcd0a592ad8b2dcde Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 7 Dec 2022 14:17:14 -0500 Subject: [PATCH 517/723] gopls/internal/lsp: avoid type-checking when computing hyperlinks This change replaces a call to PackagesForFile (which demands type-checking) by a metadata query. Another small step towards not demanding type checking unnecessarily. Also, clarify the logic and make the test assertions less opaque. Change-Id: Ibcddf3e2cbeb6bf261b32ff7f16a89f365f9c265 Reviewed-on: https://go-review.googlesource.com/c/tools/+/455975 Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan --- gopls/internal/lsp/link.go | 111 ++++++++++++----------- gopls/internal/lsp/source/hover.go | 2 +- gopls/internal/lsp/tests/util.go | 11 ++- gopls/internal/regtest/misc/link_test.go | 4 +- 4 files changed, 68 insertions(+), 60 deletions(-) diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go index ea1ff5ad0ab..011f0e44163 100644 --- a/gopls/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -81,6 +81,7 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl } // Get all the links that are contained in the comments of the file. + urlRegexp := snapshot.View().Options().URLRegexp for _, expr := range pm.File.Syntax.Stmt { comments := expr.Comment() if comments == nil { @@ -89,7 +90,7 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} { for _, comment := range section { start := tokFile.Pos(comment.Start.Byte) - l, err := findLinksInString(ctx, snapshot, comment.Token, start, tokFile, pm.Mapper) + l, err := findLinksInString(urlRegexp, comment.Token, start, tokFile, pm.Mapper) if err != nil { return nil, err } @@ -100,54 +101,54 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl return links, nil } +// goLinks returns the set of hyperlink annotations for the specified Go file. func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) { view := snapshot.View() - // We don't actually need type information, so any typecheck mode is fine. - // TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed. - pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckWorkspace, source.WidestPackage) - if err != nil { - return nil, err - } + pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) if err != nil { return nil, err } - var imports []*ast.ImportSpec - var str []*ast.BasicLit - ast.Inspect(pgf.File, func(node ast.Node) bool { - switch n := node.(type) { - case *ast.ImportSpec: - imports = append(imports, n) - return false - case *ast.BasicLit: - // Look for links in string literals. - if n.Kind == token.STRING { - str = append(str, n) - } - return false - } - return true - }) + var links []protocol.DocumentLink - // For import specs, provide a link to a documentation website, like - // https://pkg.go.dev. + + // Create links for import specs. if view.Options().ImportShortcut.ShowLinks() { - for _, imp := range imports { - target := source.UnquoteImportPath(imp) - if target == "" { - continue + + // 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 + if strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" { + if metas, _ := snapshot.MetadataForFile(ctx, fh.URI()); len(metas) > 0 { + depsByImpPath = metas[0].DepsByImpPath // 0 => narrowest package + } + } + + for _, imp := range pgf.File.Imports { + importPath := source.UnquoteImportPath(imp) + if importPath == "" { + continue // bad import } // See golang/go#36998: don't link to modules matching GOPRIVATE. - if view.IsGoPrivatePath(string(target)) { + if view.IsGoPrivatePath(string(importPath)) { continue } - if mod, version, ok := moduleAtVersion(target, pkg); ok && strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" { - target = source.ImportPath(strings.Replace(string(target), mod, mod+"@"+version, 1)) + + urlPath := string(importPath) + + // For pkg.go.dev, append module version suffix to package import path. + if m := snapshot.Metadata(depsByImpPath[importPath]); m != nil && + m.Module != nil && + m.Module.Path != "" && + m.Module.Version != "" && + !source.IsWorkspaceModuleVersion(m.Module.Version) { + urlPath = strings.Replace(urlPath, m.Module.Path, m.Module.Path+"@"+m.Module.Version, 1) } + // Account for the quotation marks in the positions. start := imp.Path.Pos() + 1 end := imp.Path.End() - 1 - targetURL := source.BuildLink(view.Options().LinkTarget, string(target), "") + targetURL := source.BuildLink(view.Options().LinkTarget, urlPath, "") l, err := toProtocolLink(pgf.Tok, pgf.Mapper, targetURL, start, end) if err != nil { return nil, err @@ -155,39 +156,42 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle links = append(links, l) } } + + urlRegexp := snapshot.View().Options().URLRegexp + + // Gather links found in string literals. + var str []*ast.BasicLit + ast.Inspect(pgf.File, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.ImportSpec: + return false // don't process import strings again + case *ast.BasicLit: + if n.Kind == token.STRING { + str = append(str, n) + } + } + return true + }) for _, s := range str { - l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Tok, pgf.Mapper) + l, err := findLinksInString(urlRegexp, s.Value, s.Pos(), pgf.Tok, pgf.Mapper) if err != nil { return nil, err } links = append(links, l...) } + + // Gather links found in comments. for _, commentGroup := range pgf.File.Comments { for _, comment := range commentGroup.List { - l, err := findLinksInString(ctx, snapshot, comment.Text, comment.Pos(), pgf.Tok, pgf.Mapper) + l, err := findLinksInString(urlRegexp, comment.Text, comment.Pos(), pgf.Tok, pgf.Mapper) if err != nil { return nil, err } links = append(links, l...) } } - return links, nil -} -func moduleAtVersion(targetImportPath source.ImportPath, pkg source.Package) (string, string, bool) { - // TODO(adonovan): opt: avoid need for package; use Metadata.DepsByImportPath only. - impPkg, err := pkg.ResolveImportPath(targetImportPath) - if err != nil { - return "", "", false - } - if impPkg.Version() == nil { - return "", "", false - } - version, modpath := impPkg.Version().Version, impPkg.Version().Path - if modpath == "" || version == "" { - return "", "", false - } - return modpath, version, true + return links, nil } // acceptedSchemes controls the schemes that URLs must have to be shown to the @@ -198,10 +202,11 @@ var acceptedSchemes = map[string]bool{ "https": true, } +// urlRegexp is the user-supplied regular expression to match URL. // tokFile may be a throwaway File for non-Go files. -func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string, pos token.Pos, tokFile *token.File, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) { +func findLinksInString(urlRegexp *regexp.Regexp, src string, pos token.Pos, tokFile *token.File, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) { var links []protocol.DocumentLink - for _, index := range snapshot.View().Options().URLRegexp.FindAllIndex([]byte(src), -1) { + for _, index := range urlRegexp.FindAllIndex([]byte(src), -1) { start, end := index[0], index[1] startPos := token.Pos(int(pos) + start) endPos := token.Pos(int(pos) + end) diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index bbea8949bca..b093afc7f37 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -854,7 +854,7 @@ func formatLink(h *HoverJSON, options *Options) string { } } -// BuildLink constructs a link with the given target, path, and anchor. +// BuildLink constructs a URL with the given target, path, and anchor. func BuildLink(target, path, anchor string) string { link := fmt.Sprintf("https://%s/%s", target, path) if anchor == "" { diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index a90093a01cf..02f60cb0a3b 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -34,6 +34,8 @@ func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []proto links[link.Src] = link.Target notePositions = append(notePositions, link.NotePosition) } + + var msg strings.Builder for _, link := range gotLinks { spn, err := mapper.RangeSpan(link.Range) if err != nil { @@ -51,19 +53,20 @@ func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []proto if linkInNote { continue } + if target, ok := links[spn]; ok { delete(links, spn) if target != link.Target { - return fmt.Sprintf("for %v want %v, got %v\n", spn, target, link.Target) + fmt.Fprintf(&msg, "%s: want link with target %q, got %q\n", spn, target, link.Target) } } else { - return fmt.Sprintf("unexpected link %v:%v\n", spn, link.Target) + fmt.Fprintf(&msg, "%s: got unexpected link with target %q\n", spn, link.Target) } } for spn, target := range links { - return fmt.Sprintf("missing link %v:%v\n", spn, target) + fmt.Fprintf(&msg, "%s: expected link with target %q is missing\n", spn, target) } - return "" + return msg.String() } // CompareDiagnostics reports testing errors to t when the diagnostic set got diff --git a/gopls/internal/regtest/misc/link_test.go b/gopls/internal/regtest/misc/link_test.go index 5e937f73a75..e1bf58954f6 100644 --- a/gopls/internal/regtest/misc/link_test.go +++ b/gopls/internal/regtest/misc/link_test.go @@ -67,11 +67,11 @@ const Hello = "Hello" } links := env.DocumentLink("main.go") if len(links) != 1 || links[0].Target != pkgLink { - t.Errorf("documentLink: got %v for main.go, want link to %q", links, pkgLink) + t.Errorf("documentLink: got links %+v for main.go, want one link with target %q", links, pkgLink) } links = env.DocumentLink("go.mod") if len(links) != 1 || links[0].Target != modLink { - t.Errorf("documentLink: got %v for go.mod, want link to %q", links, modLink) + t.Errorf("documentLink: got links %+v for go.mod, want one link with target %q", links, modLink) } // Then change the environment to make these links private. From c5343a6ac12f38b771b6577f5e5e80c8db515e05 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Fri, 2 Dec 2022 09:39:00 -0500 Subject: [PATCH 518/723] gopls/internal/lsp/regtest: fix TestRunGovulncheckError2 Changes CompletedProgress to take an optional WorkState which is filled when the expectation is met (i.e. completed progress) Fixes golang/go#57032 Change-Id: Ie876d4d5a739e31b758b5affa2e9d6e4fb772dd5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454775 Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Auto-Submit: Hyang-Ah Hana Kim Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/regtest/env.go | 9 ++++++--- gopls/internal/lsp/regtest/expectation.go | 16 ++++++++++++++-- gopls/internal/regtest/misc/vuln_test.go | 20 ++++++++++---------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go index 96c3db38a2b..192e8ed2a80 100644 --- a/gopls/internal/lsp/regtest/env.go +++ b/gopls/internal/lsp/regtest/env.go @@ -125,9 +125,9 @@ func (s State) startedWork() map[string]uint64 { } type workProgress struct { - title, msg string - percent float64 - complete bool + title, msg, endMsg string + percent float64 + complete bool // seen 'end'. } // This method, provided for debugging, accesses mutable fields without a lock, @@ -247,6 +247,9 @@ func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) erro } case "end": work.complete = true + if msg, ok := v["message"]; ok { + work.endMsg = msg.(string) + } } a.checkConditionsLocked() return nil diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index c365ae97bbd..c30a075886c 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -380,18 +380,30 @@ func CompletedWork(title string, count uint64, atLeast bool) SimpleExpectation { } } +type WorkStatus struct { + // Last seen message from either `begin` or `report` progress. + Msg string + // Message sent with `end` progress message. + EndMsg string +} + // CompletedProgress expects that workDone progress is complete for the given -// progress token. +// progress token. When non-nil WorkStatus is provided, it will be filled +// when the expectation is met. // // If the token is not a progress token that the client has seen, this // expectation is Unmeetable. -func CompletedProgress(token protocol.ProgressToken) SimpleExpectation { +func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) SimpleExpectation { check := func(s State) Verdict { work, ok := s.work[token] if !ok { return Unmeetable // TODO(rfindley): refactor to allow the verdict to explain this result } if work.complete { + if into != nil { + into.Msg = work.msg + into.EndMsg = work.endMsg + } return Met } return Unmet diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 18e500c44ca..12492d223a5 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -57,7 +57,6 @@ package foo } func TestRunGovulncheckError2(t *testing.T) { - t.Skip("skipping due to go.dev/issues/57032") const files = ` -- go.mod -- module mod.com @@ -81,13 +80,14 @@ func F() { // build error incomplete env.OpenFile("go.mod") var result command.RunVulncheckResult env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) + var ws WorkStatus env.Await( - OnceMet( - CompletedProgress(result.Token), - // TODO(hyangah): find a way to inspect $/progress 'report' message. - LogMatching(protocol.Info, "failed to load packages due to errors", 1, false), - ), + CompletedProgress(result.Token, &ws), ) + wantEndMsg, wantMsgPart := "failed", "failed to load packages due to errors" + if ws.EndMsg != "failed" || !strings.Contains(ws.Msg, wantMsgPart) { + t.Errorf("work status = %+v, want {EndMessage: %q, Message: %q}", ws, wantEndMsg, wantMsgPart) + } }) } @@ -227,7 +227,7 @@ func main() { env.Await( OnceMet( - CompletedProgress(result.Token), + CompletedProgress(result.Token, nil), ShownMessage("Found GOSTDLIB"), EmptyOrNoDiagnostics("go.mod"), ), @@ -588,7 +588,7 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( - CompletedProgress(result.Token), + CompletedProgress(result.Token, nil), ShownMessage("Found"), ), ) @@ -638,7 +638,7 @@ func TestRunVulncheckWarning(t *testing.T) { gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( - CompletedProgress(result.Token), + CompletedProgress(result.Token, nil), ShownMessage("Found"), ), ) @@ -799,7 +799,7 @@ func TestGovulncheckInfo(t *testing.T) { gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( - CompletedProgress(result.Token), + CompletedProgress(result.Token, nil), ShownMessage("No vulnerabilities found"), // only count affecting vulnerabilities. ), ) From 0f6c6f11ee8d5a4b944d21032b229ae9f89bef66 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Mon, 28 Nov 2022 20:02:14 -0500 Subject: [PATCH 519/723] gopls: delete obsolete govulncheck code and stop gopls vulncheck -summary This code was written before we have golang.org/x/vuln/exp/govulncheck. This change also removes the code to produce the old style `gopls vulncheck` output. `gopls vulncheck` will output the result `gopls vulncheck -summary` command outputted. Possibly the only user is vscode-go (Go: Run Vulncheck (Experimental)) which we are going to remove or update to use the new `gopls vulncheck` output format. The vscode command is experimental and I don't expect any serious users relying on it. So, it is safe to remove. Change-Id: I85a23b64bc573d110c8cdcdff5da72136488415e Reviewed-on: https://go-review.googlesource.com/c/tools/+/453836 Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Jamal Carvalho --- gopls/internal/govulncheck/README.md | 19 - gopls/internal/govulncheck/filepath.go | 38 -- gopls/internal/govulncheck/filepath_test.go | 42 --- gopls/internal/govulncheck/semver/semver.go | 33 -- .../govulncheck/semver/semver_test.go | 15 - gopls/internal/govulncheck/source.go | 116 ------ gopls/internal/govulncheck/util.go | 90 ----- gopls/internal/govulncheck/util_test.go | 85 ----- gopls/internal/lsp/cmd/vulncheck.go | 26 +- gopls/internal/lsp/command.go | 6 +- gopls/internal/migrate.sh | 68 ---- gopls/internal/vulncheck/command.go | 172 --------- gopls/internal/vulncheck/command_test.go | 343 ------------------ gopls/internal/vulncheck/util.go | 89 ----- gopls/internal/vulncheck/vulncheck.go | 4 - 15 files changed, 7 insertions(+), 1139 deletions(-) delete mode 100644 gopls/internal/govulncheck/README.md delete mode 100644 gopls/internal/govulncheck/filepath.go delete mode 100644 gopls/internal/govulncheck/filepath_test.go delete mode 100644 gopls/internal/govulncheck/source.go delete mode 100644 gopls/internal/govulncheck/util_test.go delete mode 100755 gopls/internal/migrate.sh delete mode 100644 gopls/internal/vulncheck/command_test.go delete mode 100644 gopls/internal/vulncheck/util.go diff --git a/gopls/internal/govulncheck/README.md b/gopls/internal/govulncheck/README.md deleted file mode 100644 index bc10d8a2ec1..00000000000 --- a/gopls/internal/govulncheck/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# internal/govulncheck package - -This package is a literal copy of the cmd/govulncheck/internal/govulncheck -package in the vuln repo (https://go.googlesource.com/vuln). - -The `copy.sh` does the copying, after removing all .go files here. To use it: - -1. Clone the vuln repo to a directory next to the directory holding this repo - (tools). After doing that your directory structure should look something like - ``` - ~/repos/x/tools/gopls/... - ~/repos/x/vuln/... - ``` - -2. cd to this directory. - -3. Run `copy.sh`. - -4. Re-add build tags for go1.18 \ No newline at end of file diff --git a/gopls/internal/govulncheck/filepath.go b/gopls/internal/govulncheck/filepath.go deleted file mode 100644 index cef78f5a47c..00000000000 --- a/gopls/internal/govulncheck/filepath.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -package govulncheck - -import ( - "path/filepath" - "strings" -) - -// AbsRelShorter takes path and returns its path relative -// to the current directory, if shorter. Returns path -// when path is an empty string or upon any error. -func AbsRelShorter(path string) string { - if path == "" { - return "" - } - - c, err := filepath.Abs(".") - if err != nil { - return path - } - r, err := filepath.Rel(c, path) - if err != nil { - return path - } - - rSegments := strings.Split(r, string(filepath.Separator)) - pathSegments := strings.Split(path, string(filepath.Separator)) - if len(rSegments) < len(pathSegments) { - return r - } - return path -} diff --git a/gopls/internal/govulncheck/filepath_test.go b/gopls/internal/govulncheck/filepath_test.go deleted file mode 100644 index 06ef40a1239..00000000000 --- a/gopls/internal/govulncheck/filepath_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -package govulncheck - -import ( - "os" - "path/filepath" - "testing" -) - -func TestAbsRelShorter(t *testing.T) { - thisFile := "filepath_test.go" - thisFileAbs, _ := filepath.Abs(thisFile) - - tf, err := os.CreateTemp("", "filepath_test.gp") - if err != nil { - t.Errorf("could not create temporary filepath_test.go file: %v", err) - } - tempFile := tf.Name() - tempFileAbs, _ := filepath.Abs(tempFile) - - for _, test := range []struct { - l string - want string - }{ - {thisFile, "filepath_test.go"}, - {thisFileAbs, "filepath_test.go"}, - // Relative path to temp file from "." is longer as - // it needs to go back the length of the absolute - // path and then in addition go to os.TempDir. - {tempFile, tempFileAbs}, - } { - if got := AbsRelShorter(test.l); got != test.want { - t.Errorf("want %s; got %s", test.want, got) - } - } -} diff --git a/gopls/internal/govulncheck/semver/semver.go b/gopls/internal/govulncheck/semver/semver.go index 8b1cfe55ea2..4ab298d137b 100644 --- a/gopls/internal/govulncheck/semver/semver.go +++ b/gopls/internal/govulncheck/semver/semver.go @@ -49,36 +49,3 @@ var ( // 5 the prerelease number tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc|-pre)(\d+))?$`) ) - -// This is a modified copy of pkgsite/internal/stdlib:VersionForTag. -func GoTagToSemver(tag string) string { - if tag == "" { - return "" - } - - tag = strings.Fields(tag)[0] - // Special cases for go1. - if tag == "go1" { - return "v1.0.0" - } - if tag == "go1.0" { - return "" - } - m := tagRegexp.FindStringSubmatch(tag) - if m == nil { - return "" - } - version := "v" + m[1] - if m[2] != "" { - version += m[2] - } else { - version += ".0" - } - if m[3] != "" { - if !strings.HasPrefix(m[4], "-") { - version += "-" - } - version += m[4] + "." + m[5] - } - return version -} diff --git a/gopls/internal/govulncheck/semver/semver_test.go b/gopls/internal/govulncheck/semver/semver_test.go index 56b6ea89999..6daead6855b 100644 --- a/gopls/internal/govulncheck/semver/semver_test.go +++ b/gopls/internal/govulncheck/semver/semver_test.go @@ -26,18 +26,3 @@ func TestCanonicalize(t *testing.T) { } } } - -func TestGoTagToSemver(t *testing.T) { - for _, test := range []struct { - v string - want string - }{ - {"go1.19", "v1.19.0"}, - {"go1.20-pre4", "v1.20.0-pre.4"}, - } { - got := GoTagToSemver(test.v) - if got != test.want { - t.Errorf("want %s; got %s", test.want, got) - } - } -} diff --git a/gopls/internal/govulncheck/source.go b/gopls/internal/govulncheck/source.go deleted file mode 100644 index d3f519d86ed..00000000000 --- a/gopls/internal/govulncheck/source.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -package govulncheck - -import ( - "fmt" - "sort" - "strings" - - "golang.org/x/tools/go/packages" - "golang.org/x/vuln/vulncheck" -) - -// A PackageError contains errors from loading a set of packages. -type PackageError struct { - Errors []packages.Error -} - -func (e *PackageError) Error() string { - var b strings.Builder - fmt.Fprintln(&b, "Packages contain errors:") - for _, e := range e.Errors { - fmt.Fprintln(&b, e) - } - return b.String() -} - -// LoadPackages loads the packages matching patterns using cfg, after setting -// the cfg mode flags that vulncheck needs for analysis. -// If the packages contain errors, a PackageError is returned containing a list of the errors, -// along with the packages themselves. -func LoadPackages(cfg *packages.Config, patterns ...string) ([]*vulncheck.Package, error) { - cfg.Mode |= packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | - packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | - packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps | - packages.NeedModule - - pkgs, err := packages.Load(cfg, patterns...) - vpkgs := vulncheck.Convert(pkgs) - if err != nil { - return nil, err - } - var perrs []packages.Error - packages.Visit(pkgs, nil, func(p *packages.Package) { - perrs = append(perrs, p.Errors...) - }) - if len(perrs) > 0 { - err = &PackageError{perrs} - } - return vpkgs, err -} - -// CallInfo is information about calls to vulnerable functions. -type CallInfo struct { - // CallStacks contains all call stacks to vulnerable functions. - CallStacks map[*vulncheck.Vuln][]vulncheck.CallStack - - // VulnGroups contains vulnerabilities grouped by ID and package. - VulnGroups [][]*vulncheck.Vuln - - // ModuleVersions is a map of module paths to versions. - ModuleVersions map[string]string - - // TopPackages contains the top-level packages in the call info. - TopPackages map[string]bool -} - -// GetCallInfo computes call stacks and related information from a vulncheck.Result. -// It also makes a set of top-level packages from pkgs. -func GetCallInfo(r *vulncheck.Result, pkgs []*vulncheck.Package) *CallInfo { - pset := map[string]bool{} - for _, p := range pkgs { - pset[p.PkgPath] = true - } - return &CallInfo{ - CallStacks: vulncheck.CallStacks(r), - VulnGroups: groupByIDAndPackage(r.Vulns), - ModuleVersions: moduleVersionMap(r.Modules), - TopPackages: pset, - } -} - -func groupByIDAndPackage(vs []*vulncheck.Vuln) [][]*vulncheck.Vuln { - groups := map[[2]string][]*vulncheck.Vuln{} - for _, v := range vs { - key := [2]string{v.OSV.ID, v.PkgPath} - groups[key] = append(groups[key], v) - } - - var res [][]*vulncheck.Vuln - for _, g := range groups { - res = append(res, g) - } - sort.Slice(res, func(i, j int) bool { - return res[i][0].PkgPath < res[j][0].PkgPath - }) - return res -} - -// moduleVersionMap builds a map from module paths to versions. -func moduleVersionMap(mods []*vulncheck.Module) map[string]string { - moduleVersions := map[string]string{} - for _, m := range mods { - v := m.Version - if m.Replace != nil { - v = m.Replace.Version - } - moduleVersions[m.Path] = v - } - return moduleVersions -} diff --git a/gopls/internal/govulncheck/util.go b/gopls/internal/govulncheck/util.go index 9bcb9b4aaed..544fba2a593 100644 --- a/gopls/internal/govulncheck/util.go +++ b/gopls/internal/govulncheck/util.go @@ -8,13 +8,9 @@ package govulncheck import ( - "fmt" - "strings" - "golang.org/x/mod/semver" isem "golang.org/x/tools/gopls/internal/govulncheck/semver" "golang.org/x/vuln/osv" - "golang.org/x/vuln/vulncheck" ) // LatestFixed returns the latest fixed version in the list of affected ranges, @@ -38,89 +34,3 @@ func LatestFixed(modulePath string, as []osv.Affected) string { } return v } - -// SummarizeCallStack returns a short description of the call stack. -// It uses one of two forms, depending on what the lowest function F in topPkgs -// calls: -// - If it calls a function V from the vulnerable package, then summarizeCallStack -// returns "F calls V". -// - If it calls a function G in some other package, which eventually calls V, -// it returns "F calls G, which eventually calls V". -// -// If it can't find any of these functions, summarizeCallStack returns the empty string. -func SummarizeCallStack(cs vulncheck.CallStack, topPkgs map[string]bool, vulnPkg string) string { - // Find the lowest function in the top packages. - iTop := lowest(cs, func(e vulncheck.StackEntry) bool { - return topPkgs[PkgPath(e.Function)] - }) - if iTop < 0 { - return "" - } - // Find the highest function in the vulnerable package that is below iTop. - iVuln := highest(cs[iTop+1:], func(e vulncheck.StackEntry) bool { - return PkgPath(e.Function) == vulnPkg - }) - if iVuln < 0 { - return "" - } - iVuln += iTop + 1 // adjust for slice in call to highest. - topName := FuncName(cs[iTop].Function) - topPos := AbsRelShorter(FuncPos(cs[iTop].Call)) - if topPos != "" { - topPos += ": " - } - vulnName := FuncName(cs[iVuln].Function) - if iVuln == iTop+1 { - return fmt.Sprintf("%s%s calls %s", topPos, topName, vulnName) - } - return fmt.Sprintf("%s%s calls %s, which eventually calls %s", - topPos, topName, FuncName(cs[iTop+1].Function), vulnName) -} - -// highest returns the highest (one with the smallest index) entry in the call -// stack for which f returns true. -func highest(cs vulncheck.CallStack, f func(e vulncheck.StackEntry) bool) int { - for i := 0; i < len(cs); i++ { - if f(cs[i]) { - return i - } - } - return -1 -} - -// lowest returns the lowest (one with the largets index) entry in the call -// stack for which f returns true. -func lowest(cs vulncheck.CallStack, f func(e vulncheck.StackEntry) bool) int { - for i := len(cs) - 1; i >= 0; i-- { - if f(cs[i]) { - return i - } - } - return -1 -} - -// PkgPath returns the package path from fn. -func PkgPath(fn *vulncheck.FuncNode) string { - if fn.PkgPath != "" { - return fn.PkgPath - } - s := strings.TrimPrefix(fn.RecvType, "*") - if i := strings.LastIndexByte(s, '.'); i > 0 { - s = s[:i] - } - return s -} - -// FuncName returns the function name from fn, adjusted -// to remove pointer annotations. -func FuncName(fn *vulncheck.FuncNode) string { - return strings.TrimPrefix(fn.String(), "*") -} - -// FuncPos returns the function position from call. -func FuncPos(call *vulncheck.CallSite) string { - if call != nil && call.Pos != nil { - return call.Pos.String() - } - return "" -} diff --git a/gopls/internal/govulncheck/util_test.go b/gopls/internal/govulncheck/util_test.go deleted file mode 100644 index 3288cd84c83..00000000000 --- a/gopls/internal/govulncheck/util_test.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -package govulncheck - -import ( - "strings" - "testing" - - "golang.org/x/vuln/vulncheck" -) - -func TestPkgPath(t *testing.T) { - for _, test := range []struct { - in vulncheck.FuncNode - want string - }{ - { - vulncheck.FuncNode{PkgPath: "math", Name: "Floor"}, - "math", - }, - { - vulncheck.FuncNode{RecvType: "a.com/b.T", Name: "M"}, - "a.com/b", - }, - { - vulncheck.FuncNode{RecvType: "*a.com/b.T", Name: "M"}, - "a.com/b", - }, - } { - got := PkgPath(&test.in) - if got != test.want { - t.Errorf("%+v: got %q, want %q", test.in, got, test.want) - } - } -} - -func TestSummarizeCallStack(t *testing.T) { - topPkgs := map[string]bool{"t1": true, "t2": true} - vulnPkg := "v" - - for _, test := range []struct { - in, want string - }{ - {"a.F", ""}, - {"t1.F", ""}, - {"v.V", ""}, - { - "t1.F v.V", - "t1.F calls v.V", - }, - { - "t1.F t2.G v.V1 v.v2", - "t2.G calls v.V1", - }, - { - "t1.F x.Y t2.G a.H b.I c.J v.V", - "t2.G calls a.H, which eventually calls v.V", - }, - } { - in := stringToCallStack(test.in) - got := SummarizeCallStack(in, topPkgs, vulnPkg) - if got != test.want { - t.Errorf("%s:\ngot %s\nwant %s", test.in, got, test.want) - } - } -} - -func stringToCallStack(s string) vulncheck.CallStack { - var cs vulncheck.CallStack - for _, e := range strings.Fields(s) { - parts := strings.Split(e, ".") - cs = append(cs, vulncheck.StackEntry{ - Function: &vulncheck.FuncNode{ - PkgPath: parts[0], - Name: parts[1], - }, - }) - } - return cs -} diff --git a/gopls/internal/lsp/cmd/vulncheck.go b/gopls/internal/lsp/cmd/vulncheck.go index c8f0358d39c..5c851b66e78 100644 --- a/gopls/internal/lsp/cmd/vulncheck.go +++ b/gopls/internal/lsp/cmd/vulncheck.go @@ -55,7 +55,7 @@ func (v *vulncheck) DetailedHelp(f *flag.FlagSet) { } func (v *vulncheck) Run(ctx context.Context, args ...string) error { - if vulnchecklib.Govulncheck == nil { + if vulnchecklib.Main == nil { return fmt.Errorf("vulncheck command is available only in gopls compiled with go1.18 or newer") } @@ -63,11 +63,6 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { if len(args) > 1 { return tool.CommandLineErrorf("vulncheck accepts at most one package pattern") } - pattern := "." - if len(args) == 1 { - pattern = args[0] - } - var cfg pkgLoadConfig if v.Config { if err := json.NewDecoder(os.Stdin).Decode(&cfg); err != nil { @@ -81,22 +76,9 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { // inherit the current process's cwd and env. } - if v.AsSummary { - if err := vulnchecklib.Main(loadCfg, args...); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - os.Exit(0) - } - // TODO(hyangah): delete. - res, err := vulnchecklib.Govulncheck(ctx, &loadCfg, pattern) - if err != nil { - return fmt.Errorf("vulncheck failed: %v", err) - } - data, err := json.MarshalIndent(res, " ", " ") - if err != nil { - return fmt.Errorf("vulncheck failed to encode result: %v", err) + if err := vulnchecklib.Main(loadCfg, args...); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } - fmt.Printf("%s", data) return nil } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index e09925a40e3..747adc8ad7e 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -886,13 +886,13 @@ func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.Vulnch view := deps.snapshot.View() opts := view.Options() // quickly test if gopls is compiled to support govulncheck - // by checking vulncheck.Govulncheck. Alternatively, we can continue and + // by checking vulncheck.Main. Alternatively, we can continue and // let the `gopls vulncheck` command fail. This is lighter-weight. - if vulncheck.Govulncheck == nil { + if vulncheck.Main == nil { return errors.New("vulncheck feature is not available") } - cmd := exec.CommandContext(ctx, os.Args[0], "vulncheck", "-summary", "-config", args.Pattern) + cmd := exec.CommandContext(ctx, os.Args[0], "vulncheck", "-config", args.Pattern) cmd.Dir = filepath.Dir(args.URI.SpanURI().Filename()) var viewEnv []string diff --git a/gopls/internal/migrate.sh b/gopls/internal/migrate.sh deleted file mode 100755 index 6f2bebc6ad6..00000000000 --- a/gopls/internal/migrate.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash -# -# Copyright 2022 The Go Authors. All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. -# -# Migrates the internal/lsp directory to gopls/internal/lsp. Run this script -# from the root of x/tools to migrate in-progress CLs. -# -# See golang/go#54509 for more details. This script may be deleted once a -# reasonable amount of time has passed such that all active in-progress CLs -# have been rebased. - -set -eu - -# A portable -i flag. Darwin requires two parameters. -# See https://stackoverflow.com/questions/5694228/sed-in-place-flag-that-works-both-on-mac-bsd-and-linux -# for more details. -sedi=(-i) -case "$(uname)" in - Darwin*) sedi=(-i "") -esac - -# mvpath moves the directory at the relative path $1 to the relative path $2, -# moving files and rewriting import paths. -# -# It uses heuristics to identify import path literals, and therefore may be -# imprecise. -function mvpath() { - # If the source also doesn't exist, it may have already been moved. - # Skip so that this script is idempotent. - if [[ ! -d $1 ]]; then - echo "WARNING: skipping nonexistent source directory $1" - return 0 - fi - - # git can sometimes leave behind empty directories, which can change the - # behavior of the mv command below. - if [[ -d $2 ]] || [[ -f $2 ]]; then - echo "ERROR: destination $2 already exists" - exit 1 - fi - - mv $1 $2 - - local old="golang.org/x/tools/$1" - local new="golang.org/x/tools/$2" - - # Replace instances of the old import path with the new. This is imprecise, - # but we are a bit careful to avoid replacing golang.org/x/tools/foox with - # golang.org/x/tools/barx when moving foo->bar: the occurrence of the import - # path must be followed by whitespace, /, or a closing " or `. - local replace="s:${old}\([[:space:]/\"\`]\):${new}\1:g" - find . -type f \( \ - -name ".git" -prune -o \ - -name "*.go" -o \ - -name "*.in" -o \ - -name "*.golden" -o \ - -name "*.hlp" -o \ - -name "*.md" \) \ - -exec sed "${sedi[@]}" -e $replace {} \; -} - -mvpath internal/lsp/diff internal/diff -mvpath internal/lsp/fuzzy internal/fuzzy -mvpath internal/lsp/debug/tag internal/event/tag -mvpath internal/lsp/bug internal/bug -mvpath internal/lsp gopls/internal/lsp diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 54c9fe5c9e3..4c4dec21c73 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -23,7 +23,6 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/govulncheck" - "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/vuln/client" gvcapi "golang.org/x/vuln/exp/govulncheck" @@ -32,31 +31,9 @@ import ( ) func init() { - Govulncheck = govulncheckFunc - VulnerablePackages = vulnerablePackages } -func govulncheckFunc(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) { - if patterns == "" { - patterns = "." - } - - dbClient, err := client.NewClient(findGOVULNDB(cfg.Env), client.Options{HTTPCache: govulncheck.DefaultCache()}) - if err != nil { - return res, err - } - - c := Cmd{Client: dbClient} - vulns, err := c.Run(ctx, cfg, patterns) - if err != nil { - return res, err - } - - res.Vuln = vulns - return res, err -} - func findGOVULNDB(env []string) []string { for _, kv := range env { if strings.HasPrefix(kv, "GOVULNDB=") { @@ -69,155 +46,6 @@ func findGOVULNDB(env []string) []string { return []string{"https://vuln.go.dev"} } -type Vuln = command.Vuln -type CallStack = command.CallStack -type StackEntry = command.StackEntry - -// Cmd is an in-process govulncheck command runner -// that uses the provided client.Client. -type Cmd struct { - Client client.Client -} - -// Run runs the govulncheck after loading packages using the provided packages.Config. -func (c *Cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) (_ []Vuln, err error) { - logger := log.New(log.Default().Writer(), "", 0) - cfg.Mode |= packages.NeedModule | packages.NeedName | packages.NeedFiles | - packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | - packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps - - logger.Println("loading packages...") - loadedPkgs, err := govulncheck.LoadPackages(cfg, patterns...) - if err != nil { - logger.Printf("%v", err) - return nil, fmt.Errorf("package load failed") - } - logger.Printf("analyzing %d packages...\n", len(loadedPkgs)) - - r, err := vulncheck.Source(ctx, loadedPkgs, &vulncheck.Config{Client: c.Client, SourceGoVersion: goVersion()}) - if err != nil { - return nil, err - } - - logger.Printf("selecting affecting vulnerabilities from %d findings...\n", len(r.Vulns)) - unaffectedMods := filterUnaffected(r.Vulns) - r.Vulns = filterCalled(r) - - logger.Printf("found %d vulnerabilities.\n", len(r.Vulns)) - callInfo := govulncheck.GetCallInfo(r, loadedPkgs) - return toVulns(callInfo, unaffectedMods) - // TODO: add import graphs. -} - -// filterCalled returns vulnerabilities where the symbols are actually called. -func filterCalled(r *vulncheck.Result) []*vulncheck.Vuln { - var vulns []*vulncheck.Vuln - for _, v := range r.Vulns { - if v.CallSink != 0 { - vulns = append(vulns, v) - } - } - return vulns -} - -// filterUnaffected returns vulnerabilities where no symbols are called, -// grouped by module. -func filterUnaffected(vulns []*vulncheck.Vuln) map[string][]*osv.Entry { - // It is possible that the same vuln.OSV.ID has vuln.CallSink != 0 - // for one symbol, but vuln.CallSink == 0 for a different one, so - // we need to filter out ones that have been called. - called := map[string]bool{} - for _, vuln := range vulns { - if vuln.CallSink != 0 { - called[vuln.OSV.ID] = true - } - } - - modToIDs := map[string]map[string]*osv.Entry{} - for _, vuln := range vulns { - if !called[vuln.OSV.ID] { - if _, ok := modToIDs[vuln.ModPath]; !ok { - modToIDs[vuln.ModPath] = map[string]*osv.Entry{} - } - // keep only one vuln.OSV instance for the same ID. - modToIDs[vuln.ModPath][vuln.OSV.ID] = vuln.OSV - } - } - output := map[string][]*osv.Entry{} - for m, vulnSet := range modToIDs { - var vulns []*osv.Entry - for _, vuln := range vulnSet { - vulns = append(vulns, vuln) - } - sort.Slice(vulns, func(i, j int) bool { return vulns[i].ID < vulns[j].ID }) - output[m] = vulns - } - return output -} - -func fixed(modPath string, v *osv.Entry) string { - lf := govulncheck.LatestFixed(modPath, v.Affected) - if lf != "" && lf[0] != 'v' { - lf = "v" + lf - } - return lf -} - -func toVulns(ci *govulncheck.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, error) { - var vulns []Vuln - - for _, vg := range ci.VulnGroups { - v0 := vg[0] - vuln := Vuln{ - ID: v0.OSV.ID, - PkgPath: v0.PkgPath, - CurrentVersion: ci.ModuleVersions[v0.ModPath], - FixedVersion: fixed(v0.ModPath, v0.OSV), - Details: v0.OSV.Details, - - Aliases: v0.OSV.Aliases, - Symbol: v0.Symbol, - ModPath: v0.ModPath, - URL: href(v0.OSV), - } - - // Keep first call stack for each vuln. - for _, v := range vg { - if css := ci.CallStacks[v]; len(css) > 0 { - vuln.CallStacks = append(vuln.CallStacks, toCallStack(css[0])) - // TODO(hyangah): https://go-review.googlesource.com/c/vuln/+/425183 added position info - // in the summary but we don't need the info. Allow SummarizeCallStack to skip it optionally. - sum := trimPosPrefix(govulncheck.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath)) - vuln.CallStackSummaries = append(vuln.CallStackSummaries, sum) - } - } - vulns = append(vulns, vuln) - } - for m, vg := range unaffectedMods { - for _, v0 := range vg { - vuln := Vuln{ - ID: v0.ID, - Details: v0.Details, - Aliases: v0.Aliases, - ModPath: m, - URL: href(v0), - CurrentVersion: "", - FixedVersion: fixed(m, v0), - } - vulns = append(vulns, vuln) - } - } - return vulns, nil -} - -func trimPosPrefix(summary string) string { - _, after, found := strings.Cut(summary, ": ") - if !found { - return summary - } - return after -} - // GoVersionForVulnTest is an internal environment variable used in gopls // testing to examine govulncheck behavior with a go version different // than what `go version` returns in the system. diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go deleted file mode 100644 index 4905fcd383d..00000000000 --- a/gopls/internal/vulncheck/command_test.go +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -package vulncheck_test - -import ( - "bytes" - "context" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "golang.org/x/tools/go/packages" - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/fake" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/vulncheck" - "golang.org/x/tools/gopls/internal/vulncheck/vulntest" - "golang.org/x/tools/internal/testenv" -) - -func TestCmd_Run(t *testing.T) { - runTest(t, workspace1, proxy1, func(ctx context.Context, snapshot source.Snapshot) { - db, err := vulntest.NewDatabase(ctx, []byte(vulnsData)) - if err != nil { - t.Fatal(err) - } - defer db.Clean() - cli, err := vulntest.NewClient(db) - if err != nil { - t.Fatal(err) - } - - cmd := &vulncheck.Cmd{Client: cli} - cfg := packagesCfg(ctx, snapshot) - result, err := cmd.Run(ctx, cfg, "./...") - if err != nil { - t.Fatal(err) - } - // Check that we find the right number of vulnerabilities. - // There should be three entries as there are three vulnerable - // symbols in the two import-reachable OSVs. - var got []report - for _, v := range result { - got = append(got, toReport(v)) - } - // drop the workspace root directory path included in the summary. - cwd := cfg.Dir - for _, g := range got { - for i, summary := range g.CallStackSummaries { - g.CallStackSummaries[i] = strings.ReplaceAll(summary, cwd, ".") - } - } - - var want = []report{ - { - Vuln: vulncheck.Vuln{ - ID: "GO-2022-01", - Details: "Something.\n", - Symbol: "VulnData.Vuln1", - PkgPath: "golang.org/amod/avuln", - ModPath: "golang.org/amod", - URL: "https://pkg.go.dev/vuln/GO-2022-01", - CurrentVersion: "v1.1.3", - FixedVersion: "v1.0.4", - CallStackSummaries: []string{ - "golang.org/entry/x.X calls golang.org/amod/avuln.VulnData.Vuln1", - "golang.org/entry/x.X calls golang.org/cmod/c.C1, which eventually calls golang.org/amod/avuln.VulnData.Vuln2", - }, - }, - CallStacksStr: []string{ - "golang.org/entry/x.X [approx.] (x.go:8)\n" + - "golang.org/amod/avuln.VulnData.Vuln1 (avuln.go:3)\n", - "golang.org/entry/x.X (x.go:8)\n" + - "golang.org/cmod/c.C1 (c.go:13)\n" + - "golang.org/amod/avuln.VulnData.Vuln2 (avuln.go:4)\n", - }, - }, - { - Vuln: vulncheck.Vuln{ - ID: "GO-2022-02", - Symbol: "Vuln", - PkgPath: "golang.org/bmod/bvuln", - ModPath: "golang.org/bmod", - URL: "https://pkg.go.dev/vuln/GO-2022-02", - CurrentVersion: "v0.5.0", - CallStackSummaries: []string{"golang.org/entry/y.Y calls golang.org/bmod/bvuln.Vuln"}, - }, - CallStacksStr: []string{ - "golang.org/entry/y.Y [approx.] (y.go:5)\n" + - "golang.org/bmod/bvuln.Vuln (bvuln.go:2)\n", - }, - }, - { - Vuln: vulncheck.Vuln{ - ID: "GO-2022-03", - Details: "unaffecting vulnerability.\n", - ModPath: "golang.org/amod", - URL: "https://pkg.go.dev/vuln/GO-2022-03", - FixedVersion: "v1.0.4", - }, - }, - } - // sort reports for stability before comparison. - for _, rpts := range [][]report{got, want} { - sort.Slice(rpts, func(i, j int) bool { - a, b := rpts[i], rpts[j] - if a.ID != b.ID { - return a.ID < b.ID - } - if a.PkgPath != b.PkgPath { - return a.PkgPath < b.PkgPath - } - return a.Symbol < b.Symbol - }) - } - if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(report{}, "Vuln.CallStacks")); diff != "" { - t.Error(diff) - } - - }) -} - -type report struct { - vulncheck.Vuln - // Trace is stringified Vuln.CallStacks - CallStacksStr []string -} - -func toReport(v vulncheck.Vuln) report { - var r = report{Vuln: v} - for _, s := range v.CallStacks { - r.CallStacksStr = append(r.CallStacksStr, CallStackString(s)) - } - return r -} - -func CallStackString(callstack vulncheck.CallStack) string { - var b bytes.Buffer - for _, entry := range callstack { - fname := filepath.Base(entry.URI.SpanURI().Filename()) - fmt.Fprintf(&b, "%v (%v:%d)\n", entry.Name, fname, entry.Pos.Line) - } - return b.String() -} - -const workspace1 = ` --- go.mod -- -module golang.org/entry - -require ( - golang.org/cmod v1.1.3 -) -go 1.18 --- x/x.go -- -package x - -import ( - "golang.org/cmod/c" - "golang.org/entry/y" -) - -func X() { - c.C1().Vuln1() // vuln use: X -> Vuln1 -} - -func CallY() { - y.Y() // vuln use: CallY -> y.Y -> bvuln.Vuln -} - --- y/y.go -- -package y - -import "golang.org/cmod/c" - -func Y() { - c.C2()() // vuln use: Y -> bvuln.Vuln -} -` - -const proxy1 = ` --- golang.org/cmod@v1.1.3/go.mod -- -module golang.org/cmod - -go 1.12 --- golang.org/cmod@v1.1.3/c/c.go -- -package c - -import ( - "golang.org/amod/avuln" - "golang.org/bmod/bvuln" -) - -type I interface { - Vuln1() -} - -func C1() I { - v := avuln.VulnData{} - v.Vuln2() // vuln use - return v -} - -func C2() func() { - return bvuln.Vuln -} --- golang.org/amod@v1.1.3/go.mod -- -module golang.org/amod - -go 1.14 --- golang.org/amod@v1.1.3/avuln/avuln.go -- -package avuln - -type VulnData struct {} -func (v VulnData) Vuln1() {} -func (v VulnData) Vuln2() {} --- golang.org/bmod@v0.5.0/go.mod -- -module golang.org/bmod - -go 1.14 --- golang.org/bmod@v0.5.0/bvuln/bvuln.go -- -package bvuln - -func Vuln() { - // something evil -} -` - -const vulnsData = ` --- GO-2022-01.yaml -- -modules: - - module: golang.org/amod - versions: - - introduced: 1.0.0 - - fixed: 1.0.4 - - introduced: 1.1.2 - packages: - - package: golang.org/amod/avuln - symbols: - - VulnData.Vuln1 - - VulnData.Vuln2 -description: | - Something. -references: - - href: pkg.go.dev/vuln/GO-2022-01 - --- GO-2022-03.yaml -- -modules: - - module: golang.org/amod - versions: - - introduced: 1.0.0 - - fixed: 1.0.4 - - introduced: 1.1.2 - packages: - - package: golang.org/amod/avuln - symbols: - - nonExisting -description: | - unaffecting vulnerability. - --- GO-2022-02.yaml -- -modules: - - module: golang.org/bmod - packages: - - package: golang.org/bmod/bvuln - symbols: - - Vuln -` - -func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Context, source.Snapshot)) { - testenv.NeedsGoBuild(t) - testenv.NeedsGoPackages(t) - - ws, err := fake.NewSandbox(&fake.SandboxConfig{ - Files: fake.UnpackTxt(workspaceData), - ProxyFiles: fake.UnpackTxt(proxyData), - }) - if err != nil { - t.Fatal(err) - } - defer ws.Close() - - ctx := tests.Context(t) - - // get the module cache populated and the go.sum file at the root auto-generated. - dir := ws.Workdir.RootURI().SpanURI().Filename() - if err := ws.RunGoCommand(ctx, dir, "list", []string{"-mod=mod", "..."}, true); err != nil { - t.Fatal(err) - } - - session := cache.NewSession(ctx, cache.New(nil, nil), nil) - options := source.DefaultOptions().Clone() - tests.DefaultOptions(options) - session.SetOptions(options) - envs := []string{} - for k, v := range ws.GoEnv() { - envs = append(envs, k+"="+v) - } - options.SetEnvSlice(envs) - name := ws.RootDir() - folder := ws.Workdir.RootURI().SpanURI() - view, snapshot, release, err := session.NewView(ctx, name, folder, options) - if err != nil { - t.Fatal(err) - } - - defer func() { - // The snapshot must be released before calling view.Shutdown, to avoid a - // deadlock. - release() - session.RemoveView(view) - }() - - test(ctx, snapshot) -} - -// TODO: expose this as a method of Snapshot. -func packagesCfg(ctx context.Context, snapshot source.Snapshot) *packages.Config { - view := snapshot.View() - viewBuildFlags := view.Options().BuildFlags - var viewEnv []string - if e := view.Options().EnvSlice(); e != nil { - viewEnv = append(os.Environ(), e...) - } - return &packages.Config{ - // Mode will be set by cmd.Run. - Context: ctx, - Tests: true, - BuildFlags: viewBuildFlags, - Env: viewEnv, - Dir: view.Folder().Filename(), - } -} diff --git a/gopls/internal/vulncheck/util.go b/gopls/internal/vulncheck/util.go deleted file mode 100644 index fe65c6eb4de..00000000000 --- a/gopls/internal/vulncheck/util.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.18 -// +build go1.18 - -package vulncheck - -import ( - "bytes" - "fmt" - "go/token" - "os" - "os/exec" - - gvc "golang.org/x/tools/gopls/internal/govulncheck" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/vuln/osv" - "golang.org/x/vuln/vulncheck" -) - -func toCallStack(src vulncheck.CallStack) CallStack { - var dest []StackEntry - for _, e := range src { - dest = append(dest, toStackEntry(e)) - } - return dest -} - -func toStackEntry(src vulncheck.StackEntry) StackEntry { - f, call := src.Function, src.Call - pos := f.Pos - desc := gvc.FuncName(f) - if src.Call != nil { - pos = src.Call.Pos // Exact call site position is helpful. - if !call.Resolved { - // In case of a statically unresolved call site, communicate to the client - // that this was approximately resolved to f - - desc += " [approx.]" - } - } - return StackEntry{ - Name: desc, - URI: filenameToURI(pos), - Pos: posToPosition(pos), - } -} - -// href returns the url for the vulnerability information. -// Eventually we should retrieve the url embedded in the osv.Entry. -// While vuln.go.dev is under development, this always returns -// the page in pkg.go.dev. -func href(vuln *osv.Entry) string { - return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vuln.ID) -} - -func filenameToURI(pos *token.Position) protocol.DocumentURI { - if pos == nil || pos.Filename == "" { - return "" - } - return protocol.URIFromPath(pos.Filename) -} - -func posToPosition(pos *token.Position) (p protocol.Position) { - // token.Position.Line starts from 1, and - // LSP protocol's position line is 0-based. - if pos != nil { - p.Line = uint32(pos.Line - 1) - // TODO(hyangah): LSP uses UTF16 column. - // We need utility like span.ToUTF16Column, - // but somthing that does not require file contents. - } - return p -} - -func goVersion() string { - if v := os.Getenv("GOVERSION"); v != "" { - // Unlikely to happen in practice, mostly used for testing. - return v - } - out, err := exec.Command("go", "env", "GOVERSION").Output() - if err != nil { - fmt.Fprintf(os.Stderr, "failed to determine go version; skipping stdlib scanning: %v\n", err) - return "" - } - return string(bytes.TrimSpace(out)) -} diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go index 71ac117c947..3c361bd01e4 100644 --- a/gopls/internal/vulncheck/vulncheck.go +++ b/gopls/internal/vulncheck/vulncheck.go @@ -13,14 +13,10 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/govulncheck" - "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/source" ) -// Govulncheck runs the in-process govulncheck implementation. // With go1.18+, this is swapped with the real implementation. -var Govulncheck func(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) = nil - var Main func(cfg packages.Config, patterns ...string) error = nil // VulnerablePackages queries the vulndb and reports which vulnerabilities From 96ff41d585069c1fc38a4ecfbe2da426bc357fba Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 23 Nov 2022 16:24:23 -0500 Subject: [PATCH 520/723] gopls/internal/vulncheck: add TODO for the vulncheck diagnostics Change-Id: I2980cd85b4627ba44c05194ae033cae2a361454c Reviewed-on: https://go-review.googlesource.com/c/tools/+/453275 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Alan Donovan --- gopls/internal/vulncheck/command.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 4c4dec21c73..45c7076f464 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -200,11 +200,26 @@ func finalDigitsIndex(s string) int { // apply to this snapshot. The result contains a set of packages, // grouped by vuln ID and by module. func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile source.FileHandle) (*govulncheck.Result, error) { + // We want to report the intersection of vulnerable packages in the vulndb + // and packages transitively imported by this module ('go list -deps all'). + // We use snapshot.AllValidMetadata to retrieve the list of packages + // as an approximation. + // + // TODO(hyangah): snapshot.AllValidMetadata is a superset of + // `go list all` - e.g. when the workspace has multiple main modules + // (multiple go.mod files), that can include packages that are not + // used by this module. Vulncheck behavior with go.work is not well + // defined. Figure out the meaning, and if we decide to present + // the result as if each module is analyzed independently, make + // gopls track a separate build list for each module and use that + // information instead of snapshot.AllValidMetadata. metadata, err := snapshot.AllValidMetadata(ctx) if err != nil { return nil, err } + // TODO(hyangah): handle vulnerabilities in the standard library. + // Group packages by modules since vuln db is keyed by module. metadataByModule := map[source.PackagePath][]*source.Metadata{} for _, md := range metadata { From f3fb218e75320162ef00d0678e4de7bbe3e1a1d4 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Thu, 8 Dec 2022 08:37:24 -0500 Subject: [PATCH 521/723] gopls/internal/lsp/fake: use robustio.RemoveAll in (*Workdir).RemoveFile os.RemoveAll is not reliable on Windows if the file may be concurrently opened, such as by a filesystem-polling goroutine. Fixes golang/go#57165. Change-Id: Ib2d2eb5c00b2e656936d8fac5b93810bdfd2ff52 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456119 TryBot-Result: Gopher Robot Auto-Submit: Bryan Mills Reviewed-by: Alan Donovan Run-TryBot: Bryan Mills gopls-CI: kokoro --- gopls/internal/lsp/fake/workdir.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index a76a3b5391d..8f0e796b046 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -219,7 +219,7 @@ func (w *Workdir) RegexpSearch(path string, re string) (Pos, error) { // change. func (w *Workdir) RemoveFile(ctx context.Context, path string) error { fp := w.AbsPath(path) - if err := os.RemoveAll(fp); err != nil { + if err := robustio.RemoveAll(fp); err != nil { return fmt.Errorf("removing %q: %w", path, err) } From d7dfffd6c681fea482c0f3c8cba1b88de13c4fe9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 8 Dec 2022 00:37:50 -0500 Subject: [PATCH 522/723] gopls/internal/lsp: eliminate more unnecessary typechecking This change downgrades type-checking operations to mere metadata queries or calls to the parser in these places: - cmd toggle-gc-details - cmd list-imports - references - rename (3 places) The parsePackageNameDecl function (formerly isInPackageName) only parses the package and imports declarations. Also, eliminate the last two uses of Snapshot.WorkspacePackageByID and the method itself. Change-Id: I2bd24c24c9a2c14c9ba15e9624222b4aaf231c79 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456040 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/snapshot.go | 4 -- gopls/internal/lsp/command.go | 31 ++++++++------ gopls/internal/lsp/source/references.go | 50 +++++++++------------- gopls/internal/lsp/source/rename.go | 57 +++++++++++-------------- gopls/internal/lsp/source/view.go | 5 +-- 5 files changed, 65 insertions(+), 82 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 8b02dea585b..52c5a26fc7b 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1197,10 +1197,6 @@ func (s *snapshot) AllValidMetadata(ctx context.Context) ([]*source.Metadata, er return meta, nil } -func (s *snapshot) WorkspacePackageByID(ctx context.Context, id PackageID) (source.Package, error) { - return s.checkedPackage(ctx, id, s.workspaceParseMode(id)) -} - func (s *snapshot) CachedImportPaths(ctx context.Context) (map[PackagePath]source.Package, error) { // Don't reload workspace package metadata. // This function is meant to only return currently cached information. diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 747adc8ad7e..4bbf0a97e9f 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -691,17 +691,17 @@ func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIAr progress: "Toggling GC Details", forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { - // TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed. - pkg, err := deps.snapshot.PackageForFile(ctx, deps.fh.URI(), source.TypecheckWorkspace, source.NarrowestPackage) + metas, err := deps.snapshot.MetadataForFile(ctx, deps.fh.URI()) if err != nil { return err } + id := metas[0].ID // 0 => narrowest package c.s.gcOptimizationDetailsMu.Lock() - if _, ok := c.s.gcOptimizationDetails[pkg.ID()]; ok { - delete(c.s.gcOptimizationDetails, pkg.ID()) + if _, ok := c.s.gcOptimizationDetails[id]; ok { + delete(c.s.gcOptimizationDetails, id) c.s.clearDiagnosticSource(gcDetailsSource) } else { - c.s.gcOptimizationDetails[pkg.ID()] = struct{}{} + c.s.gcOptimizationDetails[id] = struct{}{} } c.s.gcOptimizationDetailsMu.Unlock() c.s.diagnoseSnapshot(deps.snapshot, nil, false) @@ -758,16 +758,15 @@ func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) ( err := c.run(ctx, commandConfig{ forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { - // TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed. - pkg, err := deps.snapshot.PackageForFile(ctx, args.URI.SpanURI(), source.TypecheckWorkspace, source.NarrowestPackage) + fh, err := deps.snapshot.GetFile(ctx, args.URI.SpanURI()) if err != nil { return err } - pgf, err := pkg.File(args.URI.SpanURI()) + pgf, err := deps.snapshot.ParseGo(ctx, fh, source.ParseHeader) if err != nil { return err } - for _, group := range astutil.Imports(pkg.FileSet(), pgf.File) { + for _, group := range astutil.Imports(deps.snapshot.FileSet(), pgf.File) { for _, imp := range group { if imp.Path == nil { continue @@ -782,10 +781,16 @@ func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) ( }) } } - for _, imp := range pkg.Imports() { - result.PackageImports = append(result.PackageImports, command.PackageImport{ - Path: string(imp.PkgPath()), // This might be the vendored path under GOPATH vendoring, in which case it's a bug. - }) + metas, err := deps.snapshot.MetadataForFile(ctx, args.URI.SpanURI()) + if err != nil { + return err // e.g. cancelled + } + if len(metas) == 0 { + return fmt.Errorf("no package containing %v", args.URI.SpanURI()) + } + for pkgPath := range metas[0].DepsByPkgPath { // 0 => narrowest package + result.PackageImports = append(result.PackageImports, + command.PackageImport{Path: string(pkgPath)}) } sort.Slice(result.PackageImports, func(i, j int) bool { return result.PackageImports[i].Path < result.PackageImports[j].Path diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index bf3784bd634..49b82948ab0 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -9,12 +9,10 @@ import ( "errors" "fmt" "go/ast" - "strings" - "go/token" "go/types" "sort" - "strconv" + "strings" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/span" @@ -32,36 +30,17 @@ type ReferenceInfo struct { isDeclaration bool } -// isInPackageName reports whether the file's package name surrounds the -// given position pp (e.g. "foo" surrounds the cursor in "package foo"). -func isInPackageName(ctx context.Context, s Snapshot, f FileHandle, pgf *ParsedGoFile, pp protocol.Position) (bool, error) { - // Find position of the package name declaration - cursorPos, err := pgf.Mapper.Pos(pp) - if err != nil { - return false, err - } - - return pgf.File.Name.Pos() <= cursorPos && cursorPos <= pgf.File.Name.End(), nil -} - // References returns a list of references for a given identifier within the packages // containing i.File. Declarations appear first in the result. func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) { ctx, done := event.Start(ctx, "source.References") defer done() - // Find position of the package name declaration - pgf, err := s.ParseGo(ctx, f, ParseFull) + // Is the cursor within the package name declaration? + pgf, inPackageName, err := parsePackageNameDecl(ctx, s, f, pp) if err != nil { return nil, err } - - packageName := pgf.File.Name.Name // from package decl - inPackageName, err := isInPackageName(ctx, s, f, pgf, pp) - if err != nil { - return nil, err - } - if inPackageName { // TODO(rfindley): this is inaccurate, excluding test variants, and // redundant with package renaming. Refactor to share logic. @@ -79,13 +58,12 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit for _, dep := range rdeps { for _, f := range dep.CompiledGoFiles() { for _, imp := range f.File.Imports { - // TODO(adonovan): using UnquoteImport() here would - // reveal that there's an ImportPath==PackagePath + // TODO(adonovan): there's an ImportPath==PackagePath // comparison that doesn't account for vendoring. // Use dep.Metadata().DepsByPkgPath instead. - if path, err := strconv.Unquote(imp.Path.Value); err == nil && path == string(renamingPkg.PkgPath()) { + if string(UnquoteImportPath(imp)) == string(renamingPkg.PkgPath()) { refs = append(refs, &ReferenceInfo{ - Name: packageName, + Name: pgf.File.Name.Name, MappedRange: NewMappedRange(f.Tok, f.Mapper, imp.Pos(), imp.End()), }) } @@ -96,7 +74,7 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit // Find internal references to the package within the package itself for _, f := range renamingPkg.CompiledGoFiles() { refs = append(refs, &ReferenceInfo{ - Name: packageName, + Name: pgf.File.Name.Name, MappedRange: NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()), }) } @@ -132,6 +110,20 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit return refs, nil } +// parsePackageNameDecl is a convenience function that parses and +// returns the package name declaration of file fh, and reports +// whether the position ppos lies within it. +func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, ppos protocol.Position) (*ParsedGoFile, bool, error) { + pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) + if err != nil { + return nil, false, err + } + // Careful: because we used ParseHeader, + // Mapper.Pos(ppos) may be beyond EOF => (0, err). + pos, _ := pgf.Mapper.Pos(ppos) + return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil +} + // references is a helper function to avoid recomputing qualifiedObjsAtProtocolPos. // The first element of qos is considered to be the declaration; // if isDeclaration, the first result is an extra item for it. diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 51555b0ba1f..6855e20a90d 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -53,15 +53,12 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot // Find position of the package name declaration. ctx, done := event.Start(ctx, "source.PrepareRename") defer done() - pgf, err := snapshot.ParseGo(ctx, f, ParseFull) - if err != nil { - return nil, err, err - } - inPackageName, err := isInPackageName(ctx, snapshot, f, pgf, pp) + + // Is the cursor within the package name declaration? + pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) if err != nil { return nil, err, err } - if inPackageName { fileRenameSupported := false for _, op := range snapshot.View().Options().SupportedResourceOperations { @@ -106,17 +103,16 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path) return nil, err, err } - // TODO(rfindley): we should not need the package here. - pkg, err := snapshot.WorkspacePackageByID(ctx, meta.ID) + + // Return the location of the package declaration. + rng, err := pgf.Mapper.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) if err != nil { - err = fmt.Errorf("error building package to rename: %v", err) return nil, err, err } - result, err := computePrepareRenameResp(snapshot, pkg, pgf.File.Name, string(pkg.Name())) - if err != nil { - return nil, nil, err - } - return result, nil, nil + return &PrepareItem{ + Range: rng, + Text: string(meta.Name), + }, nil, nil } qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp) @@ -171,15 +167,11 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, ctx, done := event.Start(ctx, "source.Rename") defer done() - pgf, err := s.ParseGo(ctx, f, ParseFull) - if err != nil { - return nil, false, err - } - inPackageName, err := isInPackageName(ctx, s, f, pgf, pp) + // Is the cursor within the package name declaration? + _, inPackageName, err := parsePackageNameDecl(ctx, s, f, pp) if err != nil { return nil, false, err } - if inPackageName { if !isValidIdentifier(newName) { return nil, true, fmt.Errorf("%q is not a valid identifier", newName) @@ -336,23 +328,24 @@ func (s seenPackageRename) add(uri span.URI, path PackagePath) bool { // package clause has already been updated, to prevent duplicate edits. // // Edits are written into the edits map. -func renamePackageClause(ctx context.Context, m *Metadata, s Snapshot, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { - pkg, err := s.WorkspacePackageByID(ctx, m.ID) - if err != nil { - return err - } - +func renamePackageClause(ctx context.Context, m *Metadata, snapshot Snapshot, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { // Rename internal references to the package in the renaming package. - for _, f := range pkg.CompiledGoFiles() { - if seen.add(f.URI, m.PkgPath) { + for _, uri := range m.CompiledGoFiles { + if seen.add(uri, m.PkgPath) { continue } - + fh, err := snapshot.GetFile(ctx, uri) + if err != nil { + return err + } + f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + if err != nil { + return err + } if f.File.Name == nil { - continue + continue // no package declaration } - pkgNameMappedRange := NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()) - rng, err := pkgNameMappedRange.Range() + rng, err := f.Mapper.PosRange(f.File.Name.Pos(), f.File.Name.End()) if err != nil { return err } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 1084c744976..1168370c6f8 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -104,6 +104,7 @@ type Snapshot interface { // ParseGo returns the parsed AST for the file. // If the file is not available, returns nil and an error. + // Position information is added to FileSet(). ParseGo(ctx context.Context, fh FileHandle, mode ParseMode) (*ParsedGoFile, error) // DiagnosePackage returns basic diagnostics, including list, parse, and type errors @@ -206,10 +207,6 @@ type Snapshot interface { // AllValidMetadata returns all valid metadata loaded for the snapshot. AllValidMetadata(ctx context.Context) ([]*Metadata, error) - // WorkspacePackageByID returns the workspace package with id, type checked - // in 'workspace' mode. - WorkspacePackageByID(ctx context.Context, id PackageID) (Package, error) - // Symbols returns all symbols in the snapshot. Symbols(ctx context.Context) map[span.URI][]Symbol From 6b8674f1084eb37aa8c6d5070d899ef4642708d7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 7 Dec 2022 15:21:01 -0500 Subject: [PATCH 523/723] gopls/internal/lsp/source: avoid typechecking in findRune Hovering over a letter should not require the type checker. Another small step towards avoidung unnecessary package loading. Also, optimize the AST inspection by skipping subtrees that don't contain pos. Change-Id: I8b05a550c3ad0bd4a6bfdbd9fd919ee1f030dd24 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456035 Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/source/hover.go | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index b093afc7f37..dd13e2acc12 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -139,8 +139,11 @@ var ErrNoRuneFound = errors.New("no rune found") // findRune returns rune information for a position in a file. func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, MappedRange, error) { - // TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed. - pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) + fh, err := snapshot.GetFile(ctx, fh.URI()) + if err != nil { + return 0, MappedRange{}, err + } + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return 0, MappedRange{}, err } @@ -151,18 +154,18 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr // Find the basic literal enclosing the given position, if there is one. var lit *ast.BasicLit - var found bool ast.Inspect(pgf.File, func(n ast.Node) bool { - if found { + if n == nil || // pop + lit != nil || // found: terminate the search + !(n.Pos() <= pos && pos < n.End()) { // subtree does not contain pos: skip return false } - if n, ok := n.(*ast.BasicLit); ok && pos >= n.Pos() && pos <= n.End() { - lit = n - found = true + if n, ok := n.(*ast.BasicLit); ok { + lit = n // found! } - return !found + return lit == nil // descend unless target is found }) - if !found { + if lit == nil { return 0, MappedRange{}, ErrNoRuneFound } @@ -237,12 +240,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr default: return 0, MappedRange{}, ErrNoRuneFound } - - mappedRange, err := posToMappedRange(pkg, start, end) - if err != nil { - return 0, MappedRange{}, err - } - return r, mappedRange, nil + return r, NewMappedRange(pgf.Tok, pgf.Mapper, start, end), nil } func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) { From 3f74d914ae6d779f5346cf227a72a43706664f82 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Wed, 7 Dec 2022 22:48:28 -0500 Subject: [PATCH 524/723] gopls/internal/lsp/cache: invalidate govulncheck results older than 1hr 1hr is long enough to make the computed results stale either because the vuln db is updated or the code is changed. Drop old results. Note: OSV entry caching TTL used by the govulncheck command line tool is 2hr. https://github.com/golang/vuln/blob/05fb7250142cc6010c39968839f2f3710afdd918/client/client.go#L230 Change-Id: I17ac5307a0e966bb91be74dcaf25cee14781eb2d Reviewed-on: https://go-review.googlesource.com/c/tools/+/456255 Run-TryBot: Hyang-Ah Hana Kim Auto-Submit: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller Reviewed-by: Robert Findley --- gopls/internal/govulncheck/types.go | 10 +++- gopls/internal/lsp/cache/view.go | 23 ++++++--- gopls/internal/lsp/cache/view_test.go | 68 +++++++++++++++++++++++++++ gopls/internal/lsp/command.go | 2 + 4 files changed, 94 insertions(+), 9 deletions(-) diff --git a/gopls/internal/govulncheck/types.go b/gopls/internal/govulncheck/types.go index 75240aa7d35..71984519e02 100644 --- a/gopls/internal/govulncheck/types.go +++ b/gopls/internal/govulncheck/types.go @@ -4,11 +4,13 @@ package govulncheck +import "time" + // Result is the result of vulnerability scanning. type Result struct { // Vulns contains all vulnerabilities that are called or imported by // the analyzed module. - Vulns []*Vuln + Vulns []*Vuln `json:",omitempty"` // Mode contains the source of the vulnerability info. // Clients of the gopls.fetch_vulncheck_result command may need @@ -19,7 +21,11 @@ type Result struct { // without callstack traces just implies the package with the // vulnerability is known to the workspace and we do not know // whether the vulnerable symbols are actually used or not. - Mode AnalysisMode + Mode AnalysisMode `json:",omitempty"` + + // AsOf describes when this Result was computed using govulncheck. + // It is valid only with the govulncheck analysis mode. + AsOf time.Time `json:",omitempty"` } type AnalysisMode string diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 0f4272ef87a..7b5c05b759d 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -19,6 +19,7 @@ import ( "sort" "strings" "sync" + "time" "golang.org/x/mod/modfile" "golang.org/x/mod/semver" @@ -1038,19 +1039,27 @@ func (v *View) ClearModuleUpgrades(modfile span.URI) { delete(v.moduleUpgrades, modfile) } -func (v *View) Vulnerabilities(modfile ...span.URI) map[span.URI]*govulncheck.Result { +const maxGovulncheckResultAge = 1 * time.Hour // Invalidate results older than this limit. +var timeNow = time.Now // for testing + +func (v *View) Vulnerabilities(modfiles ...span.URI) map[span.URI]*govulncheck.Result { m := make(map[span.URI]*govulncheck.Result) + now := timeNow() v.mu.Lock() defer v.mu.Unlock() - if len(modfile) == 0 { - for k, v := range v.vulns { - m[k] = v + if len(modfiles) == 0 { // empty means all modfiles + for modfile := range v.vulns { + modfiles = append(modfiles, modfile) } - return m } - for _, f := range modfile { - m[f] = v.vulns[f] + for _, modfile := range modfiles { + vuln := v.vulns[modfile] + if vuln != nil && now.Sub(vuln.AsOf) > maxGovulncheckResultAge { + v.vulns[modfile] = nil // same as SetVulnerabilities(modfile, nil) + vuln = nil + } + m[modfile] = vuln } return m } diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go index f57fc808e80..b2fca90f151 100644 --- a/gopls/internal/lsp/cache/view_test.go +++ b/gopls/internal/lsp/cache/view_test.go @@ -5,11 +5,15 @@ package cache import ( "context" + "encoding/json" "io/ioutil" "os" "path/filepath" "testing" + "time" + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" @@ -208,3 +212,67 @@ func TestSuffixes(t *testing.T) { } } } + +func TestView_Vulnerabilities(t *testing.T) { + // TODO(hyangah): use t.Cleanup when we get rid of go1.13 legacy CI. + defer func() { timeNow = time.Now }() + + now := time.Now() + + view := &View{ + vulns: make(map[span.URI]*govulncheck.Result), + } + file1, file2 := span.URIFromPath("f1/go.mod"), span.URIFromPath("f2/go.mod") + + vuln1 := &govulncheck.Result{AsOf: now.Add(-(maxGovulncheckResultAge * 3) / 4)} // already ~3/4*maxGovulncheckResultAge old + view.SetVulnerabilities(file1, vuln1) + + vuln2 := &govulncheck.Result{AsOf: now} // fresh. + view.SetVulnerabilities(file2, vuln2) + + t.Run("fresh", func(t *testing.T) { + got := view.Vulnerabilities() + want := map[span.URI]*govulncheck.Result{ + file1: vuln1, + file2: vuln2, + } + + if diff := cmp.Diff(toJSON(want), toJSON(got)); diff != "" { + t.Errorf("view.Vulnerabilities() mismatch (-want +got):\n%s", diff) + } + }) + + // maxGovulncheckResultAge/2 later + timeNow = func() time.Time { return now.Add(maxGovulncheckResultAge / 2) } + t.Run("after30min", func(t *testing.T) { + got := view.Vulnerabilities() + want := map[span.URI]*govulncheck.Result{ + file1: nil, // expired. + file2: vuln2, + } + + if diff := cmp.Diff(toJSON(want), toJSON(got)); diff != "" { + t.Errorf("view.Vulnerabilities() mismatch (-want +got):\n%s", diff) + } + }) + + // maxGovulncheckResultAge later + timeNow = func() time.Time { return now.Add(maxGovulncheckResultAge + time.Minute) } + + t.Run("after1hr", func(t *testing.T) { + got := view.Vulnerabilities() + want := map[span.URI]*govulncheck.Result{ + file1: nil, + file2: nil, + } + + if diff := cmp.Diff(toJSON(want), toJSON(got)); diff != "" { + t.Errorf("view.Vulnerabilities() mismatch (-want +got):\n%s", diff) + } + }) +} + +func toJSON(x interface{}) string { + b, _ := json.MarshalIndent(x, "", " ") + return string(b) +} diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 4bbf0a97e9f..2d89f44a927 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -17,6 +17,7 @@ import ( "path/filepath" "sort" "strings" + "time" "golang.org/x/mod/modfile" "golang.org/x/tools/go/ast/astutil" @@ -933,6 +934,7 @@ func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.Vulnch return fmt.Errorf("failed to parse govulncheck output: %v", err) } result.Mode = govulncheck.ModeGovulncheck + result.AsOf = time.Now() deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), &result) c.s.diagnoseSnapshot(deps.snapshot, nil, false) From 88ceb2401c4d2a6357e13bad8f3424e9a349fde9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 8 Dec 2022 13:40:45 -0500 Subject: [PATCH 525/723] gopls/internal/lsp: perform analysis unconditionally Previous we would check (in a couple of places) that the package was error-free, but this was only due to bugs in the analyzer driver that are now fixed. Change-Id: I9cfba814e85c2561b902dd785fba1c7404f7a0c1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456315 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/diagnostics.go | 2 +- gopls/internal/lsp/source/diagnostics.go | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index e3fef660780..dec452b2454 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -378,7 +378,7 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, pkgDiagnostics[cgf.URI], true) } } - if includeAnalysis && !pkg.HasListOrParseErrors() { + if includeAnalysis { reports, err := source.Analyze(ctx, snapshot, pkg, false) if err != nil { event.Error(ctx, "warning: analyzing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(pkg.ID())))...) diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index 8a382043506..7780f471f11 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -31,13 +31,13 @@ func Analyze(ctx context.Context, snapshot Snapshot, pkg Package, includeConveni return nil, ctx.Err() } - categories := []map[string]*Analyzer{} - if includeConvenience { - categories = append(categories, snapshot.View().Options().ConvenienceAnalyzers) + options := snapshot.View().Options() + categories := []map[string]*Analyzer{ + options.DefaultAnalyzers, + options.StaticcheckAnalyzers, } - // If we had type errors, don't run any other analyzers. - if !pkg.HasTypeErrors() { - categories = append(categories, snapshot.View().Options().DefaultAnalyzers, snapshot.View().Options().StaticcheckAnalyzers) + if includeConvenience { // e.g. for codeAction + categories = append(categories, options.ConvenienceAnalyzers) // e.g. fillstruct } var analyzers []*Analyzer for _, cat := range categories { @@ -72,13 +72,10 @@ func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (Vers if err != nil { return VersionedFileIdentity{}, nil, err } - fileDiags := diagnostics[fh.URI()] - if !pkg.HasListOrParseErrors() { - analysisDiags, err := Analyze(ctx, snapshot, pkg, false) - if err != nil { - return VersionedFileIdentity{}, nil, err - } - fileDiags = append(fileDiags, analysisDiags[fh.URI()]...) + analysisDiags, err := Analyze(ctx, snapshot, pkg, false) + if err != nil { + return VersionedFileIdentity{}, nil, err } + fileDiags := append(diagnostics[fh.URI()], analysisDiags[fh.URI()]...) return fh.VersionedFileIdentity(), fileDiags, nil } From b0fdb78b2a0b8186d5f304ec3d4ecb933f065d52 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 8 Dec 2022 10:05:38 -0500 Subject: [PATCH 526/723] gopls/internal/lsp/mod: add Reset vulncheck result codelens We hope this gives an option to more conveniently hide unactionable govulncheck diagnostics while we are still looking for a better option than placing "Run govulncheck to verify" and "Reset govulncheck result" in Quick Fix. The command for the code action is implemented by extending the existing go mod diagnostics reset command to accept an optional diagnostic source name. If the name is given, only the go.mod diagnostics that match the name will be reset. This CL also renamed "Run govulncheck" to "Run govulncheck to verify" codelens. Change-Id: Id4e809083d572437f86380a22c70547ec12f9976 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456256 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Hyang-Ah Hana Kim --- gopls/doc/commands.md | 8 ++- gopls/internal/lsp/command.go | 17 +++--- gopls/internal/lsp/command/command_gen.go | 4 +- gopls/internal/lsp/command/interface.go | 10 +++- gopls/internal/lsp/mod/code_lens.go | 4 +- gopls/internal/lsp/mod/diagnostics.go | 63 +++++++++++++++-------- gopls/internal/lsp/source/api_json.go | 2 +- gopls/internal/regtest/misc/vuln_test.go | 40 ++++++++++++-- 8 files changed, 109 insertions(+), 39 deletions(-) diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 86cc41e4891..9d5e851ccac 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -276,8 +276,12 @@ Args: ``` { - // The file URI. - "URI": string, + "URIArg": { + "URI": string, + }, + // Optional: source of the diagnostics to reset. + // If not set, all resettable go.mod diagnostics will be cleared. + "DiagnosticSource": string, } ``` diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 2d89f44a927..aba9f029d9c 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -211,17 +211,22 @@ func (c *commandHandler) UpgradeDependency(ctx context.Context, args command.Dep return c.GoGetModule(ctx, args) } -func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, uri command.URIArg) error { +func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, args command.ResetGoModDiagnosticsArgs) error { return c.run(ctx, commandConfig{ - forURI: uri.URI, + forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { - deps.snapshot.View().ClearModuleUpgrades(uri.URI.SpanURI()) - deps.snapshot.View().SetVulnerabilities(uri.URI.SpanURI(), nil) // Clear all diagnostics coming from the upgrade check source and vulncheck. // This will clear the diagnostics in all go.mod files, but they // will be re-calculated when the snapshot is diagnosed again. - c.s.clearDiagnosticSource(modCheckUpgradesSource) - c.s.clearDiagnosticSource(modVulncheckSource) + if args.DiagnosticSource == "" || args.DiagnosticSource == string(source.UpgradeNotification) { + deps.snapshot.View().ClearModuleUpgrades(args.URI.SpanURI()) + c.s.clearDiagnosticSource(modCheckUpgradesSource) + } + + if args.DiagnosticSource == "" || args.DiagnosticSource == string(source.Vulncheck) { + deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), nil) + c.s.clearDiagnosticSource(modVulncheckSource) + } // Re-diagnose the snapshot to remove the diagnostics. c.s.diagnoseSnapshot(deps.snapshot, nil, false) diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go index 1c76fb0ec85..35d37ed4d6c 100644 --- a/gopls/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -159,7 +159,7 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte } return nil, s.RemoveDependency(ctx, a0) case "gopls.reset_go_mod_diagnostics": - var a0 URIArg + var a0 ResetGoModDiagnosticsArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { return nil, err } @@ -392,7 +392,7 @@ func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) (protocol }, nil } -func NewResetGoModDiagnosticsCommand(title string, a0 URIArg) (protocol.Command, error) { +func NewResetGoModDiagnosticsCommand(title string, a0 ResetGoModDiagnosticsArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { return protocol.Command{}, err diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index ebc967c8f38..ec910fc43d3 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -102,7 +102,7 @@ type Interface interface { // ResetGoModDiagnostics: Reset go.mod diagnostics // // Reset diagnostics in the go.mod file of a module. - ResetGoModDiagnostics(context.Context, URIArg) error + ResetGoModDiagnostics(context.Context, ResetGoModDiagnosticsArgs) error // GoGetPackage: go get a package // @@ -309,6 +309,14 @@ type DebuggingResult struct { URLs []string } +type ResetGoModDiagnosticsArgs struct { + URIArg + + // Optional: source of the diagnostics to reset. + // If not set, all resettable go.mod diagnostics will be cleared. + DiagnosticSource string +} + type VulncheckArgs struct { // Any document in the directory from which govulncheck will run. URI protocol.DocumentURI diff --git a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go index 4e08f4a2c7c..99684346362 100644 --- a/gopls/internal/lsp/mod/code_lens.go +++ b/gopls/internal/lsp/mod/code_lens.go @@ -32,7 +32,7 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File return nil, err } uri := protocol.URIFromSpanURI(fh.URI()) - reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.URIArg{URI: uri}) + reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.ResetGoModDiagnosticsArgs{URIArg: command.URIArg{URI: uri}}) if err != nil { return nil, err } @@ -178,7 +178,7 @@ func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.Fi return nil, err } - vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ + vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck to verify", command.VulncheckArgs{ URI: uri, Pattern: "./...", }) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 6b7a1a6fefa..77a7e024266 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -193,16 +193,11 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return nil, nil } - vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ - URI: protocol.DocumentURI(fh.URI()), - Pattern: "./...", - }) + suggestRunOrResetGovulncheck, err := suggestGovulncheckAction(fromGovulncheck, fh.URI()) if err != nil { // must not happen return nil, err // TODO: bug report } - suggestVulncheck := source.SuggestedFixFromCommand(vulncheck, protocol.QuickFix) - type modVuln struct { mod *govulncheck.Module vuln *govulncheck.Vuln @@ -294,14 +289,12 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, if len(infoFixes) > 0 { infoFixes = append(infoFixes, sf) } - if !fromGovulncheck { - infoFixes = append(infoFixes, suggestVulncheck) - } sort.Strings(warning) sort.Strings(info) if len(warning) > 0 { + warningFixes = append(warningFixes, suggestRunOrResetGovulncheck) vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ URI: fh.URI(), Range: rng, @@ -313,6 +306,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, }) } if len(info) > 0 { + infoFixes = append(infoFixes, suggestRunOrResetGovulncheck) vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ URI: fh.URI(), Range: rng, @@ -355,20 +349,19 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, } } if len(warning) > 0 { + fixes := []source.SuggestedFix{suggestRunOrResetGovulncheck} vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityWarning, - Source: source.Vulncheck, - Message: getVulnMessage(stdlib, warning, true, fromGovulncheck), - Related: relatedInfo, + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityWarning, + Source: source.Vulncheck, + Message: getVulnMessage(stdlib, warning, true, fromGovulncheck), + SuggestedFixes: fixes, + Related: relatedInfo, }) } if len(info) > 0 { - var fixes []source.SuggestedFix - if !fromGovulncheck { - fixes = append(fixes, suggestVulncheck) - } + fixes := []source.SuggestedFix{suggestRunOrResetGovulncheck} vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ URI: fh.URI(), Range: rng, @@ -384,6 +377,31 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return vulnDiagnostics, nil } +// suggestGovulncheckAction returns a code action that suggests either run govulncheck +// for more accurate investigation (if the present vulncheck diagnostics are based on +// analysis less accurate than govulncheck) or reset the existing govulncheck result +// (if the present vulncheck diagnostics are already based on govulncheck run). +func suggestGovulncheckAction(fromGovulncheck bool, uri span.URI) (source.SuggestedFix, error) { + if fromGovulncheck { + resetVulncheck, err := command.NewResetGoModDiagnosticsCommand("Reset govulncheck result", command.ResetGoModDiagnosticsArgs{ + URIArg: command.URIArg{URI: protocol.DocumentURI(uri)}, + DiagnosticSource: string(source.Vulncheck), + }) + if err != nil { + return source.SuggestedFix{}, err + } + return source.SuggestedFixFromCommand(resetVulncheck, protocol.QuickFix), nil + } + vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck to verify", command.VulncheckArgs{ + URI: protocol.DocumentURI(uri), + Pattern: "./...", + }) + if err != nil { + return source.SuggestedFix{}, err + } + return source.SuggestedFixFromCommand(vulncheck, protocol.QuickFix), nil +} + func getVulnMessage(mod string, vulns []string, used, fromGovulncheck bool) string { var b strings.Builder if used { @@ -493,18 +511,21 @@ func upgradeTitle(fixedVersion string) string { // SelectUpgradeCodeActions takes a list of code actions for a required module // and returns a more selective list of upgrade code actions, // where the code actions have been deduped. Code actions unrelated to upgrade -// remain untouched. +// are deduplicated by the name. func SelectUpgradeCodeActions(actions []protocol.CodeAction) []protocol.CodeAction { if len(actions) <= 1 { return actions // return early if no sorting necessary } var others []protocol.CodeAction + seen := make(map[string]bool) + set := make(map[string]protocol.CodeAction) for _, action := range actions { if strings.HasPrefix(action.Title, upgradeCodeActionPrefix) { set[action.Command.Title] = action - } else { + } else if !seen[action.Command.Title] { + seen[action.Command.Title] = true others = append(others, action) } } diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index c8a6e19a340..44334a0f2bf 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -768,7 +768,7 @@ var GeneratedAPIJSON = &APIJSON{ Command: "gopls.reset_go_mod_diagnostics", Title: "Reset go.mod diagnostics", Doc: "Reset diagnostics in the go.mod file of a module.", - ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", + ArgDoc: "{\n\t\"URIArg\": {\n\t\t\"URI\": string,\n\t},\n\t// Optional: source of the diagnostics to reset.\n\t// If not set, all resettable go.mod diagnostics will be cleared.\n\t\"DiagnosticSource\": string,\n}", }, { Command: "gopls.run_govulncheck", diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 12492d223a5..cf40da772b4 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -507,14 +507,14 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { codeActions: []string{ "Upgrade to latest", "Upgrade to v1.0.6", - "Run govulncheck", + "Run govulncheck to verify", }, }, }, codeActions: []string{ "Upgrade to latest", "Upgrade to v1.0.6", - "Run govulncheck", + "Run govulncheck to verify", }, hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, }, @@ -524,12 +524,12 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { msg: "golang.org/bmod has a vulnerability GO-2022-02.", severity: protocol.SeverityInformation, codeActions: []string{ - "Run govulncheck", + "Run govulncheck to verify", }, }, }, codeActions: []string{ - "Run govulncheck", + "Run govulncheck to verify", }, hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."}, }, @@ -667,6 +667,7 @@ func TestRunVulncheckWarning(t *testing.T) { codeActions: []string{ "Upgrade to latest", "Upgrade to v1.0.4", + "Reset govulncheck result", }, relatedInfo: []vulnRelatedInfo{ {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1 @@ -679,6 +680,7 @@ func TestRunVulncheckWarning(t *testing.T) { codeActions: []string{ "Upgrade to latest", "Upgrade to v1.0.6", + "Reset govulncheck result", }, relatedInfo: []vulnRelatedInfo{ {"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1 @@ -689,6 +691,7 @@ func TestRunVulncheckWarning(t *testing.T) { codeActions: []string{ "Upgrade to latest", "Upgrade to v1.0.6", + "Reset govulncheck result", }, hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, }, @@ -697,11 +700,17 @@ func TestRunVulncheckWarning(t *testing.T) { { msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.", severity: protocol.SeverityWarning, + codeActions: []string{ + "Reset govulncheck result", // no fix, but we should give an option to reset. + }, relatedInfo: []vulnRelatedInfo{ {"y.go", uint32(lineY.Line), "[GO-2022-02]"}, // bvuln.Vuln }, }, }, + codeActions: []string{ + "Reset govulncheck result", // no fix, but we should give an option to reset. + }, hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."}, }, } @@ -822,21 +831,44 @@ func TestGovulncheckInfo(t *testing.T) { { msg: "golang.org/bmod has a vulnerability GO-2022-02 that is not used in the code.", severity: protocol.SeverityInformation, + codeActions: []string{ + "Reset govulncheck result", + }, }, }, + codeActions: []string{ + "Reset govulncheck result", + }, hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."}, }, } + var allActions []protocol.CodeAction for mod, want := range wantDiagnostics { modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) // Check that the actions we get when including all diagnostics at a location return the same result gotActions := env.CodeAction("go.mod", modPathDiagnostics) + allActions = append(allActions, gotActions...) if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff) continue } } + + // Clear Diagnostics by using one of the reset code actions. + var reset protocol.CodeAction + for _, a := range allActions { + if a.Title == "Reset govulncheck result" { + reset = a + break + } + } + if reset.Title != "Reset govulncheck result" { + t.Errorf("failed to find a 'Reset govulncheck result' code action, got %v", allActions) + } + env.ApplyCodeAction(reset) + + env.Await(EmptyOrNoDiagnostics("go.mod")) }) } From 3cba5a847ff8ab8c232faf4e4310cb2939a835b4 Mon Sep 17 00:00:00 2001 From: Matthew Dempsky Date: Thu, 8 Dec 2022 15:09:20 -0800 Subject: [PATCH 527/723] internal/gcimporter: port CL 424876 from std importer This is a port of CL 424876, except it temporarily maintains a fallback path for older cmd/compile behavior before CL 455279, to avoid breaking users following Go tip. Updates golang/go#57015. Change-Id: I168d171153d96485e92be19645422fe65ab4b345 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456376 Run-TryBot: Matthew Dempsky TryBot-Result: Gopher Robot Auto-Submit: Matthew Dempsky gopls-CI: kokoro Reviewed-by: Robert Findley --- internal/gcimporter/gcimporter_test.go | 15 +++++++++ internal/gcimporter/testdata/issue57015.go | 16 +++++++++ internal/gcimporter/ureader_yes.go | 39 ++++++++++++++-------- 3 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 internal/gcimporter/testdata/issue57015.go diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index ab9a4db4690..bdae455d896 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -796,6 +796,21 @@ func TestIssue51836(t *testing.T) { _ = importPkg(t, "./testdata/aa", tmpdir) } +func TestIssue57015(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // requires generics + + // This package only handles gc export data. + needsCompiler(t, "gc") + + // On windows, we have to set the -D option for the compiler to avoid having a drive + // letter and an illegal ':' in the import path - just skip it (see also issue #3483). + if runtime.GOOS == "windows" { + t.Skip("avoid dealing with relative paths/drive letters on windows") + } + + compileAndImportPkg(t, "issue57015") +} + // apkg returns the package "a" prefixed by (as a package) testoutdir func apkg(testoutdir string) string { apkg := testoutdir + "/a" diff --git a/internal/gcimporter/testdata/issue57015.go b/internal/gcimporter/testdata/issue57015.go new file mode 100644 index 00000000000..b6be81191f9 --- /dev/null +++ b/internal/gcimporter/testdata/issue57015.go @@ -0,0 +1,16 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package issue57015 + +type E error + +type X[T any] struct {} + +func F() X[interface { + E +}] { + panic(0) +} + diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go index 20b99903c51..b285a11ce25 100644 --- a/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -559,18 +559,7 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { named.SetTypeParams(r.typeParamNames()) - rhs := r.typ() - pk := r.p - pk.laterFor(named, func() { - // First be sure that the rhs is initialized, if it needs to be initialized. - delete(pk.laterFors, named) // prevent cycles - if i, ok := pk.laterFors[rhs]; ok { - f := pk.laterFns[i] - pk.laterFns[i] = func() {} // function is running now, so replace it with a no-op - f() // initialize RHS - } - underlying := rhs.Underlying() - + setUnderlying := func(underlying types.Type) { // If the underlying type is an interface, we need to // duplicate its methods so we can replace the receiver // parameter's type (#49906). @@ -595,7 +584,31 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { } named.SetUnderlying(underlying) - }) + } + + // Since go.dev/cl/455279, we can assume rhs.Underlying() will + // always be non-nil. However, to temporarily support users of + // older snapshot releases, we continue to fallback to the old + // behavior for now. + // + // TODO(mdempsky): Remove fallback code and simplify after + // allowing time for snapshot users to upgrade. + rhs := r.typ() + if underlying := rhs.Underlying(); underlying != nil { + setUnderlying(underlying) + } else { + pk := r.p + pk.laterFor(named, func() { + // First be sure that the rhs is initialized, if it needs to be initialized. + delete(pk.laterFors, named) // prevent cycles + if i, ok := pk.laterFors[rhs]; ok { + f := pk.laterFns[i] + pk.laterFns[i] = func() {} // function is running now, so replace it with a no-op + f() // initialize RHS + } + setUnderlying(rhs.Underlying()) + }) + } for i, n := 0, r.Len(); i < n; i++ { named.AddMethod(r.method()) From a310bcb1d127e6734f7f97379af8f1bc001e1ba5 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 7 Dec 2022 16:24:10 -0500 Subject: [PATCH 528/723] gopls/internal/lsp/source: eliminate getStubFile It was a poor abstraction, and one of its two callers only wanted to parse the imports. (Follow-up to CL 456036.) Change-Id: I00dbf570d9b2cacbdfc5b7c105ef1b7c09f8da83 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456037 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan --- gopls/internal/lsp/source/stub.go | 37 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 4d5101ed668..fa1b07b6c84 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -37,9 +37,16 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi if si == nil { return nil, nil, fmt.Errorf("nil interface request") } - parsedConcreteFile, concreteFH, err := getStubFile(ctx, si.Concrete.Obj(), snapshot) + + // Parse the file defining the concrete type. + concreteFilename := snapshot.FileSet().Position(si.Concrete.Obj().Pos()).Filename + concreteFH, err := snapshot.GetFile(ctx, span.URIFromPath(concreteFilename)) + if err != nil { + return nil, nil, err + } + parsedConcreteFile, err := snapshot.ParseGo(ctx, concreteFH, ParseFull) if err != nil { - return nil, nil, fmt.Errorf("getFile(concrete): %w", err) + return nil, nil, fmt.Errorf("failed to parse file declaring implementation type: %w", err) } var ( methodsSrc []byte @@ -90,7 +97,7 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi NewText: []byte(edit.New), }) } - return snapshot.FileSet(), // from getStubFile + return snapshot.FileSet(), // to match snapshot.ParseGo above &analysis.SuggestedFix{TextEdits: edits}, nil } @@ -241,16 +248,20 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method } missing = append(missing, em...) } - parsedFile, _, err := getStubFile(ctx, ifaceObj, snapshot) + + // Parse the imports from the file that declares the interface. + ifaceFilename := snapshot.FileSet().Position(ifaceObj.Pos()).Filename + ifaceFH, err := snapshot.GetFile(ctx, span.URIFromPath(ifaceFilename)) if err != nil { - return nil, fmt.Errorf("error getting iface file: %w", err) + return nil, err } - if parsedFile.File == nil { - return nil, fmt.Errorf("could not find ast.File for %v", ifaceObj.Name()) + ifaceFile, err := snapshot.ParseGo(ctx, ifaceFH, ParseHeader) + if err != nil { + return nil, fmt.Errorf("error parsing imports from interface file: %w", err) } mi := &missingInterface{ iface: iface, - imports: parsedFile.File.Imports, + imports: ifaceFile.File.Imports, } for i := 0; i < iface.NumExplicitMethods(); i++ { method := iface.ExplicitMethod(i) @@ -275,16 +286,6 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method return missing, nil } -// Token position information for obj.Pos and the ParsedGoFile result is in Snapshot.FileSet. -// The returned file does not have type information. -func getStubFile(ctx context.Context, obj types.Object, snapshot Snapshot) (*ParsedGoFile, VersionedFileHandle, error) { - objPos := snapshot.FileSet().Position(obj.Pos()) - objFile := span.URIFromPath(objPos.Filename) - objectFH := snapshot.FindFile(objFile) - goFile, err := snapshot.ParseGo(ctx, objectFH, ParseFull) - return goFile, objectFH, err -} - // missingInterface represents an interface // that has all or some of its methods missing // from the destination concrete type From 3da7f1e4c2483c10122fab460ebf8d9e04095385 Mon Sep 17 00:00:00 2001 From: Peter Weinbergr Date: Fri, 9 Dec 2022 12:29:01 -0500 Subject: [PATCH 529/723] gopls/hover: remove header tags from hover markdown Lines in go doc that look like '// ## Header' generate, by default, tags in the markdown that look like '{hdr-Header}' which are harmless and annoying. This CL generates the markdown without the tags. Fixes: golang/go#57048 Change-Id: I7109775ea3ec6b51504d62108187e3614b2d1e9d Reviewed-on: https://go-review.googlesource.com/c/tools/+/456535 Run-TryBot: Peter Weinberger Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/source/comment_go118.go | 5 +++ gopls/internal/regtest/misc/hover_test.go | 39 ++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/gopls/internal/lsp/source/comment_go118.go b/gopls/internal/lsp/source/comment_go118.go index 0503670d3bf..ca4ab9d3e1c 100644 --- a/gopls/internal/lsp/source/comment_go118.go +++ b/gopls/internal/lsp/source/comment_go118.go @@ -32,6 +32,11 @@ func CommentToMarkdown(text string) string { var p comment.Parser doc := p.Parse(text) var pr comment.Printer + // The default produces {#Hdr-...} tags for headings. + // vscode displays thems, which is undesirable. + // The godoc for comment.Printer says the tags + // avoid a security problem. + pr.HeadingID = func(*comment.Heading) string { return "" } easy := pr.Markdown(doc) return string(easy) } diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go index bea370a21ce..324f88b3fca 100644 --- a/gopls/internal/regtest/misc/hover_test.go +++ b/gopls/internal/regtest/misc/hover_test.go @@ -221,3 +221,42 @@ func main() { } }) } + +// for x/tools/gopls: unhandled named anchor on the hover #57048 +func TestHoverTags(t *testing.T) { + testenv.NeedsGo1Point(t, 14) // until go1.13 is dropped from kokoro + const source = ` +-- go.mod -- +module mod.com + +go 1.19 + +-- lib/a.go -- + +// variety of execution modes. +// +// # Test package setup +// +// The regression test package uses a couple of uncommon patterns to reduce +package lib + +-- a.go -- + package main + import "mod.com/lib" + + const A = 1 + +} +` + Run(t, source, func(t *testing.T, env *Env) { + t.Run("tags", func(t *testing.T) { + env.OpenFile("a.go") + z := env.RegexpSearch("a.go", "lib") + t.Logf("%#v", z) + got, _ := env.Hover("a.go", env.RegexpSearch("a.go", "lib")) + if strings.Contains(got.Value, "{#hdr-") { + t.Errorf("Hover: got {#hdr- tag:\n%q", got) + } + }) + }) +} From a3eef2595adfe42f26e607e25f136b5d5388419c Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 9 Dec 2022 18:38:04 -0500 Subject: [PATCH 530/723] gopls/internal/lsp/cache: record parse keys when they're created When we parse a file through snapshot.ParseGo, we must record the parse key by its URI for later invalidation. This wasn't done, resulting in a memory leak. Fix the leak by actually recording parse keys in the parseKeysByURI map, which was previously always empty. Write a test that diffs the cache before and after a change, which would have caught this bug (and others). Fixes golang/go#57222 Change-Id: I308812bf1030276dff08c26d359433750f44849a Reviewed-on: https://go-review.googlesource.com/c/tools/+/456642 Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/cache/parse.go | 26 +++++++- gopls/internal/regtest/misc/leak_test.go | 82 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 gopls/internal/regtest/misc/leak_test.go diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index 4f905e96f7a..33ca98c6822 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -59,7 +59,7 @@ func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode sourc // cache miss? if !hit { - handle, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} { + promise, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} { parsed, err := parseGoImpl(ctx, arg.(*snapshot).FileSet(), fh, mode) return parseGoResult{parsed, err} }) @@ -70,8 +70,30 @@ func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode sourc entry = prev release() } else { - entry = handle + entry = promise s.parsedGoFiles.Set(key, entry, func(_, _ interface{}) { release() }) + + // In order to correctly invalidate the key above, we must keep track of + // the parse key just created. + // + // TODO(rfindley): use a two-level map URI->parseKey->promise. + keys, _ := s.parseKeysByURI.Get(fh.URI()) + + // Only record the new key if it doesn't exist. This is overly cautious: + // we should only be setting the key if it doesn't exist. However, this + // logic will be replaced soon, and erring on the side of caution seemed + // wise. + foundKey := false + for _, existing := range keys { + if existing == key { + foundKey = true + break + } + } + if !foundKey { + keys = append(keys, key) + s.parseKeysByURI.Set(fh.URI(), keys) + } } s.mu.Unlock() } diff --git a/gopls/internal/regtest/misc/leak_test.go b/gopls/internal/regtest/misc/leak_test.go new file mode 100644 index 00000000000..71c8b13309f --- /dev/null +++ b/gopls/internal/regtest/misc/leak_test.go @@ -0,0 +1,82 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package misc + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" +) + +// Test for golang/go#57222. +func TestCacheLeak(t *testing.T) { + const files = `-- a.go -- +package a + +func _() { + println("1") +} +` + c := cache.New(nil, nil) + env := setupEnv(t, files, c) + env.Await(InitialWorkspaceLoad) + env.OpenFile("a.go") + + // Make a couple edits to stabilize cache state. + // + // For some reason, after only one edit we're left with two parsed files + // (perhaps because something had to ParseHeader). If this test proves flaky, + // we'll need to investigate exactly what is causing various parse modes to + // be present (or rewrite the test to be more tolerant, for example make ~100 + // modifications and assert that we're within a few of where we're started). + env.RegexpReplace("a.go", "1", "2") + env.RegexpReplace("a.go", "2", "3") + env.AfterChange() + + // Capture cache state, make an arbitrary change, and wait for gopls to do + // its work. Afterward, we should have the exact same number of parsed + before := c.MemStats() + env.RegexpReplace("a.go", "3", "4") + env.AfterChange() + after := c.MemStats() + + if diff := cmp.Diff(before, after); diff != "" { + t.Errorf("store objects differ after change (-before +after)\n%s", diff) + } +} + +// setupEnv creates a new sandbox environment for editing the txtar encoded +// content of files. It uses a new gopls instance backed by the Cache c. +func setupEnv(t *testing.T, files string, c *cache.Cache) *Env { + ctx := debug.WithInstance(context.Background(), "", "off") + server := lsprpc.NewStreamServer(c, false, hooks.Options) + ts := servertest.NewPipeServer(server, jsonrpc2.NewRawStream) + s, err := fake.NewSandbox(&fake.SandboxConfig{ + Files: fake.UnpackTxt(files), + }) + if err != nil { + t.Fatal(err) + } + + a := NewAwaiter(s.Workdir) + e, err := fake.NewEditor(s, fake.EditorConfig{}).Connect(ctx, ts, a.Hooks()) + + return &Env{ + T: t, + Ctx: ctx, + Editor: e, + Sandbox: s, + Awaiter: a, + } +} From 84299a05de77a7927d2c13244ce40d8a4573ec46 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 8 Dec 2022 10:18:14 -0500 Subject: [PATCH 531/723] gopls/internal/lsp/cache: simplify ad-hoc package warning logic The old logic used to inspect the syntax, on the assumption that metadata was not reloaded after an import deletion. But the logic of metadataChanges clearly shows that it is reloaded. The new logic now inspects only the metadata. There's already a test that the warning is properly dismissed. A follow-up change will strength-reduce ActivePackages to avoid type-checking. Change-Id: I6b010b5b7b011544cbb69189145208101b94de65 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456257 gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/snapshot.go | 50 ++++++------------- .../regtest/diagnostics/diagnostics_test.go | 6 +++ 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 52c5a26fc7b..ac92e8cd79d 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1440,50 +1440,30 @@ func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { return loadErr } +// A portion of this text is expected by TestBrokenWorkspace_OutsideModule. const adHocPackagesWarning = `You are outside of a module and outside of $GOPATH/src. If you are using modules, please open your editor to a directory in your module. If you believe this warning is incorrect, please file an issue: https://github.com/golang/go/issues/new.` func shouldShowAdHocPackagesWarning(snapshot source.Snapshot, pkgs []source.Package) string { - if snapshot.ValidBuildConfiguration() { - return "" - } - for _, pkg := range pkgs { - if hasMissingDependencies(pkg) { - return adHocPackagesWarning - } - } - return "" -} + if !snapshot.ValidBuildConfiguration() { + for _, pkg := range pkgs { + // TODO(adonovan): strength-reduce the parameter to []Metadata. + m := snapshot.Metadata(pkg.ID()) + if m == nil { + continue + } -func hasMissingDependencies(pkg source.Package) bool { - // We don't invalidate metadata for import deletions, - // so check the package imports via the syntax tree - // (not via types.Package.Imports, since it contains packages - // synthesized under the vendoring-hostile assumption that - // ImportPath equals PackagePath). - // - // rfindley says: it looks like this is intending to implement - // a heuristic "if go list couldn't resolve import paths to - // packages, then probably you're not in GOPATH or a module". - // This is used to determine if we need to show a warning diagnostic. - // It looks like this logic is implementing the heuristic that - // "even if the metadata has a MissingDep, if the types.Package - // doesn't need that dep anymore we shouldn't show the warning". - // But either we're outside of GOPATH/Module, or we're not... - // - // If we invalidate the metadata for import deletions (which - // should be fast) then we can simply return the blank entries - // in DepsByImpPath. - for _, f := range pkg.GetSyntax() { - for _, imp := range f.Imports { - importPath := source.UnquoteImportPath(imp) - if _, err := pkg.ResolveImportPath(importPath); err != nil { - return true + // A blank entry in DepsByImpPath + // indicates a missing dependency. + for _, importID := range m.DepsByImpPath { + if importID == "" { + return adHocPackagesWarning + } } } } - return false + return "" } func containsCommandLineArguments(pkgs []source.Package) bool { diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index fb858f6ab5d..880b62f7654 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -543,6 +543,10 @@ func _() { // Expect a module/GOPATH error if there is an error in the file at startup. // Tests golang/go#37279. func TestBrokenWorkspace_OutsideModule(t *testing.T) { + // Versions of the go command before go1.16 do not support + // native overlays and so do not observe the deletion. + testenv.NeedsGo1Point(t, 16) + const noModule = ` -- a.go -- package foo @@ -556,8 +560,10 @@ func f() { Run(t, noModule, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.Await( + // Expect the adHocPackagesWarning. OutstandingWork(lsp.WorkspaceLoadFailure, "outside of a module"), ) + // Deleting the import dismisses the warning. env.RegexpReplace("a.go", `import "mod.com/hello"`, "") env.Await( NoOutstandingWork(), From 18f76ecd3f383e39cd991ffef6cb7cd5625aa4ae Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 8 Dec 2022 09:48:21 -0500 Subject: [PATCH 532/723] gopls/internal/lsp: split ActivePackages ActivePackages used to compute a set of packages, then type check them all, even though many clients need only metadata. This change replaces it by ActiveMetadata, which returns only metadata for the set of packages, and exposes the LoadPackages operation allowing callers to load only the subset they actually need. Details: - Snapshot.TypeCheck method is exposed. - Snapshot.DiagnosePackage now takes a PackageID, and calls for typechecking itself. Similarly Server.diagnosePkg. - source.Analyze now takes a PackageID, noot Package (which it previously used only for its ID). - GCOptimizationDetails accepts a Metadata, not a Package. - workspaceMetadata replaces workspacePackageID, activePackageIDs. - Combined the two loops in Server.diagnoseChangedFiles, and parallelized the diagnose calls in moddiagnostics, to ensure that type checking happens in parallel. Change-Id: Id0383a678ce45447f3313d1426e3f9b96050eaa1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456275 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan --- gopls/internal/lsp/cache/analysis.go | 8 +- gopls/internal/lsp/cache/mod_tidy.go | 7 +- gopls/internal/lsp/cache/snapshot.go | 89 ++++++------------ gopls/internal/lsp/code_action.go | 11 ++- gopls/internal/lsp/diagnostics.go | 94 +++++++++---------- gopls/internal/lsp/mod/diagnostics.go | 30 ++++-- .../internal/lsp/source/completion/package.go | 22 ++--- gopls/internal/lsp/source/diagnostics.go | 18 ++-- gopls/internal/lsp/source/gc_annotations.go | 6 +- gopls/internal/lsp/source/view.go | 52 ++++++++-- 10 files changed, 184 insertions(+), 153 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index e2c56a53c0f..69bb39313ab 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -462,8 +462,12 @@ func factType(fact analysis.Fact) reflect.Type { return t } -func (s *snapshot) DiagnosePackage(ctx context.Context, spkg source.Package) (map[span.URI][]*source.Diagnostic, error) { - pkg := spkg.(*pkg) +func (s *snapshot) DiagnosePackage(ctx context.Context, id PackageID) (map[span.URI][]*source.Diagnostic, error) { + pkgs, err := s.TypeCheck(ctx, source.TypecheckFull, id) + if err != nil { + return nil, err + } + pkg := pkgs[0].(*pkg) var errorAnalyzerDiag []*source.Diagnostic if pkg.HasTypeErrors() { // Apply type error analyzers. diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index 9495f3fa3cf..fa30df18e36 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -192,12 +192,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.Pars // Add diagnostics for missing modules anywhere they are imported in the // workspace. // TODO(adonovan): opt: opportunities for parallelism abound. - for _, id := range snapshot.workspacePackageIDs() { - m := snapshot.Metadata(id) - if m == nil { - return nil, fmt.Errorf("no metadata for %s", id) - } - + for _, m := range snapshot.workspaceMetadata() { // Read both lists of files of this package, in parallel. goFiles, compiledGoFiles, err := readGoFiles(ctx, snapshot, m) if err != nil { diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index ac92e8cd79d..b439e9e2908 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -299,6 +299,7 @@ func (s *snapshot) ValidBuildConfiguration() bool { } // The user may have a multiple directories in their GOPATH. // Check if the workspace is within any of them. + // TODO(rfindley): this should probably be subject to "if GO111MODULES = off {...}". for _, gp := range filepath.SplitList(s.view.gopath) { if source.InDir(filepath.Join(gp, "src"), s.view.rootURI.Filename()) { return true @@ -653,7 +654,7 @@ func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode sourc ids = append(ids, m.ID) } - return s.loadPackages(ctx, mode, ids...) + return s.TypeCheck(ctx, mode, ids...) } func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, pkgPolicy source.PackageFilter) (source.Package, error) { @@ -671,7 +672,7 @@ func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source case source.WidestPackage: metas = metas[len(metas)-1:] } - pkgs, err := s.loadPackages(ctx, mode, metas[0].ID) + pkgs, err := s.TypeCheck(ctx, mode, metas[0].ID) if err != nil { return nil, err } @@ -713,8 +714,8 @@ func (s *snapshot) containingPackages(ctx context.Context, uri span.URI) ([]*sou return metas, err } -// loadPackages type-checks the specified packages in the given mode. -func (s *snapshot) loadPackages(ctx context.Context, mode source.TypecheckMode, ids ...PackageID) ([]source.Package, error) { +// TypeCheck type-checks the specified packages in the given mode. +func (s *snapshot) TypeCheck(ctx context.Context, mode source.TypecheckMode, ids ...PackageID) ([]source.Package, error) { // Build all the handles... var phs []*packageHandle for _, id := range ids { @@ -854,30 +855,14 @@ func (s *snapshot) getImportedBy(id PackageID) []PackageID { return s.meta.importedBy[id] } -func (s *snapshot) workspacePackageIDs() (ids []PackageID) { +func (s *snapshot) workspaceMetadata() (meta []*source.Metadata) { s.mu.Lock() defer s.mu.Unlock() for id := range s.workspacePackages { - ids = append(ids, id) + meta = append(meta, s.meta.metadata[id]) } - return ids -} - -func (s *snapshot) activePackageIDs() (ids []PackageID) { - if s.view.Options().MemoryMode == source.ModeNormal { - return s.workspacePackageIDs() - } - - s.mu.Lock() - defer s.mu.Unlock() - - for id := range s.workspacePackages { - if s.isActiveLocked(id) { - ids = append(ids, id) - } - } - return ids + return meta } func (s *snapshot) isActiveLocked(id PackageID) (active bool) { @@ -1089,35 +1074,25 @@ func (s *snapshot) knownFilesInDir(ctx context.Context, dir span.URI) []span.URI return files } -func (s *snapshot) ActivePackages(ctx context.Context) ([]source.Package, error) { - phs, err := s.activePackageHandles(ctx) - if err != nil { +func (s *snapshot) ActiveMetadata(ctx context.Context) ([]*source.Metadata, error) { + if err := s.awaitLoaded(ctx); err != nil { return nil, err } - var pkgs []source.Package - for _, ph := range phs { - pkg, err := ph.await(ctx, s) - if err != nil { - return nil, err - } - pkgs = append(pkgs, pkg) - } - return pkgs, nil -} -func (s *snapshot) activePackageHandles(ctx context.Context) ([]*packageHandle, error) { - if err := s.awaitLoaded(ctx); err != nil { - return nil, err + if s.view.Options().MemoryMode == source.ModeNormal { + return s.workspaceMetadata(), nil } - var phs []*packageHandle - for _, pkgID := range s.activePackageIDs() { - ph, err := s.buildPackageHandle(ctx, pkgID, s.workspaceParseMode(pkgID)) - if err != nil { - return nil, err + + // ModeDegradeClosed + s.mu.Lock() + defer s.mu.Unlock() + var active []*source.Metadata + for id := range s.workspacePackages { + if s.isActiveLocked(id) { + active = append(active, s.Metadata(id)) } - phs = append(phs, ph) } - return phs, nil + return active, nil } // Symbols extracts and returns the symbols for each file in all the snapshot's views. @@ -1394,8 +1369,8 @@ func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { // Even if packages didn't fail to load, we still may want to show // additional warnings. if loadErr == nil { - wsPkgs, _ := s.ActivePackages(ctx) - if msg := shouldShowAdHocPackagesWarning(s, wsPkgs); msg != "" { + active, _ := s.ActiveMetadata(ctx) + if msg := shouldShowAdHocPackagesWarning(s, active); msg != "" { return &source.CriticalError{ MainError: errors.New(msg), } @@ -1410,7 +1385,7 @@ func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { // on-demand or via orphaned file reloading. // // TODO(rfindley): re-evaluate this heuristic. - if containsCommandLineArguments(wsPkgs) { + if containsCommandLineArguments(active) { err, diags := s.workspaceLayoutError(ctx) if err != nil { if ctx.Err() != nil { @@ -1445,15 +1420,9 @@ const adHocPackagesWarning = `You are outside of a module and outside of $GOPATH If you are using modules, please open your editor to a directory in your module. If you believe this warning is incorrect, please file an issue: https://github.com/golang/go/issues/new.` -func shouldShowAdHocPackagesWarning(snapshot source.Snapshot, pkgs []source.Package) string { +func shouldShowAdHocPackagesWarning(snapshot source.Snapshot, active []*source.Metadata) string { if !snapshot.ValidBuildConfiguration() { - for _, pkg := range pkgs { - // TODO(adonovan): strength-reduce the parameter to []Metadata. - m := snapshot.Metadata(pkg.ID()) - if m == nil { - continue - } - + for _, m := range active { // A blank entry in DepsByImpPath // indicates a missing dependency. for _, importID := range m.DepsByImpPath { @@ -1466,9 +1435,9 @@ func shouldShowAdHocPackagesWarning(snapshot source.Snapshot, pkgs []source.Pack return "" } -func containsCommandLineArguments(pkgs []source.Package) bool { - for _, pkg := range pkgs { - if source.IsCommandLineArguments(pkg.ID()) { +func containsCommandLineArguments(metas []*source.Metadata) bool { + for _, m := range metas { + if source.IsCommandLineArguments(m.ID) { return true } } diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 1cf58f9ddf2..63c0d67c6a5 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -155,16 +155,19 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara if ctx.Err() != nil { return nil, ctx.Err() } - pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckFull, source.WidestPackage) + metas, err := snapshot.MetadataForFile(ctx, fh.URI()) if err != nil { return nil, err } - - pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) + if len(metas) == 0 { + return nil, fmt.Errorf("no package containing file %q", fh.URI()) + } + id := metas[len(metas)-1].ID // last => widest package + pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, id) if err != nil { return nil, err } - analysisDiags, err := source.Analyze(ctx, snapshot, pkg, true) + analysisDiags, err := source.Analyze(ctx, snapshot, id, true) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index dec452b2454..addfa96aca8 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -15,6 +15,7 @@ import ( "sync" "time" + "golang.org/x/sync/errgroup" "golang.org/x/tools/gopls/internal/lsp/debug/log" "golang.org/x/tools/gopls/internal/lsp/mod" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -177,7 +178,8 @@ func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snaps ctx, done := event.Start(ctx, "Server.diagnoseChangedFiles", source.SnapshotLabels(snapshot)...) defer done() - packages := make(map[source.Package]struct{}) + var group errgroup.Group + seen := make(map[*source.Metadata]bool) for _, uri := range uris { // If the change is only on-disk and the file is not open, don't // directly request its package. It may not be a workspace package. @@ -194,28 +196,30 @@ func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snaps if snapshot.IsBuiltin(ctx, uri) { continue } - pkgs, err := snapshot.PackagesForFile(ctx, uri, source.TypecheckFull, false) + + // Find all packages that include this file and diagnose them in parallel. + metas, err := snapshot.MetadataForFile(ctx, uri) if err != nil { // TODO (findleyr): we should probably do something with the error here, // but as of now this can fail repeatedly if load fails, so can be too // noisy to log (and we'll handle things later in the slow pass). continue } - for _, pkg := range pkgs { - packages[pkg] = struct{}{} + for _, m := range metas { + if m.IsIntermediateTestVariant() { + continue + } + if !seen[m] { + seen[m] = true + m := m + group.Go(func() error { + s.diagnosePkg(ctx, snapshot, m, false) + return nil // error result is ignored + }) + } } } - var wg sync.WaitGroup - for pkg := range packages { - wg.Add(1) - - go func(pkg source.Package) { - defer wg.Done() - - s.diagnosePkg(ctx, snapshot, pkg, false) - }(pkg) - } - wg.Wait() + group.Wait() // ignore error } // diagnose is a helper function for running diagnostics with a given context. @@ -268,14 +272,9 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn } store(workSource, "diagnosing go.work file", workReports, workErr, true) - // All subsequent steps depend on the completion of - // type-checking of the all active packages in the workspace. - // This step may take many seconds initially. - // (mod.Diagnostics would implicitly wait for this too, - // but the control is clearer if it is explicit here.) - activePkgs, activeErr := snapshot.ActivePackages(ctx) - // Diagnose go.mod file. + // (This step demands type checking of all active packages: + // the bottleneck in the startup sequence for a big workspace.) modReports, modErr := mod.Diagnostics(ctx, snapshot) if ctx.Err() != nil { log.Trace.Log(ctx, "diagnose cancelled") @@ -291,6 +290,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn } store(modVulncheckSource, "diagnosing vulnerabilities", vulnReports, vulnErr, false) + activeMetas, activeErr := snapshot.ActiveMetadata(ctx) if s.shouldIgnoreError(ctx, snapshot, activeErr) { return } @@ -313,7 +313,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn // If there are no workspace packages, there is nothing to diagnose and // there are no orphaned files. - if len(activePkgs) == 0 { + if len(activeMetas) == 0 { return } @@ -324,16 +324,16 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn wg sync.WaitGroup seen = map[span.URI]struct{}{} ) - for _, pkg := range activePkgs { - for _, pgf := range pkg.CompiledGoFiles() { - seen[pgf.URI] = struct{}{} + for _, m := range activeMetas { + for _, uri := range m.CompiledGoFiles { + seen[uri] = struct{}{} } wg.Add(1) - go func(pkg source.Package) { + go func(m *source.Metadata) { defer wg.Done() - s.diagnosePkg(ctx, snapshot, pkg, forceAnalysis) - }(pkg) + s.diagnosePkg(ctx, snapshot, m, forceAnalysis) + }(m) } wg.Wait() @@ -352,55 +352,55 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn } } -func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg source.Package, alwaysAnalyze bool) { - ctx, done := event.Start(ctx, "Server.diagnosePkg", append(source.SnapshotLabels(snapshot), tag.Package.Of(string(pkg.ID())))...) +func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, m *source.Metadata, alwaysAnalyze bool) { + ctx, done := event.Start(ctx, "Server.diagnosePkg", append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...) defer done() enableDiagnostics := false includeAnalysis := alwaysAnalyze // only run analyses for packages with open files - for _, pgf := range pkg.CompiledGoFiles() { - enableDiagnostics = enableDiagnostics || !snapshot.IgnoredFile(pgf.URI) - includeAnalysis = includeAnalysis || snapshot.IsOpen(pgf.URI) + for _, uri := range m.CompiledGoFiles { + enableDiagnostics = enableDiagnostics || !snapshot.IgnoredFile(uri) + includeAnalysis = includeAnalysis || snapshot.IsOpen(uri) } // Don't show any diagnostics on ignored files. if !enableDiagnostics { return } - pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) + pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, m.ID) if err != nil { - event.Error(ctx, "warning: diagnosing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(pkg.ID())))...) + event.Error(ctx, "warning: diagnosing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...) return } - for _, cgf := range pkg.CompiledGoFiles() { + for _, uri := range m.CompiledGoFiles { // builtin.go exists only for documentation purposes, and is not valid Go code. // Don't report distracting errors - if !snapshot.IsBuiltin(ctx, cgf.URI) { - s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, pkgDiagnostics[cgf.URI], true) + if !snapshot.IsBuiltin(ctx, uri) { + s.storeDiagnostics(snapshot, uri, typeCheckSource, pkgDiagnostics[uri], true) } } if includeAnalysis { - reports, err := source.Analyze(ctx, snapshot, pkg, false) + reports, err := source.Analyze(ctx, snapshot, m.ID, false) if err != nil { - event.Error(ctx, "warning: analyzing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(pkg.ID())))...) + event.Error(ctx, "warning: analyzing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...) return } - for _, cgf := range pkg.CompiledGoFiles() { - s.storeDiagnostics(snapshot, cgf.URI, analysisSource, reports[cgf.URI], true) + for _, uri := range m.CompiledGoFiles { + s.storeDiagnostics(snapshot, uri, analysisSource, reports[uri], true) } } // If gc optimization details are requested, add them to the // diagnostic reports. s.gcOptimizationDetailsMu.Lock() - _, enableGCDetails := s.gcOptimizationDetails[pkg.ID()] + _, enableGCDetails := s.gcOptimizationDetails[m.ID] s.gcOptimizationDetailsMu.Unlock() if enableGCDetails { - gcReports, err := source.GCOptimizationDetails(ctx, snapshot, pkg) + gcReports, err := source.GCOptimizationDetails(ctx, snapshot, m) if err != nil { - event.Error(ctx, "warning: gc details", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(pkg.ID())))...) + event.Error(ctx, "warning: gc details", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...) } s.gcOptimizationDetailsMu.Lock() - _, enableGCDetails := s.gcOptimizationDetails[pkg.ID()] + _, enableGCDetails := s.gcOptimizationDetails[m.ID] // NOTE(golang/go#44826): hold the gcOptimizationDetails lock, and re-check // whether gc optimization details are enabled, while storing gc_details diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 77a7e024266..a0e42720a8f 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -11,9 +11,11 @@ import ( "fmt" "sort" "strings" + "sync" "golang.org/x/mod/modfile" "golang.org/x/mod/semver" + "golang.org/x/sync/errgroup" "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -90,17 +92,31 @@ func ModDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.Fil // TODO(rfindley): Try to avoid calling DiagnosePackage on all packages in the workspace here, // for every go.mod file. If gc_details is enabled, it looks like this could lead to extra // go command invocations (as gc details is not memoized). - wspkgs, err := snapshot.ActivePackages(ctx) + active, err := snapshot.ActiveMetadata(ctx) if err != nil && !source.IsNonFatalGoModError(err) { event.Error(ctx, fmt.Sprintf("workspace packages: diagnosing %s", pm.URI), err) } if err == nil { - for _, pkg := range wspkgs { - pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) - if err != nil { - return nil, err - } - diagnostics = append(diagnostics, pkgDiagnostics[fh.URI()]...) + // Diagnose packages in parallel. + // (It's possible this is the first operation after the initial + // metadata load to demand type-checking of all the active packages.) + var group errgroup.Group + var mu sync.Mutex + for _, m := range active { + m := m + group.Go(func() error { + pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, m.ID) + if err != nil { + return err + } + mu.Lock() + diagnostics = append(diagnostics, pkgDiagnostics[fh.URI()]...) + mu.Unlock() + return nil + }) + } + if err := group.Wait(); err != nil { + return nil, err } } diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index 19c52491cef..abfa8b8e8a5 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -215,7 +215,7 @@ func (c *completer) packageNameCompletions(ctx context.Context, fileURI span.URI // file. This also includes test packages for these packages (_test) and // the directory name itself. func packageSuggestions(ctx context.Context, snapshot source.Snapshot, fileURI span.URI, prefix string) (packages []candidate, err error) { - workspacePackages, err := snapshot.ActivePackages(ctx) + active, err := snapshot.ActiveMetadata(ctx) if err != nil { return nil, err } @@ -245,18 +245,18 @@ func packageSuggestions(ctx context.Context, snapshot source.Snapshot, fileURI s // The `go` command by default only allows one package per directory but we // support multiple package suggestions since gopls is build system agnostic. - for _, pkg := range workspacePackages { - if pkg.Name() == "main" || pkg.Name() == "" { + for _, m := range active { + if m.Name == "main" || m.Name == "" { continue } - if _, ok := seenPkgs[pkg.Name()]; ok { + if _, ok := seenPkgs[m.Name]; ok { continue } // Only add packages that are previously used in the current directory. var relevantPkg bool - for _, pgf := range pkg.CompiledGoFiles() { - if filepath.Dir(pgf.URI.Filename()) == dirPath { + for _, uri := range m.CompiledGoFiles { + if filepath.Dir(uri.Filename()) == dirPath { relevantPkg = true break } @@ -268,13 +268,13 @@ func packageSuggestions(ctx context.Context, snapshot source.Snapshot, fileURI s // Add a found package used in current directory as a high relevance // suggestion and the test package for it as a medium relevance // suggestion. - if score := float64(matcher.Score(string(pkg.Name()))); score > 0 { - packages = append(packages, toCandidate(string(pkg.Name()), score*highScore)) + if score := float64(matcher.Score(string(m.Name))); score > 0 { + packages = append(packages, toCandidate(string(m.Name), score*highScore)) } - seenPkgs[pkg.Name()] = struct{}{} + seenPkgs[m.Name] = struct{}{} - testPkgName := pkg.Name() + "_test" - if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(string(pkg.Name()), "_test") { + testPkgName := m.Name + "_test" + if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(string(m.Name), "_test") { continue } if score := float64(matcher.Score(string(testPkgName))); score > 0 { diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index 7780f471f11..db4fc900a4e 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -6,6 +6,7 @@ package source import ( "context" + "fmt" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/span" @@ -24,7 +25,7 @@ type RelatedInformation struct { Message string } -func Analyze(ctx context.Context, snapshot Snapshot, pkg Package, includeConvenience bool) (map[span.URI][]*Diagnostic, error) { +func Analyze(ctx context.Context, snapshot Snapshot, pkgid PackageID, includeConvenience bool) (map[span.URI][]*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 { @@ -39,6 +40,7 @@ func Analyze(ctx context.Context, snapshot Snapshot, pkg Package, includeConveni if includeConvenience { // e.g. for codeAction categories = append(categories, options.ConvenienceAnalyzers) // e.g. fillstruct } + var analyzers []*Analyzer for _, cat := range categories { for _, a := range cat { @@ -46,13 +48,13 @@ func Analyze(ctx context.Context, snapshot Snapshot, pkg Package, includeConveni } } - analysisDiagnostics, err := snapshot.Analyze(ctx, pkg.ID(), analyzers) + analysisDiagnostics, err := snapshot.Analyze(ctx, pkgid, analyzers) if err != nil { return nil, err } - reports := map[span.URI][]*Diagnostic{} // Report diagnostics and errors from root analyzers. + reports := map[span.URI][]*Diagnostic{} for _, diag := range analysisDiagnostics { reports[diag.URI] = append(reports[diag.URI], diag) } @@ -64,15 +66,19 @@ func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (Vers if err != nil { return VersionedFileIdentity{}, nil, err } - pkg, _, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) + metas, err := snapshot.MetadataForFile(ctx, uri) if err != nil { return VersionedFileIdentity{}, nil, err } - diagnostics, err := snapshot.DiagnosePackage(ctx, pkg) + if len(metas) == 0 { + return VersionedFileIdentity{}, nil, fmt.Errorf("no package containing file %q", uri) + } + id := metas[0].ID // 0 => narrowest package + diagnostics, err := snapshot.DiagnosePackage(ctx, id) if err != nil { return VersionedFileIdentity{}, nil, err } - analysisDiags, err := Analyze(ctx, snapshot, pkg, false) + analysisDiags, err := Analyze(ctx, snapshot, id, false) if err != nil { return VersionedFileIdentity{}, nil, err } diff --git a/gopls/internal/lsp/source/gc_annotations.go b/gopls/internal/lsp/source/gc_annotations.go index ab0fd6035e6..d5245c8cdd1 100644 --- a/gopls/internal/lsp/source/gc_annotations.go +++ b/gopls/internal/lsp/source/gc_annotations.go @@ -35,11 +35,11 @@ const ( Bounds Annotation = "bounds" ) -func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, pkg Package) (map[VersionedFileIdentity][]*Diagnostic, error) { - if len(pkg.CompiledGoFiles()) == 0 { +func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, m *Metadata) (map[VersionedFileIdentity][]*Diagnostic, error) { + if len(m.CompiledGoFiles) == 0 { return nil, nil } - pkgDir := filepath.Dir(pkg.CompiledGoFiles()[0].URI.Filename()) + pkgDir := filepath.Dir(m.CompiledGoFiles[0].Filename()) outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid())) if err := os.MkdirAll(outDir, 0700); err != nil { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 1168370c6f8..493553bc86d 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -107,12 +107,43 @@ type Snapshot interface { // Position information is added to FileSet(). ParseGo(ctx context.Context, fh FileHandle, mode ParseMode) (*ParsedGoFile, error) - // DiagnosePackage returns basic diagnostics, including list, parse, and type errors - // for pkg, grouped by file. - DiagnosePackage(ctx context.Context, pkg Package) (map[span.URI][]*Diagnostic, error) + // DiagnosePackage returns basic diagnostics, including list, + // parse, and type errors for pkg, grouped by file. + // + // It may include suggested fixes for type errors, created by + // running the analysis framework. + // + // TODO(adonovan): this operation does a mix of checks that + // range from cheap (list errors, parse errors) to expensive + // (type errors, type-error-analyzer results). In particular, + // type-error analyzers (as currently implemented) depend on + // the full analyzer machinery, and in the near future that + // will be completely separate from regular type checking. + // So, we must choose between: + // + // (1) rewriting them as ad-hoc functions that operate on + // type-checked packages. That's a fair amount of work + // since they are fairly deeply enmeshed in the framework, + // (some have horizontal dependencies such as Inspector), + // and quite reasonably so. So we reject this in favor of: + // + // (2) separating the generation of type errors (which happens + // at the lowest latency) from full analysis, which is + // slower, although hopefully eventually only on the order + // of seconds. In this case, type error analyzers are + // basically not special; the only special part would be + // the postprocessing step to merge the + // type-error-analyzer fixes into the ordinary diagnostics + // produced by type checking. (Not yet sure how to do that.) + // + // So then the role of this function is "report fast + // diagnostics, up to type-checking", and the role of + // Analyze() is "run the analysis framework, included + // suggested fixes for type errors" + DiagnosePackage(ctx context.Context, id PackageID) (map[span.URI][]*Diagnostic, error) - // Analyze runs the analyses for the given package at this snapshot. - Analyze(ctx context.Context, pkgID PackageID, analyzers []*Analyzer) ([]*Diagnostic, error) + // Analyze runs the specified analyzers on the given package at this snapshot. + Analyze(ctx context.Context, id PackageID, analyzers []*Analyzer) ([]*Diagnostic, error) // RunGoCommandPiped runs the given `go` command, writing its output // to stdout and stderr. Verb, Args, and WorkingDir must be specified. @@ -198,11 +229,12 @@ type Snapshot interface { // need ever to "load everything at once" using this function. KnownPackages(ctx context.Context) ([]Package, error) - // ActivePackages returns the packages considered 'active' in the workspace. + // ActiveMetadata returns a new, unordered slice containing + // metadata for all packages considered 'active' in the workspace. // // In normal memory mode, this is all workspace packages. In degraded memory // mode, this is just the reverse transitive closure of open packages. - ActivePackages(ctx context.Context) ([]Package, error) + ActiveMetadata(ctx context.Context) ([]*Metadata, error) // AllValidMetadata returns all valid metadata loaded for the snapshot. AllValidMetadata(ctx context.Context) ([]*Metadata, error) @@ -217,9 +249,15 @@ type Snapshot interface { // MetadataForFile returns a new slice containing metadata for each // package containing the Go file identified by uri, ordered by the // number of CompiledGoFiles (i.e. "narrowest" to "widest" package). + // The result may include tests and intermediate test variants of + // importable packages. // It returns an error if the context was cancelled. MetadataForFile(ctx context.Context, uri span.URI) ([]*Metadata, error) + // TypeCheck parses and type-checks the specified packages, + // and returns them in the same order as the ids. + TypeCheck(ctx context.Context, mode TypecheckMode, ids ...PackageID) ([]Package, error) + // GetCriticalError returns any critical errors in the workspace. // // A nil result may mean success, or context cancellation. From d72a64a960cdb33eaefd4fbbde3266424d314009 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Tue, 13 Dec 2022 10:33:44 +0000 Subject: [PATCH 533/723] gopls/internal/lsp: add selection range request selectionRange defines the textDocument/selectionRange feature, which, given a list of positions within a file, reports a linked list of enclosing syntactic blocks, innermost first. See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange. This feature can be used by a client to implement "expand selection" in a language-aware fashion. Multiple input positions are supported to allow for multiple cursors, and the entire path up to the whole document is returned for each cursor to avoid multiple round-trips when the user is likely to issue this command multiple times in quick succession. Fixes golang/go#36679 Change-Id: I4852db4b40be24b3dc13e4d9d9238c1a9ac5f824 GitHub-Last-Rev: 0a117415b1675d5196cb84c953bfa42fb7854291 GitHub-Pull-Request: golang/tools#416 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452315 Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Robert Findley --- gopls/internal/lsp/cmd/test/cmdtest.go | 2 + gopls/internal/lsp/lsp_test.go | 65 +++++++++++++++++ gopls/internal/lsp/selection_range.go | 69 +++++++++++++++++++ gopls/internal/lsp/server_gen.go | 4 +- gopls/internal/lsp/source/source_test.go | 4 ++ .../lsp/testdata/selectionrange/foo.go | 13 ++++ .../lsp/testdata/selectionrange/foo.go.golden | 29 ++++++++ .../internal/lsp/testdata/summary.txt.golden | 1 + .../lsp/testdata/summary_go1.18.txt.golden | 1 + gopls/internal/lsp/tests/tests.go | 18 +++++ 10 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 gopls/internal/lsp/selection_range.go create mode 100644 gopls/internal/lsp/testdata/selectionrange/foo.go create mode 100644 gopls/internal/lsp/testdata/selectionrange/foo.go.golden diff --git a/gopls/internal/lsp/cmd/test/cmdtest.go b/gopls/internal/lsp/cmd/test/cmdtest.go index fed47497576..16497093883 100644 --- a/gopls/internal/lsp/cmd/test/cmdtest.go +++ b/gopls/internal/lsp/cmd/test/cmdtest.go @@ -118,6 +118,8 @@ func (r *runner) InlayHints(t *testing.T, spn span.Span) { // TODO: inlayHints not supported on command line } +func (r *runner) SelectionRanges(t *testing.T, spn span.Span) {} + func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) { rStdout, wStdout, err := os.Pipe() if err != nil { diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 23941be0f6e..59f286b8d45 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -1282,6 +1282,71 @@ func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) { } } +func (r *runner) SelectionRanges(t *testing.T, spn span.Span) { + uri := spn.URI() + sm, err := r.data.Mapper(uri) + if err != nil { + t.Fatal(err) + } + loc, err := sm.Location(spn) + if err != nil { + t.Error(err) + } + + ranges, err := r.server.selectionRange(r.ctx, &protocol.SelectionRangeParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(uri), + }, + Positions: []protocol.Position{loc.Range.Start}, + }) + if err != nil { + t.Fatal(err) + } + + sb := &strings.Builder{} + for i, path := range ranges { + fmt.Fprintf(sb, "Ranges %d: ", i) + rng := path + for { + s, err := sm.Offset(rng.Range.Start) + if err != nil { + t.Error(err) + } + e, err := sm.Offset(rng.Range.End) + if err != nil { + t.Error(err) + } + + var snippet string + if e-s < 30 { + snippet = string(sm.Content[s:e]) + } else { + snippet = string(sm.Content[s:s+15]) + "..." + string(sm.Content[e-15:e]) + } + + fmt.Fprintf(sb, "\n\t%v %q", rng.Range, strings.ReplaceAll(snippet, "\n", "\\n")) + + if rng.Parent == nil { + break + } + rng = *rng.Parent + } + sb.WriteRune('\n') + } + got := sb.String() + + testName := "selectionrange_" + tests.SpanName(spn) + want := r.data.Golden(t, testName, uri.Filename(), func() ([]byte, error) { + return []byte(got), nil + }) + if want == nil { + t.Fatalf("golden file %q not found", uri.Filename()) + } + if diff := compare.Text(got, string(want)); diff != "" { + t.Errorf("%s mismatch\n%s", testName, diff) + } +} + func TestBytesOffset(t *testing.T) { tests := []struct { text string diff --git a/gopls/internal/lsp/selection_range.go b/gopls/internal/lsp/selection_range.go new file mode 100644 index 00000000000..314f2240799 --- /dev/null +++ b/gopls/internal/lsp/selection_range.go @@ -0,0 +1,69 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lsp + +import ( + "context" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" +) + +// selectionRange defines the textDocument/selectionRange feature, +// which, given a list of positions within a file, +// reports a linked list of enclosing syntactic blocks, innermost first. +// +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange. +// +// This feature can be used by a client to implement "expand selection" in a +// language-aware fashion. Multiple input positions are supported to allow +// for multiple cursors, and the entire path up to the whole document is +// returned for each cursor to avoid multiple round-trips when the user is +// likely to issue this command multiple times in quick succession. +func (s *Server) selectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) { + ctx, done := event.Start(ctx, "lsp.Server.documentSymbol") + defer done() + + snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) + defer release() + if !ok { + return nil, err + } + + pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) + if err != nil { + return nil, err + } + + result := make([]protocol.SelectionRange, len(params.Positions)) + for i, protocolPos := range params.Positions { + pos, err := pgf.Mapper.Pos(protocolPos) + if err != nil { + return nil, err + } + + path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) + + tail := &result[i] // tail of the Parent linked list, built head first + + for j, node := range path { + rng, err := pgf.Mapper.PosRange(node.Pos(), node.End()) + if err != nil { + return nil, err + } + + // Add node to tail. + if j > 0 { + tail.Parent = &protocol.SelectionRange{} + tail = tail.Parent + } + tail.Range = rng + } + } + + return result, nil +} diff --git a/gopls/internal/lsp/server_gen.go b/gopls/internal/lsp/server_gen.go index 8f4ab10a71f..c6f618d6ef4 100644 --- a/gopls/internal/lsp/server_gen.go +++ b/gopls/internal/lsp/server_gen.go @@ -244,8 +244,8 @@ func (s *Server) ResolveWorkspaceSymbol(context.Context, *protocol.WorkspaceSymb return nil, notImplemented("ResolveWorkspaceSymbol") } -func (s *Server) SelectionRange(context.Context, *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) { - return nil, notImplemented("SelectionRange") +func (s *Server) SelectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) { + return s.selectionRange(ctx, params) } func (s *Server) SemanticTokensFull(ctx context.Context, p *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) { diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 98bac6df894..ae5906fed01 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -497,6 +497,10 @@ func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { t.Skip("nothing to test in source") } +func (r *runner) SelectionRanges(t *testing.T, spn span.Span) { + t.Skip("nothing to test in source") +} + func (r *runner) Import(t *testing.T, spn span.Span) { fh, err := r.snapshot.GetFile(r.ctx, spn.URI()) if err != nil { diff --git a/gopls/internal/lsp/testdata/selectionrange/foo.go b/gopls/internal/lsp/testdata/selectionrange/foo.go new file mode 100644 index 00000000000..1bf41340ce6 --- /dev/null +++ b/gopls/internal/lsp/testdata/selectionrange/foo.go @@ -0,0 +1,13 @@ +package foo + +import "time" + +func Bar(x, y int, t time.Time) int { + zs := []int{1, 2, 3} //@selectionrange("1") + + for _, z := range zs { + x = x + z + y + zs[1] //@selectionrange("1") + } + + return x + y //@selectionrange("+") +} diff --git a/gopls/internal/lsp/testdata/selectionrange/foo.go.golden b/gopls/internal/lsp/testdata/selectionrange/foo.go.golden new file mode 100644 index 00000000000..fe70b30b711 --- /dev/null +++ b/gopls/internal/lsp/testdata/selectionrange/foo.go.golden @@ -0,0 +1,29 @@ +-- selectionrange_foo_12_11 -- +Ranges 0: + 11:8-11:13 "x + y" + 11:1-11:13 "return x + y" + 4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}" + 4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}" + 0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}" + +-- selectionrange_foo_6_14 -- +Ranges 0: + 5:13-5:14 "1" + 5:7-5:21 "[]int{1, 2, 3}" + 5:1-5:21 "zs := []int{1, 2, 3}" + 4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}" + 4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}" + 0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}" + +-- selectionrange_foo_9_22 -- +Ranges 0: + 8:21-8:22 "1" + 8:18-8:23 "zs[1]" + 8:6-8:23 "x + z + y + zs[1]" + 8:2-8:23 "x = x + z + y + zs[1]" + 7:22-9:2 "{\\n\t\tx = x + z +...onrange(\"1\")\\n\t}" + 7:1-9:2 "for _, z := ran...onrange(\"1\")\\n\t}" + 4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}" + 4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}" + 0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}" + diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index cfe8e4a267d..3eac194f56d 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -28,4 +28,5 @@ WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 ImplementationsCount = 14 +SelectionRangesCount = 3 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 2b7bf976b2f..98e4bd2d653 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -28,4 +28,5 @@ WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 ImplementationsCount = 14 +SelectionRangesCount = 3 diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index cab96e0e82c..3b677653e50 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -93,6 +93,7 @@ type Signatures = map[span.Span]*protocol.SignatureHelp type Links = map[span.URI][]Link type AddImport = map[span.URI]string type Hovers = map[span.Span]string +type SelectionRanges = []span.Span type Data struct { Config packages.Config @@ -128,6 +129,7 @@ type Data struct { Links Links AddImport AddImport Hovers Hovers + SelectionRanges SelectionRanges fragments map[string]string dir string @@ -177,6 +179,7 @@ type Tests interface { Link(*testing.T, span.URI, []Link) AddImport(*testing.T, span.URI, string) Hover(*testing.T, span.Span, string) + SelectionRanges(*testing.T, span.Span) } type Definition struct { @@ -499,6 +502,7 @@ func load(t testing.TB, mode string, dir string) *Data { "incomingcalls": datum.collectIncomingCalls, "outgoingcalls": datum.collectOutgoingCalls, "addimport": datum.collectAddImports, + "selectionrange": datum.collectSelectionRanges, }); err != nil { t.Fatal(err) } @@ -947,6 +951,15 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) + t.Run("SelectionRanges", func(t *testing.T) { + t.Helper() + for _, span := range data.SelectionRanges { + t.Run(SpanName(span), func(t *testing.T) { + tests.SelectionRanges(t, span) + }) + } + }) + if *UpdateGolden { for _, golden := range data.golden { if !golden.Modified { @@ -1039,6 +1052,7 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures)) fmt.Fprintf(buf, "LinksCount = %v\n", linksCount) fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations)) + fmt.Fprintf(buf, "SelectionRangesCount = %v\n", len(data.SelectionRanges)) want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) { return buf.Bytes(), nil @@ -1240,6 +1254,10 @@ func (data *Data) collectDefinitions(src, target span.Span) { } } +func (data *Data) collectSelectionRanges(spn span.Span) { + data.SelectionRanges = append(data.SelectionRanges, spn) +} + func (data *Data) collectImplementations(src span.Span, targets []span.Span) { data.Implementations[src] = targets } From 95c9dce0e5de826321c67942b40378650db32dad Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 13 Dec 2022 09:02:37 -0500 Subject: [PATCH 534/723] internal/lsp/mod: fix run_govulncheck codelens name Codelens is presented without any prior diagnostic, so there is nothing to verify. Change-Id: Ied04d69dd53399a9790808c670266cddaf658c39 Reviewed-on: https://go-review.googlesource.com/c/tools/+/457175 Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/mod/code_lens.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go index 99684346362..eb8d8b79d88 100644 --- a/gopls/internal/lsp/mod/code_lens.go +++ b/gopls/internal/lsp/mod/code_lens.go @@ -178,7 +178,7 @@ func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.Fi return nil, err } - vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck to verify", command.VulncheckArgs{ + vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ URI: uri, Pattern: "./...", }) From 601ca6ce812601c7856ce9c47aee18e72f370ecb Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 12 Dec 2022 22:43:40 -0500 Subject: [PATCH 535/723] gopls/internal/lsp/source: use correct token.File The previous code was trying to map (token.File, token.Pos, token.Pos) to a protocol.Range by parsing the file again and using its Mapper. But parsing may cause a new token.File and Mapper to be created, to which the start/end Pos values do not belong. However, if the ParseGo call makes a cache hit, the same Mapper is returned and the bug does not appear. A recent change to the parse cache has caused it to no longer always make cache hits, revealing the bug. (Converting between position representations remains far too complicated.) Also, don't discard the error message that was previously returned by the safetoken.Offset failure. Change-Id: I2bb9646cccc4336e27bdafab43f97d714c2ea409 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456977 gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/fake/editor.go | 5 ++++- gopls/internal/lsp/source/fix.go | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 4c974bde4c6..7b476529573 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -858,10 +858,13 @@ func (e *Editor) OrganizeImports(ctx context.Context, path string) error { // RefactorRewrite requests and performs the source.refactorRewrite codeAction. func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error { applied, err := e.applyCodeActions(ctx, path, rng, nil, protocol.RefactorRewrite) + if err != nil { + return err + } if applied == 0 { return fmt.Errorf("no refactorings were applied") } - return err + return nil } // ApplyQuickFixes requests and performs the quickfix codeAction. diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 537692ef677..1584e2bb38e 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -109,11 +109,12 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi } editsPerFile[fh.URI()] = te } - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + content, err := fh.Read() if err != nil { return nil, err } - rng, err := pgf.Mapper.PosRange(edit.Pos, end) + m := protocol.ColumnMapper{URI: fh.URI(), TokFile: tokFile, Content: content} + rng, err := m.PosRange(edit.Pos, end) if err != nil { return nil, err } From 444c8f69ae8bb5efb58c8aa8a00fc14f6a5d1de7 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 13 Dec 2022 13:07:41 -0500 Subject: [PATCH 536/723] gopls/internal/lsp/cache: only invalidate parsed files if they changed As an optimization, we only need to invalidate parsed files if the file content actually changed (which may not be the case for e.g. didOpen or didClose notifications). This also reduces the likelihood of redundant parsed go files across packages. Change-Id: I4fd5c8703643a109da83cdcabada4b88ffb0502f Reviewed-on: https://go-review.googlesource.com/c/tools/+/457257 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/snapshot.go | 24 ++++++++++++++++++------ gopls/internal/regtest/misc/leak_test.go | 3 +++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index b439e9e2908..88ae7feecfa 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1740,13 +1740,25 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // TODO(adonovan): merge loops over "changes". - for uri := range changes { - keys, ok := result.parseKeysByURI.Get(uri) - if ok { - for _, key := range keys { - result.parsedGoFiles.Delete(key) + for uri, change := range changes { + // Optimization: if the content did not change, we don't need to evict the + // parsed file. This is not the case for e.g. the files map, which may + // switch from on-disk state to overlay. Parsed files depend only on + // content and parse mode (which is captured in the parse key). + // + // NOTE: This also makes it less likely that we re-parse a file due to a + // cache-miss but get a cache-hit for the corresponding package. In the + // past, there was code that relied on ParseGo returning the type-checked + // syntax tree. That code was wrong, but avoiding invalidation here limits + // the blast radius of these types of bugs. + if !change.isUnchanged { + keys, ok := result.parseKeysByURI.Get(uri) + if ok { + for _, key := range keys { + result.parsedGoFiles.Delete(key) + } + result.parseKeysByURI.Delete(uri) } - result.parseKeysByURI.Delete(uri) } // Invalidate go.mod-related handles. diff --git a/gopls/internal/regtest/misc/leak_test.go b/gopls/internal/regtest/misc/leak_test.go index 71c8b13309f..28a5843ec2e 100644 --- a/gopls/internal/regtest/misc/leak_test.go +++ b/gopls/internal/regtest/misc/leak_test.go @@ -21,6 +21,9 @@ import ( // Test for golang/go#57222. func TestCacheLeak(t *testing.T) { + // TODO(rfindley): either fix this test with additional instrumentation, or + // delete it. + t.Skip("This test races with cache eviction.") const files = `-- a.go -- package a From 9ec855317b921fef87bad6bf492ef7cd0d431db8 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 8 Dec 2022 17:25:47 -0500 Subject: [PATCH 537/723] gopls/internal/lsp/source: emit interface stub methods at top level Before, the insertion point of the new methods was after the TypeSpec that declared the concrete type, but that isn't valid if the containing GenDecl is parenthesized. This change causes it to insert the methods after the decl. Also - a regression test - a would-be test of attempting to stub a function-local type, but the test framework cannot express errors yet. - sort embedded interfaces, since go/types changed its sort order in go1.18, and this CL's new test is the first time it has been exercised with an interface (io.ReadCloser) that has more than one embedded interface. Needless to say, figuring that out took 10x more time than actually fixing the reported bug. Fixes golang/go#56825 Change-Id: I0fc61f297befd12e8a7224dcbd6a197cf9e4b1cf Reviewed-on: https://go-review.googlesource.com/c/tools/+/456316 Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller --- .../lsp/analysis/stubmethods/stubmethods.go | 6 +- gopls/internal/lsp/source/stub.go | 93 +++++++++++++------ .../lsp/testdata/stub/stub_typedecl_group.go | 27 ++++++ .../stub/stub_typedecl_group.go.golden | 39 ++++++++ .../internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- 6 files changed, 137 insertions(+), 32 deletions(-) create mode 100644 gopls/internal/lsp/testdata/stub/stub_typedecl_group.go create mode 100644 gopls/internal/lsp/testdata/stub/stub_typedecl_group.go.golden diff --git a/gopls/internal/lsp/analysis/stubmethods/stubmethods.go b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go index 9092f8b1276..9ff869225ff 100644 --- a/gopls/internal/lsp/analysis/stubmethods/stubmethods.go +++ b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go @@ -84,7 +84,7 @@ type StubInfo struct { // in the case where the concrete type file requires a new import that happens to be renamed // in the interface file. // TODO(marwan-at-work): implement interface literals. - Interface types.Object + Interface *types.TypeName Concrete *types.Named Pointer bool } @@ -335,7 +335,7 @@ func RelativeToFiles(concPkg *types.Package, concFile *ast.File, ifaceImports [] // ifaceType will try to extract the types.Object that defines // the interface given the ast.Expr where the "missing method" // or "conversion" errors happen. -func ifaceType(n ast.Expr, ti *types.Info) types.Object { +func ifaceType(n ast.Expr, ti *types.Info) *types.TypeName { tv, ok := ti.Types[n] if !ok { return nil @@ -343,7 +343,7 @@ func ifaceType(n ast.Expr, ti *types.Info) types.Object { return ifaceObjFromType(tv.Type) } -func ifaceObjFromType(t types.Type) types.Object { +func ifaceObjFromType(t types.Type) *types.TypeName { named, ok := t.(*types.Named) if !ok { return nil diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index fa1b07b6c84..f4e8f0c632b 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -13,6 +13,7 @@ import ( "go/parser" "go/token" "go/types" + "sort" "strings" "golang.org/x/tools/go/analysis" @@ -38,6 +39,13 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi return nil, nil, fmt.Errorf("nil interface request") } + // A function-local type cannot be stubbed + // since there's nowhere to put the methods. + conc := si.Concrete.Obj() + if conc != conc.Pkg().Scope().Lookup(conc.Name()) { + return nil, nil, fmt.Errorf("local type %q cannot be stubbed", conc.Name()) + } + // Parse the file defining the concrete type. concreteFilename := snapshot.FileSet().Position(si.Concrete.Obj().Pos()).Filename concreteFH, err := snapshot.GetFile(ctx, span.URIFromPath(concreteFilename)) @@ -56,24 +64,36 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi methodsSrc = stubErr(ctx, parsedConcreteFile.File, si, snapshot) } else { methodsSrc, stubImports, err = stubMethods(ctx, parsedConcreteFile.File, si, snapshot) + if err != nil { + return nil, nil, fmt.Errorf("stubMethods: %w", err) + } } - if err != nil { - return nil, nil, fmt.Errorf("stubMethods: %w", err) + + // Splice the methods into the file. + // The insertion point is after the top-level declaration + // enclosing the (package-level) type object. + insertPos := parsedConcreteFile.File.End() + for _, decl := range parsedConcreteFile.File.Decls { + if decl.End() > conc.Pos() { + insertPos = decl.End() + break + } } - nodes, _ = astutil.PathEnclosingInterval(parsedConcreteFile.File, si.Concrete.Obj().Pos(), si.Concrete.Obj().Pos()) concreteSrc, err := concreteFH.Read() if err != nil { return nil, nil, fmt.Errorf("error reading concrete file source: %w", err) } - insertPos, err := safetoken.Offset(parsedConcreteFile.Tok, nodes[1].End()) - if err != nil || insertPos >= len(concreteSrc) { + insertOffset, err := safetoken.Offset(parsedConcreteFile.Tok, insertPos) + if err != nil || insertOffset >= len(concreteSrc) { return nil, nil, fmt.Errorf("insertion position is past the end of the file") } var buf bytes.Buffer - buf.Write(concreteSrc[:insertPos]) + buf.Write(concreteSrc[:insertOffset]) buf.WriteByte('\n') buf.Write(methodsSrc) - buf.Write(concreteSrc[insertPos:]) + buf.Write(concreteSrc[insertOffset:]) + + // Re-parse it, splice in imports, pretty-print it. fset := token.NewFileSet() newF, err := parser.ParseFile(fset, parsedConcreteFile.File.Name.Name, buf.Bytes(), parser.ParseComments) if err != nil { @@ -82,11 +102,12 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi for _, imp := range stubImports { astutil.AddNamedImport(fset, newF, imp.Name, imp.Path) } - var source bytes.Buffer - err = format.Node(&source, fset, newF) - if err != nil { + var source strings.Builder + if err := format.Node(&source, fset, newF); err != nil { return nil, nil, fmt.Errorf("format.Node: %w", err) } + + // Return the diff. diffs := snapshot.View().Options().ComputeEdits(string(parsedConcreteFile.Src), source.String()) tf := parsedConcreteFile.Mapper.TokFile var edits []analysis.TextEdit @@ -234,21 +255,11 @@ returns }, } */ -func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.MethodSet, concPkg *types.Package, ifaceObj types.Object, visited map[string]struct{}) ([]*missingInterface, error) { +func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.MethodSet, concPkg *types.Package, ifaceObj *types.TypeName, visited map[string]struct{}) ([]*missingInterface, error) { iface, ok := ifaceObj.Type().Underlying().(*types.Interface) if !ok { return nil, fmt.Errorf("expected %v to be an interface but got %T", iface, ifaceObj.Type().Underlying()) } - missing := []*missingInterface{} - for i := 0; i < iface.NumEmbeddeds(); i++ { - eiface := iface.Embedded(i).Obj() - em, err := missingMethods(ctx, snapshot, concMS, concPkg, eiface, visited) - if err != nil { - return nil, err - } - missing = append(missing, em...) - } - // Parse the imports from the file that declares the interface. ifaceFilename := snapshot.FileSet().Position(ifaceObj.Pos()).Filename ifaceFH, err := snapshot.GetFile(ctx, span.URIFromPath(ifaceFilename)) @@ -259,20 +270,23 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method if err != nil { return nil, fmt.Errorf("error parsing imports from interface file: %w", err) } + mi := &missingInterface{ - iface: iface, + iface: ifaceObj, imports: ifaceFile.File.Imports, } + + // Add all the interface methods not defined by the concrete type to mi.missing. for i := 0; i < iface.NumExplicitMethods(); i++ { method := iface.ExplicitMethod(i) - // if the concrete type does not have the interface method - if concMS.Lookup(concPkg, method.Name()) == nil { + if sel := concMS.Lookup(concPkg, method.Name()); sel == nil { + // Concrete type does not have the interface method. if _, ok := visited[method.Name()]; !ok { mi.missing = append(mi.missing, method) visited[method.Name()] = struct{}{} } - } - if sel := concMS.Lookup(concPkg, method.Name()); sel != nil { + } else { + // Concrete type does have the interface method. implSig := sel.Type().(*types.Signature) ifaceSig := method.Type().(*types.Signature) if !types.Identical(ifaceSig, implSig) { @@ -280,6 +294,31 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method } } } + + // Process embedded interfaces, recursively. + // + // TODO(adonovan): this whole computation could be expressed + // more simply without recursion, driven by the method + // sets of the interface and concrete types. Once the set + // difference (missing methods) is computed, the imports + // from the declaring file(s) could be loaded as needed. + var missing []*missingInterface + for i := 0; i < iface.NumEmbeddeds(); i++ { + eiface := iface.Embedded(i).Obj() + em, err := missingMethods(ctx, snapshot, concMS, concPkg, eiface, visited) + if err != nil { + return nil, err + } + missing = append(missing, em...) + } + // The type checker is deterministic, but its choice of + // ordering of embedded interfaces varies with Go version + // (e.g. go1.17 was sorted, go1.18 was lexical order). + // Sort to ensure test portability. + sort.Slice(missing, func(i, j int) bool { + return missing[i].iface.Id() < missing[j].iface.Id() + }) + if len(mi.missing) > 0 { missing = append(missing, mi) } @@ -290,7 +329,7 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method // that has all or some of its methods missing // from the destination concrete type type missingInterface struct { - iface *types.Interface + iface *types.TypeName imports []*ast.ImportSpec // the interface's import environment missing []*types.Func } diff --git a/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go b/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go new file mode 100644 index 00000000000..f82401fafdd --- /dev/null +++ b/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go @@ -0,0 +1,27 @@ +package stub + +// Regression test for Issue #56825: file corrupted by insertion of +// methods after TypeSpec in a parenthesized TypeDecl. + +import "io" + +func newReadCloser() io.ReadCloser { + return rdcloser{} //@suggestedfix("rd", "refactor.rewrite", "") +} + +type ( + A int + rdcloser struct{} + B int +) + +func _() { + // Local types can't be stubbed as there's nowhere to put the methods. + // The suggestedfix assertion can't express this yet. TODO(adonovan): support it. + type local struct{} + var _ io.ReadCloser = local{} // want error: `local type "local" cannot be stubbed` +} + +type ( + C int +) diff --git a/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go.golden b/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go.golden new file mode 100644 index 00000000000..0848789eaf6 --- /dev/null +++ b/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go.golden @@ -0,0 +1,39 @@ +-- suggestedfix_stub_typedecl_group_9_9 -- +package stub + +// Regression test for Issue #56825: file corrupted by insertion of +// methods after TypeSpec in a parenthesized TypeDecl. + +import "io" + +func newReadCloser() io.ReadCloser { + return rdcloser{} //@suggestedfix("rd", "refactor.rewrite", "") +} + +type ( + A int + rdcloser struct{} + B int +) + +// Close implements io.ReadCloser +func (rdcloser) Close() error { + panic("unimplemented") +} + +// Read implements io.ReadCloser +func (rdcloser) Read(p []byte) (n int, err error) { + panic("unimplemented") +} + +func _() { + // Local types can't be stubbed as there's nowhere to put the methods. + // The suggestedfix assertion can't express this yet. TODO(adonovan): support it. + type local struct{} + var _ io.ReadCloser = local{} // want error: `local type "local" cannot be stubbed` +} + +type ( + C int +) + diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 3eac194f56d..c4e047b5827 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -13,7 +13,7 @@ FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 SemanticTokenCount = 3 -SuggestedFixCount = 63 +SuggestedFixCount = 64 FunctionExtractionCount = 27 MethodExtractionCount = 6 DefinitionsCount = 99 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 98e4bd2d653..17df1010a76 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -13,7 +13,7 @@ FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 SemanticTokenCount = 3 -SuggestedFixCount = 69 +SuggestedFixCount = 70 FunctionExtractionCount = 27 MethodExtractionCount = 6 DefinitionsCount = 110 From cc0e696e515cbe7d8d1a02cc5f61a09ac8e94e20 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 13 Dec 2022 13:22:32 -0500 Subject: [PATCH 538/723] gopls/internal/hooks: panic if diff consistency check fails This will detect diff/apply bugs in our tests by panicking. (There is no evidence of any such bugs so far, though there is one known bug in unified, which this check would not detect.) Change-Id: If6b9d6a60716dd0de68a0e684bcb6c13d3911cab Reviewed-on: https://go-review.googlesource.com/c/tools/+/457295 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Alan Donovan --- gopls/internal/hooks/diff.go | 3 +-- gopls/test/gopls_test.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go index a0383b87675..f7fec5a7bb2 100644 --- a/gopls/internal/hooks/diff.go +++ b/gopls/internal/hooks/diff.go @@ -96,8 +96,7 @@ func disaster(before, after string) string { return "" } - // TODO(adonovan): is there a better way to surface this? - log.Printf("Bug detected in diff algorithm! Please send file %s to the maintainers of gopls if you are comfortable sharing its contents.", filename) + bug.Reportf("Bug detected in diff algorithm! Please send file %s to the maintainers of gopls if you are comfortable sharing its contents.", filename) return filename } diff --git a/gopls/test/gopls_test.go b/gopls/test/gopls_test.go index 95534d44327..efa998cd1de 100644 --- a/gopls/test/gopls_test.go +++ b/gopls/test/gopls_test.go @@ -36,7 +36,7 @@ func TestCommandLine(t *testing.T) { func commandLineOptions(options *source.Options) { options.Staticcheck = true - options.GoDiff = false + options.GoDiff = false // workaround for golang/go#57290 TODO(pjw): fix tests.DefaultOptions(options) hooks.Options(options) } From 763a030753f05c37ef543bd9b325f647229781d3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 12 Dec 2022 15:07:03 -0500 Subject: [PATCH 539/723] gopls/internal/lsp/cache: delete Snapshot.KnownPackages It is redundant w.r.t. AllValidMetadata (now named AllMetadata) followed by TypeCheck. Change-Id: Ibca1516e3d9f036e4d2682b43cd73c29e8d5d981 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456975 Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/snapshot.go | 22 +-------------------- gopls/internal/lsp/source/implementation.go | 12 +++++++++-- gopls/internal/lsp/source/known_packages.go | 4 ++-- gopls/internal/lsp/source/rename.go | 2 +- gopls/internal/lsp/source/view.go | 11 ++--------- gopls/internal/vulncheck/command.go | 8 ++++---- 6 files changed, 20 insertions(+), 39 deletions(-) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 88ae7feecfa..c518aca2c0c 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1136,27 +1136,7 @@ func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { return result } -func (s *snapshot) KnownPackages(ctx context.Context) ([]source.Package, error) { - if err := s.awaitLoaded(ctx); err != nil { - return nil, err - } - - s.mu.Lock() - g := s.meta - s.mu.Unlock() - - pkgs := make([]source.Package, 0, len(g.metadata)) - for id := range g.metadata { - pkg, err := s.checkedPackage(ctx, id, s.workspaceParseMode(id)) - if err != nil { - return nil, err - } - pkgs = append(pkgs, pkg) - } - return pkgs, nil -} - -func (s *snapshot) AllValidMetadata(ctx context.Context) ([]*source.Metadata, error) { +func (s *snapshot) AllMetadata(ctx context.Context) ([]*source.Metadata, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index ca62f4e664d..315e38857ab 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -68,11 +68,19 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. allNamed []*types.Named pkgs = make(map[*types.Package]Package) ) - knownPkgs, err := s.KnownPackages(ctx) + metas, err := s.AllMetadata(ctx) if err != nil { return nil, err } - for _, pkg := range knownPkgs { + ids := make([]PackageID, len(metas)) + for i, m := range metas { + ids[i] = m.ID + } + packages, err := s.TypeCheck(ctx, TypecheckFull, ids...) + if err != nil { + return nil, err + } + for _, pkg := range packages { pkgs[pkg.GetTypes()] = pkg for _, obj := range pkg.GetTypesInfo().Defs { obj, ok := obj.(*types.TypeName) diff --git a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go index 19abf8f6578..d84febe5417 100644 --- a/gopls/internal/lsp/source/known_packages.go +++ b/gopls/internal/lsp/source/known_packages.go @@ -58,7 +58,7 @@ func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh VersionedFileH } // Now find candidates among known packages. - knownPkgs, err := snapshot.AllValidMetadata(ctx) + knownPkgs, err := snapshot.AllMetadata(ctx) if err != nil { return nil, err } @@ -86,7 +86,7 @@ func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh VersionedFileH if isDirectlyCyclical(current, knownPkg) { continue } - // AllValidMetadata may have multiple variants of a pkg. + // AllMetadata may have multiple variants of a pkg. seen[knownPkg.PkgPath] = true } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 6855e20a90d..9b089cfe673 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -204,7 +204,7 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, return nil, true, fmt.Errorf("cannot rename to _test package") } - metadata, err := s.AllValidMetadata(ctx) + metadata, err := s.AllMetadata(ctx) if err != nil { return nil, true, err } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 493553bc86d..6b32639d521 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -222,13 +222,6 @@ type Snapshot interface { // It is intended for use only in completions. CachedImportPaths(ctx context.Context) (map[PackagePath]Package, error) - // KnownPackages returns a new unordered list of all packages - // loaded in this snapshot, checked in TypecheckWorkspace mode. - // - // TODO(adonovan): opt: rewrite 'implementations' to avoid the - // need ever to "load everything at once" using this function. - KnownPackages(ctx context.Context) ([]Package, error) - // ActiveMetadata returns a new, unordered slice containing // metadata for all packages considered 'active' in the workspace. // @@ -236,8 +229,8 @@ type Snapshot interface { // mode, this is just the reverse transitive closure of open packages. ActiveMetadata(ctx context.Context) ([]*Metadata, error) - // AllValidMetadata returns all valid metadata loaded for the snapshot. - AllValidMetadata(ctx context.Context) ([]*Metadata, error) + // AllMetadata returns a new unordered array of metadata for all packages in the workspace. + AllMetadata(ctx context.Context) ([]*Metadata, error) // Symbols returns all symbols in the snapshot. Symbols(ctx context.Context) map[span.URI][]Symbol diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index 45c7076f464..c45096689d0 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -202,18 +202,18 @@ func finalDigitsIndex(s string) int { func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile source.FileHandle) (*govulncheck.Result, error) { // We want to report the intersection of vulnerable packages in the vulndb // and packages transitively imported by this module ('go list -deps all'). - // We use snapshot.AllValidMetadata to retrieve the list of packages + // We use snapshot.AllMetadata to retrieve the list of packages // as an approximation. // - // TODO(hyangah): snapshot.AllValidMetadata is a superset of + // TODO(hyangah): snapshot.AllMetadata is a superset of // `go list all` - e.g. when the workspace has multiple main modules // (multiple go.mod files), that can include packages that are not // used by this module. Vulncheck behavior with go.work is not well // defined. Figure out the meaning, and if we decide to present // the result as if each module is analyzed independently, make // gopls track a separate build list for each module and use that - // information instead of snapshot.AllValidMetadata. - metadata, err := snapshot.AllValidMetadata(ctx) + // information instead of snapshot.AllMetadata. + metadata, err := snapshot.AllMetadata(ctx) if err != nil { return nil, err } From 5899b6a2be07c97a4c09f5b5288a574f71fed608 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 14 Dec 2022 18:58:19 -0500 Subject: [PATCH 540/723] gopls: upgrade dependencies following release Fixes golang/go#57092 Change-Id: I78a56b7cfecd308588ca27b63157b027eb42cf70 Reviewed-on: https://go-review.googlesource.com/c/tools/+/457658 Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/go.mod | 9 ++++----- gopls/go.sum | 17 +++++------------ 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/gopls/go.mod b/gopls/go.mod index e5c35a6abea..42c0e27cd03 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -11,20 +11,19 @@ require ( golang.org/x/sync v0.1.0 golang.org/x/sys v0.3.0 golang.org/x/text v0.5.0 - golang.org/x/tools v0.2.1-0.20221108172846-9474ca31d0df - golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe + golang.org/x/tools v0.4.0 + golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.3.3 mvdan.cc/gofumpt v0.4.0 mvdan.cc/xurls/v2 v2.4.0 ) -require golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect - require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/google/safehtml v0.1.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect ) replace golang.org/x/tools => ../ diff --git a/gopls/go.sum b/gopls/go.sum index 0e4b9523d60..984d8fa503c 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -45,20 +45,16 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326 h1:fl8k2zg28yA23264d82M4dp+YlJ3ngDcpuB1bewkQi4= -golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y= +golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -70,18 +66,15 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe h1:qptQiQwEpETwDiz85LKtChqif9xhVkAm8Nhxs0xnTww= -golang.org/x/vuln v0.0.0-20221109205719-3af8368ee4fe/go.mod h1:8nFLBv8KFyZ2VuczUYssYKh+fcBR3BuXDG/HIWcxlwM= +golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a h1:KWIh6uTTw7r3PEz1N1OIEM8pr5bf1uP1n6JL5Ml56X8= +golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a/go.mod h1:54iI0rrZVM8VdIvTrT/sdlVfMUJWOgvTRQN24CEtZk0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 3cb82d5f9c98d67e87672ca017a22189388bffa3 Mon Sep 17 00:00:00 2001 From: Matthew Dempsky Date: Wed, 14 Dec 2022 15:28:04 -0800 Subject: [PATCH 541/723] go/ssa/interp: fix conversion of slice to named array The sliceToArrayPointer op failed to handle the case of named array types. The slice2arrayptr.go test file actually already includes code to cover this case, but it isn't executed dynamically. While here, simplify the variable naming conventions. E.g., the old code shadowed utSrc with the Pointer type from t_dst, which was very confusing and led to discovering named arrays were mishandled. Change-Id: I9f17eaa9bce4d2bd672de83864d70e209960f9e7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/457755 gopls-CI: kokoro Run-TryBot: Matthew Dempsky TryBot-Result: Gopher Robot Auto-Submit: Matthew Dempsky Reviewed-by: Tim King Reviewed-by: Cuong Manh Le --- go/ssa/interp/ops.go | 11 ++++------- go/ssa/interp/testdata/slice2arrayptr.go | 2 ++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/go/ssa/interp/ops.go b/go/ssa/interp/ops.go index 188899d69f3..0ea10da9e02 100644 --- a/go/ssa/interp/ops.go +++ b/go/ssa/interp/ops.go @@ -1395,18 +1395,15 @@ func conv(t_dst, t_src types.Type, x value) value { // sliceToArrayPointer converts the value x of type slice to type t_dst // a pointer to array and returns the result. func sliceToArrayPointer(t_dst, t_src types.Type, x value) value { - utSrc := t_src.Underlying() - utDst := t_dst.Underlying() - - if _, ok := utSrc.(*types.Slice); ok { - if utSrc, ok := utDst.(*types.Pointer); ok { - if arr, ok := utSrc.Elem().(*types.Array); ok { + if _, ok := t_src.Underlying().(*types.Slice); ok { + if ptr, ok := t_dst.Underlying().(*types.Pointer); ok { + if arr, ok := ptr.Elem().Underlying().(*types.Array); ok { x := x.([]value) if arr.Len() > int64(len(x)) { panic("array length is greater than slice length") } if x == nil { - return zero(utSrc) + return zero(t_dst) } v := value(array(x[:arr.Len()])) return &v diff --git a/go/ssa/interp/testdata/slice2arrayptr.go b/go/ssa/interp/testdata/slice2arrayptr.go index ff2d9b55ccd..d9d8804d36a 100644 --- a/go/ssa/interp/testdata/slice2arrayptr.go +++ b/go/ssa/interp/testdata/slice2arrayptr.go @@ -32,6 +32,8 @@ func main() { }, "runtime error: array length is greater than slice length", ) + + f() } type arr [2]int From b2b9dc3de7ada0e2cafb956b2e1cdcf6e2a8e5e2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 12 Dec 2022 11:31:59 -0500 Subject: [PATCH 542/723] gopls/internal/lsp/cache: reduce type-checking in renameImports Before, package renaming would type-check the entire reverse transitive closure. This change avoids type checking for indirect reverse dependencies, or when the package's (leaf) name hasn't changed. The references operation also uses the new APIs (to request metadata separate from type checking), and uses it to avoid type checking entirely when computing references to a package name, but doesn't yet economize on the set of packages it type checks in the general case. That will come in a follow-up; eventually of course it will be based on a search index, not type checking. Also: - fix vendoring issues flagged by TODO. - Snapshot.ReverseDependencies now returns only metadata, no type checking. - metadataGraph.reverseTransitiveClosure returns a map. - added source.RemoveIntermediateTestVariants helper. - deleted snapshot.checkedPackage, no longer needed. Change-Id: Ia17c9d9c7922a45cf9b6e1bb86c4919a60da0215 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456759 Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/graph.go | 26 ++--- gopls/internal/lsp/cache/snapshot.go | 46 +++------ gopls/internal/lsp/source/references.go | 78 +++++++++++---- gopls/internal/lsp/source/rename.go | 125 ++++++++++++++++-------- gopls/internal/lsp/source/view.go | 18 +++- 5 files changed, 187 insertions(+), 106 deletions(-) diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index 709f1abfb2f..5df0234e2d6 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -103,26 +103,20 @@ func (g *metadataGraph) build() { } } -// reverseTransitiveClosure calculates the set of packages that transitively -// import an id in ids. The result also includes given ids. -// -// If includeInvalid is false, the algorithm ignores packages with invalid -// metadata (including those in the given list of ids). -func (g *metadataGraph) reverseTransitiveClosure(ids ...PackageID) map[PackageID]bool { - seen := make(map[PackageID]bool) +// reverseTransitiveClosure returns a new mapping containing the +// metadata for the specified packages along with any package that +// transitively imports one of them, keyed by ID. +func (g *metadataGraph) reverseTransitiveClosure(ids ...PackageID) map[PackageID]*source.Metadata { + seen := make(map[PackageID]*source.Metadata) var visitAll func([]PackageID) visitAll = func(ids []PackageID) { for _, id := range ids { - if seen[id] { - continue + if seen[id] == nil { + if m := g.metadata[id]; m != nil { + seen[id] = m + visitAll(g.importedBy[id]) + } } - m := g.metadata[id] - // Only use invalid metadata if we support it. - if m == nil { - continue - } - seen[id] = true - visitAll(g.importedBy[id]) } } visitAll(ids) diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index c518aca2c0c..1818d0e2e0f 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -643,17 +643,17 @@ func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode sourc return nil, err } - var ids []PackageID - for _, m := range metas { - // Filter out any intermediate test variants. - // We typically aren't interested in these - // packages for file= style queries. - if m.IsIntermediateTestVariant() && !includeTestVariants { - continue - } - ids = append(ids, m.ID) + // Optionally filter out any intermediate test variants. + // We typically aren't interested in these + // packages for file= style queries. + if !includeTestVariants { + metas = source.RemoveIntermediateTestVariants(metas) } + ids := make([]PackageID, len(metas)) + for i, m := range metas { + ids[i] = m.ID + } return s.TypeCheck(ctx, mode, ids...) } @@ -818,35 +818,21 @@ func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source return metas, nil } -func (s *snapshot) GetReverseDependencies(ctx context.Context, id PackageID) ([]source.Package, error) { +func (s *snapshot) ReverseDependencies(ctx context.Context, id PackageID) (map[PackageID]*source.Metadata, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } s.mu.Lock() meta := s.meta s.mu.Unlock() - ids := meta.reverseTransitiveClosure(id) + rdeps := meta.reverseTransitiveClosure(id) - // Make sure to delete the original package ID from the map. - delete(ids, id) - - var pkgs []source.Package - for id := range ids { - pkg, err := s.checkedPackage(ctx, id, s.workspaceParseMode(id)) - if err != nil { - return nil, err - } - pkgs = append(pkgs, pkg) - } - return pkgs, nil -} + // Remove the original package ID from the map. + // TODO(adonovan): we should make ReverseDependencies and + // reverseTransitiveClosure consistent wrt reflexiveness. + delete(rdeps, id) -func (s *snapshot) checkedPackage(ctx context.Context, id PackageID, mode source.ParseMode) (*pkg, error) { - ph, err := s.buildPackageHandle(ctx, id, mode) - if err != nil { - return nil, err - } - return ph.await(ctx, s) + return rdeps, nil } func (s *snapshot) getImportedBy(id PackageID) []PackageID { diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 49b82948ab0..b99c2b162ee 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -32,36 +32,60 @@ type ReferenceInfo struct { // References returns a list of references for a given identifier within the packages // containing i.File. Declarations appear first in the result. -func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) { +func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) { ctx, done := event.Start(ctx, "source.References") defer done() // Is the cursor within the package name declaration? - pgf, inPackageName, err := parsePackageNameDecl(ctx, s, f, pp) + pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) if err != nil { return nil, err } if inPackageName { - // TODO(rfindley): this is inaccurate, excluding test variants, and - // redundant with package renaming. Refactor to share logic. - renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckWorkspace, NarrowestPackage) + // TODO(rfindley): this is redundant with package renaming. Refactor to share logic. + metas, err := snapshot.MetadataForFile(ctx, f.URI()) if err != nil { return nil, err } + if len(metas) == 0 { + return nil, fmt.Errorf("found no package containing %s", f.URI()) + } + targetPkg := metas[len(metas)-1] // widest package // Find external references to the package. - rdeps, err := s.GetReverseDependencies(ctx, renamingPkg.ID()) + rdeps, err := snapshot.ReverseDependencies(ctx, targetPkg.ID) if err != nil { return nil, err } + var refs []*ReferenceInfo - for _, dep := range rdeps { - for _, f := range dep.CompiledGoFiles() { + for _, rdep := range rdeps { + // Optimization: skip this loop (parsing) if rdep + // doesn't directly import the target package. + // (This logic also appears in rename; perhaps we need + // a ReverseDirectDependencies method?) + direct := false + for _, importedID := range rdep.DepsByImpPath { + if importedID == targetPkg.ID { + direct = true + break + } + } + if !direct { + continue + } + + for _, uri := range rdep.CompiledGoFiles { + fh, err := snapshot.GetFile(ctx, uri) + if err != nil { + return nil, err + } + f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + if err != nil { + return nil, err + } for _, imp := range f.File.Imports { - // TODO(adonovan): there's an ImportPath==PackagePath - // comparison that doesn't account for vendoring. - // Use dep.Metadata().DepsByPkgPath instead. - if string(UnquoteImportPath(imp)) == string(renamingPkg.PkgPath()) { + if rdep.DepsByImpPath[UnquoteImportPath(imp)] == targetPkg.ID { refs = append(refs, &ReferenceInfo{ Name: pgf.File.Name.Name, MappedRange: NewMappedRange(f.Tok, f.Mapper, imp.Pos(), imp.End()), @@ -71,8 +95,16 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit } } - // Find internal references to the package within the package itself - for _, f := range renamingPkg.CompiledGoFiles() { + // Find the package declaration of each file in the target package itself. + for _, uri := range targetPkg.CompiledGoFiles { + fh, err := snapshot.GetFile(ctx, uri) + if err != nil { + return nil, err + } + f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + if err != nil { + return nil, err + } refs = append(refs, &ReferenceInfo{ Name: pgf.File.Name.Name, MappedRange: NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()), @@ -82,7 +114,7 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit return refs, nil } - qualifiedObjs, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) + qualifiedObjs, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp) // Don't return references for builtin types. if errors.Is(err, errBuiltin) { return nil, nil @@ -91,7 +123,7 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit return nil, err } - refs, err := references(ctx, s, qualifiedObjs, includeDeclaration, true, false) + refs, err := references(ctx, snapshot, qualifiedObjs, includeDeclaration, true, false) if err != nil { return nil, err } @@ -167,7 +199,19 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i // Only search dependents if the object is exported. if qo.obj.Exported() { - reverseDeps, err := snapshot.GetReverseDependencies(ctx, qo.pkg.ID()) + rdeps, err := snapshot.ReverseDependencies(ctx, qo.pkg.ID()) + if err != nil { + return nil, err + } + // TODO(adonovan): opt: if obj is a package-level object, we need + // only search among direct reverse dependencies. + ids := make([]PackageID, 0, len(rdeps)) + for _, rdep := range rdeps { + ids = append(ids, rdep.ID) + } + // TODO(adonovan): opt: build a search index + // that doesn't require type checking. + reverseDeps, err := snapshot.TypeCheck(ctx, TypecheckFull, ids...) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 9b089cfe673..5d08f0eb602 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -363,66 +363,111 @@ func renamePackageClause(ctx context.Context, m *Metadata, snapshot Snapshot, ne // newPath and name newName. // // Edits are written into the edits map. -func renameImports(ctx context.Context, s Snapshot, m *Metadata, newPath ImportPath, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { - // TODO(rfindley): we should get reverse dependencies as metadata first, - // rather then building the package immediately. We don't need reverse - // dependencies if they are intermediate test variants. - rdeps, err := s.GetReverseDependencies(ctx, m.ID) +func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath ImportPath, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { + rdeps, err := snapshot.ReverseDependencies(ctx, m.ID) if err != nil { return err } - for _, dep := range rdeps { - // Subtle: don't perform renaming in this package if it is not fully - // parsed. This can occur inside the workspace if dep is an intermediate - // test variant. ITVs are only ever parsed in export mode, and no file is - // found only in an ITV. Therefore the renaming will eventually occur in a - // full package. - // - // An alternative algorithm that may be more robust would be to first - // collect *files* that need to have their imports updated, and then - // perform the rename using s.PackageForFile(..., NarrowestPackage). - if dep.ParseMode() != ParseFull { + // Pass 1: rename import paths in import declarations. + needsTypeCheck := make(map[PackageID][]span.URI) + for _, rdep := range rdeps { + if rdep.IsIntermediateTestVariant() { + continue // for renaming, these variants are redundant + } + + // Optimization: skip packages that don't directly import m. + // (This logic also appears in 'references'; perhaps we need + // Snapshot.ReverseDirectDependencies?) + direct := false + for _, importedID := range rdep.DepsByImpPath { + if importedID == m.ID { + direct = true + break + } + } + if !direct { continue } - for _, f := range dep.CompiledGoFiles() { - if seen.add(f.URI, m.PkgPath) { + for _, uri := range rdep.CompiledGoFiles { + if seen.add(uri, m.PkgPath) { continue } - + fh, err := snapshot.GetFile(ctx, uri) + if err != nil { + return err + } + f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + if err != nil { + return err + } + if f.File.Name == nil { + continue // no package declaration + } for _, imp := range f.File.Imports { - // TODO(adonovan): what if RHS has "vendor/" prefix? - if UnquoteImportPath(imp) != ImportPath(m.PkgPath) { + if rdep.DepsByImpPath[UnquoteImportPath(imp)] != m.ID { continue // not the import we're looking for } + // If the import does not explicitly specify + // a local name, then we need to invoke the + // type checker to locate references to update. + if imp.Name == nil { + needsTypeCheck[rdep.ID] = append(needsTypeCheck[rdep.ID], uri) + } + // Create text edit for the import path (string literal). impPathMappedRange := NewMappedRange(f.Tok, f.Mapper, imp.Path.Pos(), imp.Path.End()) rng, err := impPathMappedRange.Range() if err != nil { return err } - edits[f.URI] = append(edits[f.URI], protocol.TextEdit{ + edits[uri] = append(edits[uri], protocol.TextEdit{ Range: rng, NewText: strconv.Quote(string(newPath)), }) + } + } + } - // If the package name of an import has not changed or if its import - // path already has a local package name, then we don't need to update - // the local package name. - if newName == m.Name || imp.Name != nil { - continue + // If the imported package's name hasn't changed, + // we don't need to rename references within each file. + if newName == m.Name { + return nil + } + + // Pass 2: rename local name (types.PkgName) of imported + // package throughout one or more files of the package. + ids := make([]PackageID, 0, len(needsTypeCheck)) + for id := range needsTypeCheck { + ids = append(ids, id) + } + pkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, ids...) + if err != nil { + return err + } + for i, id := range ids { + pkg := pkgs[i] + for _, uri := range needsTypeCheck[id] { + f, err := pkg.File(uri) + if err != nil { + return err + } + for _, imp := range f.File.Imports { + if imp.Name != nil { + continue // has explicit local name + } + if rdeps[id].DepsByImpPath[UnquoteImportPath(imp)] != m.ID { + continue // not the import we're looking for } - // Rename the types.PkgName locally within this file. - pkgname := dep.GetTypesInfo().Implicits[imp].(*types.PkgName) - qos := []qualifiedObject{{obj: pkgname, pkg: dep}} + pkgname := pkg.GetTypesInfo().Implicits[imp].(*types.PkgName) + qos := []qualifiedObject{{obj: pkgname, pkg: pkg}} - pkgScope := dep.GetTypes().Scope() - fileScope := dep.GetTypesInfo().Scopes[f.File] + pkgScope := pkg.GetTypes().Scope() + fileScope := pkg.GetTypesInfo().Scopes[f.File] - var changes map[span.URI][]protocol.TextEdit localName := string(newName) try := 0 @@ -431,20 +476,21 @@ func renameImports(ctx context.Context, s Snapshot, m *Metadata, newPath ImportP try++ localName = fmt.Sprintf("%s%d", newName, try) } - changes, err = renameObj(ctx, s, localName, qos) + changes, err := renameObj(ctx, snapshot, localName, qos) if err != nil { return err } - // If the chosen local package name matches the package's new name, delete the - // change that would have inserted an explicit local name, which is always - // the lexically first change. + // If the chosen local package name matches the package's + // new name, delete the change that would have inserted + // an explicit local name, which is always the lexically + // first change. if localName == string(newName) { - v := changes[f.URI] + v := changes[uri] sort.Slice(v, func(i, j int) bool { return protocol.CompareRange(v[i].Range, v[j].Range) < 0 }) - changes[f.URI] = v[1:] + changes[uri] = v[1:] } for uri, changeEdits := range changes { edits[uri] = append(edits[uri], changeEdits...) @@ -452,7 +498,6 @@ func renameImports(ctx context.Context, s Snapshot, m *Metadata, newPath ImportP } } } - return nil } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 6b32639d521..14a568a2f45 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -210,9 +210,10 @@ type Snapshot interface { // checked in mode and filtered by the package policy. PackageForFile(ctx context.Context, uri span.URI, mode TypecheckMode, selectPackage PackageFilter) (Package, error) - // GetActiveReverseDeps returns the active files belonging to the reverse - // dependencies of this file's package, checked in TypecheckWorkspace mode. - GetReverseDependencies(ctx context.Context, id PackageID) ([]Package, error) + // ReverseDependencies returns a new mapping whose entries are + // the ID and Metadata of each package in the workspace that + // transitively depends on the package denoted by id, excluding id itself. + ReverseDependencies(ctx context.Context, id PackageID) (map[PackageID]*Metadata, error) // CachedImportPaths returns all the imported packages loaded in this // snapshot, indexed by their package path (not import path, despite the name) @@ -471,6 +472,17 @@ func (m *Metadata) IsIntermediateTestVariant() bool { return m.ForTest != "" && m.ForTest != m.PkgPath && m.ForTest+"_test" != m.PkgPath } +// RemoveIntermediateTestVariants removes intermediate test variants, modifying the array. +func RemoveIntermediateTestVariants(metas []*Metadata) []*Metadata { + res := metas[:0] + for _, m := range metas { + if !m.IsIntermediateTestVariant() { + res = append(res, m) + } + } + return res +} + var ErrViewExists = errors.New("view already exists for session") // Overlay is the type for a file held in memory on a session. From 0d2045bacb3c232e0214cca4e00bcfb12b8ef703 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 11 Nov 2022 14:41:50 -0500 Subject: [PATCH 543/723] gopls/internal/lsp: document analysis-related functions This change adds doc comments to various functions related to analysis to try to reveal something of the structure. Also: - ensure all type-error analyzers return promptly if there are no errors. (Perhaps all analyzers can be applied at once?) - Replace Snapshot or View with a mere Options in various functions related to "are analyzers enabled?". Change-Id: I0dd19708b16a69474fd7ac2e2192043c6ab9f2a7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/449895 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- .../lsp/analysis/nonewvars/nonewvars.go | 3 +++ .../analysis/noresultvalues/noresultvalues.go | 3 +++ gopls/internal/lsp/cache/analysis.go | 8 +++++-- gopls/internal/lsp/cache/errors.go | 4 ++-- gopls/internal/lsp/lsp_test.go | 4 ++-- gopls/internal/lsp/server.go | 3 +++ gopls/internal/lsp/source/diagnostics.go | 10 +++++++++ gopls/internal/lsp/source/options.go | 21 +++++++++---------- gopls/internal/lsp/source/source_test.go | 2 +- gopls/internal/lsp/source/view.go | 11 ++++++---- gopls/internal/lsp/tests/util.go | 12 +++++------ 11 files changed, 53 insertions(+), 28 deletions(-) diff --git a/gopls/internal/lsp/analysis/nonewvars/nonewvars.go b/gopls/internal/lsp/analysis/nonewvars/nonewvars.go index 2d2044eb397..6937b36d1f5 100644 --- a/gopls/internal/lsp/analysis/nonewvars/nonewvars.go +++ b/gopls/internal/lsp/analysis/nonewvars/nonewvars.go @@ -39,6 +39,9 @@ var Analyzer = &analysis.Analyzer{ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + if len(pass.TypeErrors) == 0 { + return nil, nil + } nodeFilter := []ast.Node{(*ast.AssignStmt)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { diff --git a/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go index b3fd84bcc96..41952a5479e 100644 --- a/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go +++ b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go @@ -38,6 +38,9 @@ var Analyzer = &analysis.Analyzer{ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + if len(pass.TypeErrors) == 0 { + return nil, nil + } nodeFilter := []ast.Node{(*ast.ReturnStmt)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 69bb39313ab..0b1fa6a9b0f 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -23,6 +23,10 @@ import ( "golang.org/x/tools/internal/memoize" ) +// Analyze returns a new slice of diagnostics from running the +// specified analyzers on the package denoted by id. +// +// (called from: (*snapshot).DiagnosePackage, source.Analyze.) func (s *snapshot) Analyze(ctx context.Context, id PackageID, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) { // TODO(adonovan): merge these two loops. There's no need to // construct all the root action handles before beginning @@ -31,7 +35,7 @@ func (s *snapshot) Analyze(ctx context.Context, id PackageID, analyzers []*sourc // called in parallel.) var roots []*actionHandle for _, a := range analyzers { - if !a.IsEnabled(s.view) { + if !a.IsEnabled(s.view.Options()) { continue } ah, err := s.actionHandle(ctx, id, a.Analyzer) @@ -413,7 +417,7 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a var diagnostics []*source.Diagnostic for _, diag := range rawDiagnostics { - srcDiags, err := analysisDiagnosticDiagnostics(snapshot, pkg, analyzer, &diag) + srcDiags, err := analysisDiagnosticDiagnostics(snapshot.view.Options(), pkg, analyzer, &diag) if err != nil { event.Error(ctx, "unable to compute analysis error position", err, tag.Category.Of(diag.Category), tag.Package.Of(string(pkg.ID()))) continue diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 1c9c21097e9..92d58e58261 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -192,10 +192,10 @@ func editGoDirectiveQuickFix(snapshot *snapshot, uri span.URI, version string) ( return []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, nil } -func analysisDiagnosticDiagnostics(snapshot *snapshot, pkg *pkg, a *analysis.Analyzer, e *analysis.Diagnostic) ([]*source.Diagnostic, error) { +func analysisDiagnosticDiagnostics(options *source.Options, pkg *pkg, a *analysis.Analyzer, e *analysis.Diagnostic) ([]*source.Diagnostic, error) { var srcAnalyzer *source.Analyzer // Find the analyzer that generated this diagnostic. - for _, sa := range source.EnabledAnalyzers(snapshot) { + for _, sa := range source.EnabledAnalyzers(options) { if a == sa.Analyzer { srcAnalyzer = sa break diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 59f286b8d45..8a9e24d3a29 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -73,11 +73,11 @@ func testLSP(t *testing.T, datum *tests.Data) { // Enable type error analyses for tests. // TODO(golang/go#38212): Delete this once they are enabled by default. - tests.EnableAllAnalyzers(view, options) + tests.EnableAllAnalyzers(options) session.SetViewOptions(ctx, view, options) // Enable all inlay hints for tests. - tests.EnableAllInlayHints(view, options) + tests.EnableAllInlayHints(options) // Only run the -modfile specific tests in module mode with Go 1.14 or above. datum.ModfileFlagAvailable = len(snapshot.ModFiles()) > 0 && testenv.Go1Point() >= 14 diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go index 6d1775ff46b..7e70d0a76ab 100644 --- a/gopls/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -130,6 +130,9 @@ func (s *Server) nonstandardRequest(ctx context.Context, method string, params i switch method { case "gopls/diagnoseFiles": paramMap := params.(map[string]interface{}) + // TODO(adonovan): opt: parallelize FileDiagnostics(URI...), either + // by calling it in multiple goroutines or, better, by making + // the relevant APIs accept a set of URIs/packages. for _, file := range paramMap["files"].([]interface{}) { snapshot, fh, ok, release, err := s.beginFileRequest(ctx, protocol.DocumentURI(file.(string)), source.UnknownKind) defer release() diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index db4fc900a4e..60ef7e69604 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -25,6 +25,7 @@ type RelatedInformation struct { Message string } +// Analyze reports go/analysis-framework diagnostics in the specified package. func Analyze(ctx context.Context, snapshot Snapshot, pkgid PackageID, includeConvenience bool) (map[span.URI][]*Diagnostic, error) { // Exit early if the context has been canceled. This also protects us // from a race on Options, see golang/go#36699. @@ -61,6 +62,15 @@ func Analyze(ctx context.Context, snapshot Snapshot, pkgid PackageID, includeCon return reports, nil } +// FileDiagnostics reports diagnostics in the specified file. +// +// TODO(adonovan): factor in common with (*Server).codeAction, which +// executes { PackageForFile; DiagnosePackage; Analyze } too? +// +// TODO(adonovan): opt: this function is called in a loop from the +// "gopls/diagnoseFiles" nonstandard request handler. It would be more +// efficient to compute the set of packages and DiagnosePackage and +// Analyze them all at once. func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (VersionedFileIdentity, []*Diagnostic, error) { fh, err := snapshot.GetVersionedFile(ctx, uri) if err != nil { diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index fb596a82a27..cf8c6415176 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -1394,26 +1394,25 @@ func (r *OptionResult) setStringSlice(s *[]string) { } } -// EnabledAnalyzers returns all of the analyzers enabled for the given -// snapshot. -func EnabledAnalyzers(snapshot Snapshot) (analyzers []*Analyzer) { - for _, a := range snapshot.View().Options().DefaultAnalyzers { - if a.IsEnabled(snapshot.View()) { +// EnabledAnalyzers returns all of the analyzers enabled by the options. +func EnabledAnalyzers(options *Options) (analyzers []*Analyzer) { + for _, a := range options.DefaultAnalyzers { + if a.IsEnabled(options) { analyzers = append(analyzers, a) } } - for _, a := range snapshot.View().Options().TypeErrorAnalyzers { - if a.IsEnabled(snapshot.View()) { + for _, a := range options.TypeErrorAnalyzers { + if a.IsEnabled(options) { analyzers = append(analyzers, a) } } - for _, a := range snapshot.View().Options().ConvenienceAnalyzers { - if a.IsEnabled(snapshot.View()) { + for _, a := range options.ConvenienceAnalyzers { + if a.IsEnabled(options) { analyzers = append(analyzers, a) } } - for _, a := range snapshot.View().Options().StaticcheckAnalyzers { - if a.IsEnabled(snapshot.View()) { + for _, a := range options.StaticcheckAnalyzers { + if a.IsEnabled(options) { analyzers = append(analyzers, a) } } diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index ae5906fed01..ed8d9a45d68 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -62,7 +62,7 @@ func testSource(t *testing.T, datum *tests.Data) { // Enable type error analyses for tests. // TODO(golang/go#38212): Delete this once they are enabled by default. - tests.EnableAllAnalyzers(view, options) + tests.EnableAllAnalyzers(options) view, err = session.SetViewOptions(ctx, view, options) if err != nil { t.Fatal(err) diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 14a568a2f45..39a9a3ab2dc 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -692,6 +692,8 @@ type Analyzer struct { // Enabled reports whether the analyzer is enabled. This value can be // configured per-analysis in user settings. For staticcheck analyzers, // the value of the Staticcheck setting overrides this field. + // + // Most clients should use the IsEnabled method. Enabled bool // Fix is the name of the suggested fix name used to invoke the suggested @@ -709,14 +711,15 @@ type Analyzer struct { Severity protocol.DiagnosticSeverity } -func (a Analyzer) IsEnabled(view View) bool { +// Enabled reports whether this analyzer is enabled by the given options. +func (a Analyzer) IsEnabled(options *Options) bool { // Staticcheck analyzers can only be enabled when staticcheck is on. - if _, ok := view.Options().StaticcheckAnalyzers[a.Analyzer.Name]; ok { - if !view.Options().Staticcheck { + if _, ok := options.StaticcheckAnalyzers[a.Analyzer.Name]; ok { + if !options.Staticcheck { return false } } - if enabled, ok := view.Options().Analyses[a.Analyzer.Name]; ok { + if enabled, ok := options.Analyses[a.Analyzer.Name]; ok { return enabled } return a.Enabled diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index 02f60cb0a3b..4c6bfdac0e5 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -460,33 +460,33 @@ func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason return msg.String() } -func EnableAllAnalyzers(view source.View, opts *source.Options) { +func EnableAllAnalyzers(opts *source.Options) { if opts.Analyses == nil { opts.Analyses = make(map[string]bool) } for _, a := range opts.DefaultAnalyzers { - if !a.IsEnabled(view) { + if !a.IsEnabled(opts) { opts.Analyses[a.Analyzer.Name] = true } } for _, a := range opts.TypeErrorAnalyzers { - if !a.IsEnabled(view) { + if !a.IsEnabled(opts) { opts.Analyses[a.Analyzer.Name] = true } } for _, a := range opts.ConvenienceAnalyzers { - if !a.IsEnabled(view) { + if !a.IsEnabled(opts) { opts.Analyses[a.Analyzer.Name] = true } } for _, a := range opts.StaticcheckAnalyzers { - if !a.IsEnabled(view) { + if !a.IsEnabled(opts) { opts.Analyses[a.Analyzer.Name] = true } } } -func EnableAllInlayHints(view source.View, opts *source.Options) { +func EnableAllInlayHints(opts *source.Options) { if opts.Hints == nil { opts.Hints = make(map[string]bool) } From db9d10fc184e0d43e51ce6e8d03f14293a3dd067 Mon Sep 17 00:00:00 2001 From: Florian Zenker Date: Thu, 15 Dec 2022 09:05:37 +0000 Subject: [PATCH 544/723] go/gcexportdata: preallocate buffer to read into when size is known Google has a couple of very large Go targets that require reading a couple GiB of export data files. For one particular target, this optimization shaved off around 17 seconds of slice growth and GC during analysis. Change-Id: I052e009ac2d7dddd75a258aeee0cfe7ff9257d8a Reviewed-on: https://go-review.googlesource.com/c/tools/+/457835 Reviewed-by: Robert Griesemer Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Florian Zenker --- go/gcexportdata/gcexportdata.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go index 620446207e2..696927a0f10 100644 --- a/go/gcexportdata/gcexportdata.go +++ b/go/gcexportdata/gcexportdata.go @@ -27,7 +27,6 @@ import ( "go/token" "go/types" "io" - "io/ioutil" "os/exec" "golang.org/x/tools/internal/gcimporter" @@ -85,6 +84,19 @@ func NewReader(r io.Reader) (io.Reader, error) { } } +// readAll works the same way as io.ReadAll, but avoids allocations and copies +// by preallocating a byte slice of the necessary size if the size is known up +// front. This is always possible when the input is an archive. In that case, +// NewReader will return the known size using an io.LimitedReader. +func readAll(r io.Reader) ([]byte, error) { + if lr, ok := r.(*io.LimitedReader); ok { + data := make([]byte, lr.N) + _, err := io.ReadFull(lr, data) + return data, err + } + return io.ReadAll(r) +} + // Read reads export data from in, decodes it, and returns type // information for the package. // @@ -102,7 +114,7 @@ func NewReader(r io.Reader) (io.Reader, error) { // // On return, the state of the reader is undefined. func Read(in io.Reader, fset *token.FileSet, imports map[string]*types.Package, path string) (*types.Package, error) { - data, err := ioutil.ReadAll(in) + data, err := readAll(in) if err != nil { return nil, fmt.Errorf("reading export data for %q: %v", path, err) } @@ -165,7 +177,7 @@ func Write(out io.Writer, fset *token.FileSet, pkg *types.Package) error { // // Experimental: This API is experimental and may change in the future. func ReadBundle(in io.Reader, fset *token.FileSet, imports map[string]*types.Package) ([]*types.Package, error) { - data, err := ioutil.ReadAll(in) + data, err := readAll(in) if err != nil { return nil, fmt.Errorf("reading export bundle: %v", err) } From df35cd84ea1a0224a36f082f2fca82b6f70138f6 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 14 Dec 2022 18:40:14 -0500 Subject: [PATCH 545/723] x/tools: drop support for Go toolchains older than go1.16 This change removes all testenv.NeedsGo1Point(16) and earlier calls from tests, which assert that the go command used at run time is at least go1.16; this is now assumed. Also, we assume that a Go toolchain of at least go1.16 is used to build the packages in this repo. This is asserted by the introduction of a reference to io.Discard in go/packages. Change-Id: Ie12d721b44e057ad9936042afb255fa90b2ac5a8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/457657 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley --- go/packages/overlay_test.go | 6 ---- go/packages/packages.go | 5 +++ go/packages/packages_test.go | 4 --- go/packages/packagestest/modules_test.go | 2 -- .../infertypeargs/infertypeargs_test.go | 2 -- gopls/internal/lsp/lsprpc/goenv_test.go | 3 -- gopls/internal/lsp/lsprpc/lsprpc_test.go | 2 -- gopls/internal/lsp/mod/mod_test.go | 2 -- gopls/internal/lsp/tests/tests.go | 10 ------ .../regtest/codelens/codelens_test.go | 3 -- .../regtest/codelens/gcdetails_test.go | 6 ---- .../regtest/completion/completion_test.go | 6 ---- .../regtest/diagnostics/diagnostics_test.go | 32 ------------------- .../regtest/inlayhints/inlayhints_test.go | 2 -- .../internal/regtest/misc/definition_test.go | 4 --- gopls/internal/regtest/misc/embed_test.go | 2 -- gopls/internal/regtest/misc/hover_test.go | 7 ---- gopls/internal/regtest/misc/imports_test.go | 2 -- gopls/internal/regtest/misc/link_test.go | 4 --- .../internal/regtest/misc/references_test.go | 2 -- gopls/internal/regtest/misc/rename_test.go | 1 - gopls/internal/regtest/misc/vendor_test.go | 3 -- .../regtest/misc/workspace_symbol_test.go | 4 --- .../internal/regtest/modfile/modfile_test.go | 31 ------------------ gopls/internal/regtest/watch/watch_test.go | 6 ---- .../regtest/workspace/metadata_test.go | 3 -- .../regtest/workspace/standalone_test.go | 5 --- .../regtest/workspace/workspace_test.go | 12 ------- internal/gcimporter/gcimporter_test.go | 3 -- internal/imports/mod_test.go | 4 --- 30 files changed, 5 insertions(+), 173 deletions(-) diff --git a/go/packages/overlay_test.go b/go/packages/overlay_test.go index 93e306a4b94..4318739eb79 100644 --- a/go/packages/overlay_test.go +++ b/go/packages/overlay_test.go @@ -109,8 +109,6 @@ func TestOverlayChangesTestPackageName(t *testing.T) { testAllOrModulesParallel(t, testOverlayChangesTestPackageName) } func testOverlayChangesTestPackageName(t *testing.T, exporter packagestest.Exporter) { - testenv.NeedsGo1Point(t, 16) - exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "fake", Files: map[string]interface{}{ @@ -717,8 +715,6 @@ func TestInvalidFilesBeforeOverlay(t *testing.T) { } func testInvalidFilesBeforeOverlay(t *testing.T, exporter packagestest.Exporter) { - testenv.NeedsGo1Point(t, 15) - exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", @@ -756,8 +752,6 @@ func TestInvalidFilesBeforeOverlayContains(t *testing.T) { testAllOrModulesParallel(t, testInvalidFilesBeforeOverlayContains) } func testInvalidFilesBeforeOverlayContains(t *testing.T, exporter packagestest.Exporter) { - testenv.NeedsGo1Point(t, 15) - exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", diff --git a/go/packages/packages.go b/go/packages/packages.go index 9df20919ba2..1d5f0e45b2b 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -15,6 +15,7 @@ import ( "go/scanner" "go/token" "go/types" + "io" "io/ioutil" "log" "os" @@ -950,6 +951,8 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { // - golang.org/issue/52078 (flag to set release tags) // - golang.org/issue/50825 (gopls legacy version support) // - golang.org/issue/55883 (go/packages confusing error) + // + // Should we assert a hard minimum of (currently) go1.16 here? var runtimeVersion int if _, err := fmt.Sscanf(runtime.Version(), "go1.%d", &runtimeVersion); err == nil && runtimeVersion < lpkg.goVersion { defer func() { @@ -1311,3 +1314,5 @@ func impliedLoadMode(loadMode LoadMode) LoadMode { func usesExportData(cfg *Config) bool { return cfg.Mode&NeedExportFile != 0 || cfg.Mode&NeedTypes != 0 && cfg.Mode&NeedDeps == 0 } + +var _ interface{} = io.Discard // assert build toolchain is go1.16 or later diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 8180969d1ab..0da72851c76 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -2520,7 +2520,6 @@ import "C"`, // TestInvalidFilesInXTest checks the fix for golang/go#37971 in Go 1.15. func TestInvalidFilesInXTest(t *testing.T) { testAllOrModulesParallel(t, testInvalidFilesInXTest) } func testInvalidFilesInXTest(t *testing.T, exporter packagestest.Exporter) { - testenv.NeedsGo1Point(t, 15) exported := packagestest.Export(t, exporter, []packagestest.Module{ { Name: "golang.org/fake", @@ -2547,7 +2546,6 @@ func testInvalidFilesInXTest(t *testing.T, exporter packagestest.Exporter) { func TestTypecheckCgo(t *testing.T) { testAllOrModulesParallel(t, testTypecheckCgo) } func testTypecheckCgo(t *testing.T, exporter packagestest.Exporter) { - testenv.NeedsGo1Point(t, 15) testenv.NeedsTool(t, "cgo") const cgo = `package cgo @@ -2719,8 +2717,6 @@ func TestInvalidPackageName(t *testing.T) { } func testInvalidPackageName(t *testing.T, exporter packagestest.Exporter) { - testenv.NeedsGo1Point(t, 15) - exported := packagestest.Export(t, exporter, []packagestest.Module{{ Name: "golang.org/fake", Files: map[string]interface{}{ diff --git a/go/packages/packagestest/modules_test.go b/go/packages/packagestest/modules_test.go index 6f627b1e5bd..de290ead94a 100644 --- a/go/packages/packagestest/modules_test.go +++ b/go/packages/packagestest/modules_test.go @@ -9,11 +9,9 @@ import ( "testing" "golang.org/x/tools/go/packages/packagestest" - "golang.org/x/tools/internal/testenv" ) func TestModulesExport(t *testing.T) { - testenv.NeedsGo1Point(t, 11) exported := packagestest.Export(t, packagestest.Modules, testdata) defer exported.Cleanup() // Check that the cfg contains all the right bits diff --git a/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go index 2d687f0e72a..70855e1ab3e 100644 --- a/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go +++ b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go @@ -9,12 +9,10 @@ import ( "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs" - "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" ) func Test(t *testing.T) { - testenv.NeedsGo1Point(t, 13) if !typeparams.Enabled { t.Skip("type params are not enabled") } diff --git a/gopls/internal/lsp/lsprpc/goenv_test.go b/gopls/internal/lsp/lsprpc/goenv_test.go index b4a1b0ddaf5..5edd64fbe78 100644 --- a/gopls/internal/lsp/lsprpc/goenv_test.go +++ b/gopls/internal/lsp/lsprpc/goenv_test.go @@ -9,7 +9,6 @@ import ( "testing" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/testenv" . "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) @@ -26,8 +25,6 @@ func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamIniti } func TestGoEnvMiddleware(t *testing.T) { - testenv.NeedsGo1Point(t, 13) - ctx := context.Background() server := &initServer{} diff --git a/gopls/internal/lsp/lsprpc/lsprpc_test.go b/gopls/internal/lsp/lsprpc/lsprpc_test.go index 5fa1340948d..0dc78e67d8a 100644 --- a/gopls/internal/lsp/lsprpc/lsprpc_test.go +++ b/gopls/internal/lsp/lsprpc/lsprpc_test.go @@ -19,7 +19,6 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/testenv" ) type FakeClient struct { @@ -289,7 +288,6 @@ func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamIniti } func TestEnvForwarding(t *testing.T) { - testenv.NeedsGo1Point(t, 13) ctx := context.Background() server := &initServer{} diff --git a/gopls/internal/lsp/mod/mod_test.go b/gopls/internal/lsp/mod/mod_test.go index 55cbc11ee7c..eead8acfc76 100644 --- a/gopls/internal/lsp/mod/mod_test.go +++ b/gopls/internal/lsp/mod/mod_test.go @@ -23,8 +23,6 @@ func TestMain(m *testing.M) { } func TestModfileRemainsUnchanged(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - ctx := tests.Context(t) session := cache.NewSession(ctx, cache.New(nil, nil), nil) options := source.DefaultOptions().Clone() diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 3b677653e50..2c7f309b9d8 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -298,10 +298,6 @@ func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*tes } for _, mode := range modes { t.Run(mode, func(t *testing.T) { - if mode == "MultiModule" { - // Some bug in 1.12 breaks reading markers, and it's not worth figuring out. - testenv.NeedsGo1Point(t, 13) - } datum := load(t, mode, dataDir) t.Helper() f(t, datum) @@ -577,9 +573,6 @@ func Run(t *testing.T, tests Tests, data *Data) { if strings.Contains(t.Name(), "cgo") { testenv.NeedsTool(t, "cgo") } - if strings.Contains(t.Name(), "declarecgo") { - testenv.NeedsGo1Point(t, 15) - } test(t, src, e, data.CompletionItems) }) } @@ -764,9 +757,6 @@ func Run(t *testing.T, tests Tests, data *Data) { if strings.Contains(t.Name(), "cgo") { testenv.NeedsTool(t, "cgo") } - if strings.Contains(t.Name(), "declarecgo") { - testenv.NeedsGo1Point(t, 15) - } tests.Definition(t, spn, d) }) } diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index b79f5a3fe0a..aabddb91ddf 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -238,7 +238,6 @@ require golang.org/x/hello v1.2.3 } func TestUnusedDependenciesCodelens(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const proxy = ` -- golang.org/x/hello@v1.0.0/go.mod -- module golang.org/x/hello @@ -299,8 +298,6 @@ require golang.org/x/hello v1.0.0 func TestRegenerateCgo(t *testing.T) { testenv.NeedsTool(t, "cgo") - testenv.NeedsGo1Point(t, 15) - const workspace = ` -- go.mod -- module example.com diff --git a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go index 497b9730703..fdf3ae26889 100644 --- a/gopls/internal/regtest/codelens/gcdetails_test.go +++ b/gopls/internal/regtest/codelens/gcdetails_test.go @@ -9,8 +9,6 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -19,7 +17,6 @@ import ( ) func TestGCDetails_Toggle(t *testing.T) { - testenv.NeedsGo1Point(t, 15) if runtime.GOOS == "android" { t.Skipf("the gc details code lens doesn't work on Android") } @@ -89,9 +86,6 @@ func main() { // Test for the crasher in golang/go#54199 func TestGCDetails_NewFile(t *testing.T) { bug.PanicOnBugs = false - // It appears that older Go versions don't even see p.go from the initial - // workspace load. - testenv.NeedsGo1Point(t, 15) const src = ` -- go.mod -- module mod.test diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index eaf4327a686..caa70e8ae4a 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/testenv" ) func TestMain(m *testing.M) { @@ -43,7 +42,6 @@ const Name = "Hello" ` func TestPackageCompletion(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const files = ` -- go.mod -- module mod.com @@ -266,8 +264,6 @@ func compareCompletionResults(want []string, gotItems []protocol.CompletionItem) } func TestUnimportedCompletion(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const mod = ` -- go.mod -- module mod.com @@ -510,8 +506,6 @@ func doit() { } func TestUnimportedCompletion_VSCodeIssue1489(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const src = ` -- go.mod -- module mod.com diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 880b62f7654..83cd4331606 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -543,10 +543,6 @@ func _() { // Expect a module/GOPATH error if there is an error in the file at startup. // Tests golang/go#37279. func TestBrokenWorkspace_OutsideModule(t *testing.T) { - // Versions of the go command before go1.16 do not support - // native overlays and so do not observe the deletion. - testenv.NeedsGo1Point(t, 16) - const noModule = ` -- a.go -- package foo @@ -641,7 +637,6 @@ var ErrHelpWanted error // Test for golang/go#38211. func Test_Issue38211(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const ardanLabs = ` -- go.mod -- module mod.com @@ -706,8 +701,6 @@ func main() { // Test for golang/go#38207. func TestNewModule_Issue38207(t *testing.T) { - // Fails at Go 1.14 following CL 417576. Not investigated. - testenv.NeedsGo1Point(t, 15) const emptyFile = ` -- go.mod -- module mod.com @@ -743,7 +736,6 @@ func main() { // Test for golang/go#36960. func TestNewFileBadImports_Issue36960(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const simplePackage = ` -- go.mod -- module mod.com @@ -783,9 +775,6 @@ func _() { // This test tries to replicate the workflow of a user creating a new x test. // It also tests golang/go#39315. func TestManuallyCreatingXTest(t *testing.T) { - // Only for 1.15 because of golang/go#37971. - testenv.NeedsGo1Point(t, 15) - // Create a package that already has a test variant (in-package test). const testVariant = ` -- go.mod -- @@ -853,8 +842,6 @@ func TestHello(t *testing.T) { // Reproduce golang/go#40690. func TestCreateOnlyXTest(t *testing.T) { - testenv.NeedsGo1Point(t, 13) - const mod = ` -- go.mod -- module mod.com @@ -883,8 +870,6 @@ func TestX(t *testing.T) { } func TestChangePackageName(t *testing.T) { - testenv.NeedsGo1Point(t, 16) // needs native overlay support - const mod = ` -- go.mod -- module mod.com @@ -972,8 +957,6 @@ const C = a.A // This is a copy of the scenario_default/quickfix_empty_files.txt test from // govim. Reproduces golang/go#39646. func TestQuickFixEmptyFiles(t *testing.T) { - testenv.NeedsGo1Point(t, 15) - const mod = ` -- go.mod -- module mod.com @@ -1099,8 +1082,6 @@ func Foo() { } ` Run(t, basic, func(t *testing.T, env *Env) { - testenv.NeedsGo1Point(t, 16) // We can't recover cleanly from this case without good overlay support. - env.WriteWorkspaceFile("foo/foo_test.go", `package main func main() { @@ -1187,8 +1168,6 @@ func main() { // Reproduces golang/go#39763. func TestInvalidPackageName(t *testing.T) { - testenv.NeedsGo1Point(t, 15) - const pkgDefault = ` -- go.mod -- module mod.com @@ -1325,9 +1304,6 @@ func main() {} } func TestNotifyOrphanedFiles(t *testing.T) { - // Need GO111MODULE=on for this test to work with Go 1.12. - testenv.NeedsGo1Point(t, 13) - const files = ` -- go.mod -- module mod.com @@ -1452,8 +1428,6 @@ func main() { // have no more complaints about it. // https://github.com/golang/go/issues/41061 func TestRenamePackage(t *testing.T) { - testenv.NeedsGo1Point(t, 16) - const proxy = ` -- example.com@v1.2.3/go.mod -- module example.com @@ -1517,8 +1491,6 @@ package foo_ // TestProgressBarErrors confirms that critical workspace load errors are shown // and updated via progress reports. func TestProgressBarErrors(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const pkg = ` -- go.mod -- modul mod.com @@ -1551,8 +1523,6 @@ go 1.hello } func TestDeleteDirectory(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const mod = ` -- bob/bob.go -- package bob @@ -1673,8 +1643,6 @@ const B = a.B } func TestBadImport(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const mod = ` -- go.mod -- module mod.com diff --git a/gopls/internal/regtest/inlayhints/inlayhints_test.go b/gopls/internal/regtest/inlayhints/inlayhints_test.go index 4c8e75707e7..d4caabe79d3 100644 --- a/gopls/internal/regtest/inlayhints/inlayhints_test.go +++ b/gopls/internal/regtest/inlayhints/inlayhints_test.go @@ -10,7 +10,6 @@ import ( . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/testenv" ) func TestMain(m *testing.M) { @@ -19,7 +18,6 @@ func TestMain(m *testing.M) { } func TestEnablingInlayHints(t *testing.T) { - testenv.NeedsGo1Point(t, 14) // Test fails on 1.13. const workspace = ` -- go.mod -- module inlayHint.test diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index e9e56da58bd..7d6eeae5098 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/internal/testenv" "golang.org/x/tools/gopls/internal/lsp/fake" ) @@ -238,8 +237,6 @@ func main() {} // Test for golang/go#47825. func TestImportTestVariant(t *testing.T) { - testenv.NeedsGo1Point(t, 13) - const mod = ` -- go.mod -- module mod.com @@ -294,7 +291,6 @@ func TestGoToCrashingDefinition_Issue49223(t *testing.T) { // causes packages to move; see issue #55995. // See also TestImplementationsInVendor, which tests the same fix. func TestVendoringInvalidatesMetadata(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const proxy = ` -- other.com/b@v1.0.0/go.mod -- module other.com/b diff --git a/gopls/internal/regtest/misc/embed_test.go b/gopls/internal/regtest/misc/embed_test.go index 3730e5ab004..0fadda03e6e 100644 --- a/gopls/internal/regtest/misc/embed_test.go +++ b/gopls/internal/regtest/misc/embed_test.go @@ -7,11 +7,9 @@ import ( "testing" . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/testenv" ) func TestMissingPatternDiagnostic(t *testing.T) { - testenv.NeedsGo1Point(t, 16) const files = ` -- go.mod -- module example.com diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go index 324f88b3fca..d3db2572ec8 100644 --- a/gopls/internal/regtest/misc/hover_test.go +++ b/gopls/internal/regtest/misc/hover_test.go @@ -11,7 +11,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/fake" . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/testenv" ) func TestHoverUnexported(t *testing.T) { @@ -82,7 +81,6 @@ func main() { } func TestHoverIntLiteral(t *testing.T) { - testenv.NeedsGo1Point(t, 13) const source = ` -- main.go -- package main @@ -114,7 +112,6 @@ func main() { // Tests that hovering does not trigger the panic in golang/go#48249. func TestPanicInHoverBrokenCode(t *testing.T) { - testenv.NeedsGo1Point(t, 13) const source = ` -- main.go -- package main @@ -143,9 +140,6 @@ package main } func TestHoverImport(t *testing.T) { - // For Go.13 and earlier versions, Go will try to download imported but missing packages. This behavior breaks the - // workspace as Go fails to download non-existent package "mod.com/lib4" - testenv.NeedsGo1Point(t, 14) const packageDoc1 = "Package lib1 hover documentation" const packageDoc2 = "Package lib2 hover documentation" tests := []struct { @@ -224,7 +218,6 @@ func main() { // for x/tools/gopls: unhandled named anchor on the hover #57048 func TestHoverTags(t *testing.T) { - testenv.NeedsGo1Point(t, 14) // until go1.13 is dropped from kokoro const source = ` -- go.mod -- module mod.com diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index 8a781bd424a..60da30f8b3b 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -146,8 +146,6 @@ import "example.com/x" var _, _ = x.X, y.Y ` - testenv.NeedsGo1Point(t, 15) - modcache, err := ioutil.TempDir("", "TestGOMODCACHE-modcache") if err != nil { t.Fatal(err) diff --git a/gopls/internal/regtest/misc/link_test.go b/gopls/internal/regtest/misc/link_test.go index e1bf58954f6..b782fff4693 100644 --- a/gopls/internal/regtest/misc/link_test.go +++ b/gopls/internal/regtest/misc/link_test.go @@ -9,13 +9,9 @@ import ( "testing" . "golang.org/x/tools/gopls/internal/lsp/regtest" - - "golang.org/x/tools/internal/testenv" ) func TestHoverAndDocumentLink(t *testing.T) { - testenv.NeedsGo1Point(t, 13) - const program = ` -- go.mod -- module mod.test diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index c4f4c75fe51..7cff19c8d32 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -14,7 +14,6 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/testenv" ) func TestStdlibReferences(t *testing.T) { @@ -295,7 +294,6 @@ func _() { // implementations in vendored modules were not found. The actual fix // was the same as for #55995; see TestVendoringInvalidatesMetadata. func TestImplementationsInVendor(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const proxy = ` -- other.com/b@v1.0.0/go.mod -- module other.com/b diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index a772770562e..7a7c4ec1ce5 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -84,7 +84,6 @@ func _() { } func TestPrepareRenameWithNoPackageDeclaration(t *testing.T) { - testenv.NeedsGo1Point(t, 15) const files = ` go 1.14 -- lib/a.go -- diff --git a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go index cec33cad173..3210461a326 100644 --- a/gopls/internal/regtest/misc/vendor_test.go +++ b/gopls/internal/regtest/misc/vendor_test.go @@ -10,7 +10,6 @@ import ( . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/testenv" ) const basicProxy = ` @@ -25,8 +24,6 @@ var Goodbye error ` func TestInconsistentVendoring(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const pkgThatUsesVendoring = ` -- go.mod -- module mod.com diff --git a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go index d1fc8646cef..eb150f6094e 100644 --- a/gopls/internal/regtest/misc/workspace_symbol_test.go +++ b/gopls/internal/regtest/misc/workspace_symbol_test.go @@ -10,13 +10,9 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/internal/testenv" ) func TestWorkspaceSymbolMissingMetadata(t *testing.T) { - // We get 2 symbols on 1.12, for some reason. - testenv.NeedsGo1Point(t, 13) - const files = ` -- go.mod -- module mod.com diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 4369aefd3f2..aeb2f043cde 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -67,8 +67,6 @@ const Name = "Hello" ` func TestModFileModification(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const untidyModule = ` -- a/go.mod -- module mod.com @@ -161,7 +159,6 @@ func main() { } func TestGoGetFix(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const mod = ` -- a/go.mod -- module mod.com @@ -213,7 +210,6 @@ require example.com v1.2.3 // Tests that multiple missing dependencies gives good single fixes. func TestMissingDependencyFixes(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const mod = ` -- a/go.mod -- module mod.com @@ -318,8 +314,6 @@ require random.org v1.2.3 } func TestIndirectDependencyFix(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const mod = ` -- a/go.mod -- module mod.com @@ -365,7 +359,6 @@ require example.com v1.2.3 } func TestUnusedDiag(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const proxy = ` -- example.com@v1.0.0/x.go -- @@ -412,7 +405,6 @@ go 1.14 // Test to reproduce golang/go#39041. It adds a new require to a go.mod file // that already has an unused require. func TestNewDepWithUnusedDep(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const proxy = ` -- github.com/esimov/caire@v1.2.5/go.mod -- @@ -485,8 +477,6 @@ require ( // the file watching GlobPattern in the capability registration. See // golang/go#39384. func TestModuleChangesOnDisk(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const mod = ` -- a/go.mod -- module mod.com @@ -518,8 +508,6 @@ func main() { // Tests golang/go#39784: a missing indirect dependency, necessary // due to blah@v2.0.0's incomplete go.mod file. func TestBadlyVersionedModule(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const proxy = ` -- example.com/blah/@v/v1.0.0.mod -- module example.com @@ -595,11 +583,6 @@ func TestUnknownRevision(t *testing.T) { if runtime.GOOS == "plan9" { t.Skipf("skipping test that fails for unknown reasons on plan9; see https://go.dev/issue/50477") } - - // This test fails at go1.14 and go1.15 due to differing Go command behavior. - // This was not significantly investigated. - testenv.NeedsGo1Point(t, 16) - const unknown = ` -- a/go.mod -- module mod.com @@ -694,8 +677,6 @@ func main() { // Confirm that an error in an indirect dependency of a requirement is surfaced // as a diagnostic in the go.mod file. func TestErrorInIndirectDependency(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const badProxy = ` -- example.com@v1.2.3/go.mod -- module example.com @@ -782,8 +763,6 @@ func main() { } func TestMultiModuleModDiagnostics(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const mod = ` -- a/go.mod -- module moda.com @@ -826,8 +805,6 @@ func main() { } func TestModTidyWithBuildTags(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const mod = ` -- go.mod -- module mod.com @@ -875,8 +852,6 @@ func main() {} } func TestSumUpdateFixesDiagnostics(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const mod = ` -- go.mod -- module mod.com @@ -961,8 +936,6 @@ func hello() {} } func TestRemoveUnusedDependency(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const proxy = ` -- hasdep.com@v1.2.3/go.mod -- module hasdep.com @@ -1084,7 +1057,6 @@ require random.com v1.2.3 } func TestSumUpdateQuickFix(t *testing.T) { - testenv.NeedsGo1Point(t, 14) const mod = ` -- go.mod -- module mod.com @@ -1129,8 +1101,6 @@ example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= } func TestDownloadDeps(t *testing.T) { - testenv.NeedsGo1Point(t, 14) - const proxy = ` -- example.com@v1.2.3/go.mod -- module example.com @@ -1194,7 +1164,6 @@ func main() { } func TestInvalidGoVersion(t *testing.T) { - testenv.NeedsGo1Point(t, 14) // Times out on 1.13 for reasons unclear. Not worth worrying about. const files = ` -- go.mod -- module mod.com diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 32fe418228d..7c9cc9bf01e 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -13,7 +13,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/testenv" ) func TestMain(m *testing.M) { @@ -371,7 +370,6 @@ func _() { // Tests golang/go#38498. Delete a file and then force a reload. // Assert that we no longer try to load the file. func TestDeleteFiles(t *testing.T) { - testenv.NeedsGo1Point(t, 13) // Poor overlay support causes problems on 1.12. const pkg = ` -- go.mod -- module mod.com @@ -572,8 +570,6 @@ func main() { // Reproduces golang/go#40340. func TestSwitchFromGOPATHToModules(t *testing.T) { - testenv.NeedsGo1Point(t, 13) - const files = ` -- foo/blah/blah.go -- package blah @@ -607,8 +603,6 @@ func main() { // Reproduces golang/go#40487. func TestSwitchFromModulesToGOPATH(t *testing.T) { - testenv.NeedsGo1Point(t, 13) - const files = ` -- foo/go.mod -- module mod.com diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index c5598c93f76..55bc01e72d8 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -15,9 +15,6 @@ import ( // file. func TestFixImportDecl(t *testing.T) { - // It appears that older Go versions don't even see p.go from the initial - // workspace load. - testenv.NeedsGo1Point(t, 15) const src = ` -- go.mod -- module mod.test diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go index 86e3441e52d..82334583e71 100644 --- a/gopls/internal/regtest/workspace/standalone_test.go +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -11,12 +11,9 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/testenv" ) func TestStandaloneFiles(t *testing.T) { - testenv.NeedsGo1Point(t, 16) // Standalone files are only supported at Go 1.16 and later. - const files = ` -- go.mod -- module mod.test @@ -185,8 +182,6 @@ func main() { } func TestStandaloneFiles_Configuration(t *testing.T) { - testenv.NeedsGo1Point(t, 16) // Standalone files are only supported at Go 1.16 and later. - const files = ` -- go.mod -- module mod.test diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 5786f0a031b..38cd3a9294e 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -276,8 +276,6 @@ func Hello() int { } func TestMultiModuleWithExclude(t *testing.T) { - testenv.NeedsGo1Point(t, 16) - const proxy = ` -- c.com@v1.2.3/go.mod -- module c.com @@ -504,7 +502,6 @@ func Hello() int { func TestUseGoplsMod(t *testing.T) { // This test validates certain functionality related to using a gopls.mod // file to specify workspace modules. - testenv.NeedsGo1Point(t, 14) const multiModule = ` -- moda/a/go.mod -- module a.com @@ -647,7 +644,6 @@ module example.com/bar func TestUseGoWork(t *testing.T) { // This test validates certain functionality related to using a go.work // file to specify workspace modules. - testenv.NeedsGo1Point(t, 14) const multiModule = ` -- moda/a/go.mod -- module a.com @@ -924,8 +920,6 @@ use ( } func TestNonWorkspaceFileCreation(t *testing.T) { - testenv.NeedsGo1Point(t, 13) - const files = ` -- go.mod -- module mod.com @@ -1006,8 +1000,6 @@ func main() { // Confirm that a fix for a tidy module will correct all modules in the // workspace. func TestMultiModule_OneBrokenModule(t *testing.T) { - testenv.NeedsGo1Point(t, 15) - const mod = ` -- a/go.mod -- module a.com @@ -1138,10 +1130,6 @@ use ( // Tests the fix for golang/go#52500. func TestChangeTestVariant_Issue52500(t *testing.T) { - // This test fails for unknown reasons at Go <= 15. Presumably the loading of - // test variants behaves differently, possibly due to lack of support for - // native overlays. - testenv.NeedsGo1Point(t, 16) const src = ` -- go.mod -- module mod.test diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index bdae455d896..0b9f07dd1b8 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -443,7 +443,6 @@ func init() { } func TestImportedTypes(t *testing.T) { - testenv.NeedsGo1Point(t, 11) // This package only handles gc export data. needsCompiler(t, "gc") testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache @@ -471,7 +470,6 @@ func TestImportedTypes(t *testing.T) { } func TestImportedConsts(t *testing.T) { - testenv.NeedsGo1Point(t, 11) testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache tests := []struct { @@ -752,7 +750,6 @@ func TestIssue20046(t *testing.T) { } func TestIssue25301(t *testing.T) { - testenv.NeedsGo1Point(t, 11) // This package only handles gc export data. needsCompiler(t, "gc") diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go index 8063dbe0f74..cb7fd449938 100644 --- a/internal/imports/mod_test.go +++ b/internal/imports/mod_test.go @@ -884,7 +884,6 @@ package z // Tests that we handle GO111MODULE=on with no go.mod file. See #30855. func TestNoMainModule(t *testing.T) { - testenv.NeedsGo1Point(t, 12) mt := setup(t, map[string]string{"GO111MODULE": "on"}, ` -- x.go -- package x @@ -997,7 +996,6 @@ type modTest struct { // extraEnv is applied on top of the default test env. func setup(t *testing.T, extraEnv map[string]string, main, wd string) *modTest { t.Helper() - testenv.NeedsGo1Point(t, 11) testenv.NeedsTool(t, "go") proxyOnce.Do(func() { @@ -1194,7 +1192,6 @@ import _ "rsc.io/quote" // Tests that crud in the module cache is ignored. func TestInvalidModCache(t *testing.T) { - testenv.NeedsGo1Point(t, 11) dir, err := ioutil.TempDir("", t.Name()) if err != nil { t.Fatal(err) @@ -1291,7 +1288,6 @@ import ( } func BenchmarkScanModCache(b *testing.B) { - testenv.NeedsGo1Point(b, 11) env := &ProcessEnv{ GocmdRunner: &gocommand.Runner{}, Logf: log.Printf, From 9bc5dce4d63f1724f632ed0e51db860af56f2fd7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 9 Dec 2022 18:40:01 -0500 Subject: [PATCH 546/723] gopls/internal/lsp/cache: simplify DiagnosePackage Previously Snapshot.DiagnosePackage would run after type checking, would conditionally invoke the analysis framework just for type error analyzers, and would then munge those diagnostics together with those from type checking, eliminate duplicates and migrating the suggested fixes onto the type-checker errors. However, all its callers immediately then invoke the analysis framework for the full suite for analyzers. This change separates the three steps more clearly: 1) type checking, which produces list/parse/type errors, now available at Package.DiagnosticsForFile(URI). 2) analysis using all analyzers, including the type-error analyzers, which are no longer special. (Consequently the analyzers' findings are reported in more tests.) 3) munging of the results together, in source.CombineDiagnostics. The munging algorithm is no longer quadratic in the number of combined elements. This change removes one of the blockers to the new analysis driver implementation in CL 443099. The tests were updated to assert that type-error analyzers run whenever we see type checker diagnostics. This was painful to do, even by the standards to which we have become accustomed. Change-Id: I2393ad5bc13587ff4bfed86ae586ce658074a501 Reviewed-on: https://go-review.googlesource.com/c/tools/+/456643 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/analysis.go | 43 ----------- gopls/internal/lsp/cache/check.go | 5 ++ gopls/internal/lsp/cache/pkg.go | 10 +++ gopls/internal/lsp/cmd/test/check.go | 4 +- gopls/internal/lsp/code_action.go | 18 ++--- gopls/internal/lsp/diagnostics.go | 36 ++++++--- gopls/internal/lsp/mod/diagnostics.go | 35 ++++----- gopls/internal/lsp/source/diagnostics.go | 73 +++++++++++++++---- gopls/internal/lsp/source/view.go | 38 +--------- .../lsp/testdata/badstmt/badstmt.go.in | 7 +- .../lsp/testdata/noparse/noparse.go.in | 17 ++++- .../noparse_format/noparse_format.go.in | 9 ++- .../internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- 14 files changed, 152 insertions(+), 147 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 0b1fa6a9b0f..31d8f033bea 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -16,7 +16,6 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/analysis" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" @@ -465,45 +464,3 @@ func factType(fact analysis.Fact) reflect.Type { } return t } - -func (s *snapshot) DiagnosePackage(ctx context.Context, id PackageID) (map[span.URI][]*source.Diagnostic, error) { - pkgs, err := s.TypeCheck(ctx, source.TypecheckFull, id) - if err != nil { - return nil, err - } - pkg := pkgs[0].(*pkg) - var errorAnalyzerDiag []*source.Diagnostic - if pkg.HasTypeErrors() { - // Apply type error analyzers. - // They augment type error diagnostics with their own fixes. - var analyzers []*source.Analyzer - for _, a := range s.View().Options().TypeErrorAnalyzers { - analyzers = append(analyzers, a) - } - var err error - errorAnalyzerDiag, err = s.Analyze(ctx, pkg.ID(), analyzers) - if err != nil { - // Keep going: analysis failures should not block diagnostics. - event.Error(ctx, "type error analysis failed", err, tag.Package.Of(string(pkg.ID()))) - } - } - diags := map[span.URI][]*source.Diagnostic{} - for _, diag := range pkg.diagnostics { - for _, eaDiag := range errorAnalyzerDiag { - if eaDiag.URI == diag.URI && eaDiag.Range == diag.Range && eaDiag.Message == diag.Message { - // Type error analyzers just add fixes and tags. Make a copy, - // since we don't own either, and overwrite. - // The analyzer itself can't do this merge because - // analysis.Diagnostic doesn't have all the fields, and Analyze - // can't because it doesn't have the type error, notably its code. - clone := *diag - clone.SuggestedFixes = eaDiag.SuggestedFixes - clone.Tags = eaDiag.Tags - clone.Analyzer = eaDiag.Analyzer - diag = &clone - } - } - diags[diag.URI] = append(diags[diag.URI], diag) - } - return diags, nil -} diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 8adec22a17d..beec6593ba6 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -605,6 +605,11 @@ func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHand // Optionally remove parts that don't affect the exported API. if mode == source.ParseExported { + // TODO(adonovan): opt: experiment with pre-parser + // trimming, either a scanner-based implementation + // such as https://go.dev/play/p/KUrObH1YkX8 (~31% + // speedup), or a byte-oriented implementation (2x + // speedup). if astFilter != nil { // aggressive pruning based on reachability var files []*ast.File diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 5b927a5f6d2..6d138bea15c 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -161,3 +161,13 @@ func (p *pkg) HasListOrParseErrors() bool { func (p *pkg) HasTypeErrors() bool { return len(p.typeErrors) != 0 } + +func (p *pkg) DiagnosticsForFile(uri span.URI) []*source.Diagnostic { + var res []*source.Diagnostic + for _, diag := range p.diagnostics { + if diag.URI == uri { + res = append(res, diag) + } + } + return res +} diff --git a/gopls/internal/lsp/cmd/test/check.go b/gopls/internal/lsp/cmd/test/check.go index ea9747cae79..35153c2700d 100644 --- a/gopls/internal/lsp/cmd/test/check.go +++ b/gopls/internal/lsp/cmd/test/check.go @@ -15,8 +15,8 @@ import ( "golang.org/x/tools/gopls/internal/span" ) -// Diagnostics runs the gopls command on a single file, parses its -// diagnostics, and compares against the expectations defined by +// Diagnostics runs the "gopls check" command on a single file, parses +// its diagnostics, and compares against the expectations defined by // markers in the source file. func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) { out, _ := r.runGoplsCmd(t, "check", uri.Filename()) diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 63c0d67c6a5..3b333ffa641 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -155,23 +155,19 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara if ctx.Err() != nil { return nil, ctx.Err() } - metas, err := snapshot.MetadataForFile(ctx, fh.URI()) - if err != nil { - return nil, err - } - if len(metas) == 0 { - return nil, fmt.Errorf("no package containing file %q", fh.URI()) - } - id := metas[len(metas)-1].ID // last => widest package - pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, id) + + // Type-check the package and also run analysis, + // then combine their diagnostics. + pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckFull, source.WidestPackage) if err != nil { return nil, err } - analysisDiags, err := source.Analyze(ctx, snapshot, id, true) + analysisDiags, err := source.Analyze(ctx, snapshot, pkg.ID(), true) if err != nil { return nil, err } - fileDiags := append(pkgDiagnostics[uri], analysisDiags[uri]...) + var fileDiags []*source.Diagnostic + source.CombineDiagnostics(pkg, fh.URI(), analysisDiags, &fileDiags, &fileDiags) // Split diagnostics into fixes, which must match incoming diagnostics, // and non-fixes, which must match the requested range. Build actions diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index addfa96aca8..445a81914b3 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -178,6 +178,9 @@ func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snaps ctx, done := event.Start(ctx, "Server.diagnoseChangedFiles", source.SnapshotLabels(snapshot)...) defer done() + // TODO(adonovan): safety: refactor so that group.Go is called + // in a second loop, so that if we should later add an early + // return to the first loop, we don't leak goroutines. var group errgroup.Group seen := make(map[*source.Metadata]bool) for _, uri := range uris { @@ -366,27 +369,36 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, m *s return } - pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, m.ID) + pkgs, err := snapshot.TypeCheck(ctx, source.TypecheckFull, m.ID) if err != nil { - event.Error(ctx, "warning: diagnosing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...) + event.Error(ctx, "warning: typecheck failed", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...) return } - for _, uri := range m.CompiledGoFiles { - // builtin.go exists only for documentation purposes, and is not valid Go code. - // Don't report distracting errors - if !snapshot.IsBuiltin(ctx, uri) { - s.storeDiagnostics(snapshot, uri, typeCheckSource, pkgDiagnostics[uri], true) - } - } + pkg := pkgs[0] + + // Get diagnostics from analysis framework. + // This includes type-error analyzers, which suggest fixes to compiler errors. + var analysisDiags map[span.URI][]*source.Diagnostic if includeAnalysis { - reports, err := source.Analyze(ctx, snapshot, m.ID, false) + diags, err := source.Analyze(ctx, snapshot, m.ID, false) if err != nil { event.Error(ctx, "warning: analyzing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...) return } - for _, uri := range m.CompiledGoFiles { - s.storeDiagnostics(snapshot, uri, analysisSource, reports[uri], true) + analysisDiags = diags + } + + // For each file, update the server's diagnostics state. + for _, cgf := range pkg.CompiledGoFiles() { + // builtin.go exists only for documentation purposes and + // is not valid Go code. Don't report distracting errors. + if snapshot.IsBuiltin(ctx, cgf.URI) { + continue } + var tdiags, adiags []*source.Diagnostic + source.CombineDiagnostics(pkg, cgf.URI, analysisDiags, &tdiags, &adiags) + s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, tdiags, true) + s.storeDiagnostics(snapshot, cgf.URI, analysisSource, adiags, true) } // If gc optimization details are requested, add them to the diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index a0e42720a8f..65b3786f10b 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -11,11 +11,9 @@ import ( "fmt" "sort" "strings" - "sync" "golang.org/x/mod/modfile" "golang.org/x/mod/semver" - "golang.org/x/sync/errgroup" "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -89,7 +87,7 @@ func ModDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.Fil } // Packages in the workspace can contribute diagnostics to go.mod files. - // TODO(rfindley): Try to avoid calling DiagnosePackage on all packages in the workspace here, + // TODO(rfindley): Try to avoid type checking all packages in the workspace here, // for every go.mod file. If gc_details is enabled, it looks like this could lead to extra // go command invocations (as gc details is not memoized). active, err := snapshot.ActiveMetadata(ctx) @@ -97,27 +95,20 @@ func ModDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.Fil event.Error(ctx, fmt.Sprintf("workspace packages: diagnosing %s", pm.URI), err) } if err == nil { - // Diagnose packages in parallel. - // (It's possible this is the first operation after the initial - // metadata load to demand type-checking of all the active packages.) - var group errgroup.Group - var mu sync.Mutex - for _, m := range active { - m := m - group.Go(func() error { - pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, m.ID) - if err != nil { - return err - } - mu.Lock() - diagnostics = append(diagnostics, pkgDiagnostics[fh.URI()]...) - mu.Unlock() - return nil - }) - } - if err := group.Wait(); err != nil { + // Type-check packages in parallel and gather list/parse/type errors. + // (This may be the first operation after the initial metadata load + // to demand type-checking of all active packages.) + ids := make([]source.PackageID, len(active)) + for i, meta := range active { + ids[i] = meta.ID + } + pkgs, err := snapshot.TypeCheck(ctx, source.TypecheckFull, ids...) + if err != nil { return nil, err } + for _, pkg := range pkgs { + diagnostics = append(diagnostics, pkg.DiagnosticsForFile(fh.URI())...) + } } tidied, err := snapshot.ModTidy(ctx, pm) diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index 60ef7e69604..8101a043256 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -6,7 +6,6 @@ package source import ( "context" - "fmt" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/span" @@ -37,6 +36,7 @@ func Analyze(ctx context.Context, snapshot Snapshot, pkgid PackageID, includeCon categories := []map[string]*Analyzer{ options.DefaultAnalyzers, options.StaticcheckAnalyzers, + options.TypeErrorAnalyzers, } if includeConvenience { // e.g. for codeAction categories = append(categories, options.ConvenienceAnalyzers) // e.g. fillstruct @@ -55,14 +55,15 @@ func Analyze(ctx context.Context, snapshot Snapshot, pkgid PackageID, includeCon } // Report diagnostics and errors from root analyzers. - reports := map[span.URI][]*Diagnostic{} + reports := make(map[span.URI][]*Diagnostic) for _, diag := range analysisDiagnostics { reports[diag.URI] = append(reports[diag.URI], diag) } return reports, nil } -// FileDiagnostics reports diagnostics in the specified file. +// FileDiagnostics reports diagnostics in the specified file, +// as used by the "gopls check" command. // // TODO(adonovan): factor in common with (*Server).codeAction, which // executes { PackageForFile; DiagnosePackage; Analyze } too? @@ -76,22 +77,66 @@ func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (Vers if err != nil { return VersionedFileIdentity{}, nil, err } - metas, err := snapshot.MetadataForFile(ctx, uri) + pkg, err := snapshot.PackageForFile(ctx, uri, TypecheckFull, NarrowestPackage) if err != nil { return VersionedFileIdentity{}, nil, err } - if len(metas) == 0 { - return VersionedFileIdentity{}, nil, fmt.Errorf("no package containing file %q", uri) - } - id := metas[0].ID // 0 => narrowest package - diagnostics, err := snapshot.DiagnosePackage(ctx, id) + adiags, err := Analyze(ctx, snapshot, pkg.ID(), false) if err != nil { return VersionedFileIdentity{}, nil, err } - analysisDiags, err := Analyze(ctx, snapshot, id, false) - if err != nil { - return VersionedFileIdentity{}, nil, err - } - fileDiags := append(diagnostics[fh.URI()], analysisDiags[fh.URI()]...) + var fileDiags []*Diagnostic // combine load/parse/type + analysis diagnostics + CombineDiagnostics(pkg, fh.URI(), adiags, &fileDiags, &fileDiags) return fh.VersionedFileIdentity(), fileDiags, nil } + +// CombineDiagnostics combines and filters list/parse/type diagnostics +// from pkg.DiagnosticsForFile(uri) with analysisDiagnostics[uri], and +// appends the two lists to *outT and *outA, respectively. +// +// Type-error analyzers produce diagnostics that are redundant +// with type checker diagnostics, but more detailed (e.g. fixes). +// Rather than report two diagnostics for the same problem, +// we combine them by augmenting the type-checker diagnostic +// and discarding the analyzer diagnostic. +// +// If an analysis diagnostic has the same range and message as +// a list/parse/type diagnostic, the suggested fix information +// (et al) of the latter is merged into a copy of the former. +// This handles the case where a type-error analyzer suggests +// a fix to a type error, and avoids duplication. +// +// The use of out-slices, though irregular, allows the caller to +// easily choose whether to keep the results separate or combined. +// +// The arguments are not modified. +func CombineDiagnostics(pkg Package, uri span.URI, analysisDiagnostics map[span.URI][]*Diagnostic, outT, outA *[]*Diagnostic) { + + // Build index of (list+parse+)type errors. + type key struct { + Range protocol.Range + message string + } + index := make(map[key]int) // maps (Range,Message) to index in tdiags slice + tdiags := pkg.DiagnosticsForFile(uri) + for i, diag := range tdiags { + index[key{diag.Range, diag.Message}] = i + } + + // Filter out analysis diagnostics that match type errors, + // retaining their suggested fix (etc) fields. + for _, diag := range analysisDiagnostics[uri] { + if i, ok := index[key{diag.Range, diag.Message}]; ok { + copy := *tdiags[i] + copy.SuggestedFixes = diag.SuggestedFixes + copy.Tags = diag.Tags + copy.Analyzer = diag.Analyzer + tdiags[i] = © + continue + } + + *outA = append(*outA, diag) + } + + *outT = append(*outT, tdiags...) +} diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 39a9a3ab2dc..e484e3a752d 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -107,41 +107,6 @@ type Snapshot interface { // Position information is added to FileSet(). ParseGo(ctx context.Context, fh FileHandle, mode ParseMode) (*ParsedGoFile, error) - // DiagnosePackage returns basic diagnostics, including list, - // parse, and type errors for pkg, grouped by file. - // - // It may include suggested fixes for type errors, created by - // running the analysis framework. - // - // TODO(adonovan): this operation does a mix of checks that - // range from cheap (list errors, parse errors) to expensive - // (type errors, type-error-analyzer results). In particular, - // type-error analyzers (as currently implemented) depend on - // the full analyzer machinery, and in the near future that - // will be completely separate from regular type checking. - // So, we must choose between: - // - // (1) rewriting them as ad-hoc functions that operate on - // type-checked packages. That's a fair amount of work - // since they are fairly deeply enmeshed in the framework, - // (some have horizontal dependencies such as Inspector), - // and quite reasonably so. So we reject this in favor of: - // - // (2) separating the generation of type errors (which happens - // at the lowest latency) from full analysis, which is - // slower, although hopefully eventually only on the order - // of seconds. In this case, type error analyzers are - // basically not special; the only special part would be - // the postprocessing step to merge the - // type-error-analyzer fixes into the ordinary diagnostics - // produced by type checking. (Not yet sure how to do that.) - // - // So then the role of this function is "report fast - // diagnostics, up to type-checking", and the role of - // Analyze() is "run the analysis framework, included - // suggested fixes for type errors" - DiagnosePackage(ctx context.Context, id PackageID) (map[span.URI][]*Diagnostic, error) - // Analyze runs the specified analyzers on the given package at this snapshot. Analyze(ctx context.Context, id PackageID, analyzers []*Analyzer) ([]*Diagnostic, error) @@ -711,6 +676,8 @@ type Analyzer struct { Severity protocol.DiagnosticSeverity } +func (a *Analyzer) String() string { return a.Analyzer.String() } + // Enabled reports whether this analyzer is enabled by the given options. func (a Analyzer) IsEnabled(options *Options) bool { // Staticcheck analyzers can only be enabled when staticcheck is on. @@ -762,6 +729,7 @@ type Package interface { ResolveImportPath(path ImportPath) (Package, error) Imports() []Package // new slice of all direct dependencies, unordered HasTypeErrors() bool + DiagnosticsForFile(uri span.URI) []*Diagnostic // new array of list/parse/type errors } // A CriticalError is a workspace-wide error that generally prevents gopls from diff --git a/gopls/internal/lsp/testdata/badstmt/badstmt.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt.go.in index 8f0654a8a52..81aee201d7f 100644 --- a/gopls/internal/lsp/testdata/badstmt/badstmt.go.in +++ b/gopls/internal/lsp/testdata/badstmt/badstmt.go.in @@ -4,10 +4,13 @@ import ( "golang.org/lsptests/foo" ) -func _() { +// The nonewvars expectation asserts that the go/analysis framework ran. +// See comments in noparse. + +func _(x int) { defer foo.F //@complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement|expression in defer must be function call", "error") - y := 1 defer foo.F //@complete(" //", Foo) + x := 123 //@diag(":=", "nonewvars", "no new variables", "warning") } func _() { diff --git a/gopls/internal/lsp/testdata/noparse/noparse.go.in b/gopls/internal/lsp/testdata/noparse/noparse.go.in index 7dc23e02562..8b0bfaa035c 100644 --- a/gopls/internal/lsp/testdata/noparse/noparse.go.in +++ b/gopls/internal/lsp/testdata/noparse/noparse.go.in @@ -1,11 +1,24 @@ package noparse +// The type error was chosen carefully to exercise a type-error analyzer. +// We use the 'nonewvars' analyzer because the other candidates are tricky: +// +// - The 'unusedvariable' analyzer is disabled by default, so it is not +// consistently enabled across Test{LSP,CommandLine} tests, which +// both process this file. +// - The 'undeclaredname' analyzer depends on the text of the go/types +// "undeclared name" error, which changed in go1.20. +// - The 'noresultvalues' analyzer produces a diagnostic containing newlines, +// which breaks the parser used by TestCommandLine. +// +// This comment is all that remains of my afternoon. + func bye(x int) { - hi() + x := 123 //@diag(":=", "nonewvars", "no new variables", "warning") } func stuff() { - x := 5 + } func .() {} //@diag(".", "syntax", "expected 'IDENT', found '.'", "error") diff --git a/gopls/internal/lsp/testdata/noparse_format/noparse_format.go.in b/gopls/internal/lsp/testdata/noparse_format/noparse_format.go.in index 4fc3824d9b8..311a99aafb3 100644 --- a/gopls/internal/lsp/testdata/noparse_format/noparse_format.go.in +++ b/gopls/internal/lsp/testdata/noparse_format/noparse_format.go.in @@ -2,8 +2,13 @@ package noparse_format //@format("package") +// The nonewvars expectation asserts that the go/analysis framework ran. +// See comments in badstmt. + func what() { - var b int + var hi func() if { hi() //@diag("{", "syntax", "missing condition in if statement", "error") } -} \ No newline at end of file + hi := nil //@diag(":=", "nonewvars", "no new variables", "warning") +} + diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index c4e047b5827..075fdc21193 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -8,7 +8,7 @@ DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 164 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 39 +DiagnosticsCount = 42 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 17df1010a76..c26b928aba9 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -8,7 +8,7 @@ DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 174 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 39 +DiagnosticsCount = 42 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 From 1270fd75a4eb59b0f0970889a22eb94a73065f7e Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Fri, 16 Dec 2022 16:02:25 -0500 Subject: [PATCH 547/723] gopls/internal/lsp: announce selectionRangeProvider capability The server needs to tell the client it's supporting this feature. Updates golang/go#36679 Change-Id: Ia6fdb35ea90fac81367d4b45721928c606344afc Reviewed-on: https://go-review.googlesource.com/c/tools/+/458199 TryBot-Result: Gopher Robot Run-TryBot: Hyang-Ah Hana Kim gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/general.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index 57348cd564b..1c919682382 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -159,6 +159,7 @@ See https://github.com/golang/go/issues/45732 for more information.`, InlayHintProvider: protocol.InlayHintOptions{}, ReferencesProvider: true, RenameProvider: renameOpts, + SelectionRangeProvider: protocol.SelectionRangeRegistrationOptions{}, SignatureHelpProvider: protocol.SignatureHelpOptions{ TriggerCharacters: []string{"(", ","}, }, From b4dfc36097e26024efdc08455c46e8e283c505dc Mon Sep 17 00:00:00 2001 From: Zvonimir Pavlinovic Date: Fri, 16 Dec 2022 17:18:31 -0800 Subject: [PATCH 548/723] go/ssa: deref core type in emitLoad When core type of a type parameter T is *X, then the result of loading from T should be X, not T. This was not the case earlier, since emitLoad was not dereferencing core type, just the actual type (here, T). This was leading to some panics. Fixes golang/go#57272 Change-Id: I7d1fc7f281925caec2e751063e5573c539e810a5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/458235 Reviewed-by: Tim King Run-TryBot: Zvonimir Pavlinovic gopls-CI: kokoro TryBot-Result: Gopher Robot --- go/ssa/builder_generic_test.go | 15 +++++++++++++++ go/ssa/emit.go | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/go/ssa/builder_generic_test.go b/go/ssa/builder_generic_test.go index dda53e1541f..2588f74c5f9 100644 --- a/go/ssa/builder_generic_test.go +++ b/go/ssa/builder_generic_test.go @@ -443,6 +443,21 @@ func TestGenericBodies(t *testing.T) { return true }`, }, + { + pkg: "j", + contents: ` + package j + + type E interface{} + + func Foo[T E, PT interface{ *T }]() T { + pt := PT(new(T)) + x := *pt + print(x) /*@ types(T)*/ + return x + } + `, + }, } { test := test t.Run(test.pkg, func(t *testing.T) { diff --git a/go/ssa/emit.go b/go/ssa/emit.go index 3754325048f..e7cd6261dcd 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -29,7 +29,7 @@ func emitNew(f *Function, typ types.Type, pos token.Pos) *Alloc { // new temporary, and returns the value so defined. func emitLoad(f *Function, addr Value) *UnOp { v := &UnOp{Op: token.MUL, X: addr} - v.setType(deref(addr.Type())) + v.setType(deref(typeparams.CoreType(addr.Type()))) f.emit(v) return v } From eb70795aaccb8e6c9615c88085ef3414ba04b8c9 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Wed, 5 Oct 2022 10:35:41 -0400 Subject: [PATCH 549/723] gopls: ignore //line directives Gopls has trouble with line directives, because there are then two source positions for a token.Pos, and what would have been local to a single file then involves two files. This CL causes gopls to ignore line directives. The intention is that if issues arise (e.g., possibly from files generated by cgo) they will be addressed individually. Addresses golang/go#55403 Change-Id: I3fcd6c203bd247f47baa45414fa89b90716fc03f Reviewed-on: https://go-review.googlesource.com/c/tools/+/439115 Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Peter Weinberger --- gopls/doc/generate.go | 2 +- gopls/internal/lsp/analysis/fillstruct/fillstruct.go | 2 +- gopls/internal/lsp/analysis/undeclaredname/undeclared.go | 4 ++-- gopls/internal/lsp/cache/errors.go | 2 +- gopls/internal/lsp/command/commandmeta/meta.go | 2 +- gopls/internal/lsp/safetoken/safetoken_test.go | 2 +- gopls/internal/lsp/source/completion/format.go | 2 +- gopls/internal/lsp/source/completion/package.go | 2 +- gopls/internal/lsp/source/completion/snippet.go | 2 +- gopls/internal/lsp/source/extract.go | 4 ++-- gopls/internal/lsp/source/format.go | 4 ++-- gopls/internal/lsp/source/implementation.go | 5 +++-- gopls/internal/lsp/source/references.go | 2 +- gopls/internal/lsp/source/stub.go | 4 ++-- gopls/internal/lsp/source/util.go | 5 +++-- gopls/internal/lsp/source/util_test.go | 2 ++ gopls/internal/lsp/tests/tests.go | 4 ++-- gopls/internal/regtest/misc/failures_test.go | 2 ++ gopls/internal/span/token.go | 2 +- 19 files changed, 30 insertions(+), 24 deletions(-) diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go index 9ab2d01a3f2..8c0bbd3a701 100644 --- a/gopls/doc/generate.go +++ b/gopls/doc/generate.go @@ -555,7 +555,7 @@ func upperFirst(x string) string { func fileForPos(pkg *packages.Package, pos token.Pos) (*ast.File, error) { fset := pkg.Fset for _, f := range pkg.Syntax { - if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename { + if fset.PositionFor(f.Pos(), false).Filename == fset.PositionFor(pos, false).Filename { return f, nil } } diff --git a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go index faf5ba5a9a6..4731514eb5b 100644 --- a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -271,7 +271,7 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast // Find the line on which the composite literal is declared. split := bytes.Split(content, []byte("\n")) - lineNumber := fset.Position(expr.Lbrace).Line + lineNumber := fset.PositionFor(expr.Lbrace, false).Line firstLine := split[lineNumber-1] // lines are 1-indexed // Trim the whitespace from the left of the line, and use the index diff --git a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go index 742a4d265df..e225bb6ffdb 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -112,7 +112,7 @@ func runForError(pass *analysis.Pass, err types.Error) { if tok == nil { return } - offset := pass.Fset.Position(err.Pos).Offset + offset := pass.Fset.PositionFor(err.Pos, false).Offset end := tok.Pos(offset + len(name)) pass.Report(analysis.Diagnostic{ Pos: err.Pos, @@ -146,7 +146,7 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast return nil, fmt.Errorf("could not locate insertion point") } - insertBefore := fset.Position(insertBeforeStmt.Pos()).Offset + insertBefore := fset.PositionFor(insertBeforeStmt.Pos(), false).Offset // Get the indent to add on the line after the new statement. // Since this will have a parse error, we can not use format.Source(). diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 92d58e58261..550c736c560 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -372,7 +372,7 @@ func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Sp if fset != terr.Fset { return 0, span.Span{}, bug.Errorf("wrong FileSet for type error") } - posn := fset.Position(start) + posn := fset.PositionFor(start, false) if !posn.IsValid() { return 0, span.Span{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr) } diff --git a/gopls/internal/lsp/command/commandmeta/meta.go b/gopls/internal/lsp/command/commandmeta/meta.go index 11d9940f6d4..00421c73f93 100644 --- a/gopls/internal/lsp/command/commandmeta/meta.go +++ b/gopls/internal/lsp/command/commandmeta/meta.go @@ -244,7 +244,7 @@ func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) { fset := pkg.Fset var file *ast.File for _, f := range pkg.Syntax { - if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename { + if fset.PositionFor(f.Pos(), false).Filename == fset.PositionFor(pos, false).Filename { file = f break } diff --git a/gopls/internal/lsp/safetoken/safetoken_test.go b/gopls/internal/lsp/safetoken/safetoken_test.go index 86ece1f8e6f..00bf7bd9346 100644 --- a/gopls/internal/lsp/safetoken/safetoken_test.go +++ b/gopls/internal/lsp/safetoken/safetoken_test.go @@ -61,7 +61,7 @@ func TestTokenOffset(t *testing.T) { if safeOffset.Pos() <= ident.Pos() && ident.Pos() <= safeOffset.Scope().End() { continue // accepted usage } - t.Errorf(`%s: Unexpected use of (*go/token.File).Offset. Please use golang.org/x/tools/gopls/internal/lsp/safetoken.Offset instead.`, fset.Position(ident.Pos())) + t.Errorf(`%s: Unexpected use of (*go/token.File).Offset. Please use golang.org/x/tools/gopls/internal/lsp/safetoken.Offset instead.`, fset.PositionFor(ident.Pos(), false)) } } } diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go index 2ff449a454c..0f8189a7bff 100644 --- a/gopls/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -227,7 +227,7 @@ Suffixes: if !c.opts.documentation { return item, nil } - pos := c.pkg.FileSet().Position(obj.Pos()) + pos := c.pkg.FileSet().PositionFor(obj.Pos(), false) // We ignore errors here, because some types, like "unsafe" or "error", // may not have valid positions that we can use to get documentation. diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index abfa8b8e8a5..5def009a89d 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -124,7 +124,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, if cursorLine <= 0 || cursorLine > len(lines) { return nil, fmt.Errorf("invalid line number") } - if fset.Position(expr.Pos()).Line == cursorLine { + if fset.PositionFor(expr.Pos(), false).Line == cursorLine { words := strings.Fields(lines[cursorLine-1]) if len(words) > 0 && words[0] == PACKAGE { content := PACKAGE diff --git a/gopls/internal/lsp/source/completion/snippet.go b/gopls/internal/lsp/source/completion/snippet.go index 751f61b7a25..db97a72d796 100644 --- a/gopls/internal/lsp/source/completion/snippet.go +++ b/gopls/internal/lsp/source/completion/snippet.go @@ -43,7 +43,7 @@ func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snip // If the cursor position is on a different line from the literal's opening brace, // we are in a multiline literal. - if fset.Position(c.pos).Line != fset.Position(clInfo.cl.Lbrace).Line { + if fset.PositionFor(c.pos, false).Line != fset.PositionFor(clInfo.cl.Lbrace, false).Line { snip.WriteText(",") } } diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index 7f29d45ba2f..b0ad550dfe8 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -28,7 +28,7 @@ func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast. tokFile := fset.File(file.Pos()) expr, path, ok, err := CanExtractVariable(rng, file) if !ok { - return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", fset.Position(rng.Start), err) + return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", fset.PositionFor(rng.Start, false), err) } // Create new AST node for extracted code. @@ -224,7 +224,7 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file p, ok, methodOk, err := CanExtractFunction(tok, rng, src, file) if (!ok && !isMethod) || (!methodOk && isMethod) { return nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix, - fset.Position(rng.Start), err) + fset.PositionFor(rng.Start, false), err) } tok, path, rng, outer, start := p.tok, p.path, p.rng, p.outer, p.start fileScope := info.Scopes[file] diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index 2605cbb0a85..7aca5e7d06b 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -265,8 +265,8 @@ func importPrefix(src []byte) (string, error) { if end, err := safetoken.Offset(tok, c.End()); err != nil { return "", err } else if end > importEnd { - startLine := tok.Position(c.Pos()).Line - endLine := tok.Position(c.End()).Line + startLine := tok.PositionFor(c.Pos(), false).Line + endLine := tok.PositionFor(c.End(), false).Line // Work around golang/go#41197 by checking if the comment might // contain "\r", and if so, find the actual end position of the diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 315e38857ab..d3de3733320 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -153,7 +153,8 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. candObj = sel.Obj() } - if candObj == queryMethod { + pos := s.FileSet().PositionFor(candObj.Pos(), false) + if candObj == queryMethod || seen[pos] { continue } @@ -165,7 +166,7 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. var posn token.Position if pkg != nil { - posn = pkg.FileSet().Position(candObj.Pos()) + posn = pkg.FileSet().PositionFor(candObj.Pos(), false) } if seen[posn] { continue diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index b99c2b162ee..8f9110a998a 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -173,7 +173,7 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i } // Inv: qos[0].pkg != nil, since Pos is valid. // Inv: qos[*].pkg != nil, since all qos are logically the same declaration. - filename := qos[0].pkg.FileSet().File(pos).Name() + filename := qos[0].pkg.FileSet().PositionFor(pos, false).Filename pgf, err := qos[0].pkg.File(span.URIFromPath(filename)) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index f4e8f0c632b..75bab288e60 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -47,7 +47,7 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi } // Parse the file defining the concrete type. - concreteFilename := snapshot.FileSet().Position(si.Concrete.Obj().Pos()).Filename + concreteFilename := snapshot.FileSet().PositionFor(si.Concrete.Obj().Pos(), false).Filename concreteFH, err := snapshot.GetFile(ctx, span.URIFromPath(concreteFilename)) if err != nil { return nil, nil, err @@ -261,7 +261,7 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method return nil, fmt.Errorf("expected %v to be an interface but got %T", iface, ifaceObj.Type().Underlying()) } // Parse the imports from the file that declares the interface. - ifaceFilename := snapshot.FileSet().Position(ifaceObj.Pos()).Filename + ifaceFilename := snapshot.FileSet().PositionFor(ifaceObj.Pos(), false).Filename ifaceFH, err := snapshot.GetFile(ctx, span.URIFromPath(ifaceFilename)) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index cc9c9e44665..cebb99b1299 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -120,7 +120,7 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { for _, comment := range commentGroup.List { if matched := generatedRx.MatchString(comment.Text); matched { // Check if comment is at the beginning of the line in source. - if pgf.Tok.Position(comment.Slash).Column == 1 { + if pgf.Tok.PositionFor(comment.Slash, false).Column == 1 { return true } } @@ -164,7 +164,8 @@ func posToMappedRange(pkg Package, pos, end token.Pos) (MappedRange, error) { // Subtle: it is not safe to simplify this to tokFile.Name // because, due to //line directives, a Position within a // token.File may have a different filename than the File itself. - logicalFilename := tokFile.Position(pos).Filename + // BUT, things have changed, and we're ignoring line directives + logicalFilename := tokFile.PositionFor(pos, false).Filename pgf, _, err := findFileInDeps(pkg, span.URIFromPath(logicalFilename)) if err != nil { return MappedRange{}, err diff --git a/gopls/internal/lsp/source/util_test.go b/gopls/internal/lsp/source/util_test.go index 60128e2344a..da565e3602a 100644 --- a/gopls/internal/lsp/source/util_test.go +++ b/gopls/internal/lsp/source/util_test.go @@ -14,7 +14,9 @@ import ( "golang.org/x/tools/gopls/internal/span" ) +// TODO: still need to test that NewMappedRange is working correctly func TestMappedRangeAdjustment(t *testing.T) { + t.Skip("line directives not supported") // Test that mapped range adjusts positions in compiled files to positions in // the corresponding edited file. diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 2c7f309b9d8..0dc0a64fd60 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -1368,7 +1368,7 @@ func (data *Data) collectWorkspaceSymbols(typ WorkspaceSymbolsTestType) func(*ex if data.WorkspaceSymbols[typ] == nil { data.WorkspaceSymbols[typ] = make(map[span.URI][]string) } - pos := data.Exported.ExpectFileSet.Position(note.Pos) + pos := data.Exported.ExpectFileSet.PositionFor(note.Pos, false) uri := span.URIFromPath(pos.Filename) data.WorkspaceSymbols[typ][uri] = append(data.WorkspaceSymbols[typ][uri], query) } @@ -1398,7 +1398,7 @@ func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain } func (data *Data) collectLinks(spn span.Span, link string, note *expect.Note, fset *token.FileSet) { - position := fset.Position(note.Pos) + position := fset.PositionFor(note.Pos, false) uri := spn.URI() data.Links[uri] = append(data.Links[uri], Link{ Src: spn, diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index 9ec5fb5916f..19e26a5ee29 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -13,6 +13,7 @@ import ( // This test passes (TestHoverOnError in definition_test.go) without // the //line directive func TestHoverFailure(t *testing.T) { + t.Skip("line directives //line ") const mod = ` -- go.mod -- module mod.com @@ -42,6 +43,7 @@ func main() { // This test demonstrates a case where gopls is confused by line directives, // and fails to surface type checking errors. func TestFailingDiagnosticClearingOnEdit(t *testing.T) { + t.Skip("line directives //line ") // badPackageDup contains a duplicate definition of the 'a' const. // this is from diagnostics_test.go, const badPackageDup = ` diff --git a/gopls/internal/span/token.go b/gopls/internal/span/token.go index 6bde300240f..ac9376a5835 100644 --- a/gopls/internal/span/token.go +++ b/gopls/internal/span/token.go @@ -146,7 +146,7 @@ func positionFromOffset(tf *token.File, offset int) (string, int, int, error) { return "", 0, 0, fmt.Errorf("offset %d is beyond EOF (%d) in file %s", offset, tf.Size(), tf.Name()) } pos := tf.Pos(offset) - p := tf.Position(pos) + p := tf.PositionFor(pos, false) // TODO(golang/go#41029): Consider returning line, column instead of line+1, 1 if // the file's last character is not a newline. if offset == tf.Size() { From 61e2d3f4fee1a8fb382d357fadc53af7828054c0 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 14 Oct 2022 17:58:15 -0400 Subject: [PATCH 550/723] gopls/internal/lsp/cache: a new analysis driver This change is a new implementation of gopls' analysis driver that supports "facts" (separate analysis across packages), and serializes the results of each unit of analysis in a global, persistent, file-based cache of bounded size with LRU eviction (filecache). Dependency analysis is based on hashes of only material inputs to each step: source files, and the types and facts of direct dependencies. Therefore changes that do not affect these (e.g. the insertion of a debugging print statement into a function body) will usually not invalidate most of the graph. Fact serialization is shared with unitchecker, though perhaps there are gains to be made by specializing it in future. This change uses the new API for shallow export data, in which only the types for a single package are emitted, not all the indirectly reachable types. This makes the size of the export data for each package roughly constant, but requires that export data for all transitive dependencies be available to the type checker. So, the analysis summary holds a map of all necessary export data as a union across dependencies. (It's actually somewhat fewer than all transitive deps because we record the set of packages referenced during type checking and use that to prune the set.) Change-Id: I969ce09c31e2349f98ac7dc4554bf39f93a751fe Reviewed-on: https://go-review.googlesource.com/c/tools/+/443099 Reviewed-by: Tim King Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro --- go/analysis/unitchecker/unitchecker.go | 4 + gopls/internal/lsp/cache/analysis.go | 1425 +++++++++++++---- gopls/internal/lsp/cache/errors.go | 114 +- gopls/internal/lsp/cache/maps.go | 12 +- gopls/internal/lsp/cache/parse.go | 14 +- gopls/internal/lsp/cache/session.go | 2 +- gopls/internal/lsp/cache/snapshot.go | 25 +- gopls/internal/lsp/diagnostics.go | 14 +- gopls/internal/lsp/filecache/filecache.go | 272 ++++ .../internal/lsp/filecache/filecache_test.go | 189 +++ .../lsp/filecache/setfiletime_unix.go | 28 + .../lsp/filecache/setfiletime_windows.go | 33 + gopls/internal/lsp/source/diagnostics.go | 5 +- gopls/internal/lsp/source/view.go | 3 +- .../regtest/diagnostics/diagnostics_test.go | 108 ++ gopls/internal/span/uri.go | 2 + 16 files changed, 1806 insertions(+), 444 deletions(-) create mode 100644 gopls/internal/lsp/filecache/filecache.go create mode 100644 gopls/internal/lsp/filecache/filecache_test.go create mode 100644 gopls/internal/lsp/filecache/setfiletime_unix.go create mode 100644 gopls/internal/lsp/filecache/setfiletime_windows.go diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go index 6e6907d261f..37693564e5b 100644 --- a/go/analysis/unitchecker/unitchecker.go +++ b/go/analysis/unitchecker/unitchecker.go @@ -249,6 +249,10 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re // In VetxOnly mode, analyzers are only for their facts, // so we can skip any analysis that neither produces facts // nor depends on any analysis that produces facts. + // + // TODO(adonovan): fix: the command (and logic!) here are backwards. + // It should say "...nor is required by any...". (Issue 443099) + // // Also build a map to hold working state and result. type action struct { once sync.Once diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 31d8f033bea..32fcdf7f758 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -4,295 +4,1051 @@ package cache +// This file defines gopls' driver for modular static analysis (go/analysis). + import ( + "bytes" "context" + "crypto/sha256" + "encoding/gob" + "errors" "fmt" "go/ast" + "go/token" "go/types" + "log" "reflect" "runtime/debug" + "sort" + "strings" "sync" + "time" "golang.org/x/sync/errgroup" "golang.org/x/tools/go/analysis" + "golang.org/x/tools/gopls/internal/lsp/filecache" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/facts" + "golang.org/x/tools/internal/gcimporter" "golang.org/x/tools/internal/memoize" + "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" ) -// Analyze returns a new slice of diagnostics from running the -// specified analyzers on the package denoted by id. +/* + + DESIGN + + An analysis request is for a set of analyzers and an individual + package ID, notated (a*, p). The result is the set of diagnostics + for that package. It could easily be generalized to a set of + packages, (a*, p*), and perhaps should be, to improve performance + versus calling it in a loop. + + The snapshot holds a cache (persistent.Map) of entries keyed by + (a*, p) pairs ("analysisKey") that have been requested so far. Some + of these entries may be invalidated during snapshot cloning after a + modification event. The cache maps each (a*, p) to a promise of + the analysis result or "analysisSummary". The summary contains the + results of analysis (e.g. diagnostics) as well as the intermediate + results required by the recursion, such as serialized types and + facts. + + The promise represents the result of a call to analyzeImpl, which + type-checks a package and then applies a graph of analyzers to it + in parallel postorder. (These graph edges are "horizontal": within + the same package.) First, analyzeImpl reads the source files of + package p, and obtains (recursively) the results of the "vertical" + dependencies (i.e. analyzers applied to the packages imported by + p). Only the subset of analyzers that use facts need be executed + recursively, but even if this subset is empty, the step is still + necessary because it provides type information. It is possible that + a package may need to be type-checked and analyzed twice, for + different subsets of analyzers, but the overlap is typically + insignificant. + + With the file contents and the results of vertical dependencies, + analyzeImpl is then in a position to produce a key representing the + unit of work (parsing, type-checking, and analysis) that it has to + do. The key is a cryptographic hash of the "recipe" for this step, + including the Metadata, the file contents, the set of analyzers, + and the type and fact information from the vertical dependencies. + + The key is sought in a machine-global persistent file-system based + cache. If this gopls process, or another gopls process on the same + machine, has already performed this analysis step, analyzeImpl will + make a cache hit and load the serialized summary of the results. If + not, it will have to proceed to type-checking and analysis, and + write a new cache entry. The entry contains serialized types + (export data) and analysis facts. + + For types, we use "shallow" export data. Historically, the Go + compiler always produced a summary of the types for a given package + that included types from other packages that it indirectly + referenced: "deep" export data. This had the advantage that the + compiler (and analogous tools such as gopls) need only load one + file per direct import. However, it meant that the files tended to + get larger based on the level of the package in the import + graph. For example, higher-level packages in the kubernetes module + have over 1MB of "deep" export data, even when they have almost no + content of their own, merely because they mention a major type that + references many others. In pathological cases the export data was + 300x larger than the source for a package due to this quadratic + growth. + + "Shallow" export data means that the serialized types describe only + a single package. If those types mention types from other packages, + the type checker may need to request additional packages beyond + just the direct imports. This means type information for the entire + transitive closure of imports may need to be available just in + case. After a cache hit or a cache miss, the summary is + postprocessed so that it contains the union of export data payloads + of all its direct dependencies. + + For correct dependency analysis, the digest used as a cache key + must reflect the "deep" export data, so it is derived recursively + from the transitive closure. As an optimization, we needn't include + every package of the transitive closure in the deep hash, only the + packages that were actually requested by the type checker. This + allows changes to a package that have no effect on its export data + to be "pruned". The direct consumer will need to be re-executed, + but if its export data is unchanged as a result, then indirect + consumers may not need to be re-executed. This allows, for example, + one to insert a print statement in a function and not "rebuild" the + whole application (though export data does record line numbers of + types which may be perturbed by otherwise insignificant changes.) + + The summary must record whether a package is transitively + error-free (whether it would compile) because many analyzers are + not safe to run on packages with inconsistent types. + + For fact encoding, we use the same fact set as the unitchecker + (vet) to record and serialize analysis facts. The fact + serialization mechanism is analogous to "deep" export data. + +*/ + +// TODO(adonovan): +// - Profile + optimize: +// - on a cold run, mostly type checking + export data, unsurprisingly. +// - on a hot-disk run, mostly type checking the IWL. +// Would be nice to have a benchmark that separates this out. +// - measure and record in the code the typical operation times +// and file sizes (export data + facts = cache entries). +// - Do "port the old logic" tasks (see TODO in actuallyAnalyze). +// - Add a (white-box) test of pruning when a change doesn't affect export data. +// - Optimise pruning based on subset of packages mentioned in exportdata. +// - Better logging so that it is possible to deduce why an analyzer +// is not being run--often due to very indirect failures. +// Even if the ultimate consumer decides to ignore errors, +// tests and other situations want to be assured of freedom from +// errors, not just missing results. This should be recorded. +// - Check that the event trace is intelligible. +// - Split this into a subpackage, gopls/internal/lsp/cache/driver, +// consisting of this file and three helpers from errors.go. +// The (*snapshot).Analyze method would stay behind and make calls +// to the driver package. +// Steps: +// - define a narrow driver.Snapshot interface with only these methods: +// Metadata(PackageID) source.Metadata +// GetFile(Context, URI) (source.FileHandle, error) +// View() *View // for Options +// - define a State type that encapsulates the persistent map +// (with its own mutex), and has methods: +// New() *State +// Clone(invalidate map[PackageID]bool) *State +// Destroy() +// - share cache.{goVersionRx,parseGoImpl} + +var born = time.Now() + +// Analyze applies a set of analyzers to the package denoted by id, +// and returns their diagnostics for that package. +// +// The analyzers list must be duplicate free; order does not matter. // -// (called from: (*snapshot).DiagnosePackage, source.Analyze.) +// Precondition: all analyzers within the process have distinct names. +// (The names are relied on by the serialization logic.) func (s *snapshot) Analyze(ctx context.Context, id PackageID, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) { - // TODO(adonovan): merge these two loops. There's no need to - // construct all the root action handles before beginning - // analysis. Operations should be concurrent (though that first - // requires buildPackageHandle not to be inefficient when - // called in parallel.) - var roots []*actionHandle + if false { // debugging + log.Println("Analyze@", time.Since(born)) // called after the 7s IWL in k8s + } + + // Filter and sort enabled root analyzers. + // A disabled analyzer may still be run if required by another. + toSrc := make(map[*analysis.Analyzer]*source.Analyzer) + var enabled []*analysis.Analyzer for _, a := range analyzers { - if !a.IsEnabled(s.view.Options()) { - continue + if a.IsEnabled(s.view.Options()) { + toSrc[a.Analyzer] = a + enabled = append(enabled, a.Analyzer) } - ah, err := s.actionHandle(ctx, id, a.Analyzer) - if err != nil { - return nil, err + } + sort.Slice(enabled, func(i, j int) bool { + return enabled[i].Name < enabled[j].Name + }) + + // Register fact types of required analyzers. + for _, a := range requiredAnalyzers(enabled) { + for _, f := range a.FactTypes { + gob.Register(f) } - roots = append(roots, ah) } - // Run and wait for all analyzers, and report diagnostics - // only from those that succeed. Ignore the others. + if false { // debugging + // TODO(adonovan): use proper tracing. + t0 := time.Now() + defer func() { + log.Printf("%v for analyze(%s, %s)", time.Since(t0), id, enabled) + }() + } + + // Run the analysis. + res, err := s.analyze(ctx, id, enabled) + if err != nil { + return nil, err + } + + // Report diagnostics only from enabled actions that succeeded. + // Errors from creating or analyzing packages are ignored. + // Diagnostics are reported in the order of the analyzers argument. + // + // TODO(adonovan): ignoring action errors gives the caller no way + // to distinguish "there are no problems in this code" from + // "the code (or analyzers!) are so broken that we couldn't even + // begin the analysis you asked for". + // Even if current callers choose to discard the + // results, we should propagate the per-action errors. var results []*source.Diagnostic - for _, ah := range roots { - v, err := s.awaitPromise(ctx, ah.promise) - if err != nil { - return nil, err // wait was cancelled + for _, a := range enabled { + summary := res.Actions[a.Name] + if summary.Err != "" { + continue // action failed } - - res := v.(actionResult) - if res.err != nil { - continue // analysis failed; ignore it. + for _, gobDiag := range summary.Diagnostics { + results = append(results, toSourceDiagnostic(toSrc[a], &gobDiag)) } - - results = append(results, res.data.diagnostics...) } return results, nil } -type actionKey struct { - pkgid PackageID - analyzer *analysis.Analyzer +// analysisKey is the type of keys in the snapshot.analyses map. +type analysisKey struct { + analyzerNames string + pkgid PackageID } -// An action represents one unit of analysis work: the application of -// one analysis to one package. Actions form a DAG, both within a -// package (as different analyzers are applied, either in sequence or -// parallel), and across packages (as dependencies are analyzed). -type actionHandle struct { - key actionKey // just for String() - promise *memoize.Promise // [actionResult] +func (key analysisKey) String() string { + return fmt.Sprintf("%s@%s", key.analyzerNames, key.pkgid) } -// actionData is the successful result of analyzing a package. -type actionData struct { - analyzer *analysis.Analyzer - pkgTypes *types.Package // types only; don't keep syntax live - diagnostics []*source.Diagnostic - result interface{} - objectFacts map[objectFactKey]analysis.Fact - packageFacts map[packageFactKey]analysis.Fact +// analyzeSummary is a gob-serializable summary of successfully +// applying a list of analyzers to a package. +type analyzeSummary struct { + PkgPath PackagePath // types.Package.Path() (needed to decode export data) + Export []byte + DeepExportHash source.Hash // hash of reflexive transitive closure of export data + Compiles bool // transitively free of list/parse/type errors + Actions actionsMap // map from analyzer name to analysis results (*actionSummary) + + // Not serialized: populated after the summary is computed or deserialized. + allExport map[PackagePath][]byte // transitive export data +} + +// actionsMap defines a stable Gob encoding for a map. +// TODO(adonovan): generalize and move to a library when we can use generics. +type actionsMap map[string]*actionSummary + +var _ gob.GobEncoder = (actionsMap)(nil) +var _ gob.GobDecoder = (*actionsMap)(nil) + +type actionsMapEntry struct { + K string + V *actionSummary } -// actionResult holds the result of a call to actionImpl. -type actionResult struct { - data *actionData - err error +func (m actionsMap) GobEncode() ([]byte, error) { + entries := make([]actionsMapEntry, 0, len(m)) + for k, v := range m { + entries = append(entries, actionsMapEntry{k, v}) + } + sort.Slice(entries, func(i, j int) bool { + return entries[i].K < entries[j].K + }) + var buf bytes.Buffer + err := gob.NewEncoder(&buf).Encode(entries) + return buf.Bytes(), err } -type objectFactKey struct { - obj types.Object - typ reflect.Type +func (m *actionsMap) GobDecode(data []byte) error { + var entries []actionsMapEntry + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&entries); err != nil { + return err + } + *m = make(actionsMap, len(entries)) + for _, e := range entries { + (*m)[e.K] = e.V + } + return nil } -type packageFactKey struct { - pkg *types.Package - typ reflect.Type +// actionSummary is a gob-serializable summary of one possibly failed analysis action. +// If Err is non-empty, the other fields are undefined. +type actionSummary struct { + Facts []byte // the encoded facts.Set + FactsHash source.Hash // hash(Facts) + Diagnostics []gobDiagnostic + Err string // "" => success } -func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.Analyzer) (*actionHandle, error) { - key := actionKey{id, a} +// analyze is a memoization of analyzeImpl. +func (s *snapshot) analyze(ctx context.Context, id PackageID, analyzers []*analysis.Analyzer) (*analyzeSummary, error) { + // Use the sorted list of names of analyzers in the key. + // + // TODO(adonovan): opt: account for analysis results at a + // finer grain to avoid duplicate work when a + // a proper subset of analyzers is requested? + // In particular, TypeErrorAnalyzers don't use facts + // but need to request vdeps just for type information. + names := make([]string, 0, len(analyzers)) + for _, a := range analyzers { + names = append(names, a.Name) + } + // This key describes the result of applying a list of analyzers to a package. + key := analysisKey{strings.Join(names, ","), id} + + // An analysisPromise represents the result of loading, parsing, + // type-checking and analyzing a single package. + type analysisPromise struct { + promise *memoize.Promise // [analyzeImplResult] + } + type analyzeImplResult struct { + summary *analyzeSummary + err error + } + + // Access the map once, briefly, and atomically. s.mu.Lock() - entry, hit := s.actions.Get(key) + entry, hit := s.analyses.Get(key) + if !hit { + entry = analysisPromise{ + promise: memoize.NewPromise("analysis", func(ctx context.Context, arg interface{}) interface{} { + summary, err := analyzeImpl(ctx, arg.(*snapshot), analyzers, id) + return analyzeImplResult{summary, err} + }), + } + s.analyses.Set(key, entry, nil) // nothing needs releasing + } s.mu.Unlock() - if hit { - return entry.(*actionHandle), nil - } - - // TODO(adonovan): opt: this block of code sequentially loads a package - // (and all its dependencies), then sequentially creates action handles - // for the direct dependencies (whose packages have by then been loaded - // as a consequence of ph.check) which does a sequential recursion - // down the action graph. Only once all that work is complete do we - // put a handle in the cache. As with buildPackageHandle, this does - // not exploit the natural parallelism in the problem, and the naive - // use of concurrency would lead to an exponential amount of duplicated - // work. We should instead use an atomically updated future cache - // and a parallel graph traversal. - ph, err := s.buildPackageHandle(ctx, id, source.ParseFull) + // Await result. + ap := entry.(analysisPromise) + v, err := s.awaitPromise(ctx, ap.promise) if err != nil { - return nil, err + return nil, err // e.g. cancelled } - pkg, err := ph.await(ctx, s) - if err != nil { - return nil, err + res := v.(analyzeImplResult) + return res.summary, res.err +} + +// analyzeImpl applies a list of analyzers (plus any others +// transitively required by them) to a package. It succeeds as long +// as it could produce a types.Package, even if there were direct or +// indirect list/parse/type errors, and even if all the analysis +// actions failed. It usually fails only if the package was unknown, +// a file was missing, or the operation was cancelled. +// +// Postcondition: analyzeImpl must not continue to use the snapshot +// (in background goroutines) after it has returned; see memoize.RefCounted. +func analyzeImpl(ctx context.Context, snapshot *snapshot, analyzers []*analysis.Analyzer, id PackageID) (*analyzeSummary, error) { + m := snapshot.Metadata(id) + if m == nil { + return nil, fmt.Errorf("no metadata for %s", id) } - // Add a dependency on each required analyzer. - var deps []*actionHandle // unordered - for _, req := range a.Requires { - // TODO(adonovan): opt: there's no need to repeat the package-handle - // portion of the recursion here, since we have the pkg already. - reqActionHandle, err := s.actionHandle(ctx, id, req) - if err != nil { + // Recursively analyze each "vertical" dependency + // for its types.Package and (perhaps) analysis.Facts. + // If any of them fails to produce a package, we cannot continue. + // We request only the analyzers that produce facts. + // + // Also, load the contents of each "compiled" Go file through + // the snapshot's cache. + // + // Both loops occur in parallel, and parallel with each other. + vdeps := make(map[PackageID]*analyzeSummary) + compiledGoFiles := make([]source.FileHandle, len(m.CompiledGoFiles)) + { + var group errgroup.Group + + // Analyze vertical dependencies. + // We request only the required analyzers that use facts. + var useFacts []*analysis.Analyzer + for _, a := range requiredAnalyzers(analyzers) { + if len(a.FactTypes) > 0 { + useFacts = append(useFacts, a) + } + } + var vdepsMu sync.Mutex + for _, id := range m.DepsByPkgPath { + id := id + group.Go(func() error { + res, err := snapshot.analyze(ctx, id, useFacts) + if err != nil { + return err // cancelled, or failed to produce a package + } + + vdepsMu.Lock() + vdeps[id] = res + vdepsMu.Unlock() + return nil + }) + } + + // Read file contents. + // (In practice these will be cache hits + // on reads done by the initial workspace load + // or after a change modification event.) + for i, uri := range m.CompiledGoFiles { + i, uri := i, uri + group.Go(func() error { + fh, err := snapshot.GetFile(ctx, uri) // ~25us + compiledGoFiles[i] = fh + return err // e.g. cancelled + }) + } + + if err := group.Wait(); err != nil { return nil, err } - deps = append(deps, reqActionHandle) } - // TODO(golang/go#35089): Re-enable this when we doesn't use ParseExported - // mode for dependencies. In the meantime, disable analysis for dependencies, - // since we don't get anything useful out of it. - if false { - // An analysis that consumes/produces facts - // must run on the package's dependencies too. - if len(a.FactTypes) > 0 { - for _, depID := range ph.m.DepsByPkgPath { - depActionHandle, err := s.actionHandle(ctx, depID, a) - if err != nil { - return nil, err - } - deps = append(deps, depActionHandle) - } + // Inv: analyze() of all vdeps succeeded (though some actions may have failed). + + // We no longer depend on the snapshot. + snapshot = nil + + // At this point we have the action results (serialized + // packages and facts) of our immediate dependencies, + // and the metadata and content of this package. + // + // We now compute a hash for all our inputs, and consult a + // global cache of promised results. If nothing material + // has changed, we'll make a hit in the shared cache. + // + // The hash of our inputs is based on the serialized export + // data and facts so that immaterial changes can be pruned + // without decoding. + key := analysisCacheKey(analyzers, m, compiledGoFiles, vdeps) + + // Access the cache. + var summary *analyzeSummary + const cacheKind = "analysis" + if data, err := filecache.Get(cacheKind, key); err == nil { + // cache hit + mustDecode(data, &summary) + + } else if err != filecache.ErrNotFound { + return nil, bug.Errorf("internal error reading shared cache: %v", err) + + } else { + // Cache miss: do the work. + var err error + summary, err = actuallyAnalyze(ctx, analyzers, m, vdeps, compiledGoFiles) + if err != nil { + return nil, err + } + data := mustEncode(summary) + if false { + log.Printf("Set key=%d value=%d id=%s\n", len(key), len(data), id) + } + if err := filecache.Set(cacheKind, key, data); err != nil { + return nil, fmt.Errorf("internal error updating shared cache: %v", err) } } - // The promises are kept in a store on the package, - // so the key need only include the analyzer name. + // Hit or miss, we need to merge the export data from + // dependencies so that it includes all the types + // that might be summoned by the type checker. // - // (Since type-checking and analysis depend on the identity - // of packages--distinct packages produced by the same - // recipe are not fungible--we must in effect use the package - // itself as part of the key. Rather than actually use a pointer - // in the key, we get a simpler object graph if we shard the - // store by packages.) - promise, release := pkg.analyses.Promise(a.Name, func(ctx context.Context, arg interface{}) interface{} { - res, err := actionImpl(ctx, arg.(*snapshot), deps, a, pkg) - return actionResult{res, err} - }) + // TODO(adonovan): opt: reduce this set by recording + // which packages were actually summoned by insert(). + // (Just makes map smaller; probably marginal?) + allExport := make(map[PackagePath][]byte) + for _, vdep := range vdeps { + for k, v := range vdep.allExport { + allExport[k] = v + } + } + allExport[m.PkgPath] = summary.Export + summary.allExport = allExport + + return summary, nil +} - ah := &actionHandle{ - key: key, - promise: promise, +// analysisCacheKey returns a cache key that is a cryptographic digest +// of the all the values that might affect type checking and analysis: +// the analyzer names, package metadata, names and contents of +// compiled Go files, and vdeps information (export data and facts). +// +// TODO(adonovan): safety: define our own flavor of Metadata +// containing just the fields we need, and using it in the subsequent +// logic, to keep us honest about hashing all parts that matter? +func analysisCacheKey(analyzers []*analysis.Analyzer, m *source.Metadata, compiledGoFiles []source.FileHandle, vdeps map[PackageID]*analyzeSummary) [sha256.Size]byte { + hasher := sha256.New() + + // In principle, a key must be the hash of an + // unambiguous encoding of all the relevant data. + // If it's ambiguous, we risk collisons. + + // analyzers + fmt.Fprintf(hasher, "analyzers: %d\n", len(analyzers)) + for _, a := range analyzers { + fmt.Fprintln(hasher, a.Name) } - s.mu.Lock() - defer s.mu.Unlock() + // package metadata + fmt.Fprintf(hasher, "package: %s %s %s\n", m.ID, m.Name, m.PkgPath) + // We can ignore m.DepsBy{Pkg,Import}Path: although the logic + // uses those fields, we account for them by hashing vdeps. - // Check cache again in case another thread got there first. - if result, ok := s.actions.Get(key); ok { - release() - return result.(*actionHandle), nil + // type sizes + // This assertion is safe, but if a black-box implementation + // is ever needed, record Sizeof(*int) and Alignof(int64). + sz := m.TypesSizes.(*types.StdSizes) + fmt.Fprintf(hasher, "sizes: %d %d\n", sz.WordSize, sz.MaxAlign) + + // metadata errors + for _, err := range m.Errors { + fmt.Fprintf(hasher, "error: %q", err) } - s.actions.Set(key, ah, func(_, _ interface{}) { release() }) + // module Go version + if m.Module != nil && m.Module.GoVersion != "" { + fmt.Fprintf(hasher, "go %s\n", m.Module.GoVersion) + } - return ah, nil -} + // file names and contents + fmt.Fprintf(hasher, "files: %d\n", len(compiledGoFiles)) + for _, fh := range compiledGoFiles { + fmt.Fprintln(hasher, fh.FileIdentity()) + } -func (key actionKey) String() string { - return fmt.Sprintf("%s@%s", key.analyzer, key.pkgid) + // vdeps, in PackageID order + depIDs := make([]string, 0, len(vdeps)) + for depID := range vdeps { + depIDs = append(depIDs, string(depID)) + } + sort.Strings(depIDs) + for _, id := range depIDs { + vdep := vdeps[PackageID(id)] + fmt.Fprintf(hasher, "dep: %s\n", vdep.PkgPath) + fmt.Fprintf(hasher, "export: %s\n", vdep.DeepExportHash) + + // action results: errors and facts + names := make([]string, 0, len(vdep.Actions)) + for name := range vdep.Actions { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + summary := vdep.Actions[name] + fmt.Fprintf(hasher, "action %s\n", name) + if summary.Err != "" { + fmt.Fprintf(hasher, "error %s\n", summary.Err) + } else { + fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash) + // We can safely omit summary.diagnostics + // from the key since they have no downstream effect. + } + } + } + + var hash [sha256.Size]byte + hasher.Sum(hash[:0]) + return hash } -func (act *actionHandle) String() string { - return act.key.String() +// actuallyAnalyze implements the cache-miss case. +// This function does not access the snapshot. +func actuallyAnalyze(ctx context.Context, analyzers []*analysis.Analyzer, m *source.Metadata, vdeps map[PackageID]*analyzeSummary, compiledGoFiles []source.FileHandle) (*analyzeSummary, error) { + + // Create a local FileSet for processing this package only. + fset := token.NewFileSet() + + // Parse only the "compiled" Go files. + // Do the computation in parallel. + parsed := make([]*source.ParsedGoFile, len(compiledGoFiles)) + { + var group errgroup.Group + for i, fh := range compiledGoFiles { + i, fh := i, fh + group.Go(func() error { + // Call parseGoImpl directly, not the caching wrapper, + // as cached ASTs require the global FileSet. + pgf, err := parseGoImpl(ctx, fset, fh, source.ParseFull) + parsed[i] = pgf + return err + }) + } + if err := group.Wait(); err != nil { + return nil, err // cancelled, or catastrophic error (e.g. missing file) + } + } + + // Type-check the package. + pkg := typeCheckForAnalysis(fset, parsed, m, vdeps) + + // Build a map of PkgPath to *Package for all packages mentioned + // in exportdata for use by facts. + pkg.factsDecoder = facts.NewDecoder(pkg.types) + + // Poll cancellation state. + if err := ctx.Err(); err != nil { + return nil, err + } + + // TODO(adonovan): port the old logic to: + // - gather go/packages diagnostics from m.Errors? (port goPackagesErrorDiagnostics) + // - record unparseable file URIs so we can suppress type errors for these files. + // - gather diagnostics from expandErrors + typeErrorDiagnostics + depsErrors. + + // -- analysis -- + + // Build action graph for this package. + // Each graph node (action) is one unit of analysis. + actions := make(map[*analysis.Analyzer]*action) + var mkAction func(a *analysis.Analyzer) *action + mkAction = func(a *analysis.Analyzer) *action { + act, ok := actions[a] + if !ok { + var hdeps []*action + for _, req := range a.Requires { + hdeps = append(hdeps, mkAction(req)) + } + act = &action{a: a, pkg: pkg, vdeps: vdeps, hdeps: hdeps} + actions[a] = act + } + return act + } + + // Build actions for initial package. + var roots []*action + for _, a := range analyzers { + roots = append(roots, mkAction(a)) + } + + // Execute the graph in parallel. + execActions(roots) + + // Don't return (or cache) the result in case of cancellation. + if err := ctx.Err(); err != nil { + return nil, err // cancelled + } + + // Return summaries only for the requested actions. + summaries := make(map[string]*actionSummary) + for _, act := range roots { + summaries[act.a.Name] = act.summary + } + + return &analyzeSummary{ + PkgPath: PackagePath(pkg.types.Path()), + Export: pkg.export, + DeepExportHash: pkg.deepExportHash, + Compiles: pkg.compiles, + Actions: summaries, + }, nil } -// actionImpl runs the analysis for action node (analyzer, pkg), -// whose direct dependencies are deps. -func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, analyzer *analysis.Analyzer, pkg *pkg) (*actionData, error) { - // Run action dependencies first, and plumb the results and - // facts of each dependency into the inputs of this action. +func typeCheckForAnalysis(fset *token.FileSet, parsed []*source.ParsedGoFile, m *source.Metadata, vdeps map[PackageID]*analyzeSummary) *analysisPackage { + if false { // debugging + log.Println("typeCheckForAnalysis", m.PkgPath) + } + + pkg := &analysisPackage{ + m: m, + fset: fset, + parsed: parsed, + files: make([]*ast.File, len(parsed)), + compiles: len(m.Errors) == 0, // false => list error + types: types.NewPackage(string(m.PkgPath), string(m.Name)), + typesInfo: &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + }, + typesSizes: m.TypesSizes, + } + typeparams.InitInstanceInfo(pkg.typesInfo) + + for i, p := range parsed { + pkg.files[i] = p.File + if p.ParseErr != nil { + pkg.compiles = false // parse error + } + } + + // Unsafe is special. + if m.PkgPath == "unsafe" { + pkg.types = types.Unsafe + return pkg + } + + // Compute the union of transitive export data. + // (The actual values are shared, and not serialized.) + allExport := make(map[PackagePath][]byte) + for _, vdep := range vdeps { + for k, v := range vdep.allExport { + allExport[k] = v + } + + if !vdep.Compiles { + pkg.compiles = false // transitive error + } + } + + // exportHasher computes a hash of the names and export data of + // each package that was actually loaded during type checking. + // + // Because we use shallow export data, the hash for dependency + // analysis must incorporate indirect dependencies. As an + // optimization, we include only those that were actually + // used, which may be a small subset of those available. + // + // TODO(adonovan): opt: even better would be to implement a + // traversal over the package API like facts.NewDecoder does + // and only mention that set of packages in the hash. + // Perhaps there's a way to do that more efficiently. + // + // TODO(adonovan): opt: record the shallow hash alongside the + // shallow export data in the allExport map to avoid repeatedly + // hashing the export data. + // + // The writes to hasher below assume that type checking imports + // packages in a deterministic order. + exportHasher := sha256.New() + hashExport := func(pkgPath PackagePath, export []byte) { + fmt.Fprintf(exportHasher, "%s %d ", pkgPath, len(export)) + exportHasher.Write(export) + } + + // importer state var ( - mu sync.Mutex - inputs = make(map[*analysis.Analyzer]interface{}) - objectFacts = make(map[objectFactKey]analysis.Fact) - packageFacts = make(map[packageFactKey]analysis.Fact) + insert func(p *types.Package, name string) + importMap = make(map[string]*types.Package) // keys are PackagePaths ) - g, ctx := errgroup.WithContext(ctx) - for _, dep := range deps { - dep := dep - g.Go(func() error { - v, err := snapshot.awaitPromise(ctx, dep.promise) - if err != nil { - return err // e.g. cancelled + loadFromExportData := func(pkgPath PackagePath) (*types.Package, error) { + export, ok := allExport[pkgPath] + if !ok { + return nil, bug.Errorf("missing export data for %q", pkgPath) + } + hashExport(pkgPath, export) + imported, err := gcimporter.IImportShallow(fset, importMap, export, string(pkgPath), insert) + if err != nil { + return nil, bug.Errorf("invalid export data for %q: %v", pkgPath, err) + } + return imported, nil + } + insert = func(p *types.Package, name string) { + imported, err := loadFromExportData(PackagePath(p.Path())) + if err != nil { + log.Fatalf("internal error: %v", err) + } + if imported != p { + log.Fatalf("internal error: inconsistent packages") + } + } + + cfg := &types.Config{ + Error: func(e error) { + pkg.compiles = false // type error + pkg.typeErrors = append(pkg.typeErrors, e.(types.Error)) + }, + Importer: importerFunc(func(importPath string) (*types.Package, error) { + if importPath == "unsafe" { + return types.Unsafe, nil // unsafe has no export data } - res := v.(actionResult) - if res.err != nil { - return res.err // analysis of dependency failed + + // Beware that returning an error from this function + // will cause the type checker to synthesize a fake + // package whose Path is importPath, potentially + // losing a vendor/ prefix. If type-checking errors + // are swallowed, these packages may be confusing. + + id, ok := m.DepsByImpPath[ImportPath(importPath)] + if !ok { + // The import syntax is inconsistent with the metadata. + // This could be because the import declaration was + // incomplete and the metadata only includes complete + // imports; or because the metadata ignores import + // edges that would lead to cycles in the graph. + return nil, fmt.Errorf("missing metadata for import of %q", importPath) } - data := res.data - - mu.Lock() - defer mu.Unlock() - if data.pkgTypes == pkg.types { - // Same package, different analysis (horizontal edge): - // in-memory outputs of prerequisite analyzers - // become inputs to this analysis pass. - inputs[data.analyzer] = data.result - - } else if data.analyzer == analyzer { - // Same analysis, different package (vertical edge): - // serialized facts produced by prerequisite analysis - // become available to this analysis pass. - for key, fact := range data.objectFacts { - objectFacts[key] = fact - } - for key, fact := range data.packageFacts { - packageFacts[key] = fact - } - } else { - // Edge is neither vertical nor horizontal. - // This should never happen, yet an assertion here was - // observed to fail due to an edge (bools, p) -> (inspector, p') - // where p and p' are distinct packages with the - // same ID ("command-line-arguments:file=.../main.go"). - // - // It is not yet clear whether the command-line-arguments - // package is significant, but it is clear that package - // loading (the mapping from ID to *pkg) is inconsistent - // within a single graph. - - // Use the bug package so that we detect whether our tests - // discover this problem in regular packages. - // For command-line-arguments we quietly abort the analysis - // for now since we already know there is a bug. - errorf := bug.Errorf // report this discovery - if source.IsCommandLineArguments(pkg.ID()) { - errorf = fmt.Errorf // suppress reporting + depResult, ok := vdeps[id] // id may be "" + if !ok { + // Analogous to (*snapshot).missingPkgError + // in the logic for regular type-checking, + // but without a snapshot we can't provide + // such detail, and anyway most analysis + // failures aren't surfaced in the UI. + return nil, fmt.Errorf("no required module provides package %q (id=%q)", importPath, id) + } + + // (Duplicates logic from check.go.) + if !source.IsValidImport(m.PkgPath, depResult.PkgPath) { + return nil, fmt.Errorf("invalid use of internal package %s", importPath) + } + + return loadFromExportData(depResult.PkgPath) + }), + } + + // Set Go dialect. + if m.Module != nil && m.Module.GoVersion != "" { + goVersion := "go" + m.Module.GoVersion + // types.NewChecker panics if GoVersion is invalid. + // An unparsable mod file should probably stop us + // before we get here, but double check just in case. + if goVersionRx.MatchString(goVersion) { + typesinternal.SetGoVersion(cfg, goVersion) + } + } + + // We want to type check cgo code if go/types supports it. + // We passed typecheckCgo to go/packages when we Loaded. + // TODO(adonovan): do we actually need this?? + typesinternal.SetUsesCgo(cfg) + + check := types.NewChecker(cfg, fset, pkg.types, pkg.typesInfo) + + // Type checking errors are handled via the config, so ignore them here. + _ = check.Files(pkg.files) + + // debugging (type errors are quite normal) + if false { + if pkg.typeErrors != nil { + log.Printf("package %s has type errors: %v", pkg.types.Path(), pkg.typeErrors) + } + } + + // Emit the export data and compute the deep hash. + export, err := gcimporter.IExportShallow(pkg.fset, pkg.types) + if err != nil { + log.Fatalf("internal error writing shallow export data: %v", err) + } + pkg.export = export + hashExport(m.PkgPath, export) + exportHasher.Sum(pkg.deepExportHash[:0]) + + return pkg +} + +// analysisPackage contains information about a package, including +// syntax trees, used transiently during its type-checking and analysis. +type analysisPackage struct { + m *source.Metadata + fset *token.FileSet // local to this package + parsed []*source.ParsedGoFile + files []*ast.File // same as parsed[i].File + types *types.Package + compiles bool // package is transitively free of list/parse/type errors + factsDecoder *facts.Decoder + export []byte // encoding of types.Package + deepExportHash source.Hash // reflexive transitive hash of export data + typesInfo *types.Info + typeErrors []types.Error + typesSizes types.Sizes +} + +// An action represents one unit of analysis work: the application of +// one analysis to one package. Actions form a DAG, both within a +// package (as different analyzers are applied, either in sequence or +// parallel), and across packages (as dependencies are analyzed). +type action struct { + once sync.Once + a *analysis.Analyzer + pkg *analysisPackage + hdeps []*action // horizontal dependencies + vdeps map[PackageID]*analyzeSummary // vertical dependencies + + // results of action.exec(): + result interface{} // result of Run function, of type a.ResultType + summary *actionSummary + err error +} + +func (act *action) String() string { + return fmt.Sprintf("%s@%s", act.a.Name, act.pkg.m.ID) +} + +// execActions executes a set of action graph nodes in parallel. +func execActions(actions []*action) { + var wg sync.WaitGroup + for _, act := range actions { + act := act + wg.Add(1) + go func() { + defer wg.Done() + act.once.Do(func() { + execActions(act.hdeps) // analyze "horizontal" dependencies + act.result, act.summary, act.err = act.exec() + if act.err != nil { + act.summary = &actionSummary{Err: act.err.Error()} + // TODO(adonovan): suppress logging. But + // shouldn't the root error's causal chain + // include this information? + if false { // debugging + log.Printf("act.exec(%v) failed: %v", act, act.err) + } } - err := errorf("internal error: unexpected analysis dependency %s@%s -> %s", analyzer.Name, pkg.ID(), dep) - // Log the event in any case, as the ultimate - // consumer of actionResult ignores errors. - event.Error(ctx, "analysis", err) - return err + }) + }() + } + wg.Wait() +} + +// exec defines the execution of a single action. +// It returns the (ephemeral) result of the analyzer's Run function, +// along with its (serializable) facts and diagnostics. +// Or it returns an error if the analyzer did not run to +// completion and deliver a valid result. +func (act *action) exec() (interface{}, *actionSummary, error) { + analyzer := act.a + pkg := act.pkg + + hasFacts := len(analyzer.FactTypes) > 0 + + // Report an error if any action dependency (vertical or horizontal) failed. + // To avoid long error messages describing chains of failure, + // we return the dependencies' error' unadorned. + if hasFacts { + // TODO(adonovan): use deterministic order. + for _, res := range act.vdeps { + if vdep := res.Actions[analyzer.Name]; vdep.Err != "" { + return nil, nil, errors.New(vdep.Err) } - return nil - }) + } + } + for _, dep := range act.hdeps { + if dep.err != nil { + return nil, nil, dep.err + } + } + // Inv: all action dependencies succeeded. + + // Were there list/parse/type errors that might prevent analysis? + if !pkg.compiles && !analyzer.RunDespiteErrors { + return nil, nil, fmt.Errorf("skipping analysis %q because package %q does not compile", analyzer.Name, pkg.m.ID) + } + // Inv: package is well-formed enough to proceed with analysis. + + if false { // debugging + log.Println("action.exec", act) + } + + // Gather analysis Result values from horizontal dependencies. + var inputs = make(map[*analysis.Analyzer]interface{}) + for _, dep := range act.hdeps { + inputs[dep.a] = dep.result + } + + // TODO(adonovan): opt: facts.Set works but it may be more + // efficient to fork and tailor it to our precise needs. + // + // We've already sharded the fact encoding by action + // so that it can be done in parallel (hoisting the + // ImportMap call so that we build the map once per package). + // We could eliminate locking. + // We could also dovetail more closely with the export data + // decoder to obtain a more compact representation of + // packages and objects (e.g. its internal IDs, instead + // of PkgPaths and objectpaths.) + + // Read and decode analysis facts for each imported package. + factset, err := pkg.factsDecoder.Decode(func(imp *types.Package) ([]byte, error) { + if !hasFacts { + return nil, nil // analyzer doesn't use facts, so no vdeps + } + + // Package.Imports() may contain a fake "C" package. Ignore it. + if imp.Path() == "C" { + return nil, nil + } + + id, ok := pkg.m.DepsByPkgPath[PackagePath(imp.Path())] + if !ok { + // This may mean imp was synthesized by the type + // checker because it failed to import it for any reason + // (e.g. bug processing export data; metadata ignoring + // a cycle-forming import). + // In that case, the fake package's imp.Path + // is set to the failed importPath (and thus + // it may lack a "vendor/" prefix). + // + // For now, silently ignore it on the assumption + // that the error is already reported elsewhere. + // return nil, fmt.Errorf("missing metadata") + return nil, nil + } + + vdep, ok := act.vdeps[id] + if !ok { + return nil, bug.Errorf("internal error in %s: missing vdep for id=%s", pkg.types.Path(), id) + } + return vdep.Actions[analyzer.Name].Facts, nil + }) + if err != nil { + return nil, nil, fmt.Errorf("internal error decoding analysis facts: %w", err) } - if err := g.Wait(); err != nil { - return nil, err // cancelled, or dependency failed + + // TODO(adonovan): make Export*Fact panic rather than discarding + // undeclared fact types, so that we discover bugs in analyzers. + factFilter := make(map[reflect.Type]bool) + for _, f := range analyzer.FactTypes { + factFilter[reflect.TypeOf(f)] = true } - // Now run the (pkg, analyzer) analysis. - var syntax []*ast.File - for _, cgf := range pkg.compiledGoFiles { - syntax = append(syntax, cgf.File) + // posToLocation converts from token.Pos to protocol form. + // TODO(adonovan): improve error messages. + posToLocation := func(start, end token.Pos) (protocol.Location, error) { + tokFile := pkg.fset.File(start) + for _, p := range pkg.parsed { + if p.Tok == tokFile { + if end == token.NoPos { + end = start + } + rng, err := p.Mapper.PosRange(start, end) + if err != nil { + return protocol.Location{}, err + } + return protocol.Location{ + // TODO(adonovan): is this sound? + // See dual conversion in toSourceDiagnostic. + URI: protocol.DocumentURI(p.URI), + Range: rng, + }, nil + } + } + return protocol.Location{}, + bug.Errorf("internal error: token.Pos not within package") } - var rawDiagnostics []analysis.Diagnostic + + // Now run the (pkg, analyzer) action. + var diagnostics []gobDiagnostic pass := &analysis.Pass{ Analyzer: analyzer, - Fset: pkg.FileSet(), - Files: syntax, - Pkg: pkg.GetTypes(), - TypesInfo: pkg.GetTypesInfo(), - TypesSizes: pkg.GetTypesSizes(), + Fset: pkg.fset, + Files: pkg.files, + Pkg: pkg.types, + TypesInfo: pkg.typesInfo, + TypesSizes: pkg.typesSizes, TypeErrors: pkg.typeErrors, ResultOf: inputs, Report: func(d analysis.Diagnostic) { @@ -302,161 +1058,78 @@ func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, a } else { d.Category = analyzer.Name + "." + d.Category } - rawDiagnostics = append(rawDiagnostics, d) - }, - ImportObjectFact: func(obj types.Object, ptr analysis.Fact) bool { - if obj == nil { - panic("nil object") - } - key := objectFactKey{obj, factType(ptr)} - if v, ok := objectFacts[key]; ok { - reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem()) - return true - } - return false - }, - ExportObjectFact: func(obj types.Object, fact analysis.Fact) { - if obj.Pkg() != pkg.types { - panic(fmt.Sprintf("internal error: in analysis %s of package %s: Fact.Set(%s, %T): can't set facts on objects belonging another package", - analyzer, pkg.ID(), obj, fact)) - } - key := objectFactKey{obj, factType(fact)} - objectFacts[key] = fact // clobber any existing entry - }, - ImportPackageFact: func(pkg *types.Package, ptr analysis.Fact) bool { - if pkg == nil { - panic("nil package") - } - key := packageFactKey{pkg, factType(ptr)} - if v, ok := packageFacts[key]; ok { - reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem()) - return true - } - return false - }, - ExportPackageFact: func(fact analysis.Fact) { - key := packageFactKey{pkg.types, factType(fact)} - packageFacts[key] = fact // clobber any existing entry - }, - AllObjectFacts: func() []analysis.ObjectFact { - facts := make([]analysis.ObjectFact, 0, len(objectFacts)) - for k := range objectFacts { - facts = append(facts, analysis.ObjectFact{Object: k.obj, Fact: objectFacts[k]}) - } - return facts - }, - AllPackageFacts: func() []analysis.PackageFact { - facts := make([]analysis.PackageFact, 0, len(packageFacts)) - for k := range packageFacts { - facts = append(facts, analysis.PackageFact{Package: k.pkg, Fact: packageFacts[k]}) + diagnostic, err := toGobDiagnostic(posToLocation, d) + if err != nil { + bug.Reportf("internal error converting diagnostic from analyzer %s: %v", analyzer.Name, err) + return } - return facts + diagnostics = append(diagnostics, diagnostic) }, - } - - if (pkg.HasListOrParseErrors() || pkg.HasTypeErrors()) && !analyzer.RunDespiteErrors { - return nil, fmt.Errorf("skipping analysis %s because package %s contains errors", analyzer.Name, pkg.ID()) + ImportObjectFact: factset.ImportObjectFact, + ExportObjectFact: factset.ExportObjectFact, + ImportPackageFact: factset.ImportPackageFact, + ExportPackageFact: factset.ExportPackageFact, + AllObjectFacts: func() []analysis.ObjectFact { return factset.AllObjectFacts(factFilter) }, + AllPackageFacts: func() []analysis.PackageFact { return factset.AllPackageFacts(factFilter) }, } // Recover from panics (only) within the analyzer logic. // (Use an anonymous function to limit the recover scope.) var result interface{} - var err error func() { defer func() { if r := recover(); r != nil { - // An Analyzer crashed. This is often merely a symptom - // of a problem in package loading. + // An Analyzer panicked, likely due to a bug. // - // We believe that CL 420538 may have fixed these crashes, so enable - // strict checks in tests. + // In general we want to discover and fix such panics quickly, + // so we don't suppress them, but some bugs in third-party + // analyzers cannot be quickly fixed, so we use an allowlist + // to suppress panics. const strict = true - if strict && bug.PanicOnBugs && analyzer.Name != "fact_purity" { - // During testing, crash. See issues 54762, 56035. - // But ignore analyzers with known crash bugs: - // - fact_purity (dominikh/go-tools#1327) - debug.SetTraceback("all") // show all goroutines + if strict && bug.PanicOnBugs && + analyzer.Name != "buildir" { // see https://github.com/dominikh/go-tools/issues/1343 + // Uncomment this when debugging suspected failures + // in the driver, not the analyzer. + if false { + debug.SetTraceback("all") // show all goroutines + } panic(r) } else { // In production, suppress the panic and press on. - err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) + err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pass.Pkg.Path(), r) } } }() result, err = pass.Analyzer.Run(pass) }() if err != nil { - return nil, err + return nil, nil, err } if got, want := reflect.TypeOf(result), pass.Analyzer.ResultType; got != want { - return nil, fmt.Errorf( + return nil, nil, bug.Errorf( "internal error: on package %s, analyzer %s returned a result of type %v, but declared ResultType %v", pass.Pkg.Path(), pass.Analyzer, got, want) } - // disallow calls after Run + // Disallow Export*Fact calls after Run. + // (A panic means the Analyzer is abusing concurrency.) pass.ExportObjectFact = func(obj types.Object, fact analysis.Fact) { - panic(fmt.Sprintf("%s:%s: Pass.ExportObjectFact(%s, %T) called after Run", analyzer.Name, pkg.PkgPath(), obj, fact)) + panic(fmt.Sprintf("%v: Pass.ExportObjectFact(%s, %T) called after Run", act, obj, fact)) } pass.ExportPackageFact = func(fact analysis.Fact) { - panic(fmt.Sprintf("%s:%s: Pass.ExportPackageFact(%T) called after Run", analyzer.Name, pkg.PkgPath(), fact)) + panic(fmt.Sprintf("%v: Pass.ExportPackageFact(%T) called after Run", act, fact)) } - // Filter out facts related to objects that are irrelevant downstream - // (equivalently: not in the compiler export data). - for key := range objectFacts { - if !exportedFrom(key.obj, pkg.types) { - delete(objectFacts, key) - } - } - // TODO: filter out facts that belong to packages not - // mentioned in the export data to prevent side channels. - - var diagnostics []*source.Diagnostic - for _, diag := range rawDiagnostics { - srcDiags, err := analysisDiagnosticDiagnostics(snapshot.view.Options(), pkg, analyzer, &diag) - if err != nil { - event.Error(ctx, "unable to compute analysis error position", err, tag.Category.Of(diag.Category), tag.Package.Of(string(pkg.ID()))) - continue - } - diagnostics = append(diagnostics, srcDiags...) - } - return &actionData{ - analyzer: analyzer, - pkgTypes: pkg.types, - diagnostics: diagnostics, - result: result, - objectFacts: objectFacts, - packageFacts: packageFacts, + factsdata := factset.Encode() + return result, &actionSummary{ + Diagnostics: diagnostics, + Facts: factsdata, + FactsHash: source.HashOf(factsdata), }, nil } -// exportedFrom reports whether obj may be visible to a package that imports pkg. -// This includes not just the exported members of pkg, but also unexported -// constants, types, fields, and methods, perhaps belonging to other packages, -// that find there way into the API. -// This is an overapproximation of the more accurate approach used by -// gc export data, which walks the type graph, but it's much simpler. -// -// TODO(adonovan): do more accurate filtering by walking the type graph. -func exportedFrom(obj types.Object, pkg *types.Package) bool { - switch obj := obj.(type) { - case *types.Func: - return obj.Exported() && obj.Pkg() == pkg || - obj.Type().(*types.Signature).Recv() != nil - case *types.Var: - return obj.Exported() && obj.Pkg() == pkg || - obj.IsField() - case *types.TypeName: - return true - case *types.Const: - return obj.Exported() && obj.Pkg() == pkg - } - return false // Nil, Builtin, Label, or PkgName -} - func factType(fact analysis.Fact) reflect.Type { t := reflect.TypeOf(fact) if t.Kind() != reflect.Ptr { @@ -464,3 +1137,107 @@ func factType(fact analysis.Fact) reflect.Type { } return t } + +// requiredAnalyzers returns the transitive closure of required analyzers in preorder. +func requiredAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer { + var result []*analysis.Analyzer + seen := make(map[*analysis.Analyzer]bool) + var visitAll func([]*analysis.Analyzer) + visitAll = func(analyzers []*analysis.Analyzer) { + for _, a := range analyzers { + if !seen[a] { + seen[a] = true + result = append(result, a) + visitAll(a.Requires) + } + } + } + visitAll(analyzers) + return result +} + +func mustEncode(x interface{}) []byte { + var buf bytes.Buffer + if err := gob.NewEncoder(&buf).Encode(x); err != nil { + log.Fatalf("internal error encoding %T: %v", x, err) + } + return buf.Bytes() +} + +func mustDecode(data []byte, ptr interface{}) { + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(ptr); err != nil { + log.Fatalf("internal error decoding %T: %v", ptr, err) + } +} + +// -- data types for serialization of analysis.Diagnostic -- + +type gobDiagnostic struct { + Location protocol.Location + Category string + Message string + SuggestedFixes []gobSuggestedFix + Related []gobRelatedInformation +} + +type gobRelatedInformation struct { + Location protocol.Location + Message string +} + +type gobSuggestedFix struct { + Message string + TextEdits []gobTextEdit +} + +type gobTextEdit struct { + Location protocol.Location + NewText []byte +} + +// toGobDiagnostic converts an analysis.Diagnosic to a serializable gobDiagnostic, +// which requires expanding token.Pos positions into protocol.Location form. +func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location, error), diag analysis.Diagnostic) (gobDiagnostic, error) { + var fixes []gobSuggestedFix + for _, fix := range diag.SuggestedFixes { + var gobEdits []gobTextEdit + for _, textEdit := range fix.TextEdits { + loc, err := posToLocation(textEdit.Pos, textEdit.End) + if err != nil { + return gobDiagnostic{}, fmt.Errorf("in SuggestedFixes: %w", err) + } + gobEdits = append(gobEdits, gobTextEdit{ + Location: loc, + NewText: textEdit.NewText, + }) + } + fixes = append(fixes, gobSuggestedFix{ + Message: fix.Message, + TextEdits: gobEdits, + }) + } + + var related []gobRelatedInformation + for _, r := range diag.Related { + loc, err := posToLocation(r.Pos, r.End) + if err != nil { + return gobDiagnostic{}, fmt.Errorf("in Related: %w", err) + } + related = append(related, gobRelatedInformation{ + Location: loc, + Message: r.Message, + }) + } + + loc, err := posToLocation(diag.Pos, diag.End) + if err != nil { + return gobDiagnostic{}, err + } + return gobDiagnostic{ + Location: loc, + Category: diag.Category, + Message: diag.Message, + Related: related, + SuggestedFixes: fixes, + }, nil +} diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 550c736c560..1661a8c7da8 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -4,16 +4,20 @@ package cache +// This file defines routines to convert diagnostics from go list, go +// get, go/packages, parsing, type checking, and analysis into +// source.Diagnostic form, and suggesting quick fixes. + import ( "fmt" "go/scanner" "go/token" "go/types" + "log" "regexp" "strconv" "strings" - "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -192,76 +196,47 @@ func editGoDirectiveQuickFix(snapshot *snapshot, uri span.URI, version string) ( return []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, nil } -func analysisDiagnosticDiagnostics(options *source.Options, pkg *pkg, a *analysis.Analyzer, e *analysis.Diagnostic) ([]*source.Diagnostic, error) { - var srcAnalyzer *source.Analyzer - // Find the analyzer that generated this diagnostic. - for _, sa := range source.EnabledAnalyzers(options) { - if a == sa.Analyzer { - srcAnalyzer = sa - break - } - } - tokFile := pkg.fset.File(e.Pos) - if tokFile == nil { - return nil, bug.Errorf("no file for position of %q diagnostic", e.Category) - } - end := e.End - if !end.IsValid() { - end = e.Pos - } - spn, err := span.NewRange(tokFile, e.Pos, end).Span() - if err != nil { - return nil, err - } - rng, err := spanToRange(pkg, spn) - if err != nil { - return nil, err - } +// toSourceDiagnostic converts a gobDiagnostic to "source" form. +func toSourceDiagnostic(srcAnalyzer *source.Analyzer, gobDiag *gobDiagnostic) *source.Diagnostic { kinds := srcAnalyzer.ActionKind if len(srcAnalyzer.ActionKind) == 0 { kinds = append(kinds, protocol.QuickFix) } - fixes, err := suggestedAnalysisFixes(pkg, e, kinds) - if err != nil { - return nil, err - } + fixes := suggestedAnalysisFixes(gobDiag, kinds) if srcAnalyzer.Fix != "" { - cmd, err := command.NewApplyFixCommand(e.Message, command.ApplyFixArgs{ - URI: protocol.URIFromSpanURI(spn.URI()), - Range: rng, + cmd, err := command.NewApplyFixCommand(gobDiag.Message, command.ApplyFixArgs{ + URI: gobDiag.Location.URI, + Range: gobDiag.Location.Range, Fix: srcAnalyzer.Fix, }) if err != nil { - return nil, err + // JSON marshalling of these argument values cannot fail. + log.Fatalf("internal error in NewApplyFixCommand: %v", err) } for _, kind := range kinds { fixes = append(fixes, source.SuggestedFixFromCommand(cmd, kind)) } } - related, err := relatedInformation(pkg, e) - if err != nil { - return nil, err - } severity := srcAnalyzer.Severity if severity == 0 { severity = protocol.SeverityWarning } diag := &source.Diagnostic{ - URI: spn.URI(), - Range: rng, + // TODO(adonovan): is this sound? See dual conversion in posToLocation. + URI: span.URI(gobDiag.Location.URI), + Range: gobDiag.Location.Range, Severity: severity, - Source: source.AnalyzerErrorKind(e.Category), - Message: e.Message, - Related: related, + Source: source.AnalyzerErrorKind(gobDiag.Category), + Message: gobDiag.Message, + Related: relatedInformation(gobDiag), SuggestedFixes: fixes, - Analyzer: srcAnalyzer, } // If the fixes only delete code, assume that the diagnostic is reporting dead code. if onlyDeletions(fixes) { diag.Tags = []protocol.DiagnosticTag{protocol.Unnecessary} } - return []*source.Diagnostic{diag}, nil + return diag } // onlyDeletions returns true if all of the suggested fixes are deletions. @@ -289,29 +264,14 @@ func typesCodeHref(snapshot *snapshot, code typesinternal.ErrorCode) string { return source.BuildLink(target, "golang.org/x/tools/internal/typesinternal", code.String()) } -func suggestedAnalysisFixes(pkg *pkg, diag *analysis.Diagnostic, kinds []protocol.CodeActionKind) ([]source.SuggestedFix, error) { +func suggestedAnalysisFixes(diag *gobDiagnostic, kinds []protocol.CodeActionKind) []source.SuggestedFix { var fixes []source.SuggestedFix for _, fix := range diag.SuggestedFixes { edits := make(map[span.URI][]protocol.TextEdit) for _, e := range fix.TextEdits { - tokFile := pkg.fset.File(e.Pos) - if tokFile == nil { - return nil, bug.Errorf("no file for edit position") - } - end := e.End - if !end.IsValid() { - end = e.Pos - } - spn, err := span.NewRange(tokFile, e.Pos, end).Span() - if err != nil { - return nil, err - } - rng, err := spanToRange(pkg, spn) - if err != nil { - return nil, err - } - edits[spn.URI()] = append(edits[spn.URI()], protocol.TextEdit{ - Range: rng, + uri := span.URI(e.Location.URI) + edits[uri] = append(edits[uri], protocol.TextEdit{ + Range: e.Location.Range, NewText: string(e.NewText), }) } @@ -324,35 +284,19 @@ func suggestedAnalysisFixes(pkg *pkg, diag *analysis.Diagnostic, kinds []protoco } } - return fixes, nil + return fixes } -func relatedInformation(pkg *pkg, diag *analysis.Diagnostic) ([]source.RelatedInformation, error) { +func relatedInformation(diag *gobDiagnostic) []source.RelatedInformation { var out []source.RelatedInformation for _, related := range diag.Related { - tokFile := pkg.fset.File(related.Pos) - if tokFile == nil { - return nil, bug.Errorf("no file for %q diagnostic position", diag.Category) - } - end := related.End - if !end.IsValid() { - end = related.Pos - } - spn, err := span.NewRange(tokFile, related.Pos, end).Span() - if err != nil { - return nil, err - } - rng, err := spanToRange(pkg, spn) - if err != nil { - return nil, err - } out = append(out, source.RelatedInformation{ - URI: spn.URI(), - Range: rng, + URI: span.URI(related.Location.URI), + Range: related.Location.Range, Message: related.Message, }) } - return out, nil + return out } func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Span, error) { diff --git a/gopls/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go index edb8d168f24..5cbcaf78a97 100644 --- a/gopls/internal/lsp/cache/maps.go +++ b/gopls/internal/lsp/cache/maps.go @@ -5,6 +5,8 @@ package cache import ( + "strings" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/persistent" @@ -205,12 +207,12 @@ func (s knownDirsSet) Remove(key span.URI) { s.impl.Delete(key) } -// actionKeyLessInterface is the less-than relation for actionKey +// analysisKeyLessInterface is the less-than relation for analysisKey // values wrapped in an interface. -func actionKeyLessInterface(a, b interface{}) bool { - x, y := a.(actionKey), b.(actionKey) - if x.analyzer.Name != y.analyzer.Name { - return x.analyzer.Name < y.analyzer.Name +func analysisKeyLessInterface(a, b interface{}) bool { + x, y := a.(analysisKey), b.(analysisKey) + if cmp := strings.Compare(x.analyzerNames, y.analyzerNames); cmp != 0 { + return cmp < 0 } return x.pkgid < y.pkgid } diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index 33ca98c6822..83f18dabee4 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -43,6 +43,10 @@ type parseKey struct { // which is not safe for values from a shared cache. // TODO(adonovan): opt: shouldn't parseGoImpl do the trimming? // Then we can cache the result since it would never change. +// +// TODO(adonovan): in the absence of any way to add existing an +// token.File to a new FileSet (see go.dev/issue/53200), caching ASTs +// implies a global FileSet. func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { if mode == source.ParseExported { panic("only type checking should use Exported") @@ -168,7 +172,7 @@ func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, // If there were parse errors, attempt to fix them up. if parseErr != nil { // Fix any badly parsed parts of the AST. - fixed = fixAST(ctx, file, tok, src) + fixed = fixAST(file, tok, src) for i := 0; i < 10; i++ { // Fix certain syntax errors that render the file unparseable. @@ -192,7 +196,7 @@ func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, src = newSrc tok = fset.File(file.Pos()) - fixed = fixAST(ctx, file, tok, src) + fixed = fixAST(file, tok, src) } } } @@ -552,14 +556,14 @@ func arrayLength(array *ast.CompositeLit) (int, bool) { // // If fixAST returns true, the resulting AST is considered "fixed", meaning // positions have been mangled, and type checker errors may not make sense. -func fixAST(ctx context.Context, n ast.Node, tok *token.File, src []byte) (fixed bool) { +func fixAST(n ast.Node, tok *token.File, src []byte) (fixed bool) { var err error walkASTWithParent(n, func(n, parent ast.Node) bool { switch n := n.(type) { case *ast.BadStmt: if fixed = fixDeferOrGoStmt(n, parent, tok, src); fixed { // Recursively fix in our fixed node. - _ = fixAST(ctx, parent, tok, src) + _ = fixAST(parent, tok, src) } else { err = fmt.Errorf("unable to parse defer or go from *ast.BadStmt: %v", err) } @@ -567,7 +571,7 @@ func fixAST(ctx context.Context, n ast.Node, tok *token.File, src []byte) (fixed case *ast.BadExpr: if fixed = fixArrayType(n, parent, tok, src); fixed { // Recursively fix in our fixed node. - _ = fixAST(ctx, parent, tok, src) + _ = fixAST(parent, tok, src) return false } diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index cb2570e8d46..4e9b7820608 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -272,7 +272,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, parsedGoFiles: persistent.NewMap(parseKeyLessInterface), parseKeysByURI: newParseKeysByURIMap(), symbolizeHandles: persistent.NewMap(uriLessInterface), - actions: persistent.NewMap(actionKeyLessInterface), + analyses: persistent.NewMap(analysisKeyLessInterface), workspacePackages: make(map[PackageID]PackagePath), unloadableFiles: make(map[span.URI]struct{}), parseModHandles: persistent.NewMap(uriLessInterface), diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 1818d0e2e0f..ea57d574f57 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -109,9 +109,10 @@ type snapshot struct { // It may be invalidated when metadata changes or a new file is opened or closed. isActivePackageCache isActivePackageCacheMap - // actions maps an actionKey to the handle for the future - // result of execution an analysis pass on a package. - actions *persistent.Map // from actionKey to *actionHandle + // analyses maps an analysisKey (which identifies a package + // and a set of analyzers) to the handle for the future result + // of loading the package and analyzing it. + analyses *persistent.Map // from analysisKey to analysisHandle // workspacePackages contains the workspace's packages, which are loaded // when the view is created. @@ -222,7 +223,7 @@ func (s *snapshot) destroy(destroyedBy string) { s.packages.Destroy() s.isActivePackageCache.Destroy() - s.actions.Destroy() + s.analyses.Destroy() s.files.Destroy() s.parsedGoFiles.Destroy() s.parseKeysByURI.Destroy() @@ -835,12 +836,6 @@ func (s *snapshot) ReverseDependencies(ctx context.Context, id PackageID) (map[P return rdeps, nil } -func (s *snapshot) getImportedBy(id PackageID) []PackageID { - s.mu.Lock() - defer s.mu.Unlock() - return s.meta.importedBy[id] -} - func (s *snapshot) workspaceMetadata() (meta []*source.Metadata) { s.mu.Lock() defer s.mu.Unlock() @@ -1667,7 +1662,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC initializedErr: s.initializedErr, packages: s.packages.Clone(), isActivePackageCache: s.isActivePackageCache.Clone(), - actions: s.actions.Clone(), + analyses: s.analyses.Clone(), files: s.files.Clone(), parsedGoFiles: s.parsedGoFiles.Clone(), parseKeysByURI: s.parseKeysByURI.Clone(), @@ -1882,15 +1877,15 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } // Delete invalidated analysis actions. - var actionsToDelete []actionKey - result.actions.Range(func(k, _ interface{}) { - key := k.(actionKey) + var actionsToDelete []analysisKey + result.analyses.Range(func(k, _ interface{}) { + key := k.(analysisKey) if _, ok := idsToInvalidate[key.pkgid]; ok { actionsToDelete = append(actionsToDelete, key) } }) for _, key := range actionsToDelete { - result.actions.Delete(key) + result.analyses.Delete(key) } // If a file has been deleted, we must delete metadata for all packages diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 445a81914b3..402553b4264 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -149,11 +149,12 @@ func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.U if delay > 0 { // 2-phase diagnostics. // - // The first phase just parses and checks packages that have been - // affected by file modifications (no analysis). + // The first phase just parses and type-checks (but + // does not analyze) packages directly affected by + // file modifications. // - // The second phase does everything, and is debounced by the configured - // delay. + // The second phase runs analysis on the entire snapshot, + // and is debounced by the configured delay. s.diagnoseChangedFiles(ctx, snapshot, changedURIs, onDisk) s.publishDiagnostics(ctx, false, snapshot) @@ -323,6 +324,11 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn // Run go/analysis diagnosis of packages in parallel. // TODO(adonovan): opt: it may be more efficient to // have diagnosePkg take a set of packages. + // + // TODO(adonovan): opt: since the new analysis driver does its + // own type checking, we could strength-reduce pkg to + // PackageID and get this step started as soon as the set of + // active package IDs are known, without waiting for them to load. var ( wg sync.WaitGroup seen = map[span.URI]struct{}{} diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go new file mode 100644 index 00000000000..838a99e6202 --- /dev/null +++ b/gopls/internal/lsp/filecache/filecache.go @@ -0,0 +1,272 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The filecache package provides a file-based shared durable blob cache. +// +// The cache is a machine-global mapping from (kind string, key +// [32]byte) to []byte, where kind is an identifier describing the +// namespace or purpose (e.g. "analysis"), and key is a SHA-256 digest +// of the recipe of the value. (It need not be the digest of the value +// itself, so you can query the cache without knowing what value the +// recipe would produce.) +// +// The space budget of the cache can be controlled by [SetBudget]. +// Cache entries may be evicted at any time or in any order. +// +// The Get and Set operations are concurrency-safe. +package filecache + +import ( + "crypto/sha256" + "errors" + "fmt" + "io" + "log" + "math/rand" + "os" + "path/filepath" + "sort" + "sync" + "sync/atomic" + "time" + + "golang.org/x/tools/gopls/internal/robustio" +) + +// Get retrieves from the cache and returns a newly allocated +// copy of the value most recently supplied to Set(kind, key), +// possibly by another process. +// Get returns ErrNotFound if the value was not found. +func Get(kind string, key [32]byte) ([]byte, error) { + name := filename(kind, key) + data, err := robustio.ReadFile(name) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, ErrNotFound + } + return nil, err + } + + // Update file time for use by LRU eviction. + // (This turns every read into a write operation. + // If this is a performance problem, we should + // touch the files aynchronously.) + now := time.Now() + if err := setFileTime(name, now, now); err != nil { + return nil, fmt.Errorf("failed to update access time: %w", err) + } + + return data, nil +} + +// ErrNotFound is the distinguished error +// returned by Get when the key is not found. +var ErrNotFound = fmt.Errorf("not found") + +// Set updates the value in the cache. +func Set(kind string, key [32]byte, value []byte) error { + name := filename(kind, key) + if err := os.MkdirAll(filepath.Dir(name), 0700); err != nil { + return err + } + + // The sequence below uses rename to achieve atomic cache + // updates even with concurrent processes. + var cause error + for try := 0; try < 3; try++ { + tmpname := fmt.Sprintf("%s.tmp.%d", name, rand.Int()) + tmp, err := os.OpenFile(tmpname, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + if os.IsExist(err) { + // Create raced with another thread (or stale file). + // Try again. + cause = err + continue + } + return err + } + + _, err = tmp.Write(value) + if closeErr := tmp.Close(); err == nil { + err = closeErr // prefer error from write over close + } + if err != nil { + os.Remove(tmp.Name()) // ignore error + return err + } + + err = robustio.Rename(tmp.Name(), name) + if err == nil { + return nil // success + } + cause = err + + // Rename raced with another thread. Try again. + os.Remove(tmp.Name()) // ignore error + } + return cause +} + +var budget int64 = 1e9 // 1GB + +// SetBudget sets a soft limit on disk usage of the cache (in bytes) +// and returns the previous value. Supplying a negative value queries +// the current value without changing it. +// +// If two gopls processes have different budgets, the one with the +// lower budget will collect garbage more actively, but both will +// observe the effect. +func SetBudget(new int64) (old int64) { + if new < 0 { + return atomic.LoadInt64(&budget) + } + return atomic.SwapInt64(&budget, new) +} + +// --- implementation ---- + +// filename returns the cache entry of the specified kind and key. +// +// A typical cache entry is a file name such as: +// +// $HOME/Library/Caches / gopls / VVVVVVVV / kind / KK / KKKK...KKKK +// +// The portions separated by spaces are as follows: +// - The user's preferred cache directory; the default value varies by OS. +// - The constant "gopls". +// - The "version", 32 bits of the digest of the gopls executable. +// - The kind or purpose of this cache subtree (e.g. "analysis"). +// - The first 8 bits of the key, to avoid huge directories. +// - The full 256 bits of the key. +// +// Once a file is written its contents are never modified, though it +// may be atomically replaced or removed. +// +// New versions of gopls are free to reorganize the contents of the +// version directory as needs evolve. But all versions of gopls must +// in perpetuity treat the "gopls" directory in a common fashion. +// +// In particular, each gopls process attempts to garbage collect +// the entire gopls directory so that newer binaries can clean up +// after older ones: in the development cycle especially, new +// new versions may be created frequently. +func filename(kind string, key [32]byte) string { + hex := fmt.Sprintf("%x", key) + return filepath.Join(getCacheDir(), kind, hex[:2], hex) +} + +// getCacheDir returns the persistent cache directory of all processes +// running this version of the gopls executable. +// +// It must incorporate the hash of the executable so that we needn't +// worry about incompatible changes to the file format or changes to +// the algorithm that produced the index. +func getCacheDir() string { + cacheDirOnce.Do(func() { + // Use user's preferred cache directory. + userDir := os.Getenv("GOPLS_CACHE") + if userDir == "" { + var err error + userDir, err = os.UserCacheDir() + if err != nil { + userDir = os.TempDir() + } + } + goplsDir := filepath.Join(userDir, "gopls") + + // Start the garbage collector. + go gc(goplsDir) + + // Compute the hash of this executable (~20ms) and create a subdirectory. + hash, err := hashExecutable() + if err != nil { + log.Fatalf("can't hash gopls executable: %v", err) + } + // Use only 32 bits of the digest to avoid unwieldy filenames. + // It's not an adversarial situation. + cacheDir = filepath.Join(goplsDir, fmt.Sprintf("%x", hash[:4])) + if err := os.MkdirAll(cacheDir, 0700); err != nil { + log.Fatalf("can't create cache: %v", err) + } + }) + return cacheDir +} + +var ( + cacheDirOnce sync.Once + cacheDir string // only accessed by getCacheDir +) + +func hashExecutable() (hash [32]byte, err error) { + exe, err := os.Executable() + if err != nil { + return hash, err + } + f, err := os.Open(exe) + if err != nil { + return hash, err + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return hash, fmt.Errorf("can't read executable: %w", err) + } + h.Sum(hash[:0]) + return hash, nil +} + +// gc runs forever, periodically deleting files from the gopls +// directory until the space budget is no longer exceeded, and also +// deleting files older than the maximum age, regardless of budget. +// +// One gopls process may delete garbage created by a different gopls +// process, possibly running a different version of gopls, possibly +// running concurrently. +func gc(goplsDir string) { + const period = 1 * time.Minute // period between collections + const statDelay = 1 * time.Millisecond // delay between stats to smooth out I/O + const maxAge = 7 * 24 * time.Hour // max time since last access before file is deleted + + for { + // Enumerate all files in the cache. + type item struct { + path string + stat os.FileInfo + } + var files []item + var total int64 // bytes + _ = filepath.Walk(goplsDir, func(path string, stat os.FileInfo, err error) error { + // TODO(adonovan): opt: also collect empty directories, + // as they typically occupy around 1KB. + if err == nil && !stat.IsDir() { + files = append(files, item{path, stat}) + total += stat.Size() + time.Sleep(statDelay) + } + return nil + }) + + // Sort oldest files first. + sort.Slice(files, func(i, j int) bool { + return files[i].stat.ModTime().Before(files[j].stat.ModTime()) + }) + + // Delete oldest files until we're under budget. + // Unconditionally delete files we haven't used in ages. + budget := atomic.LoadInt64(&budget) + for _, file := range files { + age := time.Since(file.stat.ModTime()) + if total > budget || age > maxAge { + if false { // debugging + log.Printf("deleting stale file %s (%dB, age %v)", + file.path, file.stat.Size(), age) + } + os.Remove(filepath.Join(goplsDir, file.path)) // ignore error + total -= file.stat.Size() + } + } + + time.Sleep(period) + } +} diff --git a/gopls/internal/lsp/filecache/filecache_test.go b/gopls/internal/lsp/filecache/filecache_test.go new file mode 100644 index 00000000000..d1947b781e6 --- /dev/null +++ b/gopls/internal/lsp/filecache/filecache_test.go @@ -0,0 +1,189 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filecache_test + +// This file defines tests of the API of the filecache package. +// +// Some properties (e.g. garbage collection) cannot be exercised +// through the API, so this test does not attempt to do so. + +import ( + "bytes" + cryptorand "crypto/rand" + "fmt" + "log" + mathrand "math/rand" + "os" + "os/exec" + "strconv" + "testing" + + "golang.org/x/sync/errgroup" + "golang.org/x/tools/gopls/internal/lsp/filecache" +) + +func TestBasics(t *testing.T) { + const kind = "TestBasics" + key := uniqueKey() // never used before + value := []byte("hello") + + // Get of a never-seen key returns not found. + if _, err := filecache.Get(kind, key); err != filecache.ErrNotFound { + t.Errorf("Get of random key returned err=%q, want not found", err) + } + + // Set of a never-seen key and a small value succeeds. + if err := filecache.Set(kind, key, value); err != nil { + t.Errorf("Set failed: %v", err) + } + + // Get of the key returns a copy of the value. + if got, err := filecache.Get(kind, key); err != nil { + t.Errorf("Get after Set failed: %v", err) + } else if string(got) != string(value) { + t.Errorf("Get after Set returned different value: got %q, want %q", got, value) + } + + // The kind is effectively part of the key. + if _, err := filecache.Get("different-kind", key); err != filecache.ErrNotFound { + t.Errorf("Get with wrong kind returned err=%q, want not found", err) + } +} + +// TestConcurrency exercises concurrent access to the same entry. +func TestConcurrency(t *testing.T) { + const kind = "TestConcurrency" + key := uniqueKey() + const N = 100 // concurrency level + + // Construct N distinct values, each larger + // than a typical 4KB OS file buffer page. + var values [N][8192]byte + for i := range values { + if _, err := mathrand.Read(values[i][:]); err != nil { + t.Fatalf("rand: %v", err) + } + } + + // get calls Get and verifies that the cache entry + // matches one of the values passed to Set. + get := func(mustBeFound bool) error { + got, err := filecache.Get(kind, key) + if err != nil { + if err == filecache.ErrNotFound && !mustBeFound { + return nil // not found + } + return err + } + for _, want := range values { + if bytes.Equal(want[:], got) { + return nil // a match + } + } + return fmt.Errorf("Get returned a value that was never Set") + } + + // Perform N concurrent calls to Set and Get. + // All sets must succeed. + // All gets must return nothing, or one of the Set values; + // there is no third possibility. + var group errgroup.Group + for i := range values { + i := i + group.Go(func() error { return filecache.Set(kind, key, values[i][:]) }) + group.Go(func() error { return get(false) }) + } + if err := group.Wait(); err != nil { + t.Fatal(err) + } + + // A final Get must report one of the values that was Set. + if err := get(true); err != nil { + t.Fatalf("final Get failed: %v", err) + } +} + +const ( + testIPCKind = "TestIPC" + testIPCValueA = "hello" + testIPCValueB = "world" +) + +// TestIPC exercises interprocess communication through the cache. +// It calls Set(A) in the parent, { Get(A); Set(B) } in the child +// process, then Get(B) in the parent. +func TestIPC(t *testing.T) { + keyA := uniqueKey() + keyB := uniqueKey() + value := []byte(testIPCValueA) + + // Set keyA. + if err := filecache.Set(testIPCKind, keyA, value); err != nil { + t.Fatalf("Set: %v", err) + } + + // Call ipcChild in a child process, + // passing it the keys in the environment + // (quoted, to avoid NUL termination of C strings). + // It will Get(A) then Set(B). + cmd := exec.Command(os.Args[0], os.Args[1:]...) + cmd.Env = append(os.Environ(), + "ENTRYPOINT=ipcChild", + fmt.Sprintf("KEYA=%q", keyA), + fmt.Sprintf("KEYB=%q", keyB)) + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + + // Verify keyB. + got, err := filecache.Get(testIPCKind, keyB) + if err != nil { + t.Fatal(err) + } + if string(got) != "world" { + t.Fatalf("Get(keyB) = %q, want %q", got, "world") + } +} + +// We define our own main function so that portions of +// some tests can run in a separate (child) process. +func TestMain(m *testing.M) { + switch os.Getenv("ENTRYPOINT") { + case "ipcChild": + ipcChild() + default: + os.Exit(m.Run()) + } +} + +// ipcChild is the portion of TestIPC that runs in a child process. +func ipcChild() { + getenv := func(name string) (key [32]byte) { + s, _ := strconv.Unquote(os.Getenv(name)) + copy(key[:], []byte(s)) + return + } + + // Verify key A. + got, err := filecache.Get(testIPCKind, getenv("KEYA")) + if err != nil || string(got) != testIPCValueA { + log.Fatalf("child: Get(key) = %q, %v; want %q", got, err, testIPCValueA) + } + + // Set key B. + if err := filecache.Set(testIPCKind, getenv("KEYB"), []byte(testIPCValueB)); err != nil { + log.Fatalf("child: Set(keyB) failed: %v", err) + } +} + +// uniqueKey returns a key that has never been used before. +func uniqueKey() (key [32]byte) { + if _, err := cryptorand.Read(key[:]); err != nil { + log.Fatalf("rand: %v", err) + } + return +} diff --git a/gopls/internal/lsp/filecache/setfiletime_unix.go b/gopls/internal/lsp/filecache/setfiletime_unix.go new file mode 100644 index 00000000000..f4f25f5121e --- /dev/null +++ b/gopls/internal/lsp/filecache/setfiletime_unix.go @@ -0,0 +1,28 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !windows +// +build !windows + +// TODO(adonovan): use 'unix' tag when we can rely on newer go command. + +package filecache + +import ( + "syscall" + "time" +) + +// setFileTime updates the access and modification times of a file. +// +// (Traditionally the access time would be updated automatically, but +// for efficiency most POSIX systems have for many years set the +// noatime mount option to avoid every open or read operation +// entailing a metadata write.) +func setFileTime(filename string, atime, mtime time.Time) error { + return syscall.Utimes(filename, []syscall.Timeval{ + syscall.NsecToTimeval(atime.UnixNano()), + syscall.NsecToTimeval(mtime.UnixNano()), + }) +} diff --git a/gopls/internal/lsp/filecache/setfiletime_windows.go b/gopls/internal/lsp/filecache/setfiletime_windows.go new file mode 100644 index 00000000000..72a63491fff --- /dev/null +++ b/gopls/internal/lsp/filecache/setfiletime_windows.go @@ -0,0 +1,33 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows +// +build windows + +package filecache + +import ( + "syscall" + "time" +) + +// setFileTime updates the access and modification times of a file. +func setFileTime(filename string, atime, mtime time.Time) error { + // Latency of this function was measured on the builder + // at median=1.9ms 90%=6.8ms 95%=12ms. + + filename16, err := syscall.UTF16PtrFromString(filename) + if err != nil { + return err + } + // See https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfiletime + h, err := syscall.CreateFile(filename16, syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil, syscall.OPEN_EXISTING, 0, 0) + if err != nil { + return err + } + defer syscall.Close(h) // ignore error + afiletime := syscall.NsecToFiletime(atime.UnixNano()) + mfiletime := syscall.NsecToFiletime(mtime.UnixNano()) + return syscall.SetFileTime(h, nil, &afiletime, &mfiletime) +} diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index 8101a043256..8024f46cfe7 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -66,11 +66,11 @@ func Analyze(ctx context.Context, snapshot Snapshot, pkgid PackageID, includeCon // as used by the "gopls check" command. // // TODO(adonovan): factor in common with (*Server).codeAction, which -// executes { PackageForFile; DiagnosePackage; Analyze } too? +// executes { PackageForFile; Analyze } too? // // TODO(adonovan): opt: this function is called in a loop from the // "gopls/diagnoseFiles" nonstandard request handler. It would be more -// efficient to compute the set of packages and DiagnosePackage and +// efficient to compute the set of packages and TypeCheck and // Analyze them all at once. func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (VersionedFileIdentity, []*Diagnostic, error) { fh, err := snapshot.GetVersionedFile(ctx, uri) @@ -130,7 +130,6 @@ func CombineDiagnostics(pkg Package, uri span.URI, analysisDiagnostics map[span. copy := *tdiags[i] copy.SuggestedFixes = diag.SuggestedFixes copy.Tags = diag.Tags - copy.Analyzer = diag.Analyzer tdiags[i] = © continue } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index e484e3a752d..b6bab12c180 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -678,7 +678,7 @@ type Analyzer struct { func (a *Analyzer) String() string { return a.Analyzer.String() } -// Enabled reports whether this analyzer is enabled by the given options. +// IsEnabled reports whether this analyzer is enabled by the given options. func (a Analyzer) IsEnabled(options *Options) bool { // Staticcheck analyzers can only be enabled when staticcheck is on. if _, ok := options.StaticcheckAnalyzers[a.Analyzer.Name]; ok { @@ -764,7 +764,6 @@ type Diagnostic struct { // Fields below are used internally to generate quick fixes. They aren't // part of the LSP spec and don't leave the server. SuggestedFixes []SuggestedFix - Analyzer *Analyzer } func (d *Diagnostic) String() string { diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 83cd4331606..c7f091cde6a 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1994,3 +1994,111 @@ func F[T any](_ T) { ) }) } + +// This test demonstrates that analysis facts are correctly propagated +// across packages. +func TestInterpackageAnalysis(t *testing.T) { + const src = ` +-- go.mod -- +module example.com +-- a/a.go -- +package a + +import "example.com/b" + +func _() { + new(b.B).Printf("%d", "s") // printf error +} + +-- b/b.go -- +package b + +import "example.com/c" + +type B struct{} + +func (B) Printf(format string, args ...interface{}) { + c.MyPrintf(format, args...) +} + +-- c/c.go -- +package c + +import "fmt" + +func MyPrintf(format string, args ...interface{}) { + fmt.Printf(format, args...) +} +` + Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + env.AfterChange( + env.DiagnosticAtRegexpWithMessage("a/a.go", "new.*Printf", + "format %d has arg \"s\" of wrong type string")) + }) +} + +// This test ensures that only Analyzers with RunDespiteErrors=true +// are invoked on a package that would not compile, even if the errors +// are distant and localized. +func TestErrorsThatPreventAnalysis(t *testing.T) { + const src = ` +-- go.mod -- +module example.com +-- a/a.go -- +package a + +import "fmt" +import "sync" +import _ "example.com/b" + +func _() { + // The copylocks analyzer (RunDespiteErrors, FactTypes={}) does run. + var mu sync.Mutex + mu2 := mu // copylocks error, reported + _ = &mu2 + + // The printf analyzer (!RunDespiteErrors, FactTypes!={}) does not run: + // (c, printf) failed because of type error in c + // (b, printf) and (a, printf) do not run because of failed prerequisites. + fmt.Printf("%d", "s") // printf error, unreported + + // The bools analyzer (!RunDespiteErrors, FactTypes={}) does not run: + var cond bool + _ = cond != true && cond != true // bools error, unreported +} + +-- b/b.go -- +package b + +import _ "example.com/c" + +-- c/c.go -- +package c + +var _ = 1 / "" // type error + +` + Run(t, src, func(t *testing.T, env *Env) { + var diags protocol.PublishDiagnosticsParams + env.OpenFile("a/a.go") + env.AfterChange( + env.DiagnosticAtRegexpWithMessage("a/a.go", "mu2 := (mu)", "assignment copies lock value"), + ReadDiagnostics("a/a.go", &diags)) + + // Assert that there were no other diagnostics. + // In particular: + // - "fmt.Printf" does not trigger a [printf] finding; + // - "cond != true" does not trigger a [bools] finding. + // + // We use this check in preference to NoDiagnosticAtRegexp + // as it is robust in case of minor mistakes in the position + // regexp, and because it reports unexpected diagnostics. + if got, want := len(diags.Diagnostics), 1; got != want { + t.Errorf("got %d diagnostics in a/a.go, want %d:", got, want) + for i, diag := range diags.Diagnostics { + t.Logf("Diagnostics[%d] = %+v", i, diag) + } + } + }) +} diff --git a/gopls/internal/span/uri.go b/gopls/internal/span/uri.go index 8f6b033f41a..cf2d66df20b 100644 --- a/gopls/internal/span/uri.go +++ b/gopls/internal/span/uri.go @@ -73,6 +73,8 @@ slow: return u.Path, nil } +// TODO(adonovan): document this function, and any invariants of +// span.URI that it is supposed to establish. func URIFromURI(s string) URI { if !strings.HasPrefix(s, "file://") { return URI(s) From 099260e6e72bba5287cbad3bcdc151ddf2fd27b2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 14 Dec 2022 17:56:49 -0500 Subject: [PATCH 551/723] gopls/internal/lsp: followups to dropping line directives (This change was my independent effort to remove support for line directives, which Peter already did in CL 439115. The changes were substantially similar but this one also made some follow-on simplifications.) Also, NewMappedRange(token.File, ColumnMapper, ...) no longer needs its first argument since it is redundant with ColumnMapper.TokFile. Also, inline away cache.parseGoSpan, and update various comments. Fixes golang/go#55403 Change-Id: I951043c0ece9ee0a9ce65ae174d25057b0e1255a Reviewed-on: https://go-review.googlesource.com/c/tools/+/457656 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Peter Weinberger --- .../lsp/analysis/fillstruct/fillstruct.go | 4 +- .../lsp/analysis/undeclaredname/undeclared.go | 2 +- gopls/internal/lsp/cache/check.go | 22 +++-- gopls/internal/lsp/cache/errors.go | 9 +- .../internal/lsp/command/commandmeta/meta.go | 2 +- gopls/internal/lsp/semantic.go | 15 +--- gopls/internal/lsp/source/call_hierarchy.go | 16 +--- gopls/internal/lsp/source/code_lens.go | 10 +-- .../internal/lsp/source/completion/package.go | 2 +- .../internal/lsp/source/completion/snippet.go | 2 +- gopls/internal/lsp/source/folding_range.go | 9 +- gopls/internal/lsp/source/hover.go | 2 +- gopls/internal/lsp/source/identifier.go | 4 +- gopls/internal/lsp/source/references.go | 4 +- gopls/internal/lsp/source/rename.go | 2 +- gopls/internal/lsp/source/util.go | 36 ++------ gopls/internal/lsp/source/util_test.go | 85 ------------------- gopls/internal/lsp/tests/tests.go | 2 +- gopls/internal/regtest/misc/failures_test.go | 37 ++++---- gopls/internal/span/token.go | 43 ++-------- internal/imports/sortimports.go | 1 + 21 files changed, 79 insertions(+), 230 deletions(-) delete mode 100644 gopls/internal/lsp/source/util_test.go diff --git a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go index 4731514eb5b..7c1e0442ded 100644 --- a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -271,8 +271,8 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast // Find the line on which the composite literal is declared. split := bytes.Split(content, []byte("\n")) - lineNumber := fset.PositionFor(expr.Lbrace, false).Line - firstLine := split[lineNumber-1] // lines are 1-indexed + lineNumber := fset.PositionFor(expr.Lbrace, false).Line // ignore line directives + firstLine := split[lineNumber-1] // lines are 1-indexed // Trim the whitespace from the left of the line, and use the index // to get the amount of whitespace on the left. diff --git a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go index e225bb6ffdb..646f4fa4786 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -146,7 +146,7 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast return nil, fmt.Errorf("could not locate insertion point") } - insertBefore := fset.PositionFor(insertBeforeStmt.Pos(), false).Offset + insertBefore := fset.PositionFor(insertBeforeStmt.Pos(), false).Offset // ignore line directives // Get the indent to add on the line after the new statement. // Since this will have a parse error, we can not use format.Source(). diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index beec6593ba6..cfac5ffb107 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -460,19 +460,17 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil } typeparams.InitInstanceInfo(pkg.typesInfo) - // In the presence of line directives, we may need to report errors in - // non-compiled Go files, so we need to register them on the package. - // However, we only need to really parse them in ParseFull mode, when - // the user might actually be looking at the file. - goMode := source.ParseFull - if mode != source.ParseFull { - goMode = source.ParseHeader - } - - // Parse the GoFiles. (These aren't presented to the type - // checker but are part of the returned pkg.) + // Parse the non-compiled GoFiles. (These aren't presented to + // the type checker but are part of the returned pkg.) // TODO(adonovan): opt: parallelize parsing. for _, fh := range goFiles { + goMode := mode + if mode == source.ParseExported { + // This package is being loaded only for type information, + // to which non-compiled Go files are irrelevant, + // so parse only the header. + goMode = source.ParseHeader + } pgf, err := snapshot.ParseGo(ctx, fh, goMode) if err != nil { return nil, err @@ -685,7 +683,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost } for _, imp := range allImports[item] { - rng, err := source.NewMappedRange(imp.cgf.Tok, imp.cgf.Mapper, imp.imp.Pos(), imp.imp.End()).Range() + rng, err := source.NewMappedRange(imp.cgf.Mapper, imp.imp.Pos(), imp.imp.End()).Range() if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 1661a8c7da8..7e685f3df4c 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -11,7 +11,6 @@ package cache import ( "fmt" "go/scanner" - "go/token" "go/types" "log" "regexp" @@ -316,7 +315,7 @@ func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Sp if fset != terr.Fset { return 0, span.Span{}, bug.Errorf("wrong FileSet for type error") } - posn := fset.PositionFor(start, false) + posn := fset.PositionFor(start, false) // ignore line directives if !posn.IsValid() { return 0, span.Span{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr) } @@ -327,17 +326,13 @@ func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Sp if !end.IsValid() || end == start { end = analysisinternal.TypeErrorEndPos(fset, pgf.Src, start) } - spn, err := parsedGoSpan(pgf, start, end) + spn, err := span.FileSpan(pgf.Mapper.TokFile, start, end) if err != nil { return 0, span.Span{}, err } return ecode, spn, nil } -func parsedGoSpan(pgf *source.ParsedGoFile, start, end token.Pos) (span.Span, error) { - return span.FileSpan(pgf.Mapper.TokFile, pgf.Mapper.TokFile, start, end) -} - // spanToRange converts a span.Span to a protocol.Range, // assuming that the span belongs to the package whose diagnostics are being computed. func spanToRange(pkg *pkg, spn span.Span) (protocol.Range, error) { diff --git a/gopls/internal/lsp/command/commandmeta/meta.go b/gopls/internal/lsp/command/commandmeta/meta.go index 00421c73f93..bf85c4faa9b 100644 --- a/gopls/internal/lsp/command/commandmeta/meta.go +++ b/gopls/internal/lsp/command/commandmeta/meta.go @@ -244,7 +244,7 @@ func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) { fset := pkg.Fset var file *ast.File for _, f := range pkg.Syntax { - if fset.PositionFor(f.Pos(), false).Filename == fset.PositionFor(pos, false).Filename { + if fset.File(f.Pos()).Name() == fset.File(pos).Name() { file = f break } diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 0d76ad548d6..ea90da17378 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -178,7 +178,6 @@ const ( ) 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 @@ -186,17 +185,11 @@ func (e *encoded) token(start token.Pos, leng int, typ tokenType, mods []string) if start >= e.end || start+token.Pos(leng) <= e.start { return } - // want a line and column from start (in LSP coordinates) - // [//line directives should be ignored] - rng := source.NewMappedRange(e.pgf.Tok, e.pgf.Mapper, start, start+token.Pos(leng)) + // want a line and column from start (in LSP coordinates). Ignore line directives. + rng := source.NewMappedRange(e.pgf.Mapper, start, start+token.Pos(leng)) lspRange, err := rng.Range() if err != nil { - // possibly a //line directive. TODO(pjw): fix this somehow - // "column mapper is for file...instead of..." - // "line is beyond end of file..." - // see line 116 of internal/span/token.go which uses Position not PositionFor - // (it is too verbose to print the error on every token. some other RPC will fail) - // event.Error(e.ctx, "failed to convert to range", err) + event.Error(e.ctx, "failed to convert to range", err) return } if lspRange.End.Line != lspRange.Start.Line { @@ -254,7 +247,7 @@ func (e *encoded) strStack() string { if !safetoken.InRange(e.pgf.Tok, loc) { msg = append(msg, fmt.Sprintf("invalid position %v for %s", loc, e.pgf.URI)) } else if safetoken.InRange(e.pgf.Tok, loc) { - add := e.pgf.Tok.PositionFor(loc, false) + add := e.pgf.Tok.PositionFor(loc, false) // ignore line directives nm := filepath.Base(add.Filename) msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", nm, add.Line, add.Column)) } else { diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index 5c5c584fd9a..b8e11bc8777 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -157,7 +157,7 @@ outer: nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos() kind = protocol.Function } - rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, nameStart, nameEnd).Range() + rng, err := NewMappedRange(pgf.Mapper, nameStart, nameEnd).Range() if err != nil { return protocol.CallHierarchyItem{}, err } @@ -201,15 +201,7 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr if len(identifier.Declaration.MappedRange) == 0 { return nil, nil } - declMappedRange := identifier.Declaration.MappedRange[0] - // TODO(adonovan): avoid Fileset.File call by somehow getting at - // declMappedRange.spanRange.TokFile, or making Identifier retain the - // token.File of the identifier and its declaration, since it looks up both anyway. - tokFile := identifier.pkg.FileSet().File(node.Pos()) - if tokFile == nil { - return nil, fmt.Errorf("no file for position") - } - callExprs, err := collectCallExpressions(tokFile, declMappedRange.m, node) + callExprs, err := collectCallExpressions(identifier.Declaration.MappedRange[0].m, node) if err != nil { return nil, err } @@ -218,7 +210,7 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr } // collectCallExpressions collects call expression ranges inside a function. -func collectCallExpressions(tokFile *token.File, mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) { +func collectCallExpressions(mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) { type callPos struct { start, end token.Pos } @@ -248,7 +240,7 @@ func collectCallExpressions(tokFile *token.File, mapper *protocol.ColumnMapper, callRanges := []protocol.Range{} for _, call := range callPositions { - callRange, err := NewMappedRange(tokFile, mapper, call.start, call.end).Range() + callRange, err := NewMappedRange(mapper, call.start, call.end).Range() if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go index cb7bbc8578b..24390086cd1 100644 --- a/gopls/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -67,7 +67,7 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p return nil, err } // add a code lens to the top of the file which runs all benchmarks in the file - rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() + rng, err := NewMappedRange(pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() if err != nil { return nil, err } @@ -111,7 +111,7 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) ( continue } - rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, fn.Pos(), fn.End()).Range() + rng, err := NewMappedRange(pgf.Mapper, fn.Pos(), fn.End()).Range() if err != nil { return out, err } @@ -177,7 +177,7 @@ func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ( if !strings.HasPrefix(l.Text, ggDirective) { continue } - rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() + rng, err := NewMappedRange(pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() if err != nil { return nil, err } @@ -214,7 +214,7 @@ func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([ if c == nil { return nil, nil } - rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, c.Pos(), c.End()).Range() + rng, err := NewMappedRange(pgf.Mapper, c.Pos(), c.End()).Range() if err != nil { return nil, err } @@ -235,7 +235,7 @@ func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle // Without a package name we have nowhere to put the codelens, so give up. return nil, nil } - rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() + rng, err := NewMappedRange(pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index 5def009a89d..65f86ca249d 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -124,7 +124,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, if cursorLine <= 0 || cursorLine > len(lines) { return nil, fmt.Errorf("invalid line number") } - if fset.PositionFor(expr.Pos(), false).Line == cursorLine { + if fset.PositionFor(expr.Pos(), false).Line == cursorLine { // ignore line directives words := strings.Fields(lines[cursorLine-1]) if len(words) > 0 && words[0] == PACKAGE { content := PACKAGE diff --git a/gopls/internal/lsp/source/completion/snippet.go b/gopls/internal/lsp/source/completion/snippet.go index db97a72d796..811acf63b51 100644 --- a/gopls/internal/lsp/source/completion/snippet.go +++ b/gopls/internal/lsp/source/completion/snippet.go @@ -42,7 +42,7 @@ func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snip fset := c.pkg.FileSet() // If the cursor position is on a different line from the literal's opening brace, - // we are in a multiline literal. + // we are in a multiline literal. Ignore line directives. if fset.PositionFor(c.pos, false).Line != fset.PositionFor(clInfo.cl.Lbrace, false).Line { snip.WriteText(",") } diff --git a/gopls/internal/lsp/source/folding_range.go b/gopls/internal/lsp/source/folding_range.go index 157dff2158d..dacb5ae79f7 100644 --- a/gopls/internal/lsp/source/folding_range.go +++ b/gopls/internal/lsp/source/folding_range.go @@ -42,7 +42,7 @@ func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFol } // Get folding ranges for comments separately as they are not walked by ast.Inspect. - ranges = append(ranges, commentsFoldingRange(pgf.Tok, pgf.Mapper, pgf.File)...) + ranges = append(ranges, commentsFoldingRange(pgf.Mapper, pgf.File)...) visit := func(n ast.Node) bool { rng := foldingRangeFunc(pgf.Tok, pgf.Mapper, n, lineFoldingOnly) @@ -127,7 +127,7 @@ func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, return nil } return &FoldingRangeInfo{ - MappedRange: NewMappedRange(tokFile, m, start, end), + MappedRange: NewMappedRange(m, start, end), Kind: kind, } } @@ -157,7 +157,8 @@ func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Po // commentsFoldingRange returns the folding ranges for all comment blocks in file. // The folding range starts at the end of the first line of the comment block, and ends at the end of the // comment block and has kind protocol.Comment. -func commentsFoldingRange(tokFile *token.File, m *protocol.ColumnMapper, file *ast.File) (comments []*FoldingRangeInfo) { +func commentsFoldingRange(m *protocol.ColumnMapper, file *ast.File) (comments []*FoldingRangeInfo) { + tokFile := m.TokFile for _, commentGrp := range file.Comments { startGrpLine, endGrpLine := tokFile.Line(commentGrp.Pos()), tokFile.Line(commentGrp.End()) if startGrpLine == endGrpLine { @@ -175,7 +176,7 @@ func commentsFoldingRange(tokFile *token.File, m *protocol.ColumnMapper, file *a } comments = append(comments, &FoldingRangeInfo{ // Fold from the end of the first line comment to the end of the comment block. - MappedRange: NewMappedRange(tokFile, m, endLinePos, commentGrp.End()), + MappedRange: NewMappedRange(m, endLinePos, commentGrp.End()), Kind: protocol.Comment, }) } diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index dd13e2acc12..dd6ce4021ca 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -240,7 +240,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr default: return 0, MappedRange{}, ErrNoRuneFound } - return r, NewMappedRange(pgf.Tok, pgf.Mapper, start, end), nil + return r, NewMappedRange(pgf.Mapper, start, end), nil } func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) { diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 53ad88bc337..64167af5839 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -207,7 +207,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // The builtin package isn't in the dependency graph, so the usual // utilities won't work here. - rng := NewMappedRange(builtin.Tok, builtin.Mapper, decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) + rng := NewMappedRange(builtin.Mapper, decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } @@ -248,7 +248,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa } name := method.Names[0].Name result.Declaration.node = method - rng := NewMappedRange(builtin.Tok, builtin.Mapper, method.Pos(), method.Pos()+token.Pos(len(name))) + rng := NewMappedRange(builtin.Mapper, method.Pos(), method.Pos()+token.Pos(len(name))) result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 8f9110a998a..33ecdc32e2e 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -88,7 +88,7 @@ func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protoco if rdep.DepsByImpPath[UnquoteImportPath(imp)] == targetPkg.ID { refs = append(refs, &ReferenceInfo{ Name: pgf.File.Name.Name, - MappedRange: NewMappedRange(f.Tok, f.Mapper, imp.Pos(), imp.End()), + MappedRange: NewMappedRange(f.Mapper, imp.Pos(), imp.End()), }) } } @@ -107,7 +107,7 @@ func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protoco } refs = append(refs, &ReferenceInfo{ Name: pgf.File.Name.Name, - MappedRange: NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()), + MappedRange: NewMappedRange(f.Mapper, f.File.Name.Pos(), f.File.Name.End()), }) } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 5d08f0eb602..93048e034b7 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -418,7 +418,7 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath } // Create text edit for the import path (string literal). - impPathMappedRange := NewMappedRange(f.Tok, f.Mapper, imp.Path.Pos(), imp.Path.End()) + impPathMappedRange := NewMappedRange(f.Mapper, imp.Path.Pos(), imp.Path.End()) rng, err := impPathMappedRange.Range() if err != nil { return err diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index cebb99b1299..fa91451d625 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -26,8 +26,7 @@ import ( // MappedRange provides mapped protocol.Range for a span.Range, accounting for // UTF-16 code points. // -// TOOD(adonovan): stop treating //line directives specially, and -// eliminate this type. All callers need either m, or a protocol.Range. +// TOOD(adonovan): eliminate this type. All callers need either m, or a protocol.Range. type MappedRange struct { spanRange span.Range // the range in the compiled source (package.CompiledGoFiles) m *protocol.ColumnMapper // a mapper of the edited source (package.GoFiles) @@ -41,14 +40,9 @@ type MappedRange struct { // CompiledGoFiles contains generated files, whose positions (via // token.File.Position) point to locations in the edited file -- the file // containing `import "C"`. -func NewMappedRange(file *token.File, m *protocol.ColumnMapper, start, end token.Pos) MappedRange { - mapped := m.TokFile.Name() - adjusted := file.PositionFor(start, true) // adjusted position - if adjusted.Filename != mapped { - bug.Reportf("mapped file %q does not match start position file %q", mapped, adjusted.Filename) - } +func NewMappedRange(m *protocol.ColumnMapper, start, end token.Pos) MappedRange { return MappedRange{ - spanRange: span.NewRange(file, start, end), + spanRange: span.NewRange(m.TokFile, start, end), m: m, } } @@ -61,7 +55,7 @@ func (s MappedRange) Range() (protocol.Range, error) { if s.m == nil { return protocol.Range{}, bug.Errorf("invalid range") } - spn, err := s.Span() + spn, err := span.FileSpan(s.spanRange.TokFile, s.spanRange.Start, s.spanRange.End) if err != nil { return protocol.Range{}, err } @@ -80,7 +74,7 @@ func (s MappedRange) Span() (span.Span, error) { if s.m == nil { return span.Span{}, bug.Errorf("invalid range") } - return span.FileSpan(s.spanRange.TokFile, s.m.TokFile, s.spanRange.Start, s.spanRange.End) + return span.FileSpan(s.spanRange.TokFile, s.spanRange.Start, s.spanRange.End) } // URI returns the URI of the edited file. @@ -159,28 +153,12 @@ func posToMappedRange(pkg Package, pos, end token.Pos) (MappedRange, error) { return MappedRange{}, fmt.Errorf("invalid end position") } - fset := pkg.FileSet() - tokFile := fset.File(pos) - // Subtle: it is not safe to simplify this to tokFile.Name - // because, due to //line directives, a Position within a - // token.File may have a different filename than the File itself. - // BUT, things have changed, and we're ignoring line directives - logicalFilename := tokFile.PositionFor(pos, false).Filename + logicalFilename := pkg.FileSet().File(pos).Name() // ignore line directives pgf, _, err := findFileInDeps(pkg, span.URIFromPath(logicalFilename)) if err != nil { return MappedRange{}, err } - // It is problematic that pgf.Mapper (from the parsed Go file) is - // accompanied here not by pgf.Tok but by tokFile from the global - // FileSet, which is a distinct token.File that doesn't - // contain [pos,end). - // - // This is done because tokFile is the *token.File for the compiled go file - // containing pos, whereas Mapper is the UTF16 mapper for the go file pointed - // to by line directives. - // - // TODO(golang/go#55043): clean this up. - return NewMappedRange(tokFile, pgf.Mapper, pos, end), nil + return NewMappedRange(pgf.Mapper, pos, end), nil } // FindPackageFromPos returns the Package for the given position, which must be diff --git a/gopls/internal/lsp/source/util_test.go b/gopls/internal/lsp/source/util_test.go deleted file mode 100644 index da565e3602a..00000000000 --- a/gopls/internal/lsp/source/util_test.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package source - -import ( - "bytes" - "go/scanner" - "go/token" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/span" -) - -// TODO: still need to test that NewMappedRange is working correctly -func TestMappedRangeAdjustment(t *testing.T) { - t.Skip("line directives not supported") - // Test that mapped range adjusts positions in compiled files to positions in - // the corresponding edited file. - - compiled := []byte(`// Generated. DO NOT EDIT. - -package p - -//line edited.go:3:1 -const a𐐀b = 42`) - edited := []byte(`package p - -const a𐐀b = 42`) - - fset := token.NewFileSet() - cf := scanFile(fset, "compiled.go", compiled) - ef := scanFile(fset, "edited.go", edited) - eURI := span.URIFromPath(ef.Name()) - - mapper := &protocol.ColumnMapper{ - URI: eURI, - TokFile: ef, - Content: edited, - } - - start := cf.Pos(bytes.Index(compiled, []byte("a𐐀b"))) - end := start + token.Pos(len("a𐐀b")) - mr := NewMappedRange(cf, mapper, start, end) - gotRange, err := mr.Range() - if err != nil { - t.Fatal(err) - } - wantRange := protocol.Range{ - Start: protocol.Position{Line: 2, Character: 6}, - End: protocol.Position{Line: 2, Character: 10}, - } - if gotRange != wantRange { - t.Errorf("NewMappedRange(...).Range(): got %v, want %v", gotRange, wantRange) - } - - // Verify that the mapped span is also in the edited file. - gotSpan, err := mr.Span() - if err != nil { - t.Fatal(err) - } - if gotURI := gotSpan.URI(); gotURI != eURI { - t.Errorf("mr.Span().URI() = %v, want %v", gotURI, eURI) - } - wantOffset := bytes.Index(edited, []byte("a𐐀b")) - if gotOffset := gotSpan.Start().Offset(); gotOffset != wantOffset { - t.Errorf("mr.Span().Start().Offset() = %d, want %d", gotOffset, wantOffset) - } -} - -// scanFile scans the a file into fset, in order to honor line directives. -func scanFile(fset *token.FileSet, name string, content []byte) *token.File { - f := fset.AddFile(name, -1, len(content)) - var s scanner.Scanner - s.Init(f, content, nil, scanner.ScanComments) - for { - _, tok, _ := s.Scan() - if tok == token.EOF { - break - } - } - return f -} diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 0dc0a64fd60..8e977b08ccf 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -1398,7 +1398,7 @@ func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain } func (data *Data) collectLinks(spn span.Span, link string, note *expect.Note, fset *token.FileSet) { - position := fset.PositionFor(note.Pos, false) + position := fset.PositionFor(note.Pos, false) // ignore line directives uri := spn.URI() data.Links[uri] = append(data.Links[uri], Link{ Src: spn, diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index 19e26a5ee29..b0169664919 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -8,10 +8,12 @@ import ( "testing" . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) -// This test passes (TestHoverOnError in definition_test.go) without -// the //line directive +// This is a slight variant of TestHoverOnError in definition_test.go +// that includes a line directive, which makes no difference since +// gopls ignores line directives. func TestHoverFailure(t *testing.T) { t.Skip("line directives //line ") const mod = ` @@ -33,19 +35,23 @@ func main() { Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error")) - // without the //line comment content would be non-nil - if content != nil { - t.Fatalf("expected nil hover content for Error") + if content == nil { + t.Fatalf("Hover('Error') returned nil") + } + want := "```go\nfunc (error).Error() string\n```" + if content.Value != want { + t.Fatalf("wrong Hover('Error') content:\n%s", compare.Text(want, content.Value)) } }) } -// This test demonstrates a case where gopls is confused by line directives, -// and fails to surface type checking errors. +// This test demonstrates a case where gopls is not at all confused by +// line directives, because it completely ignores them. func TestFailingDiagnosticClearingOnEdit(t *testing.T) { t.Skip("line directives //line ") // badPackageDup contains a duplicate definition of the 'a' const. - // this is from diagnostics_test.go, + // This is a minor variant of TestDiagnosticClearingOnEditfrom from + // diagnostics_test.go, with a line directive, which makes no difference. const badPackageDup = ` -- go.mod -- module mod.com @@ -63,19 +69,16 @@ const a = 2 Run(t, badPackageDup, func(t *testing.T, env *Env) { env.OpenFile("b.go") - - // no diagnostics for any files, but there should be env.Await( - OnceMet( - env.DoneWithOpen(), - EmptyOrNoDiagnostics("a.go"), - EmptyOrNoDiagnostics("b.go"), - ), + env.DiagnosticAtRegexpWithMessage("b.go", `a = 2`, "a redeclared"), + env.DiagnosticAtRegexpWithMessage("a.go", `a = 1`, "other declaration"), ) // Fix the error by editing the const name in b.go to `b`. env.RegexpReplace("b.go", "(a) = 2", "b") - - // The diagnostics that weren't sent above should now be cleared. + env.Await( + EmptyOrNoDiagnostics("a.go"), + EmptyOrNoDiagnostics("b.go"), + ) }) } diff --git a/gopls/internal/span/token.go b/gopls/internal/span/token.go index ac9376a5835..cdc747f330e 100644 --- a/gopls/internal/span/token.go +++ b/gopls/internal/span/token.go @@ -71,37 +71,26 @@ func (r Range) IsPoint() bool { // It will fill in all the members of the Span, calculating the line and column // information. func (r Range) Span() (Span, error) { - return FileSpan(r.TokFile, r.TokFile, r.Start, r.End) + return FileSpan(r.TokFile, r.Start, r.End) } -// FileSpan returns a span within the file referenced by start and end, using a -// token.File to translate between offsets and positions. -// -// The start and end position must be contained within posFile, though due to -// line directives they may reference positions in another file. If srcFile is -// provided, it is used to map the line:column positions referenced by start -// and end to offsets in the corresponding file. -// -// TODO(adonovan): clarify whether it is valid to pass posFile==srcFile when -// //line directives are in use. If so, fix this function; if not, fix Range.Span. -func FileSpan(posFile, srcFile *token.File, start, end token.Pos) (Span, error) { +// FileSpan returns a span within the file referenced by start and +// end, using a token.File to translate between offsets and positions. +func FileSpan(file *token.File, start, end token.Pos) (Span, error) { if !start.IsValid() { return Span{}, fmt.Errorf("start pos is not valid") } - if posFile == nil { - return Span{}, bug.Errorf("missing file association") // should never get here with a nil file - } var s Span var err error var startFilename string - startFilename, s.v.Start.Line, s.v.Start.Column, err = position(posFile, start) + startFilename, s.v.Start.Line, s.v.Start.Column, err = position(file, start) if err != nil { return Span{}, err } s.v.URI = URIFromPath(startFilename) if end.IsValid() { var endFilename string - endFilename, s.v.End.Line, s.v.End.Column, err = position(posFile, end) + endFilename, s.v.End.Line, s.v.End.Column, err = position(file, end) if err != nil { return Span{}, err } @@ -114,23 +103,7 @@ func FileSpan(posFile, srcFile *token.File, start, end token.Pos) (Span, error) s.v.Start.clean() s.v.End.clean() s.v.clean() - tf := posFile - if srcFile != nil { - tf = srcFile - } - if startFilename != tf.Name() { - // 'start' identifies a position specified by a //line directive - // in a file other than the one containing the directive. - // (Debugging support for https://github.com/golang/go/issues/54655.) - // - // This used to be a bug.Errorf, but that was unsound because - // Range.Span passes this function the same TokFile argument twice, - // which is never going to pass this test for a file containing - // a //line directive. - // TODO(adonovan): decide where the bug.Errorf really belongs. - return Span{}, fmt.Errorf("must supply Converter for file %q (tf.Name() = %q)", startFilename, tf.Name()) - } - return s.WithOffset(tf) + return s.WithOffset(file) } func position(tf *token.File, pos token.Pos) (string, int, int, error) { @@ -146,7 +119,7 @@ func positionFromOffset(tf *token.File, offset int) (string, int, int, error) { return "", 0, 0, fmt.Errorf("offset %d is beyond EOF (%d) in file %s", offset, tf.Size(), tf.Name()) } pos := tf.Pos(offset) - p := tf.PositionFor(pos, false) + p := tf.PositionFor(pos, false) // ignore line directives // TODO(golang/go#41029): Consider returning line, column instead of line+1, 1 if // the file's last character is not a newline. if offset == tf.Size() { diff --git a/internal/imports/sortimports.go b/internal/imports/sortimports.go index 85144db1dfa..1a0a7ebd9e4 100644 --- a/internal/imports/sortimports.go +++ b/internal/imports/sortimports.go @@ -52,6 +52,7 @@ func sortImports(localPrefix string, tokFile *token.File, f *ast.File) { d.Specs = specs // Deduping can leave a blank line before the rparen; clean that up. + // Ignore line directives. if len(d.Specs) > 0 { lastSpec := d.Specs[len(d.Specs)-1] lastLine := tokFile.PositionFor(lastSpec.Pos(), false).Line From 57b126584613a4ac99e27138377d056bbaeed3d7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 14 Dec 2022 14:31:55 -0500 Subject: [PATCH 552/723] go/gcexportdata: drop support for go1.6 export data The gcimporter.ImportData function and its parser implementation for the old "$$" form have not been needed in many years. Change-Id: I35be3436207fad1d845d9cc8ab2f82c8dfbb15b8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/457655 Reviewed-by: Robert Griesemer Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- go/gcexportdata/gcexportdata.go | 6 - go/gcexportdata/gcexportdata_test.go | 45 -- go/gcexportdata/testdata/errors-ae16.a | Bin 5494 -> 0 bytes internal/gcimporter/gcimporter.go | 919 +------------------------ 4 files changed, 1 insertion(+), 969 deletions(-) delete mode 100644 go/gcexportdata/gcexportdata_test.go delete mode 100644 go/gcexportdata/testdata/errors-ae16.a diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go index 696927a0f10..165ede0f8f3 100644 --- a/go/gcexportdata/gcexportdata.go +++ b/go/gcexportdata/gcexportdata.go @@ -123,12 +123,6 @@ func Read(in io.Reader, fset *token.FileSet, imports map[string]*types.Package, return nil, fmt.Errorf("can't read export data for %q directly from an archive file (call gcexportdata.NewReader first to extract export data)", path) } - // The App Engine Go runtime v1.6 uses the old export data format. - // TODO(adonovan): delete once v1.7 has been around for a while. - if bytes.HasPrefix(data, []byte("package ")) { - return gcimporter.ImportData(imports, path, path, bytes.NewReader(data)) - } - // The indexed export format starts with an 'i'; the older // binary export format starts with a 'c', 'd', or 'v' // (from "version"). Select appropriate importer. diff --git a/go/gcexportdata/gcexportdata_test.go b/go/gcexportdata/gcexportdata_test.go deleted file mode 100644 index a0006c02d5a..00000000000 --- a/go/gcexportdata/gcexportdata_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2016 The Go 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 gcexportdata_test - -import ( - "go/token" - "go/types" - "log" - "os" - "testing" - - "golang.org/x/tools/go/gcexportdata" -) - -// Test to ensure that gcexportdata can read files produced by App -// Engine Go runtime v1.6. -func TestAppEngine16(t *testing.T) { - // Open and read the file. - f, err := os.Open("testdata/errors-ae16.a") - if err != nil { - t.Fatal(err) - } - defer f.Close() - r, err := gcexportdata.NewReader(f) - if err != nil { - log.Fatalf("reading export data: %v", err) - } - - // Decode the export data. - fset := token.NewFileSet() - imports := make(map[string]*types.Package) - pkg, err := gcexportdata.Read(r, fset, imports, "errors") - if err != nil { - log.Fatal(err) - } - - // Print package information. - got := pkg.Scope().Lookup("New").Type().String() - want := "func(text string) error" - if got != want { - t.Errorf("New.Type = %s, want %s", got, want) - } -} diff --git a/go/gcexportdata/testdata/errors-ae16.a b/go/gcexportdata/testdata/errors-ae16.a deleted file mode 100644 index 3f1dad54f074503fb14d9ddf702553f41e696a4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5494 zcmb^#&2QUO{KYL*7D{K0YLsbAUdXDnfM@4BWo*^d4I|K2X*+CEMe8_r8k#1N9(8^@tYXqg z<}w8}V_7*Pl`7=ZYQj*}yro*%v>J^>VzEeFH9%B>&Wc@?;^T-&`COw|w<(Lpl$ZX_gmfoz z#JN>BDbRzBtXIHrm&%od*i;*;g^4Om`ou_4vtdjrYl%d3Lb`-zNO7tl*R`b~B?J~S zZW_QE9YE$wwRySbhM*NA_B|+GseGOVPI{SmMNUTufxsp!#SqyQhMBVoxj%saMQvmF z41q^`Ul)(w`w@3{|BS1@Y0bIVG=6B>S8q{-=E>Moy4jeCg|+5POwu+gv7FXi4C6*w z{{W$6<)+q*yWm&wK>bV0KXpd6jX9v841so9yZbo)$iup|odf1v;#^E@i5z?Olhz!J zY0XM3+&Yw=eM9%Zr}p;teud|FBzzKu67os-1R>*?0CH!x0PNH7Fdi@I%>O&g#;1C)@dQcW&9dO@D-WXw_$(7q55Ely?oZ~*!( zg^p4vO-nf2qGvrBKNe7NiE9Te5+8vgz~FBRkvW9wU0{0G9aR0^0Fo*kVYz0T4F^hc zzFIS!H3u-JX9)?P0+aL=r{*lz=M_a!$x$KL4j-aINEbv*xX*2sNP}LJN8l7ooshxf z)Dx_R$Z_F~S?g+OcIN5`2@5+RB96W-d>;yp5MhMKLPI!92CtWlO3lzKjfW4cR8~o6 zlj)Rcq%#>Mld*D{Tux8rvx;F^X`Og*6(gtXIWwoIc~ws;rfwvXmZ~SzoT|Vk11=Ec zQtfQRHqO$xG~5Lag<>)O)gh>tew)vsOBnO&OR}2e{*Wk6aZ*peMVc zSGivP7bM7S0mwo~3lCj8c=U{Si^rf#zn@1>!%=sU>)^AYmxF~dLdK95x+~ZJ7=#n% z8)Pe?tN}tFfrwyjt09A=RF@p-@$i-fak?lp=>KG>0INl+yxfdO`0ScQCq)j30`kSv8Zcd#c z;c$}23WVOwAc(9$qHT((i;d<491lTn9Cqj4u82dx z+efIy+ybA(*W8q15)*AoR+4Qhlzadf+tSGnK}Zm5R~F|)FxF&!Un%{t{L87ADJ9pA zeG4xkzH)?-7cQY5?se-W1FrPfOUnnX0ts>us|cu`U?3}jeMx==DOR_zC6QrLACzVKWR4?Iwlw)wu3c)C2A`TfGo= z#hG?np{0^SK&H$Ik1~LX z417n}j0YPCc(;w~vD?RfsKeG+o5Z!1$p|_}K_kp2Zbx~!UbCIL?kr-&2)%c^{7HX; zuy1*X2j$OhLq5e{?NA(E$2c8RoYSCcU4b|@#ylbrIk;AKAendA`wPhB7qID>JrSDD zo7~2JR~}Qo_Dk0<9yXut7rLPJQ!8%o>DD)} package object (across importer) - localPkgs map[string]*types.Package // package id -> package object (just this package) -} - -func (p *parser) init(filename, id string, src io.Reader, packages map[string]*types.Package) { - p.scanner.Init(src) - p.scanner.Error = func(_ *scanner.Scanner, msg string) { p.error(msg) } - p.scanner.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanChars | scanner.ScanStrings | scanner.ScanComments | scanner.SkipComments - p.scanner.Whitespace = 1<<'\t' | 1<<' ' - p.scanner.Filename = filename // for good error messages - p.next() - p.id = id - p.sharedPkgs = packages - if debug { - // check consistency of packages map - for _, pkg := range packages { - if pkg.Name() == "" { - fmt.Printf("no package name for %s\n", pkg.Path()) - } - } - } -} - -func (p *parser) next() { - p.tok = p.scanner.Scan() - switch p.tok { - case scanner.Ident, scanner.Int, scanner.Char, scanner.String, '·': - p.lit = p.scanner.TokenText() - default: - p.lit = "" - } - if debug { - fmt.Printf("%s: %q -> %q\n", scanner.TokenString(p.tok), p.scanner.TokenText(), p.lit) - } -} - -func declTypeName(pkg *types.Package, name string) *types.TypeName { - scope := pkg.Scope() - if obj := scope.Lookup(name); obj != nil { - return obj.(*types.TypeName) - } - obj := types.NewTypeName(token.NoPos, pkg, name, nil) - // a named type may be referred to before the underlying type - // is known - set it up - types.NewNamed(obj, nil, nil) - scope.Insert(obj) - return obj -} - -// ---------------------------------------------------------------------------- -// Error handling - -// Internal errors are boxed as importErrors. -type importError struct { - pos scanner.Position - err error -} - -func (e importError) Error() string { - return fmt.Sprintf("import error %s (byte offset = %d): %s", e.pos, e.pos.Offset, e.err) -} - -func (p *parser) error(err interface{}) { - if s, ok := err.(string); ok { - err = errors.New(s) - } - // panic with a runtime.Error if err is not an error - panic(importError{p.scanner.Pos(), err.(error)}) -} - -func (p *parser) errorf(format string, args ...interface{}) { - p.error(fmt.Sprintf(format, args...)) -} - -func (p *parser) expect(tok rune) string { - lit := p.lit - if p.tok != tok { - p.errorf("expected %s, got %s (%s)", scanner.TokenString(tok), scanner.TokenString(p.tok), lit) - } - p.next() - return lit -} - -func (p *parser) expectSpecial(tok string) { - sep := 'x' // not white space - i := 0 - for i < len(tok) && p.tok == rune(tok[i]) && sep > ' ' { - sep = p.scanner.Peek() // if sep <= ' ', there is white space before the next token - p.next() - i++ - } - if i < len(tok) { - p.errorf("expected %q, got %q", tok, tok[0:i]) - } -} - -func (p *parser) expectKeyword(keyword string) { - lit := p.expect(scanner.Ident) - if lit != keyword { - p.errorf("expected keyword %s, got %q", keyword, lit) - } -} - -// ---------------------------------------------------------------------------- -// Qualified and unqualified names - -// parsePackageID parses a PackageId: -// -// PackageId = string_lit . -func (p *parser) parsePackageID() string { - id, err := strconv.Unquote(p.expect(scanner.String)) - if err != nil { - p.error(err) - } - // id == "" stands for the imported package id - // (only known at time of package installation) - if id == "" { - id = p.id - } - return id -} - -// parsePackageName parse a PackageName: -// -// PackageName = ident . -func (p *parser) parsePackageName() string { - return p.expect(scanner.Ident) -} - -// parseDotIdent parses a dotIdentifier: -// -// dotIdentifier = ( ident | '·' ) { ident | int | '·' } . -func (p *parser) parseDotIdent() string { - ident := "" - if p.tok != scanner.Int { - sep := 'x' // not white space - for (p.tok == scanner.Ident || p.tok == scanner.Int || p.tok == '·') && sep > ' ' { - ident += p.lit - sep = p.scanner.Peek() // if sep <= ' ', there is white space before the next token - p.next() - } - } - if ident == "" { - p.expect(scanner.Ident) // use expect() for error handling - } - return ident -} - -// parseQualifiedName parses a QualifiedName: -// -// QualifiedName = "@" PackageId "." ( "?" | dotIdentifier ) . -func (p *parser) parseQualifiedName() (id, name string) { - p.expect('@') - id = p.parsePackageID() - p.expect('.') - // Per rev f280b8a485fd (10/2/2013), qualified names may be used for anonymous fields. - if p.tok == '?' { - p.next() - } else { - name = p.parseDotIdent() - } - return -} - -// getPkg returns the package for a given id. If the package is -// not found, create the package and add it to the p.localPkgs -// and p.sharedPkgs maps. name is the (expected) name of the -// package. If name == "", the package name is expected to be -// set later via an import clause in the export data. -// -// id identifies a package, usually by a canonical package path like -// "encoding/json" but possibly by a non-canonical import path like -// "./json". -func (p *parser) getPkg(id, name string) *types.Package { - // package unsafe is not in the packages maps - handle explicitly - if id == "unsafe" { - return types.Unsafe - } - - pkg := p.localPkgs[id] - if pkg == nil { - // first import of id from this package - pkg = p.sharedPkgs[id] - if pkg == nil { - // first import of id by this importer; - // add (possibly unnamed) pkg to shared packages - pkg = types.NewPackage(id, name) - p.sharedPkgs[id] = pkg - } - // add (possibly unnamed) pkg to local packages - if p.localPkgs == nil { - p.localPkgs = make(map[string]*types.Package) - } - p.localPkgs[id] = pkg - } else if name != "" { - // package exists already and we have an expected package name; - // make sure names match or set package name if necessary - if pname := pkg.Name(); pname == "" { - pkg.SetName(name) - } else if pname != name { - p.errorf("%s package name mismatch: %s (given) vs %s (expected)", id, pname, name) - } - } - return pkg -} - -// parseExportedName is like parseQualifiedName, but -// the package id is resolved to an imported *types.Package. -func (p *parser) parseExportedName() (pkg *types.Package, name string) { - id, name := p.parseQualifiedName() - pkg = p.getPkg(id, "") - return -} - -// ---------------------------------------------------------------------------- -// Types - -// parseBasicType parses a BasicType: -// -// BasicType = identifier . -func (p *parser) parseBasicType() types.Type { - id := p.expect(scanner.Ident) - obj := types.Universe.Lookup(id) - if obj, ok := obj.(*types.TypeName); ok { - return obj.Type() - } - p.errorf("not a basic type: %s", id) - return nil -} - -// parseArrayType parses an ArrayType: -// -// ArrayType = "[" int_lit "]" Type . -func (p *parser) parseArrayType(parent *types.Package) types.Type { - // "[" already consumed and lookahead known not to be "]" - lit := p.expect(scanner.Int) - p.expect(']') - elem := p.parseType(parent) - n, err := strconv.ParseInt(lit, 10, 64) - if err != nil { - p.error(err) - } - return types.NewArray(elem, n) -} - -// parseMapType parses a MapType: -// -// MapType = "map" "[" Type "]" Type . -func (p *parser) parseMapType(parent *types.Package) types.Type { - p.expectKeyword("map") - p.expect('[') - key := p.parseType(parent) - p.expect(']') - elem := p.parseType(parent) - return types.NewMap(key, elem) -} - -// parseName parses a Name: -// -// Name = identifier | "?" | QualifiedName . -// -// For unqualified and anonymous names, the returned package is the parent -// package unless parent == nil, in which case the returned package is the -// package being imported. (The parent package is not nil if the name -// is an unqualified struct field or interface method name belonging to a -// type declared in another package.) -// -// For qualified names, the returned package is nil (and not created if -// it doesn't exist yet) unless materializePkg is set (which creates an -// unnamed package with valid package path). In the latter case, a -// subsequent import clause is expected to provide a name for the package. -func (p *parser) parseName(parent *types.Package, materializePkg bool) (pkg *types.Package, name string) { - pkg = parent - if pkg == nil { - pkg = p.sharedPkgs[p.id] - } - switch p.tok { - case scanner.Ident: - name = p.lit - p.next() - case '?': - // anonymous - p.next() - case '@': - // exported name prefixed with package path - pkg = nil - var id string - id, name = p.parseQualifiedName() - if materializePkg { - pkg = p.getPkg(id, "") - } - default: - p.error("name expected") - } - return -} - func deref(typ types.Type) types.Type { if p, _ := typ.(*types.Pointer); p != nil { return p.Elem() @@ -618,563 +258,6 @@ func deref(typ types.Type) types.Type { return typ } -// parseField parses a Field: -// -// Field = Name Type [ string_lit ] . -func (p *parser) parseField(parent *types.Package) (*types.Var, string) { - pkg, name := p.parseName(parent, true) - - if name == "_" { - // Blank fields should be package-qualified because they - // are unexported identifiers, but gc does not qualify them. - // Assuming that the ident belongs to the current package - // causes types to change during re-exporting, leading - // to spurious "can't assign A to B" errors from go/types. - // As a workaround, pretend all blank fields belong - // to the same unique dummy package. - const blankpkg = "<_>" - pkg = p.getPkg(blankpkg, blankpkg) - } - - typ := p.parseType(parent) - anonymous := false - if name == "" { - // anonymous field - typ must be T or *T and T must be a type name - switch typ := deref(typ).(type) { - case *types.Basic: // basic types are named types - pkg = nil // objects defined in Universe scope have no package - name = typ.Name() - case *types.Named: - name = typ.Obj().Name() - default: - p.errorf("anonymous field expected") - } - anonymous = true - } - tag := "" - if p.tok == scanner.String { - s := p.expect(scanner.String) - var err error - tag, err = strconv.Unquote(s) - if err != nil { - p.errorf("invalid struct tag %s: %s", s, err) - } - } - return types.NewField(token.NoPos, pkg, name, typ, anonymous), tag -} - -// parseStructType parses a StructType: -// -// StructType = "struct" "{" [ FieldList ] "}" . -// FieldList = Field { ";" Field } . -func (p *parser) parseStructType(parent *types.Package) types.Type { - var fields []*types.Var - var tags []string - - p.expectKeyword("struct") - p.expect('{') - for i := 0; p.tok != '}' && p.tok != scanner.EOF; i++ { - if i > 0 { - p.expect(';') - } - fld, tag := p.parseField(parent) - if tag != "" && tags == nil { - tags = make([]string, i) - } - if tags != nil { - tags = append(tags, tag) - } - fields = append(fields, fld) - } - p.expect('}') - - return types.NewStruct(fields, tags) -} - -// parseParameter parses a Parameter: -// -// Parameter = ( identifier | "?" ) [ "..." ] Type [ string_lit ] . -func (p *parser) parseParameter() (par *types.Var, isVariadic bool) { - _, name := p.parseName(nil, false) - // remove gc-specific parameter numbering - if i := strings.Index(name, "·"); i >= 0 { - name = name[:i] - } - if p.tok == '.' { - p.expectSpecial("...") - isVariadic = true - } - typ := p.parseType(nil) - if isVariadic { - typ = types.NewSlice(typ) - } - // ignore argument tag (e.g. "noescape") - if p.tok == scanner.String { - p.next() - } - // TODO(gri) should we provide a package? - par = types.NewVar(token.NoPos, nil, name, typ) - return -} - -// parseParameters parses a Parameters: -// -// Parameters = "(" [ ParameterList ] ")" . -// ParameterList = { Parameter "," } Parameter . -func (p *parser) parseParameters() (list []*types.Var, isVariadic bool) { - p.expect('(') - for p.tok != ')' && p.tok != scanner.EOF { - if len(list) > 0 { - p.expect(',') - } - par, variadic := p.parseParameter() - list = append(list, par) - if variadic { - if isVariadic { - p.error("... not on final argument") - } - isVariadic = true - } - } - p.expect(')') - - return -} - -// parseSignature parses a Signature: -// -// Signature = Parameters [ Result ] . -// Result = Type | Parameters . -func (p *parser) parseSignature(recv *types.Var) *types.Signature { - params, isVariadic := p.parseParameters() - - // optional result type - var results []*types.Var - if p.tok == '(' { - var variadic bool - results, variadic = p.parseParameters() - if variadic { - p.error("... not permitted on result type") - } - } - - return types.NewSignature(recv, types.NewTuple(params...), types.NewTuple(results...), isVariadic) -} - -// parseInterfaceType parses an InterfaceType: -// -// InterfaceType = "interface" "{" [ MethodList ] "}" . -// MethodList = Method { ";" Method } . -// Method = Name Signature . -// -// The methods of embedded interfaces are always "inlined" -// by the compiler and thus embedded interfaces are never -// visible in the export data. -func (p *parser) parseInterfaceType(parent *types.Package) types.Type { - var methods []*types.Func - - p.expectKeyword("interface") - p.expect('{') - for i := 0; p.tok != '}' && p.tok != scanner.EOF; i++ { - if i > 0 { - p.expect(';') - } - pkg, name := p.parseName(parent, true) - sig := p.parseSignature(nil) - methods = append(methods, types.NewFunc(token.NoPos, pkg, name, sig)) - } - p.expect('}') - - // Complete requires the type's embedded interfaces to be fully defined, - // but we do not define any - return newInterface(methods, nil).Complete() -} - -// parseChanType parses a ChanType: -// -// ChanType = ( "chan" [ "<-" ] | "<-" "chan" ) Type . -func (p *parser) parseChanType(parent *types.Package) types.Type { - dir := types.SendRecv - if p.tok == scanner.Ident { - p.expectKeyword("chan") - if p.tok == '<' { - p.expectSpecial("<-") - dir = types.SendOnly - } - } else { - p.expectSpecial("<-") - p.expectKeyword("chan") - dir = types.RecvOnly - } - elem := p.parseType(parent) - return types.NewChan(dir, elem) -} - -// parseType parses a Type: -// -// Type = -// BasicType | TypeName | ArrayType | SliceType | StructType | -// PointerType | FuncType | InterfaceType | MapType | ChanType | -// "(" Type ")" . -// -// BasicType = ident . -// TypeName = ExportedName . -// SliceType = "[" "]" Type . -// PointerType = "*" Type . -// FuncType = "func" Signature . -func (p *parser) parseType(parent *types.Package) types.Type { - switch p.tok { - case scanner.Ident: - switch p.lit { - default: - return p.parseBasicType() - case "struct": - return p.parseStructType(parent) - case "func": - // FuncType - p.next() - return p.parseSignature(nil) - case "interface": - return p.parseInterfaceType(parent) - case "map": - return p.parseMapType(parent) - case "chan": - return p.parseChanType(parent) - } - case '@': - // TypeName - pkg, name := p.parseExportedName() - return declTypeName(pkg, name).Type() - case '[': - p.next() // look ahead - if p.tok == ']' { - // SliceType - p.next() - return types.NewSlice(p.parseType(parent)) - } - return p.parseArrayType(parent) - case '*': - // PointerType - p.next() - return types.NewPointer(p.parseType(parent)) - case '<': - return p.parseChanType(parent) - case '(': - // "(" Type ")" - p.next() - typ := p.parseType(parent) - p.expect(')') - return typ - } - p.errorf("expected type, got %s (%q)", scanner.TokenString(p.tok), p.lit) - return nil -} - -// ---------------------------------------------------------------------------- -// Declarations - -// parseImportDecl parses an ImportDecl: -// -// ImportDecl = "import" PackageName PackageId . -func (p *parser) parseImportDecl() { - p.expectKeyword("import") - name := p.parsePackageName() - p.getPkg(p.parsePackageID(), name) -} - -// parseInt parses an int_lit: -// -// int_lit = [ "+" | "-" ] { "0" ... "9" } . -func (p *parser) parseInt() string { - s := "" - switch p.tok { - case '-': - s = "-" - p.next() - case '+': - p.next() - } - return s + p.expect(scanner.Int) -} - -// parseNumber parses a number: -// -// number = int_lit [ "p" int_lit ] . -func (p *parser) parseNumber() (typ *types.Basic, val constant.Value) { - // mantissa - mant := constant.MakeFromLiteral(p.parseInt(), token.INT, 0) - if mant == nil { - panic("invalid mantissa") - } - - if p.lit == "p" { - // exponent (base 2) - p.next() - exp, err := strconv.ParseInt(p.parseInt(), 10, 0) - if err != nil { - p.error(err) - } - if exp < 0 { - denom := constant.MakeInt64(1) - denom = constant.Shift(denom, token.SHL, uint(-exp)) - typ = types.Typ[types.UntypedFloat] - val = constant.BinaryOp(mant, token.QUO, denom) - return - } - if exp > 0 { - mant = constant.Shift(mant, token.SHL, uint(exp)) - } - typ = types.Typ[types.UntypedFloat] - val = mant - return - } - - typ = types.Typ[types.UntypedInt] - val = mant - return -} - -// parseConstDecl parses a ConstDecl: -// -// ConstDecl = "const" ExportedName [ Type ] "=" Literal . -// Literal = bool_lit | int_lit | float_lit | complex_lit | rune_lit | string_lit . -// bool_lit = "true" | "false" . -// complex_lit = "(" float_lit "+" float_lit "i" ")" . -// rune_lit = "(" int_lit "+" int_lit ")" . -// string_lit = `"` { unicode_char } `"` . -func (p *parser) parseConstDecl() { - p.expectKeyword("const") - pkg, name := p.parseExportedName() - - var typ0 types.Type - if p.tok != '=' { - // constant types are never structured - no need for parent type - typ0 = p.parseType(nil) - } - - p.expect('=') - var typ types.Type - var val constant.Value - switch p.tok { - case scanner.Ident: - // bool_lit - if p.lit != "true" && p.lit != "false" { - p.error("expected true or false") - } - typ = types.Typ[types.UntypedBool] - val = constant.MakeBool(p.lit == "true") - p.next() - - case '-', scanner.Int: - // int_lit - typ, val = p.parseNumber() - - case '(': - // complex_lit or rune_lit - p.next() - if p.tok == scanner.Char { - p.next() - p.expect('+') - typ = types.Typ[types.UntypedRune] - _, val = p.parseNumber() - p.expect(')') - break - } - _, re := p.parseNumber() - p.expect('+') - _, im := p.parseNumber() - p.expectKeyword("i") - p.expect(')') - typ = types.Typ[types.UntypedComplex] - val = constant.BinaryOp(re, token.ADD, constant.MakeImag(im)) - - case scanner.Char: - // rune_lit - typ = types.Typ[types.UntypedRune] - val = constant.MakeFromLiteral(p.lit, token.CHAR, 0) - p.next() - - case scanner.String: - // string_lit - typ = types.Typ[types.UntypedString] - val = constant.MakeFromLiteral(p.lit, token.STRING, 0) - p.next() - - default: - p.errorf("expected literal got %s", scanner.TokenString(p.tok)) - } - - if typ0 == nil { - typ0 = typ - } - - pkg.Scope().Insert(types.NewConst(token.NoPos, pkg, name, typ0, val)) -} - -// parseTypeDecl parses a TypeDecl: -// -// TypeDecl = "type" ExportedName Type . -func (p *parser) parseTypeDecl() { - p.expectKeyword("type") - pkg, name := p.parseExportedName() - obj := declTypeName(pkg, name) - - // The type object may have been imported before and thus already - // have a type associated with it. We still need to parse the type - // structure, but throw it away if the object already has a type. - // This ensures that all imports refer to the same type object for - // a given type declaration. - typ := p.parseType(pkg) - - if name := obj.Type().(*types.Named); name.Underlying() == nil { - name.SetUnderlying(typ) - } -} - -// parseVarDecl parses a VarDecl: -// -// VarDecl = "var" ExportedName Type . -func (p *parser) parseVarDecl() { - p.expectKeyword("var") - pkg, name := p.parseExportedName() - typ := p.parseType(pkg) - pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, name, typ)) -} - -// parseFunc parses a Func: -// -// Func = Signature [ Body ] . -// Body = "{" ... "}" . -func (p *parser) parseFunc(recv *types.Var) *types.Signature { - sig := p.parseSignature(recv) - if p.tok == '{' { - p.next() - for i := 1; i > 0; p.next() { - switch p.tok { - case '{': - i++ - case '}': - i-- - } - } - } - return sig -} - -// parseMethodDecl parses a MethodDecl: -// -// MethodDecl = "func" Receiver Name Func . -// Receiver = "(" ( identifier | "?" ) [ "*" ] ExportedName ")" . -func (p *parser) parseMethodDecl() { - // "func" already consumed - p.expect('(') - recv, _ := p.parseParameter() // receiver - p.expect(')') - - // determine receiver base type object - base := deref(recv.Type()).(*types.Named) - - // parse method name, signature, and possibly inlined body - _, name := p.parseName(nil, false) - sig := p.parseFunc(recv) - - // methods always belong to the same package as the base type object - pkg := base.Obj().Pkg() - - // add method to type unless type was imported before - // and method exists already - // TODO(gri) This leads to a quadratic algorithm - ok for now because method counts are small. - base.AddMethod(types.NewFunc(token.NoPos, pkg, name, sig)) -} - -// parseFuncDecl parses a FuncDecl: -// -// FuncDecl = "func" ExportedName Func . -func (p *parser) parseFuncDecl() { - // "func" already consumed - pkg, name := p.parseExportedName() - typ := p.parseFunc(nil) - pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, name, typ)) -} - -// parseDecl parses a Decl: -// -// Decl = [ ImportDecl | ConstDecl | TypeDecl | VarDecl | FuncDecl | MethodDecl ] "\n" . -func (p *parser) parseDecl() { - if p.tok == scanner.Ident { - switch p.lit { - case "import": - p.parseImportDecl() - case "const": - p.parseConstDecl() - case "type": - p.parseTypeDecl() - case "var": - p.parseVarDecl() - case "func": - p.next() // look ahead - if p.tok == '(' { - p.parseMethodDecl() - } else { - p.parseFuncDecl() - } - } - } - p.expect('\n') -} - -// ---------------------------------------------------------------------------- -// Export - -// parseExport parses an Export: -// -// Export = "PackageClause { Decl } "$$" . -// PackageClause = "package" PackageName [ "safe" ] "\n" . -func (p *parser) parseExport() *types.Package { - p.expectKeyword("package") - name := p.parsePackageName() - if p.tok == scanner.Ident && p.lit == "safe" { - // package was compiled with -u option - ignore - p.next() - } - p.expect('\n') - - pkg := p.getPkg(p.id, name) - - for p.tok != '$' && p.tok != scanner.EOF { - p.parseDecl() - } - - if ch := p.scanner.Peek(); p.tok != '$' || ch != '$' { - // don't call next()/expect() since reading past the - // export data may cause scanner errors (e.g. NUL chars) - p.errorf("expected '$$', got %s %c", scanner.TokenString(p.tok), ch) - } - - if n := p.scanner.ErrorCount; n != 0 { - p.errorf("expected no scanner errors, got %d", n) - } - - // Record all locally referenced packages as imports. - var imports []*types.Package - for id, pkg2 := range p.localPkgs { - if pkg2.Name() == "" { - p.errorf("%s package has no name", id) - } - if id == p.id { - continue // avoid self-edge - } - imports = append(imports, pkg2) - } - sort.Sort(byPath(imports)) - pkg.SetImports(imports) - - // package was imported completely and without errors - pkg.MarkComplete() - - return pkg -} - type byPath []*types.Package func (a byPath) Len() int { return len(a) } From 6df6eee46320bd774019d8512e765ee60cea5519 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sun, 30 Oct 2022 17:24:53 -0400 Subject: [PATCH 553/723] internal/diff/lcs: optimize inner loop The core operation of the diff algorithm is to advance a cursor forwards or backwards over common substrings of the two sequences. This change refactors the sequence abstraction to express this, permitting faster and type- specialized implementations of the common-prefix and common-suffix operations. This improves the running time on the new LargeFileSmallDiff benchmark by 17%: Before: BenchmarkLargeFileSmallDiff/bytes-8 9838 521006 ns/op BenchmarkLargeFileSmallDiff/runes-8 10000 522915 ns/op After: BenchmarkLargeFileSmallDiff/string-8 14545 412497 ns/op BenchmarkLargeFileSmallDiff/bytes-8 13868 432577 ns/op (-17%) BenchmarkLargeFileSmallDiff/runes-8 13744 436548 ns/op (-17%) Also, some cleanups: - Compute is replaced by three Diff{Strings,Bytes,Runes} functions. - The LCS result is now internal, as it is needed only by tests. - The caller's maxDiff constant is moved into this package. - Test{Forward,Backward,TwoSided} are factored together. - Delete unused randlines(). - {append,prepend}lcs are now methods - Diff declaration moved to more prominent location. - A test of the three DiffT functions. Future work: - faster common-prefix implementations are possible: the two operations still account for about 5% of this benchmark. - common-suffix for strings is about 50% faster than for bytes. Find out why. - There appear to be opportunities for much bigger gains on the above benchmark by stripping the common prefix and suffix as a first step. See comments in benchmark. Change-Id: I475b7fc731d628d54e652a4ace7ce67c6c2755c2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446575 TryBot-Result: Gopher Robot Reviewed-by: Peter Weinberger Run-TryBot: Alan Donovan gopls-CI: kokoro --- internal/diff/diff_test.go | 14 --- internal/diff/lcs/common.go | 19 ++- internal/diff/lcs/common_test.go | 2 +- internal/diff/lcs/old.go | 190 +++++++++++------------------ internal/diff/lcs/old_test.go | 198 +++++++++++++++++++------------ internal/diff/lcs/sequence.go | 113 ++++++++++++++++++ internal/diff/ndiff.go | 13 +- 7 files changed, 319 insertions(+), 230 deletions(-) create mode 100644 internal/diff/lcs/sequence.go diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index 7bb41a27bfa..be949372945 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -7,7 +7,6 @@ package diff_test import ( "math/rand" "reflect" - "strings" "testing" "unicode/utf8" @@ -173,16 +172,3 @@ func randstr(s string, n int) string { } return string(x) } - -// return some random lines, all ending with \n -func randlines(s string, n int) string { - src := []rune(s) - var b strings.Builder - for i := 0; i < n; i++ { - for j := 0; j < 4+rand.Intn(4); j++ { - b.WriteRune(src[rand.Intn(len(src))]) - } - b.WriteByte('\n') - } - return b.String() -} diff --git a/internal/diff/lcs/common.go b/internal/diff/lcs/common.go index e5d08014760..c3e82dd2683 100644 --- a/internal/diff/lcs/common.go +++ b/internal/diff/lcs/common.go @@ -138,10 +138,10 @@ func overlap(exist, prop diag) (direction, diag) { // manipulating Diag and lcs -// prependlcs a diagonal (x,y)-(x+1,y+1) segment either to an empty lcs -// or to its first Diag. prependlcs is only called extending diagonals +// prepend a diagonal (x,y)-(x+1,y+1) segment either to an empty lcs +// or to its first Diag. prepend is only called to extend diagonals // the backward direction. -func prependlcs(lcs lcs, x, y int) lcs { +func (lcs lcs) prepend(x, y int) lcs { if len(lcs) > 0 { d := &lcs[0] if int(d.X) == x+1 && int(d.Y) == y+1 { @@ -157,10 +157,10 @@ func prependlcs(lcs lcs, x, y int) lcs { return lcs } -// appendlcs appends a diagonal, or extends the existing one. -// by adding the edge (x,y)-(x+1.y+1). appendlcs is only called -// while extending diagonals in the forward direction. -func appendlcs(lcs lcs, x, y int) lcs { +// append appends a diagonal, or extends the existing one. +// by adding the edge (x,y)-(x+1.y+1). append is only called +// to extend diagonals in the forward direction. +func (lcs lcs) append(x, y int) lcs { if len(lcs) > 0 { last := &lcs[len(lcs)-1] // Expand last element if adjoining. @@ -177,8 +177,3 @@ func appendlcs(lcs lcs, x, y int) lcs { func ok(d, k int) bool { return d >= 0 && -d <= k && k <= d } - -type Diff struct { - Start, End int // offsets in A - Text string // replacement text -} diff --git a/internal/diff/lcs/common_test.go b/internal/diff/lcs/common_test.go index 4aa36abc2e8..f19245e404c 100644 --- a/internal/diff/lcs/common_test.go +++ b/internal/diff/lcs/common_test.go @@ -88,7 +88,7 @@ func checkDiffs(t *testing.T, before string, diffs []Diff, after string) { if sofar < d.Start { ans.WriteString(before[sofar:d.Start]) } - ans.WriteString(d.Text) + ans.WriteString(after[d.ReplStart:d.ReplEnd]) sofar = d.End } ans.WriteString(before[sofar:]) diff --git a/internal/diff/lcs/old.go b/internal/diff/lcs/old.go index a091edd5501..7af11fc896c 100644 --- a/internal/diff/lcs/old.go +++ b/internal/diff/lcs/old.go @@ -4,129 +4,90 @@ package lcs +// TODO(adonovan): remove unclear references to "old" in this package. + import ( "fmt" - "strings" ) -// non generic code. The names have Old at the end to indicate they are the -// the implementation that doesn't use generics. - -// Compute the Diffs and the lcs. -func Compute(a, b interface{}, limit int) ([]Diff, lcs) { - var ans lcs - g := newegraph(a, b, limit) - ans = g.twosided() - diffs := g.fromlcs(ans) - return diffs, ans +// A Diff is a replacement of a portion of A by a portion of B. +type Diff struct { + Start, End int // offsets of portion to delete in A + ReplStart, ReplEnd int // offset of replacement text in B } -// editGraph carries the information for computing the lcs for []byte, []rune, or []string. -type editGraph struct { - eq eq // how to compare elements of A, B, and convert slices to strings - vf, vb label // forward and backward labels +// DiffStrings returns the differences between two strings. +// It does not respect rune boundaries. +func DiffStrings(a, b string) []Diff { return diff(stringSeqs{a, b}) } - limit int // maximal value of D - // the bounding rectangle of the current edit graph - lx, ly, ux, uy int - delta int // common subexpression: (ux-lx)-(uy-ly) -} +// DiffBytes returns the differences between two byte sequences. +// It does not respect rune boundaries. +func DiffBytes(a, b []byte) []Diff { return diff(bytesSeqs{a, b}) } -// abstraction in place of generic -type eq interface { - eq(i, j int) bool - substr(i, j int) string // string from b[i:j] - lena() int - lenb() int -} +// DiffRunes returns the differences between two rune sequences. +func DiffRunes(a, b []rune) []Diff { return diff(runesSeqs{a, b}) } -type byteeq struct { - a, b []byte // the input was ascii. perhaps these could be strings +func diff(seqs sequences) []Diff { + // A limit on how deeply the LCS algorithm should search. The value is just a guess. + const maxDiffs = 30 + diff, _ := compute(seqs, twosided, maxDiffs/2) + return diff } -func (x *byteeq) eq(i, j int) bool { return x.a[i] == x.b[j] } -func (x *byteeq) substr(i, j int) string { return string(x.b[i:j]) } -func (x *byteeq) lena() int { return int(len(x.a)) } -func (x *byteeq) lenb() int { return int(len(x.b)) } - -type runeeq struct { - a, b []rune +// compute computes the list of differences between two sequences, +// along with the LCS. It is exercised directly by tests. +// The algorithm is one of {forward, backward, twosided}. +func compute(seqs sequences, algo func(*editGraph) lcs, limit int) ([]Diff, lcs) { + if limit <= 0 { + limit = 1 << 25 // effectively infinity + } + alen, blen := seqs.lengths() + g := &editGraph{ + seqs: seqs, + vf: newtriang(limit), + vb: newtriang(limit), + limit: limit, + ux: alen, + uy: blen, + delta: alen - blen, + } + lcs := algo(g) + diffs := lcs.toDiffs(alen, blen) + return diffs, lcs } -func (x *runeeq) eq(i, j int) bool { return x.a[i] == x.b[j] } -func (x *runeeq) substr(i, j int) string { return string(x.b[i:j]) } -func (x *runeeq) lena() int { return int(len(x.a)) } -func (x *runeeq) lenb() int { return int(len(x.b)) } - -type lineeq struct { - a, b []string -} +// editGraph carries the information for computing the lcs of two sequences. +type editGraph struct { + seqs sequences + vf, vb label // forward and backward labels -func (x *lineeq) eq(i, j int) bool { return x.a[i] == x.b[j] } -func (x *lineeq) substr(i, j int) string { return strings.Join(x.b[i:j], "") } -func (x *lineeq) lena() int { return int(len(x.a)) } -func (x *lineeq) lenb() int { return int(len(x.b)) } - -func neweq(a, b interface{}) eq { - switch x := a.(type) { - case []byte: - return &byteeq{a: x, b: b.([]byte)} - case []rune: - return &runeeq{a: x, b: b.([]rune)} - case []string: - return &lineeq{a: x, b: b.([]string)} - default: - panic(fmt.Sprintf("unexpected type %T in neweq", x)) - } + limit int // maximal value of D + // the bounding rectangle of the current edit graph + lx, ly, ux, uy int + delta int // common subexpression: (ux-lx)-(uy-ly) } -func (g *editGraph) fromlcs(lcs lcs) []Diff { - var ans []Diff +// toDiffs converts an LCS to a list of edits. +func (lcs lcs) toDiffs(alen, blen int) []Diff { + var diffs []Diff var pa, pb int // offsets in a, b for _, l := range lcs { - if pa < l.X && pb < l.Y { - ans = append(ans, Diff{pa, l.X, g.eq.substr(pb, l.Y)}) - } else if pa < l.X { - ans = append(ans, Diff{pa, l.X, ""}) - } else if pb < l.Y { - ans = append(ans, Diff{pa, l.X, g.eq.substr(pb, l.Y)}) + if pa < l.X || pb < l.Y { + diffs = append(diffs, Diff{pa, l.X, pb, l.Y}) } pa = l.X + l.Len pb = l.Y + l.Len } - if pa < g.eq.lena() && pb < g.eq.lenb() { - ans = append(ans, Diff{pa, g.eq.lena(), g.eq.substr(pb, g.eq.lenb())}) - } else if pa < g.eq.lena() { - ans = append(ans, Diff{pa, g.eq.lena(), ""}) - } else if pb < g.eq.lenb() { - ans = append(ans, Diff{pa, g.eq.lena(), g.eq.substr(pb, g.eq.lenb())}) + if pa < alen || pb < blen { + diffs = append(diffs, Diff{pa, alen, pb, blen}) } - return ans -} - -func newegraph(a, b interface{}, limit int) *editGraph { - if limit <= 0 { - limit = 1 << 25 // effectively infinity - } - var alen, blen int - switch a := a.(type) { - case []byte: - alen, blen = len(a), len(b.([]byte)) - case []rune: - alen, blen = len(a), len(b.([]rune)) - case []string: - alen, blen = len(a), len(b.([]string)) - default: - panic(fmt.Sprintf("unexpected type %T in newegraph", a)) - } - ans := &editGraph{eq: neweq(a, b), vf: newtriang(limit), vb: newtriang(limit), limit: int(limit), - ux: alen, uy: blen, delta: alen - blen} - return ans + return diffs } // --- FORWARD --- + // fdone decides if the forwward path has reached the upper right -// corner of the rectangele. If so, it also returns the computed lcs. +// corner of the rectangle. If so, it also returns the computed lcs. func (e *editGraph) fdone(D, k int) (bool, lcs) { // x, y, k are relative to the rectangle x := e.vf.get(D, k) @@ -138,7 +99,7 @@ func (e *editGraph) fdone(D, k int) (bool, lcs) { } // run the forward algorithm, until success or up to the limit on D. -func (e *editGraph) forward() lcs { +func forward(e *editGraph) lcs { e.setForward(0, 0, e.lx) if ok, ans := e.fdone(0, 0); ok { return ans @@ -197,13 +158,8 @@ func (e *editGraph) forwardlcs(D, k int) lcs { } // if (x-1,y-1)--(x,y) is a diagonal, prepend,x--,y--, continue y := x - k - realx, realy := x+e.lx, y+e.ly - if e.eq.eq(realx-1, realy-1) { - ans = prependlcs(ans, realx-1, realy-1) - x-- - } else { - panic("broken path") - } + ans = ans.prepend(x+e.lx-1, y+e.ly-1) + x-- } return ans } @@ -213,9 +169,8 @@ func (e *editGraph) forwardlcs(D, k int) lcs { func (e *editGraph) lookForward(k, relx int) int { rely := relx - k x, y := relx+e.lx, rely+e.ly - for x < e.ux && y < e.uy && e.eq.eq(x, y) { - x++ - y++ + if x < e.ux && y < e.uy { + x += e.seqs.commonPrefixLen(x, e.ux, y, e.uy) } return x } @@ -231,6 +186,7 @@ func (e *editGraph) getForward(d, k int) int { } // --- BACKWARD --- + // bdone decides if the backward path has reached the lower left corner func (e *editGraph) bdone(D, k int) (bool, lcs) { // x, y, k are relative to the rectangle @@ -243,7 +199,7 @@ func (e *editGraph) bdone(D, k int) (bool, lcs) { } // run the backward algorithm, until success or up to the limit on D. -func (e *editGraph) backward() lcs { +func backward(e *editGraph) lcs { e.setBackward(0, 0, e.ux) if ok, ans := e.bdone(0, 0); ok { return ans @@ -305,13 +261,8 @@ func (e *editGraph) backwardlcs(D, k int) lcs { continue } y := x - (k + e.delta) - realx, realy := x+e.lx, y+e.ly - if e.eq.eq(realx, realy) { - ans = appendlcs(ans, realx, realy) - x++ - } else { - panic("broken path") - } + ans = ans.append(x+e.lx, y+e.ly) + x++ } return ans } @@ -320,9 +271,8 @@ func (e *editGraph) backwardlcs(D, k int) lcs { func (e *editGraph) lookBackward(k, relx int) int { rely := relx - (k + e.delta) // forward k = k + e.delta x, y := relx+e.lx, rely+e.ly - for x > 0 && y > 0 && e.eq.eq(x-1, y-1) { - x-- - y-- + if x > 0 && y > 0 { + x -= e.seqs.commonSuffixLen(0, x, 0, y) } return x } @@ -340,7 +290,7 @@ func (e *editGraph) getBackward(d, k int) int { // -- TWOSIDED --- -func (e *editGraph) twosided() lcs { +func twosided(e *editGraph) lcs { // The termination condition could be improved, as either the forward // or backward pass could succeed before Myers' Lemma applies. // Aside from questions of efficiency (is the extra testing cost-effective) @@ -524,7 +474,7 @@ func (e *editGraph) twolcs(df, db, kf int) lcs { oldx, oldy := e.ux, e.uy e.ux = u e.uy = v - lcs = append(lcs, e.forward()...) + lcs = append(lcs, forward(e)...) e.ux, e.uy = oldx, oldy return lcs.sort() } diff --git a/internal/diff/lcs/old_test.go b/internal/diff/lcs/old_test.go index 1d6047694f3..0c894316fa5 100644 --- a/internal/diff/lcs/old_test.go +++ b/internal/diff/lcs/old_test.go @@ -5,57 +5,29 @@ package lcs import ( + "fmt" + "io/ioutil" + "log" "math/rand" + "strings" "testing" ) -func TestForwardOld(t *testing.T) { - for _, tx := range Btests { - lim := len(tx.a) + len(tx.b) - left, right := []byte(tx.a), []byte(tx.b) - g := newegraph(left, right, lim) - lcs := g.forward() - diffs := g.fromlcs(lcs) - check(t, tx.a, lcs, tx.lcs) - checkDiffs(t, tx.a, diffs, tx.b) - - g = newegraph(right, left, lim) - lcs = g.forward() - diffs = g.fromlcs(lcs) - check(t, tx.b, lcs, tx.lcs) - checkDiffs(t, tx.b, diffs, tx.a) - } -} +func TestAlgosOld(t *testing.T) { + for i, algo := range []func(*editGraph) lcs{forward, backward, twosided} { + t.Run(strings.Fields("forward backward twosided")[i], func(t *testing.T) { + for _, tx := range Btests { + lim := len(tx.a) + len(tx.b) -func TestBackwardOld(t *testing.T) { - for _, tx := range Btests { - lim := len(tx.a) + len(tx.b) - left, right := []byte(tx.a), []byte(tx.b) - g := newegraph(left, right, lim) - lcs := g.backward() - check(t, tx.a, lcs, tx.lcs) - diffs := g.fromlcs(lcs) - checkDiffs(t, tx.a, diffs, tx.b) - - g = newegraph(right, left, lim) - lcs = g.backward() - diffs = g.fromlcs(lcs) - check(t, tx.b, lcs, tx.lcs) - checkDiffs(t, tx.b, diffs, tx.a) - } -} + diffs, lcs := compute(stringSeqs{tx.a, tx.b}, algo, lim) + check(t, tx.a, lcs, tx.lcs) + checkDiffs(t, tx.a, diffs, tx.b) -func TestTwosidedOld(t *testing.T) { - // test both (a,b) and (b,a) - for _, tx := range Btests { - left, right := []byte(tx.a), []byte(tx.b) - lim := len(tx.a) + len(tx.b) - diffs, lcs := Compute(left, right, lim) - check(t, tx.a, lcs, tx.lcs) - checkDiffs(t, tx.a, diffs, tx.b) - diffs, lcs = Compute(right, left, lim) - check(t, tx.b, lcs, tx.lcs) - checkDiffs(t, tx.b, diffs, tx.a) + diffs, lcs = compute(stringSeqs{tx.b, tx.a}, algo, lim) + check(t, tx.b, lcs, tx.lcs) + checkDiffs(t, tx.b, diffs, tx.a) + } + }) } } @@ -66,31 +38,31 @@ func TestIntOld(t *testing.T) { if len(tx.a) < 2 || len(tx.b) < 2 { continue } - left := []byte(tx.a + lfill) - right := []byte(tx.b + rfill) + left := tx.a + lfill + right := tx.b + rfill lim := len(tx.a) + len(tx.b) - diffs, lcs := Compute(left, right, lim) - check(t, string(left), lcs, tx.lcs) - checkDiffs(t, string(left), diffs, string(right)) - diffs, lcs = Compute(right, left, lim) - check(t, string(right), lcs, tx.lcs) - checkDiffs(t, string(right), diffs, string(left)) - - left = []byte(lfill + tx.a) - right = []byte(rfill + tx.b) - diffs, lcs = Compute(left, right, lim) - check(t, string(left), lcs, tx.lcs) - checkDiffs(t, string(left), diffs, string(right)) - diffs, lcs = Compute(right, left, lim) - check(t, string(right), lcs, tx.lcs) - checkDiffs(t, string(right), diffs, string(left)) + diffs, lcs := compute(stringSeqs{left, right}, twosided, lim) + check(t, left, lcs, tx.lcs) + checkDiffs(t, left, diffs, right) + diffs, lcs = compute(stringSeqs{right, left}, twosided, lim) + check(t, right, lcs, tx.lcs) + checkDiffs(t, right, diffs, left) + + left = lfill + tx.a + right = rfill + tx.b + diffs, lcs = compute(stringSeqs{left, right}, twosided, lim) + check(t, left, lcs, tx.lcs) + checkDiffs(t, left, diffs, right) + diffs, lcs = compute(stringSeqs{right, left}, twosided, lim) + check(t, right, lcs, tx.lcs) + checkDiffs(t, right, diffs, left) } } -func TestSpecialOld(t *testing.T) { // needs lcs.fix - a := []byte("golang.org/x/tools/intern") - b := []byte("github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/intern") - diffs, lcs := Compute(a, b, 4) +func TestSpecialOld(t *testing.T) { // exercises lcs.fix + a := "golang.org/x/tools/intern" + b := "github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/intern" + diffs, lcs := compute(stringSeqs{a, b}, twosided, 4) if !lcs.valid() { t.Errorf("%d,%v", len(diffs), lcs) } @@ -101,7 +73,7 @@ func TestRegressionOld001(t *testing.T) { b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n" for i := 1; i < len(b); i++ { - diffs, lcs := Compute([]byte(a), []byte(b), int(i)) // 14 from gopls + diffs, lcs := compute(stringSeqs{a, b}, twosided, i) // 14 from gopls if !lcs.valid() { t.Errorf("%d,%v", len(diffs), lcs) } @@ -113,7 +85,7 @@ func TestRegressionOld002(t *testing.T) { a := "n\"\n)\n" b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n" for i := 1; i <= len(b); i++ { - diffs, lcs := Compute([]byte(a), []byte(b), int(i)) + diffs, lcs := compute(stringSeqs{a, b}, twosided, i) if !lcs.valid() { t.Errorf("%d,%v", len(diffs), lcs) } @@ -125,7 +97,7 @@ func TestRegressionOld003(t *testing.T) { a := "golang.org/x/hello v1.0.0\nrequire golang.org/x/unused v1" b := "golang.org/x/hello v1" for i := 1; i <= len(a); i++ { - diffs, lcs := Compute([]byte(a), []byte(b), int(i)) + diffs, lcs := compute(stringSeqs{a, b}, twosided, i) if !lcs.valid() { t.Errorf("%d,%v", len(diffs), lcs) } @@ -136,12 +108,16 @@ func TestRegressionOld003(t *testing.T) { func TestRandOld(t *testing.T) { rand.Seed(1) for i := 0; i < 1000; i++ { + // TODO(adonovan): use ASCII and bytesSeqs here? The use of + // non-ASCII isn't relevant to the property exercised by the test. a := []rune(randstr("abω", 16)) b := []rune(randstr("abωc", 16)) - g := newegraph(a, b, 24) // large enough to get true lcs - two := g.twosided() - forw := g.forward() - back := g.backward() + seq := runesSeqs{a, b} + + const lim = 24 // large enough to get true lcs + _, forw := compute(seq, forward, lim) + _, back := compute(seq, backward, lim) + _, two := compute(seq, twosided, lim) if lcslen(two) != lcslen(forw) || lcslen(forw) != lcslen(back) { t.Logf("\n%v\n%v\n%v", forw, back, two) t.Fatalf("%d forw:%d back:%d two:%d", i, lcslen(forw), lcslen(back), lcslen(two)) @@ -152,11 +128,40 @@ func TestRandOld(t *testing.T) { } } +// TestDiffAPI tests the public API functions (Diff{Bytes,Strings,Runes}) +// to ensure at least miminal parity of the three representations. +func TestDiffAPI(t *testing.T) { + for _, test := range []struct { + a, b string + wantStrings, wantBytes, wantRunes string + }{ + {"abcXdef", "abcxdef", "[{3 4 3 4}]", "[{3 4 3 4}]", "[{3 4 3 4}]"}, // ASCII + {"abcωdef", "abcΩdef", "[{3 5 3 5}]", "[{3 5 3 5}]", "[{3 4 3 4}]"}, // non-ASCII + } { + + gotStrings := fmt.Sprint(DiffStrings(test.a, test.b)) + if gotStrings != test.wantStrings { + t.Errorf("DiffStrings(%q, %q) = %v, want %v", + test.a, test.b, gotStrings, test.wantStrings) + } + gotBytes := fmt.Sprint(DiffBytes([]byte(test.a), []byte(test.b))) + if gotBytes != test.wantBytes { + t.Errorf("DiffBytes(%q, %q) = %v, want %v", + test.a, test.b, gotBytes, test.wantBytes) + } + gotRunes := fmt.Sprint(DiffRunes([]rune(test.a), []rune(test.b))) + if gotRunes != test.wantRunes { + t.Errorf("DiffRunes(%q, %q) = %v, want %v", + test.a, test.b, gotRunes, test.wantRunes) + } + } +} + func BenchmarkTwoOld(b *testing.B) { tests := genBench("abc", 96) for i := 0; i < b.N; i++ { for _, tt := range tests { - _, two := Compute([]byte(tt.before), []byte(tt.after), 100) + _, two := compute(stringSeqs{tt.before, tt.after}, twosided, 100) if !two.valid() { b.Error("check failed") } @@ -168,7 +173,7 @@ func BenchmarkForwOld(b *testing.B) { tests := genBench("abc", 96) for i := 0; i < b.N; i++ { for _, tt := range tests { - _, two := Compute([]byte(tt.before), []byte(tt.after), 100) + _, two := compute(stringSeqs{tt.before, tt.after}, forward, 100) if !two.valid() { b.Error("check failed") } @@ -201,3 +206,46 @@ func genBench(set string, n int) []struct{ before, after string } { } return ans } + +// This benchmark represents a common case for a diff command: +// large file with a single relatively small diff in the middle. +// (It's not clear whether this is representative of gopls workloads +// or whether it is important to gopls diff performance.) +// +// TODO(adonovan) opt: it could be much faster. For example, +// comparing a file against itself is about 10x faster than with the +// small deletion in the middle. Strangely, comparing a file against +// itself minus the last byte is faster still; I don't know why. +// There is much low-hanging fruit here for further improvement. +func BenchmarkLargeFileSmallDiff(b *testing.B) { + data, err := ioutil.ReadFile("old.go") // large file + if err != nil { + log.Fatal(err) + } + + n := len(data) + + src := string(data) + dst := src[:n*49/100] + src[n*51/100:] // remove 2% from the middle + b.Run("string", func(b *testing.B) { + for i := 0; i < b.N; i++ { + compute(stringSeqs{src, dst}, twosided, len(src)+len(dst)) + } + }) + + srcBytes := []byte(src) + dstBytes := []byte(dst) + b.Run("bytes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + compute(bytesSeqs{srcBytes, dstBytes}, twosided, len(srcBytes)+len(dstBytes)) + } + }) + + srcRunes := []rune(src) + dstRunes := []rune(dst) + b.Run("runes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + compute(runesSeqs{srcRunes, dstRunes}, twosided, len(srcRunes)+len(dstRunes)) + } + }) +} diff --git a/internal/diff/lcs/sequence.go b/internal/diff/lcs/sequence.go new file mode 100644 index 00000000000..2d72d263043 --- /dev/null +++ b/internal/diff/lcs/sequence.go @@ -0,0 +1,113 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +// This file defines the abstract sequence over which the LCS algorithm operates. + +// sequences abstracts a pair of sequences, A and B. +type sequences interface { + lengths() (int, int) // len(A), len(B) + commonPrefixLen(ai, aj, bi, bj int) int // len(commonPrefix(A[ai:aj], B[bi:bj])) + commonSuffixLen(ai, aj, bi, bj int) int // len(commonSuffix(A[ai:aj], B[bi:bj])) +} + +type stringSeqs struct{ a, b string } + +func (s stringSeqs) lengths() (int, int) { return len(s.a), len(s.b) } +func (s stringSeqs) commonPrefixLen(ai, aj, bi, bj int) int { + return commonPrefixLenString(s.a[ai:aj], s.b[bi:bj]) +} +func (s stringSeqs) commonSuffixLen(ai, aj, bi, bj int) int { + return commonSuffixLenString(s.a[ai:aj], s.b[bi:bj]) +} + +// The explicit capacity in s[i:j:j] leads to more efficient code. + +type bytesSeqs struct{ a, b []byte } + +func (s bytesSeqs) lengths() (int, int) { return len(s.a), len(s.b) } +func (s bytesSeqs) commonPrefixLen(ai, aj, bi, bj int) int { + return commonPrefixLenBytes(s.a[ai:aj:aj], s.b[bi:bj:bj]) +} +func (s bytesSeqs) commonSuffixLen(ai, aj, bi, bj int) int { + return commonSuffixLenBytes(s.a[ai:aj:aj], s.b[bi:bj:bj]) +} + +type runesSeqs struct{ a, b []rune } + +func (s runesSeqs) lengths() (int, int) { return len(s.a), len(s.b) } +func (s runesSeqs) commonPrefixLen(ai, aj, bi, bj int) int { + return commonPrefixLenRunes(s.a[ai:aj:aj], s.b[bi:bj:bj]) +} +func (s runesSeqs) commonSuffixLen(ai, aj, bi, bj int) int { + return commonSuffixLenRunes(s.a[ai:aj:aj], s.b[bi:bj:bj]) +} + +// TODO(adonovan): optimize these functions using ideas from: +// - https://go.dev/cl/408116 common.go +// - https://go.dev/cl/421435 xor_generic.go + +// TODO(adonovan): factor using generics when available, +// but measure performance impact. + +// commonPrefixLen* returns the length of the common prefix of a[ai:aj] and b[bi:bj]. +func commonPrefixLenBytes(a, b []byte) int { + n := min(len(a), len(b)) + i := 0 + for i < n && a[i] == b[i] { + i++ + } + return i +} +func commonPrefixLenRunes(a, b []rune) int { + n := min(len(a), len(b)) + i := 0 + for i < n && a[i] == b[i] { + i++ + } + return i +} +func commonPrefixLenString(a, b string) int { + n := min(len(a), len(b)) + i := 0 + for i < n && a[i] == b[i] { + i++ + } + return i +} + +// commonSuffixLen* returns the length of the common suffix of a[ai:aj] and b[bi:bj]. +func commonSuffixLenBytes(a, b []byte) int { + n := min(len(a), len(b)) + i := 0 + for i < n && a[len(a)-1-i] == b[len(b)-1-i] { + i++ + } + return i +} +func commonSuffixLenRunes(a, b []rune) int { + n := min(len(a), len(b)) + i := 0 + for i < n && a[len(a)-1-i] == b[len(b)-1-i] { + i++ + } + return i +} +func commonSuffixLenString(a, b string) int { + n := min(len(a), len(b)) + i := 0 + for i < n && a[len(a)-1-i] == b[len(b)-1-i] { + i++ + } + return i +} + +func min(x, y int) int { + if x < y { + return x + } else { + return y + } +} diff --git a/internal/diff/ndiff.go b/internal/diff/ndiff.go index 4dd83237af6..050b08ded46 100644 --- a/internal/diff/ndiff.go +++ b/internal/diff/ndiff.go @@ -19,6 +19,7 @@ func Strings(before, after string) []Edit { } if stringIsASCII(before) && stringIsASCII(after) { + // TODO(adonovan): opt: specialize diffASCII for strings. return diffASCII([]byte(before), []byte(after)) } return diffRunes([]rune(before), []rune(after)) @@ -38,18 +39,18 @@ func Bytes(before, after []byte) []Edit { } func diffASCII(before, after []byte) []Edit { - diffs, _ := lcs.Compute(before, after, maxDiffs/2) + diffs := lcs.DiffBytes(before, after) // Convert from LCS diffs. res := make([]Edit, len(diffs)) for i, d := range diffs { - res[i] = Edit{d.Start, d.End, d.Text} + res[i] = Edit{d.Start, d.End, string(after[d.ReplStart:d.ReplEnd])} } return res } func diffRunes(before, after []rune) []Edit { - diffs, _ := lcs.Compute(before, after, maxDiffs/2) + diffs := lcs.DiffRunes(before, after) // The diffs returned by the lcs package use indexes // into whatever slice was passed in. @@ -61,16 +62,12 @@ func diffRunes(before, after []rune) []Edit { utf8Len += runesLen(before[lastEnd:d.Start]) // text between edits start := utf8Len utf8Len += runesLen(before[d.Start:d.End]) // text deleted by this edit - res[i] = Edit{start, utf8Len, d.Text} + res[i] = Edit{start, utf8Len, string(after[d.ReplStart:d.ReplEnd])} lastEnd = d.End } return res } -// maxDiffs is a limit on how deeply the lcs algorithm should search -// the value is just a guess -const maxDiffs = 30 - // runes is like []rune(string(bytes)) without the duplicate allocation. func runes(bytes []byte) []rune { n := utf8.RuneCount(bytes) From 6ad27d09a5dc4db6a65233a99a1f98a4805b29ec Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 16 Dec 2022 16:31:15 -0500 Subject: [PATCH 554/723] gopls/internal/robustio: FileID, a portable file identifier This change adds GetFileID(filename), which returns a comparable Go value that uniquely identifies a file in the file system. On POSIX is a (device, inode) pair. On Windows it is a (volume, index) pair, i.e. much the same thing, except that the system calls to get at it are different and more expensive. The need for this has popped up twice this month in x/tools. Change-Id: I96c00663c0b525aaa7f49155b01015e4ce79110b Reviewed-on: https://go-review.googlesource.com/c/tools/+/458200 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro --- gopls/internal/robustio/robustio.go | 12 ++++ gopls/internal/robustio/robustio_posix.go | 25 ++++++++ gopls/internal/robustio/robustio_test.go | 70 +++++++++++++++++++++ gopls/internal/robustio/robustio_windows.go | 20 ++++++ 4 files changed, 127 insertions(+) create mode 100644 gopls/internal/robustio/robustio_posix.go create mode 100644 gopls/internal/robustio/robustio_test.go diff --git a/gopls/internal/robustio/robustio.go b/gopls/internal/robustio/robustio.go index 15b33773cf5..3241f1d3a7d 100644 --- a/gopls/internal/robustio/robustio.go +++ b/gopls/internal/robustio/robustio.go @@ -51,3 +51,15 @@ func RemoveAll(path string) error { func IsEphemeralError(err error) bool { return isEphemeralError(err) } + +// A FileID uniquely identifies a file in the file system. +// +// If GetFileID(name1) == GetFileID(name2), the two file names denote the same file. +// A FileID is comparable, and thus suitable for use as a map key. +type FileID struct { + device, inode uint64 +} + +// GetFileID returns the file system's identifier for the file. +// Like os.Stat, it reads through symbolic links. +func GetFileID(filename string) (FileID, error) { return getFileID(filename) } diff --git a/gopls/internal/robustio/robustio_posix.go b/gopls/internal/robustio/robustio_posix.go new file mode 100644 index 00000000000..f22d0e9ed23 --- /dev/null +++ b/gopls/internal/robustio/robustio_posix.go @@ -0,0 +1,25 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !windows && !plan9 +// +build !windows,!plan9 + +package robustio + +import ( + "os" + "syscall" +) + +func getFileID(filename string) (FileID, error) { + fi, err := os.Stat(filename) + if err != nil { + return FileID{}, err + } + stat := fi.Sys().(*syscall.Stat_t) + return FileID{ + device: uint64(stat.Dev), // (int32 on darwin, uint64 on linux) + inode: stat.Ino, + }, nil +} diff --git a/gopls/internal/robustio/robustio_test.go b/gopls/internal/robustio/robustio_test.go new file mode 100644 index 00000000000..31c9bdae6de --- /dev/null +++ b/gopls/internal/robustio/robustio_test.go @@ -0,0 +1,70 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package robustio_test + +import ( + "os" + "path/filepath" + "testing" + + "golang.org/x/tools/gopls/internal/robustio" +) + +func TestFileID(t *testing.T) { + // A nonexistent file has no ID. + nonexistent := filepath.Join(t.TempDir(), "nonexistent") + if _, err := robustio.GetFileID(nonexistent); err == nil { + t.Fatalf("GetFileID(nonexistent) succeeded unexpectedly") + } + + // A regular file has an ID. + real := filepath.Join(t.TempDir(), "real") + if err := os.WriteFile(real, nil, 0644); err != nil { + t.Fatalf("can't create regular file: %v", err) + } + realID, err := robustio.GetFileID(real) + if err != nil { + t.Fatalf("can't get ID of regular file: %v", err) + } + + // A second regular file has a different ID. + real2 := filepath.Join(t.TempDir(), "real2") + if err := os.WriteFile(real2, nil, 0644); err != nil { + t.Fatalf("can't create second regular file: %v", err) + } + real2ID, err := robustio.GetFileID(real2) + if err != nil { + t.Fatalf("can't get ID of second regular file: %v", err) + } + if realID == real2ID { + t.Errorf("realID %+v != real2ID %+v", realID, real2ID) + } + + // A symbolic link has the same ID as its target. + symlink := filepath.Join(t.TempDir(), "symlink") + if err := os.Symlink(real, symlink); err != nil { + t.Fatalf("can't create symbolic link: %v", err) + } + symlinkID, err := robustio.GetFileID(symlink) + if err != nil { + t.Fatalf("can't get ID of symbolic link: %v", err) + } + if realID != symlinkID { + t.Errorf("realID %+v != symlinkID %+v", realID, symlinkID) + } + + // Two hard-linked files have the same ID. + hardlink := filepath.Join(t.TempDir(), "hardlink") + if err := os.Link(real, hardlink); err != nil { + t.Fatal(err) + } + hardlinkID, err := robustio.GetFileID(hardlink) + if err != nil { + t.Fatalf("can't get ID of hard link: %v", err) + } + if hardlinkID != realID { + t.Errorf("realID %+v != hardlinkID %+v", realID, hardlinkID) + } +} diff --git a/gopls/internal/robustio/robustio_windows.go b/gopls/internal/robustio/robustio_windows.go index d16e976aad6..98189560bae 100644 --- a/gopls/internal/robustio/robustio_windows.go +++ b/gopls/internal/robustio/robustio_windows.go @@ -24,3 +24,23 @@ func isEphemeralError(err error) bool { } return false } + +func getFileID(filename string) (FileID, error) { + filename16, err := syscall.UTF16PtrFromString(filename) + if err != nil { + return FileID{}, err + } + h, err := syscall.CreateFile(filename16, 0, 0, nil, syscall.OPEN_EXISTING, uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS), 0) + if err != nil { + return FileID{}, err + } + defer syscall.CloseHandle(h) + var i syscall.ByHandleFileInformation + if err := syscall.GetFileInformationByHandle(h, &i); err != nil { + return FileID{}, err + } + return FileID{ + device: uint64(i.VolumeSerialNumber), + inode: uint64(i.FileIndexHigh)<<32 | uint64(i.FileIndexLow), + }, nil +} From 21f61007ced51f86fc9503a15a06983e3ebfce00 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 20 Dec 2022 12:47:01 -0500 Subject: [PATCH 555/723] internal/lsp/debug: fix broken template CL 453815 inadvertently broke the main debug template by removing the link from View->Session. Fix this breakage by removing the view summary from the main page. Views can still be inspected by clicking on the session. Change-Id: Ia04897526f3d98d6d5f6a07c712ad9f0916f92f4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/458535 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan --- gopls/internal/lsp/debug/serve.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go index abc939eff7e..6934adf490f 100644 --- a/gopls/internal/lsp/debug/serve.go +++ b/gopls/internal/lsp/debug/serve.go @@ -764,8 +764,6 @@ var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
      {{range .State.Caches}}
    • {{template "cachelink" .ID}}
    • {{end}}

    Sessions

      {{range .State.Sessions}}
    • {{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}
    • {{end}}
    -

    Views

    -
      {{range .State.Views}}
    • {{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}
    • {{end}}

    Clients

      {{range .State.Clients}}
    • {{template "clientlink" .Session.ID}}
    • {{end}}

    Servers

    From 1627e959e08c7b27dcad55614c132ba354018c33 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 20 Dec 2022 15:03:21 -0500 Subject: [PATCH 556/723] gopls/internal/lsp: more comment tweaks post-//line support Change-Id: Ia44de7449fd0d60112dd71e372813bd8b3846175 Reviewed-on: https://go-review.googlesource.com/c/tools/+/458556 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Alan Donovan --- gopls/internal/lsp/protocol/span.go | 4 +--- gopls/internal/lsp/source/util.go | 14 +++++--------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 86b14c2caeb..78e180c71a0 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -29,8 +29,7 @@ // TODO(adonovan): simplify this picture. Eliminate the optionality of // span.{Span,Point}'s position and offset fields: work internally in // terms of offsets (like span.Range), and require a mapper to convert -// them to protocol (UTF-16) line/col form. Stop honoring //line -// directives. +// them to protocol (UTF-16) line/col form. package protocol @@ -162,7 +161,6 @@ func (m *ColumnMapper) Position(p span.Point) (Position, error) { // offset within m.Content. func (m *ColumnMapper) OffsetPosition(offset int) (Position, error) { // We use span.ToPosition for its "line+1 at EOF" workaround. - // TODO(adonovan): ToPosition honors //line directives. It probably shouldn't. line, _, err := span.ToPosition(m.TokFile, offset) if err != nil { return Position{}, fmt.Errorf("OffsetPosition: %v", err) diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index fa91451d625..301e0173559 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -26,20 +26,16 @@ import ( // MappedRange provides mapped protocol.Range for a span.Range, accounting for // UTF-16 code points. // -// TOOD(adonovan): eliminate this type. All callers need either m, or a protocol.Range. +// TOOD(adonovan): eliminate this type. Replace all uses by an +// explicit pair (span.Range, protocol.ColumnMapper), and an operation +// to map both to a protocol.Range. type MappedRange struct { spanRange span.Range // the range in the compiled source (package.CompiledGoFiles) m *protocol.ColumnMapper // a mapper of the edited source (package.GoFiles) } -// NewMappedRange returns a MappedRange for the given file and valid start/end token.Pos. -// -// By convention, start and end are assumed to be positions in the compiled (== -// type checked) source, whereas the column mapper m maps positions in the -// user-edited source. Note that these may not be the same, as when using goyacc or CGo: -// CompiledGoFiles contains generated files, whose positions (via -// token.File.Position) point to locations in the edited file -- the file -// containing `import "C"`. +// NewMappedRange returns a MappedRange for the given file and +// start/end positions, which must be valid within m.TokFile. func NewMappedRange(m *protocol.ColumnMapper, start, end token.Pos) MappedRange { return MappedRange{ spanRange: span.NewRange(m.TokFile, start, end), From 7efffe1575e4bea394d7a843d6a5dbd3d57ff348 Mon Sep 17 00:00:00 2001 From: Dung Le Date: Thu, 22 Dec 2022 03:38:42 +0700 Subject: [PATCH 557/723] gopls/internal/regtest: harmless CL used for benchmark test Change-Id: I906fc5ea114e322ff8b05c62bc68e0fd8c0aa032 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459015 gopls-CI: kokoro Reviewed-by: Michael Pratt Run-TryBot: Dylan Le TryBot-Result: Gopher Robot --- gopls/internal/regtest/misc/rename_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 7a7c4ec1ce5..4dc6f8ea78e 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -658,6 +658,7 @@ func main() { // Check if the new package name exists. env.RegexpSearch("nested/a.go", "package nested") env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `_ "mod.com/nested"`) env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) }) From d462c83f2dbbbe35f6d02faabcfbfba2f9dcc5ae Mon Sep 17 00:00:00 2001 From: Viktor Blomqvist Date: Thu, 24 Nov 2022 17:43:22 +0100 Subject: [PATCH 558/723] gopls/internal/lsp: Replace input text when completing a definition Completing a definition is based on a parsed identifier and should replace the range of that identifier with the completion. Fixes golang/go#56852 Change-Id: I44c751d1db6d55f90113fb918dda344bb7d62f87 Reviewed-on: https://go-review.googlesource.com/c/tools/+/453335 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Peter Weinberger Reviewed-by: Robert Findley Run-TryBot: Peter Weinberger --- .../lsp/source/completion/definition.go | 7 +- .../regtest/completion/completion_test.go | 85 +++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/gopls/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go index fe41c55b3b8..cc7b42e81a2 100644 --- a/gopls/internal/lsp/source/completion/definition.go +++ b/gopls/internal/lsp/source/completion/definition.go @@ -36,11 +36,12 @@ func definition(path []ast.Node, obj types.Object, tokFile *token.File, fh sourc // can't happen return nil, nil } - pos := path[0].Pos() + start := path[0].Pos() + end := path[0].End() sel := &Selection{ content: "", - cursor: pos, - rng: span.NewRange(tokFile, pos, pos), + cursor: start, + rng: span.NewRange(tokFile, start, end), } var ans []CompletionItem diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index caa70e8ae4a..3d5afe4de68 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -12,6 +12,7 @@ import ( "golang.org/x/tools/gopls/internal/hooks" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -596,6 +597,90 @@ func BenchmarkFoo() }) } +// Test that completing a definition replaces source text when applied, golang/go#56852. +// Note: With go <= 1.16 the completions does not add parameters and fails these tests. +func TestDefinitionReplaceRange(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + + const mod = ` +-- go.mod -- +module mod.com + +go 1.17 +` + + tests := []struct { + name string + before, after string + }{ + { + name: "func TestMa", + before: ` +package foo_test + +func TestMa +`, + after: ` +package foo_test + +func TestMain(m *testing.M) +`, + }, + { + name: "func TestSome", + before: ` +package foo_test + +func TestSome +`, + after: ` +package foo_test + +func TestSome(t *testing.T) +`, + }, + { + name: "func Bench", + before: ` +package foo_test + +func Bench +`, + // Note: Snippet with escaped }. + after: ` +package foo_test + +func Benchmark${1:Xxx}(b *testing.B) { +$0 +\} +`, + }, + } + + Run(t, mod, func(t *testing.T, env *Env) { + env.CreateBuffer("foo_test.go", "") + + for _, tst := range tests { + tst.before = strings.Trim(tst.before, "\n") + tst.after = strings.Trim(tst.after, "\n") + env.SetBufferContent("foo_test.go", tst.before) + + pos := env.RegexpSearch("foo_test.go", tst.name) + pos.Column = len(tst.name) + completions := env.Completion("foo_test.go", pos) + if len(completions.Items) == 0 { + t.Fatalf("no completion items") + } + + env.AcceptCompletion("foo_test.go", pos, completions.Items[0]) + env.Await(env.DoneWithChange()) + if buf := env.Editor.BufferText("foo_test.go"); buf != tst.after { + t.Errorf("incorrect completion: got %q, want %q", buf, tst.after) + } + } + }) +} + func TestGoWorkCompletion(t *testing.T) { const files = ` -- go.work -- From 3be06475e084b20fbd505539810c5233cbc285b8 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Tue, 13 Dec 2022 10:11:21 -0500 Subject: [PATCH 559/723] gopls/symbols: call source.Document symbols only for Go files source.DocumentSymbols was being called for all non-template files, but only works for Go files. Change-Id: Id8d715eac4b8da99fcbf6b9d02120e50c2f4af69 Reviewed-on: https://go-review.googlesource.com/c/tools/+/457215 Run-TryBot: Peter Weinberger Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/symbols.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/gopls/internal/lsp/symbols.go b/gopls/internal/lsp/symbols.go index 0cc0b33a5b5..40fa0d05e11 100644 --- a/gopls/internal/lsp/symbols.go +++ b/gopls/internal/lsp/symbols.go @@ -7,11 +7,11 @@ package lsp import ( "context" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/template" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" ) func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]interface{}, error) { @@ -24,10 +24,13 @@ func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSy return []interface{}{}, err } var docSymbols []protocol.DocumentSymbol - if snapshot.View().FileKind(fh) == source.Tmpl { + switch snapshot.View().FileKind(fh) { + case source.Tmpl: docSymbols, err = template.DocumentSymbols(snapshot, fh) - } else { + case source.Go: docSymbols, err = source.DocumentSymbols(ctx, snapshot, fh) + default: + return []interface{}{}, nil } if err != nil { event.Error(ctx, "DocumentSymbols failed", err, tag.URI.Of(fh.URI())) From 6546d82b229aa5bd9ebcc38b09587462e34b48b6 Mon Sep 17 00:00:00 2001 From: Michael Pratt Date: Thu, 22 Dec 2022 17:59:10 +0000 Subject: [PATCH 560/723] Revert "gopls/internal/regtest: harmless CL used for benchmark test" This reverts commit 7efffe1575e4bea394d7a843d6a5dbd3d57ff348. Reason for revert: Not needed, and this serves as another test Change-Id: Ide265dac8fdd3edb705b513fbbc5463f0039db2d Reviewed-on: https://go-review.googlesource.com/c/tools/+/459235 TryBot-Result: Gopher Robot Reviewed-by: Dylan Le Run-TryBot: Michael Pratt Auto-Submit: Michael Pratt gopls-CI: kokoro --- gopls/internal/regtest/misc/rename_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 4dc6f8ea78e..7a7c4ec1ce5 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -658,7 +658,6 @@ func main() { // Check if the new package name exists. env.RegexpSearch("nested/a.go", "package nested") env.RegexpSearch("main.go", "mod.com/nested") - env.RegexpSearch("main.go", `_ "mod.com/nested"`) env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) }) From 44395ffddcf9128b5146a8cd52e557b72b00b1f3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 21 Dec 2022 12:13:36 -0500 Subject: [PATCH 561/723] gopls/internal/lsp/source: avoid unnecessary transitive rdeps Most callers of ReverseDependencies need only the direct importers. The new 'transitive bool' flag lets them express that and do less work. Change-Id: Ib0a6b1efc892424dde0e4d2c30ad67c91212fc56 Reviewed-on: https://go-review.googlesource.com/c/tools/+/458855 gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- gopls/internal/lsp/cache/graph.go | 6 ++-- gopls/internal/lsp/cache/snapshot.go | 25 ++++++++++++---- gopls/internal/lsp/source/call_hierarchy.go | 14 ++++----- gopls/internal/lsp/source/references.go | 29 ++++++------------- gopls/internal/lsp/source/rename.go | 17 ++--------- gopls/internal/lsp/source/view.go | 5 ++-- .../internal/regtest/misc/references_test.go | 2 ++ 7 files changed, 44 insertions(+), 54 deletions(-) diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index 5df0234e2d6..aaabbdfc43b 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -103,10 +103,10 @@ func (g *metadataGraph) build() { } } -// reverseTransitiveClosure returns a new mapping containing the +// reverseReflexiveTransitiveClosure returns a new mapping containing the // metadata for the specified packages along with any package that -// transitively imports one of them, keyed by ID. -func (g *metadataGraph) reverseTransitiveClosure(ids ...PackageID) map[PackageID]*source.Metadata { +// transitively imports one of them, keyed by ID, including all the initial packages. +func (g *metadataGraph) reverseReflexiveTransitiveClosure(ids ...PackageID) map[PackageID]*source.Metadata { seen := make(map[PackageID]*source.Metadata) var visitAll func([]PackageID) visitAll = func(ids []PackageID) { diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index ea57d574f57..bda36902319 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -819,19 +819,32 @@ func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source return metas, nil } -func (s *snapshot) ReverseDependencies(ctx context.Context, id PackageID) (map[PackageID]*source.Metadata, error) { +func (s *snapshot) ReverseDependencies(ctx context.Context, id PackageID, transitive bool) (map[PackageID]*source.Metadata, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } s.mu.Lock() meta := s.meta s.mu.Unlock() - rdeps := meta.reverseTransitiveClosure(id) - // Remove the original package ID from the map. - // TODO(adonovan): we should make ReverseDependencies and - // reverseTransitiveClosure consistent wrt reflexiveness. - delete(rdeps, id) + var rdeps map[PackageID]*source.Metadata + if transitive { + rdeps = meta.reverseReflexiveTransitiveClosure(id) + + // Remove the original package ID from the map. + // (Callers all want irreflexivity but it's easier + // to compute reflexively then subtract.) + delete(rdeps, id) + + } else { + // direct reverse dependencies + rdeps = make(map[PackageID]*source.Metadata) + for _, rdepID := range meta.importedBy[id] { + if rdep := meta.metadata[rdepID]; rdep != nil { + rdeps[rdepID] = rdep + } + } + } return rdeps, nil } diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index b8e11bc8777..076aed0c3d7 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -96,19 +96,17 @@ func toProtocolIncomingCalls(ctx context.Context, snapshot Snapshot, refs []*Ref event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name)) continue } + loc := protocol.Location{ URI: callItem.URI, Range: callItem.Range, } - - if incomingCall, ok := incomingCalls[loc]; ok { - incomingCall.FromRanges = append(incomingCall.FromRanges, refRange) - continue - } - incomingCalls[loc] = &protocol.CallHierarchyIncomingCall{ - From: callItem, - FromRanges: []protocol.Range{refRange}, + call, ok := incomingCalls[loc] + if !ok { + call = &protocol.CallHierarchyIncomingCall{From: callItem} + incomingCalls[loc] = call } + call.FromRanges = append(call.FromRanges, refRange) } incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls)) diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 33ecdc32e2e..f51f7f1f0da 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -52,29 +52,14 @@ func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protoco } targetPkg := metas[len(metas)-1] // widest package - // Find external references to the package. - rdeps, err := snapshot.ReverseDependencies(ctx, targetPkg.ID) + // Find external direct references to the package (imports). + rdeps, err := snapshot.ReverseDependencies(ctx, targetPkg.ID, false) if err != nil { return nil, err } var refs []*ReferenceInfo for _, rdep := range rdeps { - // Optimization: skip this loop (parsing) if rdep - // doesn't directly import the target package. - // (This logic also appears in rename; perhaps we need - // a ReverseDirectDependencies method?) - direct := false - for _, importedID := range rdep.DepsByImpPath { - if importedID == targetPkg.ID { - direct = true - break - } - } - if !direct { - continue - } - for _, uri := range rdep.CompiledGoFiles { fh, err := snapshot.GetFile(ctx, uri) if err != nil { @@ -199,12 +184,16 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i // Only search dependents if the object is exported. if qo.obj.Exported() { - rdeps, err := snapshot.ReverseDependencies(ctx, qo.pkg.ID()) + // If obj is a package-level object, we need only search + // among direct reverse dependencies. + // TODO(adonovan): opt: this will still spuriously search + // transitively for (e.g.) capitalized local variables. + // We could do better by checking for an objectpath. + transitive := qo.obj.Pkg().Scope().Lookup(qo.obj.Name()) != qo.obj + rdeps, err := snapshot.ReverseDependencies(ctx, qo.pkg.ID(), transitive) if err != nil { return nil, err } - // TODO(adonovan): opt: if obj is a package-level object, we need - // only search among direct reverse dependencies. ids := make([]PackageID, 0, len(rdeps)) for _, rdep := range rdeps { ids = append(ids, rdep.ID) diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 93048e034b7..fa98f466a58 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -268,6 +268,7 @@ func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath PackageP } if m.Module == nil { + // This check will always fail under Bazel. return nil, fmt.Errorf("cannot rename package: missing module information for package %q", m.PkgPath) } @@ -364,7 +365,7 @@ func renamePackageClause(ctx context.Context, m *Metadata, snapshot Snapshot, ne // // Edits are written into the edits map. func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath ImportPath, newName PackageName, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { - rdeps, err := snapshot.ReverseDependencies(ctx, m.ID) + rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, false) // find direct importers if err != nil { return err } @@ -376,20 +377,6 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath continue // for renaming, these variants are redundant } - // Optimization: skip packages that don't directly import m. - // (This logic also appears in 'references'; perhaps we need - // Snapshot.ReverseDirectDependencies?) - direct := false - for _, importedID := range rdep.DepsByImpPath { - if importedID == m.ID { - direct = true - break - } - } - if !direct { - continue - } - for _, uri := range rdep.CompiledGoFiles { if seen.add(uri, m.PkgPath) { continue diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index b6bab12c180..0aeac542be9 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -177,8 +177,9 @@ type Snapshot interface { // ReverseDependencies returns a new mapping whose entries are // the ID and Metadata of each package in the workspace that - // transitively depends on the package denoted by id, excluding id itself. - ReverseDependencies(ctx context.Context, id PackageID) (map[PackageID]*Metadata, error) + // directly or transitively depend on the package denoted by id, + // excluding id itself. + ReverseDependencies(ctx context.Context, id PackageID, transitive bool) (map[PackageID]*Metadata, error) // CachedImportPaths returns all the imported packages loaded in this // snapshot, indexed by their package path (not import path, despite the name) diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index 7cff19c8d32..0e8ff45561a 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -40,6 +40,7 @@ func main() { t.Fatal(err) } if len(refs) != 2 { + // TODO(adonovan): make this assertion less maintainer-hostile. t.Fatalf("got %v reference(s), want 2", len(refs)) } // The first reference is guaranteed to be the definition. @@ -152,6 +153,7 @@ func main() { pos := env.RegexpSearch(f, test.packageName) refs := env.References(fmt.Sprintf("%s/a.go", test.packageName), pos) if len(refs) != test.wantRefCount { + // TODO(adonovan): make this assertion less maintainer-hostile. t.Fatalf("got %v reference(s), want %d", len(refs), test.wantRefCount) } var refURIs []string From ef1ec5d7872fcd54023cea75d9be79bea8db2271 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 27 Dec 2022 16:58:50 -0500 Subject: [PATCH 562/723] gopls/internal/lsp/safetoken: fix error message This change uses [x:y] notation in the error message to indicate an inclusive interval, which is what the logic actually implements. We also include the file name. Also, put the inequalities in lo <= x <= hi form for clarity, and inline/simplify the token.File.Pos/Offset calls to avoid the redundant double-check. Updates golang/go#57484 Change-Id: I63e10d0e6659aae2613b5b7d51e87a8a4bfb225d Reviewed-on: https://go-review.googlesource.com/c/tools/+/459637 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/safetoken/safetoken.go | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/gopls/internal/lsp/safetoken/safetoken.go b/gopls/internal/lsp/safetoken/safetoken.go index 6898df0bd74..9f804f7e13d 100644 --- a/gopls/internal/lsp/safetoken/safetoken.go +++ b/gopls/internal/lsp/safetoken/safetoken.go @@ -11,26 +11,26 @@ import ( "go/token" ) -// Offset returns tok.Offset(pos), but first checks that the pos is in range +// Offset returns f.Offset(pos), but first checks that the pos is in range // for the given file. -func Offset(tf *token.File, pos token.Pos) (int, error) { - if !InRange(tf, pos) { - return -1, fmt.Errorf("pos %v is not in range for file [%v:%v)", pos, tf.Base(), tf.Base()+tf.Size()) +func Offset(f *token.File, pos token.Pos) (int, error) { + if !InRange(f, pos) { + return -1, fmt.Errorf("pos %d is not in range [%d:%d] of file %s", + pos, f.Base(), f.Base()+f.Size(), f.Name()) } - return tf.Offset(pos), nil + return int(pos) - f.Base(), nil } -// Pos returns tok.Pos(offset), but first checks that the offset is valid for +// Pos returns f.Pos(offset), but first checks that the offset is valid for // the given file. -func Pos(tf *token.File, offset int) (token.Pos, error) { - if offset < 0 || offset > tf.Size() { - return token.NoPos, fmt.Errorf("offset %v is not in range for file of size %v", offset, tf.Size()) +func Pos(f *token.File, offset int) (token.Pos, error) { + if !(0 <= offset && offset <= f.Size()) { + return token.NoPos, fmt.Errorf("offset %d is not in range for file %s of size %d", offset, f.Name(), f.Size()) } - return tf.Pos(offset), nil + return token.Pos(f.Base() + offset), nil } -// InRange reports whether the given position is in the given token.File. -func InRange(tf *token.File, pos token.Pos) bool { - size := tf.Pos(tf.Size()) - return int(pos) >= tf.Base() && pos <= size +// InRange reports whether file f contains position pos. +func InRange(f *token.File, pos token.Pos) bool { + return token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) } From 85e6ad773c681df9702c85baf7808ed1324de4e3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 28 Dec 2022 10:00:32 -0500 Subject: [PATCH 563/723] gopls/internal/lsp/safetoken: fix bug in Offset at EOF During parser error recovery, it may synthesize tokens such as RBRACE at EOF, causing the End position of the incomplete syntax nodes to be computed as Rbrace+len("}"), which is out of bounds, and would cause token.File.Offset to panic, or safetoken.Offset to return an error. This change is a workaround in gopls so that such End positions are considered valid, and are mapped to the end of the file. Also - a regression test. - remove safetoken.InRange, to avoid ambiguity. It was used in only one place (and dubiously even there). Fixes golang/go#57484 Updates golang/go#57490 Change-Id: I75bbe4f3b3c54aedf47a36649e8ee56bca205c8d Reviewed-on: https://go-review.googlesource.com/c/tools/+/459735 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/cache/analysis.go | 2 +- gopls/internal/lsp/safetoken/safetoken.go | 41 +++++++++++++++---- .../internal/lsp/safetoken/safetoken_test.go | 32 ++++++++++++++- gopls/internal/lsp/semantic.go | 6 +-- 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 32fcdf7f758..8185f23bed2 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -1061,7 +1061,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) { diagnostic, err := toGobDiagnostic(posToLocation, d) if err != nil { - bug.Reportf("internal error converting diagnostic from analyzer %s: %v", analyzer.Name, err) + bug.Reportf("internal error converting diagnostic from analyzer %q: %v", analyzer.Name, err) return } diagnostics = append(diagnostics, diagnostic) diff --git a/gopls/internal/lsp/safetoken/safetoken.go b/gopls/internal/lsp/safetoken/safetoken.go index 9f804f7e13d..200935612e0 100644 --- a/gopls/internal/lsp/safetoken/safetoken.go +++ b/gopls/internal/lsp/safetoken/safetoken.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 safetoken provides wrappers around methods in go/token, that return -// errors rather than panicking. +// Package safetoken provides wrappers around methods in go/token, +// that return errors rather than panicking. It also provides a +// central place for workarounds in the underlying packages. package safetoken import ( @@ -11,18 +12,36 @@ import ( "go/token" ) -// Offset returns f.Offset(pos), but first checks that the pos is in range -// for the given file. +// Offset returns f.Offset(pos), but first checks that the file +// contains the pos. +// +// The definition of "contains" here differs from that of token.File +// in order to work around a bug in the parser (issue #57490): during +// error recovery, the parser may create syntax nodes whose computed +// End position is 1 byte beyond EOF, which would cause +// token.File.Offset to panic. The workaround is that this function +// accepts a Pos that is exactly 1 byte beyond EOF and maps it to the +// EOF offset. +// +// The use of this function instead of (*token.File).Offset is +// mandatory in the gopls codebase; this is enforced by static check. func Offset(f *token.File, pos token.Pos) (int, error) { - if !InRange(f, pos) { + if !inRange(f, pos) { + // Accept a Pos that is 1 byte beyond EOF, + // and map it to the EOF offset. + // (Workaround for #57490.) + if int(pos) == f.Base()+f.Size()+1 { + return f.Size(), nil + } + return -1, fmt.Errorf("pos %d is not in range [%d:%d] of file %s", pos, f.Base(), f.Base()+f.Size(), f.Name()) } return int(pos) - f.Base(), nil } -// Pos returns f.Pos(offset), but first checks that the offset is valid for -// the given file. +// Pos returns f.Pos(offset), but first checks that the offset is +// non-negative and not larger than the size of the file. func Pos(f *token.File, offset int) (token.Pos, error) { if !(0 <= offset && offset <= f.Size()) { return token.NoPos, fmt.Errorf("offset %d is not in range for file %s of size %d", offset, f.Name(), f.Size()) @@ -30,7 +49,11 @@ func Pos(f *token.File, offset int) (token.Pos, error) { return token.Pos(f.Base() + offset), nil } -// InRange reports whether file f contains position pos. -func InRange(f *token.File, pos token.Pos) bool { +// inRange reports whether file f contains position pos, +// according to the invariants of token.File. +// +// This function is not public because of the ambiguity it would +// create w.r.t. the definition of "contains". Use Offset instead. +func inRange(f *token.File, pos token.Pos) bool { return token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) } diff --git a/gopls/internal/lsp/safetoken/safetoken_test.go b/gopls/internal/lsp/safetoken/safetoken_test.go index 00bf7bd9346..452820f679b 100644 --- a/gopls/internal/lsp/safetoken/safetoken_test.go +++ b/gopls/internal/lsp/safetoken/safetoken_test.go @@ -5,26 +5,54 @@ package safetoken_test import ( + "go/parser" "go/token" "go/types" "testing" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/testenv" ) +func TestWorkaroundIssue57490(t *testing.T) { + // During error recovery the parser synthesizes various close + // tokens at EOF, causing the End position of incomplete + // syntax nodes, computed as Rbrace+len("}"), to be beyond EOF. + src := `package p; func f() { var x struct` + fset := token.NewFileSet() + file, _ := parser.ParseFile(fset, "", src, 0) + tf := fset.File(file.Pos()) + if false { + tf.Offset(file.End()) // panic: invalid Pos value 36 (should be in [1, 35]) + } + + // The offset of the EOF position is the file size. + offset, err := safetoken.Offset(tf, file.End()-1) + if err != nil || offset != tf.Size() { + t.Errorf("Offset(EOF) = (%d, %v), want token.File.Size %d", offset, err, tf.Size()) + } + + // The offset of the file.End() position, 1 byte beyond EOF, + // is also the size of the file. + offset, err = safetoken.Offset(tf, file.End()) + if err != nil || offset != tf.Size() { + t.Errorf("Offset(ast.File.End()) = (%d, %v), want token.File.Size %d", offset, err, tf.Size()) + } +} + // This test reports any unexpected uses of (*go/token.File).Offset within // the gopls codebase to ensure that we don't check in more code that is prone // to panicking. All calls to (*go/token.File).Offset should be replaced with // calls to safetoken.Offset. -func TestTokenOffset(t *testing.T) { +func TestGoplsSourceDoesNotCallTokenFileOffset(t *testing.T) { testenv.NeedsGoPackages(t) fset := token.NewFileSet() pkgs, err := packages.Load(&packages.Config{ Fset: fset, Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, - }, "go/token", "golang.org/x/tools/gopls/internal/lsp/...", "golang.org/x/tools/gopls/...") + }, "go/token", "golang.org/x/tools/gopls/...") if err != nil { t.Fatal(err) } diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index ea90da17378..59d4fc3222b 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -244,14 +244,12 @@ func (e *encoded) strStack() string { } if len(e.stack) > 0 { loc := e.stack[len(e.stack)-1].Pos() - if !safetoken.InRange(e.pgf.Tok, loc) { + 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 if safetoken.InRange(e.pgf.Tok, loc) { + } else { add := e.pgf.Tok.PositionFor(loc, false) // ignore line directives nm := filepath.Base(add.Filename) msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", nm, add.Line, add.Column)) - } else { - msg = append(msg, fmt.Sprintf("(loc %d out of range)", loc)) } } msg = append(msg, "]") From 26fc60977c3ba7c30e77ce8349a8545557d646cd Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 7 Dec 2022 11:12:15 -0500 Subject: [PATCH 564/723] gopls/internal/lsp/cache: eliminate snapshot.containingPackages This change replaces snapshot.containingPackages by MetadataForFile, which is almost identical but lacks the mysterious call to GetFile just for its effects, specifically, inserting into the snapshot.files map, which determines the set of orphaned files. That call has been moved to the one place where it is necessary, checkForOrphanedFile, which also now requests only metadata instead of type-checking. Also, factor the logic to remove an element from the views slice. Change-Id: I2263d4778cc3765600c20e909210d0515e0270f7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459777 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/cache/session.go | 17 ++++++++---- gopls/internal/lsp/cache/snapshot.go | 41 ++-------------------------- gopls/internal/lsp/diagnostics.go | 14 +++++++--- 3 files changed, 24 insertions(+), 48 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 4e9b7820608..7e9bb6217a9 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -382,9 +382,7 @@ func (s *Session) RemoveView(view *View) { } // delete this view... we don't care about order but we do want to make // sure we can garbage collect the view - s.views[i] = s.views[len(s.views)-1] - s.views[len(s.views)-1] = nil - s.views = s.views[:len(s.views)-1] + s.views = removeElement(s.views, i) } func (s *Session) updateView(ctx context.Context, view *View, options *source.Options) (*View, error) { @@ -412,9 +410,7 @@ func (s *Session) updateView(ctx context.Context, view *View, options *source.Op // we have dropped the old view, but could not create the new one // this should not happen and is very bad, but we still need to clean // up the view array if it happens - s.views[i] = s.views[len(s.views)-1] - s.views[len(s.views)-1] = nil - s.views = s.views[:len(s.views)-1] + s.views = removeElement(s.views, i) return nil, err } // substitute the new view into the array where the old view was @@ -422,6 +418,15 @@ func (s *Session) updateView(ctx context.Context, view *View, options *source.Op return v, nil } +// removeElement removes the ith element from the slice replacing it with the last element. +// TODO(adonovan): generics, someday. +func removeElement(slice []*View, index int) []*View { + last := len(slice) - 1 + slice[index] = slice[last] + slice[last] = nil // aid GC + return slice[:last] +} + // dropView removes v from the set of views for the receiver s and calls // v.shutdown, returning the index of v in s.views (if found), or -1 if v was // not found. s.viewMu must be held while calling this function. diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index bda36902319..dc34208b95a 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -639,7 +639,7 @@ func (s *snapshot) buildOverlay() map[string][]byte { func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]source.Package, error) { ctx = event.Label(ctx, tag.URI.Of(uri)) - metas, err := s.containingPackages(ctx, uri) + metas, err := s.MetadataForFile(ctx, uri) if err != nil { return nil, err } @@ -660,7 +660,7 @@ func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode sourc func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, pkgPolicy source.PackageFilter) (source.Package, error) { ctx = event.Label(ctx, tag.URI.Of(uri)) - metas, err := s.containingPackages(ctx, uri) + metas, err := s.MetadataForFile(ctx, uri) if err != nil { return nil, err } @@ -680,41 +680,6 @@ func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source return pkgs[0], err } -// containingPackages returns a new slice of metadata records for -// packages that contain the Go file specified by uri. The results are -// ordered "narrowest" to "widest". -func (s *snapshot) containingPackages(ctx context.Context, uri span.URI) ([]*source.Metadata, error) { - // TODO(rfindley): why can't/shouldn't we awaitLoaded here? It seems that if - // the caller of this function is going to - // ask for package handles for a file, we should wait for pending loads. - // Else we will reload orphaned files before the initial load completes. - - // Check if we should reload metadata for the file. We don't invalidate IDs - // (though we should), so the IDs will be a better source of truth than the - // metadata. If there are no IDs for the file, then we should also reload. - // - // TODO(adonovan): I can't relate the GetFile call to the - // previous comment, but tests rely on its effects. - // It does more than provide fh to the FileKind check. But what? - fh, err := s.GetFile(ctx, uri) - if err != nil { - return nil, err - } - - // Check that the file kind is Go (not, say, C or assembly). - // (This check is perhaps superfluous since the go/packages "file=" - // metadata query in MetadataForFile requires .go files files.) - if kind := s.view.FileKind(fh); kind != source.Go { - return nil, fmt.Errorf("no packages for non-Go file %s (%v)", uri, kind) - } - - metas, err := s.MetadataForFile(ctx, uri) // ordered narrowest to widest - if err != nil { - return nil, err - } - return metas, err -} - // TypeCheck type-checks the specified packages in the given mode. func (s *snapshot) TypeCheck(ctx context.Context, mode source.TypecheckMode, ids ...PackageID) ([]source.Package, error) { // Build all the handles... @@ -811,7 +776,7 @@ func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source s.unloadableFiles[uri] = struct{}{} } - // Sort packages "narrowed" to "widest" (in practice: non-tests before tests). + // Sort packages "narrowest" to "widest" (in practice: non-tests before tests). sort.Slice(metas, func(i, j int) bool { return len(metas[i].CompiledGoFiles) < len(metas[j].CompiledGoFiles) }) diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 402553b4264..6ac0ee6c202 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -559,11 +559,17 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps if snapshot.IsBuiltin(ctx, fh.URI()) { return nil } - // TODO(rfindley): opt: request metadata, not type-checking. - pkgs, _ := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace, false) - if len(pkgs) > 0 { - return nil + + // This call has the effect of inserting fh into snapshot.files, + // where for better or worse (actually: just worse) it influences + // the sets of open, known, and orphaned files. + snapshot.GetFile(ctx, fh.URI()) + + metas, _ := snapshot.MetadataForFile(ctx, fh.URI()) + if len(metas) > 0 || ctx.Err() != nil { + return nil // no package, or cancelled } + // Inv: file does not belong to a package we know about. pgf, err := snapshot.ParseGo(ctx, fh, source.ParseHeader) if err != nil { return nil From fe6b3007e5a4a9b41436c8aabb3f20a30b8f22c4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 28 Dec 2022 13:44:12 -0500 Subject: [PATCH 565/723] gopls/internal/lsp/source: eliminate Snapshot.Package{,s}ForFile The PackagesForFile (plural) method was called only once, from qualifiedObjsAtLocation, which now inlines it. (It looks like further simplification may be possible; another day.) The PackageForFile (singular) method is now expressed as a standalone function in terms of Snapshot primitives. commandHandler.runTests is downgraded from type-checking to a metadata operation. Change-Id: I1ae5b95860217b1823feebd0572f689b0b4a0618 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459778 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/graph.go | 2 +- gopls/internal/lsp/cache/snapshot.go | 44 --------------------- gopls/internal/lsp/code_action.go | 2 +- gopls/internal/lsp/command.go | 8 ++-- gopls/internal/lsp/diagnostics.go | 6 +-- gopls/internal/lsp/semantic.go | 2 +- gopls/internal/lsp/source/diagnostics.go | 2 +- gopls/internal/lsp/source/highlight.go | 2 +- gopls/internal/lsp/source/identifier.go | 4 +- gopls/internal/lsp/source/implementation.go | 13 ++++-- gopls/internal/lsp/source/util.go | 6 ++- gopls/internal/lsp/source/view.go | 42 +++++++++++++------- 12 files changed, 56 insertions(+), 77 deletions(-) diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go index aaabbdfc43b..8e9e5d92c4f 100644 --- a/gopls/internal/lsp/cache/graph.go +++ b/gopls/internal/lsp/cache/graph.go @@ -91,7 +91,7 @@ func (g *metadataGraph) build() { // - Else, choose the first valid command-line-argument package, if it exists. // // TODO(rfindley): it might be better to track all IDs here, and exclude - // them later in PackagesForFile, but this is the existing behavior. + // them later when type checking, but this is the existing behavior. for i, id := range ids { // If we've seen *anything* prior to command-line arguments package, take // it. Note that ids[0] may itself be command-line-arguments. diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index dc34208b95a..f5c85941039 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -636,50 +636,6 @@ func (s *snapshot) buildOverlay() map[string][]byte { return overlays } -func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]source.Package, error) { - ctx = event.Label(ctx, tag.URI.Of(uri)) - - metas, err := s.MetadataForFile(ctx, uri) - if err != nil { - return nil, err - } - - // Optionally filter out any intermediate test variants. - // We typically aren't interested in these - // packages for file= style queries. - if !includeTestVariants { - metas = source.RemoveIntermediateTestVariants(metas) - } - - ids := make([]PackageID, len(metas)) - for i, m := range metas { - ids[i] = m.ID - } - return s.TypeCheck(ctx, mode, ids...) -} - -func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, pkgPolicy source.PackageFilter) (source.Package, error) { - ctx = event.Label(ctx, tag.URI.Of(uri)) - metas, err := s.MetadataForFile(ctx, uri) - if err != nil { - return nil, err - } - if len(metas) == 0 { - return nil, fmt.Errorf("no package metadata for file %s", uri) - } - switch pkgPolicy { - case source.NarrowestPackage: - metas = metas[:1] - case source.WidestPackage: - metas = metas[len(metas)-1:] - } - pkgs, err := s.TypeCheck(ctx, mode, metas[0].ID) - if err != nil { - return nil, err - } - return pkgs[0], err -} - // TypeCheck type-checks the specified packages in the given mode. func (s *snapshot) TypeCheck(ctx context.Context, mode source.TypecheckMode, ids ...PackageID) ([]source.Package, error) { // Build all the handles... diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 3b333ffa641..2c509e3fd7e 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -158,7 +158,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara // Type-check the package and also run analysis, // then combine their diagnostics. - pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckFull, source.WidestPackage) + pkg, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.WidestPackage) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index aba9f029d9c..2022325a815 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -432,15 +432,15 @@ func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, work *progress.WorkDone, uri protocol.DocumentURI, tests, benchmarks []string) error { // TODO: fix the error reporting when this runs async. - // TODO(adonovan): opt: request only metadata, not type checking. - pkgs, err := snapshot.PackagesForFile(ctx, uri.SpanURI(), source.TypecheckWorkspace, false) + metas, err := snapshot.MetadataForFile(ctx, uri.SpanURI()) if err != nil { return err } - if len(pkgs) == 0 { + metas = source.RemoveIntermediateTestVariants(metas) + if len(metas) == 0 { return fmt.Errorf("package could not be found for file: %s", uri.SpanURI().Filename()) } - pkgPath := pkgs[0].ForTest() + pkgPath := string(metas[0].ForTest) // create output buf := &bytes.Buffer{} diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 6ac0ee6c202..863c1428528 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -195,8 +195,8 @@ func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snaps if snapshot.FindFile(uri) == nil { continue } - // Don't call PackagesForFile for builtin.go, as it results in a - // command-line-arguments load. + + // Don't request type-checking for builtin.go: it's not a real package. if snapshot.IsBuiltin(ctx, uri) { continue } @@ -204,7 +204,7 @@ func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snaps // Find all packages that include this file and diagnose them in parallel. metas, err := snapshot.MetadataForFile(ctx, uri) if err != nil { - // TODO (findleyr): we should probably do something with the error here, + // TODO(findleyr): we should probably do something with the error here, // but as of now this can fail repeatedly if load fails, so can be too // noisy to log (and we'll handle things later in the slow pass). continue diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 59d4fc3222b..5bc2f2058e5 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -92,7 +92,7 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu if kind != source.Go { return nil, nil } - pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckFull, source.WidestPackage) + pkg, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.WidestPackage) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index 8024f46cfe7..dc9cc04716a 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -77,7 +77,7 @@ func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (Vers if err != nil { return VersionedFileIdentity{}, nil, err } - pkg, err := snapshot.PackageForFile(ctx, uri, TypecheckFull, NarrowestPackage) + pkg, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, NarrowestPackage) if err != nil { return VersionedFileIdentity{}, nil, err } diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index 2f0766b9a82..ee5d9ed162e 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -24,7 +24,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p // Don't use GetTypedFile because it uses TypecheckWorkspace, and we // always want fully parsed files for highlight, regardless of whether // the file belongs to a workspace package. - pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckFull, WidestPackage) + pkg, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, WidestPackage) if err != nil { return nil, fmt.Errorf("getting package for Highlight: %w", err) } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 64167af5839..739b51190b3 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -80,13 +80,13 @@ func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, position ctx, done := event.Start(ctx, "source.Identifier") defer done() - pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckFull, NarrowestPackage) + pkg, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return nil, err } pgf, err := pkg.File(fh.URI()) if err != nil { - // We shouldn't get a package from PackagesForFile that doesn't actually + // We shouldn't get a package from PackageForFile that doesn't actually // contain the file. bug.Report("missing package file", bug.Data{"pkg": pkg.ID(), "file": fh.URI()}) return nil, err diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index d3de3733320..460a9f7923a 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -283,8 +283,15 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s // consider: the definition of the object referenced by the location. But we // try to be comprehensive in case we ever support variations on build // constraints. - - pkgs, err := s.PackagesForFile(ctx, key.uri, TypecheckWorkspace, true) + metas, err := s.MetadataForFile(ctx, key.uri) + if err != nil { + return nil, err + } + ids := make([]PackageID, len(metas)) + for i, m := range metas { + ids[i] = m.ID + } + pkgs, err := s.TypeCheck(ctx, TypecheckWorkspace, ids...) if err != nil { return nil, err } @@ -302,7 +309,7 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s } } if !hasFullPackage { - pkg, err := s.PackageForFile(ctx, key.uri, TypecheckFull, WidestPackage) + pkg, err := PackageForFile(ctx, s, key.uri, TypecheckFull, WidestPackage) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 301e0173559..cfdfd9720eb 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -88,8 +88,10 @@ func (s MappedRange) URI() span.URI { // Type-checking is expensive. Call snapshot.ParseGo if all you need // is a parse tree, or snapshot.MetadataForFile if all you only need // metadata. -func GetTypedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgPolicy PackageFilter) (Package, *ParsedGoFile, error) { - pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckWorkspace, pkgPolicy) +// +// TODO(adonovan): add a Mode parameter and merge with PackageForFile. +func GetTypedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgSel PackageSelector) (Package, *ParsedGoFile, error) { + pkg, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, pkgSel) if err != nil { return nil, nil, err } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 0aeac542be9..aaf932949a1 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -164,17 +164,6 @@ type Snapshot interface { // IsBuiltin reports whether uri is part of the builtin package. IsBuiltin(ctx context.Context, uri span.URI) bool - // PackagesForFile returns an unordered list of packages that contain - // the file denoted by uri, type checked in the specified mode. - // - // If withIntermediateTestVariants is set, the resulting package set includes - // intermediate test variants. - PackagesForFile(ctx context.Context, uri span.URI, mode TypecheckMode, withIntermediateTestVariants bool) ([]Package, error) - - // PackageForFile returns a single package that this file belongs to, - // checked in mode and filtered by the package policy. - PackageForFile(ctx context.Context, uri span.URI, mode TypecheckMode, selectPackage PackageFilter) (Package, error) - // ReverseDependencies returns a new mapping whose entries are // the ID and Metadata of each package in the workspace that // directly or transitively depend on the package denoted by id, @@ -234,16 +223,41 @@ func SnapshotLabels(snapshot Snapshot) []label.Label { return []label.Label{tag.Snapshot.Of(snapshot.SequenceID()), tag.Directory.Of(snapshot.View().Folder())} } -// PackageFilter sets how a package is filtered out from a set of packages +// PackageForFile returns a single package that this file belongs to, +// checked in mode and filtered by the package selector. +// +// TODO(adonovan): merge with GetTypedFile. +func PackageForFile(ctx context.Context, snapshot Snapshot, uri span.URI, mode TypecheckMode, pkgSel PackageSelector) (Package, error) { + metas, err := snapshot.MetadataForFile(ctx, uri) + if err != nil { + return nil, err + } + if len(metas) == 0 { + return nil, fmt.Errorf("no package metadata for file %s", uri) + } + switch pkgSel { + case NarrowestPackage: + metas = metas[:1] + case WidestPackage: + metas = metas[len(metas)-1:] + } + pkgs, err := snapshot.TypeCheck(ctx, mode, metas[0].ID) + if err != nil { + return nil, err + } + return pkgs[0], err +} + +// PackageSelector sets how a package is selected out from a set of packages // containing a given file. -type PackageFilter int +type PackageSelector int const ( // NarrowestPackage picks the "narrowest" package for a given file. // By "narrowest" package, we mean the package with the fewest number of // files that includes the given file. This solves the problem of test // variants, as the test will have more files than the non-test package. - NarrowestPackage PackageFilter = iota + NarrowestPackage PackageSelector = iota // WidestPackage returns the Package containing the most files. // This is useful for something like diagnostics, where we'd prefer to From ae4ff823be864cd986f1928bc1b12e5aba270576 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 28 Dec 2022 14:04:50 -0500 Subject: [PATCH 566/723] gopls/internal/lsp/source: delete GetTypedFile Use PackageForFile instead. Change-Id: Ifcf2b75360fa1a0a6f277f30a68c0f78f03af9da Reviewed-on: https://go-review.googlesource.com/c/tools/+/459779 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan --- gopls/internal/lsp/code_action.go | 2 +- gopls/internal/lsp/semantic.go | 6 +---- gopls/internal/lsp/source/code_lens.go | 2 +- .../lsp/source/completion/completion.go | 2 +- gopls/internal/lsp/source/diagnostics.go | 2 +- gopls/internal/lsp/source/fix.go | 2 +- gopls/internal/lsp/source/highlight.go | 11 +++------ gopls/internal/lsp/source/identifier.go | 10 +------- gopls/internal/lsp/source/implementation.go | 2 +- gopls/internal/lsp/source/inlay_hint.go | 2 +- gopls/internal/lsp/source/signature_help.go | 2 +- gopls/internal/lsp/source/stub.go | 2 +- gopls/internal/lsp/source/util.go | 18 -------------- gopls/internal/lsp/source/view.go | 24 ++++++++++++------- 14 files changed, 30 insertions(+), 57 deletions(-) diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 2c509e3fd7e..0767d439b4c 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -158,7 +158,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara // Type-check the package and also run analysis, // then combine their diagnostics. - pkg, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.WidestPackage) + pkg, _, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.WidestPackage) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 5bc2f2058e5..7368b59b613 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -92,11 +92,7 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu if kind != source.Go { return nil, nil } - pkg, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.WidestPackage) - if err != nil { - return nil, err - } - pgf, err := pkg.File(fh.URI()) + pkg, pgf, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.WidestPackage) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go index 24390086cd1..c87f41664dd 100644 --- a/gopls/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -100,7 +100,7 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) ( if !strings.HasSuffix(fh.URI().Filename(), "_test.go") { return out, nil } - pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, WidestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, WidestPackage) if err != nil { return out, err } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 9d11d7088cd..19d16a1bbc9 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -427,7 +427,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan startTime := time.Now() - pkg, pgf, err := source.GetTypedFile(ctx, snapshot, fh, source.NarrowestPackage) + pkg, pgf, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckWorkspace, source.NarrowestPackage) 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 diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index dc9cc04716a..5144e089b5b 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -77,7 +77,7 @@ func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (Vers if err != nil { return VersionedFileIdentity{}, nil, err } - pkg, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, NarrowestPackage) + pkg, _, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, NarrowestPackage) if err != nil { return VersionedFileIdentity{}, nil, err } diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 1584e2bb38e..34a6fe8ce26 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -133,7 +133,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi // getAllSuggestedFixInputs is a helper function to collect all possible needed // inputs for an AppliesFunc or SuggestedFixFunc. func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, span.Range, []byte, *ast.File, *types.Package, *types.Info, error) { - pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) if err != nil { return nil, span.Range{}, nil, nil, nil, nil, fmt.Errorf("getting file for Identifier: %w", err) } diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index ee5d9ed162e..d073fffe732 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -21,17 +21,12 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p ctx, done := event.Start(ctx, "source.Highlight") defer done() - // Don't use GetTypedFile because it uses TypecheckWorkspace, and we - // always want fully parsed files for highlight, regardless of whether - // the file belongs to a workspace package. - pkg, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, WidestPackage) + // We always want fully parsed files for highlight, regardless + // of whether the file belongs to a workspace package. + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, WidestPackage) if err != nil { return nil, fmt.Errorf("getting package for Highlight: %w", err) } - pgf, err := pkg.File(fh.URI()) - if err != nil { - return nil, fmt.Errorf("getting file for Highlight: %w", err) - } pos, err := pgf.Mapper.Pos(position) if err != nil { diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 739b51190b3..ad826da07a3 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -18,7 +18,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" ) @@ -80,17 +79,10 @@ func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, position ctx, done := event.Start(ctx, "source.Identifier") defer done() - pkg, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return nil, err } - pgf, err := pkg.File(fh.URI()) - if err != nil { - // We shouldn't get a package from PackageForFile that doesn't actually - // contain the file. - bug.Report("missing package file", bug.Data{"pkg": pkg.ID(), "file": fh.URI()}) - return nil, err - } pos, err := pgf.Mapper.Pos(position) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 460a9f7923a..3933fdf5a9b 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -309,7 +309,7 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s } } if !hasFullPackage { - pkg, err := PackageForFile(ctx, s, key.uri, TypecheckFull, WidestPackage) + pkg, _, err := PackageForFile(ctx, s, key.uri, TypecheckFull, WidestPackage) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go index 039a1df1f8a..ade2f5fa706 100644 --- a/gopls/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -83,7 +83,7 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng proto ctx, done := event.Start(ctx, "source.InlayHint") defer done() - pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) if err != nil { return nil, fmt.Errorf("getting file for InlayHint: %w", err) } diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index ad0e69bc59b..3f12d90ad18 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -20,7 +20,7 @@ func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, positi ctx, done := event.Start(ctx, "source.SignatureHelp") defer done() - pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) if err != nil { return nil, 0, fmt.Errorf("getting file for SignatureHelp: %w", err) } diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 75bab288e60..18829fb60e0 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -26,7 +26,7 @@ import ( ) func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { - pkg, pgf, err := GetTypedFile(ctx, snapshot, fh, NarrowestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) if err != nil { return nil, nil, fmt.Errorf("GetTypedFile: %w", err) } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index cfdfd9720eb..978e2c7209f 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -81,24 +81,6 @@ func (s MappedRange) URI() span.URI { return s.m.URI } -// GetTypedFile is a convenience function that reads, parses, and -// type-checks the package containing a file in the given -// Snapshot. pkgPolicy is one of NarrowestPackage or WidestPackage. -// -// Type-checking is expensive. Call snapshot.ParseGo if all you need -// is a parse tree, or snapshot.MetadataForFile if all you only need -// metadata. -// -// TODO(adonovan): add a Mode parameter and merge with PackageForFile. -func GetTypedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgSel PackageSelector) (Package, *ParsedGoFile, error) { - pkg, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, pkgSel) - if err != nil { - return nil, nil, err - } - pgh, err := pkg.File(fh.URI()) - return pkg, pgh, err -} - func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { fh, err := snapshot.GetFile(ctx, uri) if err != nil { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index aaf932949a1..144345cfe61 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -223,17 +223,20 @@ func SnapshotLabels(snapshot Snapshot) []label.Label { return []label.Label{tag.Snapshot.Of(snapshot.SequenceID()), tag.Directory.Of(snapshot.View().Folder())} } -// PackageForFile returns a single package that this file belongs to, -// checked in mode and filtered by the package selector. +// PackageForFile is a convenience function that selects a package to +// which this file belongs (narrowest or widest), type-checks it in +// the requested mode (full or workspace), and returns it, along with +// the parse tree of that file. // -// TODO(adonovan): merge with GetTypedFile. -func PackageForFile(ctx context.Context, snapshot Snapshot, uri span.URI, mode TypecheckMode, pkgSel PackageSelector) (Package, error) { +// Type-checking is expensive. Call snapshot.ParseGo if all you need +// is a parse tree, or snapshot.MetadataForFile if you only need metadata. +func PackageForFile(ctx context.Context, snapshot Snapshot, uri span.URI, mode TypecheckMode, pkgSel PackageSelector) (Package, *ParsedGoFile, error) { metas, err := snapshot.MetadataForFile(ctx, uri) if err != nil { - return nil, err + return nil, nil, err } if len(metas) == 0 { - return nil, fmt.Errorf("no package metadata for file %s", uri) + return nil, nil, fmt.Errorf("no package metadata for file %s", uri) } switch pkgSel { case NarrowestPackage: @@ -243,9 +246,14 @@ func PackageForFile(ctx context.Context, snapshot Snapshot, uri span.URI, mode T } pkgs, err := snapshot.TypeCheck(ctx, mode, metas[0].ID) if err != nil { - return nil, err + return nil, nil, err } - return pkgs[0], err + pkg := pkgs[0] + pgf, err := pkg.File(uri) + if err != nil { + return nil, nil, err // "can't happen" + } + return pkg, pgf, err } // PackageSelector sets how a package is selected out from a set of packages From 2f31dd473a3cd96dbddb9da93b58ac55f1a2450e Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 27 Dec 2022 15:20:06 -0800 Subject: [PATCH 567/723] go/ssa,go/analysis/passes/nilness: refine when type param constants are nil Const.IsNil returns true for zero value of type params when all potential instantiations of the type parameter are nillable. nilness does not consider an invoke of a type parameter to be an error. Change-Id: I8f3f6e626861146815b17e2a04c428a3444263a7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459675 Reviewed-by: Alan Donovan Run-TryBot: Tim King TryBot-Result: Gopher Robot Reviewed-by: Zvonimir Pavlinovic Reviewed-by: Robert Findley gopls-CI: kokoro --- go/analysis/passes/nilness/nilness.go | 8 ++- go/analysis/passes/nilness/nilness_test.go | 8 +++ .../passes/nilness/testdata/src/d/d.go | 55 +++++++++++++++++++ go/ssa/const.go | 20 ++++--- 4 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 go/analysis/passes/nilness/testdata/src/d/d.go diff --git a/go/analysis/passes/nilness/nilness.go b/go/analysis/passes/nilness/nilness.go index 61fa30a5237..6849c33ccef 100644 --- a/go/analysis/passes/nilness/nilness.go +++ b/go/analysis/passes/nilness/nilness.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/buildssa" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/typeparams" ) const Doc = `check for redundant or impossible nil comparisons @@ -102,8 +103,11 @@ func runFunc(pass *analysis.Pass, fn *ssa.Function) { for _, instr := range b.Instrs { switch instr := instr.(type) { case ssa.CallInstruction: - notNil(stack, instr, instr.Common().Value, - instr.Common().Description()) + // A nil receiver may be okay for type params. + cc := instr.Common() + if !(cc.IsInvoke() && typeparams.IsTypeParam(cc.Value.Type())) { + notNil(stack, instr, cc.Value, cc.Description()) + } case *ssa.FieldAddr: notNil(stack, instr, instr.X, "field selection") case *ssa.IndexAddr: diff --git a/go/analysis/passes/nilness/nilness_test.go b/go/analysis/passes/nilness/nilness_test.go index 86c4a769da8..99c4dfbac1d 100644 --- a/go/analysis/passes/nilness/nilness_test.go +++ b/go/analysis/passes/nilness/nilness_test.go @@ -24,3 +24,11 @@ func TestInstantiated(t *testing.T) { testdata := analysistest.TestData() analysistest.Run(t, testdata, nilness.Analyzer, "c") } + +func TestTypeSet(t *testing.T) { + if !typeparams.Enabled { + t.Skip("TestTypeSet requires type parameters") + } + testdata := analysistest.TestData() + analysistest.Run(t, testdata, nilness.Analyzer, "d") +} diff --git a/go/analysis/passes/nilness/testdata/src/d/d.go b/go/analysis/passes/nilness/testdata/src/d/d.go new file mode 100644 index 00000000000..72bd1c87217 --- /dev/null +++ b/go/analysis/passes/nilness/testdata/src/d/d.go @@ -0,0 +1,55 @@ +package d + +type message interface{ PR() } + +func noparam() { + var messageT message + messageT.PR() // want "nil dereference in dynamic method call" +} + +func paramNonnil[T message]() { + var messageT T + messageT.PR() // cannot conclude messageT is nil. +} + +func instance() { + // buildssa.BuilderMode does not include InstantiateGenerics. + paramNonnil[message]() // no warning is expected as param[message] id not built. +} + +func param[T interface { + message + ~*int | ~chan int +}]() { + var messageT T // messageT is nil. + messageT.PR() // nil receiver may be okay. See param[nilMsg]. +} + +type nilMsg chan int + +func (m nilMsg) PR() { + if m == nil { + print("not an error") + } +} + +var G func() = param[nilMsg] // no warning + +func allNillable[T ~*int | ~chan int]() { + var x, y T // both are nillable and are nil. + if x != y { // want "impossible condition: nil != nil" + print("unreachable") + } +} + +func notAll[T ~*int | ~chan int | ~int]() { + var x, y T // neither are nillable due to ~int + if x != y { // no warning + print("unreachable") + } +} + +func noninvoke[T ~func()]() { + var x T + x() // want "nil dereference in dynamic function call" +} diff --git a/go/ssa/const.go b/go/ssa/const.go index a1bac7b914b..4a51a2cb4bb 100644 --- a/go/ssa/const.go +++ b/go/ssa/const.go @@ -153,23 +153,27 @@ func (c *Const) Pos() token.Pos { return token.NoPos } -// IsNil returns true if this constant represents a typed or untyped nil value -// with an underlying reference type: pointer, slice, chan, map, function, or -// *basic* interface. -// -// Note: a type parameter whose underlying type is a basic interface is -// considered a reference type. +// IsNil returns true if this constant is a nil value of +// a nillable reference type (pointer, slice, channel, map, or function), +// a basic interface type, or +// a type parameter all of whose possible instantiations are themselves nillable. func (c *Const) IsNil() bool { return c.Value == nil && nillable(c.typ) } // nillable reports whether *new(T) == nil is legal for type T. func nillable(t types.Type) bool { - switch t := t.Underlying().(type) { + if typeparams.IsTypeParam(t) { + return underIs(typeSetOf(t), func(u types.Type) bool { + // empty type set (u==nil) => any underlying types => not nillable + return u != nil && nillable(u) + }) + } + switch t.Underlying().(type) { case *types.Pointer, *types.Slice, *types.Chan, *types.Map, *types.Signature: return true case *types.Interface: - return typeSetOf(t).Len() == 0 // basic interface. + return true // basic interface. default: return false } From 0e1d01321381a6614cf23ae5b146eaa8e4c2040a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 27 Dec 2022 14:12:15 -0500 Subject: [PATCH 568/723] gopls/internal/lsp/cache: recreate Views when their root changes A View is defined by a Go environment along with a go.work file, go.mod file, or directory in GOPATH. When the environment changes via didChangeConfiguration, we recreate the View. Before this CL we didn't do the same when a view-defining go.mod or go.work was added or removed. Instead, we relied on workspace and snapshot invalidation to get us to the correct state. But since we reinitialize the view on these changes anyway there is little value in preserving any existing view state, and in fact doing so may lead to latent bugs. For example, deleting a go.work file caused the workspace to fall back to 'file system' mode, which may not be the same state that would result from restarting gopls. This change also moves us toward a model where we can have a dynamic set of views covering the full set of open files, not 1:1 with workspace folders. Also: - move the dirURI helper function to span.Dir, which is more discoverable - fix a bug where the wrong filter function was used to define the workspace - remove Session.optionsOverride, which was unused For golang/go#55331 Change-Id: Id108e848cdd942c34eda0a339d2c8a517c89c7de Reviewed-on: https://go-review.googlesource.com/c/tools/+/459635 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/session.go | 107 +++++++++++++--- gopls/internal/lsp/cache/snapshot.go | 8 +- gopls/internal/lsp/cache/view.go | 92 +++++++------- gopls/internal/lsp/cache/view_test.go | 38 +++--- gopls/internal/lsp/cache/workspace.go | 117 +++++++----------- gopls/internal/lsp/cache/workspace_test.go | 60 --------- gopls/internal/lsp/lsp_test.go | 4 +- gopls/internal/regtest/watch/watch_test.go | 7 +- .../regtest/workspace/workspace_test.go | 34 +++-- gopls/internal/span/uri.go | 8 ++ 10 files changed, 250 insertions(+), 225 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 7e9bb6217a9..b1367ff5010 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -29,9 +29,8 @@ type Session struct { id string // Immutable attributes shared across views. - cache *Cache // shared cache - gocmdRunner *gocommand.Runner // limits go command concurrency - optionsOverrides func(*source.Options) // transformation to apply on top of all options + cache *Cache // shared cache + gocmdRunner *gocommand.Runner // limits go command concurrency optionsMu sync.Mutex options *source.Options @@ -184,10 +183,6 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, seqID uint64) (*View, *snapshot, func(), error) { index := atomic.AddInt64(&viewIndex, 1) - if s.optionsOverrides != nil { - s.optionsOverrides(options) - } - // Get immutable workspace configuration. // // TODO(rfindley): this info isn't actually immutable. For example, GOWORK @@ -199,11 +194,15 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, } root := folder - if options.ExpandWorkspaceToModule { - root, err = findWorkspaceRoot(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), options.ExperimentalWorkspaceModule) - if err != nil { - return nil, nil, func() {}, err - } + // filterFunc is the path filter function for this workspace folder. Notably, + // it is relative to folder (which is specified by the user), not root. + filterFunc := pathExcludedByFilterFunc(folder.Filename(), wsInfo.gomodcache, options) + rootSrc, err := findWorkspaceModuleSource(ctx, root, s, filterFunc, options.ExperimentalWorkspaceModule) + if err != nil { + return nil, nil, func() {}, err + } + if options.ExpandWorkspaceToModule && rootSrc != "" { + root = span.Dir(rootSrc) } explicitGowork := os.Getenv("GOWORK") @@ -213,7 +212,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, goworkURI := span.URIFromPath(explicitGowork) // Build the gopls workspace, collecting active modules in the view. - workspace, err := newWorkspace(ctx, root, goworkURI, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), wsInfo.effectiveGO111MODULE() == off, options.ExperimentalWorkspaceModule) + workspace, err := newWorkspace(ctx, root, goworkURI, s, filterFunc, wsInfo.effectiveGO111MODULE() == off, options.ExperimentalWorkspaceModule) if err != nil { return nil, nil, func() {}, err } @@ -238,6 +237,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, filesByURI: make(map[span.URI]span.URI), filesByBase: make(map[string][]canonicalURI), rootURI: root, + rootSrc: rootSrc, explicitGowork: goworkURI, workspaceInformation: *wsInfo, } @@ -385,10 +385,18 @@ func (s *Session) RemoveView(view *View) { s.views = removeElement(s.views, i) } +// updateView recreates the view with the given options. +// +// If the resulting error is non-nil, the view may or may not have already been +// dropped from the session. func (s *Session) updateView(ctx context.Context, view *View, options *source.Options) (*View, error) { s.viewMu.Lock() defer s.viewMu.Unlock() + return s.updateViewLocked(ctx, view, options) +} + +func (s *Session) updateViewLocked(ctx context.Context, view *View, options *source.Options) (*View, error) { // Preserve the snapshot ID if we are recreating the view. view.snapshotMu.Lock() if view.snapshot == nil { @@ -471,22 +479,87 @@ type fileChange struct { // the affected file URIs for those snapshots. // On success, it returns a release function that // must be called when the snapshots are no longer needed. +// +// TODO(rfindley): what happens if this function fails? It must leave us in a +// broken state, which we should surface to the user, probably as a request to +// restart gopls. func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]span.URI, func(), error) { s.viewMu.Lock() defer s.viewMu.Unlock() - views := make(map[*View]map[span.URI]*fileChange) - affectedViews := map[span.URI][]*View{} + // Update overlays. + // + // TODO(rfindley): I think we do this while holding viewMu to prevent views + // from seeing the updated file content before they have processed + // invalidations, which could lead to a partial view of the changes (i.e. + // spurious diagnostics). However, any such view would immediately be + // invalidated here, so it is possible that we could update overlays before + // acquiring viewMu. overlays, err := s.updateOverlays(ctx, changes) if err != nil { return nil, nil, err } - var forceReloadMetadata bool + + // Re-create views whose root may have changed. + // + // checkRoots controls whether to re-evaluate view definitions when + // collecting views below. Any change to a go.mod or go.work file may have + // affected the definition of the view. + checkRoots := false + for _, c := range changes { + if isGoMod(c.URI) || isGoWork(c.URI) { + checkRoots = true + break + } + } + + if checkRoots { + for _, view := range s.views { + // Check whether the view must be recreated. This logic looks hacky, + // as it uses the existing view gomodcache and options to re-evaluate + // the workspace source, then expects view creation to compute the same + // root source after first re-evaluating gomodcache and options. + // + // Well, it *is* a bit hacky, but in practice we will get the same + // gomodcache and options, as any environment change affecting these + // should have already invalidated the view (c.f. minorOptionsChange). + // + // TODO(rfindley): clean this up. + filterFunc := pathExcludedByFilterFunc(view.folder.Filename(), view.gomodcache, view.Options()) + src, err := findWorkspaceModuleSource(ctx, view.folder, s, filterFunc, view.Options().ExperimentalWorkspaceModule) + if err != nil { + return nil, nil, err + } + if src != view.rootSrc { + _, err := s.updateViewLocked(ctx, view, view.Options()) + if err != nil { + // Catastrophic failure, equivalent to a failure of session + // initialization and therefore should almost never happen. One + // scenario where this failure mode could occur is if some file + // permissions have changed preventing us from reading go.mod + // files. + // + // The view may or may not still exist. The best we can do is log + // and move on. + // + // TODO(rfindley): consider surfacing this error more loudly. We + // could report a bug, but it's not really a bug. + event.Error(ctx, "recreating view", err) + } + } + } + } + + // Collect information about views affected by these changes. + views := make(map[*View]map[span.URI]*fileChange) + affectedViews := map[span.URI][]*View{} + // forceReloadMetadata records whether any change is the magic + // source.InvalidateMetadata action. + forceReloadMetadata := false for _, c := range changes { if c.Action == source.InvalidateMetadata { forceReloadMetadata = true } - // Build the list of affected views. var changedViews []*View for _, view := range s.views { diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index f5c85941039..b7bdaddc184 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1105,7 +1105,7 @@ func (s *snapshot) GoModForFile(uri span.URI) span.URI { func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { var match span.URI for modURI := range modFiles { - if !source.InDir(dirURI(modURI).Filename(), uri.Filename()) { + if !source.InDir(span.Dir(modURI).Filename(), uri.Filename()) { continue } if len(modURI) > len(match) { @@ -2240,7 +2240,7 @@ func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, } majorVersion = strings.TrimLeft(majorVersion, "/.") // handle gopkg.in versions file.AddNewRequire(path, source.WorkspaceModuleVersion(majorVersion), false) - if err := file.AddReplace(path, "", dirURI(modURI).Filename(), ""); err != nil { + if err := file.AddReplace(path, "", span.Dir(modURI).Filename(), ""); err != nil { return nil, err } for _, exclude := range parsed.Exclude { @@ -2278,11 +2278,11 @@ func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, // If a replace points to a module in the workspace, make sure we // direct it to version of the module in the workspace. if m, ok := paths[rep.New.Path]; ok { - newPath = dirURI(m).Filename() + newPath = span.Dir(m).Filename() newVersion = "" } else if rep.New.Version == "" && !filepath.IsAbs(rep.New.Path) { // Make any relative paths absolute. - newPath = filepath.Join(dirURI(modURI).Filename(), rep.New.Path) + newPath = filepath.Join(span.Dir(modURI).Filename(), rep.New.Path) } if err := file.AddReplace(rep.Old.Path, rep.Old.Version, newPath, newVersion); err != nil { return nil, err diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 7b5c05b759d..ca2639ee4cf 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -42,21 +42,32 @@ type View struct { cache *Cache // shared cache gocmdRunner *gocommand.Runner // limits go command concurrency - optionsMu sync.Mutex - options *source.Options - - // mu protects most mutable state of the view. - mu sync.Mutex - // baseCtx is the context handed to NewView. This is the parent of all // background contexts created for this view. baseCtx context.Context - // name is the user visible name of this view. + // name is the user-specified name of this view. name string - // folder is the folder with which this view was constructed. - folder span.URI + optionsMu sync.Mutex + options *source.Options + + // Workspace information. The fields below are immutable, and together with + // options define the build list. Any change to these fields results in a new + // View. + // + // TODO(rfindley): consolidate and/or eliminate redundancy in these fields, + // which have evolved from different sources over time. + folder span.URI // user-specified workspace folder + rootURI span.URI // either folder or dir(rootSrc) (TODO: deprecate, in favor of folder+rootSrc) + rootSrc span.URI // file providing module information (go.mod or go.work); may be empty + explicitGowork span.URI // explicitGowork: if non-empty, a user-specified go.work location (TODO: deprecate) + workspaceInformation // grab-bag of Go environment information (TODO: cleanup) + + // mu protects most mutable state of the view. + // + // TODO(rfindley): specify exactly which mutable state is guarded. + mu sync.Mutex importsState *importsState @@ -64,11 +75,14 @@ type View struct { // Each modfile has a map of module name to upgrade version. moduleUpgrades map[span.URI]map[string]string + // vulns maps each go.mod file's URI to its known vulnerabilities. vulns map[span.URI]*govulncheck.Result // filesByURI maps URIs to the canonical URI for the file it denotes. // We also keep a set of candidates for a given basename // to reduce the set of pairs that need to be tested for sameness. + // + // TODO(rfindley): move this file tracking to the session. filesByMu sync.Mutex filesByURI map[span.URI]span.URI // key is noncanonical URI (alias) filesByBase map[string][]canonicalURI // key is basename @@ -103,21 +117,6 @@ type View struct { // initialization of snapshots. Do not change it without adjusting snapshot // accordingly. initializationSema chan struct{} - - // rootURI is the rootURI directory of this view. If we are in GOPATH mode, this - // is just the folder. If we are in module mode, this is the module rootURI. - rootURI span.URI - - // explicitGowork is, if non-empty, the URI for the explicit go.work file - // provided via the users environment. - // - // TODO(rfindley): this is duplicated in the workspace type. Refactor to - // eliminate this duplication. - explicitGowork span.URI - - // workspaceInformation tracks various details about this view's - // environment variables, go version, and use of modules. - workspaceInformation } type workspaceInformation struct { @@ -494,12 +493,13 @@ func (v *View) relevantChange(c source.FileModification) bool { return true } } - // If the file is not known to the view, and the change is only on-disk, - // we should not invalidate the snapshot. This is necessary because Emacs - // sends didChangeWatchedFiles events for temp files. - if c.OnDisk && (c.Action == source.Change || c.Action == source.Delete) { - return false - } + + // Note: CL 219202 filtered out on-disk changes here that were not known to + // the view, but this introduces a race when changes arrive before the view + // is initialized (and therefore, before it knows about files). Since that CL + // had neither test nor associated issue, and cited only emacs behavior, this + // logic was deleted. + return v.contains(c.URI) } @@ -601,7 +601,7 @@ func (s *snapshot) IgnoredFile(uri span.URI) bool { } else { prefixes = append(prefixes, s.view.gomodcache) for m := range s.workspace.ActiveModFiles() { - prefixes = append(prefixes, dirURI(m).Filename()) + prefixes = append(prefixes, span.Dir(m).Filename()) } } for _, prefix := range prefixes { @@ -853,18 +853,15 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, }, nil } -// findWorkspaceRoot searches for the best workspace root according to the -// following heuristics: -// - First, look for a parent directory containing a gopls.mod file -// (experimental only). -// - Then, a parent directory containing a go.mod file. -// - Then, a child directory containing a go.mod file, if there is exactly -// one (non-experimental only). +// findWorkspaceModuleSource searches for a "module source" relative to the +// given folder URI. A module source is the go.work or go.mod file that +// provides module information. +// +// As a special case, this function returns a module source in a nested +// directory if it finds no other module source, and exactly one nested module. // -// Otherwise, it returns folder. -// TODO (rFindley): move this to workspace.go -// TODO (rFindley): simplify this once workspace modules are enabled by default. -func findWorkspaceRoot(ctx context.Context, folderURI span.URI, fs source.FileSource, excludePath func(string) bool, experimental bool) (span.URI, error) { +// If no module source is found, it returns "". +func findWorkspaceModuleSource(ctx context.Context, folderURI span.URI, fs source.FileSource, excludePath func(string) bool, experimental bool) (span.URI, error) { patterns := []string{"go.work", "go.mod"} if experimental { patterns = []string{"go.work", "gopls.mod", "go.mod"} @@ -879,14 +876,13 @@ func findWorkspaceRoot(ctx context.Context, folderURI span.URI, fs source.FileSo return "", err } if match != "" { - dir := span.URIFromPath(filepath.Dir(match)) - return dir, nil + return span.URIFromPath(match), nil } } // The experimental workspace can handle nested modules at this point... if experimental { - return folderURI, nil + return "", nil } // ...else we should check if there's exactly one nested module. @@ -895,7 +891,7 @@ func findWorkspaceRoot(ctx context.Context, folderURI span.URI, fs source.FileSo // Fall-back behavior: if we don't find any modules after searching 10000 // files, assume there are none. event.Log(ctx, fmt.Sprintf("stopped searching for modules after %d files", fileLimit)) - return folderURI, nil + return "", nil } if err != nil { return "", err @@ -903,10 +899,10 @@ func findWorkspaceRoot(ctx context.Context, folderURI span.URI, fs source.FileSo if len(all) == 1 { // range to access first element. for uri := range all { - return dirURI(uri), nil + return uri, nil } } - return folderURI, nil + return "", nil } // findRootPattern looks for files with the given basename in dir or any parent diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go index b2fca90f151..617dc31a0e7 100644 --- a/gopls/internal/lsp/cache/view_test.go +++ b/gopls/internal/lsp/cache/view_test.go @@ -53,7 +53,7 @@ func TestCaseInsensitiveFilesystem(t *testing.T) { } } -func TestFindWorkspaceRoot(t *testing.T) { +func TestFindWorkspaceModuleSource(t *testing.T) { workspace := ` -- a/go.mod -- module a @@ -71,6 +71,10 @@ module d-goplsworkspace module de -- f/g/go.mod -- module fg +-- h/go.work -- +go 1.18 +-- h/i/go.mod -- +module hi ` dir, err := fake.Tempdir(fake.UnpackTxt(workspace)) if err != nil { @@ -83,16 +87,18 @@ module fg experimental bool }{ {"", "", false}, // no module at root, and more than one nested module - {"a", "a", false}, - {"a/x", "a", false}, - {"a/x/y", "a", false}, - {"b/c", "b/c", false}, - {"d", "d/e", false}, - {"d", "d", true}, - {"d/e", "d/e", false}, - {"d/e", "d", true}, - {"f", "f/g", false}, - {"f", "f", true}, + {"a", "a/go.mod", false}, + {"a/x", "a/go.mod", false}, + {"a/x/y", "a/go.mod", false}, + {"b/c", "b/c/go.mod", false}, + {"d", "d/e/go.mod", false}, + {"d", "d/gopls.mod", true}, + {"d/e", "d/e/go.mod", false}, + {"d/e", "d/gopls.mod", true}, + {"f", "f/g/go.mod", false}, + {"f", "", true}, + {"h", "h/go.work", false}, + {"h/i", "h/go.work", false}, } for _, test := range tests { @@ -100,12 +106,16 @@ module fg rel := fake.RelativeTo(dir) folderURI := span.URIFromPath(rel.AbsPath(test.folder)) excludeNothing := func(string) bool { return false } - got, err := findWorkspaceRoot(ctx, folderURI, &osFileSource{}, excludeNothing, test.experimental) + got, err := findWorkspaceModuleSource(ctx, folderURI, &osFileSource{}, excludeNothing, test.experimental) if err != nil { t.Fatal(err) } - if gotf, wantf := filepath.Clean(got.Filename()), rel.AbsPath(test.want); gotf != wantf { - t.Errorf("findWorkspaceRoot(%q, %t) = %q, want %q", test.folder, test.experimental, gotf, wantf) + want := span.URI("") + if test.want != "" { + want = span.URIFromPath(rel.AbsPath(test.want)) + } + if got != want { + t.Errorf("findWorkspaceModuleSource(%q, %t) = %q, want %q", test.folder, test.experimental, got, want) } } } diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go index a97894ab5ba..da2abdb68b4 100644 --- a/gopls/internal/lsp/cache/workspace.go +++ b/gopls/internal/lsp/cache/workspace.go @@ -411,76 +411,53 @@ func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, // handleWorkspaceFileChanges handles changes related to a go.work or gopls.mod // file, updating ws accordingly. ws.root must be set. func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[span.URI]*fileChange, fs source.FileSource) (changed, reload bool) { - // If go.work/gopls.mod has changed we need to either re-read it if it - // exists or walk the filesystem if it has been deleted. - // go.work should override the gopls.mod if both exist. - for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - uri := uriForSource(ws.root, ws.explicitGowork, src) - // File opens/closes are just no-ops. - change, ok := changes[uri] - if !ok { - continue - } - if change.isUnchanged { - break + if ws.moduleSource != goWorkWorkspace && ws.moduleSource != goplsModWorkspace { + return false, false + } + + uri := uriForSource(ws.root, ws.explicitGowork, ws.moduleSource) + // File opens/closes are just no-ops. + change, ok := changes[uri] + if !ok || change.isUnchanged { + return false, false + } + if change.exists { + // Only invalidate if the file if it actually parses. + // Otherwise, stick with the current file. + var parsedFile *modfile.File + var parsedModules map[span.URI]struct{} + var err error + switch ws.moduleSource { + case goWorkWorkspace: + parsedFile, parsedModules, err = parseGoWork(ctx, ws.root, uri, change.content, fs) + case goplsModWorkspace: + parsedFile, parsedModules, err = parseGoplsMod(ws.root, uri, change.content) } - if change.exists { - // Only invalidate if the file if it actually parses. - // Otherwise, stick with the current file. - var parsedFile *modfile.File - var parsedModules map[span.URI]struct{} - var err error - switch src { - case goWorkWorkspace: - parsedFile, parsedModules, err = parseGoWork(ctx, ws.root, uri, change.content, fs) - case goplsModWorkspace: - parsedFile, parsedModules, err = parseGoplsMod(ws.root, uri, change.content) - } - if err != nil { - // An unparseable file should not invalidate the workspace: - // nothing good could come from changing the workspace in - // this case. - // - // TODO(rfindley): well actually, it could potentially lead to a better - // critical error. Evaluate whether we can unify this case with the - // error returned by newWorkspace, without needlessly invalidating - // metadata. - event.Error(ctx, fmt.Sprintf("parsing %s", filepath.Base(uri.Filename())), err) - } else { - // only update the modfile if it parsed. - changed = true - reload = change.fileHandle.Saved() - ws.mod = parsedFile - ws.moduleSource = src - ws.knownModFiles = parsedModules - ws.activeModFiles = make(map[span.URI]struct{}) - for k, v := range parsedModules { - ws.activeModFiles[k] = v - } - } - break // We've found an explicit workspace file, so can stop looking. + if err != nil { + // An unparseable file should not invalidate the workspace: + // nothing good could come from changing the workspace in + // this case. + // + // TODO(rfindley): well actually, it could potentially lead to a better + // critical error. Evaluate whether we can unify this case with the + // error returned by newWorkspace, without needlessly invalidating + // metadata. + event.Error(ctx, fmt.Sprintf("parsing %s", filepath.Base(uri.Filename())), err) } else { - // go.work/gopls.mod is deleted. search for modules again. + // only update the modfile if it parsed. changed = true - reload = true - ws.moduleSource = fileSystemWorkspace - // The parsed file is no longer valid. - ws.mod = nil - knownModFiles, err := findModules(ws.root, ws.excludePath, 0) - if err != nil { - ws.knownModFiles = nil - ws.activeModFiles = nil - event.Error(ctx, "finding file system modules", err) - } else { - ws.knownModFiles = knownModFiles - ws.activeModFiles = make(map[span.URI]struct{}) - for k, v := range ws.knownModFiles { - ws.activeModFiles[k] = v - } + reload = change.fileHandle.Saved() + ws.mod = parsedFile + ws.knownModFiles = parsedModules + ws.activeModFiles = make(map[span.URI]struct{}) + for k, v := range parsedModules { + ws.activeModFiles[k] = v } } + return changed, reload } - return changed, reload + // go.work/gopls.mod is deleted. We should never see this as the view should have been recreated. + panic(fmt.Sprintf("internal error: workspace file %q deleted without reinitialization", uri)) } // goplsModURI returns the URI for the gopls.mod file contained in root. @@ -510,6 +487,12 @@ func isGoMod(uri span.URI) bool { return filepath.Base(uri.Filename()) == "go.mod" } +// isGoWork reports if uri is a go.work file. +func isGoWork(uri span.URI) bool { + return filepath.Base(uri.Filename()) == "go.work" +} + +// isGoSum reports if uri is a go.sum or go.work.sum file. func isGoSum(uri span.URI) bool { return filepath.Base(uri.Filename()) == "go.sum" || filepath.Base(uri.Filename()) == "go.work.sum" } @@ -535,12 +518,6 @@ func fileHandleExists(fh source.FileHandle) (bool, error) { return false, err } -// TODO(rFindley): replace this (and similar) with a uripath package analogous -// to filepath. -func dirURI(uri span.URI) span.URI { - return span.URIFromPath(filepath.Dir(uri.Filename())) -} - // getLegacyModules returns a module set containing at most the root module. func getLegacyModules(ctx context.Context, root span.URI, fs source.FileSource) (map[span.URI]struct{}, error) { uri := span.URIFromPath(filepath.Join(root.Filename(), "go.mod")) diff --git a/gopls/internal/lsp/cache/workspace_test.go b/gopls/internal/lsp/cache/workspace_test.go index 188869562c5..45ae0cc3432 100644 --- a/gopls/internal/lsp/cache/workspace_test.go +++ b/gopls/internal/lsp/cache/workspace_test.go @@ -143,38 +143,6 @@ module moda.com`, dirs: []string{".", "a"}, }, }, - { - desc: "removing module", - initial: ` --- a/go.mod -- -module moda.com --- a/go.sum -- -golang.org/x/mod v0.3.0 h1:deadbeef --- b/go.mod -- -module modb.com --- b/go.sum -- -golang.org/x/mod v0.3.0 h1:beefdead`, - initialState: wsState{ - modules: []string{"a/go.mod", "b/go.mod"}, - source: fileSystemWorkspace, - dirs: []string{".", "a", "b"}, - sum: "golang.org/x/mod v0.3.0 h1:beefdead\ngolang.org/x/mod v0.3.0 h1:deadbeef\n", - }, - updates: map[string]wsChange{ - "gopls.mod": {`module gopls-workspace - -require moda.com v0.0.0-goplsworkspace -replace moda.com => $SANDBOX_WORKDIR/a`, true}, - }, - wantChanged: true, - wantReload: true, - finalState: wsState{ - modules: []string{"a/go.mod"}, - source: goplsModWorkspace, - dirs: []string{".", "a"}, - sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n", - }, - }, { desc: "adding module", initial: ` @@ -207,34 +175,6 @@ replace modb.com => $SANDBOX_WORKDIR/b`, true}, dirs: []string{".", "a", "b"}, }, }, - { - desc: "deleting gopls.mod", - initial: ` --- gopls.mod -- -module gopls-workspace - -require moda.com v0.0.0-goplsworkspace -replace moda.com => $SANDBOX_WORKDIR/a --- a/go.mod -- -module moda.com --- b/go.mod -- -module modb.com`, - initialState: wsState{ - modules: []string{"a/go.mod"}, - source: goplsModWorkspace, - dirs: []string{".", "a"}, - }, - updates: map[string]wsChange{ - "gopls.mod": {"", true}, - }, - wantChanged: true, - wantReload: true, - finalState: wsState{ - modules: []string{"a/go.mod", "b/go.mod"}, - source: fileSystemWorkspace, - dirs: []string{".", "a", "b"}, - }, - }, { desc: "broken module parsing", initial: ` diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 8a9e24d3a29..4c3c9d74be6 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -362,11 +362,11 @@ func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.Fol // TODO(adonovan): factor to use diff.ApplyEdits, which validates the input. for i := len(ranges) - 1; i >= 0; i-- { r := ranges[i] - start, err := m.Point(protocol.Position{r.StartLine, r.StartCharacter}) + start, err := m.Point(protocol.Position{Line: r.StartLine, Character: r.StartCharacter}) if err != nil { return "", err } - end, err := m.Point(protocol.Position{r.EndLine, r.EndCharacter}) + end, err := m.Point(protocol.Position{Line: r.EndLine, Character: r.EndCharacter}) if err != nil { return "", err } diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 7c9cc9bf01e..5906d4c1eac 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -533,6 +533,9 @@ module mod.com go 1.12 require example.com v1.2.2 +-- go.sum -- +example.com v1.2.3 h1:OnPPkx+rW63kj9pgILsu12MORKhSlnFa3DVRJq1HZ7g= +example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= -- main.go -- package main @@ -561,9 +564,9 @@ func main() { } `, }) - env.Await( + env.AfterChange( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("main.go"), + EmptyOrNoDiagnostics("main.go"), ) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 38cd3a9294e..0efb82213e7 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1089,13 +1089,17 @@ package main }) } -func TestAddGoWork(t *testing.T) { +func TestAddAndRemoveGoWork(t *testing.T) { + // Use a workspace with a module in the root directory to exercise the case + // where a go.work is added to the existing root directory. This verifies + // that we're detecting changes to the module source, not just the root + // directory. const nomod = ` --- a/go.mod -- +-- go.mod -- module a.com go 1.16 --- a/main.go -- +-- main.go -- package main func main() {} @@ -1111,20 +1115,34 @@ func main() {} WithOptions( Modes(Default), ).Run(t, nomod, func(t *testing.T, env *Env) { - env.OpenFile("a/main.go") + env.OpenFile("main.go") env.OpenFile("b/main.go") - env.Await( - DiagnosticAt("a/main.go", 0, 0), + // Since b/main.go is not in the workspace, it should have a warning on its + // package declaration. + env.AfterChange( + EmptyDiagnostics("main.go"), DiagnosticAt("b/main.go", 0, 0), ) env.WriteWorkspaceFile("go.work", `go 1.16 use ( - a + . b ) `) - env.Await(NoOutstandingDiagnostics()) + env.AfterChange(NoOutstandingDiagnostics()) + // Removing the go.work file should put us back where we started. + env.RemoveWorkspaceFile("go.work") + + // TODO(rfindley): fix this bug: reopening b/main.go is necessary here + // because we no longer "see" the file in any view. + env.CloseBuffer("b/main.go") + env.OpenFile("b/main.go") + + env.AfterChange( + EmptyDiagnostics("main.go"), + DiagnosticAt("b/main.go", 0, 0), + ) }) } diff --git a/gopls/internal/span/uri.go b/gopls/internal/span/uri.go index cf2d66df20b..e6191f7ab12 100644 --- a/gopls/internal/span/uri.go +++ b/gopls/internal/span/uri.go @@ -175,3 +175,11 @@ func isWindowsDriveURIPath(uri string) bool { } return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' } + +// Dir returns the URI for the directory containing uri. Dir panics if uri is +// not a file uri. +// +// TODO(rfindley): add a unit test for various edge cases. +func Dir(uri URI) URI { + return URIFromPath(filepath.Dir(uri.Filename())) +} From 3b16059a0a4f211224d2ec02033449f722e5049f Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 29 Dec 2022 09:56:30 -0500 Subject: [PATCH 569/723] gopls/internal/regtest: make BufferText strict Make Editor.BufferText differentiate between empty and unopen files. Typically, the caller assumes that the file is open, so add a wrapper that implements this assertion. Change-Id: I5cd13a60e6884a45f30ca65726acd9220bcab346 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459781 Run-TryBot: Robert Findley Reviewed-by: Dylan Le TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/fake/editor.go | 12 +++++++++--- gopls/internal/lsp/regtest/wrappers.go | 11 +++++++++++ gopls/internal/regtest/bench/completion_test.go | 8 ++++---- gopls/internal/regtest/codelens/codelens_test.go | 10 +++++----- .../regtest/completion/completion_test.go | 6 +++--- .../regtest/completion/postfix_snippet_test.go | 2 +- gopls/internal/regtest/misc/extract_test.go | 2 +- gopls/internal/regtest/misc/fix_test.go | 2 +- gopls/internal/regtest/misc/formatting_test.go | 16 ++++++++-------- gopls/internal/regtest/misc/import_test.go | 2 +- gopls/internal/regtest/misc/imports_test.go | 4 ++-- gopls/internal/regtest/misc/rename_test.go | 6 +++--- .../internal/regtest/misc/semantictokens_test.go | 2 +- gopls/internal/regtest/misc/vuln_test.go | 2 +- gopls/internal/regtest/modfile/modfile_test.go | 10 +++++----- .../regtest/workspace/directoryfilters_test.go | 8 ++++---- 16 files changed, 60 insertions(+), 43 deletions(-) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 7b476529573..a3a39d49411 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -702,11 +702,17 @@ func (e *Editor) HasBuffer(name string) bool { return ok } -// BufferText returns the content of the buffer with the given name. -func (e *Editor) BufferText(name string) string { +// BufferText returns the content of the buffer with the given name, or "" if +// the file at that path is not open. The second return value reports whether +// the file is open. +func (e *Editor) BufferText(name string) (string, bool) { e.mu.Lock() defer e.mu.Unlock() - return e.buffers[name].text() + buf, ok := e.buffers[name] + if !ok { + return "", false + } + return buf.text(), true } // BufferVersion returns the current version of the buffer corresponding to diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index 71939660b6c..19713f24635 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -78,6 +78,17 @@ func (e *Env) CreateBuffer(name string, content string) { } } +// BufferText returns the current buffer contents for the file with the given +// relative path, calling t.Fatal if the file is not open in a buffer. +func (e *Env) BufferText(name string) string { + e.T.Helper() + text, ok := e.Editor.BufferText(name) + if !ok { + e.T.Fatalf("buffer %q is not open", name) + } + return text +} + // CloseBuffer closes an editor buffer without saving, calling t.Fatal on any // error. func (e *Env) CloseBuffer(name string) { diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go index 7b833b6e7c4..51860fdc765 100644 --- a/gopls/internal/regtest/bench/completion_test.go +++ b/gopls/internal/regtest/bench/completion_test.go @@ -85,7 +85,7 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) { // endPosInBuffer returns the position for last character in the buffer for // the given file. func endPosInBuffer(env *Env, name string) fake.Pos { - buffer := env.Editor.BufferText(name) + buffer := env.BufferText(name) lines := strings.Split(buffer, "\n") numLines := len(lines) @@ -101,7 +101,7 @@ func BenchmarkStructCompletion(b *testing.B) { setup := func(env *Env) { env.OpenFile(file) - originalBuffer := env.Editor.BufferText(file) + originalBuffer := env.BufferText(file) env.EditBuffer(file, fake.Edit{ End: endPosInBuffer(env, file), Text: originalBuffer + "\nvar testVariable map[string]bool = Session{}.\n", @@ -131,7 +131,7 @@ func BenchmarkSliceCompletion(b *testing.B) { setup := func(env *Env) { env.OpenFile(file) - originalBuffer := env.Editor.BufferText(file) + originalBuffer := env.BufferText(file) env.EditBuffer(file, fake.Edit{ End: endPosInBuffer(env, file), Text: originalBuffer + "\nvar testVariable []byte = \n", @@ -155,7 +155,7 @@ func (c *completer) _() { ` setup := func(env *Env) { env.OpenFile(file) - originalBuffer := env.Editor.BufferText(file) + originalBuffer := env.BufferText(file) env.EditBuffer(file, fake.Edit{ End: endPosInBuffer(env, file), Text: originalBuffer + fileContent, diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index aabddb91ddf..734499cd63a 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -186,10 +186,10 @@ require golang.org/x/hello v1.2.3 t.Fatal(err) } env.Await(env.DoneWithChangeWatchedFiles()) - if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA { + if got := env.BufferText("a/go.mod"); got != wantGoModA { t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) } - if got := env.Editor.BufferText("b/go.mod"); got != wantGoModB { + if got := env.BufferText("b/go.mod"); got != wantGoModB { t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) } }) @@ -226,10 +226,10 @@ require golang.org/x/hello v1.2.3 // Apply the diagnostics to a/go.mod. env.ApplyQuickFixes("a/go.mod", d.Diagnostics) env.Await(env.DoneWithChangeWatchedFiles()) - if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA { + if got := env.BufferText("a/go.mod"); got != wantGoModA { t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) } - if got := env.Editor.BufferText("b/go.mod"); got != wantGoModB { + if got := env.BufferText("b/go.mod"); got != wantGoModB { t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) } }) @@ -283,7 +283,7 @@ func main() { env.OpenFile("go.mod") env.ExecuteCodeLensCommand("go.mod", command.Tidy, nil) env.Await(env.DoneWithChangeWatchedFiles()) - got := env.Editor.BufferText("go.mod") + got := env.BufferText("go.mod") const wantGoMod = `module mod.com go 1.14 diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 3d5afe4de68..08a14215e94 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -182,7 +182,7 @@ package // Check that the completion item suggestions are in the range // of the file. - lineCount := len(strings.Split(env.Editor.BufferText(tc.filename), "\n")) + lineCount := len(strings.Split(env.BufferText(tc.filename), "\n")) for _, item := range completions.Items { if start := int(item.TextEdit.Range.Start.Line); start >= lineCount { t.Fatalf("unexpected text edit range start line number: got %d, want less than %d", start, lineCount) @@ -536,7 +536,7 @@ func main() { } env.AcceptCompletion("main.go", pos, completions.Items[0]) env.Await(env.DoneWithChange()) - got := env.Editor.BufferText("main.go") + got := env.BufferText("main.go") want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:})\r\n}\r\n" if got != want { t.Errorf("unimported completion: got %q, want %q", got, want) @@ -674,7 +674,7 @@ $0 env.AcceptCompletion("foo_test.go", pos, completions.Items[0]) env.Await(env.DoneWithChange()) - if buf := env.Editor.BufferText("foo_test.go"); buf != tst.after { + if buf := env.BufferText("foo_test.go"); buf != tst.after { t.Errorf("incorrect completion: got %q, want %q", buf, tst.after) } } diff --git a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go index 56e26a235bf..6d2be0594d6 100644 --- a/gopls/internal/regtest/completion/postfix_snippet_test.go +++ b/gopls/internal/regtest/completion/postfix_snippet_test.go @@ -455,7 +455,7 @@ func foo() string { env.AcceptCompletion("foo.go", pos, completions.Items[0]) - if buf := env.Editor.BufferText("foo.go"); buf != c.after { + if buf := env.BufferText("foo.go"); buf != c.after { t.Errorf("\nGOT:\n%s\nEXPECTED:\n%s", buf, c.after) } }) diff --git a/gopls/internal/regtest/misc/extract_test.go b/gopls/internal/regtest/misc/extract_test.go index 45ddb5408a5..816827716cb 100644 --- a/gopls/internal/regtest/misc/extract_test.go +++ b/gopls/internal/regtest/misc/extract_test.go @@ -62,7 +62,7 @@ func newFunction() int { return a } ` - if got := env.Editor.BufferText("main.go"); got != want { + if got := env.BufferText("main.go"); got != want { t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) } }) diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index f9283d74f72..8939e004dc7 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -55,7 +55,7 @@ func Foo() { } } ` - if got := env.Editor.BufferText("main.go"); got != want { + if got := env.BufferText("main.go"); got != want { t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) } }) diff --git a/gopls/internal/regtest/misc/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go index 39d58229896..ee8098cc93b 100644 --- a/gopls/internal/regtest/misc/formatting_test.go +++ b/gopls/internal/regtest/misc/formatting_test.go @@ -34,7 +34,7 @@ func TestFormatting(t *testing.T) { Run(t, unformattedProgram, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.FormatBuffer("main.go") - got := env.Editor.BufferText("main.go") + got := env.BufferText("main.go") want := env.ReadWorkspaceFile("main.go.golden") if got != want { t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) @@ -56,7 +56,7 @@ func f() {} Run(t, onelineProgram, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.FormatBuffer("a.go") - got := env.Editor.BufferText("a.go") + got := env.BufferText("a.go") want := env.ReadWorkspaceFile("a.go.formatted") if got != want { t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) @@ -80,7 +80,7 @@ func f() { fmt.Println() } Run(t, onelineProgramA, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.OrganizeImports("a.go") - got := env.Editor.BufferText("a.go") + got := env.BufferText("a.go") want := env.ReadWorkspaceFile("a.go.imported") if got != want { t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) @@ -101,7 +101,7 @@ func f() {} Run(t, onelineProgramB, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.OrganizeImports("a.go") - got := env.Editor.BufferText("a.go") + got := env.BufferText("a.go") want := env.ReadWorkspaceFile("a.go.imported") if got != want { t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) @@ -147,7 +147,7 @@ func TestOrganizeImports(t *testing.T) { Run(t, disorganizedProgram, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OrganizeImports("main.go") - got := env.Editor.BufferText("main.go") + got := env.BufferText("main.go") want := env.ReadWorkspaceFile("main.go.organized") if got != want { t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) @@ -159,7 +159,7 @@ func TestFormattingOnSave(t *testing.T) { Run(t, disorganizedProgram, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.SaveBuffer("main.go") - got := env.Editor.BufferText("main.go") + got := env.BufferText("main.go") want := env.ReadWorkspaceFile("main.go.formatted") if got != want { t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) @@ -259,7 +259,7 @@ func main() { env.CreateBuffer("main.go", crlf) env.Await(env.DoneWithOpen()) env.OrganizeImports("main.go") - got := env.Editor.BufferText("main.go") + got := env.BufferText("main.go") got = strings.ReplaceAll(got, "\r\n", "\n") // convert everything to LF for simplicity if tt.want != got { t.Errorf("unexpected content after save:\n%s", compare.Text(tt.want, got)) @@ -359,7 +359,7 @@ const Bar = 42 ).Run(t, input, func(t *testing.T, env *Env) { env.OpenFile("foo.go") env.FormatBuffer("foo.go") - got := env.Editor.BufferText("foo.go") + got := env.BufferText("foo.go") want := env.ReadWorkspaceFile("foo.go.formatted") if got != want { t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) diff --git a/gopls/internal/regtest/misc/import_test.go b/gopls/internal/regtest/misc/import_test.go index 2e95e83a562..30986ba5077 100644 --- a/gopls/internal/regtest/misc/import_test.go +++ b/gopls/internal/regtest/misc/import_test.go @@ -49,7 +49,7 @@ func main() { Command: "gopls.add_import", Arguments: cmd.Arguments, }, nil) - got := env.Editor.BufferText("main.go") + got := env.BufferText("main.go") if got != want { t.Fatalf("gopls.add_import failed\n%s", compare.Text(want, got)) } diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index 60da30f8b3b..221a6ed2520 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -47,7 +47,7 @@ func TestZ(t *testing.T) { Run(t, needs, func(t *testing.T, env *Env) { env.CreateBuffer("a_test.go", ntest) env.SaveBuffer("a_test.go") - got := env.Editor.BufferText("a_test.go") + got := env.BufferText("a_test.go") if want != got { t.Errorf("got\n%q, wanted\n%q", got, want) } @@ -76,7 +76,7 @@ func main() { env.OrganizeImports("main.go") actions := env.CodeAction("main.go", nil) if len(actions) > 0 { - got := env.Editor.BufferText("main.go") + got := env.BufferText("main.go") t.Errorf("unexpected actions %#v", actions) if got == vim1 { t.Errorf("no changes") diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 7a7c4ec1ce5..82ca1d103bd 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -373,7 +373,7 @@ func main() { env.OpenFile("main.go") pos := env.RegexpSearch("main.go", `stringutil\.(Identity)`) env.Rename("main.go", pos, "Identityx") - text := env.Editor.BufferText("stringutil/stringutil_test.go") + text := env.BufferText("stringutil/stringutil_test.go") if !strings.Contains(text, "Identityx") { t.Errorf("stringutil/stringutil_test.go: missing expected token `Identityx` after rename:\n%s", text) } @@ -442,7 +442,7 @@ package b ) // As a sanity check, verify that x/x.go is open. - if text := env.Editor.BufferText("x/x.go"); text == "" { + if text := env.BufferText("x/x.go"); text == "" { t.Fatal("got empty buffer for x/x.go") } }) @@ -933,7 +933,7 @@ func checkTestdata(t *testing.T, env *Env) { } for _, file := range files { suffix := strings.TrimPrefix(file, "testdata/") - got := env.Editor.BufferText(suffix) + got := env.BufferText(suffix) want := env.ReadWorkspaceFile(file) if diff := compare.Text(want, got); diff != "" { t.Errorf("Rename: unexpected buffer content for %s (-want +got):\n%s", suffix, diff) diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index cd9dce35c68..422147cb33e 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -103,7 +103,7 @@ func Add[T int](target T, l []T) []T { if err != nil { t.Fatal(err) } - seen := interpret(v.Data, env.Editor.BufferText("main.go")) + seen := interpret(v.Data, env.BufferText("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/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index cf40da772b4..45d52c3d88f 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -749,7 +749,7 @@ require ( golang.org/bmod v0.5.0 // indirect ) ` - if got := env.Editor.BufferText("go.mod"); got != wantGoMod { + if got := env.BufferText("go.mod"); got != wantGoMod { t.Fatalf("go.mod vulncheck fix failed:\n%s", compare.Text(wantGoMod, got)) } }) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index aeb2f043cde..3128dc96ff7 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -352,7 +352,7 @@ require example.com v1.2.3 ), ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) - if got := env.Editor.BufferText("a/go.mod"); got != want { + if got := env.BufferText("a/go.mod"); got != want { t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) @@ -396,7 +396,7 @@ go 1.14 ), ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) - if got := env.Editor.BufferText("a/go.mod"); got != want { + if got := env.BufferText("a/go.mod"); got != want { t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) @@ -572,7 +572,7 @@ require ( ` env.SaveBuffer("a/go.mod") env.Await(EmptyDiagnostics("a/main.go")) - if got := env.Editor.BufferText("a/go.mod"); got != want { + if got := env.BufferText("a/go.mod"); got != want { t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got)) } }) @@ -996,7 +996,7 @@ func main() {} go 1.12 ` env.ApplyQuickFixes("go.mod", d.Diagnostics) - if got := env.Editor.BufferText("go.mod"); got != want { + if got := env.BufferText("go.mod"); got != want { t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) } }) @@ -1049,7 +1049,7 @@ require random.com v1.2.3 diagnostics = append(diagnostics, d) } env.ApplyQuickFixes("go.mod", diagnostics) - if got := env.Editor.BufferText("go.mod"); got != want { + if got := env.BufferText("go.mod"); got != want { t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) } }) diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go index 3efa9f322a8..94aa8b73464 100644 --- a/gopls/internal/regtest/workspace/directoryfilters_test.go +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -186,9 +186,9 @@ func Goodbye() {} Modes(Default), ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - beforeSave := env.Editor.BufferText("main.go") + beforeSave := env.BufferText("main.go") env.OrganizeImports("main.go") - got := env.Editor.BufferText("main.go") + got := env.BufferText("main.go") if got != beforeSave { t.Errorf("after organizeImports code action, got modified buffer:\n%s", got) } @@ -242,9 +242,9 @@ func Hi() {} }, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") - beforeSave := env.Editor.BufferText("a/main.go") + beforeSave := env.BufferText("a/main.go") env.OrganizeImports("a/main.go") - got := env.Editor.BufferText("a/main.go") + got := env.BufferText("a/main.go") if got == beforeSave { t.Errorf("after organizeImports code action, got identical buffer:\n%s", got) } From 8367fb27263d2178908844965e40b14e51800f40 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 29 Dec 2022 11:33:38 -0500 Subject: [PATCH 570/723] gopls/internal/regtest: await go.work changes in TestAddAndRemoveGoWork To temporarily avoid flakes, wait for go.work changes to be processed before reopening files. This merely works around the underlying bug, as noted in the comment. For golang/go#57508 Change-Id: I80d24c16ff709ff77a0fb07dd3ac7275bc8bcb5a Reviewed-on: https://go-review.googlesource.com/c/tools/+/459784 Auto-Submit: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/regtest/workspace/workspace_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 0efb82213e7..ceb710cea09 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1134,6 +1134,14 @@ use ( // Removing the go.work file should put us back where we started. env.RemoveWorkspaceFile("go.work") + // TODO(golang/go#57508): because file watching is asynchronous, we must + // ensure that the go.work change is seen before other changes, in order + // for the snapshot to "know" about the orphaned b/main.go below. + // + // This is a bug, plain and simple, but we await here to avoid test flakes + // while the underlying cause is fixed. + env.Await(env.DoneWithChangeWatchedFiles()) + // TODO(rfindley): fix this bug: reopening b/main.go is necessary here // because we no longer "see" the file in any view. env.CloseBuffer("b/main.go") From 81e741e32fac43eebb64438e6440cbf3dad7bbb5 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 28 Dec 2022 10:49:34 -0500 Subject: [PATCH 571/723] gopls/internal/lsp/safetoken: funnel more calls through this package This change expands the dominion of safetoken to include calls to token.File{,Set}.Position{,For}, since all need workarounds similar to that in Offset. As a side benefit, we now have a centralized place to implement the workaround for other bugs such as golang/go#41029, the newline at EOF problem). Unfortunately the former callers of FileSet.Position must stipulate whether the Pos is a start or an end, as the same value may denote the position 1 beyond the end of one file, or the start of the following file in the file set. Hence the two different functions, {Start,End}Position. The static check has been expanded, as have the tests. Of course, this approach is not foolproof: gopls has many dependencies that call methods of File and FileSet directly. Updates golang/go#41029 Updates golang/go#57484 Updates golang/go#57490 Change-Id: Ia727e3f55ca2703dd17ff2cac05e786793ca38eb Reviewed-on: https://go-review.googlesource.com/c/tools/+/459736 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan gopls-CI: kokoro --- gopls/doc/generate.go | 3 +- .../lsp/analysis/fillstruct/fillstruct.go | 5 +- .../lsp/analysis/undeclaredname/undeclared.go | 7 +- gopls/internal/lsp/cache/errors.go | 3 +- gopls/internal/lsp/safetoken/safetoken.go | 60 +++++++++++-- .../internal/lsp/safetoken/safetoken_test.go | 84 ++++++++++++------- gopls/internal/lsp/semantic.go | 12 +-- .../internal/lsp/source/completion/format.go | 3 +- .../internal/lsp/source/completion/package.go | 2 +- .../internal/lsp/source/completion/snippet.go | 3 +- gopls/internal/lsp/source/extract.go | 4 +- gopls/internal/lsp/source/format.go | 4 +- gopls/internal/lsp/source/implementation.go | 4 +- gopls/internal/lsp/source/references.go | 3 +- gopls/internal/lsp/source/stub.go | 4 +- gopls/internal/lsp/source/util.go | 3 +- gopls/internal/lsp/tests/tests.go | 5 +- gopls/internal/span/span.go | 4 +- gopls/internal/span/token.go | 3 +- 19 files changed, 152 insertions(+), 64 deletions(-) diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go index 8c0bbd3a701..d674bfce489 100644 --- a/gopls/doc/generate.go +++ b/gopls/doc/generate.go @@ -36,6 +36,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/command/commandmeta" "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" ) @@ -555,7 +556,7 @@ func upperFirst(x string) string { func fileForPos(pkg *packages.Package, pos token.Pos) (*ast.File, error) { fset := pkg.Fset for _, f := range pkg.Syntax { - if fset.PositionFor(f.Pos(), false).Filename == fset.PositionFor(pos, false).Filename { + if safetoken.StartPosition(fset, f.Pos()).Filename == safetoken.StartPosition(fset, pos).Filename { return f, nil } } diff --git a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go index 7c1e0442ded..00857a0953e 100644 --- a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -26,6 +26,7 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/fuzzy" @@ -271,8 +272,8 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast // Find the line on which the composite literal is declared. split := bytes.Split(content, []byte("\n")) - lineNumber := fset.PositionFor(expr.Lbrace, false).Line // ignore line directives - firstLine := split[lineNumber-1] // lines are 1-indexed + lineNumber := safetoken.StartPosition(fset, expr.Lbrace).Line + firstLine := split[lineNumber-1] // lines are 1-indexed // Trim the whitespace from the left of the line, and use the index // to get the amount of whitespace on the left. diff --git a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go index 646f4fa4786..3e42f08d55b 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -18,6 +18,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" ) @@ -112,8 +113,8 @@ func runForError(pass *analysis.Pass, err types.Error) { if tok == nil { return } - offset := pass.Fset.PositionFor(err.Pos, false).Offset - end := tok.Pos(offset + len(name)) + offset := safetoken.StartPosition(pass.Fset, err.Pos).Offset + end := tok.Pos(offset + len(name)) // TODO(adonovan): dubious! err.Pos + len(name)?? pass.Report(analysis.Diagnostic{ Pos: err.Pos, End: end, @@ -146,7 +147,7 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast return nil, fmt.Errorf("could not locate insertion point") } - insertBefore := fset.PositionFor(insertBeforeStmt.Pos(), false).Offset // ignore line directives + insertBefore := safetoken.StartPosition(fset, insertBeforeStmt.Pos()).Offset // Get the indent to add on the line after the new statement. // Since this will have a parse error, we can not use format.Source(). diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 7e685f3df4c..7ca4f078eff 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -20,6 +20,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" @@ -315,7 +316,7 @@ func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Sp if fset != terr.Fset { return 0, span.Span{}, bug.Errorf("wrong FileSet for type error") } - posn := fset.PositionFor(start, false) // ignore line directives + posn := safetoken.StartPosition(fset, start) if !posn.IsValid() { return 0, span.Span{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr) } diff --git a/gopls/internal/lsp/safetoken/safetoken.go b/gopls/internal/lsp/safetoken/safetoken.go index 200935612e0..fa9c67515f8 100644 --- a/gopls/internal/lsp/safetoken/safetoken.go +++ b/gopls/internal/lsp/safetoken/safetoken.go @@ -3,8 +3,12 @@ // license that can be found in the LICENSE file. // Package safetoken provides wrappers around methods in go/token, -// that return errors rather than panicking. It also provides a -// central place for workarounds in the underlying packages. +// that return errors rather than panicking. +// +// It also provides a central place for workarounds in the underlying +// packages. The use of this package's functions instead of methods of +// token.File (such as Offset, Position, and PositionFor) is mandatory +// throughout the gopls codebase and enforced by a static check. package safetoken import ( @@ -22,9 +26,6 @@ import ( // token.File.Offset to panic. The workaround is that this function // accepts a Pos that is exactly 1 byte beyond EOF and maps it to the // EOF offset. -// -// The use of this function instead of (*token.File).Offset is -// mandatory in the gopls codebase; this is enforced by static check. func Offset(f *token.File, pos token.Pos) (int, error) { if !inRange(f, pos) { // Accept a Pos that is 1 byte beyond EOF, @@ -57,3 +58,52 @@ func Pos(f *token.File, offset int) (token.Pos, error) { func inRange(f *token.File, pos token.Pos) bool { return token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) } + +// Position returns the Position for the pos value in the given file. +// +// p must be NoPos, a valid Pos in the range of f, or exactly 1 byte +// beyond the end of f. (See [Offset] for explanation.) +// Any other value causes a panic. +// +// Line directives (//line comments) are ignored. +func Position(f *token.File, pos token.Pos) token.Position { + // Work around issue #57490. + if int(pos) == f.Base()+f.Size()+1 { + pos-- + } + + // TODO(adonovan): centralize the workaround for + // golang/go#41029 (newline at EOF) here too. + + return f.PositionFor(pos, false) +} + +// StartPosition converts a start Pos in the FileSet into a Position. +// +// Call this function only if start represents the start of a token or +// parse tree, such as the result of Node.Pos(). If start is the end of +// an interval, such as Node.End(), call EndPosition instead, as it +// may need the correction described at [Position]. +func StartPosition(fset *token.FileSet, start token.Pos) (_ token.Position) { + if f := fset.File(start); f != nil { + return Position(f, start) + } + return +} + +// EndPosition converts an end Pos in the FileSet into a Position. +// +// Call this function only if pos represents the end of +// a non-empty interval, such as the result of Node.End(). +func EndPosition(fset *token.FileSet, end token.Pos) (_ token.Position) { + if f := fset.File(end); f != nil && int(end) > f.Base() { + return Position(f, end) + } + + // Work around issue #57490. + if f := fset.File(end - 1); f != nil { + return Position(f, end) + } + + return +} diff --git a/gopls/internal/lsp/safetoken/safetoken_test.go b/gopls/internal/lsp/safetoken/safetoken_test.go index 452820f679b..afd569472ac 100644 --- a/gopls/internal/lsp/safetoken/safetoken_test.go +++ b/gopls/internal/lsp/safetoken/safetoken_test.go @@ -5,9 +5,11 @@ package safetoken_test import ( + "fmt" "go/parser" "go/token" "go/types" + "os" "testing" "golang.org/x/tools/go/packages" @@ -21,10 +23,20 @@ func TestWorkaroundIssue57490(t *testing.T) { // syntax nodes, computed as Rbrace+len("}"), to be beyond EOF. src := `package p; func f() { var x struct` fset := token.NewFileSet() - file, _ := parser.ParseFile(fset, "", src, 0) + file, _ := parser.ParseFile(fset, "a.go", src, 0) tf := fset.File(file.Pos()) + + // Add another file to the FileSet. + file2, _ := parser.ParseFile(fset, "b.go", "package q", 0) + + // This is the ambiguity of #57490... + if file.End() != file2.Pos() { + t.Errorf("file.End() %d != %d file2.Pos()", file.End(), file2.Pos()) + } + // ...which causes these statements to panic. if false { - tf.Offset(file.End()) // panic: invalid Pos value 36 (should be in [1, 35]) + tf.Offset(file.End()) // panic: invalid Pos value 36 (should be in [1, 35]) + tf.Position(file.End()) // panic: invalid Pos value 36 (should be in [1, 35]) } // The offset of the EOF position is the file size. @@ -39,57 +51,71 @@ func TestWorkaroundIssue57490(t *testing.T) { if err != nil || offset != tf.Size() { t.Errorf("Offset(ast.File.End()) = (%d, %v), want token.File.Size %d", offset, err, tf.Size()) } + + if got, want := safetoken.Position(tf, file.End()).String(), "a.go:1:35"; got != want { + t.Errorf("Position(ast.File.End()) = %s, want %s", got, want) + } + + if got, want := safetoken.EndPosition(fset, file.End()).String(), "a.go:1:35"; got != want { + t.Errorf("EndPosition(ast.File.End()) = %s, want %s", got, want) + } + + // Note that calling StartPosition on an end may yield the wrong file: + if got, want := safetoken.StartPosition(fset, file.End()).String(), "b.go:1:1"; got != want { + t.Errorf("StartPosition(ast.File.End()) = %s, want %s", got, want) + } } -// This test reports any unexpected uses of (*go/token.File).Offset within -// the gopls codebase to ensure that we don't check in more code that is prone -// to panicking. All calls to (*go/token.File).Offset should be replaced with -// calls to safetoken.Offset. -func TestGoplsSourceDoesNotCallTokenFileOffset(t *testing.T) { +// To reduce the risk of panic, or bugs for which this package +// provides a workaround, this test statically reports references to +// forbidden methods of token.File or FileSet throughout gopls and +// suggests alternatives. +func TestGoplsSourceDoesNotCallTokenFileMethods(t *testing.T) { testenv.NeedsGoPackages(t) - fset := token.NewFileSet() pkgs, err := packages.Load(&packages.Config{ - Fset: fset, Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, }, "go/token", "golang.org/x/tools/gopls/...") if err != nil { t.Fatal(err) } - var tokenPkg, safePkg *packages.Package + var tokenPkg *packages.Package for _, pkg := range pkgs { - switch pkg.PkgPath { - case "go/token": + if pkg.PkgPath == "go/token" { tokenPkg = pkg - case "golang.org/x/tools/gopls/internal/lsp/safetoken": - safePkg = pkg + break } } - if tokenPkg == nil { t.Fatal("missing package go/token") } - if safePkg == nil { - t.Fatal("missing package golang.org/x/tools/gopls/internal/lsp/safetoken") - } - fileObj := tokenPkg.Types.Scope().Lookup("File") - tokenOffset, _, _ := types.LookupFieldOrMethod(fileObj.Type(), true, fileObj.Pkg(), "Offset") + File := tokenPkg.Types.Scope().Lookup("File") + FileSet := tokenPkg.Types.Scope().Lookup("FileSet") - safeOffset := safePkg.Types.Scope().Lookup("Offset").(*types.Func) + alternative := make(map[types.Object]string) + setAlternative := func(recv types.Object, old, new string) { + oldMethod, _, _ := types.LookupFieldOrMethod(recv.Type(), true, recv.Pkg(), old) + alternative[oldMethod] = new + } + setAlternative(File, "Offset", "safetoken.Offset") + setAlternative(File, "Position", "safetoken.Position") + setAlternative(File, "PositionFor", "safetoken.Position") + setAlternative(FileSet, "Position", "safetoken.StartPosition or EndPosition") + setAlternative(FileSet, "PositionFor", "safetoken.StartPosition or EndPosition") for _, pkg := range pkgs { - if pkg.PkgPath == "go/token" { // Allow usage from within go/token itself. - continue + switch pkg.PkgPath { + case "go/token", "golang.org/x/tools/gopls/internal/lsp/safetoken": + continue // allow calls within these packages } + for ident, obj := range pkg.TypesInfo.Uses { - if obj != tokenOffset { - continue - } - if safeOffset.Pos() <= ident.Pos() && ident.Pos() <= safeOffset.Scope().End() { - continue // accepted usage + if alt, ok := alternative[obj]; ok { + posn := safetoken.StartPosition(pkg.Fset, ident.Pos()) + fmt.Fprintf(os.Stderr, "%s: forbidden use of %v; use %s instead.\n", posn, obj, alt) + t.Fail() } - t.Errorf(`%s: Unexpected use of (*go/token.File).Offset. Please use golang.org/x/tools/gopls/internal/lsp/safetoken.Offset instead.`, fset.PositionFor(ident.Pos(), false)) } } } diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 7368b59b613..4117eb70b82 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -243,7 +243,7 @@ func (e *encoded) strStack() string { 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 := e.pgf.Tok.PositionFor(loc, false) // ignore line directives + 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)) } @@ -413,7 +413,7 @@ func (e *encoded) inspector(n ast.Node) bool { return true // not going to see these case *ast.File, *ast.Package: - e.unexpected(fmt.Sprintf("implement %T %s", x, e.pgf.Tok.PositionFor(x.Pos(), false))) + 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() @@ -773,7 +773,7 @@ func (e *encoded) definitionFor(x *ast.Ident, def types.Object) (tokenType, []st } } // can't happen - msg := fmt.Sprintf("failed to find the decl for %s", e.pgf.Tok.PositionFor(x.Pos(), false)) + msg := fmt.Sprintf("failed to find the decl for %s", safetoken.Position(e.pgf.Tok, x.Pos())) e.unexpected(msg) return "", []string{""} } @@ -803,8 +803,8 @@ func (e *encoded) multiline(start, end token.Pos, val string, tok tokenType) { } return int(f.LineStart(line+1) - n) } - spos := e.fset.PositionFor(start, false) - epos := e.fset.PositionFor(end, false) + 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 @@ -827,7 +827,7 @@ func (e *encoded) findKeyword(keyword string, start, end token.Pos) token.Pos { return start + token.Pos(idx) } //(in unparsable programs: type _ <-<-chan int) - e.unexpected(fmt.Sprintf("not found:%s %v", keyword, e.fset.PositionFor(start, false))) + e.unexpected(fmt.Sprintf("not found:%s %v", keyword, safetoken.StartPosition(e.fset, start))) return token.NoPos } diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go index 0f8189a7bff..bb3ccd72d69 100644 --- a/gopls/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -14,6 +14,7 @@ import ( "strings" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" @@ -227,7 +228,7 @@ Suffixes: if !c.opts.documentation { return item, nil } - pos := c.pkg.FileSet().PositionFor(obj.Pos(), false) + pos := safetoken.StartPosition(c.pkg.FileSet(), obj.Pos()) // We ignore errors here, because some types, like "unsafe" or "error", // may not have valid positions that we can use to get documentation. diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index 65f86ca249d..70d98dfc839 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -124,7 +124,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, if cursorLine <= 0 || cursorLine > len(lines) { return nil, fmt.Errorf("invalid line number") } - if fset.PositionFor(expr.Pos(), false).Line == cursorLine { // ignore line directives + if safetoken.StartPosition(fset, expr.Pos()).Line == cursorLine { words := strings.Fields(lines[cursorLine-1]) if len(words) > 0 && words[0] == PACKAGE { content := PACKAGE diff --git a/gopls/internal/lsp/source/completion/snippet.go b/gopls/internal/lsp/source/completion/snippet.go index 811acf63b51..53577232939 100644 --- a/gopls/internal/lsp/source/completion/snippet.go +++ b/gopls/internal/lsp/source/completion/snippet.go @@ -7,6 +7,7 @@ package completion import ( "go/ast" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/snippet" ) @@ -43,7 +44,7 @@ func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snip // If the cursor position is on a different line from the literal's opening brace, // we are in a multiline literal. Ignore line directives. - if fset.PositionFor(c.pos, false).Line != fset.PositionFor(clInfo.cl.Lbrace, false).Line { + if safetoken.StartPosition(fset, c.pos).Line != safetoken.StartPosition(fset, clInfo.cl.Lbrace).Line { snip.WriteText(",") } } diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index b0ad550dfe8..0ac18e10390 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -28,7 +28,7 @@ func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast. tokFile := fset.File(file.Pos()) expr, path, ok, err := CanExtractVariable(rng, file) if !ok { - return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", fset.PositionFor(rng.Start, false), err) + return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", safetoken.StartPosition(fset, rng.Start), err) } // Create new AST node for extracted code. @@ -224,7 +224,7 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file p, ok, methodOk, err := CanExtractFunction(tok, rng, src, file) if (!ok && !isMethod) || (!methodOk && isMethod) { return nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix, - fset.PositionFor(rng.Start, false), err) + safetoken.StartPosition(fset, rng.Start), err) } tok, path, rng, outer, start := p.tok, p.path, p.rng, p.outer, p.start fileScope := info.Scopes[file] diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index 7aca5e7d06b..6662137b526 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -265,8 +265,8 @@ func importPrefix(src []byte) (string, error) { if end, err := safetoken.Offset(tok, c.End()); err != nil { return "", err } else if end > importEnd { - startLine := tok.PositionFor(c.Pos(), false).Line - endLine := tok.PositionFor(c.End(), false).Line + startLine := safetoken.Position(tok, c.Pos()).Line + endLine := safetoken.Position(tok, c.End()).Line // Work around golang/go#41197 by checking if the comment might // contain "\r", and if so, find the actual end position of the diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 3933fdf5a9b..cd2fc860a6e 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -153,7 +153,7 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. candObj = sel.Obj() } - pos := s.FileSet().PositionFor(candObj.Pos(), false) + pos := safetoken.StartPosition(s.FileSet(), candObj.Pos()) if candObj == queryMethod || seen[pos] { continue } @@ -166,7 +166,7 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. var posn token.Position if pkg != nil { - posn = pkg.FileSet().PositionFor(candObj.Pos(), false) + posn = safetoken.StartPosition(pkg.FileSet(), candObj.Pos()) } if seen[posn] { continue diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index f51f7f1f0da..a1dcc5417a9 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -15,6 +15,7 @@ import ( "strings" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" @@ -158,7 +159,7 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i } // Inv: qos[0].pkg != nil, since Pos is valid. // Inv: qos[*].pkg != nil, since all qos are logically the same declaration. - filename := qos[0].pkg.FileSet().PositionFor(pos, false).Filename + filename := safetoken.StartPosition(qos[0].pkg.FileSet(), pos).Filename pgf, err := qos[0].pkg.File(span.URIFromPath(filename)) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 18829fb60e0..2568bd07e20 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -47,7 +47,7 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi } // Parse the file defining the concrete type. - concreteFilename := snapshot.FileSet().PositionFor(si.Concrete.Obj().Pos(), false).Filename + concreteFilename := safetoken.StartPosition(snapshot.FileSet(), si.Concrete.Obj().Pos()).Filename concreteFH, err := snapshot.GetFile(ctx, span.URIFromPath(concreteFilename)) if err != nil { return nil, nil, err @@ -261,7 +261,7 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method return nil, fmt.Errorf("expected %v to be an interface but got %T", iface, ifaceObj.Type().Underlying()) } // Parse the imports from the file that declares the interface. - ifaceFilename := snapshot.FileSet().PositionFor(ifaceObj.Pos(), false).Filename + ifaceFilename := safetoken.StartPosition(snapshot.FileSet(), ifaceObj.Pos()).Filename ifaceFH, err := snapshot.GetFile(ctx, span.URIFromPath(ifaceFilename)) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 978e2c7209f..face4c99ac2 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -18,6 +18,7 @@ import ( "strings" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/typeparams" @@ -94,7 +95,7 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { for _, comment := range commentGroup.List { if matched := generatedRx.MatchString(comment.Text); matched { // Check if comment is at the beginning of the line in source. - if pgf.Tok.PositionFor(comment.Slash, false).Column == 1 { + if safetoken.Position(pgf.Tok, comment.Slash).Column == 1 { return true } } diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 8e977b08ccf..b7a6b047aef 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -29,6 +29,7 @@ import ( "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/completion" "golang.org/x/tools/gopls/internal/lsp/tests/compare" @@ -1368,7 +1369,7 @@ func (data *Data) collectWorkspaceSymbols(typ WorkspaceSymbolsTestType) func(*ex if data.WorkspaceSymbols[typ] == nil { data.WorkspaceSymbols[typ] = make(map[span.URI][]string) } - pos := data.Exported.ExpectFileSet.PositionFor(note.Pos, false) + pos := safetoken.StartPosition(data.Exported.ExpectFileSet, note.Pos) uri := span.URIFromPath(pos.Filename) data.WorkspaceSymbols[typ][uri] = append(data.WorkspaceSymbols[typ][uri], query) } @@ -1398,7 +1399,7 @@ func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain } func (data *Data) collectLinks(spn span.Span, link string, note *expect.Note, fset *token.FileSet) { - position := fset.PositionFor(note.Pos, false) // ignore line directives + position := safetoken.StartPosition(fset, note.Pos) uri := spn.URI() data.Links[uri] = append(data.Links[uri], Link{ Src: spn, diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go index 00c7b3f2a43..0c24a2deeed 100644 --- a/gopls/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -13,6 +13,8 @@ import ( "path" "sort" "strings" + + "golang.org/x/tools/gopls/internal/lsp/safetoken" ) // Span represents a source code range in standardized form. @@ -294,7 +296,7 @@ func (p *point) updateOffset(tf *token.File) error { // gopls' test suites to use Spans instead of Range in parameters. func (span *Span) SetRange(file *token.File, start, end token.Pos) { point := func(pos token.Pos) Point { - posn := file.Position(pos) + posn := safetoken.Position(file, pos) return NewPoint(posn.Line, posn.Column, posn.Offset) } *span = New(URIFromPath(file.Name()), point(start), point(end)) diff --git a/gopls/internal/span/token.go b/gopls/internal/span/token.go index cdc747f330e..ca78d67800f 100644 --- a/gopls/internal/span/token.go +++ b/gopls/internal/span/token.go @@ -8,6 +8,7 @@ import ( "fmt" "go/token" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/bug" ) @@ -119,7 +120,7 @@ func positionFromOffset(tf *token.File, offset int) (string, int, int, error) { return "", 0, 0, fmt.Errorf("offset %d is beyond EOF (%d) in file %s", offset, tf.Size(), tf.Name()) } pos := tf.Pos(offset) - p := tf.PositionFor(pos, false) // ignore line directives + p := safetoken.Position(tf, pos) // TODO(golang/go#41029): Consider returning line, column instead of line+1, 1 if // the file's last character is not a newline. if offset == tf.Size() { From 224a61b354deb9afa3260a4abc12b59818b1a551 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 29 Dec 2022 10:18:25 -0500 Subject: [PATCH 572/723] gopls/internal/lsp/source: delete Snapshot.WriteEnv method This method is called once immediately after View construction. It used to run 'go version', but with minor tweaks, the View already has this information via wsInfo. This change removes it from the Snapshot interface and eliminates the unnecessary 'go version' subprocess. Change-Id: I86e8dd37a7a237949c05820ca3e4fdb5035f90f7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459782 gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/session.go | 3 ++ gopls/internal/lsp/cache/view.go | 64 ++++++++++++++--------------- gopls/internal/lsp/general.go | 10 ----- gopls/internal/lsp/source/view.go | 3 -- internal/gocommand/version.go | 16 ++++---- internal/gocommand/version_test.go | 2 +- 6 files changed, 44 insertions(+), 54 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index b1367ff5010..5cf991bced9 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -286,6 +286,9 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, // Save one reference in the view. v.releaseSnapshot = v.snapshot.Acquire() + // Record the environment of the newly created view in the log. + event.Log(ctx, viewEnv(v)) + // Initialize the view without blocking. initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx)) v.initCancelFirstAttempt = initCancel diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index ca2639ee4cf..41b5322f826 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -6,10 +6,10 @@ package cache import ( + "bytes" "context" "encoding/json" "fmt" - "io" "io/ioutil" "os" "path" @@ -123,8 +123,10 @@ type workspaceInformation struct { // The Go version in use: X in Go 1.X. goversion int - // The Go version reported by go version command. (e.g. go1.19.1, go1.20-rc.1, go1.21-abcdef01) - goversionString string + // The complete output of the go version command. + // (Call gocommand.ParseGoVersionOutput to extract a version + // substring such as go1.19.1 or go1.20-rc.1, go1.21-abcdef01.) + goversionOutput string // hasGopackagesDriver is true if the user has a value set for the // GOPACKAGESDRIVER environment variable or a gopackagesdriver binary on @@ -346,14 +348,28 @@ func (s *Session) SetViewOptions(ctx context.Context, v *View, options *source.O return newView, err } -func (s *snapshot) WriteEnv(ctx context.Context, w io.Writer) error { - s.view.optionsMu.Lock() - env := s.view.options.EnvSlice() - buildFlags := append([]string{}, s.view.options.BuildFlags...) - s.view.optionsMu.Unlock() +// viewEnv returns a string describing the environment of a newly created view. +func viewEnv(v *View) string { + v.optionsMu.Lock() + env := v.options.EnvSlice() + buildFlags := append([]string{}, v.options.BuildFlags...) + v.optionsMu.Unlock() + + var buf bytes.Buffer + fmt.Fprintf(&buf, `go env for %v +(root %s) +(go version %s) +(valid build configuration = %v) +(build flags: %v) +`, + v.folder.Filename(), + v.rootURI.Filename(), + strings.TrimRight(v.workspaceInformation.goversionOutput, "\n"), + v.snapshot.ValidBuildConfiguration(), + buildFlags) fullEnv := make(map[string]string) - for k, v := range s.view.goEnv { + for k, v := range v.goEnv { fullEnv[k] = v } for _, v := range env { @@ -365,29 +381,11 @@ func (s *snapshot) WriteEnv(ctx context.Context, w io.Writer) error { fullEnv[s[0]] = s[1] } } - goVersion, err := s.view.gocmdRunner.Run(ctx, gocommand.Invocation{ - Verb: "version", - Env: env, - WorkingDir: s.view.rootURI.Filename(), - }) - if err != nil { - return err - } - fmt.Fprintf(w, `go env for %v -(root %s) -(go version %s) -(valid build configuration = %v) -(build flags: %v) -`, - s.view.folder.Filename(), - s.view.rootURI.Filename(), - strings.TrimRight(goVersion.String(), "\n"), - s.ValidBuildConfiguration(), - buildFlags) for k, v := range fullEnv { - fmt.Fprintf(w, "%s=%s\n", k, v) + fmt.Fprintf(&buf, "%s=%s\n", k, v) } - return nil + + return buf.String() } func (s *snapshot) RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error) error { @@ -816,7 +814,7 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, if err != nil { return nil, err } - goversionString, err := gocommand.GoVersionString(ctx, inv, s.gocmdRunner) + goversionOutput, err := gocommand.GoVersionOutput(ctx, inv, s.gocmdRunner) if err != nil { return nil, err } @@ -847,7 +845,7 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, return &workspaceInformation{ hasGopackagesDriver: hasGopackagesDriver, goversion: goversion, - goversionString: goversionString, + goversionOutput: goversionOutput, environmentVariables: envVars, goEnv: env, }, nil @@ -1072,7 +1070,7 @@ func (v *View) GoVersion() int { } func (v *View) GoVersionString() string { - return v.workspaceInformation.goversionString + return gocommand.ParseGoVersionOutput(v.workspaceInformation.goversionOutput) } // Copied from diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index 1c919682382..1d7135e57a0 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -5,7 +5,6 @@ package lsp import ( - "bytes" "context" "encoding/json" "fmt" @@ -339,15 +338,6 @@ func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol } // Inv: release() must be called once. - // Print each view's environment. - var buf bytes.Buffer - if err := snapshot.WriteEnv(ctx, &buf); err != nil { - viewErrors[uri] = err - release() - continue - } - event.Log(ctx, buf.String()) - // Initialize snapshot asynchronously. initialized := make(chan struct{}) nsnapshots.Add(1) diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 144345cfe61..0b73c7494fc 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -74,9 +74,6 @@ type Snapshot interface { // and their GOPATH. ValidBuildConfiguration() bool - // WriteEnv writes the view-specific environment to the io.Writer. - WriteEnv(ctx context.Context, w io.Writer) error - // FindFile returns the FileHandle for the given URI, if it is already // in the given snapshot. FindFile(uri span.URI) VersionedFileHandle diff --git a/internal/gocommand/version.go b/internal/gocommand/version.go index 98e834f74d3..307a76d474a 100644 --- a/internal/gocommand/version.go +++ b/internal/gocommand/version.go @@ -58,22 +58,24 @@ func GoVersion(ctx context.Context, inv Invocation, r *Runner) (int, error) { return 0, fmt.Errorf("no parseable ReleaseTags in %v", tags) } -// GoVersionString reports the go version string as shown in `go version` command output. -// When `go version` outputs in non-standard form, this returns an empty string. -func GoVersionString(ctx context.Context, inv Invocation, r *Runner) (string, error) { +// GoVersionOutput returns the complete output of the go version command. +func GoVersionOutput(ctx context.Context, inv Invocation, r *Runner) (string, error) { inv.Verb = "version" goVersion, err := r.Run(ctx, inv) if err != nil { return "", err } - return parseGoVersionOutput(goVersion.Bytes()), nil + return goVersion.String(), nil } -func parseGoVersionOutput(data []byte) string { +// ParseGoVersionOutput extracts the Go version string +// from the output of the "go version" command. +// Given an unrecognized form, it returns an empty string. +func ParseGoVersionOutput(data string) string { re := regexp.MustCompile(`^go version (go\S+|devel \S+)`) - m := re.FindSubmatch(data) + m := re.FindStringSubmatch(data) if len(m) != 2 { return "" // unrecognized version } - return string(m[1]) + return m[1] } diff --git a/internal/gocommand/version_test.go b/internal/gocommand/version_test.go index d4d02259e08..27016e4c074 100644 --- a/internal/gocommand/version_test.go +++ b/internal/gocommand/version_test.go @@ -23,7 +23,7 @@ func TestParseGoVersionOutput(t *testing.T) { } for i, tt := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { - if got := parseGoVersionOutput([]byte(tt.args)); got != tt.want { + if got := ParseGoVersionOutput(tt.args); got != tt.want { t.Errorf("parseGoVersionOutput() = %v, want %v", got, tt.want) } }) From eac36cb2aca798243ecfedcf127620caf808f702 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 29 Dec 2022 11:18:31 -0500 Subject: [PATCH 573/723] gopls/internal/regtest: port experimental workspace tests to go.work Now that experimental workspace mode is deprecated, re-express tests that depended on this mode in terms of go.work files. For golang/go#55331 Change-Id: I90a876319321fd99982d13d258b7653c337f035a Reviewed-on: https://go-review.googlesource.com/c/tools/+/459785 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan --- .../internal/regtest/modfile/modfile_test.go | 9 +- gopls/internal/regtest/watch/watch_test.go | 19 +-- .../regtest/workspace/workspace_test.go | 121 ++++++++++++++---- 3 files changed, 111 insertions(+), 38 deletions(-) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 3128dc96ff7..08ab21e0f59 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -763,7 +763,15 @@ func main() { } func TestMultiModuleModDiagnostics(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work const mod = ` +-- go.work -- +go 1.18 + +use ( + a + b +) -- a/go.mod -- module moda.com @@ -796,7 +804,6 @@ func main() { ` WithOptions( ProxyFiles(workspaceProxy), - Modes(Experimental), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "is not used"), diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 5906d4c1eac..06f29aaa80e 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -572,16 +572,16 @@ func main() { } // Reproduces golang/go#40340. -func TestSwitchFromGOPATHToModules(t *testing.T) { +func TestSwitchFromGOPATHToModuleMode(t *testing.T) { const files = ` -- foo/blah/blah.go -- package blah const Name = "" --- foo/main.go -- +-- main.go -- package main -import "blah" +import "foo/blah" func main() { _ = blah.Name @@ -590,16 +590,17 @@ func main() { WithOptions( InGOPATH(), EnvVars{"GO111MODULE": "auto"}, - Modes(Experimental), // module is in a subdirectory ).Run(t, files, func(t *testing.T, env *Env) { - env.OpenFile("foo/main.go") - env.Await(env.DiagnosticAtRegexp("foo/main.go", `"blah"`)) + env.OpenFile("main.go") + env.AfterChange( + EmptyDiagnostics("main.go"), + ) if err := env.Sandbox.RunGoCommand(env.Ctx, "foo", "mod", []string{"init", "mod.com"}, true); err != nil { t.Fatal(err) } - env.RegexpReplace("foo/main.go", `"blah"`, `"mod.com/blah"`) - env.Await( - EmptyDiagnostics("foo/main.go"), + env.RegexpReplace("main.go", `"foo/blah"`, `"mod.com/foo/blah"`) + env.AfterChange( + EmptyDiagnostics("main.go"), ) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index ceb710cea09..929d656246c 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -234,6 +234,7 @@ func Hello() {} ` func TestAutomaticWorkspaceModule_Interdependent(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work const multiModule = ` -- moda/a/go.mod -- module a.com @@ -265,9 +266,10 @@ func Hello() int { ` WithOptions( ProxyFiles(workspaceModuleProxy), - Modes(Experimental), ).Run(t, multiModule, func(t *testing.T, env *Env) { - env.Await( + env.RunGoCommand("work", "init") + env.RunGoCommand("work", "use", "-r", ".") + env.AfterChange( env.DiagnosticAtRegexp("moda/a/a.go", "x"), env.DiagnosticAtRegexp("modb/b/b.go", "x"), env.NoDiagnosticAtRegexp("moda/a/a.go", `"b.com/b"`), @@ -275,7 +277,7 @@ func Hello() int { }) } -func TestMultiModuleWithExclude(t *testing.T) { +func TestModuleWithExclude(t *testing.T) { const proxy = ` -- c.com@v1.2.3/go.mod -- module c.com @@ -323,7 +325,6 @@ func main() { ` WithOptions( ProxyFiles(proxy), - Modes(Experimental), ).Run(t, multiModule, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("main.go", "x"), @@ -337,9 +338,15 @@ func main() { // TODO(golang/go#55331): delete this placeholder along with experimental // workspace module. func TestDeleteModule_Interdependent(t *testing.T) { - t.Skip("golang/go#55331: the experimental workspace module is scheduled for deletion") - + testenv.NeedsGo1Point(t, 18) // uses go.work const multiModule = ` +-- go.work -- +go 1.18 + +use ( + moda/a + modb +) -- moda/a/go.mod -- module a.com @@ -370,7 +377,6 @@ func Hello() int { ` WithOptions( ProxyFiles(workspaceModuleProxy), - Modes(Experimental), ).Run(t, multiModule, func(t *testing.T, env *Env) { env.OpenFile("moda/a/a.go") env.Await(env.DoneWithOpen()) @@ -380,13 +386,12 @@ func Hello() int { t.Errorf("expected %s, got %v", want, original) } env.CloseBuffer(original) - env.Await(env.DoneWithClose()) + env.AfterChange() env.RemoveWorkspaceFile("modb/b/b.go") env.RemoveWorkspaceFile("modb/go.mod") - env.Await( - env.DoneWithChangeWatchedFiles(), - ) + env.WriteWorkspaceFile("go.work", "go 1.18\nuse moda/a") + env.AfterChange() got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { @@ -398,7 +403,14 @@ func Hello() int { // Tests that the version of the module used changes after it has been added // to the workspace. func TestCreateModule_Interdependent(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work const multiModule = ` +-- go.work -- +go 1.18 + +use ( + moda/a +) -- moda/a/go.mod -- module a.com @@ -419,7 +431,6 @@ func main() { } ` WithOptions( - Modes(Experimental), ProxyFiles(workspaceModuleProxy), ).Run(t, multiModule, func(t *testing.T, env *Env) { env.OpenFile("moda/a/a.go") @@ -429,6 +440,13 @@ func main() { } env.CloseBuffer(original) env.WriteWorkspaceFiles(map[string]string{ + "go.work": `go 1.18 + +use ( + moda/a + modb +) +`, "modb/go.mod": "module b.com", "modb/b/b.go": `package b @@ -437,12 +455,7 @@ func Hello() int { } `, }) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - env.DiagnosticAtRegexp("modb/b/b.go", "x"), - ), - ) + env.AfterChange(env.DiagnosticAtRegexp("modb/b/b.go", "x")) got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) if want := "modb/b/b.go"; !strings.HasSuffix(got, want) { t.Errorf("expected %s, got %v", want, original) @@ -453,7 +466,17 @@ func Hello() int { // This test confirms that a gopls workspace can recover from initialization // with one invalid module. func TestOneBrokenModule(t *testing.T) { + t.Skip("golang/go#55331: this test is temporarily broken as go.work handling tries to build the workspace module") + + testenv.NeedsGo1Point(t, 18) // uses go.work const multiModule = ` +-- go.work -- +go 1.18 + +use ( + moda/a + modb +) -- moda/a/go.mod -- module a.com @@ -482,7 +505,6 @@ func Hello() int { ` WithOptions( ProxyFiles(workspaceModuleProxy), - Modes(Experimental), ).Run(t, multiModule, func(t *testing.T, env *Env) { env.OpenFile("modb/go.mod") env.Await( @@ -941,8 +963,32 @@ var _ = fmt.Printf }) } -func TestMultiModuleV2(t *testing.T) { +func TestGoWork_V2Module(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work + // When using a go.work, we must have proxy content even if it is replaced. + const proxy = ` +-- b.com/v2@v2.1.9/go.mod -- +module b.com/v2 + +go 1.12 +-- b.com/v2@v2.1.9/b/b.go -- +package b + +func Ciao()() int { + return 0 +} +` + const multiModule = ` +-- go.work -- +go 1.18 + +use ( + moda/a + modb + modb/v2 + modc +) -- moda/a/go.mod -- module a.com @@ -985,14 +1031,20 @@ func main() { var x int } ` + WithOptions( - Modes(Experimental), + ProxyFiles(proxy), ).Run(t, multiModule, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("moda/a/a.go", "x"), - env.DiagnosticAtRegexp("modb/b/b.go", "x"), - env.DiagnosticAtRegexp("modb/v2/b/b.go", "x"), - env.DiagnosticAtRegexp("modc/main.go", "x"), + OnceMet( + InitialWorkspaceLoad, + // TODO(rfindley): assert on the full set of diagnostics here. We + // should ensure that we don't have a diagnostic at b.Hi in a.go. + env.DiagnosticAtRegexp("moda/a/a.go", "x"), + env.DiagnosticAtRegexp("modb/b/b.go", "x"), + env.DiagnosticAtRegexp("modb/v2/b/b.go", "x"), + env.DiagnosticAtRegexp("modc/main.go", "x"), + ), ) }) } @@ -1000,7 +1052,21 @@ func main() { // Confirm that a fix for a tidy module will correct all modules in the // workspace. func TestMultiModule_OneBrokenModule(t *testing.T) { - const mod = ` + // In the earlier 'experimental workspace mode', gopls would aggregate go.sum + // entries for the workspace module, allowing it to correctly associate + // missing go.sum with diagnostics. With go.work files, this doesn't work: + // the go.command will happily write go.work.sum. + t.Skip("golang/go#57509: go.mod diagnostics do not work in go.work mode") + testenv.NeedsGo1Point(t, 18) // uses go.work + const files = ` +-- go.work -- +go 1.18 + +use ( + a + b +) +-- go.work.sum -- -- a/go.mod -- module a.com @@ -1027,8 +1093,7 @@ func main() { ` WithOptions( ProxyFiles(workspaceProxy), - Modes(Experimental), - ).Run(t, mod, func(t *testing.T, env *Env) { + ).Run(t, files, func(t *testing.T, env *Env) { params := &protocol.PublishDiagnosticsParams{} env.OpenFile("b/go.mod") env.Await( From 1a08d01ac985cfa2b25f7217b4e885be47495870 Mon Sep 17 00:00:00 2001 From: Dung Le Date: Tue, 27 Dec 2022 23:06:42 +0700 Subject: [PATCH 574/723] gopls/internal/lsp: update replace directives in go.mod for package renaming Check active go.mod files in the workspace to see if any replace directives need to be fixed if package renaming affects the replaced locations in any go.mod files. For golang/go#56184. Change-Id: I98ea07a602c39168d13f42f1b7a5f6738e194ece Reviewed-on: https://go-review.googlesource.com/c/tools/+/459416 Reviewed-by: Robert Findley Run-TryBot: Dylan Le gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/source/rename.go | 88 ++++++++++++++++++++++ gopls/internal/regtest/misc/rename_test.go | 30 +++++++- 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index fa98f466a58..37fb2d5570c 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -12,11 +12,13 @@ import ( "go/token" "go/types" "path" + "path/filepath" "regexp" "sort" "strconv" "strings" + "golang.org/x/mod/modfile" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" @@ -214,6 +216,92 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, return nil, true, err } + oldBase := filepath.Dir(span.URI.Filename(f.URI())) + newPkgDir := filepath.Join(filepath.Dir(oldBase), newName) + + // TODO: should this operate on all go.mod files, irrespective of whether they are included in the workspace? + // Get all active mod files in the workspace + modFiles := s.ModFiles() + for _, m := range modFiles { + fh, err := s.GetFile(ctx, m) + if err != nil { + return nil, true, err + } + pm, err := s.ParseMod(ctx, fh) + if err != nil { + return nil, true, err + } + + modFileDir := filepath.Dir(pm.URI.Filename()) + affectedReplaces := []*modfile.Replace{} + + // Check if any replace directives need to be fixed + for _, r := range pm.File.Replace { + if !strings.HasPrefix(r.New.Path, "/") && !strings.HasPrefix(r.New.Path, "./") && !strings.HasPrefix(r.New.Path, "../") { + continue + } + + replacedPath := r.New.Path + if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { + replacedPath = filepath.Join(modFileDir, r.New.Path) + } + + // TODO: Is there a risk of converting a '\' delimited replacement to a '/' delimited replacement? + if !strings.HasPrefix(filepath.ToSlash(replacedPath)+"/", filepath.ToSlash(oldBase)+"/") { + continue // not affected by the package renaming + } + + affectedReplaces = append(affectedReplaces, r) + } + + if len(affectedReplaces) == 0 { + continue + } + copied, err := modfile.Parse("", pm.Mapper.Content, nil) + if err != nil { + return nil, true, err + } + + for _, r := range affectedReplaces { + replacedPath := r.New.Path + if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { + replacedPath = filepath.Join(modFileDir, r.New.Path) + } + + suffix := strings.TrimPrefix(replacedPath, string(oldBase)) + + newReplacedPath, err := filepath.Rel(modFileDir, newPkgDir+suffix) + if err != nil { + return nil, true, err + } + + newReplacedPath = filepath.ToSlash(newReplacedPath) + + if !strings.HasPrefix(newReplacedPath, "/") && !strings.HasPrefix(newReplacedPath, "../") { + newReplacedPath = "./" + newReplacedPath + } + + if err := copied.AddReplace(r.Old.Path, "", newReplacedPath, ""); err != nil { + return nil, true, err + } + } + + copied.Cleanup() + newContent, err := copied.Format() + if err != nil { + return nil, true, err + } + + // Calculate the edits to be made due to the change. + diff := s.View().Options().ComputeEdits(string(pm.Mapper.Content), string(newContent)) + modFileEdits, err := ToProtocolEdits(pm.Mapper, diff) + if err != nil { + return nil, true, err + } + + renamingEdits[pm.URI] = append(renamingEdits[pm.URI], modFileEdits...) + } + return renamingEdits, true, nil } diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 82ca1d103bd..cb7708b527d 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -519,8 +519,16 @@ func main() { } func TestRenamePackage_NestedModule(t *testing.T) { - testenv.NeedsGo1Point(t, 17) + testenv.NeedsGo1Point(t, 18) const files = ` +-- go.work -- +go 1.18 +use ( + . + ./foo/bar + ./foo/baz +) + -- go.mod -- module mod.com @@ -530,7 +538,10 @@ require ( mod.com/foo/bar v0.0.0 ) -replace mod.com/foo/bar => ./foo/bar +replace ( + mod.com/foo/bar => ./foo/bar + mod.com/foo/baz => ./foo/baz +) -- foo/foo.go -- package foo @@ -546,7 +557,15 @@ module mod.com/foo/bar -- foo/bar/bar.go -- package bar -const Msg = "Hi" +const Msg = "Hi from package bar" + +-- foo/baz/go.mod -- +module mod.com/foo/baz + +-- foo/baz/baz.go -- +package baz + +const Msg = "Hi from package baz" -- main.go -- package main @@ -554,12 +573,14 @@ package main import ( "fmt" "mod.com/foo/bar" + "mod.com/foo/baz" "mod.com/foo" ) func main() { foo.Bar() fmt.Println(bar.Msg) + fmt.Println(baz.Msg) } ` Run(t, files, func(t *testing.T, env *Env) { @@ -574,6 +595,9 @@ func main() { env.RegexpSearch("main.go", "mod.com/foo/bar") env.RegexpSearch("main.go", "mod.com/foox") env.RegexpSearch("main.go", "foox.Bar()") + + env.RegexpSearch("go.mod", "./foox/bar") + env.RegexpSearch("go.mod", "./foox/baz") }) } From e417ea36bae18ab4aced13c6db31aa330d9f006a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 29 Dec 2022 20:07:44 -0500 Subject: [PATCH 575/723] gopls: remove dead analysis code Change-Id: I1650a5b6b5a754438db2308e8e27da228bba939c Reviewed-on: https://go-review.googlesource.com/c/tools/+/459790 TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Robert Findley Reviewed-by: Alan Donovan Run-TryBot: Robert Findley --- gopls/internal/lsp/cache/analysis.go | 8 -------- gopls/internal/lsp/source/options.go | 25 ------------------------- 2 files changed, 33 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 8185f23bed2..61e868c5e17 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -1130,14 +1130,6 @@ func (act *action) exec() (interface{}, *actionSummary, error) { }, nil } -func factType(fact analysis.Fact) reflect.Type { - t := reflect.TypeOf(fact) - if t.Kind() != reflect.Ptr { - panic(fmt.Sprintf("invalid Fact type: got %T, want pointer", fact)) - } - return t -} - // requiredAnalyzers returns the transitive closure of required analyzers in preorder. func requiredAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer { var result []*analysis.Analyzer diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index cf8c6415176..ba6c3f0da2c 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -1394,31 +1394,6 @@ func (r *OptionResult) setStringSlice(s *[]string) { } } -// EnabledAnalyzers returns all of the analyzers enabled by the options. -func EnabledAnalyzers(options *Options) (analyzers []*Analyzer) { - for _, a := range options.DefaultAnalyzers { - if a.IsEnabled(options) { - analyzers = append(analyzers, a) - } - } - for _, a := range options.TypeErrorAnalyzers { - if a.IsEnabled(options) { - analyzers = append(analyzers, a) - } - } - for _, a := range options.ConvenienceAnalyzers { - if a.IsEnabled(options) { - analyzers = append(analyzers, a) - } - } - for _, a := range options.StaticcheckAnalyzers { - if a.IsEnabled(options) { - analyzers = append(analyzers, a) - } - } - return analyzers -} - func typeErrorAnalyzers() map[string]*Analyzer { return map[string]*Analyzer{ fillreturns.Analyzer.Name: { From b01e7a4e75d3f07db097384f829839c6628a46c8 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 29 Dec 2022 20:24:17 -0500 Subject: [PATCH 576/723] gopls/internal/regtest/watch: don't run TestSwitchFromGOPATHToModuleMode in experimental mode This test no longer works with 'experimentalWorkspaceModule' set. That option is being removed, but in the meantime only run the test in default mode, to unbreak the longtest builders. Fixes golang/go#57521 Change-Id: I95083f577f93116c342787eabf75e36cd05df9f3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459792 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Auto-Submit: Robert Findley --- gopls/internal/regtest/watch/watch_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 06f29aaa80e..64b6ba6a4ed 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -589,6 +589,7 @@ func main() { ` WithOptions( InGOPATH(), + Modes(Default), // golang/go#57521: this test is temporarily failing in 'experimental' mode EnvVars{"GO111MODULE": "auto"}, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") From 33071fbe1aab4e4858458465acd0e80f0d750e62 Mon Sep 17 00:00:00 2001 From: Tim King Date: Fri, 30 Dec 2022 17:02:30 -0800 Subject: [PATCH 577/723] internal/robustio: move robustio Moves gopls/internal/robustio to internal/robustio. Allows usage by packages in x/tools outside of gopls. Change-Id: I71743c3a91458b77f12606b743b5de7e23cc5051 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460116 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Tim King gopls-CI: kokoro --- gopls/internal/lsp/fake/sandbox.go | 2 +- gopls/internal/lsp/fake/workdir.go | 2 +- gopls/internal/lsp/filecache/filecache.go | 2 +- {gopls/internal => internal}/robustio/copyfiles.go | 0 {gopls/internal => internal}/robustio/gopls_windows.go | 0 {gopls/internal => internal}/robustio/robustio.go | 0 {gopls/internal => internal}/robustio/robustio_darwin.go | 0 {gopls/internal => internal}/robustio/robustio_flaky.go | 0 {gopls/internal => internal}/robustio/robustio_other.go | 0 {gopls/internal => internal}/robustio/robustio_posix.go | 0 {gopls/internal => internal}/robustio/robustio_test.go | 2 +- {gopls/internal => internal}/robustio/robustio_windows.go | 0 12 files changed, 4 insertions(+), 4 deletions(-) rename {gopls/internal => internal}/robustio/copyfiles.go (100%) rename {gopls/internal => internal}/robustio/gopls_windows.go (100%) rename {gopls/internal => internal}/robustio/robustio.go (100%) rename {gopls/internal => internal}/robustio/robustio_darwin.go (100%) rename {gopls/internal => internal}/robustio/robustio_flaky.go (100%) rename {gopls/internal => internal}/robustio/robustio_other.go (100%) rename {gopls/internal => internal}/robustio/robustio_posix.go (100%) rename {gopls/internal => internal}/robustio/robustio_test.go (97%) rename {gopls/internal => internal}/robustio/robustio_windows.go (100%) diff --git a/gopls/internal/lsp/fake/sandbox.go b/gopls/internal/lsp/fake/sandbox.go index e4da6289d1c..018bace3b30 100644 --- a/gopls/internal/lsp/fake/sandbox.go +++ b/gopls/internal/lsp/fake/sandbox.go @@ -13,8 +13,8 @@ import ( "path/filepath" "strings" - "golang.org/x/tools/gopls/internal/robustio" "golang.org/x/tools/internal/gocommand" + "golang.org/x/tools/internal/robustio" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" ) diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index 8f0e796b046..19907ada655 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -19,8 +19,8 @@ import ( "time" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/robustio" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/robustio" ) // RelativeTo is a helper for operations relative to a given directory. diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go index 838a99e6202..15160c7c2a6 100644 --- a/gopls/internal/lsp/filecache/filecache.go +++ b/gopls/internal/lsp/filecache/filecache.go @@ -31,7 +31,7 @@ import ( "sync/atomic" "time" - "golang.org/x/tools/gopls/internal/robustio" + "golang.org/x/tools/internal/robustio" ) // Get retrieves from the cache and returns a newly allocated diff --git a/gopls/internal/robustio/copyfiles.go b/internal/robustio/copyfiles.go similarity index 100% rename from gopls/internal/robustio/copyfiles.go rename to internal/robustio/copyfiles.go diff --git a/gopls/internal/robustio/gopls_windows.go b/internal/robustio/gopls_windows.go similarity index 100% rename from gopls/internal/robustio/gopls_windows.go rename to internal/robustio/gopls_windows.go diff --git a/gopls/internal/robustio/robustio.go b/internal/robustio/robustio.go similarity index 100% rename from gopls/internal/robustio/robustio.go rename to internal/robustio/robustio.go diff --git a/gopls/internal/robustio/robustio_darwin.go b/internal/robustio/robustio_darwin.go similarity index 100% rename from gopls/internal/robustio/robustio_darwin.go rename to internal/robustio/robustio_darwin.go diff --git a/gopls/internal/robustio/robustio_flaky.go b/internal/robustio/robustio_flaky.go similarity index 100% rename from gopls/internal/robustio/robustio_flaky.go rename to internal/robustio/robustio_flaky.go diff --git a/gopls/internal/robustio/robustio_other.go b/internal/robustio/robustio_other.go similarity index 100% rename from gopls/internal/robustio/robustio_other.go rename to internal/robustio/robustio_other.go diff --git a/gopls/internal/robustio/robustio_posix.go b/internal/robustio/robustio_posix.go similarity index 100% rename from gopls/internal/robustio/robustio_posix.go rename to internal/robustio/robustio_posix.go diff --git a/gopls/internal/robustio/robustio_test.go b/internal/robustio/robustio_test.go similarity index 97% rename from gopls/internal/robustio/robustio_test.go rename to internal/robustio/robustio_test.go index 31c9bdae6de..5f9fcd9a68f 100644 --- a/gopls/internal/robustio/robustio_test.go +++ b/internal/robustio/robustio_test.go @@ -9,7 +9,7 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/gopls/internal/robustio" + "golang.org/x/tools/internal/robustio" ) func TestFileID(t *testing.T) { diff --git a/gopls/internal/robustio/robustio_windows.go b/internal/robustio/robustio_windows.go similarity index 100% rename from gopls/internal/robustio/robustio_windows.go rename to internal/robustio/robustio_windows.go From 0441b432cade7ecb5b47248667d73be87f8bc1a1 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 29 Dec 2022 20:15:45 -0500 Subject: [PATCH 578/723] gopls/internal/lsp/cache: use specific mutexes for module data View.mu was documented as protecting "most mutable state", but in fact only protected module upgrades and vulns. To be consistent, use specific mutexes for this data. Change-Id: I7454ca7548a1f9c9db786eafbb9cbef3961194dd Reviewed-on: https://go-review.googlesource.com/c/tools/+/459791 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/cache/view.go | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 41b5322f826..14536e1cc6f 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -64,19 +64,16 @@ type View struct { explicitGowork span.URI // explicitGowork: if non-empty, a user-specified go.work location (TODO: deprecate) workspaceInformation // grab-bag of Go environment information (TODO: cleanup) - // mu protects most mutable state of the view. - // - // TODO(rfindley): specify exactly which mutable state is guarded. - mu sync.Mutex - importsState *importsState // moduleUpgrades tracks known upgrades for module paths in each modfile. // Each modfile has a map of module name to upgrade version. - moduleUpgrades map[span.URI]map[string]string + moduleUpgradesMu sync.Mutex + moduleUpgrades map[span.URI]map[string]string // vulns maps each go.mod file's URI to its known vulnerabilities. - vulns map[span.URI]*govulncheck.Result + vulnsMu sync.Mutex + vulns map[span.URI]*govulncheck.Result // filesByURI maps URIs to the canonical URI for the file it denotes. // We also keep a set of candidates for a given basename @@ -997,8 +994,8 @@ func (v *View) IsGoPrivatePath(target string) bool { } func (v *View) ModuleUpgrades(modfile span.URI) map[string]string { - v.mu.Lock() - defer v.mu.Unlock() + v.moduleUpgradesMu.Lock() + defer v.moduleUpgradesMu.Unlock() upgrades := map[string]string{} for mod, ver := range v.moduleUpgrades[modfile] { @@ -1013,8 +1010,8 @@ func (v *View) RegisterModuleUpgrades(modfile span.URI, upgrades map[string]stri return } - v.mu.Lock() - defer v.mu.Unlock() + v.moduleUpgradesMu.Lock() + defer v.moduleUpgradesMu.Unlock() m := v.moduleUpgrades[modfile] if m == nil { @@ -1027,8 +1024,8 @@ func (v *View) RegisterModuleUpgrades(modfile span.URI, upgrades map[string]stri } func (v *View) ClearModuleUpgrades(modfile span.URI) { - v.mu.Lock() - defer v.mu.Unlock() + v.moduleUpgradesMu.Lock() + defer v.moduleUpgradesMu.Unlock() delete(v.moduleUpgrades, modfile) } @@ -1039,8 +1036,8 @@ var timeNow = time.Now // for testing func (v *View) Vulnerabilities(modfiles ...span.URI) map[span.URI]*govulncheck.Result { m := make(map[span.URI]*govulncheck.Result) now := timeNow() - v.mu.Lock() - defer v.mu.Unlock() + v.vulnsMu.Lock() + defer v.vulnsMu.Unlock() if len(modfiles) == 0 { // empty means all modfiles for modfile := range v.vulns { @@ -1059,8 +1056,8 @@ func (v *View) Vulnerabilities(modfiles ...span.URI) map[span.URI]*govulncheck.R } func (v *View) SetVulnerabilities(modfile span.URI, vulns *govulncheck.Result) { - v.mu.Lock() - defer v.mu.Unlock() + v.vulnsMu.Lock() + defer v.vulnsMu.Unlock() v.vulns[modfile] = vulns } From 1e0dff28f4f920fb7a7f132f775bd68e3b1e1fff Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 3 Jan 2023 11:05:58 -0500 Subject: [PATCH 579/723] gopls/internal/regtest: avoid race in TestSwitchFromGOPATHToModuleMode Fix two bugs in TestSwitchFromGOPATHToModuleMode: - `go mod init` was run in the wrong directory. - on-disk change notifications raced with the main.go edit, causing us to only encounter the problem of the previous bullet in rare cases where the on-disk notification won the race I've filed golang/go#57558 to track fixing the fundamental raciness of view changes. Fixes golang/go#57512 Change-Id: I2b21f944377e0ba45ee7be019a28f18a334f3516 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459560 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/regtest/watch/watch_test.go | 9 ++++++++- gopls/internal/regtest/workspace/workspace_test.go | 10 ++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 64b6ba6a4ed..2a1903ff624 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -596,9 +596,16 @@ func main() { env.AfterChange( EmptyDiagnostics("main.go"), ) - if err := env.Sandbox.RunGoCommand(env.Ctx, "foo", "mod", []string{"init", "mod.com"}, true); err != nil { + if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, true); err != nil { t.Fatal(err) } + + // TODO(golang/go#57558, golang/go#57512): file watching is asynchronous, + // and we must wait for the view to be reconstructed before touching + // main.go, so that the new view "knows" about main.go. This is a bug, but + // awaiting the change here avoids it. + env.AfterChange() + env.RegexpReplace("main.go", `"foo/blah"`, `"mod.com/foo/blah"`) env.AfterChange( EmptyDiagnostics("main.go"), diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 929d656246c..81e3531c376 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1199,12 +1199,10 @@ use ( // Removing the go.work file should put us back where we started. env.RemoveWorkspaceFile("go.work") - // TODO(golang/go#57508): because file watching is asynchronous, we must - // ensure that the go.work change is seen before other changes, in order - // for the snapshot to "know" about the orphaned b/main.go below. - // - // This is a bug, plain and simple, but we await here to avoid test flakes - // while the underlying cause is fixed. + // TODO(golang/go#57558, golang/go#57508): file watching is asynchronous, + // and we must wait for the view to be reconstructed before touching + // b/main.go, so that the new view "knows" about b/main.go. This is simply + // a bug, but awaiting the change here avoids it. env.Await(env.DoneWithChangeWatchedFiles()) // TODO(rfindley): fix this bug: reopening b/main.go is necessary here From 7db99dd12661adab9ce92e9b9633b6ef90867fad Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Wed, 4 Jan 2023 16:05:44 +0000 Subject: [PATCH 580/723] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Once this CL is submitted, and post-submit testing succeeds on all first-class ports across all supported Go versions, this repository will be tagged with its next minor version. Change-Id: Id3eae495650a29b882a547a2523b82ce8f187445 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460502 Reviewed-by: Heschi Kreinick Run-TryBot: Gopher Robot Reviewed-by: Dmitri Shuralyov TryBot-Result: Gopher Robot Auto-Submit: Gopher Robot --- go.mod | 4 ++-- go.sum | 12 ++++++------ gopls/go.mod | 4 ++-- gopls/go.sum | 11 ++++++----- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 72bd7ea2ec8..d90447639aa 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.18 // tagx:compat 1.16 require ( github.com/yuin/goldmark v1.4.13 golang.org/x/mod v0.7.0 - golang.org/x/net v0.3.0 - golang.org/x/sys v0.3.0 + golang.org/x/net v0.5.0 + golang.org/x/sys v0.4.0 ) require golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index 7bb017175e0..fbb3d74e321 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -19,15 +19,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/gopls/go.mod b/gopls/go.mod index 42c0e27cd03..2830c165bf3 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -9,8 +9,8 @@ require ( github.com/sergi/go-diff v1.1.0 golang.org/x/mod v0.7.0 golang.org/x/sync v0.1.0 - golang.org/x/sys v0.3.0 - golang.org/x/text v0.5.0 + golang.org/x/sys v0.4.0 + golang.org/x/text v0.6.0 golang.org/x/tools v0.4.0 golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a gopkg.in/yaml.v3 v3.0.1 diff --git a/gopls/go.sum b/gopls/go.sum index 984d8fa503c..6f6866773d6 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -55,7 +55,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -66,13 +66,14 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a h1:KWIh6uTTw7r3PEz1N1OIEM8pr5bf1uP1n6JL5Ml56X8= golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a/go.mod h1:54iI0rrZVM8VdIvTrT/sdlVfMUJWOgvTRQN24CEtZk0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From a4455febdef318395153612ab0489e96418ec51b Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 3 Jan 2023 15:48:03 -0800 Subject: [PATCH 581/723] go/callgraph: adds benchmarks comparing algorithms Change-Id: Iff83e5f5790c9d92d6641d98380e1403855c27ab Reviewed-on: https://go-review.googlesource.com/c/tools/+/460456 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Tim King Reviewed-by: Zvonimir Pavlinovic --- go/callgraph/callgraph_test.go | 233 +++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 go/callgraph/callgraph_test.go diff --git a/go/callgraph/callgraph_test.go b/go/callgraph/callgraph_test.go new file mode 100644 index 00000000000..907b317a0c2 --- /dev/null +++ b/go/callgraph/callgraph_test.go @@ -0,0 +1,233 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package callgraph_test + +import ( + "log" + "sync" + "testing" + + "golang.org/x/tools/go/callgraph" + "golang.org/x/tools/go/callgraph/cha" + "golang.org/x/tools/go/callgraph/rta" + "golang.org/x/tools/go/callgraph/static" + "golang.org/x/tools/go/callgraph/vta" + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/pointer" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +// Benchmarks comparing different callgraph algorithms implemented in +// x/tools/go/callgraph. Comparison is on both speed, memory and precision. +// Fewer edges and fewer reachable nodes implies a more precise result. +// Comparison is done on a hello world http server using net/http. +// +// Current results were on an i7 macbook on go version devel go1.20-2730. +// Number of nodes, edges, and reachable function are expected to vary between +// go versions. Timing results are expected to vary between machines. +// +// BenchmarkStatic-12 53 ms/op 6 MB/op 12113 nodes 37355 edges 1522 reachable +// BenchmarkCHA-12 86 ms/op 16 MB/op 12113 nodes 131717 edges 7640 reachable +// BenchmarkRTA-12 110 ms/op 12 MB/op 6566 nodes 42291 edges 5099 reachable +// BenchmarkPTA-12 1427 ms/op 600 MB/op 8714 nodes 28244 edges 4184 reachable +// BenchmarkVTA-12 603 ms/op 87 MB/op 12112 nodes 44857 edges 4918 reachable +// BenchmarkVTA2-12 774 ms/op 115 MB/op 4918 nodes 20247 edges 3841 reachable +// BenchmarkVTA3-12 928 ms/op 134 MB/op 3841 nodes 16502 edges 3542 reachable +// BenchmarkVTAAlt-12 395 ms/op 61 MB/op 7640 nodes 29629 edges 4257 reachable +// BenchmarkVTAAlt2-12 556 ms/op 82 MB/op 4257 nodes 18057 edges 3586 reachable +// +// Note: +// * Static is unsound and may miss real edges. +// * RTA starts from a main function and only includes reachable functions. +// * CHA starts from all functions. +// * VTA, VTA2, and VTA3 are starting from all functions and the CHA callgraph. +// VTA2 and VTA3 are the result of re-applying VTA to the functions reachable +// from main() via the callgraph of the previous stage. +// * VTAAlt, and VTAAlt2 start from the functions reachable from main via the +// CHA callgraph. +// * All algorithms are unsound w.r.t. reflection. + +const httpEx = `package main + +import ( + "fmt" + "net/http" +) + +func hello(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "hello world\n") +} + +func main() { + http.HandleFunc("/hello", hello) + http.ListenAndServe(":8090", nil) +} +` + +var ( + once sync.Once + prog *ssa.Program + main *ssa.Function +) + +func example() (*ssa.Program, *ssa.Function) { + once.Do(func() { + var conf loader.Config + f, err := conf.ParseFile("", httpEx) + if err != nil { + log.Fatal(err) + } + conf.CreateFromFiles(f.Name.Name, f) + + lprog, err := conf.Load() + if err != nil { + log.Fatalf("test 'package %s': Load: %s", f.Name.Name, err) + } + prog = ssautil.CreateProgram(lprog, ssa.InstantiateGenerics) + prog.Build() + + main = prog.Package(lprog.Created[0].Pkg).Members["main"].(*ssa.Function) + }) + return prog, main +} + +var stats bool = false // print stats? + +func logStats(b *testing.B, name string, cg *callgraph.Graph, main *ssa.Function) { + if stats { + e := 0 + for _, n := range cg.Nodes { + e += len(n.Out) + } + r := len(reaches(cg, main)) + b.Logf("%s:\t%d nodes\t%d edges\t%d reachable", name, len(cg.Nodes), e, r) + } +} + +func BenchmarkStatic(b *testing.B) { + b.StopTimer() + prog, main := example() + b.StartTimer() + + for i := 0; i < b.N; i++ { + cg := static.CallGraph(prog) + logStats(b, "static", cg, main) + } +} + +func BenchmarkCHA(b *testing.B) { + b.StopTimer() + prog, main := example() + b.StartTimer() + + for i := 0; i < b.N; i++ { + cg := cha.CallGraph(prog) + logStats(b, "cha", cg, main) + } +} + +func BenchmarkRTA(b *testing.B) { + b.StopTimer() + _, main := example() + b.StartTimer() + + for i := 0; i < b.N; i++ { + res := rta.Analyze([]*ssa.Function{main}, true) + cg := res.CallGraph + logStats(b, "rta", cg, main) + } +} + +func BenchmarkPTA(b *testing.B) { + b.StopTimer() + _, main := example() + b.StartTimer() + + for i := 0; i < b.N; i++ { + config := &pointer.Config{Mains: []*ssa.Package{main.Pkg}, BuildCallGraph: true} + res, err := pointer.Analyze(config) + if err != nil { + b.Fatal(err) + } + logStats(b, "pta", res.CallGraph, main) + } +} + +func BenchmarkVTA(b *testing.B) { + b.StopTimer() + prog, main := example() + b.StartTimer() + + for i := 0; i < b.N; i++ { + cg := vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) + logStats(b, "vta", cg, main) + } +} + +func BenchmarkVTA2(b *testing.B) { + b.StopTimer() + prog, main := example() + b.StartTimer() + + for i := 0; i < b.N; i++ { + vta1 := vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) + cg := vta.CallGraph(reaches(vta1, main), vta1) + logStats(b, "vta2", cg, main) + } +} + +func BenchmarkVTA3(b *testing.B) { + b.StopTimer() + prog, main := example() + b.StartTimer() + + for i := 0; i < b.N; i++ { + vta1 := vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) + vta2 := vta.CallGraph(reaches(vta1, main), vta1) + cg := vta.CallGraph(reaches(vta2, main), vta2) + logStats(b, "vta3", cg, main) + } +} + +func BenchmarkVTAAlt(b *testing.B) { + b.StopTimer() + prog, main := example() + b.StartTimer() + + for i := 0; i < b.N; i++ { + cha := cha.CallGraph(prog) + cg := vta.CallGraph(reaches(cha, main), cha) // start from only functions reachable by CHA. + logStats(b, "vta-alt", cg, main) + } +} + +func BenchmarkVTAAlt2(b *testing.B) { + b.StopTimer() + prog, main := example() + b.StartTimer() + + for i := 0; i < b.N; i++ { + cha := cha.CallGraph(prog) + vta1 := vta.CallGraph(reaches(cha, main), cha) + cg := vta.CallGraph(reaches(vta1, main), vta1) + logStats(b, "vta-alt2", cg, main) + } +} + +// reaches returns the set of functions forward reachable from f in g. +func reaches(g *callgraph.Graph, f *ssa.Function) map[*ssa.Function]bool { + seen := make(map[*ssa.Function]bool) + var visit func(n *callgraph.Node) + visit = func(n *callgraph.Node) { + if !seen[n.Func] { + seen[n.Func] = true + for _, e := range n.Out { + visit(e.Callee) + } + } + } + visit(g.Nodes[f]) + return seen +} From 02bea03babeebb49198bfb45f2fed12a81c2e642 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 4 Jan 2023 18:52:54 -0500 Subject: [PATCH 582/723] gopls/internal/lsp/protocol: simplify ColumnMapper This change decouples ColumnMapper from go/token. Its TokFile field is now unexported and fully encapsulated and will be removed in a follow-up; it serves only as a line-number table. ColumnMapper now provides only mapping between byte offsets and columns, in three different units (UTF-8, UTF-16, and runes). Three operations that require both a Mapper and a token.File--and require then to be consistent with each other--have been moved to ParsedGoFile: (Pos, PosRange, and RangeToSpanRange). This is another step to keeping the use of token.Pos close to its token.File or FileSet, and using byte offsets and ColumnMappers more broadly. MappedRange now holds a ParsedGoFile and (internally) a start/end Pos pair, making it self-contained for all conversions. (The File field is unfortunately public for now due to one tricky use; fixing it would have expanded this already large CL.) I'm not sure whether MappedRange carries its weight; I think it might be clearer for all users to simply expand it out (i.e. hold a ColumnMapper and two byte offsets), making one less creature in the zoo. Numerous calls to NewMappedRange followed by .Range() have been reduced to pgf.PosRange(). Also: - New ColumnMapper methods: OffsetSpan OffsetPoint - safetoken.Offsets(start, end) is the plural of Offset(pos). - span.ToPosition renamed span.OffsetToLineCol8. - span.NewTokenFile inlined into sole caller. - avoid embedding of MappedRange, as it makes the references hard to see. (Embedded fields are both a def and a ref but gopls cross-references is confused by that.) - findLinksInString uses offsets now. Change-Id: I2c775e181e456604e2ce977d618b0f1ec8e76903 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460615 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/cache/analysis.go | 2 +- gopls/internal/lsp/cache/check.go | 2 +- gopls/internal/lsp/cache/errors.go | 17 ++- gopls/internal/lsp/cache/load.go | 5 +- gopls/internal/lsp/cache/mod.go | 5 +- gopls/internal/lsp/cache/mod_tidy.go | 28 +--- gopls/internal/lsp/cache/parse.go | 37 ++---- gopls/internal/lsp/cmd/cmd.go | 16 +-- gopls/internal/lsp/code_action.go | 2 +- gopls/internal/lsp/completion.go | 2 + gopls/internal/lsp/definition.go | 4 +- gopls/internal/lsp/diagnostics.go | 2 +- gopls/internal/lsp/folding_range.go | 2 +- gopls/internal/lsp/link.go | 56 ++++---- gopls/internal/lsp/protocol/span.go | 120 ++++++++++-------- gopls/internal/lsp/references.go | 4 +- gopls/internal/lsp/safetoken/safetoken.go | 13 ++ gopls/internal/lsp/selection_range.go | 4 +- gopls/internal/lsp/semantic.go | 3 +- gopls/internal/lsp/source/call_hierarchy.go | 12 +- gopls/internal/lsp/source/code_lens.go | 10 +- .../lsp/source/completion/completion.go | 6 +- .../internal/lsp/source/completion/package.go | 2 +- gopls/internal/lsp/source/completion/util.go | 8 +- gopls/internal/lsp/source/extract.go | 18 +-- gopls/internal/lsp/source/fix.go | 12 +- gopls/internal/lsp/source/folding_range.go | 34 ++--- gopls/internal/lsp/source/format.go | 4 +- gopls/internal/lsp/source/highlight.go | 2 +- gopls/internal/lsp/source/hover.go | 54 ++++---- gopls/internal/lsp/source/identifier.go | 37 ++---- gopls/internal/lsp/source/inlay_hint.go | 2 +- gopls/internal/lsp/source/references.go | 18 +-- gopls/internal/lsp/source/rename.go | 19 ++- gopls/internal/lsp/source/signature_help.go | 2 +- gopls/internal/lsp/source/source_test.go | 10 +- gopls/internal/lsp/source/stub.go | 7 +- gopls/internal/lsp/source/util.go | 66 ++++------ gopls/internal/lsp/source/view.go | 28 ++++ gopls/internal/lsp/work/completion.go | 10 +- gopls/internal/lsp/work/hover.go | 11 +- gopls/internal/regtest/misc/failures_test.go | 2 +- gopls/internal/span/span.go | 10 +- gopls/internal/span/token.go | 22 ++-- 44 files changed, 347 insertions(+), 383 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 61e868c5e17..8bbe2ed213e 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -1024,7 +1024,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) { if end == token.NoPos { end = start } - rng, err := p.Mapper.PosRange(start, end) + rng, err := p.PosRange(start, end) if err != nil { return protocol.Location{}, err } diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index cfac5ffb107..c52a2f8f56c 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -683,7 +683,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost } for _, imp := range allImports[item] { - rng, err := source.NewMappedRange(imp.cgf.Mapper, imp.imp.Pos(), imp.imp.End()).Range() + rng, err := imp.cgf.PosRange(imp.imp.Pos(), imp.imp.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 7ca4f078eff..67771931bbf 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -86,17 +86,12 @@ func parseErrorDiagnostics(snapshot *snapshot, pkg *pkg, errList scanner.ErrorLi if err != nil { return nil, err } - pos := pgf.Tok.Pos(e.Pos.Offset) - spn, err := span.NewRange(pgf.Tok, pos, pos).Span() - if err != nil { - return nil, err - } - rng, err := spanToRange(pkg, spn) + rng, err := pgf.Mapper.OffsetRange(e.Pos.Offset, e.Pos.Offset) if err != nil { return nil, err } return []*source.Diagnostic{{ - URI: spn.URI(), + URI: pgf.URI, Range: rng, Severity: protocol.SeverityError, Source: source.ParseError, @@ -327,7 +322,7 @@ func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Sp if !end.IsValid() || end == start { end = analysisinternal.TypeErrorEndPos(fset, pgf.Src, start) } - spn, err := span.FileSpan(pgf.Mapper.TokFile, start, end) + spn, err := span.FileSpan(pgf.Tok, start, end) if err != nil { return 0, span.Span{}, err } @@ -379,7 +374,11 @@ func parseGoListImportCycleError(snapshot *snapshot, e packages.Error, pkg *pkg) // Search file imports for the import that is causing the import cycle. for _, imp := range cgf.File.Imports { if imp.Path.Value == circImp { - spn, err := span.NewRange(cgf.Tok, imp.Pos(), imp.End()).Span() + start, end, err := safetoken.Offsets(cgf.Tok, imp.Pos(), imp.End()) + if err != nil { + return msg, span.Span{}, false + } + spn, err := cgf.Mapper.OffsetSpan(start, end) if err != nil { return msg, span.Span{}, false } diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index f79109a31d1..b072aafd0a7 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -391,10 +391,7 @@ func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, fi if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil { // Check that we have a valid `package foo` range to use for positioning the error. if pgf.File.Package.IsValid() && pgf.File.Name != nil && pgf.File.Name.End().IsValid() { - pkgDecl := span.NewRange(pgf.Tok, pgf.File.Package, pgf.File.Name.End()) - if spn, err := pkgDecl.Span(); err == nil { - rng, _ = pgf.Mapper.Range(spn) - } + rng, _ = pgf.PosRange(pgf.File.Package, pgf.File.Name.End()) } } case source.Mod: diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index 757bb5e8fca..a3d207d6005 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -402,11 +402,12 @@ func (s *snapshot) matchErrorToModule(ctx context.Context, pm *source.ParsedModu if pm.File.Module == nil { return span.New(pm.URI, span.NewPoint(1, 1, 0), span.Point{}), false, nil } - spn, err := spanFromPositions(pm.Mapper, pm.File.Module.Syntax.Start, pm.File.Module.Syntax.End) + syntax := pm.File.Module.Syntax + spn, err := pm.Mapper.OffsetSpan(syntax.Start.Byte, syntax.End.Byte) return spn, false, err } - spn, err := spanFromPositions(pm.Mapper, reference.Start, reference.End) + spn, err := pm.Mapper.OffsetSpan(reference.Start.Byte, reference.End.Byte) return spn, true, err } diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index fa30df18e36..c9c02ca818e 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "go/ast" - "go/token" "io/ioutil" "os" "path/filepath" @@ -263,7 +262,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.Pars if !ok { return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path) } - srcErr, err := missingModuleForImport(pgf.Tok, m, imp, req, fixes) + srcErr, err := missingModuleForImport(pgf, imp, req, fixes) if err != nil { return nil, err } @@ -423,16 +422,16 @@ func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdi // missingModuleForImport creates an error for a given import path that comes // from a missing module. -func missingModuleForImport(file *token.File, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) { +func missingModuleForImport(pgf *source.ParsedGoFile, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) { if req.Syntax == nil { return nil, fmt.Errorf("no syntax for %v", req) } - rng, err := m.PosRange(imp.Path.Pos(), imp.Path.End()) + rng, err := pgf.PosRange(imp.Path.Pos(), imp.Path.End()) if err != nil { return nil, err } return &source.Diagnostic{ - URI: m.URI, + URI: pgf.URI, Range: rng, Severity: protocol.SeverityError, Source: source.ModTidyError, @@ -441,25 +440,6 @@ func missingModuleForImport(file *token.File, m *protocol.ColumnMapper, imp *ast }, nil } -func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) { - toPoint := func(offset int) (span.Point, error) { - l, c, err := span.ToPosition(m.TokFile, offset) - if err != nil { - return span.Point{}, err - } - return span.NewPoint(l, c, offset), nil - } - start, err := toPoint(s.Byte) - if err != nil { - return span.Span{}, err - } - end, err := toPoint(e.Byte) - if err != nil { - return span.Span{}, err - } - return span.New(m.URI, start, end), nil -} - // parseImports parses the headers of the specified files and returns // the set of strings that appear in import declarations within // GoFiles. Errors are ignored. diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index 83f18dabee4..7451cc3a652 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -202,17 +202,13 @@ func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, } return &source.ParsedGoFile{ - URI: fh.URI(), - Mode: mode, - Src: src, - Fixed: fixed, - File: file, - Tok: tok, - Mapper: &protocol.ColumnMapper{ - URI: fh.URI(), - TokFile: tok, - Content: src, - }, + URI: fh.URI(), + Mode: mode, + Src: src, + Fixed: fixed, + File: file, + Tok: tok, + Mapper: protocol.NewColumnMapper(fh.URI(), src), ParseErr: parseErr, }, nil } @@ -900,11 +896,7 @@ func fixInitStmt(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) } // Try to extract a statement from the BadExpr. - start, err := safetoken.Offset(tok, bad.Pos()) - if err != nil { - return - } - end, err := safetoken.Offset(tok, bad.End()-1) + start, end, err := safetoken.Offsets(tok, bad.Pos(), bad.End()-1) if err != nil { return } @@ -989,11 +981,7 @@ func fixArrayType(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte // Avoid doing tok.Offset(to) since that panics if badExpr ends at EOF. // It also panics if the position is not in the range of the file, and // badExprs may not necessarily have good positions, so check first. - fromOffset, err := safetoken.Offset(tok, from) - if err != nil { - return false - } - toOffset, err := safetoken.Offset(tok, to-1) + fromOffset, toOffset, err := safetoken.Offsets(tok, from, to-1) if err != nil { return false } @@ -1150,18 +1138,13 @@ FindTo: } } - fromOffset, err := safetoken.Offset(tok, from) + fromOffset, toOffset, err := safetoken.Offsets(tok, from, to) if err != nil { return false } if !from.IsValid() || fromOffset >= len(src) { return false } - - toOffset, err := safetoken.Offset(tok, to) - if err != nil { - return false - } if !to.IsValid() || toOffset >= len(src) { return false } diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index 3aa74d067ae..c2219133041 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -11,7 +11,6 @@ import ( "context" "flag" "fmt" - "go/token" "io/ioutil" "log" "os" @@ -385,8 +384,7 @@ type connection struct { type cmdClient struct { protocol.Server - app *Application - fset *token.FileSet + app *Application diagnosticsMu sync.Mutex diagnosticsDone chan struct{} @@ -407,7 +405,6 @@ func newConnection(app *Application) *connection { return &connection{ Client: &cmdClient{ app: app, - fset: token.NewFileSet(), files: make(map[span.URI]*cmdFile), }, } @@ -541,19 +538,12 @@ func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile { c.files[uri] = file } if file.mapper == nil { - fname := uri.Filename() - content, err := ioutil.ReadFile(fname) + content, err := ioutil.ReadFile(uri.Filename()) if err != nil { file.err = fmt.Errorf("getFile: %v: %v", uri, err) return file } - f := c.fset.AddFile(fname, -1, len(content)) - f.SetLinesForContent(content) - file.mapper = &protocol.ColumnMapper{ - URI: uri, - TokFile: f, - Content: content, - } + file.mapper = protocol.NewColumnMapper(uri, content) } return file } diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 0767d439b4c..5e0a778d5b7 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -316,7 +316,7 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI if err != nil { return nil, fmt.Errorf("getting file for Identifier: %w", err) } - srng, err := pgf.Mapper.RangeToSpanRange(rng) + srng, err := pgf.RangeToSpanRange(rng) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/completion.go b/gopls/internal/lsp/completion.go index c967c1faa22..e443a3c5749 100644 --- a/gopls/internal/lsp/completion.go +++ b/gopls/internal/lsp/completion.go @@ -60,6 +60,8 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara // internal/span, as the latter treats end of file as the beginning of the // next line, even when it's not newline-terminated. See golang/go#41029 for // more details. + // TODO(adonovan): make completion retain the pgf.Mapper + // so we can convert to rng without reading. src, err := fh.Read() if err != nil { return nil, err diff --git a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go index d2ad4742b97..d83512a93e7 100644 --- a/gopls/internal/lsp/definition.go +++ b/gopls/internal/lsp/definition.go @@ -58,13 +58,13 @@ func (s *Server) typeDefinition(ctx context.Context, params *protocol.TypeDefini if ident.Type.Object == nil { return nil, fmt.Errorf("no type definition for %s", ident.Name) } - identRange, err := ident.Type.Range() + identRange, err := ident.Type.MappedRange.Range() if err != nil { return nil, err } return []protocol.Location{ { - URI: protocol.URIFromSpanURI(ident.Type.URI()), + URI: protocol.URIFromSpanURI(ident.Type.MappedRange.URI()), Range: identRange, }, }, nil diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 863c1428528..6ec7a083832 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -577,7 +577,7 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps if !pgf.File.Name.Pos().IsValid() { return nil } - rng, err := pgf.Mapper.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) + rng, err := pgf.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) if err != nil { return nil } diff --git a/gopls/internal/lsp/folding_range.go b/gopls/internal/lsp/folding_range.go index 4a2d828e995..86469d37bd9 100644 --- a/gopls/internal/lsp/folding_range.go +++ b/gopls/internal/lsp/folding_range.go @@ -28,7 +28,7 @@ func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRange func toProtocolFoldingRanges(ranges []*source.FoldingRangeInfo) ([]protocol.FoldingRange, error) { result := make([]protocol.FoldingRange, 0, len(ranges)) for _, info := range ranges { - rng, err := info.Range() + rng, err := info.MappedRange.Range() if err != nil { return nil, err } diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go index 011f0e44163..e7bdb604662 100644 --- a/gopls/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -17,8 +17,8 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" ) @@ -48,7 +48,6 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl if err != nil { return nil, err } - tokFile := pm.Mapper.TokFile var links []protocol.DocumentLink for _, req := range pm.File.Require { @@ -60,16 +59,15 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl continue } dep := []byte(req.Mod.Path) - s, e := req.Syntax.Start.Byte, req.Syntax.End.Byte - i := bytes.Index(pm.Mapper.Content[s:e], dep) + start, end := req.Syntax.Start.Byte, req.Syntax.End.Byte + i := bytes.Index(pm.Mapper.Content[start:end], dep) if i == -1 { continue } // Shift the start position to the location of the // dependency within the require statement. - start, end := tokFile.Pos(s+i), tokFile.Pos(s+i+len(dep)) target := source.BuildLink(snapshot.View().Options().LinkTarget, "mod/"+req.Mod.String(), "") - l, err := toProtocolLink(tokFile, pm.Mapper, target, start, end) + l, err := toProtocolLink(pm.Mapper, target, start+i, start+i+len(dep)) if err != nil { return nil, err } @@ -89,8 +87,7 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl } for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} { for _, comment := range section { - start := tokFile.Pos(comment.Start.Byte) - l, err := findLinksInString(urlRegexp, comment.Token, start, tokFile, pm.Mapper) + l, err := findLinksInString(urlRegexp, comment.Token, comment.Start.Byte, pm.Mapper) if err != nil { return nil, err } @@ -145,11 +142,13 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle urlPath = strings.Replace(urlPath, m.Module.Path, m.Module.Path+"@"+m.Module.Version, 1) } - // Account for the quotation marks in the positions. - start := imp.Path.Pos() + 1 - end := imp.Path.End() - 1 + start, end, err := safetoken.Offsets(pgf.Tok, imp.Path.Pos(), imp.Path.End()) + if err != nil { + return nil, err + } targetURL := source.BuildLink(view.Options().LinkTarget, urlPath, "") - l, err := toProtocolLink(pgf.Tok, pgf.Mapper, targetURL, start, end) + // Account for the quotation marks in the positions. + l, err := toProtocolLink(pgf.Mapper, targetURL, start+len(`"`), end-len(`"`)) if err != nil { return nil, err } @@ -173,7 +172,11 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle return true }) for _, s := range str { - l, err := findLinksInString(urlRegexp, s.Value, s.Pos(), pgf.Tok, pgf.Mapper) + strOffset, err := safetoken.Offset(pgf.Tok, s.Pos()) + if err != nil { + return nil, err + } + l, err := findLinksInString(urlRegexp, s.Value, strOffset, pgf.Mapper) if err != nil { return nil, err } @@ -183,7 +186,11 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle // Gather links found in comments. for _, commentGroup := range pgf.File.Comments { for _, comment := range commentGroup.List { - l, err := findLinksInString(urlRegexp, comment.Text, comment.Pos(), pgf.Tok, pgf.Mapper) + commentOffset, err := safetoken.Offset(pgf.Tok, comment.Pos()) + if err != nil { + return nil, err + } + l, err := findLinksInString(urlRegexp, comment.Text, commentOffset, pgf.Mapper) if err != nil { return nil, err } @@ -203,13 +210,11 @@ var acceptedSchemes = map[string]bool{ } // urlRegexp is the user-supplied regular expression to match URL. -// tokFile may be a throwaway File for non-Go files. -func findLinksInString(urlRegexp *regexp.Regexp, src string, pos token.Pos, tokFile *token.File, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) { +// srcOffset is the start offset of 'src' within m's file. +func findLinksInString(urlRegexp *regexp.Regexp, src string, srcOffset int, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) { var links []protocol.DocumentLink for _, index := range urlRegexp.FindAllIndex([]byte(src), -1) { start, end := index[0], index[1] - startPos := token.Pos(int(pos) + start) - endPos := token.Pos(int(pos) + end) link := src[start:end] linkURL, err := url.Parse(link) // Fallback: Linkify IP addresses as suggested in golang/go#18824. @@ -227,7 +232,8 @@ func findLinksInString(urlRegexp *regexp.Regexp, src string, pos token.Pos, tokF if !acceptedSchemes[linkURL.Scheme] { continue } - l, err := toProtocolLink(tokFile, m, linkURL.String(), startPos, endPos) + + l, err := toProtocolLink(m, linkURL.String(), srcOffset+start, srcOffset+end) if err != nil { return nil, err } @@ -237,15 +243,13 @@ func findLinksInString(urlRegexp *regexp.Regexp, src string, pos token.Pos, tokF r := getIssueRegexp() for _, index := range r.FindAllIndex([]byte(src), -1) { start, end := index[0], index[1] - startPos := token.Pos(int(pos) + start) - endPos := token.Pos(int(pos) + end) matches := r.FindStringSubmatch(src) if len(matches) < 4 { continue } org, repo, number := matches[1], matches[2], matches[3] targetURL := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) - l, err := toProtocolLink(tokFile, m, targetURL, startPos, endPos) + l, err := toProtocolLink(m, targetURL, srcOffset+start, srcOffset+end) if err != nil { return nil, err } @@ -266,12 +270,8 @@ var ( issueRegexp *regexp.Regexp ) -func toProtocolLink(tokFile *token.File, m *protocol.ColumnMapper, targetURL string, start, end token.Pos) (protocol.DocumentLink, error) { - spn, err := span.NewRange(tokFile, start, end).Span() - if err != nil { - return protocol.DocumentLink{}, err - } - rng, err := m.Range(spn) +func toProtocolLink(m *protocol.ColumnMapper, targetURL string, start, end int) (protocol.DocumentLink, error) { + rng, err := m.OffsetRange(start, end) if err != nil { return protocol.DocumentLink{}, err } diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 78e180c71a0..160b925895a 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -8,12 +8,12 @@ // // Imports: source --> lsppos --> protocol --> span --> token // -// source.MappedRange = (span.Range, protocol.ColumnMapper) +// source.MappedRange = (*ParsedGoFile, start/end token.Pos) // // lsppos.TokenMapper = (token.File, lsppos.Mapper) // lsppos.Mapper = (line offset table, content) // -// protocol.ColumnMapper = (URI, token.File, content) +// protocol.ColumnMapper = (URI, Content). Does all offset <=> column conversions. // protocol.Location = (URI, protocol.Range) // protocol.Range = (start, end Position) // protocol.Position = (line, char uint32) 0-based UTF-16 @@ -24,12 +24,19 @@ // // token.Pos // token.FileSet +// token.File // offset int // -// TODO(adonovan): simplify this picture. Eliminate the optionality of -// span.{Span,Point}'s position and offset fields: work internally in -// terms of offsets (like span.Range), and require a mapper to convert -// them to protocol (UTF-16) line/col form. +// TODO(adonovan): simplify this picture: +// - Eliminate the optionality of span.{Span,Point}'s position and offset fields? +// - Move span.Range to package safetoken. Can we eliminate it? +// Without a ColumnMapper it's not really self-contained. +// It is mostly used by completion. Given access to complete.mapper, +// it could use a pair byte offsets instead. +// - Merge lsppos.Mapper and protocol.ColumnMapper. +// - Replace all uses of lsppos.TokenMapper by the underlying ParsedGoFile, +// which carries a token.File and a ColumnMapper. +// - Then delete lsppos package. package protocol @@ -41,35 +48,52 @@ import ( "strings" "unicode/utf8" - "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" ) -// A ColumnMapper maps between UTF-8 oriented positions (e.g. token.Pos, -// span.Span) and the UTF-16 oriented positions used by the LSP. +// A ColumnMapper wraps the content of a file and provides mapping +// from byte offsets to and from other notations of position: +// +// - (line, col8) pairs, where col8 is a 1-based UTF-8 column number (bytes), +// as used by go/token; +// +// - (line, col16) pairs, where col16 is a 1-based UTF-16 column number, +// as used by the LSP protocol; +// +// - (line, colRune) pairs, where colRune is a rune index, as used by ParseWork. +// +// This type does not depend on or use go/token-based representations. +// Use safetoken to map between token.Pos <=> byte offsets. type ColumnMapper struct { URI span.URI - TokFile *token.File Content []byte - // File content is only really needed for UTF-16 column - // computation, which could be be achieved more compactly. - // For example, one could record only the lines for which - // UTF-16 columns differ from the UTF-8 ones, or only the - // indices of the non-ASCII characters. + // This field provides a line-number table, nothing more. + // The public API of ColumnMapper doesn't mention go/token, + // nor should it. It need not be consistent with any + // other token.File or FileSet. // - // TODO(adonovan): consider not retaining the entire file - // content, or at least not exposing the fact that we - // currently retain it. + // TODO(adonovan): eliminate this field in a follow-up + // by inlining the line-number table. Then merge this + // type with the nearly identical lsspos.Mapper. + // + // TODO(adonovan): opt: quick experiments suggest that + // ColumnMappers are created for thousands of files but the + // m.lines field is accessed only for a small handful. + // So it would make sense to allocate it lazily. + lines *token.File } // NewColumnMapper creates a new column mapper for the given uri and content. func NewColumnMapper(uri span.URI, content []byte) *ColumnMapper { - tf := span.NewTokenFile(uri.Filename(), content) + fset := token.NewFileSet() + tf := fset.AddFile(uri.Filename(), -1, len(content)) + tf.SetLinesForContent(content) + return &ColumnMapper{ URI: uri, - TokFile: tf, + lines: tf, Content: content, } } @@ -105,7 +129,7 @@ func (m *ColumnMapper) Range(s span.Span) (Range, error) { return Range{}, bug.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI()) } - s, err := s.WithOffset(m.TokFile) + s, err := s.WithOffset(m.lines) if err != nil { return Range{}, err } @@ -135,17 +159,20 @@ func (m *ColumnMapper) OffsetRange(start, end int) (Range, error) { return Range{Start: startPosition, End: endPosition}, nil } -// PosRange returns a protocol Range for the token.Pos interval Content[start:end]. -func (m *ColumnMapper) PosRange(start, end token.Pos) (Range, error) { - startOffset, err := safetoken.Offset(m.TokFile, start) +// OffsetSpan converts a pair of byte offsets to a Span. +func (m *ColumnMapper) OffsetSpan(start, end int) (span.Span, error) { + if start > end { + return span.Span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) + } + startPoint, err := m.OffsetPoint(start) if err != nil { - return Range{}, fmt.Errorf("start: %v", err) + return span.Span{}, err } - endOffset, err := safetoken.Offset(m.TokFile, end) + endPoint, err := m.OffsetPoint(end) if err != nil { - return Range{}, fmt.Errorf("end: %v", err) + return span.Span{}, err } - return m.OffsetRange(startOffset, endOffset) + return span.New(m.URI, startPoint, endPoint), nil } // Position returns the protocol position for the specified point, @@ -160,14 +187,14 @@ func (m *ColumnMapper) Position(p span.Point) (Position, error) { // OffsetPosition returns the protocol position of the specified // offset within m.Content. func (m *ColumnMapper) OffsetPosition(offset int) (Position, error) { - // We use span.ToPosition for its "line+1 at EOF" workaround. - line, _, err := span.ToPosition(m.TokFile, offset) + // We use span.OffsetToLineCol8 for its "line+1 at EOF" workaround. + line, _, err := span.OffsetToLineCol8(m.lines, offset) if err != nil { return Position{}, fmt.Errorf("OffsetPosition: %v", err) } // If that workaround executed, skip the usual column computation. char := 0 - if offset != m.TokFile.Size() { + if offset != m.lines.Size() { char = m.utf16Column(offset) } return Position{ @@ -224,24 +251,7 @@ func (m *ColumnMapper) RangeSpan(r Range) (span.Span, error) { if err != nil { return span.Span{}, err } - return span.New(m.URI, start, end).WithAll(m.TokFile) -} - -func (m *ColumnMapper) RangeToSpanRange(r Range) (span.Range, error) { - spn, err := m.RangeSpan(r) - if err != nil { - return span.Range{}, err - } - return spn.Range(m.TokFile) -} - -// Pos returns the token.Pos of protocol position p within the mapped file. -func (m *ColumnMapper) Pos(p Position) (token.Pos, error) { - start, err := m.Point(p) - if err != nil { - return token.NoPos, err - } - return safetoken.Pos(m.TokFile, start.Offset()) + return span.New(m.URI, start, end).WithAll(m.lines) } // Offset returns the utf-8 byte offset of p within the mapped file. @@ -253,13 +263,23 @@ func (m *ColumnMapper) Offset(p Position) (int, error) { return start.Offset(), nil } +// OffsetPoint returns the span.Point for the given byte offset. +func (m *ColumnMapper) OffsetPoint(offset int) (span.Point, error) { + // We use span.ToPosition for its "line+1 at EOF" workaround. + line, col8, err := span.OffsetToLineCol8(m.lines, offset) + if err != nil { + return span.Point{}, fmt.Errorf("OffsetPoint: %v", err) + } + return span.NewPoint(line, col8, offset), nil +} + // Point returns a span.Point for the protocol position p within the mapped file. // The resulting point has a valid Position and Offset. func (m *ColumnMapper) Point(p Position) (span.Point, error) { line := int(p.Line) + 1 // Find byte offset of start of containing line. - offset, err := span.ToOffset(m.TokFile, line, 1) + offset, err := span.ToOffset(m.lines, line, 1) if err != nil { return span.Point{}, err } diff --git a/gopls/internal/lsp/references.go b/gopls/internal/lsp/references.go index 6f4e3ee2060..390e2908acb 100644 --- a/gopls/internal/lsp/references.go +++ b/gopls/internal/lsp/references.go @@ -27,12 +27,12 @@ func (s *Server) references(ctx context.Context, params *protocol.ReferenceParam } var locations []protocol.Location for _, ref := range references { - refRange, err := ref.Range() + refRange, err := ref.MappedRange.Range() if err != nil { return nil, err } locations = append(locations, protocol.Location{ - URI: protocol.URIFromSpanURI(ref.URI()), + URI: protocol.URIFromSpanURI(ref.MappedRange.URI()), Range: refRange, }) } diff --git a/gopls/internal/lsp/safetoken/safetoken.go b/gopls/internal/lsp/safetoken/safetoken.go index fa9c67515f8..29cc1b1c664 100644 --- a/gopls/internal/lsp/safetoken/safetoken.go +++ b/gopls/internal/lsp/safetoken/safetoken.go @@ -41,6 +41,19 @@ func Offset(f *token.File, pos token.Pos) (int, error) { return int(pos) - f.Base(), nil } +// Offsets returns Offset(start) and Offset(end). +func Offsets(f *token.File, start, end token.Pos) (int, int, error) { + startOffset, err := Offset(f, start) + if err != nil { + return 0, 0, fmt.Errorf("start: %v", err) + } + endOffset, err := Offset(f, end) + if err != nil { + return 0, 0, fmt.Errorf("end: %v", err) + } + return startOffset, endOffset, nil +} + // Pos returns f.Pos(offset), but first checks that the offset is // non-negative and not larger than the size of the file. func Pos(f *token.File, offset int) (token.Pos, error) { diff --git a/gopls/internal/lsp/selection_range.go b/gopls/internal/lsp/selection_range.go index 314f2240799..b7a0cdb1d80 100644 --- a/gopls/internal/lsp/selection_range.go +++ b/gopls/internal/lsp/selection_range.go @@ -41,7 +41,7 @@ func (s *Server) selectionRange(ctx context.Context, params *protocol.SelectionR result := make([]protocol.SelectionRange, len(params.Positions)) for i, protocolPos := range params.Positions { - pos, err := pgf.Mapper.Pos(protocolPos) + pos, err := pgf.Pos(protocolPos) if err != nil { return nil, err } @@ -51,7 +51,7 @@ func (s *Server) selectionRange(ctx context.Context, params *protocol.SelectionR tail := &result[i] // tail of the Parent linked list, built head first for j, node := range path { - rng, err := pgf.Mapper.PosRange(node.Pos(), node.End()) + rng, err := pgf.PosRange(node.Pos(), node.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 4117eb70b82..728f61de7ab 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -182,8 +182,7 @@ func (e *encoded) token(start token.Pos, leng int, typ tokenType, mods []string) return } // want a line and column from start (in LSP coordinates). Ignore line directives. - rng := source.NewMappedRange(e.pgf.Mapper, start, start+token.Pos(leng)) - lspRange, err := rng.Range() + lspRange, err := e.pgf.PosRange(start, start+token.Pos(leng)) if err != nil { event.Error(e.ctx, "failed to convert to range", err) return diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index 076aed0c3d7..cce8a1326c5 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -86,12 +86,12 @@ func toProtocolIncomingCalls(ctx context.Context, snapshot Snapshot, refs []*Ref // once in the result but highlight all calls using FromRanges (ranges at which the calls occur) var incomingCalls = map[protocol.Location]*protocol.CallHierarchyIncomingCall{} for _, ref := range refs { - refRange, err := ref.Range() + refRange, err := ref.MappedRange.Range() if err != nil { return nil, err } - callItem, err := enclosingNodeCallItem(snapshot, ref.pkg, ref.URI(), ref.ident.NamePos) + callItem, err := enclosingNodeCallItem(snapshot, ref.pkg, ref.MappedRange.URI(), ref.ident.NamePos) if err != nil { event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name)) continue @@ -155,7 +155,7 @@ outer: nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos() kind = protocol.Function } - rng, err := NewMappedRange(pgf.Mapper, nameStart, nameEnd).Range() + rng, err := pgf.PosRange(nameStart, nameEnd) if err != nil { return protocol.CallHierarchyItem{}, err } @@ -199,7 +199,7 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr if len(identifier.Declaration.MappedRange) == 0 { return nil, nil } - callExprs, err := collectCallExpressions(identifier.Declaration.MappedRange[0].m, node) + callExprs, err := collectCallExpressions(identifier.Declaration.MappedRange[0].File, node) if err != nil { return nil, err } @@ -208,7 +208,7 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr } // collectCallExpressions collects call expression ranges inside a function. -func collectCallExpressions(mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) { +func collectCallExpressions(pgf *ParsedGoFile, node ast.Node) ([]protocol.Range, error) { type callPos struct { start, end token.Pos } @@ -238,7 +238,7 @@ func collectCallExpressions(mapper *protocol.ColumnMapper, node ast.Node) ([]pro callRanges := []protocol.Range{} for _, call := range callPositions { - callRange, err := NewMappedRange(mapper, call.start, call.end).Range() + callRange, err := pgf.PosRange(call.start, call.end) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go index c87f41664dd..f929256c273 100644 --- a/gopls/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -67,7 +67,7 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p return nil, err } // add a code lens to the top of the file which runs all benchmarks in the file - rng, err := NewMappedRange(pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() + rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package) if err != nil { return nil, err } @@ -111,7 +111,7 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) ( continue } - rng, err := NewMappedRange(pgf.Mapper, fn.Pos(), fn.End()).Range() + rng, err := pgf.PosRange(fn.Pos(), fn.End()) if err != nil { return out, err } @@ -177,7 +177,7 @@ func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ( if !strings.HasPrefix(l.Text, ggDirective) { continue } - rng, err := NewMappedRange(pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() + rng, err := pgf.PosRange(l.Pos(), l.Pos()+token.Pos(len(ggDirective))) if err != nil { return nil, err } @@ -214,7 +214,7 @@ func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([ if c == nil { return nil, nil } - rng, err := NewMappedRange(pgf.Mapper, c.Pos(), c.End()).Range() + rng, err := pgf.PosRange(c.Pos(), c.End()) if err != nil { return nil, err } @@ -235,7 +235,7 @@ func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle // Without a package name we have nowhere to put the codelens, so give up. return nil, nil } - rng, err := NewMappedRange(pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() + rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 19d16a1bbc9..4d2da2e40e7 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -291,6 +291,10 @@ type Selection struct { content string cursor token.Pos // relative to rng.TokFile rng span.Range + // TODO(adonovan): keep the ColumnMapper (completer.mapper) + // nearby so we can convert rng to protocol form without + // needing to read the file again, as the sole caller of + // Selection.Range() must currently do. } func (p Selection) Content() string { @@ -441,7 +445,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan } return items, surrounding, nil } - pos, err := pgf.Mapper.Pos(protoPos) + pos, err := pgf.Pos(protoPos) if err != nil { return nil, nil, err } diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index 70d98dfc839..de2b75d436c 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -37,7 +37,7 @@ func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh return nil, nil, err } - pos, err := pgf.Mapper.Pos(position) + pos, err := pgf.Pos(position) if err != nil { return nil, nil, err } diff --git a/gopls/internal/lsp/source/completion/util.go b/gopls/internal/lsp/source/completion/util.go index 72877a38a35..4b6ec09a092 100644 --- a/gopls/internal/lsp/source/completion/util.go +++ b/gopls/internal/lsp/source/completion/util.go @@ -312,13 +312,9 @@ func isBasicKind(t types.Type, k types.BasicInfo) bool { } func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.TextEdit, error) { - start, err := safetoken.Offset(c.tokFile, from) + start, end, err := safetoken.Offsets(c.tokFile, from, to) if err != nil { - return nil, err // can't happen: from came from c - } - end, err := safetoken.Offset(c.tokFile, to) - if err != nil { - return nil, err // can't happen: to came from c + return nil, err // can't happen: from/to came from c } return source.ToProtocolEdits(c.mapper, []diff.Edit{{ Start: start, diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index 0ac18e10390..31a8598b118 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -134,11 +134,7 @@ func CanExtractVariable(rng span.Range, file *ast.File) (ast.Expr, []ast.Node, b // line of code on which the insertion occurs. func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.Node) (string, error) { line := tok.Line(insertBeforeStmt.Pos()) - lineOffset, err := safetoken.Offset(tok, tok.LineStart(line)) - if err != nil { - return "", err - } - stmtOffset, err := safetoken.Offset(tok, insertBeforeStmt.Pos()) + lineOffset, stmtOffset, err := safetoken.Offsets(tok, tok.LineStart(line), insertBeforeStmt.Pos()) if err != nil { return "", err } @@ -405,11 +401,7 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file // We put the selection in a constructed file. We can then traverse and edit // the extracted selection without modifying the original AST. - startOffset, err := safetoken.Offset(tok, rng.Start) - if err != nil { - return nil, err - } - endOffset, err := safetoken.Offset(tok, rng.End) + startOffset, endOffset, err := safetoken.Offsets(tok, rng.Start, rng.End) if err != nil { return nil, err } @@ -605,11 +597,7 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file // We're going to replace the whole enclosing function, // so preserve the text before and after the selected block. - outerStart, err := safetoken.Offset(tok, outer.Pos()) - if err != nil { - return nil, err - } - outerEnd, err := safetoken.Offset(tok, outer.End()) + outerStart, outerEnd, err := safetoken.Offsets(tok, outer.Pos(), outer.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 34a6fe8ce26..873db7f9479 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" ) @@ -93,6 +94,10 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi if !end.IsValid() { end = edit.Pos } + startOffset, endOffset, err := safetoken.Offsets(tokFile, edit.Pos, end) + if err != nil { + return nil, err + } fh, err := snapshot.GetVersionedFile(ctx, span.URIFromPath(tokFile.Name())) if err != nil { return nil, err @@ -109,12 +114,13 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi } editsPerFile[fh.URI()] = te } + // TODO(adonovan): once FileHandle has a ColumnMapper, eliminate this. content, err := fh.Read() if err != nil { return nil, err } - m := protocol.ColumnMapper{URI: fh.URI(), TokFile: tokFile, Content: content} - rng, err := m.PosRange(edit.Pos, end) + m := protocol.NewColumnMapper(fh.URI(), content) + rng, err := m.OffsetRange(startOffset, endOffset) if err != nil { return nil, err } @@ -137,7 +143,7 @@ func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHan if err != nil { return nil, span.Range{}, nil, nil, nil, nil, fmt.Errorf("getting file for Identifier: %w", err) } - rng, err := pgf.Mapper.RangeToSpanRange(pRng) + rng, err := pgf.RangeToSpanRange(pRng) if err != nil { return nil, span.Range{}, nil, nil, nil, nil, err } diff --git a/gopls/internal/lsp/source/folding_range.go b/gopls/internal/lsp/source/folding_range.go index dacb5ae79f7..01107fd49b7 100644 --- a/gopls/internal/lsp/source/folding_range.go +++ b/gopls/internal/lsp/source/folding_range.go @@ -16,8 +16,8 @@ import ( // FoldingRangeInfo holds range and kind info of folding for an ast.Node type FoldingRangeInfo struct { - MappedRange - Kind protocol.FoldingRangeKind + MappedRange MappedRange + Kind protocol.FoldingRangeKind } // FoldingRange gets all of the folding range for f. @@ -42,10 +42,10 @@ func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFol } // Get folding ranges for comments separately as they are not walked by ast.Inspect. - ranges = append(ranges, commentsFoldingRange(pgf.Mapper, pgf.File)...) + ranges = append(ranges, commentsFoldingRange(pgf)...) visit := func(n ast.Node) bool { - rng := foldingRangeFunc(pgf.Tok, pgf.Mapper, n, lineFoldingOnly) + rng := foldingRangeFunc(pgf, n, lineFoldingOnly) if rng != nil { ranges = append(ranges, rng) } @@ -55,8 +55,8 @@ func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFol ast.Inspect(pgf.File, visit) sort.Slice(ranges, func(i, j int) bool { - irng, _ := ranges[i].Range() - jrng, _ := ranges[j].Range() + irng, _ := ranges[i].MappedRange.Range() + jrng, _ := ranges[j].MappedRange.Range() return protocol.CompareRange(irng, jrng) < 0 }) @@ -64,7 +64,7 @@ func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFol } // foldingRangeFunc calculates the line folding range for ast.Node n -func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { +func foldingRangeFunc(pgf *ParsedGoFile, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { // TODO(suzmue): include trailing empty lines before the closing // parenthesis/brace. var kind protocol.FoldingRangeKind @@ -76,7 +76,7 @@ func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, if num := len(n.List); num != 0 { startList, endList = n.List[0].Pos(), n.List[num-1].End() } - start, end = validLineFoldingRange(tokFile, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly) + start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly) case *ast.CaseClause: // Fold from position of ":" to end. start, end = n.Colon+1, n.End() @@ -92,7 +92,7 @@ func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, if num := len(n.List); num != 0 { startList, endList = n.List[0].Pos(), n.List[num-1].End() } - start, end = validLineFoldingRange(tokFile, n.Opening, n.Closing, startList, endList, lineFoldingOnly) + start, end = validLineFoldingRange(pgf.Tok, n.Opening, n.Closing, startList, endList, lineFoldingOnly) case *ast.GenDecl: // If this is an import declaration, set the kind to be protocol.Imports. if n.Tok == token.IMPORT { @@ -103,7 +103,7 @@ func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, if num := len(n.Specs); num != 0 { startSpecs, endSpecs = n.Specs[0].Pos(), n.Specs[num-1].End() } - start, end = validLineFoldingRange(tokFile, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly) + start, end = validLineFoldingRange(pgf.Tok, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly) case *ast.BasicLit: // Fold raw string literals from position of "`" to position of "`". if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' { @@ -115,7 +115,7 @@ func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, if num := len(n.Elts); num != 0 { startElts, endElts = n.Elts[0].Pos(), n.Elts[num-1].End() } - start, end = validLineFoldingRange(tokFile, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) + start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) } // Check that folding positions are valid. @@ -123,11 +123,11 @@ func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, return nil } // in line folding mode, do not fold if the start and end lines are the same. - if lineFoldingOnly && tokFile.Line(start) == tokFile.Line(end) { + if lineFoldingOnly && pgf.Tok.Line(start) == pgf.Tok.Line(end) { return nil } return &FoldingRangeInfo{ - MappedRange: NewMappedRange(m, start, end), + MappedRange: NewMappedRange(pgf, start, end), Kind: kind, } } @@ -157,9 +157,9 @@ func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Po // commentsFoldingRange returns the folding ranges for all comment blocks in file. // The folding range starts at the end of the first line of the comment block, and ends at the end of the // comment block and has kind protocol.Comment. -func commentsFoldingRange(m *protocol.ColumnMapper, file *ast.File) (comments []*FoldingRangeInfo) { - tokFile := m.TokFile - for _, commentGrp := range file.Comments { +func commentsFoldingRange(pgf *ParsedGoFile) (comments []*FoldingRangeInfo) { + tokFile := pgf.Tok + for _, commentGrp := range pgf.File.Comments { startGrpLine, endGrpLine := tokFile.Line(commentGrp.Pos()), tokFile.Line(commentGrp.End()) if startGrpLine == endGrpLine { // Don't fold single line comments. @@ -176,7 +176,7 @@ func commentsFoldingRange(m *protocol.ColumnMapper, file *ast.File) (comments [] } comments = append(comments, &FoldingRangeInfo{ // Fold from the end of the first line comment to the end of the comment block. - MappedRange: NewMappedRange(m, endLinePos, commentGrp.End()), + MappedRange: NewMappedRange(pgf, endLinePos, commentGrp.End()), Kind: protocol.Comment, }) } diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index 6662137b526..a2722186a9f 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -199,7 +199,7 @@ func computeFixEdits(snapshot Snapshot, pgf *ParsedGoFile, options *imports.Opti fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure. } edits := snapshot.View().Options().ComputeEdits(left, string(fixedData)) - return protocolEditsFromSource([]byte(left), edits, pgf.Mapper.TokFile) + return protocolEditsFromSource([]byte(left), edits) } // importPrefix returns the prefix of the given file content through the final @@ -314,7 +314,7 @@ func computeTextEdits(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile, // protocolEditsFromSource converts text edits to LSP edits using the original // source. -func protocolEditsFromSource(src []byte, edits []diff.Edit, tf *token.File) ([]protocol.TextEdit, error) { +func protocolEditsFromSource(src []byte, edits []diff.Edit) ([]protocol.TextEdit, error) { m := lsppos.NewMapper(src) var result []protocol.TextEdit for _, edit := range edits { diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index d073fffe732..9bd90e5b05e 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -28,7 +28,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p return nil, fmt.Errorf("getting package for Highlight: %w", err) } - pos, err := pgf.Mapper.Pos(position) + pos, err := pgf.Pos(position) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index dd6ce4021ca..2cf369bb44a 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -83,7 +83,7 @@ func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position proto if err != nil { return nil, err } - rng, err := ident.Range() + rng, err := ident.MappedRange.Range() if err != nil { return nil, err } @@ -104,11 +104,7 @@ func hoverRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position p ctx, done := event.Start(ctx, "source.hoverRune") defer done() - r, mrng, err := findRune(ctx, snapshot, fh, position) - if err != nil { - return nil, err - } - rng, err := mrng.Range() + r, rng, err := findRune(ctx, snapshot, fh, position) if err != nil { return nil, err } @@ -138,18 +134,18 @@ func hoverRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position p var ErrNoRuneFound = errors.New("no rune found") // findRune returns rune information for a position in a file. -func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, MappedRange, error) { +func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, protocol.Range, error) { fh, err := snapshot.GetFile(ctx, fh.URI()) if err != nil { - return 0, MappedRange{}, err + return 0, protocol.Range{}, err } pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { - return 0, MappedRange{}, err + return 0, protocol.Range{}, err } - pos, err := pgf.Mapper.Pos(position) + pos, err := pgf.Pos(position) if err != nil { - return 0, MappedRange{}, err + return 0, protocol.Range{}, err } // Find the basic literal enclosing the given position, if there is one. @@ -166,7 +162,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr return lit == nil // descend unless target is found }) if lit == nil { - return 0, MappedRange{}, ErrNoRuneFound + return 0, protocol.Range{}, ErrNoRuneFound } var r rune @@ -177,26 +173,26 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr if err != nil { // If the conversion fails, it's because of an invalid syntax, therefore // there is no rune to be found. - return 0, MappedRange{}, ErrNoRuneFound + return 0, protocol.Range{}, ErrNoRuneFound } r, _ = utf8.DecodeRuneInString(s) if r == utf8.RuneError { - return 0, MappedRange{}, fmt.Errorf("rune error") + return 0, protocol.Range{}, fmt.Errorf("rune error") } start, end = lit.Pos(), lit.End() case token.INT: // It's an integer, scan only if it is a hex litteral whose bitsize in // ranging from 8 to 32. if !(strings.HasPrefix(lit.Value, "0x") && len(lit.Value[2:]) >= 2 && len(lit.Value[2:]) <= 8) { - return 0, MappedRange{}, ErrNoRuneFound + return 0, protocol.Range{}, ErrNoRuneFound } v, err := strconv.ParseUint(lit.Value[2:], 16, 32) if err != nil { - return 0, MappedRange{}, err + return 0, protocol.Range{}, err } r = rune(v) if r == utf8.RuneError { - return 0, MappedRange{}, fmt.Errorf("rune error") + return 0, protocol.Range{}, fmt.Errorf("rune error") } start, end = lit.Pos(), lit.End() case token.STRING: @@ -205,17 +201,17 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr var found bool litOffset, err := safetoken.Offset(pgf.Tok, lit.Pos()) if err != nil { - return 0, MappedRange{}, err + return 0, protocol.Range{}, err } offset, err := safetoken.Offset(pgf.Tok, pos) if err != nil { - return 0, MappedRange{}, err + return 0, protocol.Range{}, err } for i := offset - litOffset; i > 0; i-- { // Start at the cursor position and search backward for the beginning of a rune escape sequence. rr, _ := utf8.DecodeRuneInString(lit.Value[i:]) if rr == utf8.RuneError { - return 0, MappedRange{}, fmt.Errorf("rune error") + return 0, protocol.Range{}, fmt.Errorf("rune error") } if rr == '\\' { // Got the beginning, decode it. @@ -223,7 +219,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr r, _, tail, err = strconv.UnquoteChar(lit.Value[i:], '"') if err != nil { // If the conversion fails, it's because of an invalid syntax, therefore is no rune to be found. - return 0, MappedRange{}, ErrNoRuneFound + return 0, protocol.Range{}, ErrNoRuneFound } // Only the rune escape sequence part of the string has to be highlighted, recompute the range. runeLen := len(lit.Value) - (int(i) + len(tail)) @@ -235,12 +231,16 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr } if !found { // No escape sequence found - return 0, MappedRange{}, ErrNoRuneFound + return 0, protocol.Range{}, ErrNoRuneFound } default: - return 0, MappedRange{}, ErrNoRuneFound + return 0, protocol.Range{}, ErrNoRuneFound + } + rng, err := pgf.PosRange(start, end) + if err != nil { + return 0, protocol.Range{}, err } - return r, NewMappedRange(pgf.Mapper, start, end), nil + return r, rng, nil } func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) { @@ -580,11 +580,7 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob var spec ast.Spec for _, s := range node.Specs { // Avoid panics by guarding the calls to token.Offset (golang/go#48249). - start, err := safetoken.Offset(fullTok, s.Pos()) - if err != nil { - return nil, err - } - end, err := safetoken.Offset(fullTok, s.End()) + start, end, err := safetoken.Offsets(fullTok, s.Pos(), s.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index ad826da07a3..bfd853fd5c3 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -24,13 +24,13 @@ import ( // IdentifierInfo holds information about an identifier in Go source. type IdentifierInfo struct { - Name string - Snapshot Snapshot // only needed for .View(); TODO(adonovan): reduce. - MappedRange + Name string + Snapshot Snapshot // only needed for .View(); TODO(adonovan): reduce. + MappedRange MappedRange Type struct { - MappedRange - Object types.Object + MappedRange MappedRange // TODO(adonovan): strength-reduce to a protocol.Location + Object types.Object } Inferred *types.Signature @@ -83,7 +83,7 @@ func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, position if err != nil { return nil, err } - pos, err := pgf.Mapper.Pos(position) + pos, err := pgf.Pos(position) if err != nil { return nil, err } @@ -114,24 +114,15 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // Special case for package declarations, since they have no // corresponding types.Object. if ident == file.Name { - rng, err := posToMappedRange(pkg, file.Name.Pos(), file.Name.End()) - if err != nil { - return nil, err - } - var declAST *ast.File + rng := NewMappedRange(pgf, file.Name.Pos(), file.Name.End()) + // If there's no package documentation, just use current file. + decl := pgf for _, pgf := range pkg.CompiledGoFiles() { if pgf.File.Doc != nil { - declAST = pgf.File + decl = pgf } } - // If there's no package documentation, just use current file. - if declAST == nil { - declAST = file - } - declRng, err := posToMappedRange(pkg, declAST.Name.Pos(), declAST.Name.End()) - if err != nil { - return nil, err - } + declRng := NewMappedRange(decl, decl.File.Name.Pos(), decl.File.Name.End()) return &IdentifierInfo{ Name: file.Name.Name, ident: file.Name, @@ -140,7 +131,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa qf: qf, Snapshot: snapshot, Declaration: Declaration{ - node: declAST.Name, + node: decl.File.Name, MappedRange: []MappedRange{declRng}, }, }, nil @@ -199,7 +190,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // The builtin package isn't in the dependency graph, so the usual // utilities won't work here. - rng := NewMappedRange(builtin.Mapper, decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) + rng := NewMappedRange(builtin, decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } @@ -240,7 +231,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa } name := method.Names[0].Name result.Declaration.node = method - rng := NewMappedRange(builtin.Mapper, method.Pos(), method.Pos()+token.Pos(len(name))) + rng := NewMappedRange(builtin, method.Pos(), method.Pos()+token.Pos(len(name))) result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go index ade2f5fa706..3958e4b89cf 100644 --- a/gopls/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -111,7 +111,7 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng proto start, end := pgf.File.Pos(), pgf.File.End() if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character { // Adjust start and end for the specified range. - rng, err := pgf.Mapper.RangeToSpanRange(pRng) + rng, err := pgf.RangeToSpanRange(pRng) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index a1dcc5417a9..c7529e89e37 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -23,8 +23,8 @@ import ( // ReferenceInfo holds information about reference to an identifier in Go source. type ReferenceInfo struct { - Name string - MappedRange + Name string + MappedRange MappedRange ident *ast.Ident obj types.Object pkg Package @@ -74,7 +74,7 @@ func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protoco if rdep.DepsByImpPath[UnquoteImportPath(imp)] == targetPkg.ID { refs = append(refs, &ReferenceInfo{ Name: pgf.File.Name.Name, - MappedRange: NewMappedRange(f.Mapper, imp.Pos(), imp.End()), + MappedRange: NewMappedRange(f, imp.Pos(), imp.End()), }) } } @@ -93,7 +93,7 @@ func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protoco } refs = append(refs, &ReferenceInfo{ Name: pgf.File.Name.Name, - MappedRange: NewMappedRange(f.Mapper, f.File.Name.Pos(), f.File.Name.End()), + MappedRange: NewMappedRange(f, f.File.Name.Pos(), f.File.Name.End()), }) } @@ -120,7 +120,7 @@ func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protoco } sort.Slice(toSort, func(i, j int) bool { x, y := toSort[i], toSort[j] - if cmp := strings.Compare(string(x.URI()), string(y.URI())); cmp != 0 { + if cmp := strings.Compare(string(x.MappedRange.URI()), string(y.MappedRange.URI())); cmp != 0 { return cmp < 0 } return x.ident.Pos() < y.ident.Pos() @@ -137,8 +137,8 @@ func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, return nil, false, err } // Careful: because we used ParseHeader, - // Mapper.Pos(ppos) may be beyond EOF => (0, err). - pos, _ := pgf.Mapper.Pos(ppos) + // pgf.Pos(ppos) may be beyond EOF => (0, err). + pos, _ := pgf.Pos(ppos) return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil } @@ -261,11 +261,11 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i if includeInterfaceRefs && !isType { // TODO(adonovan): opt: don't go back into the position domain: // we have complete type information already. - declRange, err := declIdent.Range() + declRange, err := declIdent.MappedRange.Range() if err != nil { return nil, err } - fh, err := snapshot.GetFile(ctx, declIdent.URI()) + fh, err := snapshot.GetFile(ctx, declIdent.MappedRange.URI()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 37fb2d5570c..8cb50e82796 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -107,7 +107,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot } // Return the location of the package declaration. - rng, err := pgf.Mapper.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) + rng, err := pgf.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) if err != nil { return nil, err, err } @@ -434,7 +434,7 @@ func renamePackageClause(ctx context.Context, m *Metadata, snapshot Snapshot, ne if f.File.Name == nil { continue // no package declaration } - rng, err := f.Mapper.PosRange(f.File.Name.Pos(), f.File.Name.End()) + rng, err := f.PosRange(f.File.Name.Pos(), f.File.Name.End()) if err != nil { return err } @@ -493,8 +493,7 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath } // Create text edit for the import path (string literal). - impPathMappedRange := NewMappedRange(f.Mapper, imp.Path.Pos(), imp.Path.End()) - rng, err := impPathMappedRange.Range() + rng, err := f.PosRange(imp.Path.Pos(), imp.Path.End()) if err != nil { return err } @@ -667,7 +666,7 @@ func (r *renamer) update() (map[span.URI][]diff.Edit, error) { return nil, err } for _, ref := range r.refs { - refSpan, err := ref.Span() + refSpan, err := ref.MappedRange.Span() if err != nil { return nil, err } @@ -726,8 +725,7 @@ func (r *renamer) update() (map[span.URI][]diff.Edit, error) { for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) { // The File.Offset static check complains // even though these uses are manifestly safe. - start, _ := safetoken.Offset(tokFile, lineStart+token.Pos(locs[0])) - end, _ := safetoken.Offset(tokFile, lineStart+token.Pos(locs[1])) + start, end, _ := safetoken.Offsets(tokFile, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1])) result[uri] = append(result[uri], diff.Edit{ Start: start, End: end, @@ -813,15 +811,14 @@ func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.Edit, error) { // Replace the portion (possibly empty) of the spec before the path: // local "path" or "path" // -> <- -><- - rng := span.NewRange(tokFile, spec.Pos(), spec.Path.Pos()) - spn, err := rng.Span() + start, end, err := safetoken.Offsets(tokFile, spec.Pos(), spec.Path.Pos()) if err != nil { return nil, err } return &diff.Edit{ - Start: spn.Start().Offset(), - End: spn.End().Offset(), + Start: start, + End: end, New: newText, }, nil } diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index 3f12d90ad18..a751b291051 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -24,7 +24,7 @@ func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, positi if err != nil { return nil, 0, fmt.Errorf("getting file for SignatureHelp: %w", err) } - pos, err := pgf.Mapper.Pos(position) + pos, err := pgf.Pos(position) if err != nil { return nil, 0, err } diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index ed8d9a45d68..fccb91ed638 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -431,11 +431,11 @@ func nonOverlappingRanges(t *testing.T, ranges []*source.FoldingRangeInfo) (res } func conflict(t *testing.T, a, b *source.FoldingRangeInfo) bool { - arng, err := a.Range() + arng, err := a.MappedRange.Range() if err != nil { t.Fatal(err) } - brng, err := b.Range() + brng, err := b.MappedRange.Range() if err != nil { t.Fatal(err) } @@ -450,7 +450,7 @@ func foldRanges(contents string, ranges []*source.FoldingRangeInfo) (string, err // to preserve the offsets. for i := len(ranges) - 1; i >= 0; i-- { fRange := ranges[i] - spn, err := fRange.Span() + spn, err := fRange.MappedRange.Span() if err != nil { return "", err } @@ -552,7 +552,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { t.Fatal(err) } if d.IsType { - rng, err = ident.Type.Range() + rng, err = ident.Type.MappedRange.Range() if err != nil { t.Fatal(err) } @@ -725,7 +725,7 @@ func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { } got := make(map[span.Span]bool) for _, refInfo := range refs { - refSpan, err := refInfo.Span() + refSpan, err := refInfo.MappedRange.Span() if err != nil { t.Fatal(err) } diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 2568bd07e20..0d94d4825da 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -109,12 +109,11 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi // Return the diff. diffs := snapshot.View().Options().ComputeEdits(string(parsedConcreteFile.Src), source.String()) - tf := parsedConcreteFile.Mapper.TokFile var edits []analysis.TextEdit for _, edit := range diffs { edits = append(edits, analysis.TextEdit{ - Pos: tf.Pos(edit.Start), - End: tf.Pos(edit.End), + Pos: parsedConcreteFile.Tok.Pos(edit.Start), + End: parsedConcreteFile.Tok.Pos(edit.End), NewText: []byte(edit.New), }) } @@ -223,7 +222,7 @@ func getStubNodes(pgf *ParsedGoFile, pRng protocol.Range) ([]ast.Node, token.Pos if err != nil { return nil, 0, err } - rng, err := spn.Range(pgf.Mapper.TokFile) + rng, err := spn.Range(pgf.Tok) if err != nil { return nil, 0, err } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index face4c99ac2..0b7f3e44e8e 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -20,66 +20,46 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/typeparams" ) -// MappedRange provides mapped protocol.Range for a span.Range, accounting for -// UTF-16 code points. +// A MappedRange represents an interval within a parsed Go file and +// has the ability to convert to protocol.Range or span.Span form. // -// TOOD(adonovan): eliminate this type. Replace all uses by an -// explicit pair (span.Range, protocol.ColumnMapper), and an operation -// to map both to a protocol.Range. +// TOOD(adonovan): eliminate this type by inlining it: make callers +// hold the triple themselves, or convert to Mapper + start/end offsets +// and hold that. type MappedRange struct { - spanRange span.Range // the range in the compiled source (package.CompiledGoFiles) - m *protocol.ColumnMapper // a mapper of the edited source (package.GoFiles) + // TODO(adonovan): eliminate sole tricky direct use of this, + // which is entangled with IdentifierInfo.Declaration.node. + File *ParsedGoFile + start, end token.Pos } // NewMappedRange returns a MappedRange for the given file and -// start/end positions, which must be valid within m.TokFile. -func NewMappedRange(m *protocol.ColumnMapper, start, end token.Pos) MappedRange { - return MappedRange{ - spanRange: span.NewRange(m.TokFile, start, end), - m: m, - } +// start/end positions, which must be valid within the file. +func NewMappedRange(pgf *ParsedGoFile, start, end token.Pos) MappedRange { + _ = span.NewRange(pgf.Tok, start, end) // just for assertions + return MappedRange{File: pgf, start: start, end: end} } // Range returns the LSP range in the edited source. -// -// See the documentation of NewMappedRange for information on edited vs -// compiled source. func (s MappedRange) Range() (protocol.Range, error) { - if s.m == nil { - return protocol.Range{}, bug.Errorf("invalid range") - } - spn, err := span.FileSpan(s.spanRange.TokFile, s.spanRange.Start, s.spanRange.End) - if err != nil { - return protocol.Range{}, err - } - return s.m.Range(spn) + return s.File.PosRange(s.start, s.end) } -// Span returns the span corresponding to the mapped range in the edited -// source. -// -// See the documentation of NewMappedRange for information on edited vs -// compiled source. +// Span returns the span corresponding to the mapped range in the edited source. func (s MappedRange) Span() (span.Span, error) { - // In the past, some code-paths have relied on Span returning an error if s - // is the zero value (i.e. s.m is nil). But this should be treated as a bug: - // observe that s.URI() would panic in this case. - if s.m == nil { - return span.Span{}, bug.Errorf("invalid range") + start, end, err := safetoken.Offsets(s.File.Tok, s.start, s.end) + if err != nil { + return span.Span{}, err } - return span.FileSpan(s.spanRange.TokFile, s.spanRange.Start, s.spanRange.End) + return s.File.Mapper.OffsetSpan(start, end) } // URI returns the URI of the edited file. -// -// See the documentation of NewMappedRange for information on edited vs -// compiled source. func (s MappedRange) URI() span.URI { - return s.m.URI + return s.File.Mapper.URI } func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { @@ -126,6 +106,10 @@ func objToMappedRange(pkg Package, obj types.Object) (MappedRange, error) { // posToMappedRange returns the MappedRange for the given [start, end) span, // which must be among the transitive dependencies of pkg. +// +// TODO(adonovan): many of the callers need only the ParsedGoFile so +// that they can call pgf.PosRange(pos, end) to get a Range; they +// don't actually need a MappedRange. func posToMappedRange(pkg Package, pos, end token.Pos) (MappedRange, error) { if !pos.IsValid() { return MappedRange{}, fmt.Errorf("invalid start position") @@ -139,7 +123,7 @@ func posToMappedRange(pkg Package, pos, end token.Pos) (MappedRange, error) { if err != nil { return MappedRange{}, err } - return NewMappedRange(pgf.Mapper, pos, end), nil + return NewMappedRange(pgf, pos, end), nil } // FindPackageFromPos returns the Package for the given position, which must be diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 0b73c7494fc..21e99697f4e 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -23,6 +23,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event/label" "golang.org/x/tools/internal/event/tag" @@ -380,6 +381,33 @@ type ParsedGoFile struct { ParseErr scanner.ErrorList } +// Pos returns the token.Pos of protocol position p within the file. +func (pgf *ParsedGoFile) Pos(p protocol.Position) (token.Pos, error) { + point, err := pgf.Mapper.Point(p) + if err != nil { + return token.NoPos, err + } + return safetoken.Pos(pgf.Tok, point.Offset()) +} + +// PosRange returns a protocol Range for the token.Pos interval in this file. +func (pgf *ParsedGoFile) PosRange(start, end token.Pos) (protocol.Range, error) { + startOffset, endOffset, err := safetoken.Offsets(pgf.Tok, start, end) + if err != nil { + return protocol.Range{}, err + } + return pgf.Mapper.OffsetRange(startOffset, endOffset) +} + +// RangeToSpanRange parses a protocol Range back into the go/token domain. +func (pgf *ParsedGoFile) RangeToSpanRange(r protocol.Range) (span.Range, error) { + spn, err := pgf.Mapper.RangeSpan(r) + if err != nil { + return span.Range{}, err + } + return spn.Range(pgf.Tok) +} + // A ParsedModule contains the results of parsing a go.mod file. type ParsedModule struct { URI span.URI diff --git a/gopls/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go index 623d2ce8bda..b3682e1f665 100644 --- a/gopls/internal/lsp/work/completion.go +++ b/gopls/internal/lsp/work/completion.go @@ -8,15 +8,14 @@ import ( "context" "errors" "fmt" - "go/token" "os" "path/filepath" "sort" "strings" - "golang.org/x/tools/internal/event" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" ) func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, position protocol.Position) (*protocol.CompletionList, error) { @@ -28,18 +27,17 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.Version if err != nil { return nil, fmt.Errorf("getting go.work file handle: %w", err) } - pos, err := pw.Mapper.Pos(position) + cursor, err := pw.Mapper.Offset(position) if err != nil { - return nil, fmt.Errorf("computing cursor position: %w", err) + return nil, fmt.Errorf("computing cursor offset: %w", err) } // Find the use statement the user is in. - cursor := pos - 1 use, pathStart, _ := usePath(pw, cursor) if use == nil { return &protocol.CompletionList{}, nil } - completingFrom := use.Path[:cursor-token.Pos(pathStart)] + completingFrom := use.Path[:cursor-pathStart] // We're going to find the completions of the user input // (completingFrom) by doing a walk on the innermost directory diff --git a/gopls/internal/lsp/work/hover.go b/gopls/internal/lsp/work/hover.go index 641028b16e6..a29d59ce2cd 100644 --- a/gopls/internal/lsp/work/hover.go +++ b/gopls/internal/lsp/work/hover.go @@ -8,7 +8,6 @@ import ( "bytes" "context" "fmt" - "go/token" "golang.org/x/mod/modfile" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -30,14 +29,14 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, if err != nil { return nil, fmt.Errorf("getting go.work file handle: %w", err) } - pos, err := pw.Mapper.Pos(position) + offset, err := pw.Mapper.Offset(position) if err != nil { - return nil, fmt.Errorf("computing cursor position: %w", err) + return nil, fmt.Errorf("computing cursor offset: %w", err) } // Confirm that the cursor is inside a use statement, and then find // the position of the use statement's directory path. - use, pathStart, pathEnd := usePath(pw, pos) + use, pathStart, pathEnd := usePath(pw, offset) // The cursor position is not on a use statement. if use == nil { @@ -70,7 +69,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, }, nil } -func usePath(pw *source.ParsedWorkFile, pos token.Pos) (use *modfile.Use, pathStart, pathEnd int) { +func usePath(pw *source.ParsedWorkFile, offset int) (use *modfile.Use, pathStart, pathEnd int) { for _, u := range pw.File.Use { path := []byte(u.Path) s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte @@ -82,7 +81,7 @@ func usePath(pw *source.ParsedWorkFile, pos token.Pos) (use *modfile.Use, pathSt // Shift the start position to the location of the // module directory within the use statement. pathStart, pathEnd = s+i, s+i+len(path) - if token.Pos(pathStart) <= pos && pos <= token.Pos(pathEnd) { + if pathStart <= offset && offset <= pathEnd { return u, pathStart, pathEnd } } diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index b0169664919..f9965149957 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -50,7 +50,7 @@ func main() { func TestFailingDiagnosticClearingOnEdit(t *testing.T) { t.Skip("line directives //line ") // badPackageDup contains a duplicate definition of the 'a' const. - // This is a minor variant of TestDiagnosticClearingOnEditfrom from + // This is a minor variant of TestDiagnosticClearingOnEdit from // diagnostics_test.go, with a line directive, which makes no difference. const badPackageDup = ` -- go.mod -- diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go index 0c24a2deeed..36629f0f930 100644 --- a/gopls/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -36,9 +36,9 @@ type span struct { } type point struct { - Line int `json:"line"` - Column int `json:"column"` - Offset int `json:"offset"` + Line int `json:"line"` // 1-based line number + Column int `json:"column"` // 1-based, UTF-8 codes (bytes) + Offset int `json:"offset"` // 0-based byte offset } // Invalid is a span that reports false from IsValid @@ -274,12 +274,12 @@ func (s *Span) update(tf *token.File, withPos, withOffset bool) error { } func (p *point) updatePosition(tf *token.File) error { - line, col, err := ToPosition(tf, p.Offset) + line, col8, err := OffsetToLineCol8(tf, p.Offset) if err != nil { return err } p.Line = line - p.Column = col + p.Column = col8 return nil } diff --git a/gopls/internal/span/token.go b/gopls/internal/span/token.go index ca78d67800f..2e71cbac00a 100644 --- a/gopls/internal/span/token.go +++ b/gopls/internal/span/token.go @@ -15,6 +15,8 @@ import ( // Range represents a source code range in token.Pos form. // It also carries the token.File that produced the positions, so that it is // self contained. +// +// TODO(adonovan): move to safetoken.Range (but the Range.Span function must stay behind). type Range struct { TokFile *token.File // non-nil Start, End token.Pos // both IsValid() @@ -55,14 +57,6 @@ func NewRange(file *token.File, start, end token.Pos) Range { } } -// NewTokenFile returns a token.File for the given file content. -func NewTokenFile(filename string, content []byte) *token.File { - fset := token.NewFileSet() - f := fset.AddFile(filename, -1, len(content)) - f.SetLinesForContent(content) - return f -} - // IsPoint returns true if the range represents a single point. func (r Range) IsPoint() bool { return r.Start == r.End @@ -95,8 +89,6 @@ func FileSpan(file *token.File, start, end token.Pos) (Span, error) { if err != nil { return Span{}, err } - // In the presence of line directives, a single File can have sections from - // multiple file names. if endFilename != startFilename { return Span{}, fmt.Errorf("span begins in file %q but ends in %q", startFilename, endFilename) } @@ -160,11 +152,13 @@ func (s Span) Range(tf *token.File) (Range, error) { }, nil } -// ToPosition converts a byte offset in the file corresponding to tf into +// OffsetToLineCol8 converts a byte offset in the file corresponding to tf into // 1-based line and utf-8 column indexes. -func ToPosition(tf *token.File, offset int) (int, int, error) { - _, line, col, err := positionFromOffset(tf, offset) - return line, col, err +// +// TODO(adonovan): move to safetoken package for consistency? +func OffsetToLineCol8(tf *token.File, offset int) (int, int, error) { + _, line, col8, err := positionFromOffset(tf, offset) + return line, col8, err } // ToOffset converts a 1-based line and utf-8 column index into a byte offset From 1fe76afbb7ecd78488f47a4c85560bf920e9d043 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 5 Jan 2023 13:13:04 -0500 Subject: [PATCH 583/723] gopls/internal/lsp/source: MappedRange cleanup This change decouples MappedRange from the go/token domain. It is now just (mapper ColumnMapper, start, end int), using byte offsets. I was initially skeptical that this type was necessary but there does seem to be a need to record intervals in a form that can be converted to span or to protocol form, and MappedRange makes this easier than three separate values. The sole constructor NewMappedRange has moved to ParsedGoFile.PosMappedRange. (Clients are free to construct it directly but none do.) Most of the work of this change is making sure that Declaration.node, an ast.Node, is accompanied by the correct nodeFile (a ParsedGoFile). Change-Id: Ic5a187b87aef3d670273ab3cf7ac835519237f53 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460815 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/protocol/span.go | 2 +- gopls/internal/lsp/source/call_hierarchy.go | 5 +- gopls/internal/lsp/source/folding_range.go | 13 +++++- gopls/internal/lsp/source/identifier.go | 52 +++++++++++++++------ gopls/internal/lsp/source/references.go | 12 ++++- gopls/internal/lsp/source/util.go | 46 +++++++----------- gopls/internal/lsp/source/view.go | 10 ++++ 7 files changed, 89 insertions(+), 51 deletions(-) diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 160b925895a..72bd4c1dadd 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -8,7 +8,7 @@ // // Imports: source --> lsppos --> protocol --> span --> token // -// source.MappedRange = (*ParsedGoFile, start/end token.Pos) +// source.MappedRange = (protocol.ColumnMapper, {start,end}Offset int) // // lsppos.TokenMapper = (token.File, lsppos.Mapper) // lsppos.Mapper = (line offset table, content) diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index cce8a1326c5..40314866172 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -196,10 +196,7 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr if node == nil { return nil, nil } - if len(identifier.Declaration.MappedRange) == 0 { - return nil, nil - } - callExprs, err := collectCallExpressions(identifier.Declaration.MappedRange[0].File, node) + callExprs, err := collectCallExpressions(identifier.Declaration.nodeFile, node) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/folding_range.go b/gopls/internal/lsp/source/folding_range.go index 01107fd49b7..4be711cbea3 100644 --- a/gopls/internal/lsp/source/folding_range.go +++ b/gopls/internal/lsp/source/folding_range.go @@ -12,6 +12,7 @@ import ( "strings" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" ) // FoldingRangeInfo holds range and kind info of folding for an ast.Node @@ -126,8 +127,12 @@ func foldingRangeFunc(pgf *ParsedGoFile, n ast.Node, lineFoldingOnly bool) *Fold if lineFoldingOnly && pgf.Tok.Line(start) == pgf.Tok.Line(end) { return nil } + mrng, err := pgf.PosMappedRange(start, end) + if err != nil { + bug.Errorf("%w", err) // can't happen + } return &FoldingRangeInfo{ - MappedRange: NewMappedRange(pgf, start, end), + MappedRange: mrng, Kind: kind, } } @@ -174,9 +179,13 @@ func commentsFoldingRange(pgf *ParsedGoFile) (comments []*FoldingRangeInfo) { // folding range start at the end of the first line. endLinePos = token.Pos(int(startPos) + len(strings.Split(firstComment.Text, "\n")[0])) } + mrng, err := pgf.PosMappedRange(endLinePos, commentGrp.End()) + if err != nil { + bug.Errorf("%w", err) // can't happen + } comments = append(comments, &FoldingRangeInfo{ // Fold from the end of the first line comment to the end of the comment block. - MappedRange: NewMappedRange(pgf, endLinePos, commentGrp.End()), + MappedRange: mrng, Kind: protocol.Comment, }) } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index bfd853fd5c3..94fb2fb32b7 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -29,7 +29,7 @@ type IdentifierInfo struct { MappedRange MappedRange Type struct { - MappedRange MappedRange // TODO(adonovan): strength-reduce to a protocol.Location + MappedRange MappedRange Object types.Object } @@ -56,8 +56,9 @@ func (i *IdentifierInfo) IsImport() bool { type Declaration struct { MappedRange []MappedRange - // The typechecked node. - node ast.Node + // The typechecked node + node ast.Node + nodeFile *ParsedGoFile // provides token.File and ColumnMapper for node // Optional: the fully parsed node, to be used for formatting in cases where // node has missing information. This could be the case when node was parsed @@ -97,7 +98,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa file := pgf.File // Handle import specs separately, as there is no formal position for a // package declaration. - if result, err := importSpec(snapshot, pkg, file, pos); result != nil || err != nil { + if result, err := importSpec(snapshot, pkg, pgf, pos); result != nil || err != nil { return result, err } path := pathEnclosingObjNode(file, pos) @@ -114,7 +115,6 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // Special case for package declarations, since they have no // corresponding types.Object. if ident == file.Name { - rng := NewMappedRange(pgf, file.Name.Pos(), file.Name.End()) // If there's no package documentation, just use current file. decl := pgf for _, pgf := range pkg.CompiledGoFiles() { @@ -122,16 +122,24 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa decl = pgf } } - declRng := NewMappedRange(decl, decl.File.Name.Pos(), decl.File.Name.End()) + pkgRng, err := pgf.PosMappedRange(file.Name.Pos(), file.Name.End()) + if err != nil { + return nil, err + } + declRng, err := decl.PosMappedRange(decl.File.Name.Pos(), decl.File.Name.End()) + if err != nil { + return nil, err + } return &IdentifierInfo{ Name: file.Name.Name, ident: file.Name, - MappedRange: rng, + MappedRange: pkgRng, pkg: pkg, qf: qf, Snapshot: snapshot, Declaration: Declaration{ node: decl.File.Name, + nodeFile: decl, MappedRange: []MappedRange{declRng}, }, }, nil @@ -183,6 +191,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa return nil, fmt.Errorf("no declaration for %s", result.Name) } result.Declaration.node = decl + result.Declaration.nodeFile = builtin if typeSpec, ok := decl.(*ast.TypeSpec); ok { // Find the GenDecl (which has the doc comments) for the TypeSpec. result.Declaration.fullDecl = findGenDecl(builtin.File, typeSpec) @@ -190,7 +199,10 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // The builtin package isn't in the dependency graph, so the usual // utilities won't work here. - rng := NewMappedRange(builtin, decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) + rng, err := builtin.PosMappedRange(decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) + if err != nil { + return nil, err + } result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } @@ -231,7 +243,11 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa } name := method.Names[0].Name result.Declaration.node = method - rng := NewMappedRange(builtin, method.Pos(), method.Pos()+token.Pos(len(name))) + result.Declaration.nodeFile = builtin + rng, err := builtin.PosMappedRange(method.Pos(), method.Pos()+token.Pos(len(name))) + if err != nil { + return nil, err + } result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } @@ -246,17 +262,26 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa } } + // TODO(adonovan): this step calls the somewhat expensive + // findFileInDeps, which is also called below. Refactor + // objToMappedRange to separate the find-file from the + // lookup-position steps to avoid the redundancy. rng, err := objToMappedRange(pkg, result.Declaration.obj) if err != nil { return nil, err } result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) - declPkg, err := FindPackageFromPos(pkg, result.Declaration.obj.Pos()) + declPos := result.Declaration.obj.Pos() + objURI := span.URIFromPath(pkg.FileSet().File(declPos).Name()) + declFile, declPkg, err := findFileInDeps(pkg, objURI) if err != nil { return nil, err } - result.Declaration.node, _ = FindDeclAndField(declPkg.GetSyntax(), result.Declaration.obj.Pos()) // may be nil + // TODO(adonovan): there's no need to inspect the entire GetSyntax() slice: + // we already know it's declFile.File. + result.Declaration.node, _ = FindDeclAndField(declPkg.GetSyntax(), declPos) // may be nil + result.Declaration.nodeFile = declFile // Ensure that we have the full declaration, in case the declaration was // parsed in ParseExported and therefore could be missing information. @@ -424,9 +449,9 @@ func hasErrorType(obj types.Object) bool { } // importSpec handles positions inside of an *ast.ImportSpec. -func importSpec(snapshot Snapshot, pkg Package, file *ast.File, pos token.Pos) (*IdentifierInfo, error) { +func importSpec(snapshot Snapshot, pkg Package, pgf *ParsedGoFile, pos token.Pos) (*IdentifierInfo, error) { var imp *ast.ImportSpec - for _, spec := range file.Imports { + for _, spec := range pgf.File.Imports { if spec.Path.Pos() <= pos && pos < spec.Path.End() { imp = spec } @@ -461,6 +486,7 @@ func importSpec(snapshot Snapshot, pkg Package, file *ast.File, pos token.Pos) ( } result.Declaration.node = imp + result.Declaration.nodeFile = pgf return result, nil } diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index c7529e89e37..8125b689b86 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -72,9 +72,13 @@ func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protoco } for _, imp := range f.File.Imports { if rdep.DepsByImpPath[UnquoteImportPath(imp)] == targetPkg.ID { + rng, err := f.PosMappedRange(imp.Pos(), imp.End()) + if err != nil { + return nil, err + } refs = append(refs, &ReferenceInfo{ Name: pgf.File.Name.Name, - MappedRange: NewMappedRange(f, imp.Pos(), imp.End()), + MappedRange: rng, }) } } @@ -91,9 +95,13 @@ func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protoco if err != nil { return nil, err } + rng, err := f.PosMappedRange(f.File.Name.Pos(), f.File.Name.End()) + if err != nil { + return nil, err + } refs = append(refs, &ReferenceInfo{ Name: pgf.File.Name.Name, - MappedRange: NewMappedRange(f, f.File.Name.Pos(), f.File.Name.End()), + MappedRange: rng, }) } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 0b7f3e44e8e..691f00227c9 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -23,43 +23,31 @@ import ( "golang.org/x/tools/internal/typeparams" ) -// A MappedRange represents an interval within a parsed Go file and -// has the ability to convert to protocol.Range or span.Span form. +// A MappedRange represents a byte-offset interval within a file. +// Through the ColumnMapper it can be converted into other forms such +// as protocol.Range or span.Span. // -// TOOD(adonovan): eliminate this type by inlining it: make callers -// hold the triple themselves, or convert to Mapper + start/end offsets -// and hold that. +// Call ParsedGoFile.MappedPosRange to construct from the go/token domain. type MappedRange struct { - // TODO(adonovan): eliminate sole tricky direct use of this, - // which is entangled with IdentifierInfo.Declaration.node. - File *ParsedGoFile - start, end token.Pos + Mapper *protocol.ColumnMapper + Start, End int // byte offsets } -// NewMappedRange returns a MappedRange for the given file and -// start/end positions, which must be valid within the file. -func NewMappedRange(pgf *ParsedGoFile, start, end token.Pos) MappedRange { - _ = span.NewRange(pgf.Tok, start, end) // just for assertions - return MappedRange{File: pgf, start: start, end: end} -} +// -- convenience functions -- -// Range returns the LSP range in the edited source. -func (s MappedRange) Range() (protocol.Range, error) { - return s.File.PosRange(s.start, s.end) +// Range returns the range in protocol form. +func (mr MappedRange) Range() (protocol.Range, error) { + return mr.Mapper.OffsetRange(mr.Start, mr.End) } -// Span returns the span corresponding to the mapped range in the edited source. -func (s MappedRange) Span() (span.Span, error) { - start, end, err := safetoken.Offsets(s.File.Tok, s.start, s.end) - if err != nil { - return span.Span{}, err - } - return s.File.Mapper.OffsetSpan(start, end) +// Span returns the range in span form. +func (mr MappedRange) Span() (span.Span, error) { + return mr.Mapper.OffsetSpan(mr.Start, mr.End) } -// URI returns the URI of the edited file. -func (s MappedRange) URI() span.URI { - return s.File.Mapper.URI +// URI returns the URI of the range's file. +func (mr MappedRange) URI() span.URI { + return mr.Mapper.URI } func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { @@ -123,7 +111,7 @@ func posToMappedRange(pkg Package, pos, end token.Pos) (MappedRange, error) { if err != nil { return MappedRange{}, err } - return NewMappedRange(pgf, pos, end), nil + return pgf.PosMappedRange(pos, end) } // FindPackageFromPos returns the Package for the given position, which must be diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 21e99697f4e..003e8ff5ce9 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -399,6 +399,16 @@ func (pgf *ParsedGoFile) PosRange(start, end token.Pos) (protocol.Range, error) return pgf.Mapper.OffsetRange(startOffset, endOffset) } +// PosMappedRange returns a MappedRange for the token.Pos interval in this file. +// A MappedRange can be converted to any other form. +func (pgf *ParsedGoFile) PosMappedRange(startPos, endPos token.Pos) (MappedRange, error) { + start, end, err := safetoken.Offsets(pgf.Tok, startPos, endPos) + if err != nil { + return MappedRange{}, nil + } + return MappedRange{pgf.Mapper, start, end}, nil +} + // RangeToSpanRange parses a protocol Range back into the go/token domain. func (pgf *ParsedGoFile) RangeToSpanRange(r protocol.Range) (span.Range, error) { spn, err := pgf.Mapper.RangeSpan(r) From 057ed3c5c8516ebc640e0b4011f5a8c800a69d61 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 5 Jan 2023 14:34:08 -0500 Subject: [PATCH 584/723] gopls/internal/lsp/filecache: use os.Chtimes It works on Plan9, UNIX, and even Windows. Who knew? GOROOT/src/os/file_posix.go says: //go:build unix || (js && wasm) || windows I think Windows must have changed quite a bit since I last used it properly. Fixes golang/go#57629 Change-Id: Ice2f901f82e4b105f3ab4c7ccabc98e0392e211c Reviewed-on: https://go-review.googlesource.com/c/tools/+/460816 Reviewed-by: Bryan Mills Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/filecache/filecache.go | 7 +++- .../lsp/filecache/setfiletime_unix.go | 28 ---------------- .../lsp/filecache/setfiletime_windows.go | 33 ------------------- 3 files changed, 6 insertions(+), 62 deletions(-) delete mode 100644 gopls/internal/lsp/filecache/setfiletime_unix.go delete mode 100644 gopls/internal/lsp/filecache/setfiletime_windows.go diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go index 15160c7c2a6..3338adeaa4b 100644 --- a/gopls/internal/lsp/filecache/filecache.go +++ b/gopls/internal/lsp/filecache/filecache.go @@ -52,8 +52,13 @@ func Get(kind string, key [32]byte) ([]byte, error) { // (This turns every read into a write operation. // If this is a performance problem, we should // touch the files aynchronously.) + // + // (Traditionally the access time would be updated + // automatically, but for efficiency most POSIX systems have + // for many years set the noatime mount option to avoid every + // open or read operation entailing a metadata write.) now := time.Now() - if err := setFileTime(name, now, now); err != nil { + if err := os.Chtimes(name, now, now); err != nil { return nil, fmt.Errorf("failed to update access time: %w", err) } diff --git a/gopls/internal/lsp/filecache/setfiletime_unix.go b/gopls/internal/lsp/filecache/setfiletime_unix.go deleted file mode 100644 index f4f25f5121e..00000000000 --- a/gopls/internal/lsp/filecache/setfiletime_unix.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !windows -// +build !windows - -// TODO(adonovan): use 'unix' tag when we can rely on newer go command. - -package filecache - -import ( - "syscall" - "time" -) - -// setFileTime updates the access and modification times of a file. -// -// (Traditionally the access time would be updated automatically, but -// for efficiency most POSIX systems have for many years set the -// noatime mount option to avoid every open or read operation -// entailing a metadata write.) -func setFileTime(filename string, atime, mtime time.Time) error { - return syscall.Utimes(filename, []syscall.Timeval{ - syscall.NsecToTimeval(atime.UnixNano()), - syscall.NsecToTimeval(mtime.UnixNano()), - }) -} diff --git a/gopls/internal/lsp/filecache/setfiletime_windows.go b/gopls/internal/lsp/filecache/setfiletime_windows.go deleted file mode 100644 index 72a63491fff..00000000000 --- a/gopls/internal/lsp/filecache/setfiletime_windows.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build windows -// +build windows - -package filecache - -import ( - "syscall" - "time" -) - -// setFileTime updates the access and modification times of a file. -func setFileTime(filename string, atime, mtime time.Time) error { - // Latency of this function was measured on the builder - // at median=1.9ms 90%=6.8ms 95%=12ms. - - filename16, err := syscall.UTF16PtrFromString(filename) - if err != nil { - return err - } - // See https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfiletime - h, err := syscall.CreateFile(filename16, syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil, syscall.OPEN_EXISTING, 0, 0) - if err != nil { - return err - } - defer syscall.Close(h) // ignore error - afiletime := syscall.NsecToFiletime(atime.UnixNano()) - mfiletime := syscall.NsecToFiletime(mtime.UnixNano()) - return syscall.SetFileTime(h, nil, &afiletime, &mfiletime) -} From e225fd43c174ac16ee68b0421ae9476ba98c54b2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 5 Jan 2023 18:27:57 -0500 Subject: [PATCH 585/723] gopls/internal/lsp/mod: fix nil panic in go.mod hover The logic now handles a missing module directive. Fixes golang/go#57625 Change-Id: I8315c42441178bbda6770b48e5511ffbb4d53146 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460858 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro Auto-Submit: Alan Donovan Reviewed-by: Robert Findley --- gopls/internal/lsp/mod/hover.go | 10 +++++++--- gopls/internal/regtest/misc/hover_test.go | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 34173e8d89f..ef7c235b7ef 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -125,11 +125,15 @@ func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offse } func hoverOnModuleStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh source.FileHandle) (*protocol.Hover, bool) { - if offset < pm.File.Module.Syntax.Start.Byte || offset > pm.File.Module.Syntax.End.Byte { - return nil, false + module := pm.File.Module + if module == nil { + return nil, false // no module stmt + } + if offset < module.Syntax.Start.Byte || offset > module.Syntax.End.Byte { + return nil, false // cursor not in module stmt } - rng, err := pm.Mapper.OffsetRange(pm.File.Module.Syntax.Start.Byte, pm.File.Module.Syntax.End.Byte) + rng, err := pm.Mapper.OffsetRange(module.Syntax.Start.Byte, module.Syntax.End.Byte) if err != nil { return nil, false } diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go index d3db2572ec8..dafcc62037b 100644 --- a/gopls/internal/regtest/misc/hover_test.go +++ b/gopls/internal/regtest/misc/hover_test.go @@ -253,3 +253,15 @@ package lib }) }) } + +// This is a regression test for Go issue #57625. +func TestHoverModMissingModuleStmt(t *testing.T) { + const source = ` +-- go.mod -- +go 1.16 +` + Run(t, source, func(t *testing.T, env *Env) { + env.OpenFile("go.mod") + env.Hover("go.mod", env.RegexpSearch("go.mod", "go")) // no panic + }) +} From 107f43f6698a7e0880a5bbffdab2ff65eb0d9434 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Sat, 31 Dec 2022 09:32:43 -0500 Subject: [PATCH 586/723] gopls/completion: avoid duplicating text in test func completions If a user types 'func Te' in a test file, gopls suggest a completion using a snippet 'TestXxx(t *testing.T)' which will fill in the entire function declaration. Until this CL it would also suggest the snippet if the user had type 'func Te(t *testing.T)', and went back to fill in the function name. This CL fixes that by no longer suggesting the snippet if the user has already typed a parenthesis. Fixes: golang/go#57480 Change-Id: I5061a6ba5ca22ecba60de537a41fcc04356dc7ba Reviewed-on: https://go-review.googlesource.com/c/tools/+/459559 Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/regtest/wrappers.go | 2 +- .../lsp/source/completion/definition.go | 66 +++++++++++++----- .../regtest/completion/completion18_test.go | 4 +- .../regtest/completion/completion_test.go | 68 +++++++++---------- 4 files changed, 85 insertions(+), 55 deletions(-) diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index 19713f24635..fe80d89fc87 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -138,7 +138,7 @@ func (e *Env) RegexpSearch(name, re string) fake.Pos { pos, err = e.Sandbox.Workdir.RegexpSearch(name, re) } if err != nil { - e.T.Fatalf("RegexpSearch: %v, %v", name, err) + e.T.Fatalf("RegexpSearch: %v, %v for %q", name, err, re) } return pos } diff --git a/gopls/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go index cc7b42e81a2..bf14ad4a64b 100644 --- a/gopls/internal/lsp/source/completion/definition.go +++ b/gopls/internal/lsp/source/completion/definition.go @@ -18,7 +18,7 @@ import ( "golang.org/x/tools/gopls/internal/span" ) -// some definitions can be completed +// some function definitions in test files can be completed // So far, TestFoo(t *testing.T), TestMain(m *testing.M) // BenchmarkFoo(b *testing.B), FuzzFoo(f *testing.F) @@ -28,7 +28,7 @@ func definition(path []ast.Node, obj types.Object, tokFile *token.File, fh sourc return nil, nil // not a function at all } if !strings.HasSuffix(fh.URI().Filename(), "_test.go") { - return nil, nil + return nil, nil // not a test file } name := path[0].(*ast.Ident).Name @@ -44,21 +44,49 @@ func definition(path []ast.Node, obj types.Object, tokFile *token.File, fh sourc rng: span.NewRange(tokFile, start, end), } var ans []CompletionItem + var hasParens bool + n, ok := path[1].(*ast.FuncDecl) + if !ok { + return nil, nil // can't happen + } + if n.Recv != nil { + return nil, nil // a method, not a function + } + t := n.Type.Params + if t.Closing != t.Opening { + hasParens = true + } // Always suggest TestMain, if possible if strings.HasPrefix("TestMain", name) { - ans = []CompletionItem{defItem("TestMain(m *testing.M)", obj)} + if hasParens { + ans = append(ans, defItem("TestMain", obj)) + } else { + ans = append(ans, defItem("TestMain(m *testing.M)", obj)) + } } // If a snippet is possible, suggest it if strings.HasPrefix("Test", name) { - ans = append(ans, defSnippet("Test", "Xxx", "(t *testing.T)", obj)) + if hasParens { + ans = append(ans, defItem("Test", obj)) + } else { + ans = append(ans, defSnippet("Test", "(t *testing.T)", obj)) + } return ans, sel } else if strings.HasPrefix("Benchmark", name) { - ans = append(ans, defSnippet("Benchmark", "Xxx", "(b *testing.B)", obj)) + if hasParens { + ans = append(ans, defItem("Benchmark", obj)) + } else { + ans = append(ans, defSnippet("Benchmark", "(b *testing.B)", obj)) + } return ans, sel } else if strings.HasPrefix("Fuzz", name) { - ans = append(ans, defSnippet("Fuzz", "Xxx", "(f *testing.F)", obj)) + if hasParens { + ans = append(ans, defItem("Fuzz", obj)) + } else { + ans = append(ans, defSnippet("Fuzz", "(f *testing.F)", obj)) + } return ans, sel } @@ -73,9 +101,9 @@ func definition(path []ast.Node, obj types.Object, tokFile *token.File, fh sourc return ans, sel } +// defMatches returns text for defItem, never for defSnippet func defMatches(name, pat string, path []ast.Node, arg string) string { - idx := strings.Index(name, pat) - if idx < 0 { + if !strings.HasPrefix(name, pat) { return "" } c, _ := utf8.DecodeRuneInString(name[len(pat):]) @@ -88,25 +116,27 @@ func defMatches(name, pat string, path []ast.Node, arg string) string { return "" } fp := fd.Type.Params - if fp != nil && len(fp.List) > 0 { - // signature already there, minimal suggestion - return name + if len(fp.List) > 0 { + // signature already there, nothing to suggest + return "" + } + if fp.Opening != fp.Closing { + // nothing: completion works on words, not easy to insert arg + return "" } // suggesting signature too return name + arg } -func defSnippet(prefix, placeholder, suffix string, obj types.Object) CompletionItem { +func defSnippet(prefix, suffix string, obj types.Object) CompletionItem { var sn snippet.Builder sn.WriteText(prefix) - if placeholder != "" { - sn.WritePlaceholder(func(b *snippet.Builder) { b.WriteText(placeholder) }) - } - sn.WriteText(suffix + " {\n") + sn.WritePlaceholder(func(b *snippet.Builder) { b.WriteText("Xxx") }) + sn.WriteText(suffix + " {\n\t") sn.WriteFinalTabstop() sn.WriteText("\n}") return CompletionItem{ - Label: prefix + placeholder + suffix, + Label: prefix + "Xxx" + suffix, Detail: "tab, type the rest of the name, then tab", Kind: protocol.FunctionCompletion, Depth: 0, @@ -123,7 +153,7 @@ func defItem(val string, obj types.Object) CompletionItem { Kind: protocol.FunctionCompletion, Depth: 0, Score: 9, // prefer the snippets when available - Documentation: "complete the parameter", + Documentation: "complete the function name", obj: obj, } } diff --git a/gopls/internal/regtest/completion/completion18_test.go b/gopls/internal/regtest/completion/completion18_test.go index 7c532529c7b..936436b9296 100644 --- a/gopls/internal/regtest/completion/completion18_test.go +++ b/gopls/internal/regtest/completion/completion18_test.go @@ -44,7 +44,7 @@ func (s SyncMap[XX,string]) g(v UU) {} pos := env.RegexpSearch("main.go", tst.pat) pos.Column += len(tst.pat) completions := env.Completion("main.go", pos) - result := compareCompletionResults(tst.want, completions.Items) + result := compareCompletionLabels(tst.want, completions.Items) if result != "" { t.Errorf("%s: wanted %v", result, tst.want) for i, g := range completions.Items { @@ -111,7 +111,7 @@ func FuzzHex(f *testing.F) { pos := env.RegexpSearch(test.file, test.pat) pos.Column += test.offset // character user just typed? will type? completions := env.Completion(test.file, pos) - result := compareCompletionResults(test.want, completions.Items) + result := compareCompletionLabels(test.want, completions.Items) if result != "" { t.Errorf("pat %q %q", test.pat, result) for i, it := range completions.Items { diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 08a14215e94..5f655939b2f 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -207,7 +207,7 @@ package } } - diff := compareCompletionResults(tc.want, completions.Items) + diff := compareCompletionLabels(tc.want, completions.Items) if diff != "" { t.Error(diff) } @@ -234,14 +234,14 @@ package ma Column: 10, }) - diff := compareCompletionResults(want, completions.Items) + diff := compareCompletionLabels(want, completions.Items) if diff != "" { t.Fatal(diff) } }) } -func compareCompletionResults(want []string, gotItems []protocol.CompletionItem) string { +func compareCompletionLabels(want []string, gotItems []protocol.CompletionItem) string { if len(gotItems) != len(want) { return fmt.Sprintf("got %v completion(s), want %v", len(gotItems), len(want)) } @@ -391,7 +391,7 @@ type S struct { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo.go") completions := env.Completion("foo.go", env.RegexpSearch("foo.go", `if s\.()`)) - diff := compareCompletionResults([]string{"i"}, completions.Items) + diff := compareCompletionLabels([]string{"i"}, completions.Items) if diff != "" { t.Fatal(diff) } @@ -451,7 +451,7 @@ func _() { } for _, tt := range tests { completions := env.Completion("main.go", env.RegexpSearch("main.go", tt.re)) - diff := compareCompletionResults(tt.want, completions.Items) + diff := compareCompletionLabels(tt.want, completions.Items) if diff != "" { t.Errorf("%s: %s", tt.re, diff) } @@ -486,7 +486,7 @@ func doit() { pos := env.RegexpSearch("prog.go", "if fooF") pos.Column += len("if fooF") completions := env.Completion("prog.go", pos) - diff := compareCompletionResults([]string{"fooFunc"}, completions.Items) + diff := compareCompletionLabels([]string{"fooFunc"}, completions.Items) if diff != "" { t.Error(diff) } @@ -496,7 +496,7 @@ func doit() { pos = env.RegexpSearch("prog.go", "= badP") pos.Column += len("= badP") completions = env.Completion("prog.go", pos) - diff = compareCompletionResults([]string{"badPi"}, completions.Items) + diff = compareCompletionLabels([]string{"badPi"}, completions.Items) if diff != "" { t.Error(diff) } @@ -545,6 +545,7 @@ func main() { } func TestDefinition(t *testing.T) { + testenv.NeedsGo1Point(t, 17) // in go1.16, The FieldList in func x is not empty stuff := ` -- go.mod -- module mod.com @@ -552,43 +553,42 @@ module mod.com go 1.18 -- a_test.go -- package foo -func T() -func TestG() -func TestM() -func TestMi() -func Ben() -func Fuz() -func Testx() -func TestMe(t *testing.T) -func BenchmarkFoo() ` - // All those parentheses are needed for the completion code to see - // later lines as being definitions tests := []struct { - pat string - want []string + line string // the sole line in the buffer after the package statement + pat string // the pattern to search for + want []string // expected competions }{ - {"T", []string{"TestXxx(t *testing.T)", "TestMain(m *testing.M)"}}, - {"TestM", []string{"TestMain(m *testing.M)", "TestM(t *testing.T)"}}, - {"TestMi", []string{"TestMi(t *testing.T)"}}, - {"TestG", []string{"TestG(t *testing.T)"}}, - {"B", []string{"BenchmarkXxx(b *testing.B)"}}, - {"BenchmarkFoo", []string{"BenchmarkFoo(b *testing.B)"}}, - {"F", []string{"FuzzXxx(f *testing.F)"}}, - {"Testx", nil}, - {"TestMe", []string{"TestMe"}}, + {"func T", "T", []string{"TestXxx(t *testing.T)", "TestMain(m *testing.M)"}}, + {"func T()", "T", []string{"TestMain", "Test"}}, + {"func TestM", "TestM", []string{"TestMain(m *testing.M)", "TestM(t *testing.T)"}}, + {"func TestM()", "TestM", []string{"TestMain"}}, + {"func TestMi", "TestMi", []string{"TestMi(t *testing.T)"}}, + {"func TestMi()", "TestMi", nil}, + {"func TestG", "TestG", []string{"TestG(t *testing.T)"}}, + {"func TestG(", "TestG", nil}, + {"func Ben", "B", []string{"BenchmarkXxx(b *testing.B)"}}, + {"func Ben(", "Ben", []string{"Benchmark"}}, + {"func BenchmarkFoo", "BenchmarkFoo", []string{"BenchmarkFoo(b *testing.B)"}}, + {"func BenchmarkFoo(", "BenchmarkFoo", nil}, + {"func Fuz", "F", []string{"FuzzXxx(f *testing.F)"}}, + {"func Fuz(", "Fuz", []string{"Fuzz"}}, + {"func Testx", "Testx", nil}, + {"func TestMe(t *testing.T)", "TestMe", nil}, + {"func Te(t *testing.T)", "Te", []string{"TestMain", "Test"}}, } fname := "a_test.go" Run(t, stuff, func(t *testing.T, env *Env) { env.OpenFile(fname) env.Await(env.DoneWithOpen()) for _, tst := range tests { + env.SetBufferContent(fname, "package foo\n"+tst.line) pos := env.RegexpSearch(fname, tst.pat) pos.Column += len(tst.pat) completions := env.Completion(fname, pos) - result := compareCompletionResults(tst.want, completions.Items) + result := compareCompletionLabels(tst.want, completions.Items) if result != "" { - t.Errorf("%s failed: %s:%q", tst.pat, result, tst.want) + t.Errorf("\npat:%q line:%q failed: %s:%q", tst.pat, tst.line, result, tst.want) for i, it := range completions.Items { t.Errorf("%d got %q %q", i, it.Label, it.Detail) } @@ -651,7 +651,7 @@ func Bench package foo_test func Benchmark${1:Xxx}(b *testing.B) { -$0 + $0 \} `, }, @@ -675,7 +675,7 @@ $0 env.AcceptCompletion("foo_test.go", pos, completions.Items[0]) env.Await(env.DoneWithChange()) if buf := env.BufferText("foo_test.go"); buf != tst.after { - t.Errorf("incorrect completion: got %q, want %q", buf, tst.after) + t.Errorf("%s:incorrect completion: got %q, want %q", tst.name, buf, tst.after) } } }) @@ -718,7 +718,7 @@ use ./dir/foobar/ } for _, tt := range tests { completions := env.Completion("go.work", env.RegexpSearch("go.work", tt.re)) - diff := compareCompletionResults(tt.want, completions.Items) + diff := compareCompletionLabels(tt.want, completions.Items) if diff != "" { t.Errorf("%s: %s", tt.re, diff) } From 4ded35d13b4b5f9f6d57bf90f4f31720d54f837d Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 5 Jan 2023 17:55:55 -0500 Subject: [PATCH 587/723] gopls/internal/lsp/cache: use distinct types for mod and work parse keys Change-Id: Ic75b5dddb266e601c3f5e496a48a7389da4a1330 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460857 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/mod.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index a3d207d6005..55665395986 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -32,6 +32,7 @@ func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source. entry, hit := s.parseModHandles.Get(uri) s.mu.Unlock() + type parseModKey source.FileIdentity type parseModResult struct { parsed *source.ParsedModule err error @@ -39,7 +40,7 @@ func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source. // cache miss? if !hit { - promise, release := s.store.Promise(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { + promise, release := s.store.Promise(parseModKey(fh.FileIdentity()), func(ctx context.Context, _ interface{}) interface{} { parsed, err := parseModImpl(ctx, fh) return parseModResult{parsed, err} }) @@ -109,6 +110,7 @@ func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source entry, hit := s.parseWorkHandles.Get(uri) s.mu.Unlock() + type parseWorkKey source.FileIdentity type parseWorkResult struct { parsed *source.ParsedWorkFile err error @@ -116,7 +118,7 @@ func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source // cache miss? if !hit { - handle, release := s.store.Promise(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { + handle, release := s.store.Promise(parseWorkKey(fh.FileIdentity()), func(ctx context.Context, _ interface{}) interface{} { parsed, err := parseWorkImpl(ctx, fh) return parseWorkResult{parsed, err} }) From 09cbc42199ecaa823320448362e9005ad7a535f6 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 5 Jan 2023 17:41:40 -0500 Subject: [PATCH 588/723] gopls/internal/lsp/fake: fix EOF bug in applyEdits The previous logic would map EOF to a byte offset one beyond the actual file length. Fixed golang/go#57627 Change-Id: I6b2e33e1195bbf567752c0e13164e234fd1457a6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460856 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro Auto-Submit: Alan Donovan --- gopls/internal/lsp/fake/edit.go | 31 ++++++++++++------- gopls/internal/lsp/fake/edit_test.go | 17 ++++++++++ .../internal/regtest/modfile/modfile_test.go | 14 +++++++++ 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go index 3eb13ea2f4c..7f15a41c1cf 100644 --- a/gopls/internal/lsp/fake/edit.go +++ b/gopls/internal/lsp/fake/edit.go @@ -114,26 +114,33 @@ func applyEdits(lines []string, edits []Edit) ([]string, error) { src := strings.Join(lines, "\n") // Build a table of byte offset of start of each line. - lineOffset := make([]int, len(lines)+1) + lineOffset := make([]int, len(lines)) offset := 0 for i, line := range lines { lineOffset[i] = offset offset += len(line) + len("\n") } - lineOffset[len(lines)] = offset // EOF - var badCol error + var posErr error posToOffset := func(pos Pos) int { - offset := lineOffset[pos.Line] // Convert pos.Column (runes) to a UTF-8 byte offset. - if pos.Line < len(lines) { - for i := 0; i < pos.Column; i++ { - r, sz := utf8.DecodeRuneInString(src[offset:]) - if r == '\n' && badCol == nil { - badCol = fmt.Errorf("bad column") - } - offset += sz + if pos.Line > len(lines) { + posErr = fmt.Errorf("bad line") + return 0 + } + if pos.Line == len(lines) { + if pos.Column > 0 { + posErr = fmt.Errorf("bad column") + } + return len(src) // EOF + } + offset := lineOffset[pos.Line] + for i := 0; i < pos.Column; i++ { + r, sz := utf8.DecodeRuneInString(src[offset:]) + if r == '\n' && posErr == nil { + posErr = fmt.Errorf("bad column") } + offset += sz } return offset } @@ -153,5 +160,5 @@ func applyEdits(lines []string, edits []Edit) ([]string, error) { return nil, err } - return strings.Split(patched, "\n"), badCol + return strings.Split(patched, "\n"), posErr } diff --git a/gopls/internal/lsp/fake/edit_test.go b/gopls/internal/lsp/fake/edit_test.go index f87d9210336..fc4211739b0 100644 --- a/gopls/internal/lsp/fake/edit_test.go +++ b/gopls/internal/lsp/fake/edit_test.go @@ -46,6 +46,23 @@ func TestApplyEdits(t *testing.T) { }}, want: "ABC\nD12\n345\nJKL", }, + { + label: "regression test for issue #57627", + content: "go 1.18\nuse moda/a", + edits: []Edit{ + { + Start: Pos{Line: 1, Column: 0}, + End: Pos{Line: 1, Column: 0}, + Text: "\n", + }, + { + Start: Pos{Line: 2, Column: 0}, + End: Pos{Line: 2, Column: 0}, + Text: "\n", + }, + }, + want: "go 1.18\n\nuse moda/a\n", + }, { label: "end before start", content: "ABC\nDEF\nGHI\nJKL", diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 08ab21e0f59..1d2ade2fc9a 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -1185,3 +1185,17 @@ package main env.Await(EmptyDiagnostics("go.mod")) }) } + +// This is a regression test for a bug in the line-oriented implementation +// of the "apply diffs" operation used by the fake editor. +func TestIssue57627(t *testing.T) { + const files = ` +-- go.work -- +package main +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("go.work") + env.SetBufferContent("go.work", "go 1.18\nuse moda/a") + env.SaveBuffer("go.work") // doesn't fail + }) +} From 3856a5d80cf9ed9505c189333d753350aaaffcd2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 5 Jan 2023 16:17:44 -0500 Subject: [PATCH 589/723] internal/robustio: add Plan9 support to FileID Fixes golang/go#57642 Change-Id: I269162017b78e9009d60aeca87255a57611fcd4b Reviewed-on: https://go-review.googlesource.com/c/tools/+/460855 gopls-CI: kokoro TryBot-Bypass: Bryan Mills Run-TryBot: Alan Donovan Reviewed-by: Bryan Mills --- internal/robustio/robustio_plan9.go | 25 +++++++++++++++ internal/robustio/robustio_posix.go | 2 ++ internal/robustio/robustio_test.go | 47 +++++++++++++++-------------- 3 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 internal/robustio/robustio_plan9.go diff --git a/internal/robustio/robustio_plan9.go b/internal/robustio/robustio_plan9.go new file mode 100644 index 00000000000..ba1255f777e --- /dev/null +++ b/internal/robustio/robustio_plan9.go @@ -0,0 +1,25 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build plan9 +// +build plan9 + +package robustio + +import ( + "os" + "syscall" +) + +func getFileID(filename string) (FileID, error) { + fi, err := os.Stat(filename) + if err != nil { + return FileID{}, err + } + dir := fi.Sys().(*syscall.Dir) + return FileID{ + device: uint64(dir.Type)<<32 | uint64(dir.Dev), + inode: dir.Qid.Path, + }, nil +} diff --git a/internal/robustio/robustio_posix.go b/internal/robustio/robustio_posix.go index f22d0e9ed23..175a6b49ba0 100644 --- a/internal/robustio/robustio_posix.go +++ b/internal/robustio/robustio_posix.go @@ -5,6 +5,8 @@ //go:build !windows && !plan9 // +build !windows,!plan9 +// TODO(adonovan): use 'unix' tag when go1.19 can be assumed. + package robustio import ( diff --git a/internal/robustio/robustio_test.go b/internal/robustio/robustio_test.go index 5f9fcd9a68f..ed49bff7318 100644 --- a/internal/robustio/robustio_test.go +++ b/internal/robustio/robustio_test.go @@ -7,6 +7,7 @@ package robustio_test import ( "os" "path/filepath" + "runtime" "testing" "golang.org/x/tools/internal/robustio" @@ -43,28 +44,30 @@ func TestFileID(t *testing.T) { } // A symbolic link has the same ID as its target. - symlink := filepath.Join(t.TempDir(), "symlink") - if err := os.Symlink(real, symlink); err != nil { - t.Fatalf("can't create symbolic link: %v", err) - } - symlinkID, err := robustio.GetFileID(symlink) - if err != nil { - t.Fatalf("can't get ID of symbolic link: %v", err) - } - if realID != symlinkID { - t.Errorf("realID %+v != symlinkID %+v", realID, symlinkID) - } + if runtime.GOOS != "plan9" { + symlink := filepath.Join(t.TempDir(), "symlink") + if err := os.Symlink(real, symlink); err != nil { + t.Fatalf("can't create symbolic link: %v", err) + } + symlinkID, err := robustio.GetFileID(symlink) + if err != nil { + t.Fatalf("can't get ID of symbolic link: %v", err) + } + if realID != symlinkID { + t.Errorf("realID %+v != symlinkID %+v", realID, symlinkID) + } - // Two hard-linked files have the same ID. - hardlink := filepath.Join(t.TempDir(), "hardlink") - if err := os.Link(real, hardlink); err != nil { - t.Fatal(err) - } - hardlinkID, err := robustio.GetFileID(hardlink) - if err != nil { - t.Fatalf("can't get ID of hard link: %v", err) - } - if hardlinkID != realID { - t.Errorf("realID %+v != hardlinkID %+v", realID, hardlinkID) + // Two hard-linked files have the same ID. + hardlink := filepath.Join(t.TempDir(), "hardlink") + if err := os.Link(real, hardlink); err != nil { + t.Fatal(err) + } + hardlinkID, err := robustio.GetFileID(hardlink) + if err != nil { + t.Fatalf("can't get ID of hard link: %v", err) + } + if hardlinkID != realID { + t.Errorf("realID %+v != hardlinkID %+v", realID, hardlinkID) + } } } From 16b3bf8d47f0e75c635df3d23637acebc2787aa2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 6 Jan 2023 11:35:54 -0500 Subject: [PATCH 590/723] gopls/internal/lsp/cache: assume Go 1.16+ Now that we no longer support Go versions prior to Go 1.16, clean up some logic handling older Go versions. Change-Id: I906b0f8e6abaa7d6a1b75321d48aad15f149e166 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460916 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/check.go | 3 --- gopls/internal/lsp/cache/snapshot.go | 9 ++------- gopls/internal/lsp/cache/view.go | 17 +++++++---------- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index c52a2f8f56c..b476137b304 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -634,9 +634,6 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost // Up to Go 1.15, the missing package was included in the stack, which // was presumably a bug. We want the next one up. directImporterIdx := len(depsError.ImportStack) - 1 - if s.view.goversion < 15 { - directImporterIdx = len(depsError.ImportStack) - 2 - } if directImporterIdx < 0 { continue } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index b7bdaddc184..76350d5944e 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -328,8 +328,7 @@ func (s *snapshot) workspaceMode() workspaceMode { } mode |= moduleMode options := s.view.Options() - // The -modfile flag is available for Go versions >= 1.14. - if options.TempModfile && s.view.workspaceInformation.goversion >= 14 { + if options.TempModfile { mode |= tempModfile } return mode @@ -539,13 +538,9 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return "", nil, cleanup, err } - mutableModFlag := "" + const mutableModFlag = "mod" // If the mod flag isn't set, populate it based on the mode and workspace. if inv.ModFlag == "" { - if s.view.goversion >= 16 { - mutableModFlag = "mod" - } - switch mode { case source.LoadWorkspace, source.Normal: if vendorEnabled { diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 14536e1cc6f..a873b50ef14 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -141,19 +141,16 @@ type workspaceInformation struct { } // effectiveGO111MODULE reports the value of GO111MODULE effective in the go -// command at this go version, accounting for default values at different go -// versions. +// command at this go version, assuming at least Go 1.16. func (w workspaceInformation) effectiveGO111MODULE() go111module { - // Off by default until Go 1.12. - go111module := w.GO111MODULE() - if go111module == "off" || (w.goversion < 12 && go111module == "") { + switch w.GO111MODULE() { + case "off": return off - } - // On by default as of Go 1.16. - if go111module == "on" || (w.goversion >= 16 && go111module == "") { + case "on", "": return on + default: + return auto } - return auto } // GO111MODULE returns the value of GO111MODULE to use for running the go @@ -167,7 +164,7 @@ func (w workspaceInformation) effectiveGO111MODULE() go111module { // Put differently: we shouldn't go out of our way to make GOPATH work, when // the go command does not. func (w workspaceInformation) GO111MODULE() string { - if w.goversion >= 16 && w.go111module == "" { + if w.go111module == "" { return "auto" } return w.go111module From 36bd3dbccc80aa62a73b1d9325721d2d1916d48b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 6 Jan 2023 09:58:42 -0500 Subject: [PATCH 591/723] gopls/internal/lsp/protocol: move MappedRange It was formerly in the source package; now it lives adjacent to ColumnMapper, to which it is closely related. Also, encapsulate its start/end offsets and establish the invariant that they are always valid by defining a constructor method, ColumnMapper.OffsetMappedRange. A TODO comment sketches a possible next step: make its conversion methods infallible. Change-Id: I1e148ccdb37a762c0c387a77240a41237612e2cc Reviewed-on: https://go-review.googlesource.com/c/tools/+/460915 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/protocol/span.go | 57 ++++++++++++++++++++-- gopls/internal/lsp/source/folding_range.go | 2 +- gopls/internal/lsp/source/identifier.go | 8 +-- gopls/internal/lsp/source/references.go | 2 +- gopls/internal/lsp/source/util.go | 41 ++++------------ gopls/internal/lsp/source/view.go | 6 +-- 6 files changed, 73 insertions(+), 43 deletions(-) diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 72bd4c1dadd..c518c8047d3 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -6,14 +6,13 @@ // Here's a handy guide for your tour of the location zoo: // -// Imports: source --> lsppos --> protocol --> span --> token -// -// source.MappedRange = (protocol.ColumnMapper, {start,end}Offset int) +// Imports: lsppos --> protocol --> span --> token // // lsppos.TokenMapper = (token.File, lsppos.Mapper) // lsppos.Mapper = (line offset table, content) // // protocol.ColumnMapper = (URI, Content). Does all offset <=> column conversions. +// protocol.MappedRange = (protocol.ColumnMapper, {start,end} int) // protocol.Location = (URI, protocol.Range) // protocol.Range = (start, end Position) // protocol.Position = (line, char uint32) 0-based UTF-16 @@ -37,6 +36,10 @@ // - Replace all uses of lsppos.TokenMapper by the underlying ParsedGoFile, // which carries a token.File and a ColumnMapper. // - Then delete lsppos package. +// - ColumnMapper.OffsetPoint and .Position aren't used outside this package. +// OffsetSpan is barely used, and its user would better off with a MappedRange +// or protocol.Range. The span package data tyes are mostly used in tests +// and in argument parsing (without access to file content). package protocol @@ -287,6 +290,54 @@ func (m *ColumnMapper) Point(p Position) (span.Point, error) { return span.FromUTF16Column(lineStart, int(p.Character)+1, m.Content) } +// OffsetMappedRange returns a MappedRange for the given byte offsets. +// A MappedRange can be converted to any other form. +func (m *ColumnMapper) OffsetMappedRange(start, end int) (MappedRange, error) { + if !(0 <= start && start <= end && end <= len(m.Content)) { + return MappedRange{}, fmt.Errorf("invalid offsets (%d, %d) (file %s has size %d)", start, end, m.URI, len(m.Content)) + } + return MappedRange{m, start, end}, nil +} + +// A MappedRange represents a valid byte-offset range of a file. +// Through its ColumnMapper it can be converted into other forms such +// as protocol.Range or span.Span. +// +// Construct one by calling ColumnMapper.OffsetMappedRange with start/end offsets. +// From the go/token domain, call safetoken.Offsets first, +// or use a helper such as ParsedGoFile.MappedPosRange. +type MappedRange struct { + Mapper *ColumnMapper + start, end int // valid byte offsets +} + +// Offsets returns the (start, end) byte offsets of this range. +func (mr MappedRange) Offsets() (start, end int) { return mr.start, mr.end } + +// -- convenience functions -- + +// URI returns the URI of the range's file. +func (mr MappedRange) URI() span.URI { + return mr.Mapper.URI +} + +// TODO(adonovan): once the fluff is removed from all the +// location-conversion methods, it will be obvious that a properly +// constructed MappedRange is always valid and its Range and Span (and +// other) methods simply cannot fail. +// At that point we might want to provide variants of methods such as +// Range and Span below that don't return an error. + +// Range returns the range in protocol form. +func (mr MappedRange) Range() (Range, error) { + return mr.Mapper.OffsetRange(mr.start, mr.end) +} + +// Span returns the range in span form. +func (mr MappedRange) Span() (span.Span, error) { + return mr.Mapper.OffsetSpan(mr.start, mr.end) +} + func IsPoint(r Range) bool { return r.Start.Line == r.End.Line && r.Start.Character == r.End.Character } diff --git a/gopls/internal/lsp/source/folding_range.go b/gopls/internal/lsp/source/folding_range.go index 4be711cbea3..84c33af4843 100644 --- a/gopls/internal/lsp/source/folding_range.go +++ b/gopls/internal/lsp/source/folding_range.go @@ -17,7 +17,7 @@ import ( // FoldingRangeInfo holds range and kind info of folding for an ast.Node type FoldingRangeInfo struct { - MappedRange MappedRange + MappedRange protocol.MappedRange Kind protocol.FoldingRangeKind } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 94fb2fb32b7..626814f3ad1 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -26,10 +26,10 @@ import ( type IdentifierInfo struct { Name string Snapshot Snapshot // only needed for .View(); TODO(adonovan): reduce. - MappedRange MappedRange + MappedRange protocol.MappedRange Type struct { - MappedRange MappedRange + MappedRange protocol.MappedRange Object types.Object } @@ -54,7 +54,7 @@ func (i *IdentifierInfo) IsImport() bool { } type Declaration struct { - MappedRange []MappedRange + MappedRange []protocol.MappedRange // The typechecked node node ast.Node @@ -140,7 +140,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa Declaration: Declaration{ node: decl.File.Name, nodeFile: decl, - MappedRange: []MappedRange{declRng}, + MappedRange: []protocol.MappedRange{declRng}, }, }, nil } diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 8125b689b86..a85b757d696 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -24,7 +24,7 @@ import ( // ReferenceInfo holds information about reference to an identifier in Go source. type ReferenceInfo struct { Name string - MappedRange MappedRange + MappedRange protocol.MappedRange ident *ast.Ident obj types.Object pkg Package diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 691f00227c9..27ae8ff5c92 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -23,33 +23,12 @@ import ( "golang.org/x/tools/internal/typeparams" ) -// A MappedRange represents a byte-offset interval within a file. -// Through the ColumnMapper it can be converted into other forms such -// as protocol.Range or span.Span. +// IsGenerated gets and reads the file denoted by uri and reports +// whether it contains a "go:generated" directive as described at +// https://golang.org/s/generatedcode. // -// Call ParsedGoFile.MappedPosRange to construct from the go/token domain. -type MappedRange struct { - Mapper *protocol.ColumnMapper - Start, End int // byte offsets -} - -// -- convenience functions -- - -// Range returns the range in protocol form. -func (mr MappedRange) Range() (protocol.Range, error) { - return mr.Mapper.OffsetRange(mr.Start, mr.End) -} - -// Span returns the range in span form. -func (mr MappedRange) Span() (span.Span, error) { - return mr.Mapper.OffsetSpan(mr.Start, mr.End) -} - -// URI returns the URI of the range's file. -func (mr MappedRange) URI() span.URI { - return mr.Mapper.URI -} - +// TODO(adonovan): opt: this function does too much. +// Move snapshot.GetFile into the caller (most of which have already done it). func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { fh, err := snapshot.GetFile(ctx, uri) if err != nil { @@ -72,7 +51,7 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { return false } -func objToMappedRange(pkg Package, obj types.Object) (MappedRange, error) { +func objToMappedRange(pkg Package, obj types.Object) (protocol.MappedRange, error) { nameLen := len(obj.Name()) if pkgName, ok := obj.(*types.PkgName); ok { // An imported Go package has a package-local, unqualified name. @@ -98,18 +77,18 @@ func objToMappedRange(pkg Package, obj types.Object) (MappedRange, error) { // TODO(adonovan): many of the callers need only the ParsedGoFile so // that they can call pgf.PosRange(pos, end) to get a Range; they // don't actually need a MappedRange. -func posToMappedRange(pkg Package, pos, end token.Pos) (MappedRange, error) { +func posToMappedRange(pkg Package, pos, end token.Pos) (protocol.MappedRange, error) { if !pos.IsValid() { - return MappedRange{}, fmt.Errorf("invalid start position") + return protocol.MappedRange{}, fmt.Errorf("invalid start position") } if !end.IsValid() { - return MappedRange{}, fmt.Errorf("invalid end position") + return protocol.MappedRange{}, fmt.Errorf("invalid end position") } logicalFilename := pkg.FileSet().File(pos).Name() // ignore line directives pgf, _, err := findFileInDeps(pkg, span.URIFromPath(logicalFilename)) if err != nil { - return MappedRange{}, err + return protocol.MappedRange{}, err } return pgf.PosMappedRange(pos, end) } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 003e8ff5ce9..694b877e752 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -401,12 +401,12 @@ func (pgf *ParsedGoFile) PosRange(start, end token.Pos) (protocol.Range, error) // PosMappedRange returns a MappedRange for the token.Pos interval in this file. // A MappedRange can be converted to any other form. -func (pgf *ParsedGoFile) PosMappedRange(startPos, endPos token.Pos) (MappedRange, error) { +func (pgf *ParsedGoFile) PosMappedRange(startPos, endPos token.Pos) (protocol.MappedRange, error) { start, end, err := safetoken.Offsets(pgf.Tok, startPos, endPos) if err != nil { - return MappedRange{}, nil + return protocol.MappedRange{}, nil } - return MappedRange{pgf.Mapper, start, end}, nil + return pgf.Mapper.OffsetMappedRange(start, end) } // RangeToSpanRange parses a protocol Range back into the go/token domain. From 2eb6138247ef45a2227f2d8dd7f2436e809f2d91 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 6 Jan 2023 12:19:08 -0500 Subject: [PATCH 592/723] gopls/internal/lsp/filecache: use TempDir if UserCacheDir fails us On the builders, HOME=/ which causes UserCacheDir to be non-writable. This change attempts to MkdirAll the UserCacheDir and, if that fails, we fail back to TempDir, which is assumed to exist and be writable. Updates golang/go#57638 Change-Id: I6eb81c59b50f90a62c103a2511425fa2f24452fa Reviewed-on: https://go-review.googlesource.com/c/tools/+/460917 Reviewed-by: Bryan Mills gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/filecache/filecache.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go index 3338adeaa4b..12ba1ea5221 100644 --- a/gopls/internal/lsp/filecache/filecache.go +++ b/gopls/internal/lsp/filecache/filecache.go @@ -180,6 +180,20 @@ func getCacheDir() string { } goplsDir := filepath.Join(userDir, "gopls") + // UserCacheDir may return a nonexistent directory + // (in which case we must create it, which may fail), + // or it may return a non-writable directory, in + // which case we should ideally respect the user's express + // wishes (e.g. XDG_CACHE_HOME) and not write somewhere else. + // Sadly UserCacheDir doesn't currently let us distinguish + // such intent from accidental misconfiguraton such as HOME=/ + // in a CI builder. So, we check whether the gopls subdirectory + // can be created (or already exists) and not fall back to /tmp. + // See also https://github.com/golang/go/issues/57638. + if os.MkdirAll(goplsDir, 0700) != nil { + goplsDir = filepath.Join(os.TempDir(), "gopls") + } + // Start the garbage collector. go gc(goplsDir) From 67baca65192b822279984d894b3cb0b97636c505 Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 3 Jan 2023 15:48:03 -0800 Subject: [PATCH 593/723] go/callgraph/vta: optimize scc computation Optimizes how sccs are internally computed by reducing the amount of maps created and map lookups performed. Between 4-10% faster on benchmarks of callgraph construction. Updates golang/go#57357 Change-Id: I0ecd92c32358a56d1cae67beb352ec15b7b4367e Reviewed-on: https://go-review.googlesource.com/c/tools/+/460435 Run-TryBot: Tim King TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Zvonimir Pavlinovic --- go/callgraph/callgraph_test.go | 11 ++++---- go/callgraph/vta/propagation.go | 45 ++++++++++++++++----------------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/go/callgraph/callgraph_test.go b/go/callgraph/callgraph_test.go index 907b317a0c2..225656641ac 100644 --- a/go/callgraph/callgraph_test.go +++ b/go/callgraph/callgraph_test.go @@ -27,16 +27,15 @@ import ( // Current results were on an i7 macbook on go version devel go1.20-2730. // Number of nodes, edges, and reachable function are expected to vary between // go versions. Timing results are expected to vary between machines. -// // BenchmarkStatic-12 53 ms/op 6 MB/op 12113 nodes 37355 edges 1522 reachable // BenchmarkCHA-12 86 ms/op 16 MB/op 12113 nodes 131717 edges 7640 reachable // BenchmarkRTA-12 110 ms/op 12 MB/op 6566 nodes 42291 edges 5099 reachable // BenchmarkPTA-12 1427 ms/op 600 MB/op 8714 nodes 28244 edges 4184 reachable -// BenchmarkVTA-12 603 ms/op 87 MB/op 12112 nodes 44857 edges 4918 reachable -// BenchmarkVTA2-12 774 ms/op 115 MB/op 4918 nodes 20247 edges 3841 reachable -// BenchmarkVTA3-12 928 ms/op 134 MB/op 3841 nodes 16502 edges 3542 reachable -// BenchmarkVTAAlt-12 395 ms/op 61 MB/op 7640 nodes 29629 edges 4257 reachable -// BenchmarkVTAAlt2-12 556 ms/op 82 MB/op 4257 nodes 18057 edges 3586 reachable +// BenchmarkVTA-12 562 ms/op 78 MB/op 12112 nodes 44857 edges 4918 reachable +// BenchmarkVTA2-12 743 ms/op 103 MB/op 4918 nodes 20247 edges 3841 reachable +// BenchmarkVTA3-12 837 ms/op 119 MB/op 3841 nodes 16502 edges 3542 reachable +// BenchmarkVTAAlt-12 356 ms/op 56 MB/op 7640 nodes 29629 edges 4257 reachable +// BenchmarkVTAAlt2-12 490 ms/op 75 MB/op 4257 nodes 18057 edges 3586 reachable // // Note: // * Static is unsound and may miss real edges. diff --git a/go/callgraph/vta/propagation.go b/go/callgraph/vta/propagation.go index 6127780ac4e..5817e89380f 100644 --- a/go/callgraph/vta/propagation.go +++ b/go/callgraph/vta/propagation.go @@ -20,53 +20,52 @@ import ( // with ids X and Y s.t. X < Y, Y comes before X in the topological order. func scc(g vtaGraph) (map[node]int, int) { // standard data structures used by Tarjan's algorithm. - var index uint64 + type state struct { + index int + lowLink int + onStack bool + } + states := make(map[node]*state, len(g)) var stack []node - indexMap := make(map[node]uint64) - lowLink := make(map[node]uint64) - onStack := make(map[node]bool) - nodeToSccID := make(map[node]int) + nodeToSccID := make(map[node]int, len(g)) sccID := 0 var doSCC func(node) doSCC = func(n node) { - indexMap[n] = index - lowLink[n] = index - index = index + 1 - onStack[n] = true + index := len(states) + ns := &state{index: index, lowLink: index, onStack: true} + states[n] = ns stack = append(stack, n) for s := range g[n] { - if _, ok := indexMap[s]; !ok { + if ss, visited := states[s]; !visited { // Analyze successor s that has not been visited yet. doSCC(s) - lowLink[n] = min(lowLink[n], lowLink[s]) - } else if onStack[s] { + ss = states[s] + ns.lowLink = min(ns.lowLink, ss.lowLink) + } else if ss.onStack { // The successor is on the stack, meaning it has to be // in the current SCC. - lowLink[n] = min(lowLink[n], indexMap[s]) + ns.lowLink = min(ns.lowLink, ss.index) } } // if n is a root node, pop the stack and generate a new SCC. - if lowLink[n] == indexMap[n] { - for { - w := stack[len(stack)-1] + if ns.lowLink == index { + var w node + for w != n { + w = stack[len(stack)-1] stack = stack[:len(stack)-1] - onStack[w] = false + states[w].onStack = false nodeToSccID[w] = sccID - if w == n { - break - } } sccID++ } } - index = 0 for n := range g { - if _, ok := indexMap[n]; !ok { + if _, visited := states[n]; !visited { doSCC(n) } } @@ -74,7 +73,7 @@ func scc(g vtaGraph) (map[node]int, int) { return nodeToSccID, sccID } -func min(x, y uint64) uint64 { +func min(x, y int) int { if x < y { return x } From 0362ceaa744fa1e5e2bf167ac785c67dbbb1c953 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 5 Jan 2023 13:29:53 -0500 Subject: [PATCH 594/723] gopls/internal/lsp/lsppos: delete TokenMapper Instead use the existing protocol.ColumnMapper, plus a token.File to interpret token.Pos values. Also, add convenience methods ColumnMapper.{PosPosition,PosRange,NodeRange} ParsedGoFile.NodeRange All require an explicit token.File to interpret the token.Pos arguments. In completion_test, the inequality was incorrect and hid a latent bug in the "cursor at EOF" logic in completion/package.go. One test in completion_test has been temporarily suppressed until ColumnMapper gets the EOF workaround currently implemented by lsppos.TokenMapper. (It is reenabled higher in the CL stack.) Change-Id: Ie962a53108bf7af1b2a719c68147edc9c7393feb Reviewed-on: https://go-review.googlesource.com/c/tools/+/460875 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/check.go | 2 +- gopls/internal/lsp/cache/mod_tidy.go | 2 +- gopls/internal/lsp/cache/symbols.go | 21 +++--- gopls/internal/lsp/completion.go | 15 +---- gopls/internal/lsp/diagnostics.go | 2 +- gopls/internal/lsp/fake/editor.go | 10 +++ gopls/internal/lsp/lsppos/token.go | 67 ------------------- gopls/internal/lsp/lsppos/token_test.go | 57 ---------------- gopls/internal/lsp/mod/hover.go | 8 +-- gopls/internal/lsp/protocol/span.go | 41 +++++++++--- gopls/internal/lsp/selection_range.go | 2 +- gopls/internal/lsp/source/code_lens.go | 4 +- .../lsp/source/completion/completion.go | 17 ++--- .../lsp/source/completion/definition.go | 8 +-- .../internal/lsp/source/completion/package.go | 48 +++++-------- gopls/internal/lsp/source/fix.go | 7 +- gopls/internal/lsp/source/inlay_hint.go | 52 +++++++------- gopls/internal/lsp/source/rename.go | 6 +- gopls/internal/lsp/source/symbols.go | 54 +++++++-------- gopls/internal/lsp/source/view.go | 13 ++-- .../regtest/completion/completion_test.go | 34 +++++----- 21 files changed, 172 insertions(+), 298 deletions(-) delete mode 100644 gopls/internal/lsp/lsppos/token.go delete mode 100644 gopls/internal/lsp/lsppos/token_test.go diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index b476137b304..f3dc1972cea 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -680,7 +680,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost } for _, imp := range allImports[item] { - rng, err := imp.cgf.PosRange(imp.imp.Pos(), imp.imp.End()) + rng, err := imp.cgf.NodeRange(imp.imp) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index c9c02ca818e..d15fe4047f5 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -426,7 +426,7 @@ func missingModuleForImport(pgf *source.ParsedGoFile, imp *ast.ImportSpec, req * if req.Syntax == nil { return nil, fmt.Errorf("no syntax for %v", req) } - rng, err := pgf.PosRange(imp.Path.Pos(), imp.Path.End()) + rng, err := pgf.NodeRange(imp.Path) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/symbols.go b/gopls/internal/lsp/cache/symbols.go index 69b2b044273..26a78677ca3 100644 --- a/gopls/internal/lsp/cache/symbols.go +++ b/gopls/internal/lsp/cache/symbols.go @@ -12,7 +12,6 @@ import ( "go/types" "strings" - "golang.org/x/tools/gopls/internal/lsp/lsppos" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" @@ -66,8 +65,9 @@ func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, e } var ( - file *ast.File - fileDesc *token.File + file *ast.File + tokFile *token.File + mapper *protocol.ColumnMapper ) // If the file has already been fully parsed through the @@ -78,7 +78,8 @@ func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, e snapshot.mu.Unlock() if pgf != nil { file = pgf.File - fileDesc = pgf.Tok + tokFile = pgf.Tok + mapper = pgf.Mapper } // Otherwise, we parse the file ourselves. Notably we don't use parseGo here, @@ -91,11 +92,13 @@ func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, e if file == nil { return nil, err } - fileDesc = fset.File(file.Package) + tokFile = fset.File(file.Package) + mapper = protocol.NewColumnMapper(fh.URI(), src) } w := &symbolWalker{ - mapper: lsppos.NewTokenMapper(src, fileDesc), + tokFile: tokFile, + mapper: mapper, } w.fileDecls(file.Decls) @@ -104,7 +107,9 @@ func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, e } type symbolWalker struct { - mapper *lsppos.TokenMapper // for computing positions + // for computing positions + tokFile *token.File + mapper *protocol.ColumnMapper symbols []source.Symbol firstError error @@ -120,7 +125,7 @@ func (w *symbolWalker) atNode(node ast.Node, name string, kind protocol.SymbolKi } b.WriteString(name) - rng, err := w.mapper.Range(node.Pos(), node.End()) + rng, err := w.mapper.NodeRange(w.tokFile, node) if err != nil { w.error(err) return diff --git a/gopls/internal/lsp/completion.go b/gopls/internal/lsp/completion.go index e443a3c5749..d63e3a3832c 100644 --- a/gopls/internal/lsp/completion.go +++ b/gopls/internal/lsp/completion.go @@ -9,7 +9,6 @@ import ( "fmt" "strings" - "golang.org/x/tools/gopls/internal/lsp/lsppos" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/completion" @@ -56,19 +55,7 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara }, nil } - // Map positions to LSP positions using the original content, rather than - // internal/span, as the latter treats end of file as the beginning of the - // next line, even when it's not newline-terminated. See golang/go#41029 for - // more details. - // TODO(adonovan): make completion retain the pgf.Mapper - // so we can convert to rng without reading. - src, err := fh.Read() - if err != nil { - return nil, err - } - srng := surrounding.Range() - tf := snapshot.FileSet().File(srng.Start) // not same as srng.TokFile due to //line - rng, err := lsppos.NewTokenMapper(src, tf).Range(srng.Start, srng.End) + rng, err := surrounding.Range() if err != nil { return nil, err } diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 6ec7a083832..e7b290d3cbb 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -577,7 +577,7 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps if !pgf.File.Name.Pos().IsValid() { return nil } - rng, err := pgf.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) + rng, err := pgf.NodeRange(pgf.File.Name) if err != nil { return nil } diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index a3a39d49411..cda35d14b37 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -566,6 +566,16 @@ func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) erro // contentPosition returns the (Line, Column) position corresponding to offset // in the buffer referenced by path. +// +// TODO(adonovan): offset is measured in runes, according to the test, +// but I can't imagine why that would be useful, and the only actual +// caller (regexpRange) seems to pass a byte offset. +// I would expect the implementation to be simply: +// +// prefix := content[:offset] +// line := strings.Count(prefix, "\n") // 0-based +// col := utf8.RuneCountInString(prefix[strings.LastIndex(prefix, "\n")+1:]) // 0-based, runes +// return Pos{Line: line, Column: col}, nil func contentPosition(content string, offset int) (Pos, error) { scanner := bufio.NewScanner(strings.NewReader(content)) start := 0 diff --git a/gopls/internal/lsp/lsppos/token.go b/gopls/internal/lsp/lsppos/token.go deleted file mode 100644 index a42b5dad331..00000000000 --- a/gopls/internal/lsp/lsppos/token.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package lsppos - -import ( - "errors" - "go/ast" - "go/token" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/safetoken" -) - -// TokenMapper maps token.Pos to LSP positions for a single file. -type TokenMapper struct { - // file is used for computing offsets. - file *token.File - - // For now, just delegate to a Mapper for position calculation. As an - // optimization we could avoid building the mapper and just use the file, but - // then have to correctly adjust for newline-terminated files. It is easier - // to just delegate unless performance becomes a concern. - mapper *Mapper -} - -// NewTokenMapper creates a new TokenMapper for the given content, using the -// provided file to compute offsets. -func NewTokenMapper(content []byte, file *token.File) *TokenMapper { - return &TokenMapper{ - file: file, - mapper: NewMapper(content), - } -} - -// Position returns the protocol position corresponding to the given pos. It -// returns false if pos is out of bounds for the file being mapped. -func (m *TokenMapper) Position(pos token.Pos) (protocol.Position, bool) { - offset, err := safetoken.Offset(m.file, pos) - if err != nil { - return protocol.Position{}, false - } - return m.mapper.Position(offset) -} - -// Range returns the protocol range corresponding to the given start and end -// positions. It returns an error if start or end is out of bounds for the file -// being mapped. -func (m *TokenMapper) Range(start, end token.Pos) (protocol.Range, error) { - startPos, ok := m.Position(start) - if !ok { - return protocol.Range{}, errors.New("invalid start position") - } - endPos, ok := m.Position(end) - if !ok { - return protocol.Range{}, errors.New("invalid end position") - } - - return protocol.Range{Start: startPos, End: endPos}, nil -} - -// NodeRange returns the protocol range corresponding to the span of the given -// node. -func (m *TokenMapper) NodeRange(n ast.Node) (protocol.Range, error) { - return m.Range(n.Pos(), n.End()) -} diff --git a/gopls/internal/lsp/lsppos/token_test.go b/gopls/internal/lsp/lsppos/token_test.go deleted file mode 100644 index a8fa6f667c7..00000000000 --- a/gopls/internal/lsp/lsppos/token_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package lsppos_test - -import ( - "go/token" - "testing" - - . "golang.org/x/tools/gopls/internal/lsp/lsppos" - "golang.org/x/tools/gopls/internal/lsp/protocol" -) - -func makeTokenMapper(content []byte) (*TokenMapper, *token.File) { - file := token.NewFileSet().AddFile("p.go", -1, len(content)) - file.SetLinesForContent(content) - return NewTokenMapper(content, file), file -} - -func TestInvalidPosition(t *testing.T) { - content := []byte("a𐐀b\r\nx\ny") - m, _ := makeTokenMapper(content) - - for _, pos := range []token.Pos{-1, 100} { - posn, ok := m.Position(pos) - if ok { - t.Errorf("Position(%d) = %v, want error", pos, posn) - } - } -} - -func TestTokenPosition(t *testing.T) { - for _, test := range tests { - m, f := makeTokenMapper([]byte(test.content)) - pos := token.Pos(f.Base() + test.offset()) - got, ok := m.Position(pos) - if !ok { - t.Error("invalid position for", test.substrOrOffset) - continue - } - want := protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)} - if got != want { - t.Errorf("Position(%d) = %v, want %v", pos, got, want) - } - gotRange, err := m.Range(token.Pos(f.Base()), pos) - if err != nil { - t.Fatal(err) - } - wantRange := protocol.Range{ - End: want, - } - if gotRange != wantRange { - t.Errorf("Range(%d) = %v, want %v", pos, got, want) - } - } -} diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index ef7c235b7ef..dd869be8049 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -56,7 +56,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh source.FileHandle) (*protocol.Hover, error) { // Confirm that the cursor is at the position of a require statement. var req *modfile.Require - var startPos, endPos int + var startOffset, endOffset int for _, r := range pm.File.Require { dep := []byte(r.Mod.Path) s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte @@ -66,8 +66,8 @@ func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offse } // Shift the start position to the location of the // dependency within the require statement. - startPos, endPos = s+i, e - if startPos <= offset && offset <= endPos { + startOffset, endOffset = s+i, e + if startOffset <= offset && offset <= endOffset { req = r break } @@ -105,7 +105,7 @@ func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offse // Get the range to highlight for the hover. // TODO(hyangah): adjust the hover range to include the version number // to match the diagnostics' range. - rng, err := pm.Mapper.OffsetRange(startPos, endPos) + rng, err := pm.Mapper.OffsetRange(startOffset, endOffset) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index c518c8047d3..1ca1eee384e 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -8,7 +8,6 @@ // // Imports: lsppos --> protocol --> span --> token // -// lsppos.TokenMapper = (token.File, lsppos.Mapper) // lsppos.Mapper = (line offset table, content) // // protocol.ColumnMapper = (URI, Content). Does all offset <=> column conversions. @@ -33,8 +32,6 @@ // It is mostly used by completion. Given access to complete.mapper, // it could use a pair byte offsets instead. // - Merge lsppos.Mapper and protocol.ColumnMapper. -// - Replace all uses of lsppos.TokenMapper by the underlying ParsedGoFile, -// which carries a token.File and a ColumnMapper. // - Then delete lsppos package. // - ColumnMapper.OffsetPoint and .Position aren't used outside this package. // OffsetSpan is barely used, and its user would better off with a MappedRange @@ -46,11 +43,13 @@ package protocol import ( "bytes" "fmt" + "go/ast" "go/token" "path/filepath" "strings" "unicode/utf8" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" ) @@ -66,16 +65,18 @@ import ( // // - (line, colRune) pairs, where colRune is a rune index, as used by ParseWork. // -// This type does not depend on or use go/token-based representations. -// Use safetoken to map between token.Pos <=> byte offsets. +// This type does not intrinsically depend on go/token-based +// representations. Use safetoken to map between token.Pos <=> byte +// offsets, or the convenience methods such as PosPosition, +// NodePosition, or NodeRange. type ColumnMapper struct { URI span.URI Content []byte - // This field provides a line-number table, nothing more. - // The public API of ColumnMapper doesn't mention go/token, - // nor should it. It need not be consistent with any - // other token.File or FileSet. + // This field provides a line-number table, nothing more. The + // public API of ColumnMapper doesn't implicate it in any + // relationship with any instances of token.File or + // token.FileSet, nor should it. // // TODO(adonovan): eliminate this field in a follow-up // by inlining the line-number table. Then merge this @@ -206,6 +207,28 @@ func (m *ColumnMapper) OffsetPosition(offset int) (Position, error) { }, nil } +// -- go/token domain convenience methods -- + +func (m *ColumnMapper) PosPosition(tf *token.File, pos token.Pos) (Position, error) { + offset, err := safetoken.Offset(tf, pos) + if err != nil { + return Position{}, err + } + return m.OffsetPosition(offset) +} + +func (m *ColumnMapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { + startOffset, endOffset, err := safetoken.Offsets(tf, start, end) + if err != nil { + return Range{}, err + } + return m.OffsetRange(startOffset, endOffset) +} + +func (m *ColumnMapper) NodeRange(tf *token.File, node ast.Node) (Range, error) { + return m.PosRange(tf, node.Pos(), node.End()) +} + // utf16Column returns the zero-based column index of the // specified file offset, measured in UTF-16 codes. // Precondition: 0 <= offset <= len(m.Content). diff --git a/gopls/internal/lsp/selection_range.go b/gopls/internal/lsp/selection_range.go index b7a0cdb1d80..632b458ab31 100644 --- a/gopls/internal/lsp/selection_range.go +++ b/gopls/internal/lsp/selection_range.go @@ -51,7 +51,7 @@ func (s *Server) selectionRange(ctx context.Context, params *protocol.SelectionR tail := &result[i] // tail of the Parent linked list, built head first for j, node := range path { - rng, err := pgf.PosRange(node.Pos(), node.End()) + rng, err := pgf.NodeRange(node) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go index f929256c273..a864981091d 100644 --- a/gopls/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -111,7 +111,7 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) ( continue } - rng, err := pgf.PosRange(fn.Pos(), fn.End()) + rng, err := pgf.NodeRange(fn) if err != nil { return out, err } @@ -214,7 +214,7 @@ func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([ if c == nil { return nil, nil } - rng, err := pgf.PosRange(c.Pos(), c.End()) + rng, err := pgf.NodeRange(c) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 4d2da2e40e7..12bad1cf384 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -291,18 +291,15 @@ type Selection struct { content string cursor token.Pos // relative to rng.TokFile rng span.Range - // TODO(adonovan): keep the ColumnMapper (completer.mapper) - // nearby so we can convert rng to protocol form without - // needing to read the file again, as the sole caller of - // Selection.Range() must currently do. + mapper *protocol.ColumnMapper } func (p Selection) Content() string { return p.content } -func (p Selection) Range() span.Range { - return p.rng +func (p Selection) Range() (protocol.Range, error) { + return p.mapper.PosRange(p.rng.TokFile, p.rng.Start, p.rng.End) } func (p Selection) Prefix() string { @@ -325,7 +322,8 @@ func (c *completer) setSurrounding(ident *ast.Ident) { content: ident.Name, cursor: c.pos, // Overwrite the prefix only. - rng: span.NewRange(c.tokFile, ident.Pos(), ident.End()), + rng: span.NewRange(c.tokFile, ident.Pos(), ident.End()), + mapper: c.mapper, } c.setMatcherFromPrefix(c.surrounding.Prefix()) @@ -348,6 +346,7 @@ func (c *completer) getSurrounding() *Selection { content: "", cursor: c.pos, rng: span.NewRange(c.tokFile, c.pos, c.pos), + mapper: c.mapper, } } return c.surrounding @@ -486,7 +485,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan qual := types.RelativeTo(pkg.GetTypes()) objStr = types.ObjectString(obj, qual) } - ans, sel := definition(path, obj, pgf.Tok, fh) + ans, sel := definition(path, obj, pgf) if ans != nil { sort.Slice(ans, func(i, j int) bool { return ans[i].Score > ans[j].Score @@ -800,6 +799,7 @@ func (c *completer) populateImportCompletions(ctx context.Context, searchImport content: content, cursor: c.pos, rng: span.NewRange(c.tokFile, start, end), + mapper: c.mapper, } seenImports := make(map[string]struct{}) @@ -1020,6 +1020,7 @@ func (c *completer) setSurroundingForComment(comments *ast.CommentGroup) { content: cursorComment.Text[start:end], cursor: c.pos, rng: span.NewRange(c.tokFile, token.Pos(int(cursorComment.Slash)+start), token.Pos(int(cursorComment.Slash)+end)), + mapper: c.mapper, } c.setMatcherFromPrefix(c.surrounding.Prefix()) } diff --git a/gopls/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go index bf14ad4a64b..2270dd792d6 100644 --- a/gopls/internal/lsp/source/completion/definition.go +++ b/gopls/internal/lsp/source/completion/definition.go @@ -6,7 +6,6 @@ package completion import ( "go/ast" - "go/token" "go/types" "strings" "unicode" @@ -23,11 +22,11 @@ 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, tokFile *token.File, fh source.FileHandle) ([]CompletionItem, *Selection) { +func definition(path []ast.Node, obj types.Object, pgf *source.ParsedGoFile) ([]CompletionItem, *Selection) { if _, ok := obj.(*types.Func); !ok { return nil, nil // not a function at all } - if !strings.HasSuffix(fh.URI().Filename(), "_test.go") { + if !strings.HasSuffix(pgf.URI.Filename(), "_test.go") { return nil, nil // not a test file } @@ -41,7 +40,8 @@ func definition(path []ast.Node, obj types.Object, tokFile *token.File, fh sourc sel := &Selection{ content: "", cursor: start, - rng: span.NewRange(tokFile, start, end), + rng: span.NewRange(pgf.Tok, start, end), + mapper: pgf.Mapper, } var ans []CompletionItem var hasParens bool diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index de2b75d436c..8141954a073 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -22,7 +22,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/fuzzy" ) @@ -31,18 +30,17 @@ import ( func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) ([]CompletionItem, *Selection, error) { // 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. - fset := snapshot.FileSet() + // TODO(adonovan): opt: there's no need to parse just to get a mapper. pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) if err != nil { return nil, nil, err } - pos, err := pgf.Pos(position) + offset, err := pgf.Mapper.Offset(position) if err != nil { return nil, nil, err } - - surrounding, err := packageCompletionSurrounding(fset, pgf, pos) + surrounding, err := packageCompletionSurrounding(pgf, offset) if err != nil { return nil, nil, fmt.Errorf("invalid position for package completion: %w", err) } @@ -67,31 +65,18 @@ func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh } // packageCompletionSurrounding returns surrounding for package completion if a -// package completions can be suggested at a given position. A valid location +// 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(fset *token.FileSet, pgf *source.ParsedGoFile, pos token.Pos) (*Selection, error) { +func packageCompletionSurrounding(pgf *source.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. - filename := pgf.URI.Filename() - expr, _ := parser.ParseExprFrom(fset, filename, pgf.Src, parser.Mode(0)) + fset := token.NewFileSet() + expr, _ := parser.ParseExprFrom(fset, m.URI.Filename(), pgf.Src, parser.Mode(0)) if expr == nil { - return nil, fmt.Errorf("unparseable file (%s)", pgf.URI) + return nil, fmt.Errorf("unparseable file (%s)", m.URI) } tok := fset.File(expr.Pos()) - offset, err := safetoken.Offset(pgf.Tok, pos) - if err != nil { - return nil, err - } - if offset > tok.Size() { - // internal bug: we should never get an offset that exceeds the size of our - // file. - bug.Report("out of bounds cursor", bug.Data{ - "offset": offset, - "URI": pgf.URI, - "size": tok.Size(), - }) - return nil, fmt.Errorf("cursor out of bounds") - } cursor := tok.Pos(offset) // If we were able to parse out an identifier as the first expression from @@ -106,6 +91,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, content: name.Name, cursor: cursor, rng: span.NewRange(tok, name.Pos(), name.End()), + mapper: m, }, nil } } @@ -143,6 +129,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, content: content, cursor: cursor, rng: span.NewRange(tok, start, end), + mapper: m, }, nil } } @@ -155,21 +142,16 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, } // If the cursor is in a comment, don't offer any completions. - if cursorInComment(fset.File(cursor), cursor, pgf.Src) { + if cursorInComment(tok, cursor, m.Content) { return nil, fmt.Errorf("cursor in comment") } - // The surrounding range in this case is the cursor except for empty file, - // in which case it's end of file - 1 - start, end := cursor, cursor - if tok.Size() == 0 { - start, end = tok.Pos(0)-1, tok.Pos(0)-1 - } - + // The surrounding range in this case is the cursor. return &Selection{ content: "", cursor: cursor, - rng: span.NewRange(tok, start, end), + rng: span.NewRange(tok, cursor, cursor), + mapper: m, }, nil } diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 873db7f9479..7b39c44262e 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" ) @@ -94,10 +93,6 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi if !end.IsValid() { end = edit.Pos } - startOffset, endOffset, err := safetoken.Offsets(tokFile, edit.Pos, end) - if err != nil { - return nil, err - } fh, err := snapshot.GetVersionedFile(ctx, span.URIFromPath(tokFile.Name())) if err != nil { return nil, err @@ -120,7 +115,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi return nil, err } m := protocol.NewColumnMapper(fh.URI(), content) - rng, err := m.OffsetRange(startOffset, endOffset) + rng, err := m.PosRange(tokFile, edit.Pos, end) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go index 3958e4b89cf..5179c1e32c1 100644 --- a/gopls/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -13,7 +13,6 @@ import ( "go/types" "strings" - "golang.org/x/tools/gopls/internal/lsp/lsppos" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" @@ -23,7 +22,7 @@ const ( maxLabelLength = 28 ) -type InlayHintFunc func(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint +type InlayHintFunc func(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint type Hint struct { Name string @@ -103,7 +102,6 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng proto return nil, nil } - tmap := lsppos.NewTokenMapper(pgf.Src, pgf.Tok) info := pkg.GetTypesInfo() q := Qualifier(pgf.File, pkg.GetTypes(), info) @@ -125,14 +123,14 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng proto return false } for _, fn := range enabledHints { - hints = append(hints, fn(node, tmap, info, &q)...) + hints = append(hints, fn(node, pgf.Mapper, pgf.Tok, info, &q)...) } return true }) return hints, nil } -func parameterNames(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +func parameterNames(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { callExpr, ok := node.(*ast.CallExpr) if !ok { return nil @@ -144,8 +142,8 @@ func parameterNames(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ var hints []protocol.InlayHint for i, v := range callExpr.Args { - start, ok := tmap.Position(v.Pos()) - if !ok { + start, err := m.PosPosition(tf, v.Pos()) + if err != nil { continue } params := signature.Params() @@ -179,7 +177,7 @@ func parameterNames(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ return hints } -func funcTypeParams(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +func funcTypeParams(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { ce, ok := node.(*ast.CallExpr) if !ok { return nil @@ -192,8 +190,8 @@ func funcTypeParams(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ if inst.TypeArgs == nil { return nil } - start, ok := tmap.Position(id.End()) - if !ok { + start, err := m.PosPosition(tf, id.End()) + if err != nil { return nil } var args []string @@ -210,7 +208,7 @@ func funcTypeParams(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ }} } -func assignVariableTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func assignVariableTypes(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { stmt, ok := node.(*ast.AssignStmt) if !ok || stmt.Tok != token.DEFINE { return nil @@ -218,35 +216,35 @@ func assignVariableTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.In var hints []protocol.InlayHint for _, v := range stmt.Lhs { - if h := variableType(v, tmap, info, q); h != nil { + if h := variableType(v, m, tf, info, q); h != nil { hints = append(hints, *h) } } return hints } -func rangeVariableTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func rangeVariableTypes(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { rStmt, ok := node.(*ast.RangeStmt) if !ok { return nil } var hints []protocol.InlayHint - if h := variableType(rStmt.Key, tmap, info, q); h != nil { + if h := variableType(rStmt.Key, m, tf, info, q); h != nil { hints = append(hints, *h) } - if h := variableType(rStmt.Value, tmap, info, q); h != nil { + if h := variableType(rStmt.Value, m, tf, info, q); h != nil { hints = append(hints, *h) } return hints } -func variableType(e ast.Expr, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) *protocol.InlayHint { +func variableType(e ast.Expr, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint { typ := info.TypeOf(e) if typ == nil { return nil } - end, ok := tmap.Position(e.End()) - if !ok { + end, err := m.PosPosition(tf, e.End()) + if err != nil { return nil } return &protocol.InlayHint{ @@ -257,7 +255,7 @@ func variableType(e ast.Expr, tmap *lsppos.TokenMapper, info *types.Info, q *typ } } -func constantValues(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +func constantValues(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { genDecl, ok := node.(*ast.GenDecl) if !ok || genDecl.Tok != token.CONST { return nil @@ -269,8 +267,8 @@ func constantValues(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ if !ok { continue } - end, ok := tmap.Position(v.End()) - if !ok { + end, err := m.PosPosition(tf, v.End()) + if err != nil { continue } // Show hints when values are missing or at least one value is not @@ -308,7 +306,7 @@ func constantValues(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ return hints } -func compositeLiteralFields(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func compositeLiteralFields(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { compLit, ok := node.(*ast.CompositeLit) if !ok { return nil @@ -329,8 +327,8 @@ func compositeLiteralFields(node ast.Node, tmap *lsppos.TokenMapper, info *types var allEdits []protocol.TextEdit for i, v := range compLit.Elts { if _, ok := v.(*ast.KeyValueExpr); !ok { - start, ok := tmap.Position(v.Pos()) - if !ok { + start, err := m.PosPosition(tf, v.Pos()) + if err != nil { continue } if i > strct.NumFields()-1 { @@ -356,7 +354,7 @@ func compositeLiteralFields(node ast.Node, tmap *lsppos.TokenMapper, info *types return hints } -func compositeLiteralTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func compositeLiteralTypes(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { compLit, ok := node.(*ast.CompositeLit) if !ok { return nil @@ -374,8 +372,8 @@ func compositeLiteralTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types. prefix = "&" } // The type for this composite literal is implicit, add an inlay hint. - start, ok := tmap.Position(compLit.Lbrace) - if !ok { + start, err := m.PosPosition(tf, compLit.Lbrace) + if err != nil { return nil } return []protocol.InlayHint{{ diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 8cb50e82796..1867a921797 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -107,7 +107,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot } // Return the location of the package declaration. - rng, err := pgf.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) + rng, err := pgf.NodeRange(pgf.File.Name) if err != nil { return nil, err, err } @@ -434,7 +434,7 @@ func renamePackageClause(ctx context.Context, m *Metadata, snapshot Snapshot, ne if f.File.Name == nil { continue // no package declaration } - rng, err := f.PosRange(f.File.Name.Pos(), f.File.Name.End()) + rng, err := f.NodeRange(f.File.Name) if err != nil { return err } @@ -493,7 +493,7 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath } // Create text edit for the import path (string literal). - rng, err := f.PosRange(imp.Path.Pos(), imp.Path.End()) + rng, err := f.NodeRange(imp.Path) if err != nil { return err } diff --git a/gopls/internal/lsp/source/symbols.go b/gopls/internal/lsp/source/symbols.go index 2bab241e1d5..d7c067108f8 100644 --- a/gopls/internal/lsp/source/symbols.go +++ b/gopls/internal/lsp/source/symbols.go @@ -11,7 +11,6 @@ import ( "go/token" "go/types" - "golang.org/x/tools/gopls/internal/lsp/lsppos" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" ) @@ -20,18 +19,11 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p ctx, done := event.Start(ctx, "source.DocumentSymbols") defer done() - content, err := fh.Read() - if err != nil { - return nil, err - } - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, fmt.Errorf("getting file for DocumentSymbols: %w", err) } - m := lsppos.NewTokenMapper(content, pgf.Tok) - // Build symbols for file declarations. When encountering a declaration with // errors (typically because positions are invalid), we skip the declaration // entirely. VS Code fails to show any symbols if one of the top-level @@ -43,7 +35,7 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p if decl.Name.Name == "_" { continue } - fs, err := funcSymbol(m, decl) + fs, err := funcSymbol(pgf.Mapper, pgf.Tok, decl) if err == nil { // If function is a method, prepend the type of the method. if decl.Recv != nil && len(decl.Recv.List) > 0 { @@ -58,7 +50,7 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p if spec.Name.Name == "_" { continue } - ts, err := typeSymbol(m, spec) + ts, err := typeSymbol(pgf.Mapper, pgf.Tok, spec) if err == nil { symbols = append(symbols, ts) } @@ -67,7 +59,7 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p if name.Name == "_" { continue } - vs, err := varSymbol(m, spec, name, decl.Tok == token.CONST) + vs, err := varSymbol(pgf.Mapper, pgf.Tok, spec, name, decl.Tok == token.CONST) if err == nil { symbols = append(symbols, vs) } @@ -79,7 +71,7 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p return symbols, nil } -func funcSymbol(m *lsppos.TokenMapper, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) { +func funcSymbol(m *protocol.ColumnMapper, tf *token.File, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) { s := protocol.DocumentSymbol{ Name: decl.Name.Name, Kind: protocol.Function, @@ -88,11 +80,11 @@ func funcSymbol(m *lsppos.TokenMapper, decl *ast.FuncDecl) (protocol.DocumentSym s.Kind = protocol.Method } var err error - s.Range, err = m.Range(decl.Pos(), decl.End()) + s.Range, err = m.NodeRange(tf, decl) if err != nil { return protocol.DocumentSymbol{}, err } - s.SelectionRange, err = m.Range(decl.Name.Pos(), decl.Name.End()) + s.SelectionRange, err = m.NodeRange(tf, decl.Name) if err != nil { return protocol.DocumentSymbol{}, err } @@ -100,28 +92,28 @@ func funcSymbol(m *lsppos.TokenMapper, decl *ast.FuncDecl) (protocol.DocumentSym return s, nil } -func typeSymbol(m *lsppos.TokenMapper, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) { +func typeSymbol(m *protocol.ColumnMapper, tf *token.File, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) { s := protocol.DocumentSymbol{ Name: spec.Name.Name, } var err error - s.Range, err = m.NodeRange(spec) + s.Range, err = m.NodeRange(tf, spec) if err != nil { return protocol.DocumentSymbol{}, err } - s.SelectionRange, err = m.NodeRange(spec.Name) + s.SelectionRange, err = m.NodeRange(tf, spec.Name) if err != nil { return protocol.DocumentSymbol{}, err } - s.Kind, s.Detail, s.Children = typeDetails(m, spec.Type) + s.Kind, s.Detail, s.Children = typeDetails(m, tf, spec.Type) return s, nil } -func typeDetails(m *lsppos.TokenMapper, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) { +func typeDetails(m *protocol.ColumnMapper, tf *token.File, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) { switch typExpr := typExpr.(type) { case *ast.StructType: kind = protocol.Struct - children = fieldListSymbols(m, typExpr.Fields, protocol.Field) + children = fieldListSymbols(m, tf, typExpr.Fields, protocol.Field) if len(children) > 0 { detail = "struct{...}" } else { @@ -131,7 +123,7 @@ func typeDetails(m *lsppos.TokenMapper, typExpr ast.Expr) (kind protocol.SymbolK // Find interface methods and embedded types. case *ast.InterfaceType: kind = protocol.Interface - children = fieldListSymbols(m, typExpr.Methods, protocol.Method) + children = fieldListSymbols(m, tf, typExpr.Methods, protocol.Method) if len(children) > 0 { detail = "interface{...}" } else { @@ -149,7 +141,7 @@ func typeDetails(m *lsppos.TokenMapper, typExpr ast.Expr) (kind protocol.SymbolK return } -func fieldListSymbols(m *lsppos.TokenMapper, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol { +func fieldListSymbols(m *protocol.ColumnMapper, tf *token.File, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol { if fields == nil { return nil } @@ -158,7 +150,7 @@ func fieldListSymbols(m *lsppos.TokenMapper, fields *ast.FieldList, fieldKind pr for _, field := range fields.List { detail, children := "", []protocol.DocumentSymbol(nil) if field.Type != nil { - _, detail, children = typeDetails(m, field.Type) + _, detail, children = typeDetails(m, tf, field.Type) } if len(field.Names) == 0 { // embedded interface or struct field // By default, use the formatted type details as the name of this field. @@ -179,10 +171,10 @@ func fieldListSymbols(m *lsppos.TokenMapper, fields *ast.FieldList, fieldKind pr selection = id } - if rng, err := m.NodeRange(field.Type); err == nil { + if rng, err := m.NodeRange(tf, field.Type); err == nil { child.Range = rng } - if rng, err := m.NodeRange(selection); err == nil { + if rng, err := m.NodeRange(tf, selection); err == nil { child.SelectionRange = rng } @@ -196,10 +188,10 @@ func fieldListSymbols(m *lsppos.TokenMapper, fields *ast.FieldList, fieldKind pr Children: children, } - if rng, err := m.NodeRange(field); err == nil { + if rng, err := m.NodeRange(tf, field); err == nil { child.Range = rng } - if rng, err := m.NodeRange(name); err == nil { + if rng, err := m.NodeRange(tf, name); err == nil { child.SelectionRange = rng } @@ -211,7 +203,7 @@ func fieldListSymbols(m *lsppos.TokenMapper, fields *ast.FieldList, fieldKind pr return symbols } -func varSymbol(m *lsppos.TokenMapper, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) { +func varSymbol(m *protocol.ColumnMapper, tf *token.File, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) { s := protocol.DocumentSymbol{ Name: name.Name, Kind: protocol.Variable, @@ -220,16 +212,16 @@ func varSymbol(m *lsppos.TokenMapper, spec *ast.ValueSpec, name *ast.Ident, isCo s.Kind = protocol.Constant } var err error - s.Range, err = m.NodeRange(spec) + s.Range, err = m.NodeRange(tf, spec) if err != nil { return protocol.DocumentSymbol{}, err } - s.SelectionRange, err = m.NodeRange(name) + s.SelectionRange, err = m.NodeRange(tf, name) if err != nil { return protocol.DocumentSymbol{}, err } if spec.Type != nil { // type may be missing from the syntax - _, s.Detail, s.Children = typeDetails(m, spec.Type) + _, s.Detail, s.Children = typeDetails(m, tf, spec.Type) } return s, nil } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 694b877e752..a2b498fe3af 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -381,6 +381,8 @@ type ParsedGoFile struct { ParseErr scanner.ErrorList } +// -- go/token domain convenience helpers -- + // Pos returns the token.Pos of protocol position p within the file. func (pgf *ParsedGoFile) Pos(p protocol.Position) (token.Pos, error) { point, err := pgf.Mapper.Point(p) @@ -392,11 +394,12 @@ func (pgf *ParsedGoFile) Pos(p protocol.Position) (token.Pos, error) { // PosRange returns a protocol Range for the token.Pos interval in this file. func (pgf *ParsedGoFile) PosRange(start, end token.Pos) (protocol.Range, error) { - startOffset, endOffset, err := safetoken.Offsets(pgf.Tok, start, end) - if err != nil { - return protocol.Range{}, err - } - return pgf.Mapper.OffsetRange(startOffset, endOffset) + return pgf.Mapper.PosRange(pgf.Tok, start, end) +} + +// NodeRange returns a protocol Range for the ast.Node interval in this file. +func (pgf *ParsedGoFile) NodeRange(node ast.Node) (protocol.Range, error) { + return pgf.Mapper.NodeRange(pgf.Tok, node) } // PosMappedRange returns a MappedRange for the token.Pos interval in this file. diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 5f655939b2f..f8621e31e95 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -79,7 +79,7 @@ package ` var ( testfile4 = "" - testfile5 = "/*a comment*/ " + //testfile5 = "/*a comment*/ " testfile6 = "/*a comment*/\n" ) for _, tc := range []struct { @@ -137,14 +137,16 @@ package want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, editRegexp: "^$", }, - { - name: "package completion without terminal newline", - filename: "fruits/testfile5.go", - triggerRegexp: `\*\/ ()`, - content: &testfile5, - want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, - editRegexp: `\*\/ ()`, - }, + // Disabled pending correct implementation of EOF positions + // in CL 460975 (next CL in stack) following approach of lsppos.Mapper. + // { + // name: "package completion without terminal newline", + // filename: "fruits/testfile5.go", + // triggerRegexp: `\*\/ ()`, + // content: &testfile5, + // want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, + // editRegexp: `\*\/ ()`, + // }, { name: "package completion on terminal newline", filename: "fruits/testfile6.go", @@ -181,22 +183,22 @@ package completions := env.Completion(tc.filename, env.RegexpSearch(tc.filename, tc.triggerRegexp)) // Check that the completion item suggestions are in the range - // of the file. + // of the file. {Start,End}.Line are zero-based. lineCount := len(strings.Split(env.BufferText(tc.filename), "\n")) for _, item := range completions.Items { - if start := int(item.TextEdit.Range.Start.Line); start >= lineCount { - t.Fatalf("unexpected text edit range start line number: got %d, want less than %d", start, lineCount) + if start := int(item.TextEdit.Range.Start.Line); start > lineCount { + t.Fatalf("unexpected text edit range start line number: got %d, want <= %d", start, lineCount) } - if end := int(item.TextEdit.Range.End.Line); end >= lineCount { - t.Fatalf("unexpected text edit range end line number: got %d, want less than %d", end, lineCount) + if end := int(item.TextEdit.Range.End.Line); end > lineCount { + t.Fatalf("unexpected text edit range end line number: got %d, want <= %d", end, lineCount) } } if tc.want != nil { start, end := env.RegexpRange(tc.filename, tc.editRegexp) expectedRng := protocol.Range{ - Start: fake.Pos.ToProtocolPosition(start), - End: fake.Pos.ToProtocolPosition(end), + Start: start.ToProtocolPosition(), + End: end.ToProtocolPosition(), } for _, item := range completions.Items { gotRng := item.TextEdit.Range From 2be9d05fe891b194b2e099acbf48899748e292cc Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 21 Dec 2022 17:20:25 -0500 Subject: [PATCH 595/723] gopls/internal/lsp/source/xrefs: a new reference index This change defines a new, serializable cross-reference index that is constructed immediately after type checking. In due course it will be saved in the filecache alongside the other results of type-checking, such as export data, but for now it is simply hung off the cache.pkg. The index for a package P is a gob-encoded mapping from each of P's dependencies Q to the set of objects in Q that are referenced by P, along with the location of the reference. Q's objects are identified by (PackagePath, objectpath.Path). At query time, in-package references to an object are sought by type-checking its declaring package (and variants), and using the TypesInfo.Defs/Uses. Cross-package references are sought in the serialized index of each reverse dependency: direct for package-level objects, transitive for selections. Currently we don't have an incremental implementation of the "implements" algorithm (though we know what to do; see CL 452060). This algorithm is used by "references" when the query object is a method. For now, we fall back to the old implementation, rather than bite off too much in this CL. This is another step towards an incremental, scalable gopls that doesn't require holding the result of type-checking the entire workspace in memory. That said, at this stage this change is only costs. Using a large 'references' query in k8s as a benchmark, it adds about about 30MB of additional heap for the indexes, and about 10% of the command's CPU is spent building the index. (Lookup is cheap: <0.5% of CPU.) But these costs are modest relative to the existing large costs (>1GB, >22s) of this operation. Also, add missing test coverage of implicits (ImportSpec, type switch). Change-Id: I648607bab58e26e24bc99b276ffabc38db48eb09 Reviewed-on: https://go-review.googlesource.com/c/tools/+/458998 Reviewed-by: Robert Findley gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/check.go | 4 + gopls/internal/lsp/cache/pkg.go | 15 +- gopls/internal/lsp/cmd/test/references.go | 3 + gopls/internal/lsp/lsp_test.go | 44 +- gopls/internal/lsp/protocol/span.go | 35 +- gopls/internal/lsp/references.go | 17 +- gopls/internal/lsp/source/call_hierarchy.go | 8 +- gopls/internal/lsp/source/identifier.go | 12 +- gopls/internal/lsp/source/implementation.go | 5 +- gopls/internal/lsp/source/references.go | 12 +- gopls/internal/lsp/source/references2.go | 496 ++++++++++++++++++ gopls/internal/lsp/source/source_test.go | 43 +- gopls/internal/lsp/source/view.go | 4 +- gopls/internal/lsp/source/xrefs/xrefs.go | 212 ++++++++ .../internal/lsp/testdata/references/refs.go | 15 + .../internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- 17 files changed, 841 insertions(+), 88 deletions(-) create mode 100644 gopls/internal/lsp/source/references2.go create mode 100644 gopls/internal/lsp/source/xrefs/xrefs.go diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index f3dc1972cea..ae550bc5364 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -22,6 +22,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/xrefs" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" @@ -434,6 +435,9 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF } pkg.diagnostics = append(pkg.diagnostics, depsErrors...) + // Build index of outbound cross-references. + pkg.xrefs = xrefs.Index(pkg) + return pkg, nil } diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 6d138bea15c..e640191d26e 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -12,7 +12,10 @@ import ( "go/types" "golang.org/x/mod/module" + "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/xrefs" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/memoize" ) @@ -39,7 +42,8 @@ type pkg struct { typeErrors []types.Error types *types.Package typesInfo *types.Info - hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors + hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors + xrefs []byte // serializable index of outbound cross-references analyses memoize.Store // maps analyzer.Name to Promise[actionResult] } @@ -171,3 +175,12 @@ func (p *pkg) DiagnosticsForFile(uri span.URI) []*source.Diagnostic { } return res } + +func (p *pkg) ReferencesTo(pkgPath PackagePath, objPath objectpath.Path) []protocol.Location { + // TODO(adonovan): In future, p.xrefs will be retrieved from a + // section of the cache file produced by type checking. + // (Other sections will include the package's export data, + // "implements" relations, exported symbols, etc.) + // For now we just hang it off the pkg. + return xrefs.Lookup(p.m, p.xrefs, pkgPath, objPath) +} diff --git a/gopls/internal/lsp/cmd/test/references.go b/gopls/internal/lsp/cmd/test/references.go index 85c9bc84a62..4b26f982a1a 100644 --- a/gopls/internal/lsp/cmd/test/references.go +++ b/gopls/internal/lsp/cmd/test/references.go @@ -42,6 +42,9 @@ func (r *runner) References(t *testing.T, spn span.Span, itemList []span.Span) { if stderr != "" { t.Errorf("references failed for %s: %s", target, stderr) } else if expect != got { + // TODO(adonovan): print the query, the expectations, and + // the actual results clearly. Factor in common with the + // other two implementations of runner.References. t.Errorf("references failed for %s expected:\n%s\ngot:\n%s", target, expect, got) } }) diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 4c3c9d74be6..1c29adfb4b6 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -25,6 +25,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/testenv" ) @@ -873,6 +874,9 @@ func (r *runner) Hover(t *testing.T, src span.Span, text string) { } func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { + // This test is substantially the same as (*runner).References in source/source_test.go. + // TODO(adonovan): Factor (and remove fluff). Where should the common code live? + sm, err := r.data.Mapper(src.URI()) if err != nil { t.Fatal(err) @@ -912,13 +916,43 @@ func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { if err != nil { t.Fatalf("failed for %v: %v", src, err) } - if len(got) != len(want) { - t.Errorf("references failed: different lengths got %v want %v", len(got), len(want)) + + sanitize := func(s string) string { + // In practice, CONFIGDIR means "gopls/internal/lsp/testdata". + return strings.ReplaceAll(s, r.data.Config.Dir, "CONFIGDIR") + } + formatLocation := func(loc protocol.Location) string { + return fmt.Sprintf("%s:%d.%d-%d.%d", + sanitize(string(loc.URI)), + loc.Range.Start.Line+1, + loc.Range.Start.Character+1, + loc.Range.End.Line+1, + loc.Range.End.Character+1) } - for _, loc := range got { - if !want[loc] { - t.Errorf("references failed: incorrect references got %v want %v", loc, want) + toSlice := func(set map[protocol.Location]bool) []protocol.Location { + // TODO(adonovan): use generic maps.Keys(), someday. + list := make([]protocol.Location, 0, len(set)) + for key := range set { + list = append(list, key) } + return list + } + toString := func(locs []protocol.Location) string { + // TODO(adonovan): use generic JoinValues(locs, formatLocation). + strs := make([]string, len(locs)) + for i, loc := range locs { + strs[i] = formatLocation(loc) + } + sort.Strings(strs) + return strings.Join(strs, "\n") + } + gotStr := toString(got) + wantStr := toString(toSlice(want)) + if gotStr != wantStr { + t.Errorf("incorrect references (got %d, want %d) at %s:\n%s", + len(got), len(want), + formatLocation(loc), + diff.Unified("want", "got", wantStr, gotStr)) } }) } diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 1ca1eee384e..5c9c6b1c671 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -365,6 +365,19 @@ func IsPoint(r Range) bool { return r.Start.Line == r.End.Line && r.Start.Character == r.End.Character } +// CompareLocation defines a three-valued comparison over locations, +// lexicographically ordered by (URI, Range). +func CompareLocation(x, y Location) int { + if x.URI != y.URI { + if x.URI < y.URI { + return -1 + } else { + return +1 + } + } + return CompareRange(x.Range, y.Range) +} + // CompareRange returns -1 if a is before b, 0 if a == b, and 1 if a is after // b. // @@ -380,17 +393,19 @@ func CompareRange(a, b Range) int { // ComparePosition returns -1 if a is before b, 0 if a == b, and 1 if a is // after b. func ComparePosition(a, b Position) int { - if a.Line < b.Line { - return -1 - } - if a.Line > b.Line { - return 1 - } - if a.Character < b.Character { - return -1 + if a.Line != b.Line { + if a.Line < b.Line { + return -1 + } else { + return +1 + } } - if a.Character > b.Character { - return 1 + if a.Character != b.Character { + if a.Character < b.Character { + return -1 + } else { + return +1 + } } return 0 } diff --git a/gopls/internal/lsp/references.go b/gopls/internal/lsp/references.go index 390e2908acb..190c1574150 100644 --- a/gopls/internal/lsp/references.go +++ b/gopls/internal/lsp/references.go @@ -21,20 +21,5 @@ func (s *Server) references(ctx context.Context, params *protocol.ReferenceParam if snapshot.View().FileKind(fh) == source.Tmpl { return template.References(ctx, snapshot, fh, params) } - references, err := source.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration) - if err != nil { - return nil, err - } - var locations []protocol.Location - for _, ref := range references { - refRange, err := ref.MappedRange.Range() - if err != nil { - return nil, err - } - locations = append(locations, protocol.Location{ - URI: protocol.URIFromSpanURI(ref.MappedRange.URI()), - Range: refRange, - }) - } - return locations, nil + return source.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration) } diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index 40314866172..b62b6a531ac 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -68,7 +68,13 @@ func IncomingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr ctx, done := event.Start(ctx, "source.IncomingCalls") defer done() - refs, err := References(ctx, snapshot, fh, pos, false) + // TODO(adonovan): switch to referencesV2 here once it supports methods. + // This will require that we parse files containing + // references instead of accessing refs[i].pkg. + // (We could use pre-parser trimming, either a scanner-based + // implementation such as https://go.dev/play/p/KUrObH1YkX8 + // (~31% speedup), or a byte-oriented implementation (2x speedup). + refs, err := referencesV1(ctx, snapshot, fh, pos, false) if err != nil { if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) { return nil, nil diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 626814f3ad1..423f9b4ac18 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -163,7 +163,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if result.Declaration.obj == nil { // If there was no types.Object for the declaration, there might be an // implicit local variable declaration in a type switch. - if objs, typ := typeSwitchImplicits(pkg, path); len(objs) > 0 { + if objs, typ := typeSwitchImplicits(pkg.GetTypesInfo(), path); len(objs) > 0 { // There is no types.Object for the declaration of an implicit local variable, // but all of the types.Objects associated with the usages of this variable can be // used to connect it back to the declaration. @@ -493,7 +493,7 @@ func importSpec(snapshot Snapshot, pkg Package, pgf *ParsedGoFile, pos token.Pos // typeSwitchImplicits returns all the implicit type switch objects that // correspond to the leaf *ast.Ident. It also returns the original type // associated with the identifier (outside of a case clause). -func typeSwitchImplicits(pkg Package, path []ast.Node) ([]types.Object, types.Type) { +func typeSwitchImplicits(info *types.Info, path []ast.Node) ([]types.Object, types.Type) { ident, _ := path[0].(*ast.Ident) if ident == nil { return nil, nil @@ -503,7 +503,7 @@ func typeSwitchImplicits(pkg Package, path []ast.Node) ([]types.Object, types.Ty ts *ast.TypeSwitchStmt assign *ast.AssignStmt cc *ast.CaseClause - obj = pkg.GetTypesInfo().ObjectOf(ident) + obj = info.ObjectOf(ident) ) // Walk our ancestors to determine if our leaf ident refers to a @@ -522,7 +522,7 @@ Outer: // case clause implicitly maps "a" to a different types.Object, // so check if ident's object is the case clause's implicit // object. - if obj != nil && pkg.GetTypesInfo().Implicits[n] == obj { + if obj != nil && info.Implicits[n] == obj { cc = n } case *ast.TypeSwitchStmt: @@ -548,7 +548,7 @@ Outer: // type switch's implicit case clause objects. var objs []types.Object for _, cc := range ts.Body.List { - if ccObj := pkg.GetTypesInfo().Implicits[cc]; ccObj != nil { + if ccObj := info.Implicits[cc]; ccObj != nil { objs = append(objs, ccObj) } } @@ -558,7 +558,7 @@ Outer: var typ types.Type if assign, ok := ts.Assign.(*ast.AssignStmt); ok && len(assign.Rhs) == 1 { if rhs := assign.Rhs[0].(*ast.TypeAssertExpr); ok { - typ = pkg.GetTypesInfo().TypeOf(rhs.X) + typ = info.TypeOf(rhs.X) } } return objs, typ diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index cd2fc860a6e..dd5d148b8ec 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -328,6 +328,8 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s return nil, err } pos := pgf.Tok.Pos(key.offset) + + // TODO(adonovan): replace this section with a call to objectsAt(). path := pathEnclosingObjNode(pgf.File, pos) if path == nil { continue @@ -338,7 +340,7 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s // If leaf represents an implicit type switch object or the type // switch "assign" variable, expand to all of the type switch's // implicit objects. - if implicits, _ := typeSwitchImplicits(searchpkg, path); len(implicits) > 0 { + if implicits, _ := typeSwitchImplicits(searchpkg.GetTypesInfo(), path); len(implicits) > 0 { objs = append(objs, implicits...) } else { obj := searchpkg.GetTypesInfo().ObjectOf(leaf) @@ -355,6 +357,7 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s } objs = append(objs, obj) } + // Get all of the transitive dependencies of the search package. pkgs := make(map[*types.Package]Package) var addPkg func(pkg Package) diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index a85b757d696..a3b3abf3b00 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -31,9 +31,12 @@ type ReferenceInfo struct { isDeclaration bool } -// References returns a list of references for a given identifier within the packages -// containing i.File. Declarations appear first in the result. -func References(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) { +// referencesV1 returns a list of references for a given identifier within the packages +// containing pp. Declarations appear first in the result. +// +// Currently used by Server.{incomingCalls,rename}. +// TODO(adonovan): switch each over to referencesV2 in turn. +func referencesV1(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) { ctx, done := event.Start(ctx, "source.References") defer done() @@ -155,6 +158,9 @@ func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, // if isDeclaration, the first result is an extra item for it. // Only the definition-related fields of qualifiedObject are used. // (Arguably it should accept a smaller data type.) +// +// This implementation serves referencesV1 (the soon-to-be obsolete +// portion of Server.references) and Server.rename. func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, includeDeclaration, includeInterfaceRefs, includeEmbeddedRefs bool) ([]*ReferenceInfo, error) { var ( references []*ReferenceInfo diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go new file mode 100644 index 00000000000..80d6fbf5e17 --- /dev/null +++ b/gopls/internal/lsp/source/references2.go @@ -0,0 +1,496 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +// This file defines a new implementation of the 'references' query +// based on a serializable (and eventually file-based) index +// constructed during type checking, thus avoiding the need to +// type-check packages at search time. In due course it will replace +// the old implementation, which is also used by renaming. +// +// See the ./xrefs/ subpackage for the index construction and lookup. +// +// This implementation does not intermingle objects from distinct +// calls to TypeCheck. + +import ( + "context" + "errors" + "fmt" + "go/ast" + "go/token" + "go/types" + "sort" + "strings" + "sync" + + "golang.org/x/sync/errgroup" + "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" +) + +// A ReferenceInfoV2 describes an identifier that refers to the same +// object as the subject of a References query. +type ReferenceInfoV2 struct { + IsDeclaration bool + Name string // TODO(adonovan): same for all elements; factor out of the slice? + Location protocol.Location +} + +var ErrFallback = errors.New("fallback") + +// References returns a list of all references (sorted with +// 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 Snapshot, fh FileHandle, pp protocol.Position, includeDeclaration bool) ([]protocol.Location, error) { + references, err := referencesV2(ctx, snapshot, fh, pp, includeDeclaration) + if err != nil { + if err == ErrFallback { + // Fall back to old implementation. + // TODO(adonovan): support methods in V2 and eliminate referencesV1. + references, err := referencesV1(ctx, snapshot, fh, pp, includeDeclaration) + if err != nil { + return nil, err + } + var locations []protocol.Location + for _, ref := range references { + // TODO(adonovan): add MappedRange.Location() helper. + refRange, err := ref.MappedRange.Range() + if err != nil { + return nil, err + } + locations = append(locations, protocol.Location{ + URI: protocol.URIFromSpanURI(ref.MappedRange.URI()), + Range: refRange, + }) + } + return locations, nil + } + return nil, err + } + // TODO(adonovan): eliminate references[i].Name field? + // But it may be needed when replacing referencesV1. + var locations []protocol.Location + for _, ref := range references { + locations = append(locations, ref.Location) + } + return locations, nil +} + +// referencesV2 returns a list of all references (sorted with +// definitions before uses) to the object denoted by the identifier at +// the given file/position, searching the entire workspace. +// +// Returns ErrFallback if it can't yet handle the case, indicating +// that we should call the old implementation. +func referencesV2(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfoV2, error) { + ctx, done := event.Start(ctx, "source.References2") + defer done() + + // Is the cursor within the package name declaration? + pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) + if err != nil { + return nil, err + } + + var refs []*ReferenceInfoV2 + if inPackageName { + refs, err = packageReferences(ctx, snapshot, f.URI(), pgf.File.Name.Name) + } else { + refs, err = ordinaryReferences(ctx, snapshot, f.URI(), pp) + } + if err != nil { + return nil, err + } + + sort.Slice(refs, func(i, j int) bool { + x, y := refs[i], refs[j] + if x.IsDeclaration != y.IsDeclaration { + return x.IsDeclaration // decls < refs + } + return protocol.CompareLocation(x.Location, y.Location) < 0 + }) + + // De-duplicate by location, and optionally remove declarations. + out := refs[:0] + for _, ref := range refs { + if !includeDeclaration && ref.IsDeclaration { + continue + } + if len(out) == 0 || out[len(out)-1].Location != ref.Location { + out = append(out, ref) + } + } + refs = out + + return refs, nil +} + +// packageReferences returns a list of references to the package +// declaration of the specified name and uri by searching among the +// import declarations of all packages that directly import the target +// package. +func packageReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pkgname string) ([]*ReferenceInfoV2, error) { + metas, err := snapshot.MetadataForFile(ctx, uri) + if err != nil { + return nil, err + } + if len(metas) == 0 { + return nil, fmt.Errorf("found no package containing %s", uri) + } + + var refs []*ReferenceInfoV2 + + // Find external references to the package declaration + // from each direct import of the package. + // + // The narrowest package is the most broadly imported, + // so we choose it for the external references. + // + // But if the file ends with _test.go then we need to + // find the package it is testing; there's no direct way + // to do that, so pick a file from the same package that + // doesn't end in _test.go and start over. + narrowest := metas[0] + if narrowest.ForTest != "" && strings.HasSuffix(string(uri), "_test.go") { + for _, f := range narrowest.CompiledGoFiles { + if !strings.HasSuffix(string(f), "_test.go") { + return packageReferences(ctx, snapshot, f, pkgname) + } + } + // This package has no non-test files. + // Skip the search for external references. + // (Conceivably one could blank-import an empty package, but why?) + } else { + rdeps, err := snapshot.ReverseDependencies(ctx, narrowest.ID, false) // direct + if err != nil { + return nil, err + } + for _, rdep := range rdeps { + for _, uri := range rdep.CompiledGoFiles { + fh, err := snapshot.GetFile(ctx, uri) + if err != nil { + return nil, err + } + f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + if err != nil { + return nil, err + } + for _, imp := range f.File.Imports { + if rdep.DepsByImpPath[UnquoteImportPath(imp)] == narrowest.ID { + refs = append(refs, &ReferenceInfoV2{ + IsDeclaration: false, + Name: pkgname, + Location: mustLocation(f, imp), + }) + } + } + } + } + } + + // Find interal "references" to the package from + // of each package declaration in the target package itself. + // + // The widest package (possibly a test variant) has the + // greatest number of files and thus we choose it for the + // "internal" references. + widest := metas[len(metas)-1] + for _, uri := range widest.CompiledGoFiles { + fh, err := snapshot.GetFile(ctx, uri) + if err != nil { + return nil, err + } + f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + if err != nil { + return nil, err + } + refs = append(refs, &ReferenceInfoV2{ + IsDeclaration: true, // (one of many) + Name: pkgname, + Location: mustLocation(f, f.File.Name), + }) + } + + return refs, nil +} + +// ordinaryReferences computes references for all ordinary objects (not package declarations). +func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp protocol.Position) ([]*ReferenceInfoV2, error) { + // Strategy: use the reference information computed by the + // type checker to find the declaration. First type-check this + // package to find the declaration, then type check the + // declaring package (which may be different), plus variants, + // to find local (in-package) references. + // Global references are satisfied by the index. + + // Strictly speaking, a wider package could provide a different + // declaration (e.g. because the _test.go files can change the + // meaning of a field or method selection), but the narrower + // package reports the more broadly referenced object. + pkg, pgf, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, NarrowestPackage) + if err != nil { + return nil, err + } + + // Find the selected object (declaration or reference). + pos, err := pgf.Pos(pp) + if err != nil { + return nil, err + } + candidates, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) + if err != nil { + return nil, err + } + + // Pick first object arbitrarily. + // The case variables of a type switch have different + // types but that difference is immaterial here. + var obj types.Object + for obj = range candidates { + break + } + if obj == nil { + return nil, ErrNoIdentFound + } + + // If the object is a method, we need to search for all + // matching implementations and/or interfaces, without + // type-checking everything. + // + // That will require an approach such as the one sketched in + // go.dev/cl/452060. Until then, we simply fall back to the + // old implementation for now. TODO(adonovan): fix. + if fn, ok := obj.(*types.Func); ok && fn.Type().(*types.Signature).Recv() != nil { + return nil, ErrFallback + } + + // nil, error, iota, or other built-in? + if obj.Pkg() == nil { + // For some reason, existing tests require that iota has no references, + // nor an error. TODO(adonovan): do something more principled. + if obj.Name() == "iota" { + return nil, nil + } + + return nil, fmt.Errorf("references to builtin %q are not supported", obj.Name()) + } + + // Find metadata of all packages containing the object's defining file. + // This may include the query pkg, and possibly other variants. + declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) + declURI := span.URIFromPath(declPosn.Filename) + metas, err := snapshot.MetadataForFile(ctx, declURI) + if err != nil { + return nil, err + } + + // We want the ordinary importable package, + // plus any test-augmented variants, since + // declarations in _test.go files may change + // the reference of a selection, or even a + // field into a method or vice versa. + // + // But we don't need intermediate test variants, + // as both their local and global references + // will be covered already by equivalent (though + // not types.Identical) objects in other variants. + RemoveIntermediateTestVariants(metas) + + // The search functions will call report(loc) for each hit. + var ( + refsMu sync.Mutex + refs []*ReferenceInfoV2 + ) + report := func(loc protocol.Location, isDecl bool) { + ref := &ReferenceInfoV2{ + IsDeclaration: isDecl, + Name: obj.Name(), + Location: loc, + } + refsMu.Lock() + refs = append(refs, ref) + refsMu.Unlock() + } + + // Is the object exported? + // (objectpath succeeds for lowercase names, arguably a bug.) + var exportedObjectPath objectpath.Path + if path, err := objectpath.For(obj); err == nil && obj.Exported() { + exportedObjectPath = path + } + + // If it is exported, how far need we search? + // For package-level objects, we need only search the direct importers. + // For fields and methods, we must search transitively. + transitive := obj.Pkg().Scope().Lookup(obj.Name()) != obj + + // Loop over the variants of the declaring package, + // and perform both the local (in-package) and global + // (cross-package) searches, in parallel. + // + // TODO(adonovan): opt: support LSP reference streaming. See: + // - https://github.com/microsoft/vscode-languageserver-node/pull/164 + // - https://github.com/microsoft/language-server-protocol/pull/182 + // + // Careful: this goroutine must not return before group.Wait. + var group errgroup.Group + for _, m := range metas { // for each variant + m := m + + // local + group.Go(func() error { + return localReferences(ctx, snapshot, declURI, declPosn.Offset, m, report) + }) + + if exportedObjectPath == "" { + continue // non-exported + } + + // global + group.Go(func() error { + // Compute the global-scope query for each variant + // of the declaring package in parallel. + // We may assume the rdeps of each variant are disjoint. + rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, transitive) + if err != nil { + return err + } + for _, rdep := range rdeps { + rdep := rdep + group.Go(func() error { + return globalReferences(ctx, snapshot, rdep, m.PkgPath, exportedObjectPath, report) + }) + } + return nil + }) + } + if err := group.Wait(); err != nil { + return nil, err + } + return refs, nil +} + +// localReferences reports (concurrently) each reference to the object +// declared at the specified URI/offset within its enclosing package m. +func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, declOffset int, m *Metadata, report func(loc protocol.Location, isDecl bool)) error { + pkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, m.ID) + if err != nil { + return err + } + pkg := pkgs[0] // narrowest + + // Find declaration of corresponding object + // in this package based on (URI, offset). + pgf, err := pkg.File(declURI) + if err != nil { + return err + } + pos, err := safetoken.Pos(pgf.Tok, declOffset) + if err != nil { + return err + } + targets, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) + if err != nil { + return err // unreachable? (probably caught earlier) + } + + // Report the locations of the declaration(s). + for _, node := range targets { + report(mustLocation(pgf, node), true) + } + + // Scan through syntax looking for uses of one of the target objects. + for _, pgf := range pkg.CompiledGoFiles() { + ast.Inspect(pgf.File, func(n ast.Node) bool { + if id, ok := n.(*ast.Ident); ok { + if used, ok := pkg.GetTypesInfo().Uses[id]; ok && targets[used] != nil { + report(mustLocation(pgf, id), false) + } + } + return true + }) + } + return nil +} + +// objectsAt returns the non-empty set of objects denoted (def or use) +// by the specified position within a file syntax tree, or an error if +// none were found. +// +// The result may contain more than one element because all case +// variables of a type switch appear to be declared at the same +// position. +// +// Each object is mapped to the syntax node that was treated as an +// identifier, which is not always an ast.Ident. +func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Object]ast.Node, error) { + path := pathEnclosingObjNode(file, pos) + if path == nil { + return nil, ErrNoIdentFound + } + + targets := make(map[types.Object]ast.Node) + + switch leaf := path[0].(type) { + case *ast.Ident: + // If leaf represents an implicit type switch object or the type + // switch "assign" variable, expand to all of the type switch's + // implicit objects. + if implicits, _ := typeSwitchImplicits(info, path); len(implicits) > 0 { + for _, obj := range implicits { + targets[obj] = leaf + } + } else { + obj := info.ObjectOf(leaf) + if obj == nil { + return nil, fmt.Errorf("%w for %q", errNoObjectFound, leaf.Name) + } + targets[obj] = leaf + } + case *ast.ImportSpec: + // Look up the implicit *types.PkgName. + obj := info.Implicits[leaf] + if obj == nil { + return nil, fmt.Errorf("%w for import %s", errNoObjectFound, UnquoteImportPath(leaf)) + } + targets[obj] = leaf + } + return targets, nil +} + +// globalReferences reports (concurrently) each cross-package +// reference to the object identified by (pkgPath, objectPath). +func globalReferences(ctx context.Context, snapshot Snapshot, m *Metadata, pkgPath PackagePath, objectPath objectpath.Path, report func(loc protocol.Location, isDecl bool)) error { + // TODO(adonovan): opt: don't actually type-check here, + // since we quite intentionally don't look at type information. + // Instead, access the reference index computed during + // type checking that will in due course be a file-based cache. + pkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, m.ID) + if err != nil { + return err + } + for _, loc := range pkgs[0].ReferencesTo(pkgPath, objectPath) { + report(loc, false) + } + return nil +} + +// mustLocation reports the location interval a syntax node, +// which must belong to m.File! Safe for use only by references2. +func mustLocation(pgf *ParsedGoFile, n ast.Node) protocol.Location { + // TODO(adonovan): add pgf.PosLocation helper. + refRange, err := pgf.PosRange(n.Pos(), n.End()) + if err != nil { + panic(err) // can't happen in references2 + } + return protocol.Location{ + URI: protocol.URIFromSpanURI(pgf.Mapper.URI), + Range: refRange, + } +} diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index fccb91ed638..f27f9d783ad 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -699,48 +699,7 @@ func (r *runner) Hover(t *testing.T, src span.Span, text string) { } func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { - ctx := r.ctx - _, srcRng, err := spanToRange(r.data, src) - if err != nil { - t.Fatal(err) - } - snapshot := r.snapshot - fh, err := snapshot.GetFile(r.ctx, src.URI()) - if err != nil { - t.Fatal(err) - } - for _, includeDeclaration := range []bool{true, false} { - t.Run(fmt.Sprintf("refs-declaration-%v", includeDeclaration), func(t *testing.T) { - want := make(map[span.Span]bool) - for i, pos := range itemList { - // We don't want the first result if we aren't including the declaration. - if i == 0 && !includeDeclaration { - continue - } - want[pos] = true - } - refs, err := source.References(ctx, snapshot, fh, srcRng.Start, includeDeclaration) - if err != nil { - t.Fatalf("failed for %s: %v", src, err) - } - got := make(map[span.Span]bool) - for _, refInfo := range refs { - refSpan, err := refInfo.MappedRange.Span() - if err != nil { - t.Fatal(err) - } - got[refSpan] = true - } - if len(got) != len(want) { - t.Errorf("references failed: different lengths got %v want %v", len(got), len(want)) - } - for spn := range got { - if !want[spn] { - t.Errorf("references failed: incorrect references got %v want locations %v", got, want) - } - } - }) - } + // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go } func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index a2b498fe3af..5b755f74282 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -21,6 +21,7 @@ import ( "golang.org/x/mod/module" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" @@ -790,7 +791,8 @@ type Package interface { ResolveImportPath(path ImportPath) (Package, error) Imports() []Package // new slice of all direct dependencies, unordered HasTypeErrors() bool - DiagnosticsForFile(uri span.URI) []*Diagnostic // new array of list/parse/type errors + DiagnosticsForFile(uri span.URI) []*Diagnostic // new array of list/parse/type errors + ReferencesTo(PackagePath, objectpath.Path) []protocol.Location // new sorted array of xrefs } // A CriticalError is a workspace-wide error that generally prevents gopls from diff --git a/gopls/internal/lsp/source/xrefs/xrefs.go b/gopls/internal/lsp/source/xrefs/xrefs.go new file mode 100644 index 00000000000..db9bab12975 --- /dev/null +++ b/gopls/internal/lsp/source/xrefs/xrefs.go @@ -0,0 +1,212 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package xrefs defines the serializable index of cross-package +// references that is computed during type checking. +// +// See ../references2.go for the 'references' query. +package xrefs + +import ( + "bytes" + "encoding/gob" + "go/ast" + "go/types" + "log" + "sort" + + "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" +) + +// Index constructs a serializable index of outbound cross-references +// for the specified type-checked package. +func Index(pkg source.Package) []byte { + // pkgObjects maps each referenced package Q to a mapping: + // from each referenced symbol in Q to the ordered list + // of references to that symbol from this package. + // A nil types.Object indicates a reference + // to the package as a whole: an import. + pkgObjects := make(map[*types.Package]map[types.Object]*gobObject) + + // getObjects returns the object-to-references mapping for a package. + getObjects := func(pkg *types.Package) map[types.Object]*gobObject { + objects, ok := pkgObjects[pkg] + if !ok { + objects = make(map[types.Object]*gobObject) + pkgObjects[pkg] = objects + } + return objects + } + + for fileIndex, pgf := range pkg.CompiledGoFiles() { + + nodeRange := func(n ast.Node) protocol.Range { + rng, err := pgf.PosRange(n.Pos(), n.End()) + if err != nil { + panic(err) // can't fail + } + return rng + } + + ast.Inspect(pgf.File, func(n ast.Node) bool { + switch n := n.(type) { + case *ast.Ident: + // Report a reference for each identifier that + // uses a symbol exported from another package. + // (The built-in error.Error method has no package.) + if n.IsExported() { + if obj, ok := pkg.GetTypesInfo().Uses[n]; ok && + obj.Pkg() != nil && + obj.Pkg() != pkg.GetTypes() { + + objects := getObjects(obj.Pkg()) + gobObj, ok := objects[obj] + if !ok { + path, err := objectpath.For(obj) + if err != nil { + // Capitalized but not exported + // (e.g. local const/var/type). + return true + } + gobObj = &gobObject{Path: path} + objects[obj] = gobObj + } + + gobObj.Refs = append(gobObj.Refs, gobRef{ + FileIndex: fileIndex, + Range: nodeRange(n), + }) + } + } + + case *ast.ImportSpec: + // Report a reference from each import path + // string to the imported package. + var obj types.Object + if n.Name != nil { + obj = pkg.GetTypesInfo().Defs[n.Name] + } else { + obj = pkg.GetTypesInfo().Implicits[n] + } + if obj == nil { + return true // missing import + } + objects := getObjects(obj.(*types.PkgName).Imported()) + gobObj, ok := objects[nil] + if !ok { + gobObj = &gobObject{Path: ""} + objects[nil] = gobObj + } + gobObj.Refs = append(gobObj.Refs, gobRef{ + FileIndex: fileIndex, + Range: nodeRange(n.Path), + }) + } + return true + }) + } + + // Flatten the maps into slices, and sort for determinism. + var packages []*gobPackage + for p := range pkgObjects { + objects := pkgObjects[p] + gp := &gobPackage{ + PkgPath: source.PackagePath(p.Path()), + Objects: make([]*gobObject, 0, len(objects)), + } + for _, gobObj := range objects { + gp.Objects = append(gp.Objects, gobObj) + } + sort.Slice(gp.Objects, func(i, j int) bool { + return gp.Objects[i].Path < gp.Objects[j].Path + }) + packages = append(packages, gp) + } + sort.Slice(packages, func(i, j int) bool { + return packages[i].PkgPath < packages[j].PkgPath + }) + + return mustEncode(packages) +} + +// Lookup searches a serialized index produced by an indexPackage +// operation on m, and returns the locations of all references from m +// to the object denoted by (pkgPath, objectPath). +func Lookup(m *source.Metadata, data []byte, pkgPath source.PackagePath, objPath objectpath.Path) []protocol.Location { + + // TODO(adonovan): opt: evaluate whether it would be faster to decode + // in two passes, first with struct { PkgPath string; Objects BLOB } + // to find the relevant record without decoding the Objects slice, + // then decode just the desired BLOB into a slice. BLOB would be a + // type whose Unmarshal method just retains (a copy of) the bytes. + var packages []gobPackage + mustDecode(data, &packages) + + for _, gp := range packages { + if gp.PkgPath == pkgPath { + var locs []protocol.Location + for _, gobObj := range gp.Objects { + if gobObj.Path == objPath { + for _, ref := range gobObj.Refs { + uri := m.CompiledGoFiles[ref.FileIndex] + locs = append(locs, protocol.Location{ + URI: protocol.URIFromSpanURI(uri), + Range: ref.Range, + }) + } + } + } + return locs + } + } + return nil // this package does not reference that one +} + +// -- serialized representation -- + +// The cross-reference index records the location of all references +// from one package to symbols defined in other packages +// (dependencies). It does not record within-package references. +// The index for package P consists of a list of gopPackage records, +// each enumerating references to symbols defined a single dependency, Q. + +// TODO(adonovan): opt: choose a more compact encoding. Gzip reduces +// the gob output to about one third its size, so clearly there's room +// to improve. The gobRef.Range field is the obvious place to begin. + +// A gobPackage records the set of outgoing references from the index +// package to symbols defined in a dependency package. +type gobPackage struct { + PkgPath source.PackagePath // defining package (Q) + Objects []*gobObject // set of Q objects referenced by P +} + +// A gobObject records all references to a particular symbol. +type gobObject struct { + Path objectpath.Path // symbol name within package; "" => import of package itself + Refs []gobRef // locations of references within P, in lexical order +} + +type gobRef struct { + FileIndex int // index of enclosing file within P's CompiledGoFiles + Range protocol.Range // source range of reference +} + +// -- duplicated from ../../cache/analysis.go -- + +func mustEncode(x interface{}) []byte { + var buf bytes.Buffer + if err := gob.NewEncoder(&buf).Encode(x); err != nil { + log.Fatalf("internal error encoding %T: %v", x, err) + } + return buf.Bytes() +} + +func mustDecode(data []byte, ptr interface{}) { + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(ptr); err != nil { + log.Fatalf("internal error decoding %T: %v", ptr, err) + } +} diff --git a/gopls/internal/lsp/testdata/references/refs.go b/gopls/internal/lsp/testdata/references/refs.go index 933a36f54e9..e7ff5049430 100644 --- a/gopls/internal/lsp/testdata/references/refs.go +++ b/gopls/internal/lsp/testdata/references/refs.go @@ -1,6 +1,8 @@ // Package refs is a package used to test find references. package refs +import "os" //@mark(osDecl, `"os"`),refs("os", osDecl, osUse) + type i int //@mark(typeI, "i"),refs("i", typeI, argI, returnI, embeddedI) type X struct { @@ -36,3 +38,16 @@ func _() { const ( foo = iota //@refs("iota") ) + +func _(x interface{}) { + // We use the _ prefix because the markers inhabit a single + // namespace and yDecl is already used in ../highlights/highlights.go. + switch _y := x.(type) { //@mark(_yDecl, "_y"),refs("_y", _yDecl, _yInt, _yDefault) + case int: + println(_y) //@mark(_yInt, "_y"),refs("_y", _yDecl, _yInt, _yDefault) + default: + println(_y) //@mark(_yDefault, "_y") + } + + os.Getwd() //@mark(osUse, "os") +} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 075fdc21193..f68800875e3 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -20,7 +20,7 @@ DefinitionsCount = 99 TypeDefinitionsCount = 18 HighlightsCount = 69 InlayHintsCount = 4 -ReferencesCount = 27 +ReferencesCount = 30 RenamesCount = 41 PrepareRenamesCount = 7 SymbolsCount = 1 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index c26b928aba9..3a931ac3c75 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -20,7 +20,7 @@ DefinitionsCount = 110 TypeDefinitionsCount = 18 HighlightsCount = 69 InlayHintsCount = 5 -ReferencesCount = 27 +ReferencesCount = 30 RenamesCount = 48 PrepareRenamesCount = 7 SymbolsCount = 2 From fef5b76e4e042af4a3d889a3014fbdc85167319b Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 5 Jan 2023 23:45:28 -0800 Subject: [PATCH 596/723] go/callgraph: fix slicing in callgraph_test.go Include references to *ssa.Functions when forward slicing for reachable functions. This is the same definition as vulncheck uses for slicing. Change-Id: I8c8634b24a58be3ebf83634eb37e6eb437f3e7b0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460762 Run-TryBot: Tim King gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Zvonimir Pavlinovic --- go/callgraph/callgraph_test.go | 83 +++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/go/callgraph/callgraph_test.go b/go/callgraph/callgraph_test.go index 225656641ac..dd6baafa5ec 100644 --- a/go/callgraph/callgraph_test.go +++ b/go/callgraph/callgraph_test.go @@ -31,11 +31,11 @@ import ( // BenchmarkCHA-12 86 ms/op 16 MB/op 12113 nodes 131717 edges 7640 reachable // BenchmarkRTA-12 110 ms/op 12 MB/op 6566 nodes 42291 edges 5099 reachable // BenchmarkPTA-12 1427 ms/op 600 MB/op 8714 nodes 28244 edges 4184 reachable -// BenchmarkVTA-12 562 ms/op 78 MB/op 12112 nodes 44857 edges 4918 reachable -// BenchmarkVTA2-12 743 ms/op 103 MB/op 4918 nodes 20247 edges 3841 reachable -// BenchmarkVTA3-12 837 ms/op 119 MB/op 3841 nodes 16502 edges 3542 reachable -// BenchmarkVTAAlt-12 356 ms/op 56 MB/op 7640 nodes 29629 edges 4257 reachable -// BenchmarkVTAAlt2-12 490 ms/op 75 MB/op 4257 nodes 18057 edges 3586 reachable +// BenchmarkVTA-12 600 ms/op 78 MB/op 12114 nodes 44861 edges 4919 reachable +// BenchmarkVTA2-12 793 ms/op 104 MB/op 5450 nodes 22208 edges 4042 reachable +// BenchmarkVTA3-12 977 ms/op 124 MB/op 4621 nodes 19331 edges 3700 reachable +// BenchmarkVTAAlt-12 372 ms/op 57 MB/op 7763 nodes 29912 edges 4258 reachable +// BenchmarkVTAAlt2-12 570 ms/op 78 MB/op 4838 nodes 20169 edges 3737 reachable // // Note: // * Static is unsound and may miss real edges. @@ -94,13 +94,13 @@ func example() (*ssa.Program, *ssa.Function) { var stats bool = false // print stats? -func logStats(b *testing.B, name string, cg *callgraph.Graph, main *ssa.Function) { - if stats { +func logStats(b *testing.B, cnd bool, name string, cg *callgraph.Graph, main *ssa.Function) { + if cnd && stats { e := 0 for _, n := range cg.Nodes { e += len(n.Out) } - r := len(reaches(cg, main)) + r := len(reaches(main, cg, false)) b.Logf("%s:\t%d nodes\t%d edges\t%d reachable", name, len(cg.Nodes), e, r) } } @@ -112,7 +112,7 @@ func BenchmarkStatic(b *testing.B) { for i := 0; i < b.N; i++ { cg := static.CallGraph(prog) - logStats(b, "static", cg, main) + logStats(b, i == 0, "static", cg, main) } } @@ -123,7 +123,7 @@ func BenchmarkCHA(b *testing.B) { for i := 0; i < b.N; i++ { cg := cha.CallGraph(prog) - logStats(b, "cha", cg, main) + logStats(b, i == 0, "cha", cg, main) } } @@ -135,7 +135,7 @@ func BenchmarkRTA(b *testing.B) { for i := 0; i < b.N; i++ { res := rta.Analyze([]*ssa.Function{main}, true) cg := res.CallGraph - logStats(b, "rta", cg, main) + logStats(b, i == 0, "rta", cg, main) } } @@ -150,7 +150,7 @@ func BenchmarkPTA(b *testing.B) { if err != nil { b.Fatal(err) } - logStats(b, "pta", res.CallGraph, main) + logStats(b, i == 0, "pta", res.CallGraph, main) } } @@ -161,7 +161,7 @@ func BenchmarkVTA(b *testing.B) { for i := 0; i < b.N; i++ { cg := vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) - logStats(b, "vta", cg, main) + logStats(b, i == 0, "vta", cg, main) } } @@ -172,8 +172,8 @@ func BenchmarkVTA2(b *testing.B) { for i := 0; i < b.N; i++ { vta1 := vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) - cg := vta.CallGraph(reaches(vta1, main), vta1) - logStats(b, "vta2", cg, main) + cg := vta.CallGraph(reaches(main, vta1, true), vta1) + logStats(b, i == 0, "vta2", cg, main) } } @@ -184,9 +184,9 @@ func BenchmarkVTA3(b *testing.B) { for i := 0; i < b.N; i++ { vta1 := vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) - vta2 := vta.CallGraph(reaches(vta1, main), vta1) - cg := vta.CallGraph(reaches(vta2, main), vta2) - logStats(b, "vta3", cg, main) + vta2 := vta.CallGraph(reaches(main, vta1, true), vta1) + cg := vta.CallGraph(reaches(main, vta2, true), vta2) + logStats(b, i == 0, "vta3", cg, main) } } @@ -197,8 +197,8 @@ func BenchmarkVTAAlt(b *testing.B) { for i := 0; i < b.N; i++ { cha := cha.CallGraph(prog) - cg := vta.CallGraph(reaches(cha, main), cha) // start from only functions reachable by CHA. - logStats(b, "vta-alt", cg, main) + cg := vta.CallGraph(reaches(main, cha, true), cha) // start from only functions reachable by CHA. + logStats(b, i == 0, "vta-alt", cg, main) } } @@ -209,24 +209,45 @@ func BenchmarkVTAAlt2(b *testing.B) { for i := 0; i < b.N; i++ { cha := cha.CallGraph(prog) - vta1 := vta.CallGraph(reaches(cha, main), cha) - cg := vta.CallGraph(reaches(vta1, main), vta1) - logStats(b, "vta-alt2", cg, main) + vta1 := vta.CallGraph(reaches(main, cha, true), cha) + cg := vta.CallGraph(reaches(main, vta1, true), vta1) + logStats(b, i == 0, "vta-alt2", cg, main) } } -// reaches returns the set of functions forward reachable from f in g. -func reaches(g *callgraph.Graph, f *ssa.Function) map[*ssa.Function]bool { +// reaches computes the transitive closure of functions forward reachable +// via calls in cg starting from `sources`. If refs is true, include +// functions referred to in an instruction. +func reaches(source *ssa.Function, cg *callgraph.Graph, refs bool) map[*ssa.Function]bool { seen := make(map[*ssa.Function]bool) - var visit func(n *callgraph.Node) - visit = func(n *callgraph.Node) { - if !seen[n.Func] { - seen[n.Func] = true + var visit func(f *ssa.Function) + visit = func(f *ssa.Function) { + if seen[f] { + return + } + seen[f] = true + + if n := cg.Nodes[f]; n != nil { for _, e := range n.Out { - visit(e.Callee) + if e.Site != nil { + visit(e.Callee.Func) + } + } + } + + if refs { + var buf [10]*ssa.Value // avoid alloc in common case + for _, b := range f.Blocks { + for _, instr := range b.Instrs { + for _, op := range instr.Operands(buf[:0]) { + if fn, ok := (*op).(*ssa.Function); ok { + visit(fn) + } + } + } } } } - visit(g.Nodes[f]) + visit(source) return seen } From 6e9a35db6103921bfaf0c26fc9d3b9a2ad36fe58 Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 5 Jan 2023 12:19:36 -0800 Subject: [PATCH 597/723] go/callgraph/cha: refactor callee construction Refactors callee construction to create a function mapping a callsite to its callees. This consumes 4x less memory and is 37% faster than fully constructing a callgraph.Graph on a benchmark for a net/http server. If made public, this could be used to optimize the performance of vulncheck. Updates golang/go#57357 Change-Id: I42faa859d10f30361d3d497a81245af7f61e8a01 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460758 gopls-CI: kokoro Run-TryBot: Tim King Reviewed-by: Zvonimir Pavlinovic TryBot-Result: Gopher Robot --- go/callgraph/cha/cha.go | 100 ++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/go/callgraph/cha/cha.go b/go/callgraph/cha/cha.go index 7075a73cbe8..6296d48d91a 100644 --- a/go/callgraph/cha/cha.go +++ b/go/callgraph/cha/cha.go @@ -40,6 +40,54 @@ func CallGraph(prog *ssa.Program) *callgraph.Graph { allFuncs := ssautil.AllFunctions(prog) + calleesOf := lazyCallees(allFuncs) + + addEdge := func(fnode *callgraph.Node, site ssa.CallInstruction, g *ssa.Function) { + gnode := cg.CreateNode(g) + callgraph.AddEdge(fnode, site, gnode) + } + + addEdges := func(fnode *callgraph.Node, site ssa.CallInstruction, callees []*ssa.Function) { + // Because every call to a highly polymorphic and + // frequently used abstract method such as + // (io.Writer).Write is assumed to call every concrete + // Write method in the program, the call graph can + // contain a lot of duplication. + // + // TODO(taking): opt: consider making lazyCallees public. + // Using the same benchmarks as callgraph_test.go, removing just + // the explicit callgraph.Graph construction is 4x less memory + // and is 37% faster. + // CHA 86 ms/op 16 MB/op + // lazyCallees 63 ms/op 4 MB/op + for _, g := range callees { + addEdge(fnode, site, g) + } + } + + for f := range allFuncs { + fnode := cg.CreateNode(f) + for _, b := range f.Blocks { + for _, instr := range b.Instrs { + if site, ok := instr.(ssa.CallInstruction); ok { + if g := site.Common().StaticCallee(); g != nil { + addEdge(fnode, site, g) + } else { + addEdges(fnode, site, calleesOf(site)) + } + } + } + } + } + + return cg +} + +// lazyCallees returns a function that maps a call site (in a function in fns) +// to its callees within fns. +// +// The resulting function is not concurrency safe. +func lazyCallees(fns map[*ssa.Function]bool) func(site ssa.CallInstruction) []*ssa.Function { // funcsBySig contains all functions, keyed by signature. It is // the effective set of address-taken functions used to resolve // a dynamic call of a particular signature. @@ -81,7 +129,7 @@ func CallGraph(prog *ssa.Program) *callgraph.Graph { return methods } - for f := range allFuncs { + for f := range fns { if f.Signature.Recv() == nil { // Package initializers can never be address-taken. if f.Name() == "init" && f.Synthetic == "package initializer" { @@ -95,45 +143,17 @@ func CallGraph(prog *ssa.Program) *callgraph.Graph { } } - addEdge := func(fnode *callgraph.Node, site ssa.CallInstruction, g *ssa.Function) { - gnode := cg.CreateNode(g) - callgraph.AddEdge(fnode, site, gnode) - } - - addEdges := func(fnode *callgraph.Node, site ssa.CallInstruction, callees []*ssa.Function) { - // Because every call to a highly polymorphic and - // frequently used abstract method such as - // (io.Writer).Write is assumed to call every concrete - // Write method in the program, the call graph can - // contain a lot of duplication. - // - // TODO(adonovan): opt: consider factoring the callgraph - // API so that the Callers component of each edge is a - // slice of nodes, not a singleton. - for _, g := range callees { - addEdge(fnode, site, g) + return func(site ssa.CallInstruction) []*ssa.Function { + call := site.Common() + if call.IsInvoke() { + tiface := call.Value.Type().Underlying().(*types.Interface) + return lookupMethods(tiface, call.Method) + } else if g := call.StaticCallee(); g != nil { + return []*ssa.Function{g} + } else if _, ok := call.Value.(*ssa.Builtin); !ok { + fns, _ := funcsBySig.At(call.Signature()).([]*ssa.Function) + return fns } + return nil } - - for f := range allFuncs { - fnode := cg.CreateNode(f) - for _, b := range f.Blocks { - for _, instr := range b.Instrs { - if site, ok := instr.(ssa.CallInstruction); ok { - call := site.Common() - if call.IsInvoke() { - tiface := call.Value.Type().Underlying().(*types.Interface) - addEdges(fnode, site, lookupMethods(tiface, call.Method)) - } else if g := call.StaticCallee(); g != nil { - addEdge(fnode, site, g) - } else if _, ok := call.Value.(*ssa.Builtin); !ok { - callees, _ := funcsBySig.At(call.Signature()).([]*ssa.Function) - addEdges(fnode, site, callees) - } - } - } - } - } - - return cg } From 6a3bc37a9d8dd9f821c97824fe0c3e552726045c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 6 Jan 2023 17:45:14 -0500 Subject: [PATCH 598/723] gopls/internal/lsp/protocol: reimplement ColumnMapper line logic Before, ColumnMapper relied on token.File and various workarounds such as span.OffsetToLineCol8 and ColumnMapper.OffsetPosition and span.OffsetToLineCol8. Now it implements all the mapping from byte offsets to/from (line, column) itself, using logic mostly plundered from lsppos.Mapper. The line number table is initialized lazily on first use, since only a few percent of instances actually need it. ColumnMapper now returns span.Point values that are fully populated (offset and line+col), and now accepts both kinds of partly populated Point value. Also: - rename Point to PositionPoint and Offset to PositionOffset and document the naming convention. - group conversion methods by argument domain. - add TODOs for an upcoming big renaming. - the span package no longer cares about UTF-16. Most of its tests were saved. Change-Id: If6075b6087e30a1a7e773f2a7dfe1a441adf3497 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460975 gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/lsp_test.go | 17 +- gopls/internal/lsp/mod/hover.go | 2 +- .../protocol/mapper_test.go} | 72 ++- gopls/internal/lsp/protocol/span.go | 444 +++++++++++------- .../internal/lsp/source/completion/package.go | 2 +- gopls/internal/lsp/source/implementation.go | 2 +- gopls/internal/lsp/source/view.go | 5 +- gopls/internal/lsp/work/completion.go | 2 +- gopls/internal/lsp/work/hover.go | 2 +- gopls/internal/span/span.go | 7 +- gopls/internal/span/token.go | 12 +- gopls/internal/span/utf16.go | 105 ----- 12 files changed, 331 insertions(+), 341 deletions(-) rename gopls/internal/{span/utf16_test.go => lsp/protocol/mapper_test.go} (82%) delete mode 100644 gopls/internal/span/utf16.go diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 1c29adfb4b6..1f6b6f1dfde 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -7,7 +7,6 @@ package lsp import ( "context" "fmt" - "go/token" "os" "os/exec" "path/filepath" @@ -363,11 +362,11 @@ func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.Fol // TODO(adonovan): factor to use diff.ApplyEdits, which validates the input. for i := len(ranges) - 1; i >= 0; i-- { r := ranges[i] - start, err := m.Point(protocol.Position{Line: r.StartLine, Character: r.StartCharacter}) + start, err := m.PositionPoint(protocol.Position{Line: r.StartLine, Character: r.StartCharacter}) if err != nil { return "", err } - end, err := m.Point(protocol.Position{Line: r.EndLine, Character: r.EndCharacter}) + end, err := m.PositionPoint(protocol.Position{Line: r.EndLine, Character: r.EndCharacter}) if err != nil { return "", err } @@ -1342,11 +1341,7 @@ func (r *runner) SelectionRanges(t *testing.T, spn span.Span) { fmt.Fprintf(sb, "Ranges %d: ", i) rng := path for { - s, err := sm.Offset(rng.Range.Start) - if err != nil { - t.Error(err) - } - e, err := sm.Offset(rng.Range.End) + s, e, err := sm.RangeOffsets(rng.Range) if err != nil { t.Error(err) } @@ -1387,6 +1382,7 @@ func TestBytesOffset(t *testing.T) { pos protocol.Position want int }{ + // U+10400 encodes as [F0 90 90 80] in UTF-8 and [D801 DC00] in UTF-16. {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0}, {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1}, {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1}, @@ -1405,12 +1401,9 @@ func TestBytesOffset(t *testing.T) { for i, test := range tests { fname := fmt.Sprintf("test %d", i) - fset := token.NewFileSet() - f := fset.AddFile(fname, -1, len(test.text)) - f.SetLinesForContent([]byte(test.text)) uri := span.URIFromPath(fname) mapper := protocol.NewColumnMapper(uri, []byte(test.text)) - got, err := mapper.Point(test.pos) + got, err := mapper.PositionPoint(test.pos) if err != nil && test.want != -1 { t.Errorf("unexpected error: %v", err) } diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index dd869be8049..3236635b1c3 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -41,7 +41,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, if err != nil { return nil, fmt.Errorf("getting modfile handle: %w", err) } - offset, err := pm.Mapper.Offset(position) + offset, err := pm.Mapper.PositionOffset(position) if err != nil { return nil, fmt.Errorf("computing cursor position: %w", err) } diff --git a/gopls/internal/span/utf16_test.go b/gopls/internal/lsp/protocol/mapper_test.go similarity index 82% rename from gopls/internal/span/utf16_test.go rename to gopls/internal/lsp/protocol/mapper_test.go index 5f75095dcf4..cd09a8f8fd8 100644 --- a/gopls/internal/span/utf16_test.go +++ b/gopls/internal/lsp/protocol/mapper_test.go @@ -1,17 +1,22 @@ -// Copyright 2019 The Go Authors. All rights reserved. +// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package span_test +package protocol_test import ( "strings" "testing" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/span" ) -// The funny character below is 4 bytes long in UTF-8; two UTF-16 code points +// This file tests ColumnMapper's logic for converting between +// span.Point and UTF-16 columns. (The strange form attests to an +// earlier abstraction.) + +// 𐐀 is U+10400 = [F0 90 90 80] in UTF-8, [D801 DC00] in UTF-16. var funnyString = []byte("𐐀23\n𐐀45") var toUTF16Tests = []struct { @@ -29,22 +34,16 @@ var toUTF16Tests = []struct { { scenario: "cursor missing content", input: nil, - err: "ToUTF16Column: point is missing position", + offset: -1, + err: "point has neither offset nor line/column", }, { scenario: "cursor missing position", input: funnyString, line: -1, col: -1, - err: "ToUTF16Column: point is missing position", - }, - { - scenario: "cursor missing offset", - input: funnyString, - line: 1, - col: 1, offset: -1, - err: "ToUTF16Column: point is missing offset", + err: "point has neither offset nor line/column", }, { scenario: "zero length input; cursor at first col, first line", @@ -69,7 +68,7 @@ var toUTF16Tests = []struct { input: funnyString, line: 1, col: 5, // 4 + 1 (1-indexed) - offset: 4, + offset: 4, // (unused since we have line+col) resUTF16col: 3, // 2 + 1 (1-indexed) pre: "𐐀", post: "23", @@ -79,7 +78,7 @@ var toUTF16Tests = []struct { input: funnyString, line: 1, col: 7, // 4 + 1 + 1 + 1 (1-indexed) - offset: 6, // 4 + 1 + 1 + offset: 6, // 4 + 1 + 1 (unused since we have line+col) resUTF16col: 5, // 2 + 1 + 1 + 1 (1-indexed) pre: "𐐀23", post: "", @@ -89,7 +88,7 @@ var toUTF16Tests = []struct { input: funnyString, line: 2, col: 1, - offset: 7, // length of first line + offset: 7, // length of first line (unused since we have line+col) resUTF16col: 1, pre: "", post: "𐐀45", @@ -99,7 +98,7 @@ var toUTF16Tests = []struct { input: funnyString, line: 1, col: 5, // 4 + 1 (1-indexed) - offset: 11, // 7 (length of first line) + 4 + offset: 11, // 7 (length of first line) + 4 (unused since we have line+col) resUTF16col: 3, // 2 + 1 (1-indexed) pre: "𐐀", post: "45", @@ -109,7 +108,7 @@ var toUTF16Tests = []struct { input: funnyString, line: 2, col: 7, // 4 + 1 + 1 + 1 (1-indexed) - offset: 13, // 7 (length of first line) + 4 + 1 + 1 + offset: 13, // 7 (length of first line) + 4 + 1 + 1 (unused since we have line+col) resUTF16col: 5, // 2 + 1 + 1 + 1 (1-indexed) pre: "𐐀45", post: "", @@ -119,8 +118,8 @@ var toUTF16Tests = []struct { input: funnyString, line: 2, col: 8, // 4 + 1 + 1 + 1 + 1 (1-indexed) - offset: 14, // 4 + 1 + 1 + 1 - err: "ToUTF16Column: offsets 7-14 outside file contents (13)", + offset: 14, // 4 + 1 + 1 + 1 (unused since we have line+col) + err: "column is beyond end of file", }, } @@ -128,7 +127,6 @@ var fromUTF16Tests = []struct { scenario string input []byte line int // 1-indexed line number (isn't actually used) - offset int // 0-indexed byte offset to beginning of line utf16col int // 1-indexed UTF-16 col number resCol int // 1-indexed byte position in line resOffset int // 0-indexed byte offset into input @@ -140,20 +138,12 @@ var fromUTF16Tests = []struct { scenario: "zero length input; cursor at first col, first line", input: []byte(""), line: 1, - offset: 0, utf16col: 1, resCol: 1, resOffset: 0, pre: "", post: "", }, - { - scenario: "missing offset", - input: funnyString, - line: 1, - offset: -1, - err: "FromUTF16Column: point is missing offset", - }, { scenario: "cursor before funny character", input: funnyString, @@ -188,7 +178,6 @@ var fromUTF16Tests = []struct { scenario: "cursor beyond last character on line", input: funnyString, line: 1, - offset: 0, utf16col: 6, resCol: 7, resOffset: 6, @@ -199,7 +188,6 @@ var fromUTF16Tests = []struct { scenario: "cursor before funny character; second line", input: funnyString, line: 2, - offset: 7, // length of first line utf16col: 1, resCol: 1, resOffset: 7, @@ -210,7 +198,6 @@ var fromUTF16Tests = []struct { scenario: "cursor after funny character; second line", input: funnyString, line: 2, - offset: 7, // length of first line utf16col: 3, // 2 + 1 (1-indexed) resCol: 5, // 4 + 1 (1-indexed) resOffset: 11, // 7 (length of first line) + 4 @@ -221,7 +208,6 @@ var fromUTF16Tests = []struct { scenario: "cursor after last character on second line", input: funnyString, line: 2, - offset: 7, // length of first line utf16col: 5, // 2 + 1 + 1 + 1 (1-indexed) resCol: 7, // 4 + 1 + 1 + 1 (1-indexed) resOffset: 13, // 7 (length of first line) + 4 + 1 + 1 @@ -232,19 +218,10 @@ var fromUTF16Tests = []struct { scenario: "cursor beyond end of file", input: funnyString, line: 2, - offset: 7, utf16col: 6, // 2 + 1 + 1 + 1 + 1(1-indexed) resCol: 8, // 4 + 1 + 1 + 1 + 1 (1-indexed) resOffset: 14, // 7 (length of first line) + 4 + 1 + 1 + 1 - err: "FromUTF16Column: chr goes beyond the content", - }, - { - scenario: "offset beyond end of file", - input: funnyString, - line: 2, - offset: 14, - utf16col: 2, - err: "FromUTF16Column: offset (14) greater than length of content (13)", + err: "column is beyond end of file", }, } @@ -255,7 +232,8 @@ func TestToUTF16(t *testing.T) { t.Skip("expected to fail") } p := span.NewPoint(e.line, e.col, e.offset) - got, err := span.ToUTF16Column(p, e.input) + m := protocol.NewColumnMapper("", e.input) + pos, err := m.PointPosition(p) if err != nil { if err.Error() != e.err { t.Fatalf("expected error %v; got %v", e.err, err) @@ -265,6 +243,7 @@ func TestToUTF16(t *testing.T) { if e.err != "" { t.Fatalf("unexpected success; wanted %v", e.err) } + got := int(pos.Character) + 1 if got != e.resUTF16col { t.Fatalf("expected result %v; got %v", e.resUTF16col, got) } @@ -282,8 +261,11 @@ func TestToUTF16(t *testing.T) { func TestFromUTF16(t *testing.T) { for _, e := range fromUTF16Tests { t.Run(e.scenario, func(t *testing.T) { - p := span.NewPoint(e.line, 1, e.offset) - p, err := span.FromUTF16Column(p, e.utf16col, []byte(e.input)) + m := protocol.NewColumnMapper("", []byte(e.input)) + p, err := m.PositionPoint(protocol.Position{ + Line: uint32(e.line - 1), + Character: uint32(e.utf16col - 1), + }) if err != nil { if err.Error() != e.err { t.Fatalf("expected error %v; got %v", e.err, err) diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 5c9c6b1c671..604407d99d7 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -2,41 +2,48 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// this file contains protocol<->span converters - +// This file defines ColumnMapper, which wraps a file content buffer +// ([]byte) and provides efficient conversion between every kind of +// position representation. +// // Here's a handy guide for your tour of the location zoo: // // Imports: lsppos --> protocol --> span --> token // // lsppos.Mapper = (line offset table, content) // +// protocol: for the LSP protocol. // protocol.ColumnMapper = (URI, Content). Does all offset <=> column conversions. // protocol.MappedRange = (protocol.ColumnMapper, {start,end} int) // protocol.Location = (URI, protocol.Range) // protocol.Range = (start, end Position) // protocol.Position = (line, char uint32) 0-based UTF-16 // +// span: for optional fields; useful for CLIs and tests without access to file contents. // span.Point = (line?, col?, offset?) 1-based UTF-8 // span.Span = (uri URI, start, end span.Point) // span.Range = (file token.File, start, end token.Pos) // +// token: for interaction with the go/* syntax packages: // token.Pos // token.FileSet // token.File // offset int // // TODO(adonovan): simplify this picture: -// - Eliminate the optionality of span.{Span,Point}'s position and offset fields? -// - Move span.Range to package safetoken. Can we eliminate it? +// - Move span.Range to package safetoken. Can we eliminate most uses in gopls? // Without a ColumnMapper it's not really self-contained. // It is mostly used by completion. Given access to complete.mapper, -// it could use a pair byte offsets instead. +// it could use a pair of byte offsets instead. // - Merge lsppos.Mapper and protocol.ColumnMapper. // - Then delete lsppos package. -// - ColumnMapper.OffsetPoint and .Position aren't used outside this package. +// - ColumnMapper.OffsetPoint and .PointPosition aren't used outside this package. // OffsetSpan is barely used, and its user would better off with a MappedRange -// or protocol.Range. The span package data tyes are mostly used in tests +// or protocol.Range. The span package data types are mostly used in tests // and in argument parsing (without access to file content). +// - share core conversion primitives with span package. +// - rename ColumnMapper to just Mapper, since it also maps lines +// <=> offsets, and move it to file mapper.go. package protocol @@ -46,7 +53,9 @@ import ( "go/ast" "go/token" "path/filepath" + "sort" "strings" + "sync" "unicode/utf8" "golang.org/x/tools/gopls/internal/lsp/safetoken" @@ -64,8 +73,12 @@ import ( // as used by the LSP protocol; // // - (line, colRune) pairs, where colRune is a rune index, as used by ParseWork. +// (Not yet implemented, but could easily be.) +// +// All conversion methods are named "FromTo", where From and To are the two types. +// For example, the PointPosition method converts from a Point to a Position. // -// This type does not intrinsically depend on go/token-based +// ColumnMapper does not intrinsically depend on go/token-based // representations. Use safetoken to map between token.Pos <=> byte // offsets, or the convenience methods such as PosPosition, // NodePosition, or NodeRange. @@ -73,47 +86,43 @@ type ColumnMapper struct { URI span.URI Content []byte - // This field provides a line-number table, nothing more. The - // public API of ColumnMapper doesn't implicate it in any - // relationship with any instances of token.File or - // token.FileSet, nor should it. - // - // TODO(adonovan): eliminate this field in a follow-up - // by inlining the line-number table. Then merge this - // type with the nearly identical lsspos.Mapper. - // - // TODO(adonovan): opt: quick experiments suggest that - // ColumnMappers are created for thousands of files but the - // m.lines field is accessed only for a small handful. - // So it would make sense to allocate it lazily. - lines *token.File -} - -// NewColumnMapper creates a new column mapper for the given uri and content. -func NewColumnMapper(uri span.URI, content []byte) *ColumnMapper { - fset := token.NewFileSet() - tf := fset.AddFile(uri.Filename(), -1, len(content)) - tf.SetLinesForContent(content) - - return &ColumnMapper{ - URI: uri, - lines: tf, - Content: content, - } -} + // Line-number information is requested only for a tiny + // fraction of ColumnMappers, so we compute it lazily. + // Call initLines() before accessing fields below. + linesOnce sync.Once + lineStart []int // byte offset of start of ith line (0-based); last=EOF iff \n-terminated + nonASCII bool -func URIFromSpanURI(uri span.URI) DocumentURI { - return DocumentURI(uri) + // TODO(adonovan): adding an extra lineStart entry for EOF + // might simplify every method that accesses it. Try it out. } -func URIFromPath(path string) DocumentURI { - return URIFromSpanURI(span.URIFromPath(path)) +// NewColumnMapper creates a new column mapper for the given URI and content. +func NewColumnMapper(uri span.URI, content []byte) *ColumnMapper { + return &ColumnMapper{URI: uri, Content: content} +} + +// initLines populates the lineStart table. +func (m *ColumnMapper) initLines() { + m.linesOnce.Do(func() { + nlines := bytes.Count(m.Content, []byte("\n")) + m.lineStart = make([]int, 1, nlines+1) // initially []int{0} + for offset, b := range m.Content { + if b == '\n' { + m.lineStart = append(m.lineStart, offset+1) + } + if b >= utf8.RuneSelf { + m.nonASCII = true + } + } + }) } -func (u DocumentURI) SpanURI() span.URI { - return span.URIFromURI(string(u)) -} +// -- conversions from span (UTF-8) domain -- +// Location converts a (UTF-8) span to a protocol (UTF-16) range. +// Precondition: the URIs of Location and ColumnMapper match. +// TODO(adonovan): rename to SpanLocation. func (m *ColumnMapper) Location(s span.Span) (Location, error) { rng, err := m.Range(s) if err != nil { @@ -122,6 +131,9 @@ func (m *ColumnMapper) Location(s span.Span) (Location, error) { return Location{URI: URIFromSpanURI(s.URI()), Range: rng}, nil } +// Range converts a (UTF-8) span to a protocol (UTF-16) range. +// Precondition: the URIs of Span and ColumnMapper match. +// TODO(adonovan): rename to SpanRange func (m *ColumnMapper) Range(s span.Span) (Range, error) { // Assert that we aren't using the wrong mapper. // We check only the base name, and case insensitively, @@ -132,187 +144,263 @@ func (m *ColumnMapper) Range(s span.Span) (Range, error) { if !strings.EqualFold(filepath.Base(string(m.URI)), filepath.Base(string(s.URI()))) { return Range{}, bug.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI()) } - - s, err := s.WithOffset(m.lines) + start, err := m.PointPosition(s.Start()) if err != nil { - return Range{}, err + return Range{}, fmt.Errorf("start: %w", err) } - start, err := m.Position(s.Start()) + end, err := m.PointPosition(s.End()) if err != nil { - return Range{}, err - } - end, err := m.Position(s.End()) - if err != nil { - return Range{}, err + return Range{}, fmt.Errorf("end: %w", err) } return Range{Start: start, End: end}, nil } -// OffsetRange returns a Range for the byte-offset interval Content[start:end], +// PointPosition converts a valid span (UTF-8) point to a protocol (UTF-8) position. +func (m *ColumnMapper) PointPosition(p span.Point) (Position, error) { + if p.HasPosition() { + line, col8 := p.Line()-1, p.Column()-1 // both 0-based + m.initLines() + if line >= len(m.lineStart) { + return Position{}, fmt.Errorf("line number %d out of range (max %d)", line, len(m.lineStart)) + } + offset := m.lineStart[line] + end := offset + col8 + + // Validate column. + if end > len(m.Content) { + return Position{}, fmt.Errorf("column is beyond end of file") + } else if line+1 < len(m.lineStart) && end >= m.lineStart[line+1] { + return Position{}, fmt.Errorf("column is beyond end of line") + } + + char := utf16len(m.Content[offset:end]) + return Position{Line: uint32(line), Character: uint32(char)}, nil + } + if p.HasOffset() { + return m.OffsetPosition(p.Offset()) + } + return Position{}, fmt.Errorf("point has neither offset nor line/column") +} + +// -- conversions from byte offsets -- + +// OffsetRange converts a byte-offset interval to a protocol (UTF-16) range. func (m *ColumnMapper) OffsetRange(start, end int) (Range, error) { + if start > end { + return Range{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) + } startPosition, err := m.OffsetPosition(start) if err != nil { return Range{}, fmt.Errorf("start: %v", err) } - endPosition, err := m.OffsetPosition(end) if err != nil { return Range{}, fmt.Errorf("end: %v", err) } - return Range{Start: startPosition, End: endPosition}, nil } -// OffsetSpan converts a pair of byte offsets to a Span. +// OffsetSpan converts a byte-offset interval to a (UTF-8) span. func (m *ColumnMapper) OffsetSpan(start, end int) (span.Span, error) { if start > end { return span.Span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) } startPoint, err := m.OffsetPoint(start) if err != nil { - return span.Span{}, err + return span.Span{}, fmt.Errorf("start: %v", err) } endPoint, err := m.OffsetPoint(end) if err != nil { - return span.Span{}, err + return span.Span{}, fmt.Errorf("end: %v", err) } return span.New(m.URI, startPoint, endPoint), nil } -// Position returns the protocol position for the specified point, -// which must have a byte offset. -func (m *ColumnMapper) Position(p span.Point) (Position, error) { - if !p.HasOffset() { - return Position{}, fmt.Errorf("point is missing offset") +// OffsetPosition converts a byte offset to a protocol (UTF-16) position. +func (m *ColumnMapper) OffsetPosition(offset int) (Position, error) { + if !(0 <= offset && offset <= len(m.Content)) { + return Position{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) + } + line, col16 := m.lineCol16(offset) + return Position{Line: uint32(line), Character: uint32(col16)}, nil +} + +// lineCol16 converts a valid byte offset to line and UTF-16 column numbers, both 0-based. +func (m *ColumnMapper) lineCol16(offset int) (int, int) { + line, start, cr := m.line(offset) + var col16 int + if m.nonASCII { + col16 = utf16len(m.Content[start:offset]) + } else { + col16 = offset - start } - return m.OffsetPosition(p.Offset()) + if cr { + col16-- // retreat from \r at line end + } + return line, col16 } -// OffsetPosition returns the protocol position of the specified -// offset within m.Content. -func (m *ColumnMapper) OffsetPosition(offset int) (Position, error) { - // We use span.OffsetToLineCol8 for its "line+1 at EOF" workaround. - line, _, err := span.OffsetToLineCol8(m.lines, offset) - if err != nil { - return Position{}, fmt.Errorf("OffsetPosition: %v", err) +// lineCol8 converts a valid byte offset to line and UTF-8 column numbers, both 0-based. +func (m *ColumnMapper) lineCol8(offset int) (int, int) { + line, start, cr := m.line(offset) + col8 := offset - start + if cr { + col8-- // retreat from \r at line end } - // If that workaround executed, skip the usual column computation. - char := 0 - if offset != m.lines.Size() { - char = m.utf16Column(offset) + return line, col8 +} + +// line returns: +// - the 0-based index of the line that encloses the (valid) byte offset; +// - the start offset of that line; and +// - whether the offset denotes a carriage return (\r) at line end. +func (m *ColumnMapper) line(offset int) (int, int, bool) { + m.initLines() + // In effect, binary search returns a 1-based result. + line := sort.Search(len(m.lineStart), func(i int) bool { + return offset < m.lineStart[i] + }) + + // Adjustment for line-endings: \r|\n is the same as |\r\n. + var eol int + if line == len(m.lineStart) { + eol = len(m.Content) // EOF + } else { + eol = m.lineStart[line] - 1 } - return Position{ - Line: uint32(line - 1), - Character: uint32(char), - }, nil + cr := offset == eol && offset > 0 && m.Content[offset-1] == '\r' + + line-- // 0-based + + return line, m.lineStart[line], cr } -// -- go/token domain convenience methods -- +// OffsetPoint converts a byte offset to a span (UTF-8) point. +func (m *ColumnMapper) OffsetPoint(offset int) (span.Point, error) { + if !(0 <= offset && offset <= len(m.Content)) { + return span.Point{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) + } + line, col8 := m.lineCol8(offset) + return span.NewPoint(line+1, col8+1, offset), nil +} -func (m *ColumnMapper) PosPosition(tf *token.File, pos token.Pos) (Position, error) { - offset, err := safetoken.Offset(tf, pos) +// -- conversions from protocol domain -- + +// Span converts a protocol (UTF-16) Location to a (UTF-8) span. +// Precondition: the URIs of Location and ColumnMapper match. +// TODO(adonovan): rename to LocationSpan. +func (m *ColumnMapper) Span(l Location) (span.Span, error) { + // TODO(adonovan): check that l.URI matches m.URI. + return m.RangeSpan(l.Range) +} + +// RangeSpan converts a protocol (UTF-16) range to a (UTF-8) span. +// The resulting span has valid Positions and Offsets. +func (m *ColumnMapper) RangeSpan(r Range) (span.Span, error) { + start, end, err := m.RangeOffsets(r) if err != nil { - return Position{}, err + return span.Span{}, err } - return m.OffsetPosition(offset) + return m.OffsetSpan(start, end) } -func (m *ColumnMapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { - startOffset, endOffset, err := safetoken.Offsets(tf, start, end) +// RangeOffsets converts a protocol (UTF-16) range to start/end byte offsets. +func (m *ColumnMapper) RangeOffsets(r Range) (int, int, error) { + start, err := m.PositionOffset(r.Start) if err != nil { - return Range{}, err + return 0, 0, err } - return m.OffsetRange(startOffset, endOffset) + end, err := m.PositionOffset(r.End) + if err != nil { + return 0, 0, err + } + return start, end, nil } -func (m *ColumnMapper) NodeRange(tf *token.File, node ast.Node) (Range, error) { - return m.PosRange(tf, node.Pos(), node.End()) -} +// PositionOffset converts a protocol (UTF-16) position to a byte offset. +func (m *ColumnMapper) PositionOffset(p Position) (int, error) { + m.initLines() -// utf16Column returns the zero-based column index of the -// specified file offset, measured in UTF-16 codes. -// Precondition: 0 <= offset <= len(m.Content). -func (m *ColumnMapper) utf16Column(offset int) int { - s := m.Content[:offset] - if i := bytes.LastIndex(s, []byte("\n")); i >= 0 { - s = s[i+1:] + // Validate line number. + if p.Line > uint32(len(m.lineStart)) { + return 0, fmt.Errorf("line number %d out of range 0-%d", p.Line, len(m.lineStart)) + } else if p.Line == uint32(len(m.lineStart)) { + if p.Character == 0 { + return len(m.Content), nil // EOF + } + return 0, fmt.Errorf("column is beyond end of file") } - // s is the prefix of the line before offset. - return utf16len(s) -} -// utf16len returns the number of codes in the UTF-16 transcoding of s. -func utf16len(s []byte) int { - var n int - for len(s) > 0 { - n++ + offset := m.lineStart[p.Line] + content := m.Content[offset:] // rest of file from start of enclosing line - // Fast path for ASCII. - if s[0] < 0x80 { - s = s[1:] - continue + // Advance bytes up to the required number of UTF-16 codes. + col8 := 0 + for col16 := 0; col16 < int(p.Character); col16++ { + r, sz := utf8.DecodeRune(content) + if sz == 0 { + return 0, fmt.Errorf("column is beyond end of file") + } + if r == '\n' { + // TODO(adonovan): report an error? + break // column denotes position beyond EOL: truncate } + if sz == 1 && r == utf8.RuneError { + return 0, fmt.Errorf("buffer contains invalid UTF-8 text") + } + content = content[sz:] - r, size := utf8.DecodeRune(s) if r >= 0x10000 { - n++ // surrogate pair + col16++ // rune was encoded by a pair of surrogate UTF-16 codes + + if col16 == int(p.Character) { + break // requested position is in the middle of a rune + } } - s = s[size:] + col8 += sz } - return n + return offset + col8, nil } -func (m *ColumnMapper) Span(l Location) (span.Span, error) { - return m.RangeSpan(l.Range) -} - -// RangeSpan converts a UTF-16 range to a Span with both the -// position (line/col) and offset fields populated. -func (m *ColumnMapper) RangeSpan(r Range) (span.Span, error) { - start, err := m.Point(r.Start) - if err != nil { - return span.Span{}, err - } - end, err := m.Point(r.End) +// PositionPoint converts a protocol (UTF-16) position to a span (UTF-8) point. +// The resulting point has a valid Position and Offset. +func (m *ColumnMapper) PositionPoint(p Position) (span.Point, error) { + offset, err := m.PositionOffset(p) if err != nil { - return span.Span{}, err + return span.Point{}, err } - return span.New(m.URI, start, end).WithAll(m.lines) + line, col8 := m.lineCol8(offset) + + return span.NewPoint(line+1, col8+1, offset), nil } -// Offset returns the utf-8 byte offset of p within the mapped file. -func (m *ColumnMapper) Offset(p Position) (int, error) { - start, err := m.Point(p) +// -- go/token domain convenience methods -- + +// PosPosition converts a token pos to a protocol (UTF-16) position. +func (m *ColumnMapper) PosPosition(tf *token.File, pos token.Pos) (Position, error) { + offset, err := safetoken.Offset(tf, pos) if err != nil { - return 0, err + return Position{}, err } - return start.Offset(), nil + return m.OffsetPosition(offset) } -// OffsetPoint returns the span.Point for the given byte offset. -func (m *ColumnMapper) OffsetPoint(offset int) (span.Point, error) { - // We use span.ToPosition for its "line+1 at EOF" workaround. - line, col8, err := span.OffsetToLineCol8(m.lines, offset) +// PosPosition converts a token range to a protocol (UTF-16) range. +func (m *ColumnMapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { + startOffset, endOffset, err := safetoken.Offsets(tf, start, end) if err != nil { - return span.Point{}, fmt.Errorf("OffsetPoint: %v", err) + return Range{}, err } - return span.NewPoint(line, col8, offset), nil + return m.OffsetRange(startOffset, endOffset) } -// Point returns a span.Point for the protocol position p within the mapped file. -// The resulting point has a valid Position and Offset. -func (m *ColumnMapper) Point(p Position) (span.Point, error) { - line := int(p.Line) + 1 - - // Find byte offset of start of containing line. - offset, err := span.ToOffset(m.lines, line, 1) - if err != nil { - return span.Point{}, err - } - lineStart := span.NewPoint(line, 1, offset) - return span.FromUTF16Column(lineStart, int(p.Character)+1, m.Content) +// PosPosition converts a syntax node range to a protocol (UTF-16) range. +func (m *ColumnMapper) NodeRange(tf *token.File, node ast.Node) (Range, error) { + return m.PosRange(tf, node.Pos(), node.End()) } +// -- MappedRange -- + // OffsetMappedRange returns a MappedRange for the given byte offsets. // A MappedRange can be converted to any other form. func (m *ColumnMapper) OffsetMappedRange(start, end int) (MappedRange, error) { @@ -331,7 +419,7 @@ func (m *ColumnMapper) OffsetMappedRange(start, end int) (MappedRange, error) { // or use a helper such as ParsedGoFile.MappedPosRange. type MappedRange struct { Mapper *ColumnMapper - start, end int // valid byte offsets + start, end int // valid byte offsets: 0 <= start <= end <= len(Mapper.Content) } // Offsets returns the (start, end) byte offsets of this range. @@ -344,12 +432,9 @@ func (mr MappedRange) URI() span.URI { return mr.Mapper.URI } -// TODO(adonovan): once the fluff is removed from all the -// location-conversion methods, it will be obvious that a properly -// constructed MappedRange is always valid and its Range and Span (and -// other) methods simply cannot fail. -// At that point we might want to provide variants of methods such as -// Range and Span below that don't return an error. +// TODO(adonovan): the Range and Span methods of a properly +// constructed MappedRange cannot fail. Change them to panic instead +// of returning the error, for convenience of the callers. // Range returns the range in protocol form. func (mr MappedRange) Range() (Range, error) { @@ -361,6 +446,20 @@ func (mr MappedRange) Span() (span.Span, error) { return mr.Mapper.OffsetSpan(mr.start, mr.end) } +// -- misc helpers -- + +func URIFromSpanURI(uri span.URI) DocumentURI { + return DocumentURI(uri) // simple conversion +} + +func URIFromPath(path string) DocumentURI { + return URIFromSpanURI(span.URIFromPath(path)) // normalizing conversion +} + +func (u DocumentURI) SpanURI() span.URI { + return span.URIFromURI(string(u)) // normalizing conversion +} + func IsPoint(r Range) bool { return r.Start.Line == r.End.Line && r.Start.Character == r.End.Character } @@ -378,8 +477,7 @@ func CompareLocation(x, y Location) int { return CompareRange(x.Range, y.Range) } -// CompareRange returns -1 if a is before b, 0 if a == b, and 1 if a is after -// b. +// CompareRange returns -1 if a is before b, 0 if a == b, and 1 if a is after b. // // A range a is defined to be 'before' b if a.Start is before b.Start, or // a.Start == b.Start and a.End is before b.End. @@ -390,8 +488,7 @@ func CompareRange(a, b Range) int { return ComparePosition(a.End, b.End) } -// ComparePosition returns -1 if a is before b, 0 if a == b, and 1 if a is -// after b. +// ComparePosition returns -1 if a is before b, 0 if a == b, and 1 if a is after b. func ComparePosition(a, b Position) int { if a.Line != b.Line { if a.Line < b.Line { @@ -433,3 +530,26 @@ func (r Range) Format(f fmt.State, _ rune) { func (p Position) Format(f fmt.State, _ rune) { fmt.Fprintf(f, "%v:%v", p.Line, p.Character) } + +// -- implementation helpers -- + +// utf16len returns the number of codes in the UTF-16 transcoding of s. +func utf16len(s []byte) int { + var n int + for len(s) > 0 { + n++ + + // Fast path for ASCII. + if s[0] < 0x80 { + s = s[1:] + continue + } + + r, size := utf8.DecodeRune(s) + if r >= 0x10000 { + n++ // surrogate pair + } + s = s[size:] + } + return n +} diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index 8141954a073..b8270e5f1d2 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -36,7 +36,7 @@ func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh return nil, nil, err } - offset, err := pgf.Mapper.Offset(position) + offset, err := pgf.Mapper.PositionOffset(position) if err != nil { return nil, nil, err } diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index dd5d148b8ec..92765494871 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -249,7 +249,7 @@ func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, p return nil, err } m := protocol.NewColumnMapper(uri, content) - offset, err := m.Offset(pp) + offset, err := m.PositionOffset(pp) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 5b755f74282..9421b50e2cf 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -385,12 +385,13 @@ type ParsedGoFile struct { // -- go/token domain convenience helpers -- // Pos returns the token.Pos of protocol position p within the file. +// TODO(adonovan): rename to PositionPos. func (pgf *ParsedGoFile) Pos(p protocol.Position) (token.Pos, error) { - point, err := pgf.Mapper.Point(p) + offset, err := pgf.Mapper.PositionOffset(p) if err != nil { return token.NoPos, err } - return safetoken.Pos(pgf.Tok, point.Offset()) + return safetoken.Pos(pgf.Tok, offset) } // PosRange returns a protocol Range for the token.Pos interval in this file. diff --git a/gopls/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go index b3682e1f665..223f022a1a5 100644 --- a/gopls/internal/lsp/work/completion.go +++ b/gopls/internal/lsp/work/completion.go @@ -27,7 +27,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.Version if err != nil { return nil, fmt.Errorf("getting go.work file handle: %w", err) } - cursor, err := pw.Mapper.Offset(position) + cursor, err := pw.Mapper.PositionOffset(position) if err != nil { return nil, fmt.Errorf("computing cursor offset: %w", err) } diff --git a/gopls/internal/lsp/work/hover.go b/gopls/internal/lsp/work/hover.go index a29d59ce2cd..1a1b299fd76 100644 --- a/gopls/internal/lsp/work/hover.go +++ b/gopls/internal/lsp/work/hover.go @@ -29,7 +29,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, if err != nil { return nil, fmt.Errorf("getting go.work file handle: %w", err) } - offset, err := pw.Mapper.Offset(position) + offset, err := pw.Mapper.PositionOffset(position) if err != nil { return nil, fmt.Errorf("computing cursor offset: %w", err) } diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go index 36629f0f930..9687bbb9f31 100644 --- a/gopls/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -232,13 +232,14 @@ func (s Span) withPosition(tf *token.File) (Span, error) { return s, nil } -func (s Span) WithOffset(tf *token.File) (Span, error) { +func (s Span) withOffset(tf *token.File) (Span, error) { if err := s.update(tf, false, true); err != nil { return Span{}, err } return s, nil } +// (Currently unused except by test.) func (s Span) WithAll(tf *token.File) (Span, error) { if err := s.update(tf, true, true); err != nil { return Span{}, err @@ -274,7 +275,7 @@ func (s *Span) update(tf *token.File, withPos, withOffset bool) error { } func (p *point) updatePosition(tf *token.File) error { - line, col8, err := OffsetToLineCol8(tf, p.Offset) + line, col8, err := offsetToLineCol8(tf, p.Offset) if err != nil { return err } @@ -284,7 +285,7 @@ func (p *point) updatePosition(tf *token.File) error { } func (p *point) updateOffset(tf *token.File) error { - offset, err := ToOffset(tf, p.Line, p.Column) + offset, err := toOffset(tf, p.Line, p.Column) if err != nil { return err } diff --git a/gopls/internal/span/token.go b/gopls/internal/span/token.go index 2e71cbac00a..1b8a6495f5b 100644 --- a/gopls/internal/span/token.go +++ b/gopls/internal/span/token.go @@ -96,7 +96,7 @@ func FileSpan(file *token.File, start, end token.Pos) (Span, error) { s.v.Start.clean() s.v.End.clean() s.v.clean() - return s.WithOffset(file) + return s.withOffset(file) } func position(tf *token.File, pos token.Pos) (string, int, int, error) { @@ -133,7 +133,7 @@ func offset(tf *token.File, pos token.Pos) (int, error) { // Range converts a Span to a Range that represents the Span for the supplied // File. func (s Span) Range(tf *token.File) (Range, error) { - s, err := s.WithOffset(tf) + s, err := s.withOffset(tf) if err != nil { return Range{}, err } @@ -152,18 +152,16 @@ func (s Span) Range(tf *token.File) (Range, error) { }, nil } -// OffsetToLineCol8 converts a byte offset in the file corresponding to tf into +// offsetToLineCol8 converts a byte offset in the file corresponding to tf into // 1-based line and utf-8 column indexes. -// -// TODO(adonovan): move to safetoken package for consistency? -func OffsetToLineCol8(tf *token.File, offset int) (int, int, error) { +func offsetToLineCol8(tf *token.File, offset int) (int, int, error) { _, line, col8, err := positionFromOffset(tf, offset) return line, col8, err } // ToOffset converts a 1-based line and utf-8 column index into a byte offset // in the file corresponding to tf. -func ToOffset(tf *token.File, line, col int) (int, error) { +func toOffset(tf *token.File, line, col int) (int, error) { if line < 1 { // token.File.LineStart panics if line < 1 return -1, fmt.Errorf("invalid line: %d", line) } diff --git a/gopls/internal/span/utf16.go b/gopls/internal/span/utf16.go deleted file mode 100644 index 0f8e1bcacdf..00000000000 --- a/gopls/internal/span/utf16.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package span - -import ( - "fmt" - "unicode/utf8" -) - -// ToUTF16Column calculates the utf16 column expressed by the point given the -// supplied file contents. -// This is used to convert from the native (always in bytes) column -// representation and the utf16 counts used by some editors. -// -// TODO(adonovan): this function is unused except by its test. Delete, -// or consolidate with (*protocol.ColumnMapper).utf16Column. -func ToUTF16Column(p Point, content []byte) (int, error) { - if !p.HasPosition() { - return -1, fmt.Errorf("ToUTF16Column: point is missing position") - } - if !p.HasOffset() { - return -1, fmt.Errorf("ToUTF16Column: point is missing offset") - } - offset := p.Offset() // 0-based - colZero := p.Column() - 1 // 0-based - if colZero == 0 { - // 0-based column 0, so it must be chr 1 - return 1, nil - } else if colZero < 0 { - return -1, fmt.Errorf("ToUTF16Column: column is invalid (%v)", colZero) - } - // work out the offset at the start of the line using the column - lineOffset := offset - colZero - if lineOffset < 0 || offset > len(content) { - return -1, fmt.Errorf("ToUTF16Column: offsets %v-%v outside file contents (%v)", lineOffset, offset, len(content)) - } - // Use the offset to pick out the line start. - // This cannot panic: offset > len(content) and lineOffset < offset. - start := content[lineOffset:] - - // Now, truncate down to the supplied column. - start = start[:colZero] - - cnt := 0 - for _, r := range string(start) { - cnt++ - if r > 0xffff { - cnt++ - } - } - return cnt + 1, nil // the +1 is for 1-based columns -} - -// FromUTF16Column advances the point by the utf16 character offset given the -// supplied line contents. -// This is used to convert from the utf16 counts used by some editors to the -// native (always in bytes) column representation. -// -// The resulting Point always has an offset. -// -// TODO: it looks like this may incorrectly confer a "position" to the -// resulting Point, when it shouldn't. If p.HasPosition() == false, the -// resulting Point will return p.HasPosition() == true, but have the wrong -// position. -func FromUTF16Column(p Point, chr int, content []byte) (Point, error) { - if !p.HasOffset() { - return Point{}, fmt.Errorf("FromUTF16Column: point is missing offset") - } - // if chr is 1 then no adjustment needed - if chr <= 1 { - return p, nil - } - if p.Offset() >= len(content) { - return p, fmt.Errorf("FromUTF16Column: offset (%v) greater than length of content (%v)", p.Offset(), len(content)) - } - remains := content[p.Offset():] - // scan forward the specified number of characters - for count := 1; count < chr; count++ { - if len(remains) <= 0 { - return Point{}, fmt.Errorf("FromUTF16Column: chr goes beyond the content") - } - r, w := utf8.DecodeRune(remains) - if r == '\n' { - // Per the LSP spec: - // - // > If the character value is greater than the line length it - // > defaults back to the line length. - break - } - remains = remains[w:] - if r >= 0x10000 { - // a two point rune - count++ - // if we finished in a two point rune, do not advance past the first - if count >= chr { - break - } - } - p.v.Column += w - p.v.Offset += w - } - return p, nil -} From 40a1c97a34408ef783f3f174746f18484b0da287 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 6 Jan 2023 13:57:07 -0500 Subject: [PATCH 599/723] gopls/internal/lsp/lsppos: delete Mapper Its sole remaining use in format is replaced by protocol.ColumnMapper. Much of the logic of Mapper lives on in ColumnMapper. Its tests have been ported. Also, re-enable a suppressed test. Change-Id: Ie5ef8a3e28c8221a1d5ae4cf4d26cb05bfe28b34 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460955 Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/lsp_test.go | 6 +- gopls/internal/lsp/lsppos/lsppos.go | 134 ------------------ gopls/internal/lsp/lsppos/lsppos_test.go | 107 -------------- gopls/internal/lsp/protocol/mapper_test.go | 100 +++++++++++++ gopls/internal/lsp/protocol/span.go | 23 +-- gopls/internal/lsp/source/format.go | 11 +- gopls/internal/lsp/source/stub.go | 6 +- .../regtest/completion/completion_test.go | 20 ++- 8 files changed, 132 insertions(+), 275 deletions(-) delete mode 100644 gopls/internal/lsp/lsppos/lsppos.go delete mode 100644 gopls/internal/lsp/lsppos/lsppos_test.go diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 1f6b6f1dfde..b75b54ba12a 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -1390,10 +1390,10 @@ func TestBytesOffset(t *testing.T) { {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6}, {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3}, - {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: 3}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: -1}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7}, - {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: 7}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: -1}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1}, {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, @@ -1405,7 +1405,7 @@ func TestBytesOffset(t *testing.T) { mapper := protocol.NewColumnMapper(uri, []byte(test.text)) got, err := mapper.PositionPoint(test.pos) if err != nil && test.want != -1 { - t.Errorf("unexpected error: %v", err) + t.Errorf("%d: unexpected error: %v", i, err) } if err == nil && got.Offset() != test.want { t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got.Offset()) diff --git a/gopls/internal/lsp/lsppos/lsppos.go b/gopls/internal/lsp/lsppos/lsppos.go deleted file mode 100644 index 425a4f74aeb..00000000000 --- a/gopls/internal/lsp/lsppos/lsppos.go +++ /dev/null @@ -1,134 +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 lsppos provides utilities for working with LSP positions. Much of -// this functionality is duplicated from the internal/span package, but this -// package is simpler and more accurate with respect to newline terminated -// content. -// -// See https://microsoft.github.io/language-server-protocol/specification#textDocuments -// for a description of LSP positions. Notably: -// - Positions are specified by a 0-based line count and 0-based utf-16 -// character offset. -// - Positions are line-ending agnostic: there is no way to specify \r|\n or -// \n|. Instead the former maps to the end of the current line, and the -// latter to the start of the next line. -package lsppos - -import ( - "bytes" - "errors" - "sort" - "unicode/utf8" - - "golang.org/x/tools/gopls/internal/lsp/protocol" -) - -// Mapper maps utf-8 byte offsets to LSP positions for a single file. -type Mapper struct { - nonASCII bool - content []byte - - // Start-of-line positions. If src is newline-terminated, the final entry - // will be len(content). - lines []int -} - -// NewMapper creates a new Mapper for the given content. -func NewMapper(content []byte) *Mapper { - nlines := bytes.Count(content, []byte("\n")) - m := &Mapper{ - content: content, - lines: make([]int, 1, nlines+1), // initially []int{0} - } - for offset, b := range content { - if b == '\n' { - m.lines = append(m.lines, offset+1) - } - if b >= utf8.RuneSelf { - m.nonASCII = true - } - } - return m -} - -// LineColUTF16 returns the 0-based UTF-16 line and character index for the -// given offset. It returns -1, -1 if offset is out of bounds for the file -// being mapped. -func (m *Mapper) LineColUTF16(offset int) (line, char int) { - if offset < 0 || offset > len(m.content) { - return -1, -1 - } - nextLine := sort.Search(len(m.lines), func(i int) bool { - return offset < m.lines[i] - }) - if nextLine == 0 { - return -1, -1 - } - line = nextLine - 1 - start := m.lines[line] - var charOffset int - if m.nonASCII { - charOffset = UTF16len(m.content[start:offset]) - } else { - charOffset = offset - start - } - - var eol int - if line == len(m.lines)-1 { - eol = len(m.content) - } else { - eol = m.lines[line+1] - 1 - } - - // Adjustment for line-endings: \r|\n is the same as |\r\n. - if offset == eol && offset > 0 && m.content[offset-1] == '\r' { - charOffset-- - } - - return line, charOffset -} - -// Position returns the protocol position corresponding to the given offset. It -// returns false if offset is out of bounds for the file being mapped. -func (m *Mapper) Position(offset int) (protocol.Position, bool) { - l, c := m.LineColUTF16(offset) - if l < 0 { - return protocol.Position{}, false - } - return protocol.Position{ - Line: uint32(l), - Character: uint32(c), - }, true -} - -// Range returns the protocol range corresponding to the given start and end -// offsets. -func (m *Mapper) Range(start, end int) (protocol.Range, error) { - startPos, ok := m.Position(start) - if !ok { - return protocol.Range{}, errors.New("invalid start position") - } - endPos, ok := m.Position(end) - if !ok { - return protocol.Range{}, errors.New("invalid end position") - } - - return protocol.Range{Start: startPos, End: endPos}, nil -} - -// UTF16len returns the UTF-16 length of the UTF-8 encoded content, were it to -// be re-encoded as UTF-16. -func UTF16len(buf []byte) int { - // This function copies buf, but microbenchmarks showed it to be faster than - // using utf8.DecodeRune due to inlining and avoiding bounds checks. - cnt := 0 - for _, r := range string(buf) { - cnt++ - if r >= 1<<16 { - cnt++ - } - } - return cnt -} diff --git a/gopls/internal/lsp/lsppos/lsppos_test.go b/gopls/internal/lsp/lsppos/lsppos_test.go deleted file mode 100644 index f65b64ff804..00000000000 --- a/gopls/internal/lsp/lsppos/lsppos_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package lsppos_test - -import ( - "fmt" - "strings" - "testing" - - . "golang.org/x/tools/gopls/internal/lsp/lsppos" - "golang.org/x/tools/gopls/internal/lsp/protocol" -) - -type testCase struct { - content string // input text - substrOrOffset interface{} // explicit integer offset, or a substring - wantLine, wantChar int // expected LSP position information -} - -// offset returns the test case byte offset -func (c testCase) offset() int { - switch x := c.substrOrOffset.(type) { - case int: - return x - case string: - i := strings.Index(c.content, x) - if i < 0 { - panic(fmt.Sprintf("%q does not contain substring %q", c.content, x)) - } - return i - } - panic("substrOrIndex must be an integer or string") -} - -var tests = []testCase{ - {"a𐐀b", "a", 0, 0}, - {"a𐐀b", "𐐀", 0, 1}, - {"a𐐀b", "b", 0, 3}, - {"a𐐀b\n", "\n", 0, 4}, - {"a𐐀b\r\n", "\n", 0, 4}, // \r|\n is not a valid position, so we move back to the end of the first line. - {"a𐐀b\r\nx", "x", 1, 0}, - {"a𐐀b\r\nx\ny", "y", 2, 0}, - - // Testing EOL and EOF positions - {"", 0, 0, 0}, // 0th position of an empty buffer is (0, 0) - {"abc", "c", 0, 2}, - {"abc", 3, 0, 3}, - {"abc\n", "\n", 0, 3}, - {"abc\n", 4, 1, 0}, // position after a newline is on the next line -} - -func TestLineChar(t *testing.T) { - for _, test := range tests { - m := NewMapper([]byte(test.content)) - offset := test.offset() - gotLine, gotChar := m.LineColUTF16(offset) - if gotLine != test.wantLine || gotChar != test.wantChar { - t.Errorf("LineChar(%d) = (%d,%d), want (%d,%d)", offset, gotLine, gotChar, test.wantLine, test.wantChar) - } - } -} - -func TestInvalidOffset(t *testing.T) { - content := []byte("a𐐀b\r\nx\ny") - m := NewMapper(content) - for _, offset := range []int{-1, 100} { - gotLine, gotChar := m.LineColUTF16(offset) - if gotLine != -1 { - t.Errorf("LineChar(%d) = (%d,%d), want (-1,-1)", offset, gotLine, gotChar) - } - } -} - -func TestPosition(t *testing.T) { - for _, test := range tests { - m := NewMapper([]byte(test.content)) - offset := test.offset() - got, ok := m.Position(offset) - if !ok { - t.Error("invalid position for", test.substrOrOffset) - continue - } - want := protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)} - if got != want { - t.Errorf("Position(%d) = %v, want %v", offset, got, want) - } - } -} - -func TestRange(t *testing.T) { - for _, test := range tests { - m := NewMapper([]byte(test.content)) - offset := test.offset() - got, err := m.Range(0, offset) - if err != nil { - t.Fatal(err) - } - want := protocol.Range{ - End: protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)}, - } - if got != want { - t.Errorf("Range(%d) = %v, want %v", offset, got, want) - } - } -} diff --git a/gopls/internal/lsp/protocol/mapper_test.go b/gopls/internal/lsp/protocol/mapper_test.go index cd09a8f8fd8..f57f4bc85de 100644 --- a/gopls/internal/lsp/protocol/mapper_test.go +++ b/gopls/internal/lsp/protocol/mapper_test.go @@ -5,6 +5,7 @@ package protocol_test import ( + "fmt" "strings" "testing" @@ -183,6 +184,7 @@ var fromUTF16Tests = []struct { resOffset: 6, pre: "𐐀23", post: "", + err: "column is beyond end of line", }, { scenario: "cursor before funny character; second line", @@ -302,3 +304,101 @@ func getPrePost(content []byte, offset int) (string, string) { } return pre, post } + +// -- these are the historical lsppos tests -- + +type testCase struct { + content string // input text + substrOrOffset interface{} // explicit integer offset, or a substring + wantLine, wantChar int // expected LSP position information +} + +// offset returns the test case byte offset +func (c testCase) offset() int { + switch x := c.substrOrOffset.(type) { + case int: + return x + case string: + i := strings.Index(c.content, x) + if i < 0 { + panic(fmt.Sprintf("%q does not contain substring %q", c.content, x)) + } + return i + } + panic("substrOrIndex must be an integer or string") +} + +var tests = []testCase{ + {"a𐐀b", "a", 0, 0}, + {"a𐐀b", "𐐀", 0, 1}, + {"a𐐀b", "b", 0, 3}, + {"a𐐀b\n", "\n", 0, 4}, + {"a𐐀b\r\n", "\n", 0, 4}, // \r|\n is not a valid position, so we move back to the end of the first line. + {"a𐐀b\r\nx", "x", 1, 0}, + {"a𐐀b\r\nx\ny", "y", 2, 0}, + + // Testing EOL and EOF positions + {"", 0, 0, 0}, // 0th position of an empty buffer is (0, 0) + {"abc", "c", 0, 2}, + {"abc", 3, 0, 3}, + {"abc\n", "\n", 0, 3}, + {"abc\n", 4, 1, 0}, // position after a newline is on the next line +} + +func TestLineChar(t *testing.T) { + for _, test := range tests { + m := protocol.NewColumnMapper("", []byte(test.content)) + offset := test.offset() + posn, _ := m.OffsetPosition(offset) + gotLine, gotChar := int(posn.Line), int(posn.Character) + if gotLine != test.wantLine || gotChar != test.wantChar { + t.Errorf("LineChar(%d) = (%d,%d), want (%d,%d)", offset, gotLine, gotChar, test.wantLine, test.wantChar) + } + } +} + +func TestInvalidOffset(t *testing.T) { + content := []byte("a𐐀b\r\nx\ny") + m := protocol.NewColumnMapper("", content) + for _, offset := range []int{-1, 100} { + posn, err := m.OffsetPosition(offset) + if err == nil { + t.Errorf("OffsetPosition(%d) = %s, want error", offset, posn) + } + } +} + +func TestPosition(t *testing.T) { + for _, test := range tests { + m := protocol.NewColumnMapper("", []byte(test.content)) + offset := test.offset() + got, err := m.OffsetPosition(offset) + if err != nil { + t.Errorf("OffsetPosition(%d) failed: %v", offset, err) + continue + } + want := protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)} + if got != want { + t.Errorf("Position(%d) = %v, want %v", offset, got, want) + } + } +} + +func TestRange(t *testing.T) { + for _, test := range tests { + m := protocol.NewColumnMapper("", []byte(test.content)) + offset := test.offset() + got, err := m.OffsetRange(0, offset) + if err != nil { + t.Fatal(err) + } + want := protocol.Range{ + End: protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)}, + } + if got != want { + t.Errorf("Range(%d) = %v, want %v", offset, got, want) + } + } +} + +// -- end -- diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 604407d99d7..d07614cc030 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -8,9 +8,7 @@ // // Here's a handy guide for your tour of the location zoo: // -// Imports: lsppos --> protocol --> span --> token -// -// lsppos.Mapper = (line offset table, content) +// Imports: protocol --> span --> token // // protocol: for the LSP protocol. // protocol.ColumnMapper = (URI, Content). Does all offset <=> column conversions. @@ -29,14 +27,13 @@ // token.FileSet // token.File // offset int +// (see also safetoken) // // TODO(adonovan): simplify this picture: // - Move span.Range to package safetoken. Can we eliminate most uses in gopls? // Without a ColumnMapper it's not really self-contained. // It is mostly used by completion. Given access to complete.mapper, // it could use a pair of byte offsets instead. -// - Merge lsppos.Mapper and protocol.ColumnMapper. -// - Then delete lsppos package. // - ColumnMapper.OffsetPoint and .PointPosition aren't used outside this package. // OffsetSpan is barely used, and its user would better off with a MappedRange // or protocol.Range. The span package data types are mostly used in tests @@ -44,6 +41,14 @@ // - share core conversion primitives with span package. // - rename ColumnMapper to just Mapper, since it also maps lines // <=> offsets, and move it to file mapper.go. +// +// TODO(adonovan): also, write an overview of the position landscape +// in the ColumnMapper doc comment, mentioning the various subtleties, +// the EOF+1 bug (#57490), the \n-at-EOF bug (#41029), the workarounds +// for both bugs in both safetoken and ColumnMapper. Also mention that +// export data doesn't currently preserve accurate column or offset +// information: both are set to garbage based on the assumption of a +// "rectangular" file. package protocol @@ -67,7 +72,7 @@ import ( // from byte offsets to and from other notations of position: // // - (line, col8) pairs, where col8 is a 1-based UTF-8 column number (bytes), -// as used by go/token; +// as used by the go/token and span packages. // // - (line, col16) pairs, where col16 is a 1-based UTF-16 column number, // as used by the LSP protocol; @@ -155,7 +160,7 @@ func (m *ColumnMapper) Range(s span.Span) (Range, error) { return Range{Start: start, End: end}, nil } -// PointPosition converts a valid span (UTF-8) point to a protocol (UTF-8) position. +// PointPosition converts a valid span (UTF-8) point to a protocol (UTF-16) position. func (m *ColumnMapper) PointPosition(p span.Point) (Position, error) { if p.HasPosition() { line, col8 := p.Line()-1, p.Column()-1 // both 0-based @@ -342,8 +347,7 @@ func (m *ColumnMapper) PositionOffset(p Position) (int, error) { return 0, fmt.Errorf("column is beyond end of file") } if r == '\n' { - // TODO(adonovan): report an error? - break // column denotes position beyond EOL: truncate + return 0, fmt.Errorf("column is beyond end of line") } if sz == 1 && r == utf8.RuneError { return 0, fmt.Errorf("buffer contains invalid UTF-8 text") @@ -435,6 +439,7 @@ func (mr MappedRange) URI() span.URI { // TODO(adonovan): the Range and Span methods of a properly // constructed MappedRange cannot fail. Change them to panic instead // of returning the error, for convenience of the callers. +// This means we can also add a String() method! // Range returns the range in protocol form. func (mr MappedRange) Range() (Range, error) { diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index a2722186a9f..51ba8c6ff44 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -16,7 +16,6 @@ import ( "strings" "text/scanner" - "golang.org/x/tools/gopls/internal/lsp/lsppos" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/diff" @@ -315,10 +314,10 @@ func computeTextEdits(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile, // protocolEditsFromSource converts text edits to LSP edits using the original // source. func protocolEditsFromSource(src []byte, edits []diff.Edit) ([]protocol.TextEdit, error) { - m := lsppos.NewMapper(src) + m := protocol.NewColumnMapper("", src) var result []protocol.TextEdit for _, edit := range edits { - rng, err := m.Range(edit.Start, edit.End) + rng, err := m.OffsetRange(edit.Start, edit.End) if err != nil { return nil, err } @@ -367,13 +366,13 @@ func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]d } result := make([]diff.Edit, len(edits)) for i, edit := range edits { - spn, err := m.RangeSpan(edit.Range) + start, end, err := m.RangeOffsets(edit.Range) if err != nil { return nil, err } result[i] = diff.Edit{ - Start: spn.Start().Offset(), - End: spn.End().Offset(), + Start: start, + End: end, New: edit.NewText, } } diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 0d94d4825da..dd41fbeb85a 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -218,11 +218,7 @@ func deduceIfaceName(concretePkg, ifacePkg *types.Package, ifaceObj types.Object } func getStubNodes(pgf *ParsedGoFile, pRng protocol.Range) ([]ast.Node, token.Pos, error) { - spn, err := pgf.Mapper.RangeSpan(pRng) - if err != nil { - return nil, 0, err - } - rng, err := spn.Range(pgf.Tok) + rng, err := pgf.RangeToSpanRange(pRng) if err != nil { return nil, 0, err } diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index f8621e31e95..a4ac9ed8491 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -79,7 +79,7 @@ package ` var ( testfile4 = "" - //testfile5 = "/*a comment*/ " + testfile5 = "/*a comment*/ " testfile6 = "/*a comment*/\n" ) for _, tc := range []struct { @@ -137,16 +137,14 @@ package want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, editRegexp: "^$", }, - // Disabled pending correct implementation of EOF positions - // in CL 460975 (next CL in stack) following approach of lsppos.Mapper. - // { - // name: "package completion without terminal newline", - // filename: "fruits/testfile5.go", - // triggerRegexp: `\*\/ ()`, - // content: &testfile5, - // want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, - // editRegexp: `\*\/ ()`, - // }, + { + name: "package completion without terminal newline", + filename: "fruits/testfile5.go", + triggerRegexp: `\*\/ ()`, + content: &testfile5, + want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, + editRegexp: `\*\/ ()`, + }, { name: "package completion on terminal newline", filename: "fruits/testfile6.go", From 55935f417da11f03aeeba4dd8b53ef13652f583f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sun, 8 Jan 2023 10:56:46 -0500 Subject: [PATCH 600/723] gopls/internal/span: simplify Span This change moves span.Range to safetoken.Range, since it's in the token domain. The span package now doesn't care about the token domain (except for one testing-related hook). Spans are now just a set of optional offset, line, and column, any subset of which may be populated. All conversions between these forms, or to other forms such as protocol or token, are done by ColumnMapper. The With{Position,Offset,All} methods and their redundant implementations of the conversion logic (and the necessary workarounds) are gone. Also: - rename pgf.RangeToSpanRange to RangeToSpanRange. - add ColumnMapper.PosLocation method - typeErrorDiagnostics uses it, avoiding last call to span.FileSpan. - delete span.FileSpan - delete Span.Range method - update TestFormat now that there is no "filling in" of Spans based on a token.File. Change-Id: I04000a1666d9e5333070dbbfac397f88bba23e11 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460936 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan Reviewed-by: Robert Findley Auto-Submit: Alan Donovan gopls-CI: kokoro --- .../lsp/analysis/fillstruct/fillstruct.go | 3 +- .../lsp/analysis/undeclaredname/undeclared.go | 3 +- gopls/internal/lsp/cache/errors.go | 41 ++-- gopls/internal/lsp/code_action.go | 2 +- gopls/internal/lsp/protocol/span.go | 22 +- gopls/internal/lsp/safetoken/range.go | 57 +++++ .../lsp/source/completion/completion.go | 12 +- .../lsp/source/completion/definition.go | 4 +- .../internal/lsp/source/completion/package.go | 6 +- gopls/internal/lsp/source/extract.go | 29 ++- gopls/internal/lsp/source/fix.go | 11 +- gopls/internal/lsp/source/inlay_hint.go | 2 +- gopls/internal/lsp/source/stub.go | 2 +- gopls/internal/lsp/source/view.go | 8 +- gopls/internal/span/span.go | 87 ++------ gopls/internal/span/span_test.go | 77 +++---- gopls/internal/span/token.go | 195 ------------------ gopls/internal/span/token_test.go | 80 ------- 18 files changed, 177 insertions(+), 464 deletions(-) create mode 100644 gopls/internal/lsp/safetoken/range.go delete mode 100644 gopls/internal/span/token.go delete mode 100644 gopls/internal/span/token_test.go diff --git a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go index 00857a0953e..1b331572073 100644 --- a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -27,7 +27,6 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/lsp/safetoken" - "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/typeparams" @@ -136,7 +135,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // SuggestedFix computes the suggested fix for the kinds of // diagnostics produced by the Analyzer above. -func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { +func SuggestedFix(fset *token.FileSet, rng safetoken.Range, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { if info == nil { return nil, fmt.Errorf("nil types.Info") } diff --git a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go index 3e42f08d55b..996bf2ddb60 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -19,7 +19,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/lsp/safetoken" - "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" ) @@ -122,7 +121,7 @@ func runForError(pass *analysis.Pass, err types.Error) { }) } -func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { +func SuggestedFix(fset *token.FileSet, rng safetoken.Range, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { pos := rng.Start // don't use the end path, _ := astutil.PathEnclosingInterval(file, pos, pos) if len(path) < 2 { diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 67771931bbf..16c0e78f1af 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -103,17 +103,13 @@ var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`) var unsupportedFeatureRe = regexp.MustCompile(`.*require.* go(\d+\.\d+) or later`) func typeErrorDiagnostics(snapshot *snapshot, pkg *pkg, e extendedError) ([]*source.Diagnostic, error) { - code, spn, err := typeErrorData(pkg, e.primary) - if err != nil { - return nil, err - } - rng, err := spanToRange(pkg, spn) + code, loc, err := typeErrorData(pkg, e.primary) if err != nil { return nil, err } diag := &source.Diagnostic{ - URI: spn.URI(), - Range: rng, + URI: loc.URI.SpanURI(), + Range: loc.Range, Severity: protocol.SeverityError, Source: source.TypeError, Message: e.primary.Msg, @@ -128,29 +124,25 @@ func typeErrorDiagnostics(snapshot *snapshot, pkg *pkg, e extendedError) ([]*sou } for _, secondary := range e.secondaries { - _, secondarySpan, err := typeErrorData(pkg, secondary) - if err != nil { - return nil, err - } - rng, err := spanToRange(pkg, secondarySpan) + _, secondaryLoc, err := typeErrorData(pkg, secondary) if err != nil { return nil, err } diag.Related = append(diag.Related, source.RelatedInformation{ - URI: secondarySpan.URI(), - Range: rng, + URI: secondaryLoc.URI.SpanURI(), + Range: secondaryLoc.Range, Message: secondary.Msg, }) } if match := importErrorRe.FindStringSubmatch(e.primary.Msg); match != nil { - diag.SuggestedFixes, err = goGetQuickFixes(snapshot, spn.URI(), match[1]) + diag.SuggestedFixes, err = goGetQuickFixes(snapshot, loc.URI.SpanURI(), match[1]) if err != nil { return nil, err } } if match := unsupportedFeatureRe.FindStringSubmatch(e.primary.Msg); match != nil { - diag.SuggestedFixes, err = editGoDirectiveQuickFix(snapshot, spn.URI(), match[1]) + diag.SuggestedFixes, err = editGoDirectiveQuickFix(snapshot, loc.URI.SpanURI(), match[1]) if err != nil { return nil, err } @@ -294,7 +286,7 @@ func relatedInformation(diag *gobDiagnostic) []source.RelatedInformation { return out } -func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Span, error) { +func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, protocol.Location, error) { ecode, start, end, ok := typesinternal.ReadGo116ErrorData(terr) if !ok { start, end = terr.Pos, terr.Pos @@ -303,30 +295,27 @@ func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Sp // go/types may return invalid positions in some cases, such as // in errors on tokens missing from the syntax tree. if !start.IsValid() { - return 0, span.Span{}, fmt.Errorf("type error (%q, code %d, go116=%t) without position", terr.Msg, ecode, ok) + return 0, protocol.Location{}, fmt.Errorf("type error (%q, code %d, go116=%t) without position", terr.Msg, ecode, ok) } // go/types errors retain their FileSet. // Sanity-check that we're using the right one. fset := pkg.FileSet() if fset != terr.Fset { - return 0, span.Span{}, bug.Errorf("wrong FileSet for type error") + return 0, protocol.Location{}, bug.Errorf("wrong FileSet for type error") } posn := safetoken.StartPosition(fset, start) if !posn.IsValid() { - return 0, span.Span{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr) + return 0, protocol.Location{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr) } pgf, err := pkg.File(span.URIFromPath(posn.Filename)) if err != nil { - return 0, span.Span{}, err + return 0, protocol.Location{}, err } if !end.IsValid() || end == start { end = analysisinternal.TypeErrorEndPos(fset, pgf.Src, start) } - spn, err := span.FileSpan(pgf.Tok, start, end) - if err != nil { - return 0, span.Span{}, err - } - return ecode, spn, nil + loc, err := pgf.Mapper.PosLocation(pgf.Tok, start, end) + return ecode, loc, err } // spanToRange converts a span.Span to a protocol.Range, diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 5e0a778d5b7..8f39ea97ece 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -316,7 +316,7 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI if err != nil { return nil, fmt.Errorf("getting file for Identifier: %w", err) } - srng, err := pgf.RangeToSpanRange(rng) + srng, err := pgf.RangeToTokenRange(rng) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index d07614cc030..14dbdeacc75 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -20,9 +20,9 @@ // span: for optional fields; useful for CLIs and tests without access to file contents. // span.Point = (line?, col?, offset?) 1-based UTF-8 // span.Span = (uri URI, start, end span.Point) -// span.Range = (file token.File, start, end token.Pos) // // token: for interaction with the go/* syntax packages: +// safetoken.Range = (file token.File, start, end token.Pos) // token.Pos // token.FileSet // token.File @@ -30,15 +30,14 @@ // (see also safetoken) // // TODO(adonovan): simplify this picture: -// - Move span.Range to package safetoken. Can we eliminate most uses in gopls? -// Without a ColumnMapper it's not really self-contained. +// - Eliminate most/all uses of safetoken.Range in gopls, as +// without a ColumnMapper it's not really self-contained. // It is mostly used by completion. Given access to complete.mapper, // it could use a pair of byte offsets instead. // - ColumnMapper.OffsetPoint and .PointPosition aren't used outside this package. // OffsetSpan is barely used, and its user would better off with a MappedRange // or protocol.Range. The span package data types are mostly used in tests // and in argument parsing (without access to file content). -// - share core conversion primitives with span package. // - rename ColumnMapper to just Mapper, since it also maps lines // <=> offsets, and move it to file mapper.go. // @@ -206,6 +205,7 @@ func (m *ColumnMapper) OffsetRange(start, end int) (Range, error) { } // OffsetSpan converts a byte-offset interval to a (UTF-8) span. +// The resulting span contains line, column, and offset information. func (m *ColumnMapper) OffsetSpan(start, end int) (span.Span, error) { if start > end { return span.Span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) @@ -281,6 +281,7 @@ func (m *ColumnMapper) line(offset int) (int, int, bool) { } // OffsetPoint converts a byte offset to a span (UTF-8) point. +// The resulting point contains line, column, and offset information. func (m *ColumnMapper) OffsetPoint(offset int) (span.Point, error) { if !(0 <= offset && offset <= len(m.Content)) { return span.Point{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) @@ -389,6 +390,19 @@ func (m *ColumnMapper) PosPosition(tf *token.File, pos token.Pos) (Position, err return m.OffsetPosition(offset) } +// PosPosition converts a token range to a protocol (UTF-16) location. +func (m *ColumnMapper) PosLocation(tf *token.File, start, end token.Pos) (Location, error) { + startOffset, endOffset, err := safetoken.Offsets(tf, start, end) + if err != nil { + return Location{}, err + } + rng, err := m.OffsetRange(startOffset, endOffset) + if err != nil { + return Location{}, err + } + return Location{URI: URIFromSpanURI(m.URI), Range: rng}, nil +} + // PosPosition converts a token range to a protocol (UTF-16) range. func (m *ColumnMapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { startOffset, endOffset, err := safetoken.Offsets(tf, start, end) diff --git a/gopls/internal/lsp/safetoken/range.go b/gopls/internal/lsp/safetoken/range.go new file mode 100644 index 00000000000..827a2572785 --- /dev/null +++ b/gopls/internal/lsp/safetoken/range.go @@ -0,0 +1,57 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package safetoken + +import "go/token" + +// Range represents a source code range in token.Pos form. +// +// It also carries the token.File that produced the position, +// so that it is capable of returning (file, line, col8) information. +// However it cannot be converted to protocol (UTF-16) form +// without access to file content; to do that, use a protocol.ContentMapper. +type Range struct { + TokFile *token.File // non-nil + Start, End token.Pos // both IsValid() +} + +// NewRange creates a new Range from a token.File and two positions within it. +// The given start position must be valid; if end is invalid, start is used as +// the end position. +// +// (If you only have a token.FileSet, use file = fset.File(start). But +// most callers know exactly which token.File they're dealing with and +// should pass it explicitly. Not only does this save a lookup, but it +// brings us a step closer to eliminating the global FileSet.) +func NewRange(file *token.File, start, end token.Pos) Range { + if file == nil { + panic("nil *token.File") + } + if !start.IsValid() { + panic("invalid start token.Pos") + } + if !end.IsValid() { + end = start + } + + // TODO(adonovan): ideally we would make this stronger assertion: + // + // // Assert that file is non-nil and contains start and end. + // _ = file.Offset(start) + // _ = file.Offset(end) + // + // but some callers (e.g. packageCompletionSurrounding) don't ensure this precondition. + + return Range{ + TokFile: file, + Start: start, + End: end, + } +} + +// IsPoint returns true if the range represents a single point. +func (r Range) IsPoint() bool { + return r.Start == r.End +} diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 12bad1cf384..845b9596428 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -24,9 +24,9 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/imports" @@ -290,7 +290,7 @@ type completionContext struct { type Selection struct { content string cursor token.Pos // relative to rng.TokFile - rng span.Range + rng safetoken.Range mapper *protocol.ColumnMapper } @@ -322,7 +322,7 @@ func (c *completer) setSurrounding(ident *ast.Ident) { content: ident.Name, cursor: c.pos, // Overwrite the prefix only. - rng: span.NewRange(c.tokFile, ident.Pos(), ident.End()), + rng: safetoken.NewRange(c.tokFile, ident.Pos(), ident.End()), mapper: c.mapper, } @@ -345,7 +345,7 @@ func (c *completer) getSurrounding() *Selection { c.surrounding = &Selection{ content: "", cursor: c.pos, - rng: span.NewRange(c.tokFile, c.pos, c.pos), + rng: safetoken.NewRange(c.tokFile, c.pos, c.pos), mapper: c.mapper, } } @@ -798,7 +798,7 @@ func (c *completer) populateImportCompletions(ctx context.Context, searchImport c.surrounding = &Selection{ content: content, cursor: c.pos, - rng: span.NewRange(c.tokFile, start, end), + rng: safetoken.NewRange(c.tokFile, start, end), mapper: c.mapper, } @@ -1019,7 +1019,7 @@ func (c *completer) setSurroundingForComment(comments *ast.CommentGroup) { c.surrounding = &Selection{ content: cursorComment.Text[start:end], cursor: c.pos, - rng: span.NewRange(c.tokFile, token.Pos(int(cursorComment.Slash)+start), token.Pos(int(cursorComment.Slash)+end)), + rng: safetoken.NewRange(c.tokFile, token.Pos(int(cursorComment.Slash)+start), token.Pos(int(cursorComment.Slash)+end)), mapper: c.mapper, } c.setMatcherFromPrefix(c.surrounding.Prefix()) diff --git a/gopls/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go index 2270dd792d6..564d82c2bfd 100644 --- a/gopls/internal/lsp/source/completion/definition.go +++ b/gopls/internal/lsp/source/completion/definition.go @@ -12,9 +12,9 @@ import ( "unicode/utf8" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/span" ) // some function definitions in test files can be completed @@ -40,7 +40,7 @@ func definition(path []ast.Node, obj types.Object, pgf *source.ParsedGoFile) ([] sel := &Selection{ content: "", cursor: start, - rng: span.NewRange(pgf.Tok, start, end), + rng: safetoken.NewRange(pgf.Tok, start, end), mapper: pgf.Mapper, } var ans []CompletionItem diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index b8270e5f1d2..f6b514889c4 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -90,7 +90,7 @@ func packageCompletionSurrounding(pgf *source.ParsedGoFile, offset int) (*Select return &Selection{ content: name.Name, cursor: cursor, - rng: span.NewRange(tok, name.Pos(), name.End()), + rng: safetoken.NewRange(tok, name.Pos(), name.End()), mapper: m, }, nil } @@ -128,7 +128,7 @@ func packageCompletionSurrounding(pgf *source.ParsedGoFile, offset int) (*Select return &Selection{ content: content, cursor: cursor, - rng: span.NewRange(tok, start, end), + rng: safetoken.NewRange(tok, start, end), mapper: m, }, nil } @@ -150,7 +150,7 @@ func packageCompletionSurrounding(pgf *source.ParsedGoFile, offset int) (*Select return &Selection{ content: "", cursor: cursor, - rng: span.NewRange(tok, cursor, cursor), + rng: safetoken.NewRange(tok, cursor, cursor), mapper: m, }, nil } diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index 31a8598b118..6f3c7800998 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -19,12 +19,11 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/lsp/safetoken" - "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/bug" ) -func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { +func extractVariable(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { tokFile := fset.File(file.Pos()) expr, path, ok, err := CanExtractVariable(rng, file) if !ok { @@ -99,7 +98,7 @@ func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast. // CanExtractVariable reports whether the code in the given range can be // extracted to a variable. -func CanExtractVariable(rng span.Range, file *ast.File) (ast.Expr, []ast.Node, bool, error) { +func CanExtractVariable(rng safetoken.Range, file *ast.File) (ast.Expr, []ast.Node, bool, error) { if rng.Start == rng.End { return nil, nil, false, fmt.Errorf("start and end are equal") } @@ -190,12 +189,12 @@ type returnVariable struct { } // extractMethod refactors the selected block of code into a new method. -func extractMethod(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { +func extractMethod(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { return extractFunctionMethod(fset, rng, src, file, pkg, info, true) } // extractFunction refactors the selected block of code into a new function. -func extractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { +func extractFunction(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { return extractFunctionMethod(fset, rng, src, file, pkg, info, false) } @@ -207,7 +206,7 @@ func extractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast. // and return values of the extracted function/method. Lastly, we construct the call // of the function/method and insert this call as well as the extracted function/method into // their proper locations. -func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info, isMethod bool) (*analysis.SuggestedFix, error) { +func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info, isMethod bool) (*analysis.SuggestedFix, error) { errorPrefix := "extractFunction" if isMethod { errorPrefix = "extractMethod" @@ -344,7 +343,7 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file if v.obj.Parent() == nil { return nil, fmt.Errorf("parent nil") } - isUsed, firstUseAfter := objUsed(info, span.NewRange(tok, rng.End, v.obj.Parent().End()), v.obj) + isUsed, firstUseAfter := objUsed(info, safetoken.NewRange(tok, rng.End, v.obj.Parent().End()), v.obj) if v.assigned && isUsed && !varOverridden(info, firstUseAfter, v.obj, v.free, outer) { returnTypes = append(returnTypes, &ast.Field{Type: typ}) returns = append(returns, identifier) @@ -653,7 +652,7 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file // their cursors for whitespace. To support this use case, we must manually adjust the // ranges to match the correct AST node. In this particular example, we would adjust // rng.Start forward to the start of 'if' and rng.End backward to after '}'. -func adjustRangeForCommentsAndWhiteSpace(rng span.Range, tok *token.File, content []byte, file *ast.File) (span.Range, error) { +func adjustRangeForCommentsAndWhiteSpace(rng safetoken.Range, tok *token.File, content []byte, file *ast.File) (safetoken.Range, error) { // Adjust the end of the range to after leading whitespace and comments. prevStart, start := token.NoPos, rng.Start startComment := sort.Search(len(file.Comments), func(i int) bool { @@ -671,7 +670,7 @@ func adjustRangeForCommentsAndWhiteSpace(rng span.Range, tok *token.File, conten // Move forwards to find a non-whitespace character. offset, err := safetoken.Offset(tok, start) if err != nil { - return span.Range{}, err + return safetoken.Range{}, err } for offset < len(content) && isGoWhiteSpace(content[offset]) { offset++ @@ -701,7 +700,7 @@ func adjustRangeForCommentsAndWhiteSpace(rng span.Range, tok *token.File, conten // Move backwards to find a non-whitespace character. offset, err := safetoken.Offset(tok, end) if err != nil { - return span.Range{}, err + return safetoken.Range{}, err } for offset > 0 && isGoWhiteSpace(content[offset-1]) { offset-- @@ -709,7 +708,7 @@ func adjustRangeForCommentsAndWhiteSpace(rng span.Range, tok *token.File, conten end = tok.Pos(offset) } - return span.NewRange(tok, start, end), nil + return safetoken.NewRange(tok, start, end), nil } // isGoWhiteSpace returns true if b is a considered white space in @@ -753,7 +752,7 @@ type variable struct { // variables will be used as arguments in the extracted function. It also returns a // list of identifiers that may need to be returned by the extracted function. // Some of the code in this function has been adapted from tools/cmd/guru/freevars.go. -func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *types.Scope, rng span.Range, node ast.Node) ([]*variable, error) { +func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *types.Scope, rng safetoken.Range, node ast.Node) ([]*variable, error) { // id returns non-nil if n denotes an object that is referenced by the span // and defined either within the span or in the lexical environment. The bool // return value acts as an indicator for where it was defined. @@ -962,14 +961,14 @@ func referencesObj(info *types.Info, expr ast.Expr, obj types.Object) bool { type fnExtractParams struct { tok *token.File path []ast.Node - rng span.Range + rng safetoken.Range outer *ast.FuncDecl start ast.Node } // CanExtractFunction reports whether the code in the given range can be // extracted to a function. -func CanExtractFunction(tok *token.File, rng span.Range, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) { +func CanExtractFunction(tok *token.File, rng safetoken.Range, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) { if rng.Start == rng.End { return nil, false, false, fmt.Errorf("start and end are equal") } @@ -1041,7 +1040,7 @@ func CanExtractFunction(tok *token.File, rng span.Range, src []byte, file *ast.F // objUsed checks if the object is used within the range. It returns the first // occurrence of the object in the range, if it exists. -func objUsed(info *types.Info, rng span.Range, obj types.Object) (bool, *ast.Ident) { +func objUsed(info *types.Info, rng safetoken.Range, obj types.Object) (bool, *ast.Ident) { var firstUse *ast.Ident for id, objUse := range info.Uses { if obj != objUse { diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 7b39c44262e..9b6322c9243 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" ) @@ -27,7 +28,7 @@ type ( // separately. Such analyzers should provide a function with a signature of // SuggestedFixFunc. SuggestedFixFunc func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) - singleFileFixFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) + singleFileFixFunc func(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) ) const ( @@ -133,14 +134,14 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi // getAllSuggestedFixInputs is a helper function to collect all possible needed // inputs for an AppliesFunc or SuggestedFixFunc. -func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, span.Range, []byte, *ast.File, *types.Package, *types.Info, error) { +func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, safetoken.Range, []byte, *ast.File, *types.Package, *types.Info, error) { pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) if err != nil { - return nil, span.Range{}, nil, nil, nil, nil, fmt.Errorf("getting file for Identifier: %w", err) + return nil, safetoken.Range{}, nil, nil, nil, nil, fmt.Errorf("getting file for Identifier: %w", err) } - rng, err := pgf.RangeToSpanRange(pRng) + rng, err := pgf.RangeToTokenRange(pRng) if err != nil { - return nil, span.Range{}, nil, nil, nil, nil, err + return nil, safetoken.Range{}, nil, nil, nil, nil, err } return pkg.FileSet(), rng, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo(), nil } diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go index 5179c1e32c1..3db3248726a 100644 --- a/gopls/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -109,7 +109,7 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng proto start, end := pgf.File.Pos(), pgf.File.End() if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character { // Adjust start and end for the specified range. - rng, err := pgf.RangeToSpanRange(pRng) + rng, err := pgf.RangeToTokenRange(pRng) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index dd41fbeb85a..c2edf08f939 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -218,7 +218,7 @@ func deduceIfaceName(concretePkg, ifacePkg *types.Package, ifaceObj types.Object } func getStubNodes(pgf *ParsedGoFile, pRng protocol.Range) ([]ast.Node, token.Pos, error) { - rng, err := pgf.RangeToSpanRange(pRng) + rng, err := pgf.RangeToTokenRange(pRng) if err != nil { return nil, 0, err } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 9421b50e2cf..61a4a95c081 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -415,12 +415,12 @@ func (pgf *ParsedGoFile) PosMappedRange(startPos, endPos token.Pos) (protocol.Ma } // RangeToSpanRange parses a protocol Range back into the go/token domain. -func (pgf *ParsedGoFile) RangeToSpanRange(r protocol.Range) (span.Range, error) { - spn, err := pgf.Mapper.RangeSpan(r) +func (pgf *ParsedGoFile) RangeToTokenRange(r protocol.Range) (safetoken.Range, error) { + start, end, err := pgf.Mapper.RangeOffsets(r) if err != nil { - return span.Range{}, err + return safetoken.Range{}, err } - return spn.Range(pgf.Tok) + return safetoken.NewRange(pgf.Tok, pgf.Tok.Pos(start), pgf.Tok.Pos(end)), nil } // A ParsedModule contains the results of parsing a go.mod file. diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go index 9687bbb9f31..65c6e2071d7 100644 --- a/gopls/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -17,7 +17,17 @@ import ( "golang.org/x/tools/gopls/internal/lsp/safetoken" ) -// Span represents a source code range in standardized form. +// A Span represents a range of text within a source file. The start +// and end points of a valid span may be hold either its byte offset, +// or its (line, column) pair, or both. Columns are measured in bytes. +// +// Spans are appropriate in user interfaces (e.g. command-line tools) +// and tests where a position is notated without access to the content +// of the file. +// +// Use protocol.ColumnMapper to convert between Span and other +// representations, such as go/token (also UTF-8) or the LSP protocol +// (UTF-16). The latter requires access to file contents. type Span struct { v span } @@ -29,6 +39,12 @@ type Point struct { v point } +// The private span/point types have public fields to support JSON +// encoding, but the public Span/Point types hide these fields by +// defining methods that shadow them. (This is used by a few of the +// command-line tool subcommands, which emit spans and have a -json +// flag.) + type span struct { URI URI `json:"uri"` Start point `json:"start"` @@ -224,75 +240,6 @@ func (s Span) Format(f fmt.State, c rune) { } } -// (Currently unused, but we gain little yet by deleting it.) -func (s Span) withPosition(tf *token.File) (Span, error) { - if err := s.update(tf, true, false); err != nil { - return Span{}, err - } - return s, nil -} - -func (s Span) withOffset(tf *token.File) (Span, error) { - if err := s.update(tf, false, true); err != nil { - return Span{}, err - } - return s, nil -} - -// (Currently unused except by test.) -func (s Span) WithAll(tf *token.File) (Span, error) { - if err := s.update(tf, true, true); err != nil { - return Span{}, err - } - return s, nil -} - -func (s *Span) update(tf *token.File, withPos, withOffset bool) error { - if !s.IsValid() { - return fmt.Errorf("cannot add information to an invalid span") - } - if withPos && !s.HasPosition() { - if err := s.v.Start.updatePosition(tf); err != nil { - return err - } - if s.v.End.Offset == s.v.Start.Offset { - s.v.End = s.v.Start - } else if err := s.v.End.updatePosition(tf); err != nil { - return err - } - } - if withOffset && (!s.HasOffset() || (s.v.End.hasPosition() && !s.v.End.hasOffset())) { - if err := s.v.Start.updateOffset(tf); err != nil { - return err - } - if s.v.End.Line == s.v.Start.Line && s.v.End.Column == s.v.Start.Column { - s.v.End.Offset = s.v.Start.Offset - } else if err := s.v.End.updateOffset(tf); err != nil { - return err - } - } - return nil -} - -func (p *point) updatePosition(tf *token.File) error { - line, col8, err := offsetToLineCol8(tf, p.Offset) - if err != nil { - return err - } - p.Line = line - p.Column = col8 - return nil -} - -func (p *point) updateOffset(tf *token.File) error { - offset, err := toOffset(tf, p.Line, p.Column) - if err != nil { - return err - } - p.Offset = offset - return nil -} - // SetRange implements packagestest.rangeSetter, allowing // gopls' test suites to use Spans instead of Range in parameters. func (span *Span) SetRange(file *token.File, start, end token.Pos) { diff --git a/gopls/internal/span/span_test.go b/gopls/internal/span/span_test.go index 63c0752f959..d2aaff12cab 100644 --- a/gopls/internal/span/span_test.go +++ b/gopls/internal/span/span_test.go @@ -6,7 +6,6 @@ package span_test import ( "fmt" - "go/token" "path/filepath" "strings" "testing" @@ -14,40 +13,37 @@ import ( "golang.org/x/tools/gopls/internal/span" ) -var ( - tests = [][]string{ - {"C:/file_a", "C:/file_a", "file:///C:/file_a:1:1#0"}, - {"C:/file_b:1:2", "C:/file_b:#1", "file:///C:/file_b:1:2#1"}, - {"C:/file_c:1000", "C:/file_c:#9990", "file:///C:/file_c:1000:1#9990"}, - {"C:/file_d:14:9", "C:/file_d:#138", "file:///C:/file_d:14:9#138"}, - {"C:/file_e:1:2-7", "C:/file_e:#1-#6", "file:///C:/file_e:1:2#1-1:7#6"}, - {"C:/file_f:500-502", "C:/file_f:#4990-#5010", "file:///C:/file_f:500:1#4990-502:1#5010"}, - {"C:/file_g:3:7-8", "C:/file_g:#26-#27", "file:///C:/file_g:3:7#26-3:8#27"}, - {"C:/file_h:3:7-4:8", "C:/file_h:#26-#37", "file:///C:/file_h:3:7#26-4:8#37"}, - } -) - func TestFormat(t *testing.T) { - converter := lines(10) - for _, test := range tests { - for ti, text := range test[:2] { - spn := span.Parse(text) - if ti <= 1 { - // we can check %v produces the same as the input - expect := toPath(test[ti]) - if got := fmt.Sprintf("%v", spn); got != expect { - t.Errorf("printing %q got %q expected %q", text, got, expect) - } - } - complete, err := spn.WithAll(converter) - if err != nil { - t.Error(err) - } - for fi, format := range []string{"%v", "%#v", "%+v"} { - expect := toPath(test[fi]) - if got := fmt.Sprintf(format, complete); got != expect { - t.Errorf("printing completed %q as %q got %q expected %q [%+v]", text, format, got, expect, spn) - } + formats := []string{"%v", "%#v", "%+v"} + + // Element 0 is the input, and the elements 0-2 are the expected + // output in [%v %#v %+v] formats. Thus the first must be in + // canonical form (invariant under span.Parse + fmt.Sprint). + // The '#' form displays offsets; the '+' form outputs a URI. + // If len=4, element 0 is a noncanonical input and 1-3 are expected outputs. + for _, test := range [][]string{ + {"C:/file_a", "C:/file_a", "file:///C:/file_a:#0"}, + {"C:/file_b:1:2", "C:/file_b:1:2", "file:///C:/file_b:1:2"}, + {"C:/file_c:1000", "C:/file_c:1000", "file:///C:/file_c:1000:1"}, + {"C:/file_d:14:9", "C:/file_d:14:9", "file:///C:/file_d:14:9"}, + {"C:/file_e:1:2-7", "C:/file_e:1:2-7", "file:///C:/file_e:1:2-1:7"}, + {"C:/file_f:500-502", "C:/file_f:500-502", "file:///C:/file_f:500:1-502:1"}, + {"C:/file_g:3:7-8", "C:/file_g:3:7-8", "file:///C:/file_g:3:7-3:8"}, + {"C:/file_h:3:7-4:8", "C:/file_h:3:7-4:8", "file:///C:/file_h:3:7-4:8"}, + {"C:/file_i:#100", "C:/file_i:#100", "file:///C:/file_i:#100"}, + {"C:/file_j:#26-#28", "C:/file_j:#26-#28", "file:///C:/file_j:#26-0#28"}, // 0#28? + {"C:/file_h:3:7#26-4:8#37", // not canonical + "C:/file_h:3:7-4:8", "C:/file_h:#26-#37", "file:///C:/file_h:3:7#26-4:8#37"}} { + input := test[0] + spn := span.Parse(input) + wants := test[0:3] + if len(test) == 4 { + wants = test[1:4] + } + for i, format := range formats { + want := toPath(wants[i]) + if got := fmt.Sprintf(format, spn); got != want { + t.Errorf("Sprintf(%q, %q) = %q, want %q", format, input, got, want) } } } @@ -59,16 +55,3 @@ func toPath(value string) string { } return filepath.FromSlash(value) } - -// lines creates a new tokenConverter for a file with 1000 lines, each width -// bytes wide. -func lines(width int) *token.File { - fset := token.NewFileSet() - f := fset.AddFile("", -1, 1000*width) - var lines []int - for i := 0; i < 1000; i++ { - lines = append(lines, i*width) - } - f.SetLines(lines) - return f -} diff --git a/gopls/internal/span/token.go b/gopls/internal/span/token.go deleted file mode 100644 index 1b8a6495f5b..00000000000 --- a/gopls/internal/span/token.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package span - -import ( - "fmt" - "go/token" - - "golang.org/x/tools/gopls/internal/lsp/safetoken" - "golang.org/x/tools/internal/bug" -) - -// Range represents a source code range in token.Pos form. -// It also carries the token.File that produced the positions, so that it is -// self contained. -// -// TODO(adonovan): move to safetoken.Range (but the Range.Span function must stay behind). -type Range struct { - TokFile *token.File // non-nil - Start, End token.Pos // both IsValid() -} - -// NewRange creates a new Range from a token.File and two positions within it. -// The given start position must be valid; if end is invalid, start is used as -// the end position. -// -// (If you only have a token.FileSet, use file = fset.File(start). But -// most callers know exactly which token.File they're dealing with and -// should pass it explicitly. Not only does this save a lookup, but it -// brings us a step closer to eliminating the global FileSet.) -func NewRange(file *token.File, start, end token.Pos) Range { - if file == nil { - panic("nil *token.File") - } - if !start.IsValid() { - panic("invalid start token.Pos") - } - if !end.IsValid() { - end = start - } - - // TODO(adonovan): ideally we would make this stronger assertion: - // - // // Assert that file is non-nil and contains start and end. - // _ = file.Offset(start) - // _ = file.Offset(end) - // - // but some callers (e.g. packageCompletionSurrounding, - // posToMappedRange) don't ensure this precondition. - - return Range{ - TokFile: file, - Start: start, - End: end, - } -} - -// IsPoint returns true if the range represents a single point. -func (r Range) IsPoint() bool { - return r.Start == r.End -} - -// Span converts a Range to a Span that represents the Range. -// It will fill in all the members of the Span, calculating the line and column -// information. -func (r Range) Span() (Span, error) { - return FileSpan(r.TokFile, r.Start, r.End) -} - -// FileSpan returns a span within the file referenced by start and -// end, using a token.File to translate between offsets and positions. -func FileSpan(file *token.File, start, end token.Pos) (Span, error) { - if !start.IsValid() { - return Span{}, fmt.Errorf("start pos is not valid") - } - var s Span - var err error - var startFilename string - startFilename, s.v.Start.Line, s.v.Start.Column, err = position(file, start) - if err != nil { - return Span{}, err - } - s.v.URI = URIFromPath(startFilename) - if end.IsValid() { - var endFilename string - endFilename, s.v.End.Line, s.v.End.Column, err = position(file, end) - if err != nil { - return Span{}, err - } - if endFilename != startFilename { - return Span{}, fmt.Errorf("span begins in file %q but ends in %q", startFilename, endFilename) - } - } - s.v.Start.clean() - s.v.End.clean() - s.v.clean() - return s.withOffset(file) -} - -func position(tf *token.File, pos token.Pos) (string, int, int, error) { - off, err := offset(tf, pos) - if err != nil { - return "", 0, 0, err - } - return positionFromOffset(tf, off) -} - -func positionFromOffset(tf *token.File, offset int) (string, int, int, error) { - if offset > tf.Size() { - return "", 0, 0, fmt.Errorf("offset %d is beyond EOF (%d) in file %s", offset, tf.Size(), tf.Name()) - } - pos := tf.Pos(offset) - p := safetoken.Position(tf, pos) - // TODO(golang/go#41029): Consider returning line, column instead of line+1, 1 if - // the file's last character is not a newline. - if offset == tf.Size() { - return p.Filename, p.Line + 1, 1, nil - } - return p.Filename, p.Line, p.Column, nil -} - -// offset is a copy of the Offset function in go/token, but with the adjustment -// that it does not panic on invalid positions. -func offset(tf *token.File, pos token.Pos) (int, error) { - if int(pos) < tf.Base() || int(pos) > tf.Base()+tf.Size() { - return 0, fmt.Errorf("invalid pos: %d not in [%d, %d]", pos, tf.Base(), tf.Base()+tf.Size()) - } - return int(pos) - tf.Base(), nil -} - -// Range converts a Span to a Range that represents the Span for the supplied -// File. -func (s Span) Range(tf *token.File) (Range, error) { - s, err := s.withOffset(tf) - if err != nil { - return Range{}, err - } - // go/token will panic if the offset is larger than the file's size, - // so check here to avoid panicking. - if s.Start().Offset() > tf.Size() { - return Range{}, bug.Errorf("start offset %v is past the end of the file %v", s.Start(), tf.Size()) - } - if s.End().Offset() > tf.Size() { - return Range{}, bug.Errorf("end offset %v is past the end of the file %v", s.End(), tf.Size()) - } - return Range{ - Start: tf.Pos(s.Start().Offset()), - End: tf.Pos(s.End().Offset()), - TokFile: tf, - }, nil -} - -// offsetToLineCol8 converts a byte offset in the file corresponding to tf into -// 1-based line and utf-8 column indexes. -func offsetToLineCol8(tf *token.File, offset int) (int, int, error) { - _, line, col8, err := positionFromOffset(tf, offset) - return line, col8, err -} - -// ToOffset converts a 1-based line and utf-8 column index into a byte offset -// in the file corresponding to tf. -func toOffset(tf *token.File, line, col int) (int, error) { - if line < 1 { // token.File.LineStart panics if line < 1 - return -1, fmt.Errorf("invalid line: %d", line) - } - - lineMax := tf.LineCount() + 1 - if line > lineMax { - return -1, fmt.Errorf("line %d is beyond end of file %v", line, lineMax) - } else if line == lineMax { - if col > 1 { - return -1, fmt.Errorf("column is beyond end of file") - } - // at the end of the file, allowing for a trailing eol - return tf.Size(), nil - } - pos := tf.LineStart(line) - if !pos.IsValid() { - // bug.Errorf here because LineStart panics on out-of-bound input, and so - // should never return invalid positions. - return -1, bug.Errorf("line is not in file") - } - // we assume that column is in bytes here, and that the first byte of a - // line is at column 1 - pos += token.Pos(col - 1) - - // Debugging support for https://github.com/golang/go/issues/54655. - if pos > token.Pos(tf.Base()+tf.Size()) { - return 0, fmt.Errorf("ToOffset: column %d is beyond end of file", col) - } - - return offset(tf, pos) -} diff --git a/gopls/internal/span/token_test.go b/gopls/internal/span/token_test.go deleted file mode 100644 index 997c8fb53ef..00000000000 --- a/gopls/internal/span/token_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package span_test - -import ( - "fmt" - "go/token" - "path" - "testing" - - "golang.org/x/tools/gopls/internal/span" -) - -var testdata = []struct { - uri string - content []byte -}{ - {"/a.go", []byte(` -// file a.go -package test -`)}, - {"/b.go", []byte(` -// -// -// file b.go -package test`)}, - {"/c.go", []byte(` -// file c.go -package test`)}, -} - -var tokenTests = []span.Span{ - span.New(span.URIFromPath("/a.go"), span.NewPoint(1, 1, 0), span.Point{}), - span.New(span.URIFromPath("/a.go"), span.NewPoint(3, 7, 20), span.NewPoint(3, 7, 20)), - span.New(span.URIFromPath("/b.go"), span.NewPoint(4, 9, 15), span.NewPoint(4, 13, 19)), - span.New(span.URIFromPath("/c.go"), span.NewPoint(4, 1, 26), span.Point{}), -} - -func TestToken(t *testing.T) { - fset := token.NewFileSet() - files := map[span.URI]*token.File{} - for _, f := range testdata { - file := fset.AddFile(f.uri, -1, len(f.content)) - file.SetLinesForContent(f.content) - files[span.URIFromPath(f.uri)] = file - } - for _, test := range tokenTests { - f := files[test.URI()] - t.Run(path.Base(f.Name()), func(t *testing.T) { - checkToken(t, f, span.New( - test.URI(), - span.NewPoint(test.Start().Line(), test.Start().Column(), 0), - span.NewPoint(test.End().Line(), test.End().Column(), 0), - ), test) - checkToken(t, f, span.New( - test.URI(), - span.NewPoint(0, 0, test.Start().Offset()), - span.NewPoint(0, 0, test.End().Offset()), - ), test) - }) - } -} - -func checkToken(t *testing.T, f *token.File, in, expect span.Span) { - rng, err := in.Range(f) - if err != nil { - t.Error(err) - } - gotLoc, err := rng.Span() - if err != nil { - t.Error(err) - } - expected := fmt.Sprintf("%+v", expect) - got := fmt.Sprintf("%+v", gotLoc) - if expected != got { - t.Errorf("For %v expected %q got %q", in, expected, got) - } -} From a24944e25b1b8f2065e5b072ca715459abb4d56d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 10 Jan 2023 15:52:16 -0500 Subject: [PATCH 601/723] gopls/internal/lsp/protocol: rename s/ColumnMapper/Mapper/ Also three methods: Location -> SpanLocation Range -> SpanRange Span -> LocationSpan Also, move ColumnMapper into its own file mapper.go. No other changes were made. Change-Id: I697500e0deafcef1d1d1b53d04c8862010fa09eb Reviewed-on: https://go-review.googlesource.com/c/tools/+/461415 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/cache/errors.go | 2 +- gopls/internal/lsp/cache/mod.go | 6 +- gopls/internal/lsp/cache/mod_tidy.go | 6 +- gopls/internal/lsp/cache/parse.go | 2 +- gopls/internal/lsp/cache/symbols.go | 6 +- gopls/internal/lsp/cmd/call_hierarchy.go | 6 +- gopls/internal/lsp/cmd/cmd.go | 4 +- gopls/internal/lsp/cmd/definition.go | 4 +- gopls/internal/lsp/cmd/format.go | 2 +- gopls/internal/lsp/cmd/highlight.go | 4 +- gopls/internal/lsp/cmd/implementation.go | 4 +- gopls/internal/lsp/cmd/prepare_rename.go | 4 +- gopls/internal/lsp/cmd/references.go | 4 +- gopls/internal/lsp/cmd/rename.go | 2 +- gopls/internal/lsp/cmd/semantictokens.go | 4 +- gopls/internal/lsp/cmd/signature.go | 2 +- gopls/internal/lsp/cmd/suggested_fix.go | 2 +- gopls/internal/lsp/cmd/test/call_hierarchy.go | 2 +- gopls/internal/lsp/cmd/test/check.go | 4 +- gopls/internal/lsp/cmd/test/prepare_rename.go | 2 +- gopls/internal/lsp/cmd/workspace_symbol.go | 2 +- gopls/internal/lsp/command.go | 2 +- gopls/internal/lsp/link.go | 4 +- gopls/internal/lsp/lsp_test.go | 40 +- gopls/internal/lsp/protocol/mapper.go | 462 ++++++++++++++++++ gopls/internal/lsp/protocol/mapper_test.go | 14 +- gopls/internal/lsp/protocol/span.go | 456 ----------------- .../lsp/source/completion/completion.go | 4 +- gopls/internal/lsp/source/fix.go | 4 +- gopls/internal/lsp/source/format.go | 8 +- gopls/internal/lsp/source/identifier.go | 2 +- gopls/internal/lsp/source/implementation.go | 2 +- gopls/internal/lsp/source/inlay_hint.go | 18 +- gopls/internal/lsp/source/rename.go | 2 +- gopls/internal/lsp/source/source_test.go | 10 +- gopls/internal/lsp/source/symbols.go | 10 +- gopls/internal/lsp/source/view.go | 6 +- gopls/internal/lsp/tests/tests.go | 10 +- gopls/internal/lsp/tests/util.go | 4 +- gopls/internal/lsp/text_synchronization.go | 4 +- gopls/internal/span/span.go | 2 +- 41 files changed, 572 insertions(+), 566 deletions(-) create mode 100644 gopls/internal/lsp/protocol/mapper.go diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 16c0e78f1af..8af1ac355c0 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -325,7 +325,7 @@ func spanToRange(pkg *pkg, spn span.Span) (protocol.Range, error) { if err != nil { return protocol.Range{}, err } - return pgf.Mapper.Range(spn) + return pgf.Mapper.SpanRange(spn) } // parseGoListError attempts to parse a standard `go list` error message diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index 55665395986..48590ecaa5b 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -70,7 +70,7 @@ func parseModImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedModu if err != nil { return nil, err } - m := protocol.NewColumnMapper(fh.URI(), contents) + m := protocol.NewMapper(fh.URI(), contents) file, parseErr := modfile.Parse(fh.URI().Filename(), contents, nil) // Attempt to convert the error to a standardized parse error. var parseErrors []*source.Diagnostic @@ -147,7 +147,7 @@ func parseWorkImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedWor if err != nil { return nil, err } - m := protocol.NewColumnMapper(fh.URI(), contents) + m := protocol.NewMapper(fh.URI(), contents) file, parseErr := modfile.ParseWork(fh.URI().Filename(), contents, nil) // Attempt to convert the error to a standardized parse error. var parseErrors []*source.Diagnostic @@ -415,7 +415,7 @@ func (s *snapshot) matchErrorToModule(ctx context.Context, pm *source.ParsedModu // goCommandDiagnostic creates a diagnostic for a given go command error. func (s *snapshot) goCommandDiagnostic(pm *source.ParsedModule, spn span.Span, goCmdError string) (*source.Diagnostic, error) { - rng, err := pm.Mapper.Range(spn) + rng, err := pm.Mapper.SpanRange(spn) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index d15fe4047f5..385b0143795 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -283,7 +283,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.Pars } // unusedDiagnostic returns a source.Diagnostic for an unused require. -func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) { +func unusedDiagnostic(m *protocol.Mapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) { rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) if err != nil { return nil, err @@ -309,7 +309,7 @@ func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagno // directnessDiagnostic extracts errors when a dependency is labeled indirect when // it should be direct and vice versa. -func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits source.DiffFunction) (*source.Diagnostic, error) { +func directnessDiagnostic(m *protocol.Mapper, req *modfile.Require, computeEdits source.DiffFunction) (*source.Diagnostic, error) { rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) if err != nil { return nil, err @@ -382,7 +382,7 @@ func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*so // switchDirectness gets the edits needed to change an indirect dependency to // direct and vice versa. -func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits source.DiffFunction) ([]protocol.TextEdit, error) { +func switchDirectness(req *modfile.Require, m *protocol.Mapper, computeEdits source.DiffFunction) ([]protocol.TextEdit, error) { // We need a private copy of the parsed go.mod file, since we're going to // modify it. copied, err := modfile.Parse("", m.Content, nil) diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go index 7451cc3a652..4a3d9b7c53a 100644 --- a/gopls/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -208,7 +208,7 @@ func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, Fixed: fixed, File: file, Tok: tok, - Mapper: protocol.NewColumnMapper(fh.URI(), src), + Mapper: protocol.NewMapper(fh.URI(), src), ParseErr: parseErr, }, nil } diff --git a/gopls/internal/lsp/cache/symbols.go b/gopls/internal/lsp/cache/symbols.go index 26a78677ca3..8cdb147a957 100644 --- a/gopls/internal/lsp/cache/symbols.go +++ b/gopls/internal/lsp/cache/symbols.go @@ -67,7 +67,7 @@ func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, e var ( file *ast.File tokFile *token.File - mapper *protocol.ColumnMapper + mapper *protocol.Mapper ) // If the file has already been fully parsed through the @@ -93,7 +93,7 @@ func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, e return nil, err } tokFile = fset.File(file.Package) - mapper = protocol.NewColumnMapper(fh.URI(), src) + mapper = protocol.NewMapper(fh.URI(), src) } w := &symbolWalker{ @@ -109,7 +109,7 @@ func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, e type symbolWalker struct { // for computing positions tokFile *token.File - mapper *protocol.ColumnMapper + mapper *protocol.Mapper symbols []source.Symbol firstError error diff --git a/gopls/internal/lsp/cmd/call_hierarchy.go b/gopls/internal/lsp/cmd/call_hierarchy.go index 295dea8b0d4..0220fd275e2 100644 --- a/gopls/internal/lsp/cmd/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/call_hierarchy.go @@ -52,7 +52,7 @@ func (c *callHierarchy) Run(ctx context.Context, args ...string) error { return file.err } - loc, err := file.mapper.Location(from) + loc, err := file.mapper.SpanLocation(from) if err != nil { return err } @@ -118,7 +118,7 @@ func callItemPrintString(ctx context.Context, conn *connection, item protocol.Ca if itemFile.err != nil { return "", itemFile.err } - itemSpan, err := itemFile.mapper.Span(protocol.Location{URI: item.URI, Range: item.Range}) + itemSpan, err := itemFile.mapper.LocationSpan(protocol.Location{URI: item.URI, Range: item.Range}) if err != nil { return "", err } @@ -129,7 +129,7 @@ func callItemPrintString(ctx context.Context, conn *connection, item protocol.Ca } var callRanges []string for _, rng := range calls { - callSpan, err := callsFile.mapper.Span(protocol.Location{URI: item.URI, Range: rng}) + callSpan, err := callsFile.mapper.LocationSpan(protocol.Location{URI: item.URI, Range: rng}) if err != nil { return "", err } diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index c2219133041..aea05d5e9da 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -395,7 +395,7 @@ type cmdClient struct { type cmdFile struct { uri span.URI - mapper *protocol.ColumnMapper + mapper *protocol.Mapper err error open bool diagnostics []protocol.Diagnostic @@ -543,7 +543,7 @@ func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile { file.err = fmt.Errorf("getFile: %v: %v", uri, err) return file } - file.mapper = protocol.NewColumnMapper(uri, content) + file.mapper = protocol.NewMapper(uri, content) } return file } diff --git a/gopls/internal/lsp/cmd/definition.go b/gopls/internal/lsp/cmd/definition.go index edfd7392902..269172db952 100644 --- a/gopls/internal/lsp/cmd/definition.go +++ b/gopls/internal/lsp/cmd/definition.go @@ -84,7 +84,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { if file.err != nil { return file.err } - loc, err := file.mapper.Location(from) + loc, err := file.mapper.SpanLocation(from) if err != nil { return err } @@ -117,7 +117,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { if file.err != nil { return fmt.Errorf("%v: %v", from, file.err) } - definition, err := file.mapper.Span(locs[0]) + definition, err := file.mapper.LocationSpan(locs[0]) if err != nil { return fmt.Errorf("%v: %v", from, err) } diff --git a/gopls/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go index 2b8109c670a..31dfb172ece 100644 --- a/gopls/internal/lsp/cmd/format.go +++ b/gopls/internal/lsp/cmd/format.go @@ -62,7 +62,7 @@ func (c *format) Run(ctx context.Context, args ...string) error { return file.err } filename := spn.URI().Filename() - loc, err := file.mapper.Location(spn) + loc, err := file.mapper.SpanLocation(spn) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/highlight.go b/gopls/internal/lsp/cmd/highlight.go index d8b3d226e0b..6f9eac69ea5 100644 --- a/gopls/internal/lsp/cmd/highlight.go +++ b/gopls/internal/lsp/cmd/highlight.go @@ -51,7 +51,7 @@ func (r *highlight) Run(ctx context.Context, args ...string) error { return file.err } - loc, err := file.mapper.Location(from) + loc, err := file.mapper.SpanLocation(from) if err != nil { return err } @@ -70,7 +70,7 @@ func (r *highlight) Run(ctx context.Context, args ...string) error { var results []span.Span for _, h := range highlights { l := protocol.Location{Range: h.Range} - s, err := file.mapper.Span(l) + s, err := file.mapper.LocationSpan(l) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/implementation.go b/gopls/internal/lsp/cmd/implementation.go index dbc5fc3223b..eed41ab4402 100644 --- a/gopls/internal/lsp/cmd/implementation.go +++ b/gopls/internal/lsp/cmd/implementation.go @@ -52,7 +52,7 @@ func (i *implementation) Run(ctx context.Context, args ...string) error { return file.err } - loc, err := file.mapper.Location(from) + loc, err := file.mapper.SpanLocation(from) if err != nil { return err } @@ -72,7 +72,7 @@ func (i *implementation) Run(ctx context.Context, args ...string) error { var spans []string for _, impl := range implementations { f := conn.openFile(ctx, fileURI(impl.URI)) - span, err := f.mapper.Span(impl) + span, err := f.mapper.LocationSpan(impl) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/prepare_rename.go b/gopls/internal/lsp/cmd/prepare_rename.go index e61bd622fe0..88b33c4838e 100644 --- a/gopls/internal/lsp/cmd/prepare_rename.go +++ b/gopls/internal/lsp/cmd/prepare_rename.go @@ -55,7 +55,7 @@ func (r *prepareRename) Run(ctx context.Context, args ...string) error { if file.err != nil { return file.err } - loc, err := file.mapper.Location(from) + loc, err := file.mapper.SpanLocation(from) if err != nil { return err } @@ -74,7 +74,7 @@ func (r *prepareRename) Run(ctx context.Context, args ...string) error { } l := protocol.Location{Range: result.Range} - s, err := file.mapper.Span(l) + s, err := file.mapper.LocationSpan(l) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/references.go b/gopls/internal/lsp/cmd/references.go index 2abbb919299..533fcc19b81 100644 --- a/gopls/internal/lsp/cmd/references.go +++ b/gopls/internal/lsp/cmd/references.go @@ -55,7 +55,7 @@ func (r *references) Run(ctx context.Context, args ...string) error { if file.err != nil { return file.err } - loc, err := file.mapper.Location(from) + loc, err := file.mapper.SpanLocation(from) if err != nil { return err } @@ -77,7 +77,7 @@ func (r *references) Run(ctx context.Context, args ...string) error { f := conn.openFile(ctx, fileURI(l.URI)) // convert location to span for user-friendly 1-indexed line // and column numbers - span, err := f.mapper.Span(l) + span, err := f.mapper.LocationSpan(l) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go index 2cbd260febb..5a6677d5112 100644 --- a/gopls/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -65,7 +65,7 @@ func (r *rename) Run(ctx context.Context, args ...string) error { if file.err != nil { return file.err } - loc, err := file.mapper.Location(from) + loc, err := file.mapper.SpanLocation(from) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/semantictokens.go b/gopls/internal/lsp/cmd/semantictokens.go index 3ed08d0248b..3749ed2aefc 100644 --- a/gopls/internal/lsp/cmd/semantictokens.go +++ b/gopls/internal/lsp/cmd/semantictokens.go @@ -49,7 +49,7 @@ type semtok struct { app *Application } -var colmap *protocol.ColumnMapper +var colmap *protocol.Mapper func (c *semtok) Name() string { return "semtok" } func (c *semtok) Parent() string { return c.app.Name() } @@ -117,7 +117,7 @@ func (c *semtok) Run(ctx context.Context, args ...string) error { // can't happen; just parsed this file return fmt.Errorf("can't find %s in fset", args[0]) } - colmap = protocol.NewColumnMapper(uri, buf) + colmap = protocol.NewMapper(uri, buf) err = decorate(file.uri.Filename(), resp.Data) if err != nil { return err diff --git a/gopls/internal/lsp/cmd/signature.go b/gopls/internal/lsp/cmd/signature.go index 77805628ad0..2f34b9bef75 100644 --- a/gopls/internal/lsp/cmd/signature.go +++ b/gopls/internal/lsp/cmd/signature.go @@ -51,7 +51,7 @@ func (r *signature) Run(ctx context.Context, args ...string) error { return file.err } - loc, err := file.mapper.Location(from) + loc, err := file.mapper.SpanLocation(from) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go index 78310b3b3b9..b96849d3781 100644 --- a/gopls/internal/lsp/cmd/suggested_fix.go +++ b/gopls/internal/lsp/cmd/suggested_fix.go @@ -75,7 +75,7 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { } } - rng, err := file.mapper.Range(from) + rng, err := file.mapper.SpanRange(from) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/test/call_hierarchy.go b/gopls/internal/lsp/cmd/test/call_hierarchy.go index bb8d306224a..5517a51d5eb 100644 --- a/gopls/internal/lsp/cmd/test/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/test/call_hierarchy.go @@ -23,7 +23,7 @@ func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests if err != nil { t.Fatal(err) } - callSpan, err := mapper.Span(protocol.Location{URI: call.URI, Range: call.Range}) + callSpan, err := mapper.LocationSpan(protocol.Location{URI: call.URI, Range: call.Range}) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/lsp/cmd/test/check.go b/gopls/internal/lsp/cmd/test/check.go index 35153c2700d..dcbb2e0f2c1 100644 --- a/gopls/internal/lsp/cmd/test/check.go +++ b/gopls/internal/lsp/cmd/test/check.go @@ -25,7 +25,7 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost if err != nil { t.Fatal(err) } - mapper := protocol.NewColumnMapper(uri, content) + mapper := protocol.NewMapper(uri, content) // Parse command output into a set of diagnostics. var got []*source.Diagnostic @@ -38,7 +38,7 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost t.Fatalf("output line not of form 'span: message': %q", line) } spn, message := span.Parse(parts[0]), parts[1] - rng, err := mapper.Range(spn) + rng, err := mapper.SpanRange(spn) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/lsp/cmd/test/prepare_rename.go b/gopls/internal/lsp/cmd/test/prepare_rename.go index c818c0197da..2c22f38a624 100644 --- a/gopls/internal/lsp/cmd/test/prepare_rename.go +++ b/gopls/internal/lsp/cmd/test/prepare_rename.go @@ -34,7 +34,7 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare return } - ws, err := m.Span(protocol.Location{Range: want.Range}) + ws, err := m.LocationSpan(protocol.Location{Range: want.Range}) if err != nil { t.Errorf("prepare_rename failed: %v", err) } diff --git a/gopls/internal/lsp/cmd/workspace_symbol.go b/gopls/internal/lsp/cmd/workspace_symbol.go index be1e24ef324..0c7160af399 100644 --- a/gopls/internal/lsp/cmd/workspace_symbol.go +++ b/gopls/internal/lsp/cmd/workspace_symbol.go @@ -74,7 +74,7 @@ func (r *workspaceSymbol) Run(ctx context.Context, args ...string) error { } for _, s := range symbols { f := conn.openFile(ctx, fileURI(s.Location.URI)) - span, err := f.mapper.Span(s.Location) + span, err := f.mapper.LocationSpan(s.Location) if err != nil { return err } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 2022325a815..210ce9b4f79 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -626,7 +626,7 @@ func applyFileEdits(ctx context.Context, snapshot source.Snapshot, uri span.URI, return nil, err } - m := protocol.NewColumnMapper(fh.URI(), oldContent) + m := protocol.NewMapper(fh.URI(), oldContent) diff := snapshot.View().Options().ComputeEdits(string(oldContent), string(newContent)) edits, err := source.ToProtocolEdits(m, diff) if err != nil { diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go index e7bdb604662..3adc4debfa5 100644 --- a/gopls/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -211,7 +211,7 @@ var acceptedSchemes = map[string]bool{ // urlRegexp is the user-supplied regular expression to match URL. // srcOffset is the start offset of 'src' within m's file. -func findLinksInString(urlRegexp *regexp.Regexp, src string, srcOffset int, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) { +func findLinksInString(urlRegexp *regexp.Regexp, src string, srcOffset int, m *protocol.Mapper) ([]protocol.DocumentLink, error) { var links []protocol.DocumentLink for _, index := range urlRegexp.FindAllIndex([]byte(src), -1) { start, end := index[0], index[1] @@ -270,7 +270,7 @@ var ( issueRegexp *regexp.Regexp ) -func toProtocolLink(m *protocol.ColumnMapper, targetURL string, start, end int) (protocol.DocumentLink, error) { +func toProtocolLink(m *protocol.Mapper, targetURL string, start, end int) (protocol.DocumentLink, error) { rng, err := m.OffsetRange(start, end) if err != nil { return protocol.DocumentLink{}, err diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index b75b54ba12a..1138c36344b 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -144,7 +144,7 @@ func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests if err != nil { t.Fatal(err) } - loc, err := mapper.Location(spn) + loc, err := mapper.SpanLocation(spn) if err != nil { t.Fatalf("failed for %v: %v", spn, err) } @@ -354,7 +354,7 @@ func conflict(a, b protocol.FoldingRange) bool { (a.EndLine > b.StartLine || (a.EndLine == b.StartLine && a.EndCharacter > b.StartCharacter)) } -func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.FoldingRange) (string, error) { +func foldRanges(m *protocol.Mapper, contents string, ranges []protocol.FoldingRange) (string, error) { foldedText := "<>" res := contents // Apply the edits from the end of the file forward @@ -484,7 +484,7 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.S if err != nil { t.Fatal(err) } - rng, err := m.Range(spn) + rng, err := m.SpanRange(spn) if err != nil { t.Fatal(err) } @@ -578,7 +578,7 @@ func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span t.Fatal(err) } spn := span.New(start.URI(), start.Start(), end.End()) - rng, err := m.Range(spn) + rng, err := m.SpanRange(spn) if err != nil { t.Fatal(err) } @@ -630,7 +630,7 @@ func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) t.Fatal(err) } spn := span.New(start.URI(), start.Start(), end.End()) - rng, err := m.Range(spn) + rng, err := m.SpanRange(spn) if err != nil { t.Fatal(err) } @@ -680,7 +680,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if err != nil { t.Fatal(err) } - loc, err := sm.Location(d.Src) + loc, err := sm.SpanLocation(d.Src) if err != nil { t.Fatalf("failed for %v: %v", d.Src, err) } @@ -734,7 +734,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if err != nil { t.Fatal(err) } - if def, err := lm.Span(locs[0]); err != nil { + if def, err := lm.LocationSpan(locs[0]); err != nil { t.Fatalf("failed for %v: %v", locs[0], err) } else if def != d.Def { t.Errorf("for %v got %v want %v", d.Src, def, d.Def) @@ -750,7 +750,7 @@ func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) if err != nil { t.Fatal(err) } - loc, err := sm.Location(spn) + loc, err := sm.SpanLocation(spn) if err != nil { t.Fatalf("failed for %v: %v", spn, err) } @@ -777,7 +777,7 @@ func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) if err != nil { t.Fatal(err) } - imp, err := lm.Span(locs[i]) + imp, err := lm.LocationSpan(locs[i]) if err != nil { t.Fatalf("failed for %v: %v", locs[i], err) } @@ -798,7 +798,7 @@ func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) { if err != nil { t.Fatal(err) } - loc, err := m.Location(src) + loc, err := m.SpanLocation(src) if err != nil { t.Fatalf("failed for %v: %v", locations[0], err) } @@ -840,7 +840,7 @@ func (r *runner) Hover(t *testing.T, src span.Span, text string) { if err != nil { t.Fatal(err) } - loc, err := m.Location(src) + loc, err := m.SpanLocation(src) if err != nil { t.Fatalf("failed for %v", err) } @@ -880,7 +880,7 @@ func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { if err != nil { t.Fatal(err) } - loc, err := sm.Location(src) + loc, err := sm.SpanLocation(src) if err != nil { t.Fatalf("failed for %v: %v", src, err) } @@ -896,7 +896,7 @@ func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { if err != nil { t.Fatal(err) } - loc, err := m.Location(pos) + loc, err := m.SpanLocation(pos) if err != nil { t.Fatalf("failed for %v: %v", src, err) } @@ -1014,7 +1014,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { if err != nil { t.Fatal(err) } - loc, err := sm.Location(spn) + loc, err := sm.SpanLocation(spn) if err != nil { t.Fatalf("failed for %v: %v", spn, err) } @@ -1070,7 +1070,7 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare if err != nil { t.Fatal(err) } - loc, err := m.Location(src) + loc, err := m.SpanLocation(src) if err != nil { t.Fatalf("failed for %v: %v", src, err) } @@ -1119,11 +1119,11 @@ func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[sp for _, docEdits := range edits { if docEdits.TextDocumentEdit != nil { uri := docEdits.TextDocumentEdit.TextDocument.URI.SpanURI() - var m *protocol.ColumnMapper + var m *protocol.Mapper // If we have already edited this file, we use the edited version (rather than the // file in its original state) so that we preserve our initial changes. if content, ok := res[uri]; ok { - m = protocol.NewColumnMapper(uri, []byte(content)) + m = protocol.NewMapper(uri, []byte(content)) } else { var err error if m, err = r.data.Mapper(uri); err != nil { @@ -1211,7 +1211,7 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa if err != nil { t.Fatal(err) } - loc, err := m.Location(spn) + loc, err := m.SpanLocation(spn) if err != nil { t.Fatalf("failed for %v: %v", loc, err) } @@ -1321,7 +1321,7 @@ func (r *runner) SelectionRanges(t *testing.T, spn span.Span) { if err != nil { t.Fatal(err) } - loc, err := sm.Location(spn) + loc, err := sm.SpanLocation(spn) if err != nil { t.Error(err) } @@ -1402,7 +1402,7 @@ func TestBytesOffset(t *testing.T) { for i, test := range tests { fname := fmt.Sprintf("test %d", i) uri := span.URIFromPath(fname) - mapper := protocol.NewColumnMapper(uri, []byte(test.text)) + mapper := protocol.NewMapper(uri, []byte(test.text)) got, err := mapper.PositionPoint(test.pos) if err != nil && test.want != -1 { t.Errorf("%d: unexpected error: %v", i, err) diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go new file mode 100644 index 00000000000..17bc73f5e49 --- /dev/null +++ b/gopls/internal/lsp/protocol/mapper.go @@ -0,0 +1,462 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protocol + +// This file defines Mapper, which wraps a file content buffer +// ([]byte) and provides efficient conversion between every kind of +// position representation. +// +// Here's a handy guide for your tour of the location zoo: +// +// Imports: protocol --> span --> token +// +// protocol: for the LSP protocol. +// protocol.Mapper = (URI, Content). Does all offset <=> column conversions. +// protocol.MappedRange = (protocol.Mapper, {start,end} int) +// protocol.Location = (URI, protocol.Range) +// protocol.Range = (start, end Position) +// protocol.Position = (line, char uint32) 0-based UTF-16 +// +// span: for optional fields; useful for CLIs and tests without access to file contents. +// span.Point = (line?, col?, offset?) 1-based UTF-8 +// span.Span = (uri URI, start, end span.Point) +// +// token: for interaction with the go/* syntax packages: +// safetoken.Range = (file token.File, start, end token.Pos) +// token.Pos +// token.FileSet +// token.File +// offset int +// (see also safetoken) +// +// TODO(adonovan): simplify this picture: +// - Eliminate most/all uses of safetoken.Range in gopls, as +// without a Mapper it's not really self-contained. +// It is mostly used by completion. Given access to complete.mapper, +// it could use a pair of byte offsets instead. +// - Mapper.OffsetPoint and .PointPosition aren't used outside this package. +// OffsetSpan is barely used, and its user would better off with a MappedRange +// or protocol.Range. The span package data types are mostly used in tests +// and in argument parsing (without access to file content). +// - move Mapper to mapper.go. +// +// TODO(adonovan): also, write an overview of the position landscape +// in the Mapper doc comment, mentioning the various subtleties, +// the EOF+1 bug (#57490), the \n-at-EOF bug (#41029), the workarounds +// for both bugs in both safetoken and Mapper. Also mention that +// export data doesn't currently preserve accurate column or offset +// information: both are set to garbage based on the assumption of a +// "rectangular" file. + +import ( + "bytes" + "fmt" + "go/ast" + "go/token" + "path/filepath" + "sort" + "strings" + "sync" + "unicode/utf8" + + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" +) + +// A Mapper wraps the content of a file and provides mapping +// from byte offsets to and from other notations of position: +// +// - (line, col8) pairs, where col8 is a 1-based UTF-8 column number (bytes), +// as used by the go/token and span packages. +// +// - (line, col16) pairs, where col16 is a 1-based UTF-16 column number, +// as used by the LSP protocol; +// +// - (line, colRune) pairs, where colRune is a rune index, as used by ParseWork. +// (Not yet implemented, but could easily be.) +// +// All conversion methods are named "FromTo", where From and To are the two types. +// For example, the PointPosition method converts from a Point to a Position. +// +// Mapper does not intrinsically depend on go/token-based +// representations. Use safetoken to map between token.Pos <=> byte +// offsets, or the convenience methods such as PosPosition, +// NodePosition, or NodeRange. +type Mapper struct { + URI span.URI + Content []byte + + // Line-number information is requested only for a tiny + // fraction of Mappers, so we compute it lazily. + // Call initLines() before accessing fields below. + linesOnce sync.Once + lineStart []int // byte offset of start of ith line (0-based); last=EOF iff \n-terminated + nonASCII bool + + // TODO(adonovan): adding an extra lineStart entry for EOF + // might simplify every method that accesses it. Try it out. +} + +// NewMapper creates a new mapper for the given URI and content. +func NewMapper(uri span.URI, content []byte) *Mapper { + return &Mapper{URI: uri, Content: content} +} + +// initLines populates the lineStart table. +func (m *Mapper) initLines() { + m.linesOnce.Do(func() { + nlines := bytes.Count(m.Content, []byte("\n")) + m.lineStart = make([]int, 1, nlines+1) // initially []int{0} + for offset, b := range m.Content { + if b == '\n' { + m.lineStart = append(m.lineStart, offset+1) + } + if b >= utf8.RuneSelf { + m.nonASCII = true + } + } + }) +} + +// -- conversions from span (UTF-8) domain -- + +// SpanLocation converts a (UTF-8) span to a protocol (UTF-16) range. +// Precondition: the URIs of SpanLocation and Mapper match. +func (m *Mapper) SpanLocation(s span.Span) (Location, error) { + rng, err := m.SpanRange(s) + if err != nil { + return Location{}, err + } + return Location{URI: URIFromSpanURI(s.URI()), Range: rng}, nil +} + +// SpanRange converts a (UTF-8) span to a protocol (UTF-16) range. +// Precondition: the URIs of Span and Mapper match. +func (m *Mapper) SpanRange(s span.Span) (Range, error) { + // Assert that we aren't using the wrong mapper. + // We check only the base name, and case insensitively, + // because we can't assume clean paths, no symbolic links, + // case-sensitive directories. The authoritative answer + // requires querying the file system, and we don't want + // to do that. + if !strings.EqualFold(filepath.Base(string(m.URI)), filepath.Base(string(s.URI()))) { + return Range{}, bug.Errorf("mapper is for file %q instead of %q", m.URI, s.URI()) + } + start, err := m.PointPosition(s.Start()) + if err != nil { + return Range{}, fmt.Errorf("start: %w", err) + } + end, err := m.PointPosition(s.End()) + if err != nil { + return Range{}, fmt.Errorf("end: %w", err) + } + return Range{Start: start, End: end}, nil +} + +// PointPosition converts a valid span (UTF-8) point to a protocol (UTF-16) position. +func (m *Mapper) PointPosition(p span.Point) (Position, error) { + if p.HasPosition() { + line, col8 := p.Line()-1, p.Column()-1 // both 0-based + m.initLines() + if line >= len(m.lineStart) { + return Position{}, fmt.Errorf("line number %d out of range (max %d)", line, len(m.lineStart)) + } + offset := m.lineStart[line] + end := offset + col8 + + // Validate column. + if end > len(m.Content) { + return Position{}, fmt.Errorf("column is beyond end of file") + } else if line+1 < len(m.lineStart) && end >= m.lineStart[line+1] { + return Position{}, fmt.Errorf("column is beyond end of line") + } + + char := utf16len(m.Content[offset:end]) + return Position{Line: uint32(line), Character: uint32(char)}, nil + } + if p.HasOffset() { + return m.OffsetPosition(p.Offset()) + } + return Position{}, fmt.Errorf("point has neither offset nor line/column") +} + +// -- conversions from byte offsets -- + +// OffsetRange converts a byte-offset interval to a protocol (UTF-16) range. +func (m *Mapper) OffsetRange(start, end int) (Range, error) { + if start > end { + return Range{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) + } + startPosition, err := m.OffsetPosition(start) + if err != nil { + return Range{}, fmt.Errorf("start: %v", err) + } + endPosition, err := m.OffsetPosition(end) + if err != nil { + return Range{}, fmt.Errorf("end: %v", err) + } + return Range{Start: startPosition, End: endPosition}, nil +} + +// OffsetSpan converts a byte-offset interval to a (UTF-8) span. +// The resulting span contains line, column, and offset information. +func (m *Mapper) OffsetSpan(start, end int) (span.Span, error) { + if start > end { + return span.Span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) + } + startPoint, err := m.OffsetPoint(start) + if err != nil { + return span.Span{}, fmt.Errorf("start: %v", err) + } + endPoint, err := m.OffsetPoint(end) + if err != nil { + return span.Span{}, fmt.Errorf("end: %v", err) + } + return span.New(m.URI, startPoint, endPoint), nil +} + +// OffsetPosition converts a byte offset to a protocol (UTF-16) position. +func (m *Mapper) OffsetPosition(offset int) (Position, error) { + if !(0 <= offset && offset <= len(m.Content)) { + return Position{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) + } + line, col16 := m.lineCol16(offset) + return Position{Line: uint32(line), Character: uint32(col16)}, nil +} + +// lineCol16 converts a valid byte offset to line and UTF-16 column numbers, both 0-based. +func (m *Mapper) lineCol16(offset int) (int, int) { + line, start, cr := m.line(offset) + var col16 int + if m.nonASCII { + col16 = utf16len(m.Content[start:offset]) + } else { + col16 = offset - start + } + if cr { + col16-- // retreat from \r at line end + } + return line, col16 +} + +// lineCol8 converts a valid byte offset to line and UTF-8 column numbers, both 0-based. +func (m *Mapper) lineCol8(offset int) (int, int) { + line, start, cr := m.line(offset) + col8 := offset - start + if cr { + col8-- // retreat from \r at line end + } + return line, col8 +} + +// line returns: +// - the 0-based index of the line that encloses the (valid) byte offset; +// - the start offset of that line; and +// - whether the offset denotes a carriage return (\r) at line end. +func (m *Mapper) line(offset int) (int, int, bool) { + m.initLines() + // In effect, binary search returns a 1-based result. + line := sort.Search(len(m.lineStart), func(i int) bool { + return offset < m.lineStart[i] + }) + + // Adjustment for line-endings: \r|\n is the same as |\r\n. + var eol int + if line == len(m.lineStart) { + eol = len(m.Content) // EOF + } else { + eol = m.lineStart[line] - 1 + } + cr := offset == eol && offset > 0 && m.Content[offset-1] == '\r' + + line-- // 0-based + + return line, m.lineStart[line], cr +} + +// OffsetPoint converts a byte offset to a span (UTF-8) point. +// The resulting point contains line, column, and offset information. +func (m *Mapper) OffsetPoint(offset int) (span.Point, error) { + if !(0 <= offset && offset <= len(m.Content)) { + return span.Point{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) + } + line, col8 := m.lineCol8(offset) + return span.NewPoint(line+1, col8+1, offset), nil +} + +// -- conversions from protocol domain -- + +// LocationSpan converts a protocol (UTF-16) Location to a (UTF-8) span. +// Precondition: the URIs of Location and Mapper match. +func (m *Mapper) LocationSpan(l Location) (span.Span, error) { + // TODO(adonovan): check that l.URI matches m.URI. + return m.RangeSpan(l.Range) +} + +// RangeSpan converts a protocol (UTF-16) range to a (UTF-8) span. +// The resulting span has valid Positions and Offsets. +func (m *Mapper) RangeSpan(r Range) (span.Span, error) { + start, end, err := m.RangeOffsets(r) + if err != nil { + return span.Span{}, err + } + return m.OffsetSpan(start, end) +} + +// RangeOffsets converts a protocol (UTF-16) range to start/end byte offsets. +func (m *Mapper) RangeOffsets(r Range) (int, int, error) { + start, err := m.PositionOffset(r.Start) + if err != nil { + return 0, 0, err + } + end, err := m.PositionOffset(r.End) + if err != nil { + return 0, 0, err + } + return start, end, nil +} + +// PositionOffset converts a protocol (UTF-16) position to a byte offset. +func (m *Mapper) PositionOffset(p Position) (int, error) { + m.initLines() + + // Validate line number. + if p.Line > uint32(len(m.lineStart)) { + return 0, fmt.Errorf("line number %d out of range 0-%d", p.Line, len(m.lineStart)) + } else if p.Line == uint32(len(m.lineStart)) { + if p.Character == 0 { + return len(m.Content), nil // EOF + } + return 0, fmt.Errorf("column is beyond end of file") + } + + offset := m.lineStart[p.Line] + content := m.Content[offset:] // rest of file from start of enclosing line + + // Advance bytes up to the required number of UTF-16 codes. + col8 := 0 + for col16 := 0; col16 < int(p.Character); col16++ { + r, sz := utf8.DecodeRune(content) + if sz == 0 { + return 0, fmt.Errorf("column is beyond end of file") + } + if r == '\n' { + return 0, fmt.Errorf("column is beyond end of line") + } + if sz == 1 && r == utf8.RuneError { + return 0, fmt.Errorf("buffer contains invalid UTF-8 text") + } + content = content[sz:] + + if r >= 0x10000 { + col16++ // rune was encoded by a pair of surrogate UTF-16 codes + + if col16 == int(p.Character) { + break // requested position is in the middle of a rune + } + } + col8 += sz + } + return offset + col8, nil +} + +// PositionPoint converts a protocol (UTF-16) position to a span (UTF-8) point. +// The resulting point has a valid Position and Offset. +func (m *Mapper) PositionPoint(p Position) (span.Point, error) { + offset, err := m.PositionOffset(p) + if err != nil { + return span.Point{}, err + } + line, col8 := m.lineCol8(offset) + + return span.NewPoint(line+1, col8+1, offset), nil +} + +// -- go/token domain convenience methods -- + +// PosPosition converts a token pos to a protocol (UTF-16) position. +func (m *Mapper) PosPosition(tf *token.File, pos token.Pos) (Position, error) { + offset, err := safetoken.Offset(tf, pos) + if err != nil { + return Position{}, err + } + return m.OffsetPosition(offset) +} + +// PosPosition converts a token range to a protocol (UTF-16) location. +func (m *Mapper) PosLocation(tf *token.File, start, end token.Pos) (Location, error) { + startOffset, endOffset, err := safetoken.Offsets(tf, start, end) + if err != nil { + return Location{}, err + } + rng, err := m.OffsetRange(startOffset, endOffset) + if err != nil { + return Location{}, err + } + return Location{URI: URIFromSpanURI(m.URI), Range: rng}, nil +} + +// PosPosition converts a token range to a protocol (UTF-16) range. +func (m *Mapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { + startOffset, endOffset, err := safetoken.Offsets(tf, start, end) + if err != nil { + return Range{}, err + } + return m.OffsetRange(startOffset, endOffset) +} + +// PosPosition converts a syntax node range to a protocol (UTF-16) range. +func (m *Mapper) NodeRange(tf *token.File, node ast.Node) (Range, error) { + return m.PosRange(tf, node.Pos(), node.End()) +} + +// -- MappedRange -- + +// OffsetMappedRange returns a MappedRange for the given byte offsets. +// A MappedRange can be converted to any other form. +func (m *Mapper) OffsetMappedRange(start, end int) (MappedRange, error) { + if !(0 <= start && start <= end && end <= len(m.Content)) { + return MappedRange{}, fmt.Errorf("invalid offsets (%d, %d) (file %s has size %d)", start, end, m.URI, len(m.Content)) + } + return MappedRange{m, start, end}, nil +} + +// A MappedRange represents a valid byte-offset range of a file. +// Through its Mapper it can be converted into other forms such +// as protocol.Range or span.Span. +// +// Construct one by calling Mapper.OffsetMappedRange with start/end offsets. +// From the go/token domain, call safetoken.Offsets first, +// or use a helper such as ParsedGoFile.MappedPosRange. +type MappedRange struct { + Mapper *Mapper + start, end int // valid byte offsets: 0 <= start <= end <= len(Mapper.Content) +} + +// Offsets returns the (start, end) byte offsets of this range. +func (mr MappedRange) Offsets() (start, end int) { return mr.start, mr.end } + +// -- convenience functions -- + +// URI returns the URI of the range's file. +func (mr MappedRange) URI() span.URI { + return mr.Mapper.URI +} + +// TODO(adonovan): the Range and Span methods of a properly +// constructed MappedRange cannot fail. Change them to panic instead +// of returning the error, for convenience of the callers. +// This means we can also add a String() method! + +// Range returns the range in protocol form. +func (mr MappedRange) Range() (Range, error) { + return mr.Mapper.OffsetRange(mr.start, mr.end) +} + +// Span returns the range in span form. +func (mr MappedRange) Span() (span.Span, error) { + return mr.Mapper.OffsetSpan(mr.start, mr.end) +} diff --git a/gopls/internal/lsp/protocol/mapper_test.go b/gopls/internal/lsp/protocol/mapper_test.go index f57f4bc85de..5e854812793 100644 --- a/gopls/internal/lsp/protocol/mapper_test.go +++ b/gopls/internal/lsp/protocol/mapper_test.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/gopls/internal/span" ) -// This file tests ColumnMapper's logic for converting between +// This file tests Mapper's logic for converting between // span.Point and UTF-16 columns. (The strange form attests to an // earlier abstraction.) @@ -234,7 +234,7 @@ func TestToUTF16(t *testing.T) { t.Skip("expected to fail") } p := span.NewPoint(e.line, e.col, e.offset) - m := protocol.NewColumnMapper("", e.input) + m := protocol.NewMapper("", e.input) pos, err := m.PointPosition(p) if err != nil { if err.Error() != e.err { @@ -263,7 +263,7 @@ func TestToUTF16(t *testing.T) { func TestFromUTF16(t *testing.T) { for _, e := range fromUTF16Tests { t.Run(e.scenario, func(t *testing.T) { - m := protocol.NewColumnMapper("", []byte(e.input)) + m := protocol.NewMapper("", []byte(e.input)) p, err := m.PositionPoint(protocol.Position{ Line: uint32(e.line - 1), Character: uint32(e.utf16col - 1), @@ -347,7 +347,7 @@ var tests = []testCase{ func TestLineChar(t *testing.T) { for _, test := range tests { - m := protocol.NewColumnMapper("", []byte(test.content)) + m := protocol.NewMapper("", []byte(test.content)) offset := test.offset() posn, _ := m.OffsetPosition(offset) gotLine, gotChar := int(posn.Line), int(posn.Character) @@ -359,7 +359,7 @@ func TestLineChar(t *testing.T) { func TestInvalidOffset(t *testing.T) { content := []byte("a𐐀b\r\nx\ny") - m := protocol.NewColumnMapper("", content) + m := protocol.NewMapper("", content) for _, offset := range []int{-1, 100} { posn, err := m.OffsetPosition(offset) if err == nil { @@ -370,7 +370,7 @@ func TestInvalidOffset(t *testing.T) { func TestPosition(t *testing.T) { for _, test := range tests { - m := protocol.NewColumnMapper("", []byte(test.content)) + m := protocol.NewMapper("", []byte(test.content)) offset := test.offset() got, err := m.OffsetPosition(offset) if err != nil { @@ -386,7 +386,7 @@ func TestPosition(t *testing.T) { func TestRange(t *testing.T) { for _, test := range tests { - m := protocol.NewColumnMapper("", []byte(test.content)) + m := protocol.NewMapper("", []byte(test.content)) offset := test.offset() got, err := m.OffsetRange(0, offset) if err != nil { diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 14dbdeacc75..387c1d9535f 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -2,471 +2,15 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file defines ColumnMapper, which wraps a file content buffer -// ([]byte) and provides efficient conversion between every kind of -// position representation. -// -// Here's a handy guide for your tour of the location zoo: -// -// Imports: protocol --> span --> token -// -// protocol: for the LSP protocol. -// protocol.ColumnMapper = (URI, Content). Does all offset <=> column conversions. -// protocol.MappedRange = (protocol.ColumnMapper, {start,end} int) -// protocol.Location = (URI, protocol.Range) -// protocol.Range = (start, end Position) -// protocol.Position = (line, char uint32) 0-based UTF-16 -// -// span: for optional fields; useful for CLIs and tests without access to file contents. -// span.Point = (line?, col?, offset?) 1-based UTF-8 -// span.Span = (uri URI, start, end span.Point) -// -// token: for interaction with the go/* syntax packages: -// safetoken.Range = (file token.File, start, end token.Pos) -// token.Pos -// token.FileSet -// token.File -// offset int -// (see also safetoken) -// -// TODO(adonovan): simplify this picture: -// - Eliminate most/all uses of safetoken.Range in gopls, as -// without a ColumnMapper it's not really self-contained. -// It is mostly used by completion. Given access to complete.mapper, -// it could use a pair of byte offsets instead. -// - ColumnMapper.OffsetPoint and .PointPosition aren't used outside this package. -// OffsetSpan is barely used, and its user would better off with a MappedRange -// or protocol.Range. The span package data types are mostly used in tests -// and in argument parsing (without access to file content). -// - rename ColumnMapper to just Mapper, since it also maps lines -// <=> offsets, and move it to file mapper.go. -// -// TODO(adonovan): also, write an overview of the position landscape -// in the ColumnMapper doc comment, mentioning the various subtleties, -// the EOF+1 bug (#57490), the \n-at-EOF bug (#41029), the workarounds -// for both bugs in both safetoken and ColumnMapper. Also mention that -// export data doesn't currently preserve accurate column or offset -// information: both are set to garbage based on the assumption of a -// "rectangular" file. - package protocol import ( - "bytes" "fmt" - "go/ast" - "go/token" - "path/filepath" - "sort" - "strings" - "sync" "unicode/utf8" - "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/bug" ) -// A ColumnMapper wraps the content of a file and provides mapping -// from byte offsets to and from other notations of position: -// -// - (line, col8) pairs, where col8 is a 1-based UTF-8 column number (bytes), -// as used by the go/token and span packages. -// -// - (line, col16) pairs, where col16 is a 1-based UTF-16 column number, -// as used by the LSP protocol; -// -// - (line, colRune) pairs, where colRune is a rune index, as used by ParseWork. -// (Not yet implemented, but could easily be.) -// -// All conversion methods are named "FromTo", where From and To are the two types. -// For example, the PointPosition method converts from a Point to a Position. -// -// ColumnMapper does not intrinsically depend on go/token-based -// representations. Use safetoken to map between token.Pos <=> byte -// offsets, or the convenience methods such as PosPosition, -// NodePosition, or NodeRange. -type ColumnMapper struct { - URI span.URI - Content []byte - - // Line-number information is requested only for a tiny - // fraction of ColumnMappers, so we compute it lazily. - // Call initLines() before accessing fields below. - linesOnce sync.Once - lineStart []int // byte offset of start of ith line (0-based); last=EOF iff \n-terminated - nonASCII bool - - // TODO(adonovan): adding an extra lineStart entry for EOF - // might simplify every method that accesses it. Try it out. -} - -// NewColumnMapper creates a new column mapper for the given URI and content. -func NewColumnMapper(uri span.URI, content []byte) *ColumnMapper { - return &ColumnMapper{URI: uri, Content: content} -} - -// initLines populates the lineStart table. -func (m *ColumnMapper) initLines() { - m.linesOnce.Do(func() { - nlines := bytes.Count(m.Content, []byte("\n")) - m.lineStart = make([]int, 1, nlines+1) // initially []int{0} - for offset, b := range m.Content { - if b == '\n' { - m.lineStart = append(m.lineStart, offset+1) - } - if b >= utf8.RuneSelf { - m.nonASCII = true - } - } - }) -} - -// -- conversions from span (UTF-8) domain -- - -// Location converts a (UTF-8) span to a protocol (UTF-16) range. -// Precondition: the URIs of Location and ColumnMapper match. -// TODO(adonovan): rename to SpanLocation. -func (m *ColumnMapper) Location(s span.Span) (Location, error) { - rng, err := m.Range(s) - if err != nil { - return Location{}, err - } - return Location{URI: URIFromSpanURI(s.URI()), Range: rng}, nil -} - -// Range converts a (UTF-8) span to a protocol (UTF-16) range. -// Precondition: the URIs of Span and ColumnMapper match. -// TODO(adonovan): rename to SpanRange -func (m *ColumnMapper) Range(s span.Span) (Range, error) { - // Assert that we aren't using the wrong mapper. - // We check only the base name, and case insensitively, - // because we can't assume clean paths, no symbolic links, - // case-sensitive directories. The authoritative answer - // requires querying the file system, and we don't want - // to do that. - if !strings.EqualFold(filepath.Base(string(m.URI)), filepath.Base(string(s.URI()))) { - return Range{}, bug.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI()) - } - start, err := m.PointPosition(s.Start()) - if err != nil { - return Range{}, fmt.Errorf("start: %w", err) - } - end, err := m.PointPosition(s.End()) - if err != nil { - return Range{}, fmt.Errorf("end: %w", err) - } - return Range{Start: start, End: end}, nil -} - -// PointPosition converts a valid span (UTF-8) point to a protocol (UTF-16) position. -func (m *ColumnMapper) PointPosition(p span.Point) (Position, error) { - if p.HasPosition() { - line, col8 := p.Line()-1, p.Column()-1 // both 0-based - m.initLines() - if line >= len(m.lineStart) { - return Position{}, fmt.Errorf("line number %d out of range (max %d)", line, len(m.lineStart)) - } - offset := m.lineStart[line] - end := offset + col8 - - // Validate column. - if end > len(m.Content) { - return Position{}, fmt.Errorf("column is beyond end of file") - } else if line+1 < len(m.lineStart) && end >= m.lineStart[line+1] { - return Position{}, fmt.Errorf("column is beyond end of line") - } - - char := utf16len(m.Content[offset:end]) - return Position{Line: uint32(line), Character: uint32(char)}, nil - } - if p.HasOffset() { - return m.OffsetPosition(p.Offset()) - } - return Position{}, fmt.Errorf("point has neither offset nor line/column") -} - -// -- conversions from byte offsets -- - -// OffsetRange converts a byte-offset interval to a protocol (UTF-16) range. -func (m *ColumnMapper) OffsetRange(start, end int) (Range, error) { - if start > end { - return Range{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) - } - startPosition, err := m.OffsetPosition(start) - if err != nil { - return Range{}, fmt.Errorf("start: %v", err) - } - endPosition, err := m.OffsetPosition(end) - if err != nil { - return Range{}, fmt.Errorf("end: %v", err) - } - return Range{Start: startPosition, End: endPosition}, nil -} - -// OffsetSpan converts a byte-offset interval to a (UTF-8) span. -// The resulting span contains line, column, and offset information. -func (m *ColumnMapper) OffsetSpan(start, end int) (span.Span, error) { - if start > end { - return span.Span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) - } - startPoint, err := m.OffsetPoint(start) - if err != nil { - return span.Span{}, fmt.Errorf("start: %v", err) - } - endPoint, err := m.OffsetPoint(end) - if err != nil { - return span.Span{}, fmt.Errorf("end: %v", err) - } - return span.New(m.URI, startPoint, endPoint), nil -} - -// OffsetPosition converts a byte offset to a protocol (UTF-16) position. -func (m *ColumnMapper) OffsetPosition(offset int) (Position, error) { - if !(0 <= offset && offset <= len(m.Content)) { - return Position{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) - } - line, col16 := m.lineCol16(offset) - return Position{Line: uint32(line), Character: uint32(col16)}, nil -} - -// lineCol16 converts a valid byte offset to line and UTF-16 column numbers, both 0-based. -func (m *ColumnMapper) lineCol16(offset int) (int, int) { - line, start, cr := m.line(offset) - var col16 int - if m.nonASCII { - col16 = utf16len(m.Content[start:offset]) - } else { - col16 = offset - start - } - if cr { - col16-- // retreat from \r at line end - } - return line, col16 -} - -// lineCol8 converts a valid byte offset to line and UTF-8 column numbers, both 0-based. -func (m *ColumnMapper) lineCol8(offset int) (int, int) { - line, start, cr := m.line(offset) - col8 := offset - start - if cr { - col8-- // retreat from \r at line end - } - return line, col8 -} - -// line returns: -// - the 0-based index of the line that encloses the (valid) byte offset; -// - the start offset of that line; and -// - whether the offset denotes a carriage return (\r) at line end. -func (m *ColumnMapper) line(offset int) (int, int, bool) { - m.initLines() - // In effect, binary search returns a 1-based result. - line := sort.Search(len(m.lineStart), func(i int) bool { - return offset < m.lineStart[i] - }) - - // Adjustment for line-endings: \r|\n is the same as |\r\n. - var eol int - if line == len(m.lineStart) { - eol = len(m.Content) // EOF - } else { - eol = m.lineStart[line] - 1 - } - cr := offset == eol && offset > 0 && m.Content[offset-1] == '\r' - - line-- // 0-based - - return line, m.lineStart[line], cr -} - -// OffsetPoint converts a byte offset to a span (UTF-8) point. -// The resulting point contains line, column, and offset information. -func (m *ColumnMapper) OffsetPoint(offset int) (span.Point, error) { - if !(0 <= offset && offset <= len(m.Content)) { - return span.Point{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) - } - line, col8 := m.lineCol8(offset) - return span.NewPoint(line+1, col8+1, offset), nil -} - -// -- conversions from protocol domain -- - -// Span converts a protocol (UTF-16) Location to a (UTF-8) span. -// Precondition: the URIs of Location and ColumnMapper match. -// TODO(adonovan): rename to LocationSpan. -func (m *ColumnMapper) Span(l Location) (span.Span, error) { - // TODO(adonovan): check that l.URI matches m.URI. - return m.RangeSpan(l.Range) -} - -// RangeSpan converts a protocol (UTF-16) range to a (UTF-8) span. -// The resulting span has valid Positions and Offsets. -func (m *ColumnMapper) RangeSpan(r Range) (span.Span, error) { - start, end, err := m.RangeOffsets(r) - if err != nil { - return span.Span{}, err - } - return m.OffsetSpan(start, end) -} - -// RangeOffsets converts a protocol (UTF-16) range to start/end byte offsets. -func (m *ColumnMapper) RangeOffsets(r Range) (int, int, error) { - start, err := m.PositionOffset(r.Start) - if err != nil { - return 0, 0, err - } - end, err := m.PositionOffset(r.End) - if err != nil { - return 0, 0, err - } - return start, end, nil -} - -// PositionOffset converts a protocol (UTF-16) position to a byte offset. -func (m *ColumnMapper) PositionOffset(p Position) (int, error) { - m.initLines() - - // Validate line number. - if p.Line > uint32(len(m.lineStart)) { - return 0, fmt.Errorf("line number %d out of range 0-%d", p.Line, len(m.lineStart)) - } else if p.Line == uint32(len(m.lineStart)) { - if p.Character == 0 { - return len(m.Content), nil // EOF - } - return 0, fmt.Errorf("column is beyond end of file") - } - - offset := m.lineStart[p.Line] - content := m.Content[offset:] // rest of file from start of enclosing line - - // Advance bytes up to the required number of UTF-16 codes. - col8 := 0 - for col16 := 0; col16 < int(p.Character); col16++ { - r, sz := utf8.DecodeRune(content) - if sz == 0 { - return 0, fmt.Errorf("column is beyond end of file") - } - if r == '\n' { - return 0, fmt.Errorf("column is beyond end of line") - } - if sz == 1 && r == utf8.RuneError { - return 0, fmt.Errorf("buffer contains invalid UTF-8 text") - } - content = content[sz:] - - if r >= 0x10000 { - col16++ // rune was encoded by a pair of surrogate UTF-16 codes - - if col16 == int(p.Character) { - break // requested position is in the middle of a rune - } - } - col8 += sz - } - return offset + col8, nil -} - -// PositionPoint converts a protocol (UTF-16) position to a span (UTF-8) point. -// The resulting point has a valid Position and Offset. -func (m *ColumnMapper) PositionPoint(p Position) (span.Point, error) { - offset, err := m.PositionOffset(p) - if err != nil { - return span.Point{}, err - } - line, col8 := m.lineCol8(offset) - - return span.NewPoint(line+1, col8+1, offset), nil -} - -// -- go/token domain convenience methods -- - -// PosPosition converts a token pos to a protocol (UTF-16) position. -func (m *ColumnMapper) PosPosition(tf *token.File, pos token.Pos) (Position, error) { - offset, err := safetoken.Offset(tf, pos) - if err != nil { - return Position{}, err - } - return m.OffsetPosition(offset) -} - -// PosPosition converts a token range to a protocol (UTF-16) location. -func (m *ColumnMapper) PosLocation(tf *token.File, start, end token.Pos) (Location, error) { - startOffset, endOffset, err := safetoken.Offsets(tf, start, end) - if err != nil { - return Location{}, err - } - rng, err := m.OffsetRange(startOffset, endOffset) - if err != nil { - return Location{}, err - } - return Location{URI: URIFromSpanURI(m.URI), Range: rng}, nil -} - -// PosPosition converts a token range to a protocol (UTF-16) range. -func (m *ColumnMapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { - startOffset, endOffset, err := safetoken.Offsets(tf, start, end) - if err != nil { - return Range{}, err - } - return m.OffsetRange(startOffset, endOffset) -} - -// PosPosition converts a syntax node range to a protocol (UTF-16) range. -func (m *ColumnMapper) NodeRange(tf *token.File, node ast.Node) (Range, error) { - return m.PosRange(tf, node.Pos(), node.End()) -} - -// -- MappedRange -- - -// OffsetMappedRange returns a MappedRange for the given byte offsets. -// A MappedRange can be converted to any other form. -func (m *ColumnMapper) OffsetMappedRange(start, end int) (MappedRange, error) { - if !(0 <= start && start <= end && end <= len(m.Content)) { - return MappedRange{}, fmt.Errorf("invalid offsets (%d, %d) (file %s has size %d)", start, end, m.URI, len(m.Content)) - } - return MappedRange{m, start, end}, nil -} - -// A MappedRange represents a valid byte-offset range of a file. -// Through its ColumnMapper it can be converted into other forms such -// as protocol.Range or span.Span. -// -// Construct one by calling ColumnMapper.OffsetMappedRange with start/end offsets. -// From the go/token domain, call safetoken.Offsets first, -// or use a helper such as ParsedGoFile.MappedPosRange. -type MappedRange struct { - Mapper *ColumnMapper - start, end int // valid byte offsets: 0 <= start <= end <= len(Mapper.Content) -} - -// Offsets returns the (start, end) byte offsets of this range. -func (mr MappedRange) Offsets() (start, end int) { return mr.start, mr.end } - -// -- convenience functions -- - -// URI returns the URI of the range's file. -func (mr MappedRange) URI() span.URI { - return mr.Mapper.URI -} - -// TODO(adonovan): the Range and Span methods of a properly -// constructed MappedRange cannot fail. Change them to panic instead -// of returning the error, for convenience of the callers. -// This means we can also add a String() method! - -// Range returns the range in protocol form. -func (mr MappedRange) Range() (Range, error) { - return mr.Mapper.OffsetRange(mr.start, mr.end) -} - -// Span returns the range in span form. -func (mr MappedRange) Span() (span.Span, error) { - return mr.Mapper.OffsetSpan(mr.start, mr.end) -} - -// -- misc helpers -- - func URIFromSpanURI(uri span.URI) DocumentURI { return DocumentURI(uri) // simple conversion } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 845b9596428..a19b07605c3 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -218,7 +218,7 @@ type completer struct { methodSetCache map[methodSetKey]*types.MethodSet // mapper converts the positions in the file from which the completion originated. - mapper *protocol.ColumnMapper + mapper *protocol.Mapper // startTime is when we started processing this completion request. It does // not include any time the request spent in the queue. @@ -291,7 +291,7 @@ type Selection struct { content string cursor token.Pos // relative to rng.TokFile rng safetoken.Range - mapper *protocol.ColumnMapper + mapper *protocol.Mapper } func (p Selection) Content() string { diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 9b6322c9243..b3f09f04314 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -110,12 +110,12 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi } editsPerFile[fh.URI()] = te } - // TODO(adonovan): once FileHandle has a ColumnMapper, eliminate this. + // TODO(adonovan): once FileHandle has a Mapper, eliminate this. content, err := fh.Read() if err != nil { return nil, err } - m := protocol.NewColumnMapper(fh.URI(), content) + m := protocol.NewMapper(fh.URI(), content) rng, err := m.PosRange(tokFile, edit.Pos, end) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go index 51ba8c6ff44..932671686ba 100644 --- a/gopls/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -314,7 +314,7 @@ func computeTextEdits(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile, // protocolEditsFromSource converts text edits to LSP edits using the original // source. func protocolEditsFromSource(src []byte, edits []diff.Edit) ([]protocol.TextEdit, error) { - m := protocol.NewColumnMapper("", src) + m := protocol.NewMapper("", src) var result []protocol.TextEdit for _, edit := range edits { rng, err := m.OffsetRange(edit.Start, edit.End) @@ -337,7 +337,7 @@ func protocolEditsFromSource(src []byte, edits []diff.Edit) ([]protocol.TextEdit // ToProtocolEdits converts diff.Edits to LSP TextEdits. // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray -func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.Edit) ([]protocol.TextEdit, error) { +func ToProtocolEdits(m *protocol.Mapper, edits []diff.Edit) ([]protocol.TextEdit, error) { // LSP doesn't require TextEditArray to be sorted: // this is the receiver's concern. But govim, and perhaps // other clients have historically relied on the order. @@ -360,7 +360,7 @@ func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.Edit) ([]protocol.Te // FromProtocolEdits converts LSP TextEdits to diff.Edits. // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray -func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.Edit, error) { +func FromProtocolEdits(m *protocol.Mapper, edits []protocol.TextEdit) ([]diff.Edit, error) { if edits == nil { return nil, nil } @@ -381,7 +381,7 @@ func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]d // ApplyProtocolEdits applies the patch (edits) to m.Content and returns the result. // It also returns the edits converted to diff-package form. -func ApplyProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) (string, []diff.Edit, error) { +func ApplyProtocolEdits(m *protocol.Mapper, edits []protocol.TextEdit) (string, []diff.Edit, error) { diffEdits, err := FromProtocolEdits(m, edits) if err != nil { return "", nil, err diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 423f9b4ac18..02c959fbb96 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -58,7 +58,7 @@ type Declaration struct { // The typechecked node node ast.Node - nodeFile *ParsedGoFile // provides token.File and ColumnMapper for node + nodeFile *ParsedGoFile // provides token.File and Mapper for node // Optional: the fully parsed node, to be used for formatting in cases where // node has missing information. This could be the case when node was parsed diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 92765494871..83884188f05 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -248,7 +248,7 @@ func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, p if err != nil { return nil, err } - m := protocol.NewColumnMapper(uri, content) + m := protocol.NewMapper(uri, content) offset, err := m.PositionOffset(pp) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go index 3db3248726a..08fefa24158 100644 --- a/gopls/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -22,7 +22,7 @@ const ( maxLabelLength = 28 ) -type InlayHintFunc func(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint +type InlayHintFunc func(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint type Hint struct { Name string @@ -130,7 +130,7 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng proto return hints, nil } -func parameterNames(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +func parameterNames(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { callExpr, ok := node.(*ast.CallExpr) if !ok { return nil @@ -177,7 +177,7 @@ func parameterNames(node ast.Node, m *protocol.ColumnMapper, tf *token.File, inf return hints } -func funcTypeParams(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +func funcTypeParams(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { ce, ok := node.(*ast.CallExpr) if !ok { return nil @@ -208,7 +208,7 @@ func funcTypeParams(node ast.Node, m *protocol.ColumnMapper, tf *token.File, inf }} } -func assignVariableTypes(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func assignVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { stmt, ok := node.(*ast.AssignStmt) if !ok || stmt.Tok != token.DEFINE { return nil @@ -223,7 +223,7 @@ func assignVariableTypes(node ast.Node, m *protocol.ColumnMapper, tf *token.File return hints } -func rangeVariableTypes(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func rangeVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { rStmt, ok := node.(*ast.RangeStmt) if !ok { return nil @@ -238,7 +238,7 @@ func rangeVariableTypes(node ast.Node, m *protocol.ColumnMapper, tf *token.File, return hints } -func variableType(e ast.Expr, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint { +func variableType(e ast.Expr, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint { typ := info.TypeOf(e) if typ == nil { return nil @@ -255,7 +255,7 @@ func variableType(e ast.Expr, m *protocol.ColumnMapper, tf *token.File, info *ty } } -func constantValues(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +func constantValues(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { genDecl, ok := node.(*ast.GenDecl) if !ok || genDecl.Tok != token.CONST { return nil @@ -306,7 +306,7 @@ func constantValues(node ast.Node, m *protocol.ColumnMapper, tf *token.File, inf return hints } -func compositeLiteralFields(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { compLit, ok := node.(*ast.CompositeLit) if !ok { return nil @@ -354,7 +354,7 @@ func compositeLiteralFields(node ast.Node, m *protocol.ColumnMapper, tf *token.F return hints } -func compositeLiteralTypes(node ast.Node, m *protocol.ColumnMapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func compositeLiteralTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { compLit, ok := node.(*ast.CompositeLit) if !ok { return nil diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 1867a921797..e07127938d3 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -646,7 +646,7 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO if err != nil { return nil, err } - m := protocol.NewColumnMapper(uri, data) + m := protocol.NewMapper(uri, data) protocolEdits, err := ToProtocolEdits(m, edits) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index f27f9d783ad..520900e4456 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -102,7 +102,7 @@ func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests if err != nil { t.Fatal(err) } - loc, err := mapper.Location(spn) + loc, err := mapper.SpanLocation(spn) if err != nil { t.Fatalf("failed for %v: %v", spn, err) } @@ -590,7 +590,7 @@ func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) if err != nil { t.Fatal(err) } - loc, err := sm.Location(spn) + loc, err := sm.SpanLocation(spn) if err != nil { t.Fatalf("failed for %v: %v", spn, err) } @@ -612,7 +612,7 @@ func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) if err != nil { t.Fatal(err) } - imp, err := lm.Span(locs[i]) + imp, err := lm.LocationSpan(locs[i]) if err != nil { t.Fatalf("failed for %v: %v", locs[i], err) } @@ -817,12 +817,12 @@ func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {} func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {} -func spanToRange(data *tests.Data, spn span.Span) (*protocol.ColumnMapper, protocol.Range, error) { +func spanToRange(data *tests.Data, spn span.Span) (*protocol.Mapper, protocol.Range, error) { m, err := data.Mapper(spn.URI()) if err != nil { return nil, protocol.Range{}, err } - srcRng, err := m.Range(spn) + srcRng, err := m.SpanRange(spn) if err != nil { return nil, protocol.Range{}, err } diff --git a/gopls/internal/lsp/source/symbols.go b/gopls/internal/lsp/source/symbols.go index d7c067108f8..a5c015e0aa0 100644 --- a/gopls/internal/lsp/source/symbols.go +++ b/gopls/internal/lsp/source/symbols.go @@ -71,7 +71,7 @@ func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p return symbols, nil } -func funcSymbol(m *protocol.ColumnMapper, tf *token.File, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) { +func funcSymbol(m *protocol.Mapper, tf *token.File, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) { s := protocol.DocumentSymbol{ Name: decl.Name.Name, Kind: protocol.Function, @@ -92,7 +92,7 @@ func funcSymbol(m *protocol.ColumnMapper, tf *token.File, decl *ast.FuncDecl) (p return s, nil } -func typeSymbol(m *protocol.ColumnMapper, tf *token.File, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) { +func typeSymbol(m *protocol.Mapper, tf *token.File, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) { s := protocol.DocumentSymbol{ Name: spec.Name.Name, } @@ -109,7 +109,7 @@ func typeSymbol(m *protocol.ColumnMapper, tf *token.File, spec *ast.TypeSpec) (p return s, nil } -func typeDetails(m *protocol.ColumnMapper, tf *token.File, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) { +func typeDetails(m *protocol.Mapper, tf *token.File, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) { switch typExpr := typExpr.(type) { case *ast.StructType: kind = protocol.Struct @@ -141,7 +141,7 @@ func typeDetails(m *protocol.ColumnMapper, tf *token.File, typExpr ast.Expr) (ki return } -func fieldListSymbols(m *protocol.ColumnMapper, tf *token.File, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol { +func fieldListSymbols(m *protocol.Mapper, tf *token.File, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol { if fields == nil { return nil } @@ -203,7 +203,7 @@ func fieldListSymbols(m *protocol.ColumnMapper, tf *token.File, fields *ast.Fiel return symbols } -func varSymbol(m *protocol.ColumnMapper, tf *token.File, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) { +func varSymbol(m *protocol.Mapper, tf *token.File, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) { s := protocol.DocumentSymbol{ Name: name.Name, Kind: protocol.Variable, diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 61a4a95c081..4ed89e4be01 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -378,7 +378,7 @@ type ParsedGoFile struct { // actual content of the file if we have fixed the AST. Src []byte Fixed bool - Mapper *protocol.ColumnMapper + Mapper *protocol.Mapper ParseErr scanner.ErrorList } @@ -427,7 +427,7 @@ func (pgf *ParsedGoFile) RangeToTokenRange(r protocol.Range) (safetoken.Range, e type ParsedModule struct { URI span.URI File *modfile.File - Mapper *protocol.ColumnMapper + Mapper *protocol.Mapper ParseErrors []*Diagnostic } @@ -435,7 +435,7 @@ type ParsedModule struct { type ParsedWorkFile struct { URI span.URI File *modfile.WorkFile - Mapper *protocol.ColumnMapper + Mapper *protocol.Mapper ParseErrors []*Diagnostic } diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index b7a6b047aef..3225c15b22c 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -140,7 +140,7 @@ type Data struct { ModfileFlagAvailable bool mappersMu sync.Mutex - mappers map[span.URI]*protocol.ColumnMapper + mappers map[span.URI]*protocol.Mapper } // TODO(adonovan): there are multiple implementations of this (undocumented) @@ -339,7 +339,7 @@ func load(t testing.TB, mode string, dir string) *Data { fragments: map[string]string{}, golden: map[string]*Golden{}, mode: mode, - mappers: map[span.URI]*protocol.ColumnMapper{}, + mappers: map[span.URI]*protocol.Mapper{}, } if !*UpdateGolden { @@ -1057,7 +1057,7 @@ func checkData(t *testing.T, data *Data) { } } -func (data *Data) Mapper(uri span.URI) (*protocol.ColumnMapper, error) { +func (data *Data) Mapper(uri span.URI) (*protocol.Mapper, error) { data.mappersMu.Lock() defer data.mappersMu.Unlock() @@ -1066,7 +1066,7 @@ func (data *Data) Mapper(uri span.URI) (*protocol.ColumnMapper, error) { if err != nil { return nil, err } - data.mappers[uri] = protocol.NewColumnMapper(uri, content) + data.mappers[uri] = protocol.NewMapper(uri, content) } return data.mappers[uri], nil } @@ -1357,7 +1357,7 @@ func (data *Data) collectSymbols(name string, selectionRng span.Span, kind, deta // mustRange converts spn into a protocol.Range, panicking on any error. func (data *Data) mustRange(spn span.Span) protocol.Range { m, err := data.Mapper(spn.URI()) - rng, err := m.Range(spn) + rng, err := m.SpanRange(spn) if err != nil { panic(fmt.Sprintf("converting span %s to range: %v", spn, err)) } diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index 4c6bfdac0e5..3d4c58c5d6e 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -27,7 +27,7 @@ import ( // DiffLinks takes the links we got and checks if they are located within the source or a Note. // If the link is within a Note, the link is removed. // Returns an diff comment if there are differences and empty string if no diffs. -func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string { +func DiffLinks(mapper *protocol.Mapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string { var notePositions []token.Position links := make(map[span.Span]string, len(wantLinks)) for _, link := range wantLinks { @@ -508,7 +508,7 @@ func WorkspaceSymbolsString(ctx context.Context, data *Data, queryURI span.URI, if err != nil { return "", err } - spn, err := m.Span(s.Location) + spn, err := m.LocationSpan(s.Location) if err != nil { return "", err } diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go index ab765b60dd3..e70bf9eb5b6 100644 --- a/gopls/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -362,8 +362,8 @@ func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, chan // TODO(adonovan): refactor to use diff.Apply, which is robust w.r.t. // out-of-order or overlapping changes---and much more efficient. - // Make sure to update column mapper along with the content. - m := protocol.NewColumnMapper(uri, content) + // Make sure to update mapper along with the content. + m := protocol.NewMapper(uri, content) if change.Range == nil { return nil, fmt.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal) } diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go index 65c6e2071d7..442c3216982 100644 --- a/gopls/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -25,7 +25,7 @@ import ( // and tests where a position is notated without access to the content // of the file. // -// Use protocol.ColumnMapper to convert between Span and other +// Use protocol.Mapper to convert between Span and other // representations, such as go/token (also UTF-8) or the LSP protocol // (UTF-16). The latter requires access to file contents. type Span struct { From b79893424cb0a12a4192f38bf959773a3a69c145 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 10 Jan 2023 21:55:45 -0500 Subject: [PATCH 602/723] gopls/internal/lsp/protocol: cleanups and docs for Mapper Improve documentation at Mapper. - Add MappedRange.Location convenience method. Also a String method. - Make all methods of MappedRange infallible (no error result). - Call PosLocation, RangeSpan, and other Mapper methods instead of making two calls. - Add ParsedGoFile.PosLocation convenience method. Also rename Pos to PositionPos. I'm going to stop mucking about with positions soon, I promise. Change-Id: Iea1bd3f60f0454988f6d781e9d55f8a22417af15 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461535 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/analysis.go | 11 +- gopls/internal/lsp/cmd/highlight.go | 3 +- gopls/internal/lsp/cmd/prepare_rename.go | 3 +- gopls/internal/lsp/cmd/test/prepare_rename.go | 3 +- gopls/internal/lsp/definition.go | 21 +-- gopls/internal/lsp/folding_range.go | 5 +- gopls/internal/lsp/protocol/mapper.go | 139 +++++++++++------- gopls/internal/lsp/safetoken/range.go | 5 + gopls/internal/lsp/selection_range.go | 2 +- gopls/internal/lsp/source/call_hierarchy.go | 16 +- .../lsp/source/completion/completion.go | 2 +- gopls/internal/lsp/source/diagnostics.go | 1 + gopls/internal/lsp/source/folding_range.go | 4 +- gopls/internal/lsp/source/highlight.go | 8 +- gopls/internal/lsp/source/hover.go | 8 +- gopls/internal/lsp/source/identifier.go | 2 +- gopls/internal/lsp/source/implementation.go | 9 +- gopls/internal/lsp/source/references.go | 7 +- gopls/internal/lsp/source/references2.go | 20 +-- gopls/internal/lsp/source/rename.go | 10 +- gopls/internal/lsp/source/signature_help.go | 2 +- gopls/internal/lsp/source/source_test.go | 25 +--- gopls/internal/lsp/source/view.go | 10 +- gopls/internal/lsp/template/parse.go | 2 + gopls/internal/span/span.go | 2 + 25 files changed, 136 insertions(+), 184 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 8bbe2ed213e..d48d298654b 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -1024,16 +1024,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) { if end == token.NoPos { end = start } - rng, err := p.PosRange(start, end) - if err != nil { - return protocol.Location{}, err - } - return protocol.Location{ - // TODO(adonovan): is this sound? - // See dual conversion in toSourceDiagnostic. - URI: protocol.DocumentURI(p.URI), - Range: rng, - }, nil + return p.PosLocation(start, end) } } return protocol.Location{}, diff --git a/gopls/internal/lsp/cmd/highlight.go b/gopls/internal/lsp/cmd/highlight.go index 6f9eac69ea5..bcb7149a43d 100644 --- a/gopls/internal/lsp/cmd/highlight.go +++ b/gopls/internal/lsp/cmd/highlight.go @@ -69,8 +69,7 @@ func (r *highlight) Run(ctx context.Context, args ...string) error { var results []span.Span for _, h := range highlights { - l := protocol.Location{Range: h.Range} - s, err := file.mapper.LocationSpan(l) + s, err := file.mapper.RangeSpan(h.Range) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/prepare_rename.go b/gopls/internal/lsp/cmd/prepare_rename.go index 88b33c4838e..774433df605 100644 --- a/gopls/internal/lsp/cmd/prepare_rename.go +++ b/gopls/internal/lsp/cmd/prepare_rename.go @@ -73,8 +73,7 @@ func (r *prepareRename) Run(ctx context.Context, args ...string) error { return ErrInvalidRenamePosition } - l := protocol.Location{Range: result.Range} - s, err := file.mapper.LocationSpan(l) + s, err := file.mapper.RangeSpan(result.Range) if err != nil { return err } diff --git a/gopls/internal/lsp/cmd/test/prepare_rename.go b/gopls/internal/lsp/cmd/test/prepare_rename.go index 2c22f38a624..b24738881e5 100644 --- a/gopls/internal/lsp/cmd/test/prepare_rename.go +++ b/gopls/internal/lsp/cmd/test/prepare_rename.go @@ -9,7 +9,6 @@ import ( "testing" "golang.org/x/tools/gopls/internal/lsp/cmd" - "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" ) @@ -34,7 +33,7 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare return } - ws, err := m.LocationSpan(protocol.Location{Range: want.Range}) + ws, err := m.RangeSpan(want.Range) if err != nil { t.Errorf("prepare_rename failed: %v", err) } diff --git a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go index d83512a93e7..cd91a0447db 100644 --- a/gopls/internal/lsp/definition.go +++ b/gopls/internal/lsp/definition.go @@ -31,15 +31,7 @@ func (s *Server) definition(ctx context.Context, params *protocol.DefinitionPara } var locations []protocol.Location for _, ref := range ident.Declaration.MappedRange { - decRange, err := ref.Range() - if err != nil { - return nil, err - } - - locations = append(locations, protocol.Location{ - URI: protocol.URIFromSpanURI(ref.URI()), - Range: decRange, - }) + locations = append(locations, ref.Location()) } return locations, nil @@ -58,14 +50,5 @@ func (s *Server) typeDefinition(ctx context.Context, params *protocol.TypeDefini if ident.Type.Object == nil { return nil, fmt.Errorf("no type definition for %s", ident.Name) } - identRange, err := ident.Type.MappedRange.Range() - if err != nil { - return nil, err - } - return []protocol.Location{ - { - URI: protocol.URIFromSpanURI(ident.Type.MappedRange.URI()), - Range: identRange, - }, - }, nil + return []protocol.Location{ident.Type.MappedRange.Location()}, nil } diff --git a/gopls/internal/lsp/folding_range.go b/gopls/internal/lsp/folding_range.go index 86469d37bd9..3a29ce9927b 100644 --- a/gopls/internal/lsp/folding_range.go +++ b/gopls/internal/lsp/folding_range.go @@ -28,10 +28,7 @@ func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRange func toProtocolFoldingRanges(ranges []*source.FoldingRangeInfo) ([]protocol.FoldingRange, error) { result := make([]protocol.FoldingRange, 0, len(ranges)) for _, info := range ranges { - rng, err := info.MappedRange.Range() - if err != nil { - return nil, err - } + rng := info.MappedRange.Range() result = append(result, protocol.FoldingRange{ StartLine: rng.Start.Line, StartCharacter: rng.Start.Character, diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go index 17bc73f5e49..2a9df8a2f81 100644 --- a/gopls/internal/lsp/protocol/mapper.go +++ b/gopls/internal/lsp/protocol/mapper.go @@ -8,47 +8,59 @@ package protocol // ([]byte) and provides efficient conversion between every kind of // position representation. // -// Here's a handy guide for your tour of the location zoo: +// gopls uses four main representations of position: // -// Imports: protocol --> span --> token +// 1. byte offsets, e.g. (start, end int), starting from zero. // -// protocol: for the LSP protocol. -// protocol.Mapper = (URI, Content). Does all offset <=> column conversions. -// protocol.MappedRange = (protocol.Mapper, {start,end} int) -// protocol.Location = (URI, protocol.Range) -// protocol.Range = (start, end Position) -// protocol.Position = (line, char uint32) 0-based UTF-16 +// 2. go/token notation. Use these types when interacting directly +// with the go/* syntax packages: // -// span: for optional fields; useful for CLIs and tests without access to file contents. -// span.Point = (line?, col?, offset?) 1-based UTF-8 -// span.Span = (uri URI, start, end span.Point) +// token.Pos +// token.FileSet +// token.File +// safetoken.Range = (file token.File, start, end token.Pos) // -// token: for interaction with the go/* syntax packages: -// safetoken.Range = (file token.File, start, end token.Pos) -// token.Pos -// token.FileSet -// token.File -// offset int -// (see also safetoken) +// Because File.Offset and File.Pos panic on invalid inputs, +// we do not call them directly and instead use the safetoken package +// for these conversions. This is enforced by a static check. // -// TODO(adonovan): simplify this picture: -// - Eliminate most/all uses of safetoken.Range in gopls, as -// without a Mapper it's not really self-contained. -// It is mostly used by completion. Given access to complete.mapper, -// it could use a pair of byte offsets instead. -// - Mapper.OffsetPoint and .PointPosition aren't used outside this package. -// OffsetSpan is barely used, and its user would better off with a MappedRange -// or protocol.Range. The span package data types are mostly used in tests -// and in argument parsing (without access to file content). -// - move Mapper to mapper.go. +// Beware also that the methods of token.File have two bugs for which +// safetoken contain workarounds: +// - #57490, whereby the parser may create ast.Nodes during error +// recovery whose computed positions are out of bounds (EOF+1). +// - #41029, whereby the wrong line number is returned for the EOF position. // -// TODO(adonovan): also, write an overview of the position landscape -// in the Mapper doc comment, mentioning the various subtleties, -// the EOF+1 bug (#57490), the \n-at-EOF bug (#41029), the workarounds -// for both bugs in both safetoken and Mapper. Also mention that -// export data doesn't currently preserve accurate column or offset -// information: both are set to garbage based on the assumption of a -// "rectangular" file. +// 3. the span package. +// +// span.Point = (line, col8, offset). +// span.Span = (uri URI, start, end span.Point) +// +// Line and column are 1-based. +// Columns are measured in bytes (UTF-8 codes). +// All fields are optional. +// +// These types are useful as intermediate conversions of validated +// ranges (though MappedRange is superior as it is self contained +// and universally convertible). Since their fields are optional +// they are also useful for parsing user-provided positions (e.g. in +// the CLI) before we have access to file contents. +// +// 4. protocol, the LSP wire format. +// +// protocol.Position = (Line, Character uint32) +// protocol.Range = (start, end Position) +// protocol.Location = (URI, protocol.Range) +// +// Line and Character are 0-based. +// Characters (columns) are measured in UTF-16 codes. +// +// protocol.Mapper holds the (URI, Content) of a file, enabling +// efficient mapping between byte offsets, span ranges, and +// protocol ranges. +// +// protocol.MappedRange holds a protocol.Mapper and valid (start, +// end int) byte offsets, enabling infallible, efficient conversion +// to any other format. import ( "bytes" @@ -67,16 +79,13 @@ import ( ) // A Mapper wraps the content of a file and provides mapping -// from byte offsets to and from other notations of position: +// between byte offsets and notations of position such as: // -// - (line, col8) pairs, where col8 is a 1-based UTF-8 column number (bytes), -// as used by the go/token and span packages. +// - (line, col8) pairs, where col8 is a 1-based UTF-8 column number +// (bytes), as used by the go/token and span packages. // -// - (line, col16) pairs, where col16 is a 1-based UTF-16 column number, -// as used by the LSP protocol; -// -// - (line, colRune) pairs, where colRune is a rune index, as used by ParseWork. -// (Not yet implemented, but could easily be.) +// - (line, col16) pairs, where col16 is a 1-based UTF-16 column +// number, as used by the LSP protocol. // // All conversion methods are named "FromTo", where From and To are the two types. // For example, the PointPosition method converts from a Point to a Position. @@ -85,6 +94,8 @@ import ( // representations. Use safetoken to map between token.Pos <=> byte // offsets, or the convenience methods such as PosPosition, // NodePosition, or NodeRange. +// +// See overview comments at top of this file. type Mapper struct { URI span.URI Content []byte @@ -223,6 +234,10 @@ func (m *Mapper) OffsetPosition(offset int) (Position, error) { if !(0 <= offset && offset <= len(m.Content)) { return Position{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) } + // No error may be returned after this point, + // even if the offset does not fall at a rune boundary. + // (See panic in MappedRange.Range reachable.) + line, col16 := m.lineCol16(offset) return Position{Line: uint32(line), Character: uint32(col16)}, nil } @@ -287,7 +302,7 @@ func (m *Mapper) OffsetPoint(offset int) (span.Point, error) { return span.NewPoint(line+1, col8+1, offset), nil } -// -- conversions from protocol domain -- +// -- conversions from protocol (UTF-16) domain -- // LocationSpan converts a protocol (UTF-16) Location to a (UTF-8) span. // Precondition: the URIs of Location and Mapper match. @@ -446,17 +461,33 @@ func (mr MappedRange) URI() span.URI { return mr.Mapper.URI } -// TODO(adonovan): the Range and Span methods of a properly -// constructed MappedRange cannot fail. Change them to panic instead -// of returning the error, for convenience of the callers. -// This means we can also add a String() method! +// Range returns the range in protocol (UTF-16) form. +func (mr MappedRange) Range() Range { + rng, err := mr.Mapper.OffsetRange(mr.start, mr.end) + if err != nil { + panic(err) // can't happen + } + return rng +} -// Range returns the range in protocol form. -func (mr MappedRange) Range() (Range, error) { - return mr.Mapper.OffsetRange(mr.start, mr.end) +// Location returns the range in protocol location (UTF-16) form. +func (mr MappedRange) Location() Location { + return Location{ + URI: URIFromSpanURI(mr.URI()), + Range: mr.Range(), + } +} + +// Span returns the range in span (UTF-8) form. +func (mr MappedRange) Span() span.Span { + spn, err := mr.Mapper.OffsetSpan(mr.start, mr.end) + if err != nil { + panic(err) // can't happen + } + return spn } -// Span returns the range in span form. -func (mr MappedRange) Span() (span.Span, error) { - return mr.Mapper.OffsetSpan(mr.start, mr.end) +// String formats the range in span (UTF-8) notation. +func (mr MappedRange) String() string { + return fmt.Sprint(mr.Span()) } diff --git a/gopls/internal/lsp/safetoken/range.go b/gopls/internal/lsp/safetoken/range.go index 827a2572785..f44b4499dfc 100644 --- a/gopls/internal/lsp/safetoken/range.go +++ b/gopls/internal/lsp/safetoken/range.go @@ -12,6 +12,11 @@ import "go/token" // so that it is capable of returning (file, line, col8) information. // However it cannot be converted to protocol (UTF-16) form // without access to file content; to do that, use a protocol.ContentMapper. +// +// TODO(adonovan): Eliminate most/all uses of Range in gopls, as +// without a Mapper it's not really self-contained. It is mostly used +// by completion. Given access to complete.mapper, it could use a pair +// of byte offsets instead. type Range struct { TokFile *token.File // non-nil Start, End token.Pos // both IsValid() diff --git a/gopls/internal/lsp/selection_range.go b/gopls/internal/lsp/selection_range.go index 632b458ab31..5b3fd31e9f1 100644 --- a/gopls/internal/lsp/selection_range.go +++ b/gopls/internal/lsp/selection_range.go @@ -41,7 +41,7 @@ func (s *Server) selectionRange(ctx context.Context, params *protocol.SelectionR result := make([]protocol.SelectionRange, len(params.Positions)) for i, protocolPos := range params.Positions { - pos, err := pgf.Pos(protocolPos) + pos, err := pgf.PositionPos(protocolPos) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index b62b6a531ac..efe5f9a8281 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -46,10 +46,7 @@ func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, return nil, nil } declMappedRange := identifier.Declaration.MappedRange[0] - rng, err := declMappedRange.Range() - if err != nil { - return nil, err - } + rng := declMappedRange.Range() callHierarchyItem := protocol.CallHierarchyItem{ Name: identifier.Name, @@ -92,11 +89,7 @@ func toProtocolIncomingCalls(ctx context.Context, snapshot Snapshot, refs []*Ref // once in the result but highlight all calls using FromRanges (ranges at which the calls occur) var incomingCalls = map[protocol.Location]*protocol.CallHierarchyIncomingCall{} for _, ref := range refs { - refRange, err := ref.MappedRange.Range() - if err != nil { - return nil, err - } - + refRange := ref.MappedRange.Range() callItem, err := enclosingNodeCallItem(snapshot, ref.pkg, ref.MappedRange.URI(), ref.ident.NamePos) if err != nil { event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name)) @@ -284,10 +277,7 @@ func toProtocolOutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHand continue } declMappedRange := identifier.Declaration.MappedRange[0] - rng, err := declMappedRange.Range() - if err != nil { - return nil, err - } + rng := declMappedRange.Range() outgoingCalls[key{identifier.Declaration.node, identifier.Name}] = &protocol.CallHierarchyOutgoingCall{ To: protocol.CallHierarchyItem{ diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index a19b07605c3..92ef40533c0 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -444,7 +444,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan } return items, surrounding, nil } - pos, err := pgf.Pos(protoPos) + pos, err := pgf.PositionPos(protoPos) if err != nil { return nil, nil, err } diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index 5144e089b5b..b8fab6a4436 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -19,6 +19,7 @@ type SuggestedFix struct { } type RelatedInformation struct { + // TOOD(adonovan): replace these two fields by a protocol.Location. URI span.URI Range protocol.Range Message string diff --git a/gopls/internal/lsp/source/folding_range.go b/gopls/internal/lsp/source/folding_range.go index 84c33af4843..41f7b5bf5e3 100644 --- a/gopls/internal/lsp/source/folding_range.go +++ b/gopls/internal/lsp/source/folding_range.go @@ -56,8 +56,8 @@ func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFol ast.Inspect(pgf.File, visit) sort.Slice(ranges, func(i, j int) bool { - irng, _ := ranges[i].MappedRange.Range() - jrng, _ := ranges[j].MappedRange.Range() + irng := ranges[i].MappedRange.Range() + jrng := ranges[j].MappedRange.Range() return protocol.CompareRange(irng, jrng) < 0 }) diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index 9bd90e5b05e..4fe3ae8b3bb 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -28,7 +28,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p return nil, fmt.Errorf("getting package for Highlight: %w", err) } - pos, err := pgf.Pos(position) + pos, err := pgf.PositionPos(position) if err != nil { return nil, err } @@ -58,11 +58,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p if err != nil { return nil, err } - pRng, err := mRng.Range() - if err != nil { - return nil, err - } - ranges = append(ranges, pRng) + ranges = append(ranges, mRng.Range()) } return ranges, nil } diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 2cf369bb44a..d023a360f01 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -83,10 +83,6 @@ func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position proto if err != nil { return nil, err } - rng, err := ident.MappedRange.Range() - if err != nil { - return nil, err - } hover, err := FormatHover(h, snapshot.View().Options()) if err != nil { return nil, err @@ -96,7 +92,7 @@ func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position proto Kind: snapshot.View().Options().PreferredContentFormat, Value: hover, }, - Range: rng, + Range: ident.MappedRange.Range(), }, nil } @@ -143,7 +139,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr if err != nil { return 0, protocol.Range{}, err } - pos, err := pgf.Pos(position) + pos, err := pgf.PositionPos(position) if err != nil { return 0, protocol.Range{}, err } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 02c959fbb96..519cc9a4712 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -84,7 +84,7 @@ func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, position if err != nil { return nil, err } - pos, err := pgf.Pos(position) + pos, err := pgf.PositionPos(position) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 83884188f05..1c80096bc25 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -36,14 +36,7 @@ func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp pro if err != nil { return nil, err } - pr, err := rng.Range() - if err != nil { - return nil, err - } - locations = append(locations, protocol.Location{ - URI: protocol.URIFromSpanURI(rng.URI()), - Range: pr, - }) + locations = append(locations, rng.Location()) } sort.Slice(locations, func(i, j int) bool { li, lj := locations[i], locations[j] diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index a3b3abf3b00..25d4a99cafe 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -149,7 +149,7 @@ func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, } // Careful: because we used ParseHeader, // pgf.Pos(ppos) may be beyond EOF => (0, err). - pos, _ := pgf.Pos(ppos) + pos, _ := pgf.PositionPos(ppos) return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil } @@ -275,10 +275,7 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i if includeInterfaceRefs && !isType { // TODO(adonovan): opt: don't go back into the position domain: // we have complete type information already. - declRange, err := declIdent.MappedRange.Range() - if err != nil { - return nil, err - } + declRange := declIdent.MappedRange.Range() fh, err := snapshot.GetFile(ctx, declIdent.MappedRange.URI()) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index 80d6fbf5e17..f24c07f1bc3 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -59,15 +59,7 @@ func References(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protoc } var locations []protocol.Location for _, ref := range references { - // TODO(adonovan): add MappedRange.Location() helper. - refRange, err := ref.MappedRange.Range() - if err != nil { - return nil, err - } - locations = append(locations, protocol.Location{ - URI: protocol.URIFromSpanURI(ref.MappedRange.URI()), - Range: refRange, - }) + locations = append(locations, ref.MappedRange.Location()) } return locations, nil } @@ -239,7 +231,7 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp } // Find the selected object (declaration or reference). - pos, err := pgf.Pos(pp) + pos, err := pgf.PositionPos(pp) if err != nil { return nil, err } @@ -484,13 +476,9 @@ func globalReferences(ctx context.Context, snapshot Snapshot, m *Metadata, pkgPa // mustLocation reports the location interval a syntax node, // which must belong to m.File! Safe for use only by references2. func mustLocation(pgf *ParsedGoFile, n ast.Node) protocol.Location { - // TODO(adonovan): add pgf.PosLocation helper. - refRange, err := pgf.PosRange(n.Pos(), n.End()) + loc, err := pgf.PosLocation(n.Pos(), n.End()) if err != nil { panic(err) // can't happen in references2 } - return protocol.Location{ - URI: protocol.URIFromSpanURI(pgf.Mapper.URI), - Range: refRange, - } + return loc } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index e07127938d3..fe2439cc8fe 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -137,10 +137,7 @@ func computePrepareRenameResp(snapshot Snapshot, pkg Package, node ast.Node, tex if err != nil { return nil, err } - rng, err := mr.Range() - if err != nil { - return nil, err - } + rng := mr.Range() if _, isImport := node.(*ast.ImportSpec); isImport { // We're not really renaming the import path. rng.End = rng.Start @@ -666,10 +663,7 @@ func (r *renamer) update() (map[span.URI][]diff.Edit, error) { return nil, err } for _, ref := range r.refs { - refSpan, err := ref.MappedRange.Span() - if err != nil { - return nil, err - } + refSpan := ref.MappedRange.Span() if seen[refSpan] { continue } diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index a751b291051..3f81c27b053 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -24,7 +24,7 @@ func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, positi if err != nil { return nil, 0, fmt.Errorf("getting file for SignatureHelp: %w", err) } - pos, err := pgf.Pos(position) + pos, err := pgf.PositionPos(position) if err != nil { return nil, 0, err } diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 520900e4456..02625256be0 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -431,14 +431,8 @@ func nonOverlappingRanges(t *testing.T, ranges []*source.FoldingRangeInfo) (res } func conflict(t *testing.T, a, b *source.FoldingRangeInfo) bool { - arng, err := a.MappedRange.Range() - if err != nil { - t.Fatal(err) - } - brng, err := b.MappedRange.Range() - if err != nil { - t.Fatal(err) - } + arng := a.MappedRange.Range() + brng := b.MappedRange.Range() // a start position is <= b start positions return protocol.ComparePosition(arng.Start, brng.Start) <= 0 && protocol.ComparePosition(arng.End, brng.Start) > 0 } @@ -450,10 +444,7 @@ func foldRanges(contents string, ranges []*source.FoldingRangeInfo) (string, err // to preserve the offsets. for i := len(ranges) - 1; i >= 0; i-- { fRange := ranges[i] - spn, err := fRange.MappedRange.Span() - if err != nil { - return "", err - } + spn := fRange.MappedRange.Span() start := spn.Start().Offset() end := spn.End().Offset() @@ -547,15 +538,9 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if err != nil { t.Fatal(err) } - rng, err := ident.Declaration.MappedRange[0].Range() - if err != nil { - t.Fatal(err) - } + rng := ident.Declaration.MappedRange[0].Range() if d.IsType { - rng, err = ident.Type.MappedRange.Range() - if err != nil { - t.Fatal(err) - } + rng = ident.Type.MappedRange.Range() hover = "" } didSomething := false diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 4ed89e4be01..f6a9549bffc 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -384,9 +384,8 @@ type ParsedGoFile struct { // -- go/token domain convenience helpers -- -// Pos returns the token.Pos of protocol position p within the file. -// TODO(adonovan): rename to PositionPos. -func (pgf *ParsedGoFile) Pos(p protocol.Position) (token.Pos, error) { +// PositionPos returns the token.Pos of protocol position p within the file. +func (pgf *ParsedGoFile) PositionPos(p protocol.Position) (token.Pos, error) { offset, err := pgf.Mapper.PositionOffset(p) if err != nil { return token.NoPos, err @@ -399,6 +398,11 @@ func (pgf *ParsedGoFile) PosRange(start, end token.Pos) (protocol.Range, error) return pgf.Mapper.PosRange(pgf.Tok, start, end) } +// PosLocation returns a protocol Location for the token.Pos interval in this file. +func (pgf *ParsedGoFile) PosLocation(start, end token.Pos) (protocol.Location, error) { + return pgf.Mapper.PosLocation(pgf.Tok, start, end) +} + // NodeRange returns a protocol Range for the ast.Node interval in this file. func (pgf *ParsedGoFile) NodeRange(node ast.Node) (protocol.Range, error) { return pgf.Mapper.NodeRange(pgf.Tok, node) diff --git a/gopls/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go index 06b7568094d..eb644c0a007 100644 --- a/gopls/internal/lsp/template/parse.go +++ b/gopls/internal/lsp/template/parse.go @@ -269,6 +269,8 @@ func (p *Parsed) Tokens() []Token { return p.tokens } +// TODO(adonovan): the next 100 lines could perhaps replaced by use of protocol.Mapper. + func (p *Parsed) utf16len(buf []byte) int { cnt := 0 if !p.nonASCII { diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go index 442c3216982..69e89ae123a 100644 --- a/gopls/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -28,6 +28,8 @@ import ( // Use protocol.Mapper to convert between Span and other // representations, such as go/token (also UTF-8) or the LSP protocol // (UTF-16). The latter requires access to file contents. +// +// See overview comments at ../lsp/protocol/mapper.go. type Span struct { v span } From 43158af4a2dca1e2f8f2d3ef9e8998aabb463108 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Thu, 17 Nov 2022 10:28:13 -0500 Subject: [PATCH 603/723] cmd/cover: carve out deprecated command into its own module Since cmd/cover imports x/tools/cover (which is not itself deprecated), its go.mod file needs to require a version of x/tools that includes cover but not cmd/cover. Arbitrarily choose x/tools v0.4.0 as the likely next x/tools release tag and slot it in using a 'replace' directive. For golang/go#56783. Change-Id: I26a5829d0421724ab14437955a8cec4acbfd62b0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/451595 Run-TryBot: Bryan Mills gopls-CI: kokoro Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot Reviewed-by: Than McIntosh --- cmd/cover/go.mod | 10 ++++++++++ cmd/cover/go.sum | 2 ++ 2 files changed, 12 insertions(+) create mode 100644 cmd/cover/go.mod create mode 100644 cmd/cover/go.sum diff --git a/cmd/cover/go.mod b/cmd/cover/go.mod new file mode 100644 index 00000000000..5c7280a2dfd --- /dev/null +++ b/cmd/cover/go.mod @@ -0,0 +1,10 @@ +module golang.org/x/tools/cmd/cover + +go 1.18 + +replace golang.org/x/tools v0.4.0 => ../.. + +require ( + golang.org/x/sys v0.2.0 + golang.org/x/tools v0.4.0 +) diff --git a/cmd/cover/go.sum b/cmd/cover/go.sum new file mode 100644 index 00000000000..beac707cffb --- /dev/null +++ b/cmd/cover/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 227ee72c945f973fc3cf00cc3dcde7b1ac660d1b Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 10 Jan 2023 15:55:44 -0500 Subject: [PATCH 604/723] internal/regtest/misc: fail eagerly in TestRenameFileFromEditor Updates golang/go#55324 Change-Id: I07cb5465afc7f9e76cad96cb5fbe55e6e16a73b7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461416 Reviewed-by: Bryan Mills TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/internal/regtest/misc/rename_test.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index cb7708b527d..ebbe5d465f7 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -413,12 +413,9 @@ package b // Moving x.go should make the diagnostic go away. env.RenameFile("a/x.go", "b/x.go") - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - EmptyDiagnostics("a/a.go"), // no more duplicate declarations - env.DiagnosticAtRegexp("b/b.go", "package"), // as package names mismatch - ), + env.AfterChange( + EmptyDiagnostics("a/a.go"), // no more duplicate declarations + env.DiagnosticAtRegexp("b/b.go", "package"), // as package names mismatch ) // Renaming should also work on open buffers. @@ -426,17 +423,14 @@ package b // Moving x.go back to a/ should cause the diagnostics to reappear. env.RenameFile("b/x.go", "a/x.go") - // TODO(rfindley): enable using a OnceMet precondition here. We can't - // currently do this because DidClose, DidOpen and DidChangeWatchedFiles - // are sent, and it is not easy to use all as a precondition. - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("a/a.go", "X"), env.DiagnosticAtRegexp("a/x.go", "X"), ) // Renaming the entire directory should move both the open and closed file. env.RenameFile("a", "x") - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("x/a.go", "X"), env.DiagnosticAtRegexp("x/x.go", "X"), ) From 4305a2291f4827df4bb96a4bd3f68d847ccbf1f4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 10 Jan 2023 16:32:03 -0500 Subject: [PATCH 605/723] gopls/internal/lsp/cache: don't cache files if mtime is too recent Following the pattern used by the Go command, don't cache file handles if their modification time is too recent to be reliable. It looks like this is causing regtest failures in the s390x builders when edits do not affect the file size. Remove the file size check as it is a flawed heuristic, should no longer be necessary, and can only lead to confusion. Fixes golang/go#55070 Fixes golang/go#55324 Change-Id: I585445fcacf36923d7d9c9d6ff480519e4362c71 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461418 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Bryan Mills Reviewed-by: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/cache/cache.go | 38 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go index 46b2e26a331..23a96118738 100644 --- a/gopls/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -72,12 +72,6 @@ type fileHandle struct { bytes []byte hash source.Hash err error - - // size is the file length as reported by Stat, for the purpose of - // invalidation. Probably we could just use len(bytes), but this is done - // defensively in case the definition of file size in the file system - // differs. - size int64 } func (h *fileHandle) Saved() bool { @@ -98,22 +92,22 @@ func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error) }, nil } + // We check if the file has changed by comparing modification times. Notably, + // this is an imperfect heuristic as various systems have low resolution + // mtimes (as much as 1s on WSL or s390x builders), so we only cache + // filehandles if mtime is old enough to be reliable, meaning that we don't + // expect a subsequent write to have the same mtime. + // + // The coarsest mtime precision we've seen in practice is 1s, so consider + // mtime to be unreliable if it is less than 2s old. Capture this before + // doing anything else. + recentlyModified := time.Since(fi.ModTime()) < 2*time.Second + c.fileMu.Lock() fh, ok := c.fileContent[uri] c.fileMu.Unlock() - // Check mtime and file size to infer whether the file has changed. This is - // an imperfect heuristic. Notably on some real systems (such as WSL) the - // filesystem clock resolution can be large -- 1/64s was observed. Therefore - // it's quite possible for multiple file modifications to occur within a - // single logical 'tick'. This can leave the cache in an incorrect state, but - // unfortunately we can't afford to pay the price of reading the actual file - // content here. Or to be more precise, reading would be a risky change and - // we don't know if we can afford it. - // - // We check file size in an attempt to reduce the probability of false cache - // hits. - if ok && fh.modTime.Equal(fi.ModTime()) && fh.size == fi.Size() { + if ok && fh.modTime.Equal(fi.ModTime()) { return fh, nil } @@ -122,7 +116,11 @@ func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error) return nil, err } c.fileMu.Lock() - c.fileContent[uri] = fh + if !recentlyModified { + c.fileContent[uri] = fh + } else { + delete(c.fileContent, uri) + } c.fileMu.Unlock() return fh, nil } @@ -146,13 +144,11 @@ func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, e if err != nil { return &fileHandle{ modTime: fi.ModTime(), - size: fi.Size(), err: err, }, nil } return &fileHandle{ modTime: fi.ModTime(), - size: fi.Size(), uri: uri, bytes: data, hash: source.HashOf(data), From e345d46ec2f580866c55f9a710b06b8083e6722e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 11 Jan 2023 10:29:45 -0500 Subject: [PATCH 606/723] internal/gcimporter: fix export of invalid methods The declaration "func () F()" used to cause the exporter to fail. This change now exports it as "func F()" which is how the type checker (mostly) repairs it. Fixes golang/go#57729 Change-Id: I80958a75a57800ff8afc35e95e0c8e98191aac97 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461575 gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/analysis.go | 3 ++ internal/gcimporter/gcimporter_test.go | 55 ++++++++++++++++++++++++++ internal/gcimporter/iexport.go | 8 +++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index d48d298654b..e9f1c7f7ce9 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -841,6 +841,9 @@ func typeCheckForAnalysis(fset *token.FileSet, parsed []*source.ParsedGoFile, m // Emit the export data and compute the deep hash. export, err := gcimporter.IExportShallow(pkg.fset, pkg.types) if err != nil { + // TODO(adonovan): in light of exporter bugs such as #57729, + // consider using bug.Report here and retrying the IExportShallow + // call here using an empty types.Package. log.Fatalf("internal error writing shallow export data: %v", err) } pkg.export = export diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index 0b9f07dd1b8..afc54475cec 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -808,6 +808,61 @@ func TestIssue57015(t *testing.T) { compileAndImportPkg(t, "issue57015") } +// This is a regression test for a failure to export a package +// containing a specific type error. +// +// Though the issue and test are specific, they may be representatives +// of class of exporter bugs on ill-typed code that we have yet to +// flush out. +// +// TODO(adonovan): systematize our search for similar problems using +// fuzz testing, and drive this test from a table of test cases +// discovered by fuzzing. +func TestIssue57729(t *testing.T) { + // The lack of a receiver causes Recv.Type=Invalid. + // (The type checker then treats Foo as a package-level + // function, inserting it into the package scope.) + // The exporter needs to apply the same treatment. + const src = `package p; func () Foo() {}` + + // Parse the ill-typed input. + fset := token.NewFileSet() + f, err := goparser.ParseFile(fset, "p.go", src, 0) + if err != nil { + t.Fatalf("parse: %v", err) + } + + // Type check it, expecting errors. + config := &types.Config{ + Error: func(err error) { t.Log(err) }, // don't abort at first error + } + pkg1, _ := config.Check("p", fset, []*ast.File{f}, nil) + + // Export it. + // (Shallowness isn't important here.) + data, err := IExportShallow(fset, pkg1) + if err != nil { + t.Fatalf("export: %v", err) // any failure to export is a bug + } + + // Re-import it. + imports := make(map[string]*types.Package) + insert := func(pkg1 *types.Package, name string) { panic("unexpected insert") } + pkg2, err := IImportShallow(fset, imports, data, "p", insert) + if err != nil { + t.Fatalf("import: %v", err) // any failure of IExport+IImport is a bug. + } + + // Check that Lookup("Foo") still returns something. + // We can't assert the type hasn't change: it has, + // from a method of Invalid to a standalone function. + hasObj1 := pkg1.Scope().Lookup("Foo") != nil + hasObj2 := pkg2.Scope().Lookup("Foo") != nil + if hasObj1 != hasObj2 { + t.Errorf("export+import changed Lookup('Foo')!=nil: was %t, became %t", hasObj1, hasObj2) + } +} + // apkg returns the package "a" prefixed by (as a package) testoutdir func apkg(testoutdir string) string { apkg := testoutdir + "/a" diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 7d90f00f323..61e1dd2ad6a 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -346,7 +346,13 @@ func (p *iexporter) doDecl(obj types.Object) { case *types.Func: sig, _ := obj.Type().(*types.Signature) if sig.Recv() != nil { - panic(internalErrorf("unexpected method: %v", sig)) + // We shouldn't see methods in the package scope, + // but the type checker may repair "func () F() {}" + // to "func (Invalid) F()" and then treat it like "func F()", + // so allow that. See golang/go#57729. + if sig.Recv().Type() != types.Typ[types.Invalid] { + panic(internalErrorf("unexpected method: %v", sig)) + } } // Function. From f9a10c01c936702f09d5f3dd51db8101c30e9369 Mon Sep 17 00:00:00 2001 From: Bryan Mills Date: Wed, 11 Jan 2023 21:15:54 +0000 Subject: [PATCH 607/723] Revert "cmd/cover: carve out deprecated command into its own module" This reverts CL 451595. Reason for revert: ended up at a different x/sys commit due to merge conflict. Change-Id: I064170e21dfbaa9bbaf6cf6701994824ba55f2ed Reviewed-on: https://go-review.googlesource.com/c/tools/+/461616 Reviewed-by: Than McIntosh gopls-CI: kokoro Run-TryBot: Bryan Mills Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot --- cmd/cover/go.mod | 10 ---------- cmd/cover/go.sum | 2 -- 2 files changed, 12 deletions(-) delete mode 100644 cmd/cover/go.mod delete mode 100644 cmd/cover/go.sum diff --git a/cmd/cover/go.mod b/cmd/cover/go.mod deleted file mode 100644 index 5c7280a2dfd..00000000000 --- a/cmd/cover/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module golang.org/x/tools/cmd/cover - -go 1.18 - -replace golang.org/x/tools v0.4.0 => ../.. - -require ( - golang.org/x/sys v0.2.0 - golang.org/x/tools v0.4.0 -) diff --git a/cmd/cover/go.sum b/cmd/cover/go.sum deleted file mode 100644 index beac707cffb..00000000000 --- a/cmd/cover/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From a7f7db3f17fc06ad54f9ef6cd388f4f3f8918ae8 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Wed, 11 Jan 2023 16:10:46 -0500 Subject: [PATCH 608/723] cmd/cover: carve out deprecated command into its own module Since cmd/cover imports x/tools/cover (which is not itself deprecated), its go.mod file needs to require a version of x/tools that includes cover but not cmd/cover. Arbitrarily choose x/tools v0.6.0 as the likely next x/tools release tag and slot it in using a 'replace' directive. In a followup commit, I will remove the replace directive and update the x/tools dependency to the pseudo-version of the exact commit of this CL. For golang/go#56783. Change-Id: Ia70206cb8c4e6128b90a048aad4e2607db19740b Reviewed-on: https://go-review.googlesource.com/c/tools/+/461655 gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Bryan Mills Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot --- cmd/cover/go.mod | 10 ++++++++++ cmd/cover/go.sum | 2 ++ 2 files changed, 12 insertions(+) create mode 100644 cmd/cover/go.mod create mode 100644 cmd/cover/go.sum diff --git a/cmd/cover/go.mod b/cmd/cover/go.mod new file mode 100644 index 00000000000..2b91ff1b7b0 --- /dev/null +++ b/cmd/cover/go.mod @@ -0,0 +1,10 @@ +module golang.org/x/tools/cmd/cover + +go 1.18 + +replace golang.org/x/tools v0.6.0 => ../.. + +require ( + golang.org/x/sys v0.4.0 + golang.org/x/tools v0.6.0 +) diff --git a/cmd/cover/go.sum b/cmd/cover/go.sum new file mode 100644 index 00000000000..c2a6782ea03 --- /dev/null +++ b/cmd/cover/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 77655679d852705b3755d32b97c593436cbfd423 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 11 Jan 2023 15:55:00 -0500 Subject: [PATCH 609/723] gopls/internal/lsp/source: minor clarifications With this change I originally intended to add a protocol.Mapper to FileHandle, since it seems more closely associated with file reading than with Go parsing (where it is now, in ParsedGoFile). This would have avoided a number of places that construct them (and would amortize the cost). But this turned out to be a bad idea because ParsedGoFile's Mapper actually maps the fixed-up source tree, not the raw content of the file, which led to inconsistency. So I backed out the change and instead deleted the TODOs and documented the situation. Also, a logic tweak: set the URI and Hash fields of FileHandle correctly even for non-existent files. Change-Id: I744b917fc9c1be77e1f793948e662138f1cd4d16 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461579 Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/cache.go | 17 +++++++---------- gopls/internal/lsp/source/fix.go | 1 - gopls/internal/lsp/source/view.go | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go index 23a96118738..88d1b435bdf 100644 --- a/gopls/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -11,7 +11,6 @@ import ( "go/token" "go/types" "html/template" - "io/ioutil" "os" "reflect" "sort" @@ -69,7 +68,7 @@ type Cache struct { type fileHandle struct { modTime time.Time uri span.URI - bytes []byte + content []byte hash source.Hash err error } @@ -140,18 +139,16 @@ func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, e _ = ctx defer done() - data, err := ioutil.ReadFile(uri.Filename()) // ~20us + content, err := os.ReadFile(uri.Filename()) // ~20us if err != nil { - return &fileHandle{ - modTime: fi.ModTime(), - err: err, - }, nil + content = nil // just in case } return &fileHandle{ modTime: fi.ModTime(), uri: uri, - bytes: data, - hash: source.HashOf(data), + content: content, + hash: source.HashOf(content), + err: err, }, nil } @@ -187,7 +184,7 @@ func (h *fileHandle) FileIdentity() source.FileIdentity { } func (h *fileHandle) Read() ([]byte, error) { - return h.bytes, h.err + return h.content, h.err } var cacheIndex, sessionIndex, viewIndex int64 diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index b3f09f04314..50f1cb04512 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -110,7 +110,6 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi } editsPerFile[fh.URI()] = te } - // TODO(adonovan): once FileHandle has a Mapper, eliminate this. content, err := fh.Read() if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index f6a9549bffc..118638aabfe 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -378,7 +378,7 @@ type ParsedGoFile struct { // actual content of the file if we have fixed the AST. Src []byte Fixed bool - Mapper *protocol.Mapper + Mapper *protocol.Mapper // may map fixed Src, not file content ParseErr scanner.ErrorList } From 98dcb0ee0638f87be8ead5b6665c4b12b2be7238 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Wed, 11 Jan 2023 16:56:27 -0500 Subject: [PATCH 610/723] cmd/cover: remove replace directive This makes it so that 'go install golang.org/x/tools/cmd/cover@latest' will continue to work. ('go install pkg@version' does not allow replace directives.) For golang/go#56783. Change-Id: I9f899eee36ab4118367b0a3118f701b7b9a5cceb Reviewed-on: https://go-review.googlesource.com/c/tools/+/461656 gopls-CI: kokoro Run-TryBot: Bryan Mills TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim Auto-Submit: Bryan Mills --- cmd/cover/go.mod | 4 +--- cmd/cover/go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/cover/go.mod b/cmd/cover/go.mod index 2b91ff1b7b0..4d1f325bfb9 100644 --- a/cmd/cover/go.mod +++ b/cmd/cover/go.mod @@ -2,9 +2,7 @@ module golang.org/x/tools/cmd/cover go 1.18 -replace golang.org/x/tools v0.6.0 => ../.. - require ( golang.org/x/sys v0.4.0 - golang.org/x/tools v0.6.0 + golang.org/x/tools v0.5.1-0.20230111220935-a7f7db3f17fc ) diff --git a/cmd/cover/go.sum b/cmd/cover/go.sum index c2a6782ea03..c8ab130792d 100644 --- a/cmd/cover/go.sum +++ b/cmd/cover/go.sum @@ -1,2 +1,4 @@ golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.5.1-0.20230111220935-a7f7db3f17fc h1:zRn9MzwG18RZhyanShCfUwJTcobvqw8fOjjROFN9jtM= +golang.org/x/tools v0.5.1-0.20230111220935-a7f7db3f17fc/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= From a3c22fca0a6119bcdede79e270af5fe7d9aacf0b Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Thu, 17 Nov 2022 10:47:30 -0500 Subject: [PATCH 611/723] cmd/cover: delete package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CL 461656 places the package in its own module. Once that commit is tagged as 'cmd/cover/v…', 'go install golang.org/x/tools/cmd/cover@latest' will be able to resolve the command from that module (to cover the unlikely event that anyone still builds and uses the tool). Fixes golang/go#56783. Change-Id: I4baa5fcc4b5273fad4cc7bf150e411f2776b12d2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/451596 TryBot-Bypass: Bryan Mills Auto-Submit: Bryan Mills gopls-CI: kokoro Run-TryBot: Bryan Mills Reviewed-by: Russ Cox --- cmd/cover/README.md | 3 - cmd/cover/cover.go | 721 ------------------------------------- cmd/cover/cover_test.go | 107 ------ cmd/cover/doc.go | 25 -- cmd/cover/func.go | 166 --------- cmd/cover/go.mod | 8 - cmd/cover/go.sum | 4 - cmd/cover/html.go | 284 --------------- cmd/cover/testdata/main.go | 112 ------ cmd/cover/testdata/test.go | 218 ----------- 10 files changed, 1648 deletions(-) delete mode 100644 cmd/cover/README.md delete mode 100644 cmd/cover/cover.go delete mode 100644 cmd/cover/cover_test.go delete mode 100644 cmd/cover/doc.go delete mode 100644 cmd/cover/func.go delete mode 100644 cmd/cover/go.mod delete mode 100644 cmd/cover/go.sum delete mode 100644 cmd/cover/html.go delete mode 100644 cmd/cover/testdata/main.go delete mode 100644 cmd/cover/testdata/test.go diff --git a/cmd/cover/README.md b/cmd/cover/README.md deleted file mode 100644 index 62e60279a9b..00000000000 --- a/cmd/cover/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Deprecated - -NOTE: For Go releases 1.5 and later, this tool lives in the standard repository. The code here is not maintained. diff --git a/cmd/cover/cover.go b/cmd/cover/cover.go deleted file mode 100644 index 0c7db1025ad..00000000000 --- a/cmd/cover/cover.go +++ /dev/null @@ -1,721 +0,0 @@ -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -import ( - "bytes" - "flag" - "fmt" - "go/ast" - "go/parser" - "go/printer" - "go/token" - "io" - "io/ioutil" - "log" - "os" - "path/filepath" - "sort" - "strconv" - "strings" -) - -const usageMessage = "" + - `Usage of 'go tool cover': -Given a coverage profile produced by 'go test': - go test -coverprofile=c.out - -Open a web browser displaying annotated source code: - go tool cover -html=c.out - -Write out an HTML file instead of launching a web browser: - go tool cover -html=c.out -o coverage.html - -Display coverage percentages to stdout for each function: - go tool cover -func=c.out - -Finally, to generate modified source code with coverage annotations -(what go test -cover does): - go tool cover -mode=set -var=CoverageVariableName program.go -` - -func usage() { - fmt.Fprint(os.Stderr, usageMessage) - fmt.Fprintln(os.Stderr, "\nFlags:") - flag.PrintDefaults() - fmt.Fprintln(os.Stderr, "\n Only one of -html, -func, or -mode may be set.") - os.Exit(2) -} - -var ( - mode = flag.String("mode", "", "coverage mode: set, count, atomic") - varVar = flag.String("var", "GoCover", "name of coverage variable to generate") - output = flag.String("o", "", "file for output; default: stdout") - htmlOut = flag.String("html", "", "generate HTML representation of coverage profile") - funcOut = flag.String("func", "", "output coverage profile information for each function") -) - -var profile string // The profile to read; the value of -html or -func - -var counterStmt func(*File, ast.Expr) ast.Stmt - -const ( - atomicPackagePath = "sync/atomic" - atomicPackageName = "_cover_atomic_" -) - -func main() { - flag.Usage = usage - flag.Parse() - - // Usage information when no arguments. - if flag.NFlag() == 0 && flag.NArg() == 0 { - flag.Usage() - } - - err := parseFlags() - if err != nil { - fmt.Fprintln(os.Stderr, err) - fmt.Fprintln(os.Stderr, `For usage information, run "go tool cover -help"`) - os.Exit(2) - } - - // Generate coverage-annotated source. - if *mode != "" { - annotate(flag.Arg(0)) - return - } - - // Output HTML or function coverage information. - if *htmlOut != "" { - err = htmlOutput(profile, *output) - } else { - err = funcOutput(profile, *output) - } - - if err != nil { - fmt.Fprintf(os.Stderr, "cover: %v\n", err) - os.Exit(2) - } -} - -// parseFlags sets the profile and counterStmt globals and performs validations. -func parseFlags() error { - profile = *htmlOut - if *funcOut != "" { - if profile != "" { - return fmt.Errorf("too many options") - } - profile = *funcOut - } - - // Must either display a profile or rewrite Go source. - if (profile == "") == (*mode == "") { - return fmt.Errorf("too many options") - } - - if *mode != "" { - switch *mode { - case "set": - counterStmt = setCounterStmt - case "count": - counterStmt = incCounterStmt - case "atomic": - counterStmt = atomicCounterStmt - default: - return fmt.Errorf("unknown -mode %v", *mode) - } - - if flag.NArg() == 0 { - return fmt.Errorf("missing source file") - } else if flag.NArg() == 1 { - return nil - } - } else if flag.NArg() == 0 { - return nil - } - return fmt.Errorf("too many arguments") -} - -// Block represents the information about a basic block to be recorded in the analysis. -// Note: Our definition of basic block is based on control structures; we don't break -// apart && and ||. We could but it doesn't seem important enough to bother. -type Block struct { - startByte token.Pos - endByte token.Pos - numStmt int -} - -// File is a wrapper for the state of a file used in the parser. -// The basic parse tree walker is a method of this type. -type File struct { - fset *token.FileSet - name string // Name of file. - astFile *ast.File - blocks []Block - atomicPkg string // Package name for "sync/atomic" in this file. -} - -// Visit implements the ast.Visitor interface. -func (f *File) Visit(node ast.Node) ast.Visitor { - switch n := node.(type) { - case *ast.BlockStmt: - // If it's a switch or select, the body is a list of case clauses; don't tag the block itself. - if len(n.List) > 0 { - switch n.List[0].(type) { - case *ast.CaseClause: // switch - for _, n := range n.List { - clause := n.(*ast.CaseClause) - clause.Body = f.addCounters(clause.Pos(), clause.End(), clause.Body, false) - } - return f - case *ast.CommClause: // select - for _, n := range n.List { - clause := n.(*ast.CommClause) - clause.Body = f.addCounters(clause.Pos(), clause.End(), clause.Body, false) - } - return f - } - } - n.List = f.addCounters(n.Lbrace, n.Rbrace+1, n.List, true) // +1 to step past closing brace. - case *ast.IfStmt: - ast.Walk(f, n.Body) - if n.Else == nil { - return nil - } - // The elses are special, because if we have - // if x { - // } else if y { - // } - // we want to cover the "if y". To do this, we need a place to drop the counter, - // so we add a hidden block: - // if x { - // } else { - // if y { - // } - // } - switch stmt := n.Else.(type) { - case *ast.IfStmt: - block := &ast.BlockStmt{ - Lbrace: n.Body.End(), // Start at end of the "if" block so the covered part looks like it starts at the "else". - List: []ast.Stmt{stmt}, - Rbrace: stmt.End(), - } - n.Else = block - case *ast.BlockStmt: - stmt.Lbrace = n.Body.End() // Start at end of the "if" block so the covered part looks like it starts at the "else". - default: - panic("unexpected node type in if") - } - ast.Walk(f, n.Else) - return nil - case *ast.SelectStmt: - // Don't annotate an empty select - creates a syntax error. - if n.Body == nil || len(n.Body.List) == 0 { - return nil - } - case *ast.SwitchStmt: - // Don't annotate an empty switch - creates a syntax error. - if n.Body == nil || len(n.Body.List) == 0 { - return nil - } - case *ast.TypeSwitchStmt: - // Don't annotate an empty type switch - creates a syntax error. - if n.Body == nil || len(n.Body.List) == 0 { - return nil - } - } - return f -} - -// unquote returns the unquoted string. -func unquote(s string) string { - t, err := strconv.Unquote(s) - if err != nil { - log.Fatalf("cover: improperly quoted string %q\n", s) - } - return t -} - -// addImport adds an import for the specified path, if one does not already exist, and returns -// the local package name. -func (f *File) addImport(path string) string { - // Does the package already import it? - for _, s := range f.astFile.Imports { - if unquote(s.Path.Value) == path { - if s.Name != nil { - return s.Name.Name - } - return filepath.Base(path) - } - } - newImport := &ast.ImportSpec{ - Name: ast.NewIdent(atomicPackageName), - Path: &ast.BasicLit{ - Kind: token.STRING, - Value: fmt.Sprintf("%q", path), - }, - } - impDecl := &ast.GenDecl{ - Tok: token.IMPORT, - Specs: []ast.Spec{ - newImport, - }, - } - // Make the new import the first Decl in the file. - astFile := f.astFile - astFile.Decls = append(astFile.Decls, nil) - copy(astFile.Decls[1:], astFile.Decls[0:]) - astFile.Decls[0] = impDecl - astFile.Imports = append(astFile.Imports, newImport) - - // Now refer to the package, just in case it ends up unused. - // That is, append to the end of the file the declaration - // var _ = _cover_atomic_.AddUint32 - reference := &ast.GenDecl{ - Tok: token.VAR, - Specs: []ast.Spec{ - &ast.ValueSpec{ - Names: []*ast.Ident{ - ast.NewIdent("_"), - }, - Values: []ast.Expr{ - &ast.SelectorExpr{ - X: ast.NewIdent(atomicPackageName), - Sel: ast.NewIdent("AddUint32"), - }, - }, - }, - }, - } - astFile.Decls = append(astFile.Decls, reference) - return atomicPackageName -} - -var slashslash = []byte("//") - -// initialComments returns the prefix of content containing only -// whitespace and line comments. Any +build directives must appear -// within this region. This approach is more reliable than using -// go/printer to print a modified AST containing comments. -func initialComments(content []byte) []byte { - // Derived from go/build.Context.shouldBuild. - end := 0 - p := content - for len(p) > 0 { - line := p - if i := bytes.IndexByte(line, '\n'); i >= 0 { - line, p = line[:i], p[i+1:] - } else { - p = p[len(p):] - } - line = bytes.TrimSpace(line) - if len(line) == 0 { // Blank line. - end = len(content) - len(p) - continue - } - if !bytes.HasPrefix(line, slashslash) { // Not comment line. - break - } - } - return content[:end] -} - -func annotate(name string) { - fset := token.NewFileSet() - content, err := ioutil.ReadFile(name) - if err != nil { - log.Fatalf("cover: %s: %s", name, err) - } - parsedFile, err := parser.ParseFile(fset, name, content, parser.ParseComments) - if err != nil { - log.Fatalf("cover: %s: %s", name, err) - } - parsedFile.Comments = trimComments(parsedFile, fset) - - file := &File{ - fset: fset, - name: name, - astFile: parsedFile, - } - if *mode == "atomic" { - file.atomicPkg = file.addImport(atomicPackagePath) - } - ast.Walk(file, file.astFile) - fd := os.Stdout - if *output != "" { - var err error - fd, err = os.Create(*output) - if err != nil { - log.Fatalf("cover: %s", err) - } - } - fd.Write(initialComments(content)) // Retain '// +build' directives. - file.print(fd) - // After printing the source tree, add some declarations for the counters etc. - // We could do this by adding to the tree, but it's easier just to print the text. - file.addVariables(fd) -} - -// trimComments drops all but the //go: comments, some of which are semantically important. -// We drop all others because they can appear in places that cause our counters -// to appear in syntactically incorrect places. //go: appears at the beginning of -// the line and is syntactically safe. -func trimComments(file *ast.File, fset *token.FileSet) []*ast.CommentGroup { - var comments []*ast.CommentGroup - for _, group := range file.Comments { - var list []*ast.Comment - for _, comment := range group.List { - if strings.HasPrefix(comment.Text, "//go:") && fset.Position(comment.Slash).Column == 1 { - list = append(list, comment) - } - } - if list != nil { - comments = append(comments, &ast.CommentGroup{List: list}) - } - } - return comments -} - -func (f *File) print(w io.Writer) { - printer.Fprint(w, f.fset, f.astFile) -} - -// intLiteral returns an ast.BasicLit representing the integer value. -func (f *File) intLiteral(i int) *ast.BasicLit { - node := &ast.BasicLit{ - Kind: token.INT, - Value: fmt.Sprint(i), - } - return node -} - -// index returns an ast.BasicLit representing the number of counters present. -func (f *File) index() *ast.BasicLit { - return f.intLiteral(len(f.blocks)) -} - -// setCounterStmt returns the expression: __count[23] = 1. -func setCounterStmt(f *File, counter ast.Expr) ast.Stmt { - return &ast.AssignStmt{ - Lhs: []ast.Expr{counter}, - Tok: token.ASSIGN, - Rhs: []ast.Expr{f.intLiteral(1)}, - } -} - -// incCounterStmt returns the expression: __count[23]++. -func incCounterStmt(f *File, counter ast.Expr) ast.Stmt { - return &ast.IncDecStmt{ - X: counter, - Tok: token.INC, - } -} - -// atomicCounterStmt returns the expression: atomic.AddUint32(&__count[23], 1) -func atomicCounterStmt(f *File, counter ast.Expr) ast.Stmt { - return &ast.ExprStmt{ - X: &ast.CallExpr{ - Fun: &ast.SelectorExpr{ - X: ast.NewIdent(f.atomicPkg), - Sel: ast.NewIdent("AddUint32"), - }, - Args: []ast.Expr{&ast.UnaryExpr{ - Op: token.AND, - X: counter, - }, - f.intLiteral(1), - }, - }, - } -} - -// newCounter creates a new counter expression of the appropriate form. -func (f *File) newCounter(start, end token.Pos, numStmt int) ast.Stmt { - counter := &ast.IndexExpr{ - X: &ast.SelectorExpr{ - X: ast.NewIdent(*varVar), - Sel: ast.NewIdent("Count"), - }, - Index: f.index(), - } - stmt := counterStmt(f, counter) - f.blocks = append(f.blocks, Block{start, end, numStmt}) - return stmt -} - -// addCounters takes a list of statements and adds counters to the beginning of -// each basic block at the top level of that list. For instance, given -// -// S1 -// if cond { -// S2 -// } -// S3 -// -// counters will be added before S1 and before S3. The block containing S2 -// will be visited in a separate call. -// TODO: Nested simple blocks get unnecessary (but correct) counters -func (f *File) addCounters(pos, blockEnd token.Pos, list []ast.Stmt, extendToClosingBrace bool) []ast.Stmt { - // Special case: make sure we add a counter to an empty block. Can't do this below - // or we will add a counter to an empty statement list after, say, a return statement. - if len(list) == 0 { - return []ast.Stmt{f.newCounter(pos, blockEnd, 0)} - } - // We have a block (statement list), but it may have several basic blocks due to the - // appearance of statements that affect the flow of control. - var newList []ast.Stmt - for { - // Find first statement that affects flow of control (break, continue, if, etc.). - // It will be the last statement of this basic block. - var last int - end := blockEnd - for last = 0; last < len(list); last++ { - end = f.statementBoundary(list[last]) - if f.endsBasicSourceBlock(list[last]) { - extendToClosingBrace = false // Block is broken up now. - last++ - break - } - } - if extendToClosingBrace { - end = blockEnd - } - if pos != end { // Can have no source to cover if e.g. blocks abut. - newList = append(newList, f.newCounter(pos, end, last)) - } - newList = append(newList, list[0:last]...) - list = list[last:] - if len(list) == 0 { - break - } - pos = list[0].Pos() - } - return newList -} - -// hasFuncLiteral reports the existence and position of the first func literal -// in the node, if any. If a func literal appears, it usually marks the termination -// of a basic block because the function body is itself a block. -// Therefore we draw a line at the start of the body of the first function literal we find. -// TODO: what if there's more than one? Probably doesn't matter much. -func hasFuncLiteral(n ast.Node) (bool, token.Pos) { - if n == nil { - return false, 0 - } - var literal funcLitFinder - ast.Walk(&literal, n) - return literal.found(), token.Pos(literal) -} - -// statementBoundary finds the location in s that terminates the current basic -// block in the source. -func (f *File) statementBoundary(s ast.Stmt) token.Pos { - // Control flow statements are easy. - switch s := s.(type) { - case *ast.BlockStmt: - // Treat blocks like basic blocks to avoid overlapping counters. - return s.Lbrace - case *ast.IfStmt: - found, pos := hasFuncLiteral(s.Init) - if found { - return pos - } - found, pos = hasFuncLiteral(s.Cond) - if found { - return pos - } - return s.Body.Lbrace - case *ast.ForStmt: - found, pos := hasFuncLiteral(s.Init) - if found { - return pos - } - found, pos = hasFuncLiteral(s.Cond) - if found { - return pos - } - found, pos = hasFuncLiteral(s.Post) - if found { - return pos - } - return s.Body.Lbrace - case *ast.LabeledStmt: - return f.statementBoundary(s.Stmt) - case *ast.RangeStmt: - found, pos := hasFuncLiteral(s.X) - if found { - return pos - } - return s.Body.Lbrace - case *ast.SwitchStmt: - found, pos := hasFuncLiteral(s.Init) - if found { - return pos - } - found, pos = hasFuncLiteral(s.Tag) - if found { - return pos - } - return s.Body.Lbrace - case *ast.SelectStmt: - return s.Body.Lbrace - case *ast.TypeSwitchStmt: - found, pos := hasFuncLiteral(s.Init) - if found { - return pos - } - return s.Body.Lbrace - } - // If not a control flow statement, it is a declaration, expression, call, etc. and it may have a function literal. - // If it does, that's tricky because we want to exclude the body of the function from this block. - // Draw a line at the start of the body of the first function literal we find. - // TODO: what if there's more than one? Probably doesn't matter much. - found, pos := hasFuncLiteral(s) - if found { - return pos - } - return s.End() -} - -// endsBasicSourceBlock reports whether s changes the flow of control: break, if, etc., -// or if it's just problematic, for instance contains a function literal, which will complicate -// accounting due to the block-within-an expression. -func (f *File) endsBasicSourceBlock(s ast.Stmt) bool { - switch s := s.(type) { - case *ast.BlockStmt: - // Treat blocks like basic blocks to avoid overlapping counters. - return true - case *ast.BranchStmt: - return true - case *ast.ForStmt: - return true - case *ast.IfStmt: - return true - case *ast.LabeledStmt: - return f.endsBasicSourceBlock(s.Stmt) - case *ast.RangeStmt: - return true - case *ast.SwitchStmt: - return true - case *ast.SelectStmt: - return true - case *ast.TypeSwitchStmt: - return true - case *ast.ExprStmt: - // Calls to panic change the flow. - // We really should verify that "panic" is the predefined function, - // but without type checking we can't and the likelihood of it being - // an actual problem is vanishingly small. - if call, ok := s.X.(*ast.CallExpr); ok { - if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "panic" && len(call.Args) == 1 { - return true - } - } - } - found, _ := hasFuncLiteral(s) - return found -} - -// funcLitFinder implements the ast.Visitor pattern to find the location of any -// function literal in a subtree. -type funcLitFinder token.Pos - -func (f *funcLitFinder) Visit(node ast.Node) (w ast.Visitor) { - if f.found() { - return nil // Prune search. - } - switch n := node.(type) { - case *ast.FuncLit: - *f = funcLitFinder(n.Body.Lbrace) - return nil // Prune search. - } - return f -} - -func (f *funcLitFinder) found() bool { - return token.Pos(*f) != token.NoPos -} - -// Sort interface for []block1; used for self-check in addVariables. - -type block1 struct { - Block - index int -} - -type blockSlice []block1 - -func (b blockSlice) Len() int { return len(b) } -func (b blockSlice) Less(i, j int) bool { return b[i].startByte < b[j].startByte } -func (b blockSlice) Swap(i, j int) { b[i], b[j] = b[j], b[i] } - -// offset translates a token position into a 0-indexed byte offset. -func (f *File) offset(pos token.Pos) int { - return f.fset.Position(pos).Offset -} - -// addVariables adds to the end of the file the declarations to set up the counter and position variables. -func (f *File) addVariables(w io.Writer) { - // Self-check: Verify that the instrumented basic blocks are disjoint. - t := make([]block1, len(f.blocks)) - for i := range f.blocks { - t[i].Block = f.blocks[i] - t[i].index = i - } - sort.Sort(blockSlice(t)) - for i := 1; i < len(t); i++ { - if t[i-1].endByte > t[i].startByte { - fmt.Fprintf(os.Stderr, "cover: internal error: block %d overlaps block %d\n", t[i-1].index, t[i].index) - // Note: error message is in byte positions, not token positions. - fmt.Fprintf(os.Stderr, "\t%s:#%d,#%d %s:#%d,#%d\n", - f.name, f.offset(t[i-1].startByte), f.offset(t[i-1].endByte), - f.name, f.offset(t[i].startByte), f.offset(t[i].endByte)) - } - } - - // Declare the coverage struct as a package-level variable. - fmt.Fprintf(w, "\nvar %s = struct {\n", *varVar) - fmt.Fprintf(w, "\tCount [%d]uint32\n", len(f.blocks)) - fmt.Fprintf(w, "\tPos [3 * %d]uint32\n", len(f.blocks)) - fmt.Fprintf(w, "\tNumStmt [%d]uint16\n", len(f.blocks)) - fmt.Fprintf(w, "} {\n") - - // Initialize the position array field. - fmt.Fprintf(w, "\tPos: [3 * %d]uint32{\n", len(f.blocks)) - - // A nice long list of positions. Each position is encoded as follows to reduce size: - // - 32-bit starting line number - // - 32-bit ending line number - // - (16 bit ending column number << 16) | (16-bit starting column number). - for i, block := range f.blocks { - start := f.fset.Position(block.startByte) - end := f.fset.Position(block.endByte) - fmt.Fprintf(w, "\t\t%d, %d, %#x, // [%d]\n", start.Line, end.Line, (end.Column&0xFFFF)<<16|(start.Column&0xFFFF), i) - } - - // Close the position array. - fmt.Fprintf(w, "\t},\n") - - // Initialize the position array field. - fmt.Fprintf(w, "\tNumStmt: [%d]uint16{\n", len(f.blocks)) - - // A nice long list of statements-per-block, so we can give a conventional - // valuation of "percent covered". To save space, it's a 16-bit number, so we - // clamp it if it overflows - won't matter in practice. - for i, block := range f.blocks { - n := block.numStmt - if n > 1<<16-1 { - n = 1<<16 - 1 - } - fmt.Fprintf(w, "\t\t%d, // %d\n", n, i) - } - - // Close the statements-per-block array. - fmt.Fprintf(w, "\t},\n") - - // Close the struct initialization. - fmt.Fprintf(w, "}\n") -} diff --git a/cmd/cover/cover_test.go b/cmd/cover/cover_test.go deleted file mode 100644 index 91c7695b44d..00000000000 --- a/cmd/cover/cover_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// No testdata on Android. - -//go:build !android -// +build !android - -package main_test - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "testing" - - "golang.org/x/tools/internal/testenv" -) - -const ( - // Data directory, also the package directory for the test. - testdata = "testdata" -) - -var debug = false // Keeps the rewritten files around if set. - -// Run this shell script, but do it in Go so it can be run by "go test". -// -// replace the word LINE with the line number < testdata/test.go > testdata/test_line.go -// go build -o ./testcover -// ./testcover -mode=count -var=CoverTest -o ./testdata/test_cover.go testdata/test_line.go -// go run ./testdata/main.go ./testdata/test.go -func TestCover(t *testing.T) { - testenv.NeedsTool(t, "go") - - tmpdir, err := ioutil.TempDir("", "TestCover") - if err != nil { - t.Fatal(err) - } - defer func() { - if debug { - fmt.Printf("test files left in %s\n", tmpdir) - } else { - os.RemoveAll(tmpdir) - } - }() - - testcover := filepath.Join(tmpdir, "testcover.exe") - testMain := filepath.Join(tmpdir, "main.go") - testTest := filepath.Join(tmpdir, "test.go") - coverInput := filepath.Join(tmpdir, "test_line.go") - coverOutput := filepath.Join(tmpdir, "test_cover.go") - - for _, f := range []string{testMain, testTest} { - data, err := ioutil.ReadFile(filepath.Join(testdata, filepath.Base(f))) - if err != nil { - t.Fatal(err) - } - if err := ioutil.WriteFile(f, data, 0644); err != nil { - t.Fatal(err) - } - } - - // Read in the test file (testTest) and write it, with LINEs specified, to coverInput. - file, err := ioutil.ReadFile(testTest) - if err != nil { - t.Fatal(err) - } - lines := bytes.Split(file, []byte("\n")) - for i, line := range lines { - lines[i] = bytes.Replace(line, []byte("LINE"), []byte(fmt.Sprint(i+1)), -1) - } - err = ioutil.WriteFile(coverInput, bytes.Join(lines, []byte("\n")), 0666) - if err != nil { - t.Fatal(err) - } - - // go build -o testcover - cmd := exec.Command("go", "build", "-o", testcover) - run(cmd, t) - - // ./testcover -mode=count -var=coverTest -o ./testdata/test_cover.go testdata/test_line.go - cmd = exec.Command(testcover, "-mode=count", "-var=coverTest", "-o", coverOutput, coverInput) - run(cmd, t) - - // defer removal of ./testdata/test_cover.go - if !debug { - defer os.Remove(coverOutput) - } - - // go run ./testdata/main.go ./testdata/test.go - cmd = exec.Command("go", "run", testMain, coverOutput) - run(cmd, t) -} - -func run(c *exec.Cmd, t *testing.T) { - c.Stdout = os.Stdout - c.Stderr = os.Stderr - err := c.Run() - if err != nil { - t.Fatal(err) - } -} diff --git a/cmd/cover/doc.go b/cmd/cover/doc.go deleted file mode 100644 index 77dce442f15..00000000000 --- a/cmd/cover/doc.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Cover is a program for analyzing the coverage profiles generated by -'go test -coverprofile=cover.out'. - -Deprecated: For Go releases 1.5 and later, this tool lives in the -standard repository. The code here is not maintained. - -Cover is also used by 'go test -cover' to rewrite the source code with -annotations to track which parts of each function are executed. -It operates on one Go source file at a time, computing approximate -basic block information by studying the source. It is thus more portable -than binary-rewriting coverage tools, but also a little less capable. -For instance, it does not probe inside && and || expressions, and can -be mildly confused by single statements with multiple function literals. - -For usage information, please see: - - go help testflag - go tool cover -help -*/ -package main // import "golang.org/x/tools/cmd/cover" diff --git a/cmd/cover/func.go b/cmd/cover/func.go deleted file mode 100644 index 41d9fceca58..00000000000 --- a/cmd/cover/func.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This file implements the visitor that computes the (line, column)-(line-column) range for each function. - -package main - -import ( - "bufio" - "fmt" - "go/ast" - "go/build" - "go/parser" - "go/token" - "os" - "path/filepath" - "text/tabwriter" - - "golang.org/x/tools/cover" -) - -// funcOutput takes two file names as arguments, a coverage profile to read as input and an output -// file to write ("" means to write to standard output). The function reads the profile and produces -// as output the coverage data broken down by function, like this: -// -// fmt/format.go:30: init 100.0% -// fmt/format.go:57: clearflags 100.0% -// ... -// fmt/scan.go:1046: doScan 100.0% -// fmt/scan.go:1075: advance 96.2% -// fmt/scan.go:1119: doScanf 96.8% -// total: (statements) 91.9% - -func funcOutput(profile, outputFile string) error { - profiles, err := cover.ParseProfiles(profile) - if err != nil { - return err - } - - var out *bufio.Writer - if outputFile == "" { - out = bufio.NewWriter(os.Stdout) - } else { - fd, err := os.Create(outputFile) - if err != nil { - return err - } - defer fd.Close() - out = bufio.NewWriter(fd) - } - defer out.Flush() - - tabber := tabwriter.NewWriter(out, 1, 8, 1, '\t', 0) - defer tabber.Flush() - - var total, covered int64 - for _, profile := range profiles { - fn := profile.FileName - file, err := findFile(fn) - if err != nil { - return err - } - funcs, err := findFuncs(file) - if err != nil { - return err - } - // Now match up functions and profile blocks. - for _, f := range funcs { - c, t := f.coverage(profile) - fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n", fn, f.startLine, f.name, 100.0*float64(c)/float64(t)) - total += t - covered += c - } - } - fmt.Fprintf(tabber, "total:\t(statements)\t%.1f%%\n", 100.0*float64(covered)/float64(total)) - - return nil -} - -// findFuncs parses the file and returns a slice of FuncExtent descriptors. -func findFuncs(name string) ([]*FuncExtent, error) { - fset := token.NewFileSet() - parsedFile, err := parser.ParseFile(fset, name, nil, 0) - if err != nil { - return nil, err - } - visitor := &FuncVisitor{ - fset: fset, - name: name, - astFile: parsedFile, - } - ast.Walk(visitor, visitor.astFile) - return visitor.funcs, nil -} - -// FuncExtent describes a function's extent in the source by file and position. -type FuncExtent struct { - name string - startLine int - startCol int - endLine int - endCol int -} - -// FuncVisitor implements the visitor that builds the function position list for a file. -type FuncVisitor struct { - fset *token.FileSet - name string // Name of file. - astFile *ast.File - funcs []*FuncExtent -} - -// Visit implements the ast.Visitor interface. -func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor { - switch n := node.(type) { - case *ast.FuncDecl: - start := v.fset.Position(n.Pos()) - end := v.fset.Position(n.End()) - fe := &FuncExtent{ - name: n.Name.Name, - startLine: start.Line, - startCol: start.Column, - endLine: end.Line, - endCol: end.Column, - } - v.funcs = append(v.funcs, fe) - } - return v -} - -// coverage returns the fraction of the statements in the function that were covered, as a numerator and denominator. -func (f *FuncExtent) coverage(profile *cover.Profile) (num, den int64) { - // We could avoid making this n^2 overall by doing a single scan and annotating the functions, - // but the sizes of the data structures is never very large and the scan is almost instantaneous. - var covered, total int64 - // The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block. - for _, b := range profile.Blocks { - if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) { - // Past the end of the function. - break - } - if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) { - // Before the beginning of the function - continue - } - total += int64(b.NumStmt) - if b.Count > 0 { - covered += int64(b.NumStmt) - } - } - if total == 0 { - total = 1 // Avoid zero denominator. - } - return covered, total -} - -// findFile finds the location of the named file in GOROOT, GOPATH etc. -func findFile(file string) (string, error) { - dir, file := filepath.Split(file) - pkg, err := build.Import(dir, ".", build.FindOnly) - if err != nil { - return "", fmt.Errorf("can't find %q: %v", file, err) - } - return filepath.Join(pkg.Dir, file), nil -} diff --git a/cmd/cover/go.mod b/cmd/cover/go.mod deleted file mode 100644 index 4d1f325bfb9..00000000000 --- a/cmd/cover/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module golang.org/x/tools/cmd/cover - -go 1.18 - -require ( - golang.org/x/sys v0.4.0 - golang.org/x/tools v0.5.1-0.20230111220935-a7f7db3f17fc -) diff --git a/cmd/cover/go.sum b/cmd/cover/go.sum deleted file mode 100644 index c8ab130792d..00000000000 --- a/cmd/cover/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/tools v0.5.1-0.20230111220935-a7f7db3f17fc h1:zRn9MzwG18RZhyanShCfUwJTcobvqw8fOjjROFN9jtM= -golang.org/x/tools v0.5.1-0.20230111220935-a7f7db3f17fc/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= diff --git a/cmd/cover/html.go b/cmd/cover/html.go deleted file mode 100644 index 0f8c72542b8..00000000000 --- a/cmd/cover/html.go +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -import ( - "bufio" - "bytes" - "fmt" - exec "golang.org/x/sys/execabs" - "html/template" - "io" - "io/ioutil" - "math" - "os" - "path/filepath" - "runtime" - - "golang.org/x/tools/cover" -) - -// htmlOutput reads the profile data from profile and generates an HTML -// coverage report, writing it to outfile. If outfile is empty, -// it writes the report to a temporary file and opens it in a web browser. -func htmlOutput(profile, outfile string) error { - profiles, err := cover.ParseProfiles(profile) - if err != nil { - return err - } - - var d templateData - - for _, profile := range profiles { - fn := profile.FileName - if profile.Mode == "set" { - d.Set = true - } - file, err := findFile(fn) - if err != nil { - return err - } - src, err := ioutil.ReadFile(file) - if err != nil { - return fmt.Errorf("can't read %q: %v", fn, err) - } - var buf bytes.Buffer - err = htmlGen(&buf, src, profile.Boundaries(src)) - if err != nil { - return err - } - d.Files = append(d.Files, &templateFile{ - Name: fn, - Body: template.HTML(buf.String()), - Coverage: percentCovered(profile), - }) - } - - var out *os.File - if outfile == "" { - var dir string - dir, err = ioutil.TempDir("", "cover") - if err != nil { - return err - } - out, err = os.Create(filepath.Join(dir, "coverage.html")) - } else { - out, err = os.Create(outfile) - } - if err != nil { - return err - } - err = htmlTemplate.Execute(out, d) - if err == nil { - err = out.Close() - } - if err != nil { - return err - } - - if outfile == "" { - if !startBrowser("file://" + out.Name()) { - fmt.Fprintf(os.Stderr, "HTML output written to %s\n", out.Name()) - } - } - - return nil -} - -// percentCovered returns, as a percentage, the fraction of the statements in -// the profile covered by the test run. -// In effect, it reports the coverage of a given source file. -func percentCovered(p *cover.Profile) float64 { - var total, covered int64 - for _, b := range p.Blocks { - total += int64(b.NumStmt) - if b.Count > 0 { - covered += int64(b.NumStmt) - } - } - if total == 0 { - return 0 - } - return float64(covered) / float64(total) * 100 -} - -// htmlGen generates an HTML coverage report with the provided filename, -// source code, and tokens, and writes it to the given Writer. -func htmlGen(w io.Writer, src []byte, boundaries []cover.Boundary) error { - dst := bufio.NewWriter(w) - for i := range src { - for len(boundaries) > 0 && boundaries[0].Offset == i { - b := boundaries[0] - if b.Start { - n := 0 - if b.Count > 0 { - n = int(math.Floor(b.Norm*9)) + 1 - } - fmt.Fprintf(dst, ``, n, b.Count) - } else { - dst.WriteString("") - } - boundaries = boundaries[1:] - } - switch b := src[i]; b { - case '>': - dst.WriteString(">") - case '<': - dst.WriteString("<") - case '&': - dst.WriteString("&") - case '\t': - dst.WriteString(" ") - default: - dst.WriteByte(b) - } - } - return dst.Flush() -} - -// startBrowser tries to open the URL in a browser -// and reports whether it succeeds. -func startBrowser(url string) bool { - // try to start the browser - var args []string - switch runtime.GOOS { - case "darwin": - args = []string{"open"} - case "windows": - args = []string{"cmd", "/c", "start"} - default: - args = []string{"xdg-open"} - } - cmd := exec.Command(args[0], append(args[1:], url)...) - return cmd.Start() == nil -} - -// rgb returns an rgb value for the specified coverage value -// between 0 (no coverage) and 10 (max coverage). -func rgb(n int) string { - if n == 0 { - return "rgb(192, 0, 0)" // Red - } - // Gradient from gray to green. - r := 128 - 12*(n-1) - g := 128 + 12*(n-1) - b := 128 + 3*(n-1) - return fmt.Sprintf("rgb(%v, %v, %v)", r, g, b) -} - -// colors generates the CSS rules for coverage colors. -func colors() template.CSS { - var buf bytes.Buffer - for i := 0; i < 11; i++ { - fmt.Fprintf(&buf, ".cov%v { color: %v }\n", i, rgb(i)) - } - return template.CSS(buf.String()) -} - -var htmlTemplate = template.Must(template.New("html").Funcs(template.FuncMap{ - "colors": colors, -}).Parse(tmplHTML)) - -type templateData struct { - Files []*templateFile - Set bool -} - -type templateFile struct { - Name string - Body template.HTML - Coverage float64 -} - -const tmplHTML = ` - - - - - - - -
    - -
    - not tracked - {{if .Set}} - not covered - covered - {{else}} - no coverage - low coverage - * - * - * - * - * - * - * - * - high coverage - {{end}} -
    -
    -
    - {{range $i, $f := .Files}} -
    {{$f.Body}}
    - {{end}} -
    - - - -` diff --git a/cmd/cover/testdata/main.go b/cmd/cover/testdata/main.go deleted file mode 100644 index 6ed39c4f230..00000000000 --- a/cmd/cover/testdata/main.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Test runner for coverage test. This file is not coverage-annotated; test.go is. -// It knows the coverage counter is called "coverTest". - -package main - -import ( - "fmt" - "os" -) - -func main() { - testAll() - verify() -} - -type block struct { - count uint32 - line uint32 -} - -var counters = make(map[block]bool) - -// check records the location and expected value for a counter. -func check(line, count uint32) { - b := block{ - count, - line, - } - counters[b] = true -} - -// checkVal is a version of check that returns its extra argument, -// so it can be used in conditionals. -func checkVal(line, count uint32, val int) int { - b := block{ - count, - line, - } - counters[b] = true - return val -} - -var PASS = true - -// verify checks the expected counts against the actual. It runs after the test has completed. -func verify() { - for b := range counters { - got, index := count(b.line) - if b.count == anything && got != 0 { - got = anything - } - if got != b.count { - fmt.Fprintf(os.Stderr, "test_go:%d expected count %d got %d [counter %d]\n", b.line, b.count, got, index) - PASS = false - } - } - verifyPanic() - if !PASS { - fmt.Fprintf(os.Stderr, "FAIL\n") - os.Exit(2) - } -} - -// verifyPanic is a special check for the known counter that should be -// after the panic call in testPanic. -func verifyPanic() { - if coverTest.Count[panicIndex-1] != 1 { - // Sanity check for test before panic. - fmt.Fprintf(os.Stderr, "bad before panic") - PASS = false - } - if coverTest.Count[panicIndex] != 0 { - fmt.Fprintf(os.Stderr, "bad at panic: %d should be 0\n", coverTest.Count[panicIndex]) - PASS = false - } - if coverTest.Count[panicIndex+1] != 1 { - fmt.Fprintf(os.Stderr, "bad after panic") - PASS = false - } -} - -// count returns the count and index for the counter at the specified line. -func count(line uint32) (uint32, int) { - // Linear search is fine. Choose perfect fit over approximate. - // We can have a closing brace for a range on the same line as a condition for an "else if" - // and we don't want that brace to steal the count for the condition on the "if". - // Therefore we test for a perfect (lo==line && hi==line) match, but if we can't - // find that we take the first imperfect match. - index := -1 - indexLo := uint32(1e9) - for i := range coverTest.Count { - lo, hi := coverTest.Pos[3*i], coverTest.Pos[3*i+1] - if lo == line && line == hi { - return coverTest.Count[i], i - } - // Choose the earliest match (the counters are in unpredictable order). - if lo <= line && line <= hi && indexLo > lo { - index = i - indexLo = lo - } - } - if index == -1 { - fmt.Fprintln(os.Stderr, "cover_test: no counter for line", line) - PASS = false - return 0, 0 - } - return coverTest.Count[index], index -} diff --git a/cmd/cover/testdata/test.go b/cmd/cover/testdata/test.go deleted file mode 100644 index 9013950a2b3..00000000000 --- a/cmd/cover/testdata/test.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This program is processed by the cover command, and then testAll is called. -// The test driver in main.go can then compare the coverage statistics with expectation. - -// The word LINE is replaced by the line number in this file. When the file is executed, -// the coverage processing has changed the line numbers, so we can't use runtime.Caller. - -package main - -const anything = 1e9 // Just some unlikely value that means "we got here, don't care how often" - -func testAll() { - testSimple() - testBlockRun() - testIf() - testFor() - testRange() - testSwitch() - testTypeSwitch() - testSelect1() - testSelect2() - testPanic() - testEmptySwitches() -} - -// The indexes of the counters in testPanic are known to main.go -const panicIndex = 3 - -// This test appears first because the index of its counters is known to main.go -func testPanic() { - defer func() { - recover() - }() - check(LINE, 1) - panic("should not get next line") - check(LINE, 0) // this is GoCover.Count[panicIndex] - // The next counter is in testSimple and it will be non-zero. - // If the panic above does not trigger a counter, the test will fail - // because GoCover.Count[panicIndex] will be the one in testSimple. -} - -func testSimple() { - check(LINE, 1) -} - -func testIf() { - if true { - check(LINE, 1) - } else { - check(LINE, 0) - } - if false { - check(LINE, 0) - } else { - check(LINE, 1) - } - for i := 0; i < 3; i++ { - if checkVal(LINE, 3, i) <= 2 { - check(LINE, 3) - } - if checkVal(LINE, 3, i) <= 1 { - check(LINE, 2) - } - if checkVal(LINE, 3, i) <= 0 { - check(LINE, 1) - } - } - for i := 0; i < 3; i++ { - if checkVal(LINE, 3, i) <= 1 { - check(LINE, 2) - } else { - check(LINE, 1) - } - } - for i := 0; i < 3; i++ { - if checkVal(LINE, 3, i) <= 0 { - check(LINE, 1) - } else if checkVal(LINE, 2, i) <= 1 { - check(LINE, 1) - } else if checkVal(LINE, 1, i) <= 2 { - check(LINE, 1) - } else if checkVal(LINE, 0, i) <= 3 { - check(LINE, 0) - } - } - if func(a, b int) bool { return a < b }(3, 4) { - check(LINE, 1) - } -} - -func testFor() { - for i := 0; i < 10; func() { i++; check(LINE, 10) }() { - check(LINE, 10) - } -} - -func testRange() { - for _, f := range []func(){ - func() { check(LINE, 1) }, - } { - f() - check(LINE, 1) - } -} - -func testBlockRun() { - check(LINE, 1) - { - check(LINE, 1) - } - { - check(LINE, 1) - } - check(LINE, 1) - { - check(LINE, 1) - } - { - check(LINE, 1) - } - check(LINE, 1) -} - -func testSwitch() { - for i := 0; i < 5; func() { i++; check(LINE, 5) }() { - switch i { - case 0: - check(LINE, 1) - case 1: - check(LINE, 1) - case 2: - check(LINE, 1) - default: - check(LINE, 2) - } - } -} - -func testTypeSwitch() { - var x = []interface{}{1, 2.0, "hi"} - for _, v := range x { - switch func() { check(LINE, 3) }(); v.(type) { - case int: - check(LINE, 1) - case float64: - check(LINE, 1) - case string: - check(LINE, 1) - case complex128: - check(LINE, 0) - default: - check(LINE, 0) - } - } -} - -func testSelect1() { - c := make(chan int) - go func() { - for i := 0; i < 1000; i++ { - c <- i - } - }() - for { - select { - case <-c: - check(LINE, anything) - case <-c: - check(LINE, anything) - default: - check(LINE, 1) - return - } - } -} - -func testSelect2() { - c1 := make(chan int, 1000) - c2 := make(chan int, 1000) - for i := 0; i < 1000; i++ { - c1 <- i - c2 <- i - } - for { - select { - case <-c1: - check(LINE, 1000) - case <-c2: - check(LINE, 1000) - default: - check(LINE, 1) - return - } - } -} - -// Empty control statements created syntax errors. This function -// is here just to be sure that those are handled correctly now. -func testEmptySwitches() { - check(LINE, 1) - switch 3 { - } - check(LINE, 1) - switch i := (interface{})(3).(int); i { - } - check(LINE, 1) - c := make(chan int) - go func() { - check(LINE, 1) - c <- 1 - select {} - }() - <-c - check(LINE, 1) -} From 58691bc257f4210147995cbe4cfea977fff53df4 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 9 Jan 2023 10:29:37 -0500 Subject: [PATCH 612/723] gopls/internal/lsp/glob: add an LSP compliant glob implementation Add a quick-and-dirty (or rather not so quick, and-dirty) implementation of the LSP glob specification, and use it in the fake editor for filtering file change notifications. Change-Id: I521bf1b6559a63047f1f7fc690ab940d9c3c7379 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461256 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/fake/client.go | 37 +- gopls/internal/lsp/fake/editor.go | 18 +- gopls/internal/lsp/glob/glob.go | 349 ++++++++++++++++++ gopls/internal/lsp/glob/glob_test.go | 118 ++++++ .../internal/regtest/misc/definition_test.go | 2 + .../internal/regtest/misc/references_test.go | 2 + gopls/internal/regtest/watch/watch_test.go | 17 +- 7 files changed, 532 insertions(+), 11 deletions(-) create mode 100644 gopls/internal/lsp/glob/glob.go create mode 100644 gopls/internal/lsp/glob/glob_test.go diff --git a/gopls/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go index f44bd73b215..34b95dbd1c1 100644 --- a/gopls/internal/lsp/fake/client.go +++ b/gopls/internal/lsp/fake/client.go @@ -6,8 +6,10 @@ package fake import ( "context" + "encoding/json" "fmt" + "golang.org/x/tools/gopls/internal/lsp/glob" "golang.org/x/tools/gopls/internal/lsp/protocol" ) @@ -89,7 +91,40 @@ func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration func (c *Client) RegisterCapability(ctx context.Context, params *protocol.RegistrationParams) error { if c.hooks.OnRegistration != nil { - return c.hooks.OnRegistration(ctx, params) + if err := c.hooks.OnRegistration(ctx, params); err != nil { + return err + } + } + // Update file watching patterns. + // + // TODO(rfindley): We could verify more here, like verify that the + // registration ID is distinct, and that the capability is not currently + // registered. + for _, registration := range params.Registrations { + if registration.Method == "workspace/didChangeWatchedFiles" { + // Marshal and unmarshal to interpret RegisterOptions as + // DidChangeWatchedFilesRegistrationOptions. + raw, err := json.Marshal(registration.RegisterOptions) + if err != nil { + return fmt.Errorf("marshaling registration options: %v", err) + } + var opts protocol.DidChangeWatchedFilesRegistrationOptions + if err := json.Unmarshal(raw, &opts); err != nil { + return fmt.Errorf("unmarshaling registration options: %v", err) + } + var globs []*glob.Glob + for _, watcher := range opts.Watchers { + // TODO(rfindley): honor the watch kind. + g, err := glob.Parse(watcher.GlobPattern) + if err != nil { + return fmt.Errorf("error parsing glob pattern %q: %v", watcher.GlobPattern, err) + } + globs = append(globs, g) + } + c.editor.mu.Lock() + c.editor.watchPatterns = globs + c.editor.mu.Unlock() + } } return nil } diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index cda35d14b37..25aa6d5446e 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -17,6 +17,7 @@ import ( "sync" "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/glob" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" @@ -38,10 +39,11 @@ type Editor struct { sandbox *Sandbox defaultEnv map[string]string - mu sync.Mutex // guards config, buffers, serverCapabilities + mu sync.Mutex config EditorConfig // editor configuration buffers map[string]buffer // open buffers (relative path -> buffer content) serverCapabilities protocol.ServerCapabilities // capabilities / options + watchPatterns []*glob.Glob // glob patterns to watch // Call metrics for the purpose of expectations. This is done in an ad-hoc // manner for now. Perhaps in the future we should do something more @@ -361,8 +363,20 @@ func (e *Editor) onFileChanges(ctx context.Context, evts []protocol.FileEvent) { _ = e.setBufferContentLocked(ctx, path, false, lines(content), nil) } } + var matchedEvts []protocol.FileEvent + for _, evt := range evts { + filename := filepath.ToSlash(evt.URI.SpanURI().Filename()) + for _, g := range e.watchPatterns { + if g.Match(filename) { + matchedEvts = append(matchedEvts, evt) + break + } + } + } + + // TODO(rfindley): don't send notifications while locked. e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{ - Changes: evts, + Changes: matchedEvts, }) }() } diff --git a/gopls/internal/lsp/glob/glob.go b/gopls/internal/lsp/glob/glob.go new file mode 100644 index 00000000000..a540ebefac5 --- /dev/null +++ b/gopls/internal/lsp/glob/glob.go @@ -0,0 +1,349 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package glob implements an LSP-compliant glob pattern matcher for testing. +package glob + +import ( + "errors" + "fmt" + "strings" + "unicode/utf8" +) + +// A Glob is an LSP-compliant glob pattern, as defined by the spec: +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentFilter +// +// NOTE: this implementation is currently only intended for testing. In order +// to make it production ready, we'd need to: +// - verify it against the VS Code implementation +// - add more tests +// - microbenchmark, likely avoiding the element interface +// - resolve the question of what is meant by "character". If it's a UTF-16 +// code (as we suspect) it'll be a bit more work. +// +// Quoting from the spec: +// Glob patterns can have the following syntax: +// - `*` to match one or more characters in a path segment +// - `?` to match on one character in a path segment +// - `**` to match any number of path segments, including none +// - `{}` to group sub patterns into an OR expression. (e.g. `**/*.{ts,js}` +// matches all TypeScript and JavaScript files) +// - `[]` to declare a range of characters to match in a path segment +// (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) +// - `[!...]` to negate a range of characters to match in a path segment +// (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but +// not `example.0`) +// +// Expanding on this: +// - '/' matches one or more literal slashes. +// - any other character matches itself literally. +type Glob struct { + elems []element // pattern elements +} + +// Parse builds a Glob for the given pattern, returning an error if the pattern +// is invalid. +func Parse(pattern string) (*Glob, error) { + g, _, err := parse(pattern, false) + return g, err +} + +func parse(pattern string, nested bool) (*Glob, string, error) { + g := new(Glob) + for len(pattern) > 0 { + switch pattern[0] { + case '/': + pattern = pattern[1:] + g.elems = append(g.elems, slash{}) + + case '*': + if len(pattern) > 1 && pattern[1] == '*' { + if (len(g.elems) > 0 && g.elems[len(g.elems)-1] != slash{}) || (len(pattern) > 2 && pattern[2] != '/') { + return nil, "", errors.New("** may only be adjacent to '/'") + } + pattern = pattern[2:] + g.elems = append(g.elems, starStar{}) + break + } + pattern = pattern[1:] + g.elems = append(g.elems, star{}) + + case '?': + pattern = pattern[1:] + g.elems = append(g.elems, anyChar{}) + + case '{': + var gs group + for pattern[0] != '}' { + pattern = pattern[1:] + g, pat, err := parse(pattern, true) + if err != nil { + return nil, "", err + } + if len(pat) == 0 { + return nil, "", errors.New("unmatched '{'") + } + pattern = pat + gs = append(gs, g) + } + pattern = pattern[1:] + g.elems = append(g.elems, gs) + + case '}', ',': + if nested { + return g, pattern, nil + } + pattern = g.parseLiteral(pattern, false) + + case '[': + pattern = pattern[1:] + if len(pattern) == 0 { + return nil, "", errBadRange + } + negate := false + if pattern[0] == '!' { + pattern = pattern[1:] + negate = true + } + low, sz, err := readRangeRune(pattern) + if err != nil { + return nil, "", err + } + pattern = pattern[sz:] + if len(pattern) == 0 || pattern[0] != '-' { + return nil, "", errBadRange + } + pattern = pattern[1:] + high, sz, err := readRangeRune(pattern) + if err != nil { + return nil, "", err + } + pattern = pattern[sz:] + if len(pattern) == 0 || pattern[0] != ']' { + return nil, "", errBadRange + } + pattern = pattern[1:] + g.elems = append(g.elems, charRange{negate, low, high}) + + default: + pattern = g.parseLiteral(pattern, nested) + } + } + return g, "", nil +} + +// helper for decoding a rune in range elements, e.g. [a-z] +func readRangeRune(input string) (rune, int, error) { + r, sz := utf8.DecodeRuneInString(input) + var err error + if r == utf8.RuneError { + // See the documentation for DecodeRuneInString. + switch sz { + case 0: + err = errBadRange + case 1: + err = errInvalidUTF8 + } + } + return r, sz, err +} + +var ( + errBadRange = errors.New("'[' patterns must be of the form [x-y]") + errInvalidUTF8 = errors.New("invalid UTF-8 encoding") +) + +func (g *Glob) parseLiteral(pattern string, nested bool) string { + var specialChars string + if nested { + specialChars = "*?{[/}," + } else { + specialChars = "*?{[/" + } + end := strings.IndexAny(pattern, specialChars) + if end == -1 { + end = len(pattern) + } + g.elems = append(g.elems, literal(pattern[:end])) + return pattern[end:] +} + +func (g *Glob) String() string { + var b strings.Builder + for _, e := range g.elems { + fmt.Fprint(&b, e) + } + return b.String() +} + +// element holds a glob pattern element, as defined below. +type element fmt.Stringer + +// element types. +type ( + slash struct{} // One or more '/' separators + literal string // string literal, not containing /, *, ?, {}, or [] + star struct{} // * + anyChar struct{} // ? + starStar struct{} // ** + group []*Glob // {foo, bar, ...} grouping + charRange struct { // [a-z] character range + negate bool + low, high rune + } +) + +func (s slash) String() string { return "/" } +func (l literal) String() string { return string(l) } +func (s star) String() string { return "*" } +func (a anyChar) String() string { return "?" } +func (s starStar) String() string { return "**" } +func (g group) String() string { + var parts []string + for _, g := range g { + parts = append(parts, g.String()) + } + return "{" + strings.Join(parts, ",") + "}" +} +func (r charRange) String() string { + return "[" + string(r.low) + "-" + string(r.high) + "]" +} + +// Match reports whether the input string matches the glob pattern. +func (g *Glob) Match(input string) bool { + return match(g.elems, input) +} + +func match(elems []element, input string) (ok bool) { + var elem interface{} + for len(elems) > 0 { + elem, elems = elems[0], elems[1:] + switch elem := elem.(type) { + case slash: + if len(input) == 0 || input[0] != '/' { + return false + } + for input[0] == '/' { + input = input[1:] + } + + case starStar: + // Special cases: + // - **/a matches "a" + // - **/ matches everything + // + // Note that if ** is followed by anything, it must be '/' (this is + // enforced by Parse). + if len(elems) > 0 { + elems = elems[1:] + } + + // A trailing ** matches anything. + if len(elems) == 0 { + return true + } + + // Backtracking: advance pattern segments until the remaining pattern + // elements match. + for len(input) != 0 { + if match(elems, input) { + return true + } + _, input = split(input) + } + return false + + case literal: + if !strings.HasPrefix(input, string(elem)) { + return false + } + input = input[len(elem):] + + case star: + var segInput string + segInput, input = split(input) + + elemEnd := len(elems) + for i, e := range elems { + if e == (slash{}) { + elemEnd = i + break + } + } + segElems := elems[:elemEnd] + elems = elems[elemEnd:] + + // A trailing * matches the entire segment. + if len(segElems) == 0 { + break + } + + // Backtracking: advance characters until remaining subpattern elements + // match. + matched := false + for i := range segInput { + if match(segElems, segInput[i:]) { + matched = true + break + } + } + if !matched { + return false + } + + case anyChar: + if len(input) == 0 || input[0] == '/' { + return false + } + input = input[1:] + + case group: + // Append remaining pattern elements to each group member looking for a + // match. + var branch []element + for _, m := range elem { + branch = branch[:0] + branch = append(branch, m.elems...) + branch = append(branch, elems...) + if match(branch, input) { + return true + } + } + return false + + case charRange: + if len(input) == 0 || input[0] == '/' { + return false + } + c, sz := utf8.DecodeRuneInString(input) + if c < elem.low || c > elem.high { + return false + } + input = input[sz:] + + default: + panic(fmt.Sprintf("segment type %T not implemented", elem)) + } + } + + return len(input) == 0 +} + +// split returns the portion before and after the first slash +// (or sequence of consecutive slashes). If there is no slash +// it returns (input, nil). +func split(input string) (first, rest string) { + i := strings.IndexByte(input, '/') + if i < 0 { + return input, "" + } + first = input[:i] + for j := i; j < len(input); j++ { + if input[j] != '/' { + return first, input[j:] + } + } + return first, "" +} diff --git a/gopls/internal/lsp/glob/glob_test.go b/gopls/internal/lsp/glob/glob_test.go new file mode 100644 index 00000000000..df602624d9c --- /dev/null +++ b/gopls/internal/lsp/glob/glob_test.go @@ -0,0 +1,118 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package glob_test + +import ( + "testing" + + "golang.org/x/tools/gopls/internal/lsp/glob" +) + +func TestParseErrors(t *testing.T) { + tests := []string{ + "***", + "ab{c", + "[]", + "[a-]", + "ab{c{d}", + } + + for _, test := range tests { + _, err := glob.Parse(test) + if err == nil { + t.Errorf("Parse(%q) succeeded unexpectedly", test) + } + } +} + +func TestMatch(t *testing.T) { + tests := []struct { + pattern, input string + want bool + }{ + // Basic cases. + {"", "", true}, + {"", "a", false}, + {"", "/", false}, + {"abc", "abc", true}, + + // ** behavior + {"**", "abc", true}, + {"**/abc", "abc", true}, + {"**", "abc/def", true}, + {"{a/**/c,a/**/d}", "a/b/c", true}, + {"{a/**/c,a/**/d}", "a/b/c/d", true}, + {"{a/**/c,a/**/e}", "a/b/c/d", false}, + {"{a/**/c,a/**/e,a/**/d}", "a/b/c/d", true}, + {"{/a/**/c,a/**/e,a/**/d}", "a/b/c/d", true}, + {"{/a/**/c,a/**/e,a/**/d}", "/a/b/c/d", false}, + {"{/a/**/c,a/**/e,a/**/d}", "/a/b/c", true}, + {"{/a/**/e,a/**/e,a/**/d}", "/a/b/c", false}, + + // * and ? behavior + {"/*", "/a", true}, + {"*", "foo", true}, + {"*o", "foo", true}, + {"*o", "foox", false}, + {"f*o", "foo", true}, + {"f*o", "fo", true}, + {"fo?", "foo", true}, + {"fo?", "fox", true}, + {"fo?", "fooo", false}, + {"fo?", "fo", false}, + {"?", "a", true}, + {"?", "ab", false}, + {"?", "", false}, + {"*?", "", false}, + {"?b", "ab", true}, + {"?c", "ab", false}, + + // {} behavior + {"ab{c,d}e", "abce", true}, + {"ab{c,d}e", "abde", true}, + {"ab{c,d}e", "abxe", false}, + {"ab{c,d}e", "abe", false}, + {"{a,b}c", "ac", true}, + {"{a,b}c", "bc", true}, + {"{a,b}c", "ab", false}, + {"a{b,c}", "ab", true}, + {"a{b,c}", "ac", true}, + {"a{b,c}", "bc", false}, + {"ab{c{1,2},d}e", "abc1e", true}, + {"ab{c{1,2},d}e", "abde", true}, + {"ab{c{1,2},d}e", "abc1f", false}, + {"ab{c{1,2},d}e", "abce", false}, + {"ab{c[}-~]}d", "abc}d", true}, + {"ab{c[}-~]}d", "abc~d", true}, + {"ab{c[}-~],y}d", "abcxd", false}, + {"ab{c[}-~],y}d", "abyd", true}, + {"ab{c[}-~],y}d", "abd", false}, + {"{a/b/c,d/e/f}", "a/b/c", true}, + {"/ab{/c,d}e", "/ab/ce", true}, + {"/ab{/c,d}e", "/ab/cf", false}, + + // [-] behavior + {"[a-c]", "a", true}, + {"[a-c]", "b", true}, + {"[a-c]", "c", true}, + {"[a-c]", "d", false}, + {"[a-c]", " ", false}, + + // Realistic examples. + {"**/*.{ts,js}", "path/to/foo.ts", true}, + {"**/*.{ts,js}", "path/to/foo.js", true}, + {"**/*.{ts,js}", "path/to/foo.go", false}, + } + + for _, test := range tests { + g, err := glob.Parse(test.pattern) + if err != nil { + t.Fatalf("New(%q) failed unexpectedly: %v", test.pattern, err) + } + if got := g.Match(test.input); got != test.want { + t.Errorf("New(%q).Match(%q) = %t, want %t", test.pattern, test.input, got, test.want) + } + } +} diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index 7d6eeae5098..a662e4c1061 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -291,6 +291,8 @@ func TestGoToCrashingDefinition_Issue49223(t *testing.T) { // causes packages to move; see issue #55995. // See also TestImplementationsInVendor, which tests the same fix. func TestVendoringInvalidatesMetadata(t *testing.T) { + t.Skip("golang/go#56169: file watching does not capture vendor dirs") + const proxy = ` -- other.com/b@v1.0.0/go.mod -- module other.com/b diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index 0e8ff45561a..e5ee019857f 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -296,6 +296,8 @@ func _() { // implementations in vendored modules were not found. The actual fix // was the same as for #55995; see TestVendoringInvalidatesMetadata. func TestImplementationsInVendor(t *testing.T) { + t.Skip("golang/go#56169: file watching does not capture vendor dirs") + const proxy = ` -- other.com/b@v1.0.0/go.mod -- module other.com/b diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 2a1903ff624..99c76ff9341 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -38,10 +38,13 @@ func _() { t.Run("unopened", func(t *testing.T) { Run(t, pkg, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("a/a.go", "x"), + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "x"), + ), ) env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) - env.Await( + env.AfterChange( EmptyDiagnostics("a/a.go"), ) }) @@ -55,13 +58,11 @@ func _() { // Insert a trivial edit so that we don't automatically update the buffer // (see CL 267577). env.EditBuffer("a/a.go", fake.NewEdit(0, 0, 0, 0, " ")) - env.Await(env.DoneWithOpen()) + env.AfterChange() env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - env.DiagnosticAtRegexp("a/a.go", "x"), - )) + env.AfterChange( + env.DiagnosticAtRegexp("a/a.go", "x"), + ) }) }) } From 97d5de5ada86924765d4f95f1e72080d903a8516 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 5 Jan 2023 10:31:04 -0500 Subject: [PATCH 613/723] gopls/internal/cache: don't mark initialized after cancellation When a snapshot reinitialization is canceled, we should not mark it as complete. Initialization is special, in that it does not set GOPROXY=off. Canceling it can result in subsequent load failures. Fixes golang/go#57626 Change-Id: I9bc2c9226539fa8d240ecf2be62cc8d9915e5642 Reviewed-on: https://go-review.googlesource.com/c/tools/+/460795 Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/view.go | 42 ++++++---- gopls/internal/lsp/regtest/expectation.go | 7 ++ .../regtest/workspace/metadata_test.go | 78 +++++++++++++++++++ 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index a873b50ef14..c2ad0d41620 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -657,11 +657,22 @@ func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) { s.collectAllKnownSubdirs(ctx) } -func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { +func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadErr error) { + // A failure is retryable if it may have been due to context cancellation, + // and this is not the initial workspace load (firstAttempt==true). + // + // The IWL runs on a detached context with a long (~10m) timeout, so + // if the context was canceled we consider loading to have failed + // permanently. + retryableFailure := func() bool { + return loadErr != nil && ctx.Err() != nil && !firstAttempt + } defer func() { - s.mu.Lock() - s.initialized = true - s.mu.Unlock() + if !retryableFailure() { + s.mu.Lock() + s.initialized = true + s.mu.Unlock() + } if firstAttempt { close(s.view.initialWorkspaceLoad) } @@ -719,26 +730,24 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { if len(scopes) > 0 { scopes = append(scopes, packageLoadScope("builtin")) } - err := s.load(ctx, true, scopes...) + loadErr = s.load(ctx, true, scopes...) - // If the context is canceled on the first attempt, loading has failed - // because the go command has timed out--that should be a critical error. - if err != nil && !firstAttempt && ctx.Err() != nil { - return + if retryableFailure() { + return loadErr } var criticalErr *source.CriticalError switch { - case err != nil && ctx.Err() != nil: - event.Error(ctx, fmt.Sprintf("initial workspace load: %v", err), err) + case loadErr != nil && ctx.Err() != nil: + event.Error(ctx, fmt.Sprintf("initial workspace load: %v", loadErr), loadErr) criticalErr = &source.CriticalError{ - MainError: err, + MainError: loadErr, } - case err != nil: - event.Error(ctx, "initial workspace load failed", err) - extractedDiags := s.extractGoCommandErrors(ctx, err) + case loadErr != nil: + event.Error(ctx, "initial workspace load failed", loadErr) + extractedDiags := s.extractGoCommandErrors(ctx, loadErr) criticalErr = &source.CriticalError{ - MainError: err, + MainError: loadErr, Diagnostics: append(modDiagnostics, extractedDiags...), } case len(modDiagnostics) == 1: @@ -757,6 +766,7 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { s.mu.Lock() defer s.mu.Unlock() s.initializedErr = criticalErr + return loadErr } // invalidateContent invalidates the content of a Go file, diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index c30a075886c..3b623d06059 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -328,6 +328,13 @@ func (e *Env) DoneWithSave() Expectation { return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves, true) } +// StartedChangeWatchedFiles expects that the server has at least started +// processing all didChangeWatchedFiles notifications sent from the client. +func (e *Env) StartedChangeWatchedFiles() Expectation { + changes := e.Editor.Stats().DidChangeWatchedFiles + return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes) +} + // DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications // currently sent by the editor to be completely processed. func (e *Env) DoneWithChangeWatchedFiles() Expectation { diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index 55bc01e72d8..4620b93cbb9 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -5,6 +5,7 @@ package workspace import ( + "strings" "testing" . "golang.org/x/tools/gopls/internal/lsp/regtest" @@ -109,3 +110,80 @@ func main() {} ) }) } + +func TestReinitializeRepeatedly(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work + + const multiModule = ` +-- go.work -- +go 1.18 + +use ( + moda/a + modb +) +-- moda/a/go.mod -- +module a.com + +require b.com v1.2.3 +-- moda/a/go.sum -- +b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= +b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= +-- moda/a/a.go -- +package a + +import ( + "b.com/b" +) + +func main() { + var x int + _ = b.Hello() + // AAA +} +-- modb/go.mod -- +module b.com + +-- modb/b/b.go -- +package b + +func Hello() int { + var x int +} +` + WithOptions( + ProxyFiles(workspaceModuleProxy), + Settings{ + // For this test, we want workspace diagnostics to start immediately + // during change processing. + "diagnosticsDelay": "0", + }, + ).Run(t, multiModule, func(t *testing.T, env *Env) { + env.OpenFile("moda/a/a.go") + env.AfterChange() + + // This test verifies that we fully process workspace reinitialization + // (which allows GOPROXY), even when the reinitialized snapshot is + // invalidated by subsequent changes. + // + // First, update go.work to remove modb. This will cause reinitialization + // to fetch b.com from the proxy. + env.WriteWorkspaceFile("go.work", "go 1.18\nuse moda/a") + // Next, wait for gopls to start processing the change. Because we've set + // diagnosticsDelay to zero, this will start diagnosing the workspace (and + // try to reinitialize on the snapshot context). + env.Await(env.StartedChangeWatchedFiles()) + // Finally, immediately make a file change to cancel the previous + // operation. This is racy, but will usually cause initialization to be + // canceled. + env.RegexpReplace("moda/a/a.go", "AAA", "BBB") + env.AfterChange() + // Now, to satisfy a definition request, gopls will try to reload moda. But + // without access to the proxy (because this is no longer a + // reinitialization), this loading will fail. + got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) + if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { + t.Errorf("expected %s, got %v", want, got) + } + }) +} From 5b300bd6499fe435792f81641320667c3edaabfa Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 29 Dec 2022 13:39:59 -0500 Subject: [PATCH 614/723] gopls/internal/lsp/cache: clean up view workspace information Tracking of workspace information in the View contained several inconsistencies and redundancies. Clean this up, with the following changes: - eliminate the View.rootURI, opting to derive it instead - eliminate the View.explicitGowork field, instead using the view.gowork field and checking if it is outside of the workspace. - eliminate many places where directory filters were interpreted relative to the rootURI. This is wrong: directory filters are expressed relative to the workspace folder. - remove special handling of GOMODCACHE, now that we're on Go 1.16+ - rewrite the locateTemplateFiles function to use view.filterFunc and filepath.WalkDir (now that we're on Go 1.16+). - don't request goimports env vars when loading environment variables for the view. They weren't being propagated to goimports anyway, and goimports will load them as needed. For golang/go#55331 Change-Id: I5e7f7e77e86d9ae425d2feaff31030278fed8240 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459789 Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/imports.go | 4 +- gopls/internal/lsp/cache/load.go | 7 +- gopls/internal/lsp/cache/session.go | 24 +- gopls/internal/lsp/cache/snapshot.go | 10 +- gopls/internal/lsp/cache/view.go | 333 +++++++++--------- .../regtest/workspace/fromenv_test.go | 3 + internal/imports/fix.go | 6 +- 7 files changed, 189 insertions(+), 198 deletions(-) diff --git a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go index 2bda377746d..5b094cb9f78 100644 --- a/gopls/internal/lsp/cache/imports.go +++ b/gopls/internal/lsp/cache/imports.go @@ -138,7 +138,7 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho // and has led to memory leaks in the past, when the snapshot was // unintentionally held past its lifetime. _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ - WorkingDir: snapshot.view.rootURI.Filename(), + WorkingDir: snapshot.view.workingDir().Filename(), }) if err != nil { return nil, err @@ -175,7 +175,7 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho os.RemoveAll(tmpDir) // ignore error } } else { - pe.WorkingDir = snapshot.view.rootURI.Filename() + pe.WorkingDir = snapshot.view.workingDir().Filename() } return cleanup, nil diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index b072aafd0a7..07dae760320 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -113,7 +113,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc flags |= source.AllowNetwork } _, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{ - WorkingDir: s.view.rootURI.Filename(), + WorkingDir: s.view.workingDir().Filename(), }) if err != nil { return err @@ -152,7 +152,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc } moduleErrs := make(map[string][]packages.Error) // module path -> errors - filterer := buildFilterer(s.view.rootURI.Filename(), s.view.gomodcache, s.view.Options()) + filterFunc := s.view.filterFunc() newMetadata := make(map[PackageID]*source.Metadata) for _, pkg := range pkgs { // The Go command returns synthetic list results for module queries that @@ -199,7 +199,8 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc // // TODO(rfindley): why exclude metadata arbitrarily here? It should be safe // to capture all metadata. - if s.view.allFilesExcluded(pkg, filterer) { + // TODO(rfindley): what about compiled go files? + if allFilesExcluded(pkg.GoFiles, filterFunc) { continue } if err := buildMetadata(ctx, pkg, cfg, query, newMetadata, nil); err != nil { diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 5cf991bced9..167ce90cca5 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -7,7 +7,6 @@ package cache import ( "context" "fmt" - "os" "strconv" "strings" "sync" @@ -180,6 +179,8 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt return view, snapshot, release, nil } +// TODO(rfindley): clarify that createView can never be cancelled (with the +// possible exception of server shutdown). func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, seqID uint64) (*View, *snapshot, func(), error) { index := atomic.AddInt64(&viewIndex, 1) @@ -188,7 +189,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, // TODO(rfindley): this info isn't actually immutable. For example, GOWORK // could be changed, or a user's environment could be modified. // We need a mechanism to invalidate it. - wsInfo, err := s.getWorkspaceInformation(ctx, folder, options) + info, err := s.getWorkspaceInformation(ctx, folder, options) if err != nil { return nil, nil, func() {}, err } @@ -196,7 +197,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, root := folder // filterFunc is the path filter function for this workspace folder. Notably, // it is relative to folder (which is specified by the user), not root. - filterFunc := pathExcludedByFilterFunc(folder.Filename(), wsInfo.gomodcache, options) + filterFunc := pathExcludedByFilterFunc(folder.Filename(), info.gomodcache, options) rootSrc, err := findWorkspaceModuleSource(ctx, root, s, filterFunc, options.ExperimentalWorkspaceModule) if err != nil { return nil, nil, func() {}, err @@ -205,14 +206,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, root = span.Dir(rootSrc) } - explicitGowork := os.Getenv("GOWORK") - if v, ok := options.Env["GOWORK"]; ok { - explicitGowork = v - } - goworkURI := span.URIFromPath(explicitGowork) - // Build the gopls workspace, collecting active modules in the view. - workspace, err := newWorkspace(ctx, root, goworkURI, s, filterFunc, wsInfo.effectiveGO111MODULE() == off, options.ExperimentalWorkspaceModule) + workspace, err := newWorkspace(ctx, root, info.effectiveGOWORK(), s, filterFunc, info.effectiveGO111MODULE() == off, options.ExperimentalWorkspaceModule) if err != nil { return nil, nil, func() {}, err } @@ -236,10 +231,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, vulns: map[span.URI]*govulncheck.Result{}, filesByURI: make(map[span.URI]span.URI), filesByBase: make(map[string][]canonicalURI), - rootURI: root, rootSrc: rootSrc, - explicitGowork: goworkURI, - workspaceInformation: *wsInfo, + workspaceInformation: info, } v.importsState = &importsState{ ctx: backgroundCtx, @@ -506,11 +499,12 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // Re-create views whose root may have changed. // // checkRoots controls whether to re-evaluate view definitions when - // collecting views below. Any change to a go.mod or go.work file may have - // affected the definition of the view. + // collecting views below. Any addition or deletion of a go.mod or go.work + // file may have affected the definition of the view. checkRoots := false for _, c := range changes { if isGoMod(c.URI) || isGoWork(c.URI) { + // TODO(rfindley): only consider additions or deletions here. checkRoots = true break } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 76350d5944e..87f3e1c8f9b 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -302,7 +302,7 @@ func (s *snapshot) ValidBuildConfiguration() bool { // Check if the workspace is within any of them. // TODO(rfindley): this should probably be subject to "if GO111MODULES = off {...}". for _, gp := range filepath.SplitList(s.view.gopath) { - if source.InDir(filepath.Join(gp, "src"), s.view.rootURI.Filename()) { + if source.InDir(filepath.Join(gp, "src"), s.view.folder.Filename()) { return true } } @@ -820,8 +820,10 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru fmt.Sprintf("**/*.{%s}", extensions): {}, } - if s.view.explicitGowork != "" { - patterns[s.view.explicitGowork.Filename()] = struct{}{} + // If GOWORK is outside the folder, ensure we are watching it. + gowork := s.view.effectiveGOWORK() + if gowork != "" && !source.InDir(s.view.folder.Filename(), gowork.Filename()) { + patterns[gowork.Filename()] = struct{}{} } // Add a pattern for each Go module in the workspace that is not within the view. @@ -2178,7 +2180,7 @@ func (s *snapshot) setBuiltin(path string) { // BuildGoplsMod generates a go.mod file for all modules in the workspace. It // bypasses any existing gopls.mod. func (s *snapshot) BuildGoplsMod(ctx context.Context) (*modfile.File, error) { - allModules, err := findModules(s.view.folder, pathExcludedByFilterFunc(s.view.rootURI.Filename(), s.view.gomodcache, s.View().Options()), 0) + allModules, err := findModules(s.view.folder, pathExcludedByFilterFunc(s.view.folder.Filename(), s.view.gomodcache, s.View().Options()), 0) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index c2ad0d41620..5703b3086cc 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -24,12 +24,10 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/mod/semver" exec "golang.org/x/sys/execabs" - "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" @@ -55,14 +53,9 @@ type View struct { // Workspace information. The fields below are immutable, and together with // options define the build list. Any change to these fields results in a new // View. - // - // TODO(rfindley): consolidate and/or eliminate redundancy in these fields, - // which have evolved from different sources over time. folder span.URI // user-specified workspace folder - rootURI span.URI // either folder or dir(rootSrc) (TODO: deprecate, in favor of folder+rootSrc) rootSrc span.URI // file providing module information (go.mod or go.work); may be empty - explicitGowork span.URI // explicitGowork: if non-empty, a user-specified go.work location (TODO: deprecate) - workspaceInformation // grab-bag of Go environment information (TODO: cleanup) + workspaceInformation // Go environment information importsState *importsState @@ -117,6 +110,9 @@ type View struct { } type workspaceInformation struct { + // `go env` variables that need to be tracked by gopls. + goEnv + // The Go version in use: X in Go 1.X. goversion int @@ -129,15 +125,6 @@ type workspaceInformation struct { // GOPACKAGESDRIVER environment variable or a gopackagesdriver binary on // their machine. hasGopackagesDriver bool - - // `go env` variables that need to be tracked by gopls. - // - // TODO(rfindley): eliminate this in favor of goEnv, or vice-versa. - environmentVariables - - // goEnv is the `go env` output collected when a view is created. - // It includes the values of the environment variables above. - goEnv map[string]string } // effectiveGO111MODULE reports the value of GO111MODULE effective in the go @@ -153,6 +140,15 @@ func (w workspaceInformation) effectiveGO111MODULE() go111module { } } +// effectiveGOWORK returns the effective GOWORK value for this workspace, if +// any, in URI form. +func (w workspaceInformation) effectiveGOWORK() span.URI { + if w.gowork == "off" || w.gowork == "" { + return "" + } + return span.URIFromPath(w.gowork) +} + // GO111MODULE returns the value of GO111MODULE to use for running the go // command. It differs from the user's environment in order to allow for the // more forgiving default value "auto" when using recent go versions. @@ -178,17 +174,73 @@ const ( on ) -// environmentVariables holds important environment variables captured by a -// call to `go env`. -type environmentVariables struct { - gocache, gopath, goroot, goprivate, gomodcache string +// goEnv holds important environment variables that gopls cares about. +type goEnv struct { + gocache, gopath, goroot, goprivate, gomodcache, gowork, goflags string - // Don't use go111module directly, because we choose to use a different + // go111module holds the value of GO111MODULE as reported by go env. + // + // Don't use this value directly, because we choose to use a different // default (auto) on Go 1.16 and later, to avoid spurious errors. Use - // the workspaceInformation.GO111MODULE method instead. + // the effectiveGO111MODULE method instead. go111module string } +// loadGoEnv loads `go env` values into the receiver, using the provided user +// environment and go command runner. +func (env *goEnv) load(ctx context.Context, folder string, configEnv []string, runner *gocommand.Runner) error { + vars := env.vars() + + // We can save ~200 ms by requesting only the variables we care about. + args := []string{"-json"} + for k := range vars { + args = append(args, k) + } + + inv := gocommand.Invocation{ + Verb: "env", + Args: args, + Env: configEnv, + WorkingDir: folder, + } + stdout, err := runner.Run(ctx, inv) + if err != nil { + return err + } + envMap := make(map[string]string) + if err := json.Unmarshal(stdout.Bytes(), &envMap); err != nil { + return fmt.Errorf("internal error unmarshaling JSON from 'go env': %w", err) + } + for key, ptr := range vars { + *ptr = envMap[key] + } + + return nil +} + +func (env goEnv) String() string { + var vars []string + for govar, ptr := range env.vars() { + vars = append(vars, fmt.Sprintf("%s=%s", govar, *ptr)) + } + sort.Strings(vars) + return "[" + strings.Join(vars, ", ") + "]" +} + +// vars returns a map from Go environment variable to field value containing it. +func (env *goEnv) vars() map[string]*string { + return map[string]*string{ + "GOCACHE": &env.gocache, + "GOPATH": &env.gopath, + "GOROOT": &env.goroot, + "GOPRIVATE": &env.goprivate, + "GOMODCACHE": &env.gomodcache, + "GO111MODULE": &env.go111module, + "GOWORK": &env.gowork, + "GOFLAGS": &env.goflags, + } +} + // workspaceMode holds various flags defining how the gopls workspace should // behave. They may be derived from the environment, user configuration, or // depend on the Go version. @@ -312,6 +364,9 @@ func minorOptionsChange(a, b *source.Options) bool { if !reflect.DeepEqual(a.StandaloneTags, b.StandaloneTags) { return false } + if a.ExpandWorkspaceToModule != b.ExpandWorkspaceToModule { + return false + } if a.MemoryMode != b.MemoryMode { return false } @@ -343,40 +398,33 @@ func (s *Session) SetViewOptions(ctx context.Context, v *View, options *source.O } // viewEnv returns a string describing the environment of a newly created view. +// +// It must not be called concurrently with any other view methods. func viewEnv(v *View) string { - v.optionsMu.Lock() env := v.options.EnvSlice() buildFlags := append([]string{}, v.options.BuildFlags...) - v.optionsMu.Unlock() var buf bytes.Buffer - fmt.Fprintf(&buf, `go env for %v -(root %s) + fmt.Fprintf(&buf, `go info for %v +(go dir %s) (go version %s) (valid build configuration = %v) (build flags: %v) +(selected go env: %v) `, v.folder.Filename(), - v.rootURI.Filename(), + v.workingDir().Filename(), strings.TrimRight(v.workspaceInformation.goversionOutput, "\n"), v.snapshot.ValidBuildConfiguration(), - buildFlags) + buildFlags, + v.goEnv, + ) - fullEnv := make(map[string]string) - for k, v := range v.goEnv { - fullEnv[k] = v - } for _, v := range env { s := strings.SplitN(v, "=", 2) if len(s) != 2 { continue } - if _, ok := fullEnv[s[0]]; ok { - fullEnv[s[0]] = s[1] - } - } - for k, v := range fullEnv { - fmt.Fprintf(&buf, "%s=%s\n", k, v) } return buf.String() @@ -400,41 +448,42 @@ func fileHasExtension(path string, suffixes []string) bool { return false } +// locateTemplateFiles ensures that the snapshot has mapped template files +// within the workspace folder. func (s *snapshot) locateTemplateFiles(ctx context.Context) { if len(s.view.Options().TemplateExtensions) == 0 { return } suffixes := s.view.Options().TemplateExtensions - // The workspace root may have been expanded to a module, but we should apply - // directory filters based on the configured workspace folder. - // - // TODO(rfindley): we should be more principled about paths outside of the - // workspace folder: do we even consider them? Do we support absolute - // exclusions? Relative exclusions starting with ..? - dir := s.workspace.root.Filename() - relativeTo := s.view.folder.Filename() - searched := 0 - filterer := buildFilterer(dir, s.view.gomodcache, s.view.Options()) - // Change to WalkDir when we move up to 1.16 - err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { + filterFunc := s.view.filterFunc() + err := filepath.WalkDir(s.view.folder.Filename(), func(path string, entry os.DirEntry, err error) error { if err != nil { return err } - relpath := strings.TrimPrefix(path, relativeTo) - excluded := pathExcludedByFilter(relpath, filterer) - if fileHasExtension(path, suffixes) && !excluded && !fi.IsDir() { - k := span.URIFromPath(path) - _, err := s.GetVersionedFile(ctx, k) - if err != nil { - return nil - } + if entry.IsDir() { + return nil } - searched++ if fileLimit > 0 && searched > fileLimit { return errExhausted } + searched++ + if !fileHasExtension(path, suffixes) { + return nil + } + uri := span.URIFromPath(path) + if filterFunc(uri) { + return nil + } + // Get the file in order to include it in the snapshot. + // TODO(golang/go#57558): it is fundamentally broken to track files in this + // way; we may lose them if configuration or layout changes cause a view to + // be recreated. + // + // Furthermore, this operation must ignore errors, including context + // cancellation, or risk leaving the snapshot in an undefined state. + s.GetFile(ctx, uri) return nil }) if err != nil { @@ -443,13 +492,18 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) { } func (v *View) contains(uri span.URI) bool { + // If we've expanded the go dir to a parent directory, consider if the + // expanded dir contains the uri. // TODO(rfindley): should we ignore the root here? It is not provided by the - // user, and is undefined when go.work is outside the workspace. It would be - // better to explicitly consider the set of active modules wherever relevant. - inRoot := source.InDir(v.rootURI.Filename(), uri.Filename()) + // user. It would be better to explicitly consider the set of active modules + // wherever relevant. + inGoDir := false + if source.InDir(v.workingDir().Filename(), v.folder.Filename()) { + inGoDir = source.InDir(v.workingDir().Filename(), uri.Filename()) + } inFolder := source.InDir(v.folder.Filename(), uri.Filename()) - if !inRoot && !inFolder { + if !inGoDir && !inFolder { return false } @@ -459,7 +513,7 @@ func (v *View) contains(uri span.URI) bool { // filterFunc returns a func that reports whether uri is filtered by the currently configured // directoryFilters. func (v *View) filterFunc() func(span.URI) bool { - filterer := buildFilterer(v.rootURI.Filename(), v.gomodcache, v.Options()) + filterer := buildFilterer(v.folder.Filename(), v.gomodcache, v.Options()) return func(uri span.URI) bool { // Only filter relative to the configured root directory. if source.InDir(v.folder.Filename(), uri.Filename()) { @@ -481,7 +535,7 @@ func (v *View) relevantChange(c source.FileModification) bool { // TODO(rstambler): Make sure the go.work/gopls.mod files are always known // to the view. for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - if c.URI == uriForSource(v.rootURI, v.explicitGowork, src) { + if c.URI == uriForSource(v.workingDir(), v.effectiveGOWORK(), src) { return true } } @@ -805,54 +859,35 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]*file return v.snapshot, v.snapshot.Acquire() } -func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, options *source.Options) (*workspaceInformation, error) { +func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, options *source.Options) (workspaceInformation, error) { if err := checkPathCase(folder.Filename()); err != nil { - return nil, fmt.Errorf("invalid workspace folder path: %w; check that the casing of the configured workspace folder path agrees with the casing reported by the operating system", err) + return workspaceInformation{}, fmt.Errorf("invalid workspace folder path: %w; check that the casing of the configured workspace folder path agrees with the casing reported by the operating system", err) } var err error + var info workspaceInformation inv := gocommand.Invocation{ WorkingDir: folder.Filename(), Env: options.EnvSlice(), } - goversion, err := gocommand.GoVersion(ctx, inv, s.gocmdRunner) + info.goversion, err = gocommand.GoVersion(ctx, inv, s.gocmdRunner) if err != nil { - return nil, err + return info, err } - goversionOutput, err := gocommand.GoVersionOutput(ctx, inv, s.gocmdRunner) + info.goversionOutput, err = gocommand.GoVersionOutput(ctx, inv, s.gocmdRunner) if err != nil { - return nil, err + return info, err } - - // Make sure to get the `go env` before continuing with initialization. - envVars, env, err := s.getGoEnv(ctx, folder.Filename(), goversion, options.EnvSlice()) - if err != nil { - return nil, err + if err := info.goEnv.load(ctx, folder.Filename(), options.EnvSlice(), s.gocmdRunner); err != nil { + return info, err } // The value of GOPACKAGESDRIVER is not returned through the go command. gopackagesdriver := os.Getenv("GOPACKAGESDRIVER") - // TODO(rfindley): this looks wrong, or at least overly defensive. If the - // value of GOPACKAGESDRIVER is not returned from the go command... why do we - // look it up here? - for _, s := range env { - split := strings.SplitN(s, "=", 2) - if split[0] == "GOPACKAGESDRIVER" { - bug.Reportf("found GOPACKAGESDRIVER from the go command") // see note above - gopackagesdriver = split[1] - } - } - // A user may also have a gopackagesdriver binary on their machine, which // works the same way as setting GOPACKAGESDRIVER. tool, _ := exec.LookPath("gopackagesdriver") - hasGopackagesDriver := gopackagesdriver != "off" && (gopackagesdriver != "" || tool != "") - - return &workspaceInformation{ - hasGopackagesDriver: hasGopackagesDriver, - goversion: goversion, - goversionOutput: goversionOutput, - environmentVariables: envVars, - goEnv: env, - }, nil + info.hasGopackagesDriver = gopackagesdriver != "off" && (gopackagesdriver != "" || tool != "") + + return info, nil } // findWorkspaceModuleSource searches for a "module source" relative to the @@ -907,6 +942,19 @@ func findWorkspaceModuleSource(ctx context.Context, folderURI span.URI, fs sourc return "", nil } +// workingDir returns the directory from which to run Go commands. +// +// The only case where this should matter is if we've narrowed the workspace to +// a singular nested module. In that case, the go command won't be able to find +// the module unless we tell it the nested directory. +func (v *View) workingDir() span.URI { + // TODO(golang/go#57514): eliminate the expandWorkspaceToModule setting. + if v.Options().ExpandWorkspaceToModule && v.rootSrc != "" { + return span.Dir(v.rootSrc) + } + return v.folder +} + // findRootPattern looks for files with the given basename in dir or any parent // directory of dir, using the provided FileSource. It returns the first match, // starting from dir and search parents. @@ -940,62 +988,6 @@ func defaultCheckPathCase(path string) error { return nil } -// getGoEnv gets the view's various GO* values. -func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, configEnv []string) (environmentVariables, map[string]string, error) { - envVars := environmentVariables{} - vars := map[string]*string{ - "GOCACHE": &envVars.gocache, - "GOPATH": &envVars.gopath, - "GOROOT": &envVars.goroot, - "GOPRIVATE": &envVars.goprivate, - "GOMODCACHE": &envVars.gomodcache, - "GO111MODULE": &envVars.go111module, - } - - // We can save ~200 ms by requesting only the variables we care about. - args := append([]string{"-json"}, imports.RequiredGoEnvVars...) - for k := range vars { - args = append(args, k) - } - // TODO(rfindley): GOWORK is not a property of the session. It may change - // when a workfile is added or removed. - // - // We need to distinguish between GOWORK values that are set by the GOWORK - // environment variable, and GOWORK values that are computed based on the - // location of a go.work file in the directory hierarchy. - args = append(args, "GOWORK") - - inv := gocommand.Invocation{ - Verb: "env", - Args: args, - Env: configEnv, - WorkingDir: folder, - } - // Don't go through runGoCommand, as we don't need a temporary -modfile to - // run `go env`. - stdout, err := s.gocmdRunner.Run(ctx, inv) - if err != nil { - return environmentVariables{}, nil, err - } - env := make(map[string]string) - if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { - return environmentVariables{}, nil, err - } - - for key, ptr := range vars { - *ptr = env[key] - } - - // Old versions of Go don't have GOMODCACHE, so emulate it. - // - // TODO(rfindley): consistent with the treatment of go111module, we should - // provide a wrapper method rather than mutating this value. - if envVars.gomodcache == "" && envVars.gopath != "" { - envVars.gomodcache = filepath.Join(filepath.SplitList(envVars.gopath)[0], "pkg/mod") - } - return envVars, env, err -} - func (v *View) IsGoPrivatePath(target string) bool { return globsMatchPath(v.goprivate, target) } @@ -1132,7 +1124,7 @@ func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modConten } // Explicit -mod flag? - matches := modFlagRegexp.FindStringSubmatch(s.view.goEnv["GOFLAGS"]) + matches := modFlagRegexp.FindStringSubmatch(s.view.goflags) if len(matches) != 0 { modFlag := matches[1] if modFlag != "" { @@ -1149,7 +1141,9 @@ func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modConten } // No vendor directory? - if fi, err := os.Stat(filepath.Join(s.view.rootURI.Filename(), "vendor")); err != nil || !fi.IsDir() { + // TODO(golang/go#57514): this is wrong if the working dir is not the module + // root. + if fi, err := os.Stat(filepath.Join(s.view.workingDir().Filename(), "vendor")); err != nil || !fi.IsDir() { return false, nil } @@ -1158,22 +1152,20 @@ func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modConten return vendorEnabled, nil } -func (v *View) allFilesExcluded(pkg *packages.Package, filterer *source.Filterer) bool { - folder := filepath.ToSlash(v.folder.Filename()) - for _, f := range pkg.GoFiles { - f = filepath.ToSlash(f) - if !strings.HasPrefix(f, folder) { - return false - } - if !pathExcludedByFilter(strings.TrimPrefix(f, folder), filterer) { +// TODO(rfindley): clean up the redundancy of allFilesExcluded, +// pathExcludedByFilterFunc, pathExcludedByFilter, view.filterFunc... +func allFilesExcluded(files []string, filterFunc func(span.URI) bool) bool { + for _, f := range files { + uri := span.URIFromPath(f) + if !filterFunc(uri) { return false } } return true } -func pathExcludedByFilterFunc(root, gomodcache string, opts *source.Options) func(string) bool { - filterer := buildFilterer(root, gomodcache, opts) +func pathExcludedByFilterFunc(folder, gomodcache string, opts *source.Options) func(string) bool { + filterer := buildFilterer(folder, gomodcache, opts) return func(path string) bool { return pathExcludedByFilter(path, filterer) } @@ -1190,13 +1182,12 @@ func pathExcludedByFilter(path string, filterer *source.Filterer) bool { return filterer.Disallow(path) } -func buildFilterer(root, gomodcache string, opts *source.Options) *source.Filterer { - // TODO(rfindley): this looks wrong. If gomodcache isn't actually nested - // under root, this will do the wrong thing. - gomodcache = strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(gomodcache, root)), "/") +func buildFilterer(folder, gomodcache string, opts *source.Options) *source.Filterer { filters := opts.DirectoryFilters - if gomodcache != "" { - filters = append(filters, "-"+gomodcache) + + if pref := strings.TrimPrefix(gomodcache, folder); pref != gomodcache { + modcacheFilter := "-" + strings.TrimPrefix(filepath.ToSlash(pref), "/") + filters = append(filters, modcacheFilter) } return source.NewFilterer(filters) } diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go index 83059bc75ce..47405c3a145 100644 --- a/gopls/internal/regtest/workspace/fromenv_test.go +++ b/gopls/internal/regtest/workspace/fromenv_test.go @@ -8,10 +8,12 @@ import ( "testing" . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" ) // Test that setting go.work via environment variables or settings works. func TestUseGoWorkOutsideTheWorkspace(t *testing.T) { + testenv.NeedsGo1Point(t, 18) const files = ` -- work/a/go.mod -- module a.com @@ -39,6 +41,7 @@ use ( ` WithOptions( + WorkspaceFolders("work"), // use a nested workspace dir, so that GOWORK is outside the workspace EnvVars{"GOWORK": "$SANDBOX_WORKDIR/config/go.work"}, ).Run(t, files, func(t *testing.T, env *Env) { // When we have an explicit GOWORK set, we should get a file watch request. diff --git a/internal/imports/fix.go b/internal/imports/fix.go index 646455802ba..642a5ac2d75 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -799,7 +799,7 @@ func GetPackageExports(ctx context.Context, wrapped func(PackageExport), searchP return getCandidatePkgs(ctx, callback, filename, filePkg, env) } -var RequiredGoEnvVars = []string{"GO111MODULE", "GOFLAGS", "GOINSECURE", "GOMOD", "GOMODCACHE", "GONOPROXY", "GONOSUMDB", "GOPATH", "GOPROXY", "GOROOT", "GOSUMDB", "GOWORK"} +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. @@ -869,7 +869,7 @@ func (e *ProcessEnv) init() error { } foundAllRequired := true - for _, k := range RequiredGoEnvVars { + for _, k := range requiredGoEnvVars { if _, ok := e.Env[k]; !ok { foundAllRequired = false break @@ -885,7 +885,7 @@ func (e *ProcessEnv) init() error { } goEnv := map[string]string{} - stdout, err := e.invokeGo(context.TODO(), "env", append([]string{"-json"}, RequiredGoEnvVars...)...) + stdout, err := e.invokeGo(context.TODO(), "env", append([]string{"-json"}, requiredGoEnvVars...)...) if err != nil { return err } From 8087911863a8b7dcb44166290f838e06ef1bbfdc Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 16 Dec 2022 09:11:12 -0500 Subject: [PATCH 615/723] gopls: remove the experimentalWorkspaceModule mode Remove the experimentalWorkspaceModule setting, and all of the roots it has planted. Specifically, this removes: - the gopls.mod and filesystem workspace modes - the command to generate a gopls.mod - the need to track separate workspace modes entirely, as we align with the Go command and don't need any additional logic - the need to maintain a workspace directory - the 'cache.workspace' abstraction entirely; now we can just track workspace modules Along the way, further simplify the treatment of view workspace information. In particular, just use the value of GOWORK returned by the go command, rather than computing it ourselves. This means that we may need to re-run `go env` while processing a change to go.mod or go.work files. If that proves to be problematic, we can improve it in the future. Many workspace tests had to be restricted to just Go 1.18+, because we no longer fake go.work support at earlier Go versions. Fixes golang/go#55331 Change-Id: I15ad58f548295727a51b99f43c7572b066f9df07 Reviewed-on: https://go-review.googlesource.com/c/tools/+/458116 gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- gopls/doc/commands.md | 14 - gopls/doc/settings.md | 12 - gopls/internal/lsp/cache/check.go | 6 +- gopls/internal/lsp/cache/imports.go | 58 +- gopls/internal/lsp/cache/load.go | 80 +-- gopls/internal/lsp/cache/mod.go | 10 - gopls/internal/lsp/cache/session.go | 95 ++- gopls/internal/lsp/cache/snapshot.go | 354 +++-------- gopls/internal/lsp/cache/view.go | 91 +-- gopls/internal/lsp/cache/view_test.go | 32 +- gopls/internal/lsp/cache/workspace.go | 564 ++---------------- gopls/internal/lsp/cache/workspace_test.go | 280 --------- gopls/internal/lsp/cmd/cmd.go | 1 - gopls/internal/lsp/cmd/help_test.go | 13 +- gopls/internal/lsp/cmd/usage/usage.hlp | 1 - gopls/internal/lsp/cmd/usage/workspace.hlp | 7 - gopls/internal/lsp/cmd/workspace.go | 77 --- gopls/internal/lsp/command.go | 29 - gopls/internal/lsp/command/command_gen.go | 20 - gopls/internal/lsp/command/interface.go | 5 - gopls/internal/lsp/command/interface_test.go | 6 +- gopls/internal/lsp/link.go | 6 +- gopls/internal/lsp/regtest/runner.go | 3 - gopls/internal/lsp/source/api_json.go | 14 - gopls/internal/lsp/source/options.go | 14 +- gopls/internal/lsp/source/view.go | 32 +- gopls/internal/lsp/tests/tests.go | 1 - .../regtest/codelens/codelens_test.go | 6 +- .../internal/regtest/workspace/broken_test.go | 3 + .../workspace/directoryfilters_test.go | 3 + .../regtest/workspace/fromenv_test.go | 21 +- .../regtest/workspace/workspace_test.go | 146 +---- 32 files changed, 320 insertions(+), 1684 deletions(-) delete mode 100644 gopls/internal/lsp/cmd/usage/workspace.hlp delete mode 100644 gopls/internal/lsp/cmd/workspace.go diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 9d5e851ccac..ac22c55113a 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -147,20 +147,6 @@ Args: } ``` -### **Generate gopls.mod** -Identifier: `gopls.generate_gopls_mod` - -(Re)generate the gopls.mod file for a workspace. - -Args: - -``` -{ - // The file URI. - "URI": string, -} -``` - ### **go get a package** Identifier: `gopls.go_get_package` diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 6816967ee21..3df086cf82b 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -116,18 +116,6 @@ a go.mod file, narrowing the scope to that directory if it exists. Default: `true`. -#### **experimentalWorkspaceModule** *bool* - -**This setting is experimental and may be deleted.** - -experimentalWorkspaceModule opts a user into the experimental support -for multi-module workspaces. - -Deprecated: this feature is deprecated and will be removed in a future -version of gopls (https://go.dev/issue/55331). - -Default: `false`. - #### **experimentalPackageCacheKey** *bool* **This setting is experimental and may be deleted.** diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index ae550bc5364..01259583ce2 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -357,13 +357,9 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF // If this is a replaced module in the workspace, the version is // meaningless, and we don't want clients to access it. if m.Module != nil { - version := m.Module.Version - if source.IsWorkspaceModuleVersion(version) { - version = "" - } pkg.version = &module.Version{ Path: m.Module.Path, - Version: version, + Version: m.Module.Version, } } diff --git a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go index 5b094cb9f78..46b8d151fc5 100644 --- a/gopls/internal/lsp/cache/imports.go +++ b/gopls/internal/lsp/cache/imports.go @@ -7,7 +7,6 @@ package cache import ( "context" "fmt" - "os" "reflect" "strings" "sync" @@ -25,12 +24,18 @@ type importsState struct { mu sync.Mutex processEnv *imports.ProcessEnv - cleanupProcessEnv func() cacheRefreshDuration time.Duration cacheRefreshTimer *time.Timer cachedModFileHash source.Hash cachedBuildFlags []string cachedDirectoryFilters []string + + // runOnce records whether runProcessEnvFunc has been called at least once. + // This is necessary to avoid resetting state before the process env is + // populated. + // + // TODO(rfindley): this shouldn't be necessary. + runOnce bool } func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(*imports.Options) error) error { @@ -43,7 +48,7 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot // // TODO(rfindley): consider instead hashing on-disk modfiles here. var modFileHash source.Hash - for m := range snapshot.workspace.ActiveModFiles() { + for m := range snapshot.workspaceModFiles { fh, err := snapshot.GetFile(ctx, m) if err != nil { return err @@ -70,22 +75,21 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot // As a special case, skip cleanup the first time -- we haven't fully // initialized the environment yet and calling GetResolver will do // unnecessary work and potentially mess up the go.mod file. - if s.cleanupProcessEnv != nil { + if s.runOnce { if resolver, err := s.processEnv.GetResolver(); err == nil { if modResolver, ok := resolver.(*imports.ModuleResolver); ok { modResolver.ClearForNewMod() } } - s.cleanupProcessEnv() } + s.cachedModFileHash = modFileHash s.cachedBuildFlags = currentBuildFlags s.cachedDirectoryFilters = currentDirectoryFilters - var err error - s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot) - if err != nil { + if err := s.populateProcessEnv(ctx, snapshot); err != nil { return err } + s.runOnce = true } // Run the user function. @@ -120,7 +124,7 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot // populateProcessEnv sets the dynamically configurable fields for the view's // process environment. Assumes that the caller is holding the s.view.importsMu. -func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapshot) (cleanup func(), err error) { +func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapshot) error { pe := s.processEnv if snapshot.view.Options().VerboseOutput { @@ -141,7 +145,7 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho WorkingDir: snapshot.view.workingDir().Filename(), }) if err != nil { - return nil, err + return err } pe.BuildFlags = inv.BuildFlags @@ -156,29 +160,9 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho } // We don't actually use the invocation, so clean it up now. cleanupInvocation() - - // If the snapshot uses a synthetic workspace directory, create a copy for - // the lifecycle of the importsState. - // - // Notably, we cannot use the snapshot invocation working directory, as that - // is tied to the lifecycle of the snapshot. - // - // Otherwise return a no-op cleanup function. - cleanup = func() {} - if snapshot.usesWorkspaceDir() { - tmpDir, err := makeWorkspaceDir(ctx, snapshot.workspace, snapshot) - if err != nil { - return nil, err - } - pe.WorkingDir = tmpDir - cleanup = func() { - os.RemoveAll(tmpDir) // ignore error - } - } else { - pe.WorkingDir = snapshot.view.workingDir().Filename() - } - - return cleanup, nil + // TODO(rfindley): should this simply be inv.WorkingDir? + pe.WorkingDir = snapshot.view.workingDir().Filename() + return nil } func (s *importsState) refreshProcessEnv() { @@ -202,11 +186,3 @@ func (s *importsState) refreshProcessEnv() { s.cacheRefreshTimer = nil s.mu.Unlock() } - -func (s *importsState) destroy() { - s.mu.Lock() - if s.cleanupProcessEnv != nil { - s.cleanupProcessEnv() - } - s.mu.Unlock() -} diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 07dae760320..c4e0296a3c6 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -9,8 +9,6 @@ import ( "context" "errors" "fmt" - "io/ioutil" - "os" "path/filepath" "sort" "strings" @@ -287,22 +285,17 @@ func (m *moduleErrorMap) Error() string { // TODO(rfindley): separate workspace diagnostics from critical workspace // errors. func (s *snapshot) workspaceLayoutError(ctx context.Context) (error, []*source.Diagnostic) { - // TODO(rfindley): do we really not want to show a critical error if the user - // has no go.mod files? - if len(s.workspace.getKnownModFiles()) == 0 { - return nil, nil - } - // TODO(rfindley): both of the checks below should be delegated to the workspace. + if s.view.effectiveGO111MODULE() == off { return nil, nil } - if s.workspace.moduleSource != legacyWorkspace { - return nil, nil - } - // If the user has one module per view, there is nothing to warn about. - if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 { + // If the user is using a go.work file, we assume that they know what they + // are doing. + // + // TODO(golang/go#53880): improve orphaned file diagnostics when using go.work. + if s.view.gowork != "" { return nil, nil } @@ -335,10 +328,10 @@ https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` // If the user has one active go.mod file, they may still be editing files // in nested modules. Check the module of each open file and add warnings // that the nested module must be opened as a workspace folder. - if len(s.workspace.ActiveModFiles()) == 1 { + if len(s.workspaceModFiles) == 1 { // Get the active root go.mod file to compare against. var rootMod string - for uri := range s.workspace.ActiveModFiles() { + for uri := range s.workspaceModFiles { rootMod = uri.Filename() } rootDir := filepath.Dir(rootMod) @@ -413,55 +406,6 @@ func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, fi return srcDiags } -// getWorkspaceDir returns the URI for the workspace directory -// associated with this snapshot. The workspace directory is a -// temporary directory containing the go.mod file computed from all -// active modules. -func (s *snapshot) getWorkspaceDir(ctx context.Context) (span.URI, error) { - s.mu.Lock() - dir, err := s.workspaceDir, s.workspaceDirErr - s.mu.Unlock() - if dir == "" && err == nil { // cache miss - dir, err = makeWorkspaceDir(ctx, s.workspace, s) - s.mu.Lock() - s.workspaceDir, s.workspaceDirErr = dir, err - s.mu.Unlock() - } - return span.URIFromPath(dir), err -} - -// makeWorkspaceDir creates a temporary directory containing a go.mod -// and go.sum file for each module in the workspace. -// Note: snapshot's mutex must be unlocked for it to satisfy FileSource. -func makeWorkspaceDir(ctx context.Context, workspace *workspace, fs source.FileSource) (string, error) { - file, err := workspace.modFile(ctx, fs) - if err != nil { - return "", err - } - modContent, err := file.Format() - if err != nil { - return "", err - } - sumContent, err := workspace.sumFile(ctx, fs) - if err != nil { - return "", err - } - tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod") - if err != nil { - return "", err - } - for name, content := range map[string][]byte{ - "go.mod": modContent, - "go.sum": sumContent, - } { - if err := ioutil.WriteFile(filepath.Join(tmpdir, name), content, 0644); err != nil { - os.RemoveAll(tmpdir) // ignore error - return "", err - } - } - return tmpdir, nil -} - // buildMetadata populates the updates map with metadata updates to // apply, based on the given pkg. It recurs through pkg.Imports to ensure that // metadata exists for all dependencies. @@ -628,9 +572,13 @@ func containsPackageLocked(s *snapshot, m *source.Metadata) bool { // Otherwise if the package has a module it must be an active module (as // defined by the module root or go.work file) and at least one file must not // be filtered out by directoryFilters. - if m.Module != nil && s.workspace.moduleSource != legacyWorkspace { + // + // TODO(rfindley): revisit this function. We should not need to predicate on + // gowork != "". It should suffice to consider workspace mod files (also, we + // will hopefully eliminate the concept of a workspace package soon). + if m.Module != nil && s.view.gowork != "" { modURI := span.URIFromPath(m.Module.GoMod) - _, ok := s.workspace.activeModFiles[modURI] + _, ok := s.workspaceModFiles[modURI] if !ok { return false } diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index 48590ecaa5b..b198dffb392 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -383,11 +383,6 @@ func (s *snapshot) matchErrorToModule(ctx context.Context, pm *source.ParsedModu for i := len(matches) - 1; i >= 0; i-- { ver := module.Version{Path: matches[i][1], Version: matches[i][2]} - // Any module versions that come from the workspace module should not - // be shown to the user. - if source.IsWorkspaceModuleVersion(ver.Version) { - continue - } if err := module.Check(ver.Path, ver.Version); err != nil { continue } @@ -424,11 +419,6 @@ func (s *snapshot) goCommandDiagnostic(pm *source.ParsedModule, spn span.Span, g var innermost *module.Version for i := len(matches) - 1; i >= 0; i-- { ver := module.Version{Path: matches[i][1], Version: matches[i][2]} - // Any module versions that come from the workspace module should not - // be shown to the user. - if source.IsWorkspaceModuleVersion(ver.Version) { - continue - } if err := module.Check(ver.Path, ver.Version); err != nil { continue } diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 167ce90cca5..82938732581 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -184,33 +184,13 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, seqID uint64) (*View, *snapshot, func(), error) { index := atomic.AddInt64(&viewIndex, 1) - // Get immutable workspace configuration. - // - // TODO(rfindley): this info isn't actually immutable. For example, GOWORK - // could be changed, or a user's environment could be modified. - // We need a mechanism to invalidate it. + // Get immutable workspace information. info, err := s.getWorkspaceInformation(ctx, folder, options) if err != nil { return nil, nil, func() {}, err } - root := folder - // filterFunc is the path filter function for this workspace folder. Notably, - // it is relative to folder (which is specified by the user), not root. - filterFunc := pathExcludedByFilterFunc(folder.Filename(), info.gomodcache, options) - rootSrc, err := findWorkspaceModuleSource(ctx, root, s, filterFunc, options.ExperimentalWorkspaceModule) - if err != nil { - return nil, nil, func() {}, err - } - if options.ExpandWorkspaceToModule && rootSrc != "" { - root = span.Dir(rootSrc) - } - - // Build the gopls workspace, collecting active modules in the view. - workspace, err := newWorkspace(ctx, root, info.effectiveGOWORK(), s, filterFunc, info.effectiveGO111MODULE() == off, options.ExperimentalWorkspaceModule) - if err != nil { - return nil, nil, func() {}, err - } + wsModFiles, wsModFilesErr := computeWorkspaceModFiles(ctx, info.gomod, info.effectiveGOWORK(), info.effectiveGO111MODULE(), s) // We want a true background context and not a detached context here // the spans need to be unrelated and no tag values should pollute it. @@ -231,7 +211,6 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, vulns: map[span.URI]*govulncheck.Result{}, filesByURI: make(map[span.URI]span.URI), filesByBase: make(map[string][]canonicalURI), - rootSrc: rootSrc, workspaceInformation: info, } v.importsState = &importsState{ @@ -274,7 +253,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, modVulnHandles: persistent.NewMap(uriLessInterface), modWhyHandles: persistent.NewMap(uriLessInterface), knownSubdirs: newKnownDirsSet(), - workspace: workspace, + workspaceModFiles: wsModFiles, + workspaceModFilesErr: wsModFilesErr, } // Save one reference in the view. v.releaseSnapshot = v.snapshot.Acquire() @@ -496,51 +476,52 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif return nil, nil, err } - // Re-create views whose root may have changed. + // Re-create views whose definition may have changed. // - // checkRoots controls whether to re-evaluate view definitions when + // checkViews controls whether to re-evaluate view definitions when // collecting views below. Any addition or deletion of a go.mod or go.work // file may have affected the definition of the view. - checkRoots := false + checkViews := false + for _, c := range changes { if isGoMod(c.URI) || isGoWork(c.URI) { - // TODO(rfindley): only consider additions or deletions here. - checkRoots = true - break + // Change, InvalidateMetadata, and UnknownFileAction actions do not cause + // us to re-evaluate views. + redoViews := (c.Action != source.Change && + c.Action != source.InvalidateMetadata && + c.Action != source.UnknownFileAction) + + if redoViews { + checkViews = true + break + } } } - if checkRoots { + if checkViews { for _, view := range s.views { - // Check whether the view must be recreated. This logic looks hacky, - // as it uses the existing view gomodcache and options to re-evaluate - // the workspace source, then expects view creation to compute the same - // root source after first re-evaluating gomodcache and options. - // - // Well, it *is* a bit hacky, but in practice we will get the same - // gomodcache and options, as any environment change affecting these - // should have already invalidated the view (c.f. minorOptionsChange). - // - // TODO(rfindley): clean this up. - filterFunc := pathExcludedByFilterFunc(view.folder.Filename(), view.gomodcache, view.Options()) - src, err := findWorkspaceModuleSource(ctx, view.folder, s, filterFunc, view.Options().ExperimentalWorkspaceModule) + // TODO(rfindley): can we avoid running the go command (go env) + // synchronously to change processing? Can we assume that the env did not + // change, and derive go.work using a combination of the configured + // GOWORK value and filesystem? + info, err := s.getWorkspaceInformation(ctx, view.folder, view.Options()) if err != nil { - return nil, nil, err + // Catastrophic failure, equivalent to a failure of session + // initialization and therefore should almost never happen. One + // scenario where this failure mode could occur is if some file + // permissions have changed preventing us from reading go.mod + // files. + // + // TODO(rfindley): consider surfacing this error more loudly. We + // could report a bug, but it's not really a bug. + event.Error(ctx, "fetching workspace information", err) } - if src != view.rootSrc { + + if info != view.workspaceInformation { _, err := s.updateViewLocked(ctx, view, view.Options()) if err != nil { - // Catastrophic failure, equivalent to a failure of session - // initialization and therefore should almost never happen. One - // scenario where this failure mode could occur is if some file - // permissions have changed preventing us from reading go.mod - // files. - // - // The view may or may not still exist. The best we can do is log - // and move on. - // - // TODO(rfindley): consider surfacing this error more loudly. We - // could report a bug, but it's not really a bug. + // More catastrophic failure. The view may or may not still exist. + // The best we can do is log and move on. event.Error(ctx, "recreating view", err) } } @@ -695,7 +676,7 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes func knownDirectories(ctx context.Context, snapshots []*snapshot) knownDirsSet { result := newKnownDirsSet() for _, snapshot := range snapshots { - dirs := snapshot.workspace.dirs(ctx, snapshot) + dirs := snapshot.dirs(ctx) for _, dir := range dirs { result.Insert(dir) } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 87f3e1c8f9b..b78962e15b1 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -26,9 +26,6 @@ import ( "sync/atomic" "unsafe" - "golang.org/x/mod/modfile" - "golang.org/x/mod/module" - "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/source" @@ -143,12 +140,6 @@ type snapshot struct { modWhyHandles *persistent.Map // from span.URI to *memoize.Promise[modWhyResult] modVulnHandles *persistent.Map // from span.URI to *memoize.Promise[modVulnResult] - workspace *workspace // (not guarded by mu) - - // The cached result of makeWorkspaceDir, created on demand and deleted by Snapshot.Destroy. - workspaceDir string - workspaceDirErr error - // knownSubdirs is the set of subdirectories in the workspace, used to // create glob patterns for file watching. knownSubdirs knownDirsSet @@ -157,6 +148,15 @@ type snapshot struct { // subdirectories in the workspace. They are not reflected to knownSubdirs // during the snapshot cloning step as it can slow down cloning. unprocessedSubdirChanges []*fileChange + + // workspaceModFiles holds the set of mod files active in this snapshot. + // + // This is either empty, a single entry for the workspace go.mod file, or the + // set of mod files used by the workspace go.work file. + // + // This set is immutable inside the snapshot, and therefore is not guarded by mu. + workspaceModFiles map[span.URI]struct{} + workspaceModFilesErr error // error encountered computing workspaceModFiles } var globalSnapshotID uint64 @@ -234,12 +234,6 @@ func (s *snapshot) destroy(destroyedBy string) { s.modTidyHandles.Destroy() s.modVulnHandles.Destroy() s.modWhyHandles.Destroy() - - if s.workspaceDir != "" { - if err := os.RemoveAll(s.workspaceDir); err != nil { - event.Error(context.Background(), "cleaning workspace dir", err) - } - } } func (s *snapshot) SequenceID() uint64 { @@ -264,14 +258,14 @@ func (s *snapshot) FileSet() *token.FileSet { func (s *snapshot) ModFiles() []span.URI { var uris []span.URI - for modURI := range s.workspace.ActiveModFiles() { + for modURI := range s.workspaceModFiles { uris = append(uris, modURI) } return uris } func (s *snapshot) WorkFile() span.URI { - return s.workspace.workFile + return s.view.effectiveGOWORK() } func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle { @@ -295,7 +289,7 @@ func (s *snapshot) ValidBuildConfiguration() bool { } // Check if the user is working within a module or if we have found // multiple modules in the workspace. - if len(s.workspace.ActiveModFiles()) > 0 { + if len(s.workspaceModFiles) > 0 { return true } // The user may have a multiple directories in their GOPATH. @@ -309,8 +303,37 @@ func (s *snapshot) ValidBuildConfiguration() bool { return false } +// moduleMode reports whether the current snapshot uses Go modules. +// +// From https://go.dev/ref/mod, module mode is active if either of the +// following hold: +// - GO111MODULE=on +// - GO111MODULE=auto and we are inside a module or have a GOWORK value. +// +// Additionally, this method returns false if GOPACKAGESDRIVER is set. +// +// TODO(rfindley): use this more widely. +func (s *snapshot) moduleMode() bool { + // Since we only really understand the `go` command, if the user has a + // different GOPACKAGESDRIVER, assume that their configuration is valid. + if s.view.hasGopackagesDriver { + return false + } + + switch s.view.effectiveGO111MODULE() { + case on: + return true + case off: + return false + default: + return len(s.workspaceModFiles) > 0 || s.view.gowork != "" + } +} + // workspaceMode describes the way in which the snapshot's workspace should // be loaded. +// +// TODO(rfindley): remove this, in favor of specific methods. func (s *snapshot) workspaceMode() workspaceMode { var mode workspaceMode @@ -323,7 +346,7 @@ func (s *snapshot) workspaceMode() workspaceMode { // If the view is not in a module and contains no modules, but still has a // valid workspace configuration, do not create the workspace module. // It could be using GOPATH or a different build system entirely. - if len(s.workspace.ActiveModFiles()) == 0 && validBuildConfiguration { + if len(s.workspaceModFiles) == 0 && validBuildConfiguration { return mode } mode |= moduleMode @@ -492,27 +515,8 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat // the main (workspace) module. Otherwise, we should use the module for // the passed-in working dir. if mode == source.LoadWorkspace { - switch s.workspace.moduleSource { - case legacyWorkspace: - for m := range s.workspace.ActiveModFiles() { // range to access the only element - modURI = m - } - case goWorkWorkspace: - if s.view.goversion >= 18 { - break - } - // Before go 1.18, the Go command did not natively support go.work files, - // so we 'fake' them with a workspace module. - fallthrough - case fileSystemWorkspace, goplsModWorkspace: - var tmpDir span.URI - var err error - tmpDir, err = s.getWorkspaceDir(ctx) - if err != nil { - return "", nil, cleanup, err - } - inv.WorkingDir = tmpDir.Filename() - modURI = span.URIFromPath(filepath.Join(tmpDir.Filename(), "go.mod")) + if s.view.effectiveGOWORK() == "" && s.view.gomod != "" { + modURI = s.view.gomod } } else { modURI = s.GoModForFile(span.URIFromPath(inv.WorkingDir)) @@ -540,6 +544,7 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat const mutableModFlag = "mod" // If the mod flag isn't set, populate it based on the mode and workspace. + // TODO(rfindley): this doesn't make sense if we're not in module mode if inv.ModFlag == "" { switch mode { case source.LoadWorkspace, source.Normal: @@ -570,8 +575,7 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat // example, if running go mod tidy in a go.work workspace) // // TODO(rfindley): this is very hard to follow. Refactor. - useWorkFile := !needTempMod && s.workspace.moduleSource == goWorkWorkspace && s.view.goversion >= 18 - if useWorkFile { + if !needTempMod && s.view.gowork != "" { // Since we're running in the workspace root, the go command will resolve GOWORK automatically. } else if useTempMod { if modURI == "" { @@ -593,25 +597,6 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return tmpURI, inv, cleanup, nil } -// usesWorkspaceDir reports whether the snapshot should use a synthetic -// workspace directory for running workspace go commands such as go list. -// -// TODO(rfindley): this logic is duplicated with goCommandInvocation. Clean up -// the latter, and deduplicate. -func (s *snapshot) usesWorkspaceDir() bool { - switch s.workspace.moduleSource { - case legacyWorkspace: - return false - case goWorkWorkspace: - if s.view.goversion >= 18 { - return false - } - // Before go 1.18, the Go command did not natively support go.work files, - // so we 'fake' them with a workspace module. - } - return true -} - func (s *snapshot) buildOverlay() map[string][]byte { s.mu.Lock() defer s.mu.Unlock() @@ -827,7 +812,7 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru } // Add a pattern for each Go module in the workspace that is not within the view. - dirs := s.workspace.dirs(ctx, s) + dirs := s.dirs(ctx) for _, dir := range dirs { dirName := dir.Filename() @@ -886,7 +871,7 @@ func (s *snapshot) getKnownSubdirsPattern(wsDirs []span.URI) string { // snapshot's workspace directories. None of the workspace directories are // included. func (s *snapshot) collectAllKnownSubdirs(ctx context.Context) { - dirs := s.workspace.dirs(ctx, s) + dirs := s.dirs(ctx) s.mu.Lock() defer s.mu.Unlock() @@ -1096,7 +1081,7 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[PackagePath]sourc // TODO(rfindley): clarify that this is only active modules. Or update to just // use findRootPattern. func (s *snapshot) GoModForFile(uri span.URI) span.URI { - return moduleForURI(s.workspace.activeModFiles, uri) + return moduleForURI(s.workspaceModFiles, uri) } func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { @@ -1249,8 +1234,12 @@ func (s *snapshot) awaitLoaded(ctx context.Context) error { } func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { - if wsErr := s.workspace.criticalError(ctx, s); wsErr != nil { - return wsErr + // If we couldn't compute workspace mod files, then the load below is + // invalid. + // + // TODO(rfindley): is this a clear error to present to the user? + if s.workspaceModFilesErr != nil { + return &source.CriticalError{MainError: s.workspaceModFilesErr} } loadErr := s.awaitLoadedAllErrors(ctx) @@ -1559,10 +1548,45 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC ctx, done := event.Start(ctx, "snapshot.clone") defer done() - newWorkspace, reinit := s.workspace.Clone(ctx, changes, &unappliedChanges{ - originalSnapshot: s, - changes: changes, - }) + reinit := false + wsModFiles, wsModFilesErr := s.workspaceModFiles, s.workspaceModFilesErr + + if workURI := s.view.effectiveGOWORK(); workURI != "" { + if change, ok := changes[workURI]; ok { + wsModFiles, wsModFilesErr = computeWorkspaceModFiles(ctx, s.view.gomod, workURI, s.view.effectiveGO111MODULE(), &unappliedChanges{ + originalSnapshot: s, + changes: changes, + }) + // TODO(rfindley): don't rely on 'isUnchanged' here. Use a content hash instead. + reinit = change.fileHandle.Saved() && !change.isUnchanged + } + } + + // Reinitialize if any workspace mod file has changed on disk. + for uri, change := range changes { + if _, ok := wsModFiles[uri]; ok && change.fileHandle.Saved() && !change.isUnchanged { + reinit = true + } + } + + // Finally, process sumfile changes that may affect loading. + for uri, change := range changes { + if !change.fileHandle.Saved() { + continue // like with go.mod files, we only reinit when things are saved + } + if filepath.Base(uri.Filename()) == "go.work.sum" && s.view.gowork != "" { + if filepath.Dir(uri.Filename()) == filepath.Dir(s.view.gowork) { + reinit = true + } + } + if filepath.Base(uri.Filename()) == "go.sum" { + dir := filepath.Dir(uri.Filename()) + modURI := span.URIFromPath(filepath.Join(dir, "go.mod")) + if _, active := wsModFiles[modURI]; active { + reinit = true + } + } + } s.mu.Lock() defer s.mu.Unlock() @@ -1606,7 +1630,8 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC modWhyHandles: s.modWhyHandles.Clone(), modVulnHandles: s.modVulnHandles.Clone(), knownSubdirs: s.knownSubdirs.Clone(), - workspace: newWorkspace, + workspaceModFiles: wsModFiles, + workspaceModFilesErr: wsModFilesErr, } // The snapshot should be initialized if either s was uninitialized, or we've @@ -2176,190 +2201,3 @@ func (s *snapshot) setBuiltin(path string) { s.builtin = span.URIFromPath(path) } - -// BuildGoplsMod generates a go.mod file for all modules in the workspace. It -// bypasses any existing gopls.mod. -func (s *snapshot) BuildGoplsMod(ctx context.Context) (*modfile.File, error) { - allModules, err := findModules(s.view.folder, pathExcludedByFilterFunc(s.view.folder.Filename(), s.view.gomodcache, s.View().Options()), 0) - if err != nil { - return nil, err - } - return buildWorkspaceModFile(ctx, allModules, s) -} - -// TODO(rfindley): move this to workspace.go -func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, fs source.FileSource) (*modfile.File, error) { - file := &modfile.File{} - file.AddModuleStmt("gopls-workspace") - // Track the highest Go version, to be set on the workspace module. - // Fall back to 1.12 -- old versions insist on having some version. - goVersion := "1.12" - - paths := map[string]span.URI{} - excludes := map[string][]string{} - var sortedModURIs []span.URI - for uri := range modFiles { - sortedModURIs = append(sortedModURIs, uri) - } - sort.Slice(sortedModURIs, func(i, j int) bool { - return sortedModURIs[i] < sortedModURIs[j] - }) - for _, modURI := range sortedModURIs { - fh, err := fs.GetFile(ctx, modURI) - if err != nil { - return nil, err - } - content, err := fh.Read() - if err != nil { - return nil, err - } - parsed, err := modfile.Parse(fh.URI().Filename(), content, nil) - if err != nil { - return nil, err - } - if file == nil || parsed.Module == nil { - return nil, fmt.Errorf("no module declaration for %s", modURI) - } - // Prepend "v" to go versions to make them valid semver. - if parsed.Go != nil && semver.Compare("v"+goVersion, "v"+parsed.Go.Version) < 0 { - goVersion = parsed.Go.Version - } - path := parsed.Module.Mod.Path - if seen, ok := paths[path]; ok { - return nil, fmt.Errorf("found module %q multiple times in the workspace, at:\n\t%q\n\t%q", path, seen, modURI) - } - paths[path] = modURI - // If the module's path includes a major version, we expect it to have - // a matching major version. - _, majorVersion, _ := module.SplitPathVersion(path) - if majorVersion == "" { - majorVersion = "/v0" - } - majorVersion = strings.TrimLeft(majorVersion, "/.") // handle gopkg.in versions - file.AddNewRequire(path, source.WorkspaceModuleVersion(majorVersion), false) - if err := file.AddReplace(path, "", span.Dir(modURI).Filename(), ""); err != nil { - return nil, err - } - for _, exclude := range parsed.Exclude { - excludes[exclude.Mod.Path] = append(excludes[exclude.Mod.Path], exclude.Mod.Version) - } - } - if goVersion != "" { - file.AddGoStmt(goVersion) - } - // Go back through all of the modules to handle any of their replace - // statements. - for _, modURI := range sortedModURIs { - fh, err := fs.GetFile(ctx, modURI) - if err != nil { - return nil, err - } - content, err := fh.Read() - if err != nil { - return nil, err - } - parsed, err := modfile.Parse(fh.URI().Filename(), content, nil) - if err != nil { - return nil, err - } - // If any of the workspace modules have replace directives, they need - // to be reflected in the workspace module. - for _, rep := range parsed.Replace { - // Don't replace any modules that are in our workspace--we should - // always use the version in the workspace. - if _, ok := paths[rep.Old.Path]; ok { - continue - } - newPath := rep.New.Path - newVersion := rep.New.Version - // If a replace points to a module in the workspace, make sure we - // direct it to version of the module in the workspace. - if m, ok := paths[rep.New.Path]; ok { - newPath = span.Dir(m).Filename() - newVersion = "" - } else if rep.New.Version == "" && !filepath.IsAbs(rep.New.Path) { - // Make any relative paths absolute. - newPath = filepath.Join(span.Dir(modURI).Filename(), rep.New.Path) - } - if err := file.AddReplace(rep.Old.Path, rep.Old.Version, newPath, newVersion); err != nil { - return nil, err - } - } - } - for path, versions := range excludes { - for _, version := range versions { - file.AddExclude(path, version) - } - } - file.SortBlocks() - return file, nil -} - -func buildWorkspaceSumFile(ctx context.Context, modFiles map[span.URI]struct{}, fs source.FileSource) ([]byte, error) { - allSums := map[module.Version][]string{} - for modURI := range modFiles { - // TODO(rfindley): factor out this pattern into a uripath package. - sumURI := span.URIFromPath(filepath.Join(filepath.Dir(modURI.Filename()), "go.sum")) - fh, err := fs.GetFile(ctx, sumURI) - if err != nil { - continue - } - data, err := fh.Read() - if os.IsNotExist(err) { - continue - } - if err != nil { - return nil, fmt.Errorf("reading go sum: %w", err) - } - if err := readGoSum(allSums, sumURI.Filename(), data); err != nil { - return nil, err - } - } - // This logic to write go.sum is copied (with minor modifications) from - // https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/modfetch/fetch.go;l=631;drc=762eda346a9f4062feaa8a9fc0d17d72b11586f0 - var mods []module.Version - for m := range allSums { - mods = append(mods, m) - } - module.Sort(mods) - - var buf bytes.Buffer - for _, m := range mods { - list := allSums[m] - sort.Strings(list) - // Note (rfindley): here we add all sum lines without verification, because - // the assumption is that if they come from a go.sum file, they are - // trusted. - for _, h := range list { - fmt.Fprintf(&buf, "%s %s %s\n", m.Path, m.Version, h) - } - } - return buf.Bytes(), nil -} - -// readGoSum is copied (with minor modifications) from -// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/modfetch/fetch.go;l=398;drc=762eda346a9f4062feaa8a9fc0d17d72b11586f0 -func readGoSum(dst map[module.Version][]string, file string, data []byte) error { - lineno := 0 - for len(data) > 0 { - var line []byte - lineno++ - i := bytes.IndexByte(data, '\n') - if i < 0 { - line, data = data, nil - } else { - line, data = data[:i], data[i+1:] - } - f := strings.Fields(string(line)) - if len(f) == 0 { - // blank line; skip it - continue - } - if len(f) != 3 { - return fmt.Errorf("malformed go.sum:\n%s:%d: wrong number of fields %v", file, lineno, len(f)) - } - mod := module.Version{Path: f[0], Version: f[1]} - dst[mod] = append(dst[mod], f[2]) - } - return nil -} diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 5703b3086cc..a4c87c3dddc 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -54,7 +54,6 @@ type View struct { // options define the build list. Any change to these fields results in a new // View. folder span.URI // user-specified workspace folder - rootSrc span.URI // file providing module information (go.mod or go.work); may be empty workspaceInformation // Go environment information importsState *importsState @@ -109,10 +108,16 @@ type View struct { initializationSema chan struct{} } +// workspaceInformation holds the defining features of the View workspace. +// +// This type is compared to see if the View needs to be reconstructed. type workspaceInformation struct { // `go env` variables that need to be tracked by gopls. goEnv + // gomod holds the relevant go.mod file for this workspace. + gomod span.URI + // The Go version in use: X in Go 1.X. goversion int @@ -528,16 +533,14 @@ func (v *View) relevantChange(c source.FileModification) bool { if v.knownFile(c.URI) { return true } - // The go.work/gopls.mod may not be "known" because we first access it - // through the session. As a result, treat changes to the view's go.work or - // gopls.mod file as always relevant, even if they are only on-disk - // changes. - // TODO(rstambler): Make sure the go.work/gopls.mod files are always known + // The go.work file may not be "known" because we first access it through the + // session. As a result, treat changes to the view's go.work file as always + // relevant, even if they are only on-disk changes. + // + // TODO(rfindley): Make sure the go.work files are always known // to the view. - for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - if c.URI == uriForSource(v.workingDir(), v.effectiveGOWORK(), src) { - return true - } + if c.URI == v.effectiveGOWORK() { + return true } // Note: CL 219202 filtered out on-disk changes here that were not known to @@ -633,20 +636,19 @@ func (v *View) shutdown() { } v.snapshotMu.Unlock() - v.importsState.destroy() v.snapshotWG.Wait() } func (s *snapshot) IgnoredFile(uri span.URI) bool { filename := uri.Filename() var prefixes []string - if len(s.workspace.ActiveModFiles()) == 0 { + if len(s.workspaceModFiles) == 0 { for _, entry := range filepath.SplitList(s.view.gopath) { prefixes = append(prefixes, filepath.Join(entry, "src")) } } else { prefixes = append(prefixes, s.view.gomodcache) - for m := range s.workspace.ActiveModFiles() { + for m := range s.workspaceModFiles { prefixes = append(prefixes, span.Dir(m).Filename()) } } @@ -749,8 +751,8 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadEr }) } - if len(s.workspace.ActiveModFiles()) > 0 { - for modURI := range s.workspace.ActiveModFiles() { + if len(s.workspaceModFiles) > 0 { + for modURI := range s.workspaceModFiles { // Be careful not to add context cancellation errors as critical module // errors. fh, err := s.GetFile(ctx, modURI) @@ -887,39 +889,33 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, tool, _ := exec.LookPath("gopackagesdriver") info.hasGopackagesDriver = gopackagesdriver != "off" && (gopackagesdriver != "" || tool != "") + // filterFunc is the path filter function for this workspace folder. Notably, + // it is relative to folder (which is specified by the user), not root. + filterFunc := pathExcludedByFilterFunc(folder.Filename(), info.gomodcache, options) + info.gomod, err = findWorkspaceModFile(ctx, folder, s, filterFunc) + if err != nil { + return info, err + } + return info, nil } -// findWorkspaceModuleSource searches for a "module source" relative to the -// given folder URI. A module source is the go.work or go.mod file that -// provides module information. -// -// As a special case, this function returns a module source in a nested -// directory if it finds no other module source, and exactly one nested module. -// -// If no module source is found, it returns "". -func findWorkspaceModuleSource(ctx context.Context, folderURI span.URI, fs source.FileSource, excludePath func(string) bool, experimental bool) (span.URI, error) { - patterns := []string{"go.work", "go.mod"} - if experimental { - patterns = []string{"go.work", "gopls.mod", "go.mod"} - } +// findWorkspaceModFile searches for a single go.mod file relative to the given +// folder URI, using the following algorithm: +// 1. if there is a go.mod file in a parent directory, return it +// 2. else, if there is exactly one nested module, return it +// 3. else, return "" +func findWorkspaceModFile(ctx context.Context, folderURI span.URI, fs source.FileSource, excludePath func(string) bool) (span.URI, error) { folder := folderURI.Filename() - for _, basename := range patterns { - match, err := findRootPattern(ctx, folder, basename, fs) - if err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return "", ctxErr - } - return "", err - } - if match != "" { - return span.URIFromPath(match), nil + match, err := findRootPattern(ctx, folder, "go.mod", fs) + if err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return "", ctxErr } + return "", err } - - // The experimental workspace can handle nested modules at this point... - if experimental { - return "", nil + if match != "" { + return span.URIFromPath(match), nil } // ...else we should check if there's exactly one nested module. @@ -948,9 +944,14 @@ func findWorkspaceModuleSource(ctx context.Context, folderURI span.URI, fs sourc // a singular nested module. In that case, the go command won't be able to find // the module unless we tell it the nested directory. func (v *View) workingDir() span.URI { - // TODO(golang/go#57514): eliminate the expandWorkspaceToModule setting. - if v.Options().ExpandWorkspaceToModule && v.rootSrc != "" { - return span.Dir(v.rootSrc) + // Note: if gowork is in use, this will default to the workspace folder. In + // the past, we would instead use the folder containing go.work. This should + // not make a difference, and in fact may improve go list error messages. + // + // TODO(golang/go#57514): eliminate the expandWorkspaceToModule setting + // entirely. + if v.Options().ExpandWorkspaceToModule && v.gomod != "" { + return span.Dir(v.gomod) } return v.folder } diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go index 617dc31a0e7..8adfbfaf0f8 100644 --- a/gopls/internal/lsp/cache/view_test.go +++ b/gopls/internal/lsp/cache/view_test.go @@ -53,7 +53,7 @@ func TestCaseInsensitiveFilesystem(t *testing.T) { } } -func TestFindWorkspaceModuleSource(t *testing.T) { +func TestFindWorkspaceModFile(t *testing.T) { workspace := ` -- a/go.mod -- module a @@ -71,10 +71,6 @@ module d-goplsworkspace module de -- f/g/go.mod -- module fg --- h/go.work -- -go 1.18 --- h/i/go.mod -- -module hi ` dir, err := fake.Tempdir(fake.UnpackTxt(workspace)) if err != nil { @@ -84,21 +80,15 @@ module hi tests := []struct { folder, want string - experimental bool }{ - {"", "", false}, // no module at root, and more than one nested module - {"a", "a/go.mod", false}, - {"a/x", "a/go.mod", false}, - {"a/x/y", "a/go.mod", false}, - {"b/c", "b/c/go.mod", false}, - {"d", "d/e/go.mod", false}, - {"d", "d/gopls.mod", true}, - {"d/e", "d/e/go.mod", false}, - {"d/e", "d/gopls.mod", true}, - {"f", "f/g/go.mod", false}, - {"f", "", true}, - {"h", "h/go.work", false}, - {"h/i", "h/go.work", false}, + {"", ""}, // no module at root, and more than one nested module + {"a", "a/go.mod"}, + {"a/x", "a/go.mod"}, + {"a/x/y", "a/go.mod"}, + {"b/c", "b/c/go.mod"}, + {"d", "d/e/go.mod"}, + {"d/e", "d/e/go.mod"}, + {"f", "f/g/go.mod"}, } for _, test := range tests { @@ -106,7 +96,7 @@ module hi rel := fake.RelativeTo(dir) folderURI := span.URIFromPath(rel.AbsPath(test.folder)) excludeNothing := func(string) bool { return false } - got, err := findWorkspaceModuleSource(ctx, folderURI, &osFileSource{}, excludeNothing, test.experimental) + got, err := findWorkspaceModFile(ctx, folderURI, &osFileSource{}, excludeNothing) if err != nil { t.Fatal(err) } @@ -115,7 +105,7 @@ module hi want = span.URIFromPath(rel.AbsPath(test.want)) } if got != want { - t.Errorf("findWorkspaceModuleSource(%q, %t) = %q, want %q", test.folder, test.experimental, got, want) + t.Errorf("findWorkspaceModFile(%q) = %q, want %q", test.folder, got, want) } } } diff --git a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go index da2abdb68b4..e9845e89b23 100644 --- a/gopls/internal/lsp/cache/workspace.go +++ b/gopls/internal/lsp/cache/workspace.go @@ -12,476 +12,85 @@ import ( "path/filepath" "sort" "strings" - "sync" "golang.org/x/mod/modfile" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/xcontext" ) -// workspaceSource reports how the set of active modules has been derived. -type workspaceSource int +// TODO(rfindley): now that experimentalWorkspaceModule is gone, this file can +// be massively cleaned up and/or removed. -const ( - legacyWorkspace = iota // non-module or single module mode - goplsModWorkspace // modules provided by a gopls.mod file - goWorkWorkspace // modules provided by a go.work file - fileSystemWorkspace // modules found by walking the filesystem -) - -func (s workspaceSource) String() string { - switch s { - case legacyWorkspace: - return "legacy" - case goplsModWorkspace: - return "gopls.mod" - case goWorkWorkspace: - return "go.work" - case fileSystemWorkspace: - return "file system" - default: - return "!(unknown module source)" - } -} - -// workspaceCommon holds immutable information about the workspace setup. -// -// TODO(rfindley): there is some redundancy here with workspaceInformation. -// Reconcile these two types. -type workspaceCommon struct { - root span.URI - excludePath func(string) bool - - // explicitGowork is, if non-empty, the URI for the explicit go.work file - // provided via the user's environment. - explicitGowork span.URI -} - -// workspace tracks go.mod files in the workspace, along with the -// gopls.mod file, to provide support for multi-module workspaces. -// -// Specifically, it provides: -// - the set of modules contained within in the workspace root considered to -// be 'active' -// - the workspace modfile, to be used for the go command `-modfile` flag -// - the set of workspace directories -// -// This type is immutable (or rather, idempotent), so that it may be shared -// across multiple snapshots. -type workspace struct { - workspaceCommon - - // The source of modules in this workspace. - moduleSource workspaceSource - - // activeModFiles holds the active go.mod files. - activeModFiles map[span.URI]struct{} - - // knownModFiles holds the set of all go.mod files in the workspace. - // In all modes except for legacy, this is equivalent to modFiles. - knownModFiles map[span.URI]struct{} - - // workFile, if nonEmpty, is the go.work file for the workspace. - workFile span.URI - - // The workspace module is lazily re-built once after being invalidated. - // buildMu+built guards this reconstruction. - // - // file and wsDirs may be non-nil even if built == false, if they were copied - // from the previous workspace module version. In this case, they will be - // preserved if building fails. - buildMu sync.Mutex - built bool - buildErr error - mod *modfile.File - sum []byte - wsDirs map[span.URI]struct{} -} - -// newWorkspace creates a new workspace at the given root directory, -// determining its module source based on the presence of a gopls.mod or -// go.work file, and the go111moduleOff and useWsModule settings. -// -// If useWsModule is set, the workspace may use a synthetic mod file replacing -// all modules in the root. -// -// If there is no active workspace file (a gopls.mod or go.work), newWorkspace -// scans the filesystem to find modules. -// -// TODO(rfindley): newWorkspace should perhaps never fail, relying instead on -// the criticalError method to surface problems in the workspace. -func newWorkspace(ctx context.Context, root, explicitGowork span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff, useWsModule bool) (*workspace, error) { - ws := &workspace{ - workspaceCommon: workspaceCommon{ - root: root, - explicitGowork: explicitGowork, - excludePath: excludePath, - }, - } - - // The user may have a gopls.mod or go.work file that defines their - // workspace. - // - // TODO(rfindley): if GO111MODULE=off, this looks wrong, though there are - // probably other problems. - if err := ws.loadExplicitWorkspaceFile(ctx, fs); err == nil { - return ws, nil - } - - // Otherwise, in all other modes, search for all of the go.mod files in the - // workspace. - knownModFiles, err := findModules(root, excludePath, 0) - if err != nil { - return nil, err +// computeWorkspaceModFiles computes the set of workspace mod files based on the +// value of go.mod, go.work, and GO111MODULE. +func computeWorkspaceModFiles(ctx context.Context, gomod, gowork span.URI, go111module go111module, fs source.FileSource) (map[span.URI]struct{}, error) { + if go111module == off { + return nil, nil } - ws.knownModFiles = knownModFiles - - switch { - case go111moduleOff: - ws.moduleSource = legacyWorkspace - case useWsModule: - ws.activeModFiles = knownModFiles - ws.moduleSource = fileSystemWorkspace - default: - ws.moduleSource = legacyWorkspace - activeModFiles, err := getLegacyModules(ctx, root, fs) + if gowork != "" { + fh, err := fs.GetFile(ctx, gowork) if err != nil { return nil, err } - ws.activeModFiles = activeModFiles - } - return ws, nil -} - -// loadExplicitWorkspaceFile loads workspace information from go.work or -// gopls.mod files, setting the active modules, mod file, and module source -// accordingly. -func (ws *workspace) loadExplicitWorkspaceFile(ctx context.Context, fs source.FileSource) error { - for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - fh, err := fs.GetFile(ctx, uriForSource(ws.root, ws.explicitGowork, src)) - if err != nil { - return err - } - contents, err := fh.Read() + content, err := fh.Read() if err != nil { - continue // TODO(rfindley): is it correct to proceed here? - } - var file *modfile.File - var activeModFiles map[span.URI]struct{} - switch src { - case goWorkWorkspace: - file, activeModFiles, err = parseGoWork(ctx, ws.root, fh.URI(), contents, fs) - ws.workFile = fh.URI() - case goplsModWorkspace: - file, activeModFiles, err = parseGoplsMod(ws.root, fh.URI(), contents) + return nil, err } + filename := gowork.Filename() + dir := filepath.Dir(filename) + workFile, err := modfile.ParseWork(filename, content, nil) if err != nil { - ws.buildMu.Lock() - ws.built = true - ws.buildErr = err - ws.buildMu.Unlock() + return nil, fmt.Errorf("parsing go.work: %w", err) } - ws.mod = file - ws.activeModFiles = activeModFiles - ws.moduleSource = src - return nil - } - return noHardcodedWorkspace -} - -var noHardcodedWorkspace = errors.New("no hardcoded workspace") - -// TODO(rfindley): eliminate getKnownModFiles. -func (w *workspace) getKnownModFiles() map[span.URI]struct{} { - return w.knownModFiles -} - -// ActiveModFiles returns the set of active mod files for the current workspace. -func (w *workspace) ActiveModFiles() map[span.URI]struct{} { - return w.activeModFiles -} - -// criticalError returns a critical error related to the workspace setup. -func (w *workspace) criticalError(ctx context.Context, fs source.FileSource) (res *source.CriticalError) { - // For now, we narrowly report errors related to `go.work` files. - // - // TODO(rfindley): investigate whether other workspace validation errors - // can be consolidated here. - if w.moduleSource == goWorkWorkspace { - // We should have already built the modfile, but build here to be - // consistent about accessing w.mod after w.build. - // - // TODO(rfindley): build eagerly. Building lazily is a premature - // optimization that poses a significant burden on the code. - w.build(ctx, fs) - if w.buildErr != nil { - return &source.CriticalError{ - MainError: w.buildErr, + modFiles := make(map[span.URI]struct{}) + for _, use := range workFile.Use { + modDir := filepath.FromSlash(use.Path) + if !filepath.IsAbs(modDir) { + modDir = filepath.Join(dir, modDir) } + modURI := span.URIFromPath(filepath.Join(modDir, "go.mod")) + modFiles[modURI] = struct{}{} } + return modFiles, nil + } + if gomod != "" { + return map[span.URI]struct{}{gomod: {}}, nil } - return nil + return nil, nil } -// modFile gets the workspace modfile associated with this workspace, -// computing it if it doesn't exist. +// dirs returns the workspace directories for the loaded modules. // -// A fileSource must be passed in to solve a chicken-egg problem: it is not -// correct to pass in the snapshot file source to newWorkspace when -// invalidating, because at the time these are called the snapshot is locked. -// So we must pass it in later on when actually using the modFile. -func (w *workspace) modFile(ctx context.Context, fs source.FileSource) (*modfile.File, error) { - w.build(ctx, fs) - return w.mod, w.buildErr -} - -func (w *workspace) sumFile(ctx context.Context, fs source.FileSource) ([]byte, error) { - w.build(ctx, fs) - return w.sum, w.buildErr -} - -func (w *workspace) build(ctx context.Context, fs source.FileSource) { - w.buildMu.Lock() - defer w.buildMu.Unlock() - - if w.built { - return - } - // Building should never be cancelled. Since the workspace module is shared - // across multiple snapshots, doing so would put us in a bad state, and it - // would not be obvious to the user how to recover. - ctx = xcontext.Detach(ctx) - - // If the module source is from the filesystem, try to build the workspace - // module from active modules discovered by scanning the filesystem. Fall - // back on the pre-existing mod file if parsing fails. - if w.moduleSource == fileSystemWorkspace { - file, err := buildWorkspaceModFile(ctx, w.activeModFiles, fs) - switch { - case err == nil: - w.mod = file - case w.mod != nil: - // Parsing failed, but we have a previous file version. - event.Error(ctx, "building workspace mod file", err) - default: - // No file to fall back on. - w.buildErr = err - } - } +// A workspace directory is, roughly speaking, a directory for which we care +// about file changes. This is used for the purpose of registering file +// watching patterns, and expanding directory modifications to their adjacent +// files. +// +// TODO(rfindley): move this to snapshot.go. +// TODO(rfindley): can we make this abstraction simpler and/or more accurate? +func (s *snapshot) dirs(ctx context.Context) []span.URI { + dirSet := make(map[span.URI]struct{}) - if w.mod != nil { - w.wsDirs = map[span.URI]struct{}{ - w.root: {}, - } - for _, r := range w.mod.Replace { - // We may be replacing a module with a different version, not a path - // on disk. - if r.New.Version != "" { - continue - } - w.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{} - } - } + // Dirs should, at the very least, contain the working directory and folder. + dirSet[s.view.workingDir()] = struct{}{} + dirSet[s.view.folder] = struct{}{} - // Ensure that there is always at least the root dir. - if len(w.wsDirs) == 0 { - w.wsDirs = map[span.URI]struct{}{ - w.root: {}, + // Additionally, if e.g. go.work indicates other workspace modules, we should + // include their directories too. + if s.workspaceModFilesErr == nil { + for modFile := range s.workspaceModFiles { + dir := filepath.Dir(modFile.Filename()) + dirSet[span.URIFromPath(dir)] = struct{}{} } } - - sum, err := buildWorkspaceSumFile(ctx, w.activeModFiles, fs) - if err == nil { - w.sum = sum - } else { - event.Error(ctx, "building workspace sum file", err) - } - - w.built = true -} - -// dirs returns the workspace directories for the loaded modules. -func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI { - w.build(ctx, fs) var dirs []span.URI - for d := range w.wsDirs { + for d := range dirSet { dirs = append(dirs, d) } sort.Slice(dirs, func(i, j int) bool { return dirs[i] < dirs[j] }) return dirs } -// Clone returns a (possibly) new workspace after invalidating the changed -// files. If w is still valid in the presence of changedURIs, it returns itself -// unmodified. -// -// The returned needReinit flag indicates to the caller that the workspace -// needs to be reinitialized (because a relevant go.mod or go.work file has -// been changed). -// -// TODO(rfindley): it looks wrong that we return 'needReinit' here. The caller -// should determine whether to re-initialize.. -func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, fs source.FileSource) (_ *workspace, needReinit bool) { - // Prevent races to w.modFile or w.wsDirs below, if w has not yet been built. - w.buildMu.Lock() - defer w.buildMu.Unlock() - - // Clone the workspace. This may be discarded if nothing changed. - changed := false - result := &workspace{ - workspaceCommon: w.workspaceCommon, - moduleSource: w.moduleSource, - knownModFiles: make(map[span.URI]struct{}), - activeModFiles: make(map[span.URI]struct{}), - workFile: w.workFile, - mod: w.mod, - sum: w.sum, - wsDirs: w.wsDirs, - } - for k, v := range w.knownModFiles { - result.knownModFiles[k] = v - } - for k, v := range w.activeModFiles { - result.activeModFiles[k] = v - } - - equalURI := func(a, b span.URI) (r bool) { - // This query is a strange mix of syntax and file system state: - // deletion of a file causes a false result if the name doesn't change. - // Our tests exercise only the first clause. - return a == b || span.SameExistingFile(a, b) - } - - // First handle changes to the go.work or gopls.mod file. This must be - // considered before any changes to go.mod or go.sum files, as these files - // determine which modules we care about. If go.work/gopls.mod has changed - // we need to either re-read it if it exists or walk the filesystem if it - // has been deleted. go.work should override the gopls.mod if both exist. - changed, needReinit = handleWorkspaceFileChanges(ctx, result, changes, fs) - // Next, handle go.mod changes that could affect our workspace. - for uri, change := range changes { - // Otherwise, we only care about go.mod files in the workspace directory. - if change.isUnchanged || !isGoMod(uri) || !source.InDir(result.root.Filename(), uri.Filename()) { - continue - } - changed = true - active := result.moduleSource != legacyWorkspace || equalURI(modURI(w.root), uri) - needReinit = needReinit || (active && change.fileHandle.Saved()) - // Don't mess with the list of mod files if using go.work or gopls.mod. - if result.moduleSource == goplsModWorkspace || result.moduleSource == goWorkWorkspace { - continue - } - if change.exists { - result.knownModFiles[uri] = struct{}{} - if active { - result.activeModFiles[uri] = struct{}{} - } - } else { - delete(result.knownModFiles, uri) - delete(result.activeModFiles, uri) - } - } - - // Finally, process go.sum changes for any modules that are now active. - for uri, change := range changes { - if !isGoSum(uri) { - continue - } - // TODO(rFindley) factor out this URI mangling. - dir := filepath.Dir(uri.Filename()) - modURI := span.URIFromPath(filepath.Join(dir, "go.mod")) - if _, active := result.activeModFiles[modURI]; !active { - continue - } - // Only changes to active go.sum files actually cause the workspace to - // change. - changed = true - needReinit = needReinit || change.fileHandle.Saved() - } - - if !changed { - return w, false - } - - return result, needReinit -} - -// handleWorkspaceFileChanges handles changes related to a go.work or gopls.mod -// file, updating ws accordingly. ws.root must be set. -func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[span.URI]*fileChange, fs source.FileSource) (changed, reload bool) { - if ws.moduleSource != goWorkWorkspace && ws.moduleSource != goplsModWorkspace { - return false, false - } - - uri := uriForSource(ws.root, ws.explicitGowork, ws.moduleSource) - // File opens/closes are just no-ops. - change, ok := changes[uri] - if !ok || change.isUnchanged { - return false, false - } - if change.exists { - // Only invalidate if the file if it actually parses. - // Otherwise, stick with the current file. - var parsedFile *modfile.File - var parsedModules map[span.URI]struct{} - var err error - switch ws.moduleSource { - case goWorkWorkspace: - parsedFile, parsedModules, err = parseGoWork(ctx, ws.root, uri, change.content, fs) - case goplsModWorkspace: - parsedFile, parsedModules, err = parseGoplsMod(ws.root, uri, change.content) - } - if err != nil { - // An unparseable file should not invalidate the workspace: - // nothing good could come from changing the workspace in - // this case. - // - // TODO(rfindley): well actually, it could potentially lead to a better - // critical error. Evaluate whether we can unify this case with the - // error returned by newWorkspace, without needlessly invalidating - // metadata. - event.Error(ctx, fmt.Sprintf("parsing %s", filepath.Base(uri.Filename())), err) - } else { - // only update the modfile if it parsed. - changed = true - reload = change.fileHandle.Saved() - ws.mod = parsedFile - ws.knownModFiles = parsedModules - ws.activeModFiles = make(map[span.URI]struct{}) - for k, v := range parsedModules { - ws.activeModFiles[k] = v - } - } - return changed, reload - } - // go.work/gopls.mod is deleted. We should never see this as the view should have been recreated. - panic(fmt.Sprintf("internal error: workspace file %q deleted without reinitialization", uri)) -} - -// goplsModURI returns the URI for the gopls.mod file contained in root. -func uriForSource(root, explicitGowork span.URI, src workspaceSource) span.URI { - var basename string - switch src { - case goplsModWorkspace: - basename = "gopls.mod" - case goWorkWorkspace: - if explicitGowork != "" { - return explicitGowork - } - basename = "go.work" - default: - return "" - } - return span.URIFromPath(filepath.Join(root.Filename(), basename)) -} - -// modURI returns the URI for the go.mod file contained in root. -func modURI(root span.URI) span.URI { - return span.URIFromPath(filepath.Join(root.Filename(), "go.mod")) -} - // isGoMod reports if uri is a go.mod file. func isGoMod(uri span.URI) bool { return filepath.Base(uri.Filename()) == "go.mod" @@ -492,11 +101,6 @@ func isGoWork(uri span.URI) bool { return filepath.Base(uri.Filename()) == "go.work" } -// isGoSum reports if uri is a go.sum or go.work.sum file. -func isGoSum(uri span.URI) bool { - return filepath.Base(uri.Filename()) == "go.sum" || filepath.Base(uri.Filename()) == "go.work.sum" -} - // fileExists reports if the file uri exists within source. func fileExists(ctx context.Context, uri span.URI, source source.FileSource) (bool, error) { fh, err := source.GetFile(ctx, uri) @@ -518,80 +122,6 @@ func fileHandleExists(fh source.FileHandle) (bool, error) { return false, err } -// getLegacyModules returns a module set containing at most the root module. -func getLegacyModules(ctx context.Context, root span.URI, fs source.FileSource) (map[span.URI]struct{}, error) { - uri := span.URIFromPath(filepath.Join(root.Filename(), "go.mod")) - modules := make(map[span.URI]struct{}) - exists, err := fileExists(ctx, uri, fs) - if err != nil { - return nil, err - } - if exists { - modules[uri] = struct{}{} - } - return modules, nil -} - -func parseGoWork(ctx context.Context, root, uri span.URI, contents []byte, fs source.FileSource) (*modfile.File, map[span.URI]struct{}, error) { - workFile, err := modfile.ParseWork(uri.Filename(), contents, nil) - if err != nil { - return nil, nil, fmt.Errorf("parsing go.work: %w", err) - } - modFiles := make(map[span.URI]struct{}) - for _, dir := range workFile.Use { - // The resulting modfile must use absolute paths, so that it can be - // written to a temp directory. - dir.Path = absolutePath(root, dir.Path) - modURI := span.URIFromPath(filepath.Join(dir.Path, "go.mod")) - modFiles[modURI] = struct{}{} - } - - // TODO(rfindley): we should either not build the workspace modfile here, or - // not fail so hard. A failure in building the workspace modfile should not - // invalidate the active module paths extracted above. - modFile, err := buildWorkspaceModFile(ctx, modFiles, fs) - if err != nil { - return nil, nil, err - } - - // Require a go directive, per the spec. - if workFile.Go == nil || workFile.Go.Version == "" { - return nil, nil, fmt.Errorf("go.work has missing or incomplete go directive") - } - if err := modFile.AddGoStmt(workFile.Go.Version); err != nil { - return nil, nil, err - } - - return modFile, modFiles, nil -} - -func parseGoplsMod(root, uri span.URI, contents []byte) (*modfile.File, map[span.URI]struct{}, error) { - modFile, err := modfile.Parse(uri.Filename(), contents, nil) - if err != nil { - return nil, nil, fmt.Errorf("parsing gopls.mod: %w", err) - } - modFiles := make(map[span.URI]struct{}) - for _, replace := range modFile.Replace { - if replace.New.Version != "" { - return nil, nil, fmt.Errorf("gopls.mod: replaced module %q@%q must not have version", replace.New.Path, replace.New.Version) - } - // The resulting modfile must use absolute paths, so that it can be - // written to a temp directory. - replace.New.Path = absolutePath(root, replace.New.Path) - modURI := span.URIFromPath(filepath.Join(replace.New.Path, "go.mod")) - modFiles[modURI] = struct{}{} - } - return modFile, modFiles, nil -} - -func absolutePath(root span.URI, path string) string { - dirFP := filepath.FromSlash(path) - if !filepath.IsAbs(dirFP) { - dirFP = filepath.Join(root.Filename(), dirFP) - } - return dirFP -} - // errExhausted is returned by findModules if the file scan limit is reached. var errExhausted = errors.New("exhausted") diff --git a/gopls/internal/lsp/cache/workspace_test.go b/gopls/internal/lsp/cache/workspace_test.go index 45ae0cc3432..5f1e13ef124 100644 --- a/gopls/internal/lsp/cache/workspace_test.go +++ b/gopls/internal/lsp/cache/workspace_test.go @@ -6,13 +6,8 @@ package cache import ( "context" - "errors" "os" - "strings" - "testing" - "golang.org/x/mod/modfile" - "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" ) @@ -89,278 +84,3 @@ func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHa } return fh, nil } - -type wsState struct { - source workspaceSource - modules []string - dirs []string - sum string -} - -type wsChange struct { - content string - saved bool -} - -func TestWorkspaceModule(t *testing.T) { - tests := []struct { - desc string - initial string // txtar-encoded - legacyMode bool - initialState wsState - updates map[string]wsChange - wantChanged bool - wantReload bool - finalState wsState - }{ - { - desc: "legacy mode", - initial: ` --- go.mod -- -module mod.com --- go.sum -- -golang.org/x/mod v0.3.0 h1:deadbeef --- a/go.mod -- -module moda.com`, - legacyMode: true, - initialState: wsState{ - modules: []string{"./go.mod"}, - source: legacyWorkspace, - dirs: []string{"."}, - sum: "golang.org/x/mod v0.3.0 h1:deadbeef\n", - }, - }, - { - desc: "nested module", - initial: ` --- go.mod -- -module mod.com --- a/go.mod -- -module moda.com`, - initialState: wsState{ - modules: []string{"./go.mod", "a/go.mod"}, - source: fileSystemWorkspace, - dirs: []string{".", "a"}, - }, - }, - { - desc: "adding module", - initial: ` --- gopls.mod -- -require moda.com v0.0.0-goplsworkspace -replace moda.com => $SANDBOX_WORKDIR/a --- a/go.mod -- -module moda.com --- b/go.mod -- -module modb.com`, - initialState: wsState{ - modules: []string{"a/go.mod"}, - source: goplsModWorkspace, - dirs: []string{".", "a"}, - }, - updates: map[string]wsChange{ - "gopls.mod": {`module gopls-workspace - -require moda.com v0.0.0-goplsworkspace -require modb.com v0.0.0-goplsworkspace - -replace moda.com => $SANDBOX_WORKDIR/a -replace modb.com => $SANDBOX_WORKDIR/b`, true}, - }, - wantChanged: true, - wantReload: true, - finalState: wsState{ - modules: []string{"a/go.mod", "b/go.mod"}, - source: goplsModWorkspace, - dirs: []string{".", "a", "b"}, - }, - }, - { - desc: "broken module parsing", - initial: ` --- a/go.mod -- -module moda.com - -require gopls.test v0.0.0-goplsworkspace -replace gopls.test => ../../gopls.test // (this path shouldn't matter) --- b/go.mod -- -module modb.com`, - initialState: wsState{ - modules: []string{"a/go.mod", "b/go.mod"}, - source: fileSystemWorkspace, - dirs: []string{".", "a", "b", "../gopls.test"}, - }, - updates: map[string]wsChange{ - "a/go.mod": {`modul moda.com - -require gopls.test v0.0.0-goplsworkspace -replace gopls.test => ../../gopls.test2`, false}, - }, - wantChanged: true, - wantReload: false, - finalState: wsState{ - modules: []string{"a/go.mod", "b/go.mod"}, - source: fileSystemWorkspace, - // finalDirs should be unchanged: we should preserve dirs in the presence - // of a broken modfile. - dirs: []string{".", "a", "b", "../gopls.test"}, - }, - }, - } - - for _, test := range tests { - t.Run(test.desc, func(t *testing.T) { - ctx := context.Background() - dir, err := fake.Tempdir(fake.UnpackTxt(test.initial)) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - root := span.URIFromPath(dir) - - fs := &osFileSource{} - excludeNothing := func(string) bool { return false } - w, err := newWorkspace(ctx, root, "", fs, excludeNothing, false, !test.legacyMode) - if err != nil { - t.Fatal(err) - } - rel := fake.RelativeTo(dir) - checkState(ctx, t, fs, rel, w, test.initialState) - - // Apply updates. - if test.updates != nil { - changes := make(map[span.URI]*fileChange) - for k, v := range test.updates { - content := strings.ReplaceAll(v.content, "$SANDBOX_WORKDIR", string(rel)) - uri := span.URIFromPath(rel.AbsPath(k)) - changes[uri], err = fs.change(ctx, uri, content, v.saved) - if err != nil { - t.Fatal(err) - } - } - got, gotReinit := w.Clone(ctx, changes, fs) - gotChanged := got != w - if gotChanged != test.wantChanged { - t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged) - } - if gotReinit != test.wantReload { - t.Errorf("w.invalidate(): got reload %t, want %t", gotReinit, test.wantReload) - } - checkState(ctx, t, fs, rel, got, test.finalState) - } - }) - } -} - -func workspaceFromTxtar(t *testing.T, files string) (*workspace, func(), error) { - ctx := context.Background() - dir, err := fake.Tempdir(fake.UnpackTxt(files)) - if err != nil { - return nil, func() {}, err - } - cleanup := func() { - os.RemoveAll(dir) - } - root := span.URIFromPath(dir) - - fs := &osFileSource{} - excludeNothing := func(string) bool { return false } - workspace, err := newWorkspace(ctx, root, "", fs, excludeNothing, false, false) - return workspace, cleanup, err -} - -func TestWorkspaceParseError(t *testing.T) { - w, cleanup, err := workspaceFromTxtar(t, ` --- go.work -- -go 1.18 - -usa ./typo --- typo/go.mod -- -module foo -`) - defer cleanup() - if err != nil { - t.Fatalf("error creating workspace: %v; want no error", err) - } - w.buildMu.Lock() - built, buildErr := w.built, w.buildErr - w.buildMu.Unlock() - if !built || buildErr == nil { - t.Fatalf("built, buildErr: got %v, %v; want true, non-nil", built, buildErr) - } - var errList modfile.ErrorList - if !errors.As(buildErr, &errList) { - t.Fatalf("expected error to be an errorlist; got %v", buildErr) - } - if len(errList) != 1 { - t.Fatalf("expected errorList to have one element; got %v elements", len(errList)) - } - parseErr := errList[0] - if parseErr.Pos.Line != 3 { - t.Fatalf("expected error to be on line 3; got %v", parseErr.Pos.Line) - } -} - -func TestWorkspaceMissingModFile(t *testing.T) { - w, cleanup, err := workspaceFromTxtar(t, ` --- go.work -- -go 1.18 - -use ./missing -`) - defer cleanup() - if err != nil { - t.Fatalf("error creating workspace: %v; want no error", err) - } - w.buildMu.Lock() - built, buildErr := w.built, w.buildErr - w.buildMu.Unlock() - if !built || buildErr == nil { - t.Fatalf("built, buildErr: got %v, %v; want true, non-nil", built, buildErr) - } -} - -func checkState(ctx context.Context, t *testing.T, fs source.FileSource, rel fake.RelativeTo, got *workspace, want wsState) { - t.Helper() - if got.moduleSource != want.source { - t.Errorf("module source = %v, want %v", got.moduleSource, want.source) - } - modules := make(map[span.URI]struct{}) - for k := range got.ActiveModFiles() { - modules[k] = struct{}{} - } - for _, modPath := range want.modules { - path := rel.AbsPath(modPath) - uri := span.URIFromPath(path) - if _, ok := modules[uri]; !ok { - t.Errorf("missing module %q", uri) - } - delete(modules, uri) - } - for remaining := range modules { - t.Errorf("unexpected module %q", remaining) - } - gotDirs := got.dirs(ctx, fs) - gotM := make(map[span.URI]bool) - for _, dir := range gotDirs { - gotM[dir] = true - } - for _, dir := range want.dirs { - path := rel.AbsPath(dir) - uri := span.URIFromPath(path) - if !gotM[uri] { - t.Errorf("missing dir %q", uri) - } - delete(gotM, uri) - } - for remaining := range gotM { - t.Errorf("unexpected dir %q", remaining) - } - gotSumBytes, err := got.sumFile(ctx, fs) - if err != nil { - t.Fatal(err) - } - if gotSum := string(gotSumBytes); gotSum != want.sum { - t.Errorf("got final sum %q, want %q", gotSum, want.sum) - } -} diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index aea05d5e9da..afe6dda7336 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -270,7 +270,6 @@ func (app *Application) featureCommands() []tool.Application { &signature{app: app}, &suggestedFix{app: app}, &symbols{app: app}, - newWorkspace(app), &workspaceSymbol{app: app}, &vulncheck{app: app}, } diff --git a/gopls/internal/lsp/cmd/help_test.go b/gopls/internal/lsp/cmd/help_test.go index f8d9b0b75ca..6bd3c8c501f 100644 --- a/gopls/internal/lsp/cmd/help_test.go +++ b/gopls/internal/lsp/cmd/help_test.go @@ -12,6 +12,7 @@ import ( "path/filepath" "testing" + "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/lsp/cmd" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/tool" @@ -45,12 +46,12 @@ func TestHelpFiles(t *testing.T) { } return } - expect, err := ioutil.ReadFile(helpFile) - switch { - case err != nil: - t.Errorf("Missing help file %q", helpFile) - case !bytes.Equal(expect, got): - t.Errorf("Help file %q did not match, got:\n%q\nwant:\n%q", helpFile, string(got), string(expect)) + want, err := ioutil.ReadFile(helpFile) + if err != nil { + t.Fatalf("Missing help file %q", helpFile) + } + if diff := cmp.Diff(string(want), string(got)); diff != "" { + t.Errorf("Help file %q did not match, run with -update-help-files to fix (-want +got)\n%s", helpFile, diff) } }) } diff --git a/gopls/internal/lsp/cmd/usage/usage.hlp b/gopls/internal/lsp/cmd/usage/usage.hlp index eaa05c5fa3a..404750b7d38 100644 --- a/gopls/internal/lsp/cmd/usage/usage.hlp +++ b/gopls/internal/lsp/cmd/usage/usage.hlp @@ -37,7 +37,6 @@ Features signature display selected identifier's signature fix apply suggested fixes symbols display selected file's symbols - workspace manage the gopls workspace (experimental: under development) workspace_symbol search symbols in workspace vulncheck run experimental vulncheck analysis (experimental: under development) diff --git a/gopls/internal/lsp/cmd/usage/workspace.hlp b/gopls/internal/lsp/cmd/usage/workspace.hlp deleted file mode 100644 index 912cf294610..00000000000 --- a/gopls/internal/lsp/cmd/usage/workspace.hlp +++ /dev/null @@ -1,7 +0,0 @@ -manage the gopls workspace (experimental: under development) - -Usage: - gopls [flags] workspace [arg]... - -Subcommand: - generate generate a gopls.mod file for a workspace diff --git a/gopls/internal/lsp/cmd/workspace.go b/gopls/internal/lsp/cmd/workspace.go deleted file mode 100644 index 2038d276348..00000000000 --- a/gopls/internal/lsp/cmd/workspace.go +++ /dev/null @@ -1,77 +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 cmd - -import ( - "context" - "flag" - "fmt" - - "golang.org/x/tools/gopls/internal/lsp/command" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" -) - -// workspace is a top-level command for working with the gopls workspace. This -// is experimental and subject to change. The idea is that subcommands could be -// used for manipulating the workspace mod file, rather than editing it -// manually. -type workspace struct { - app *Application - subcommands -} - -func newWorkspace(app *Application) *workspace { - return &workspace{ - app: app, - subcommands: subcommands{ - &generateWorkspaceMod{app: app}, - }, - } -} - -func (w *workspace) Name() string { return "workspace" } -func (w *workspace) Parent() string { return w.app.Name() } -func (w *workspace) ShortHelp() string { - return "manage the gopls workspace (experimental: under development)" -} - -// generateWorkspaceMod (re)generates the gopls.mod file for the current -// workspace. -type generateWorkspaceMod struct { - app *Application -} - -func (c *generateWorkspaceMod) Name() string { return "generate" } -func (c *generateWorkspaceMod) Usage() string { return "" } -func (c *generateWorkspaceMod) ShortHelp() string { - return "generate a gopls.mod file for a workspace" -} - -func (c *generateWorkspaceMod) DetailedHelp(f *flag.FlagSet) { - printFlagDefaults(f) -} - -func (c *generateWorkspaceMod) Run(ctx context.Context, args ...string) error { - origOptions := c.app.options - c.app.options = func(opts *source.Options) { - origOptions(opts) - opts.ExperimentalWorkspaceModule = true - } - conn, err := c.app.connect(ctx) - if err != nil { - return err - } - defer conn.terminate(ctx) - cmd, err := command.NewGenerateGoplsModCommand("", command.URIArg{}) - if err != nil { - return err - } - params := &protocol.ExecuteCommandParams{Command: cmd.Command, Arguments: cmd.Arguments} - if _, err := conn.ExecuteCommand(ctx, params); err != nil { - return fmt.Errorf("executing server command: %v", err) - } - return nil -} diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 210ce9b4f79..b9e1fe6dd64 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -715,35 +715,6 @@ func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIAr }) } -func (c *commandHandler) GenerateGoplsMod(ctx context.Context, args command.URIArg) error { - // TODO: go back to using URI - return c.run(ctx, commandConfig{ - requireSave: true, - progress: "Generating gopls.mod", - }, func(ctx context.Context, deps commandDeps) error { - views := c.s.session.Views() - if len(views) != 1 { - return fmt.Errorf("cannot resolve view: have %d views", len(views)) - } - v := views[0] - snapshot, release := v.Snapshot(ctx) - defer release() - modFile, err := snapshot.BuildGoplsMod(ctx) - if err != nil { - return fmt.Errorf("getting workspace mod file: %w", err) - } - content, err := modFile.Format() - if err != nil { - return fmt.Errorf("formatting mod file: %w", err) - } - filename := filepath.Join(v.Folder().Filename(), "gopls.mod") - if err := ioutil.WriteFile(filename, content, 0644); err != nil { - return fmt.Errorf("writing mod file: %w", err) - } - return nil - }) -} - func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URIArg) (command.ListKnownPackagesResult, error) { var result command.ListKnownPackagesResult err := c.run(ctx, commandConfig{ diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go index 35d37ed4d6c..b6aea98c15a 100644 --- a/gopls/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -27,7 +27,6 @@ const ( FetchVulncheckResult Command = "fetch_vulncheck_result" GCDetails Command = "gc_details" Generate Command = "generate" - GenerateGoplsMod Command = "generate_gopls_mod" GoGetPackage Command = "go_get_package" ListImports Command = "list_imports" ListKnownPackages Command = "list_known_packages" @@ -54,7 +53,6 @@ var Commands = []Command{ FetchVulncheckResult, GCDetails, Generate, - GenerateGoplsMod, GoGetPackage, ListImports, ListKnownPackages, @@ -122,12 +120,6 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.Generate(ctx, a0) - case "gopls.generate_gopls_mod": - var a0 URIArg - if err := UnmarshalArgs(params.Arguments, &a0); err != nil { - return nil, err - } - return nil, s.GenerateGoplsMod(ctx, a0) case "gopls.go_get_package": var a0 GoGetPackageArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -320,18 +312,6 @@ func NewGenerateCommand(title string, a0 GenerateArgs) (protocol.Command, error) }, nil } -func NewGenerateGoplsModCommand(title string, a0 URIArg) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ - Title: title, - Command: "gopls.generate_gopls_mod", - Arguments: args, - }, nil -} - func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index ec910fc43d3..965158aef13 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -121,11 +121,6 @@ type Interface interface { // Toggle the calculation of gc annotations. ToggleGCDetails(context.Context, URIArg) error - // GenerateGoplsMod: Generate gopls.mod - // - // (Re)generate the gopls.mod file for a workspace. - GenerateGoplsMod(context.Context, URIArg) error - // ListKnownPackages: List known packages // // Retrieve a list of packages that are importable from the given URI. diff --git a/gopls/internal/lsp/command/interface_test.go b/gopls/internal/lsp/command/interface_test.go index de3ce62737f..e602293a19f 100644 --- a/gopls/internal/lsp/command/interface_test.go +++ b/gopls/internal/lsp/command/interface_test.go @@ -5,10 +5,10 @@ package command_test import ( - "bytes" "io/ioutil" "testing" + "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/lsp/command/gen" "golang.org/x/tools/internal/testenv" ) @@ -25,7 +25,7 @@ func TestGenerated(t *testing.T) { if err != nil { t.Fatal(err) } - if !bytes.Equal(onDisk, generated) { - t.Error("command_gen.go is stale -- regenerate") + if diff := cmp.Diff(string(generated), string(onDisk)); diff != "" { + t.Errorf("command_gen.go is stale -- regenerate (-generated +on disk)\n%s", diff) } } diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go index 3adc4debfa5..2713715cd64 100644 --- a/gopls/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -134,11 +134,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle urlPath := string(importPath) // For pkg.go.dev, append module version suffix to package import path. - if m := snapshot.Metadata(depsByImpPath[importPath]); m != nil && - m.Module != nil && - m.Module.Path != "" && - m.Module.Version != "" && - !source.IsWorkspaceModuleVersion(m.Module.Version) { + if m := snapshot.Metadata(depsByImpPath[importPath]); m != nil && m.Module != nil && m.Module.Path != "" && m.Module.Version != "" { urlPath = strings.Replace(urlPath, m.Module.Path, m.Module.Path+"@"+m.Module.Version, 1) } diff --git a/gopls/internal/lsp/regtest/runner.go b/gopls/internal/lsp/regtest/runner.go index a006164a076..20dac842316 100644 --- a/gopls/internal/lsp/regtest/runner.go +++ b/gopls/internal/lsp/regtest/runner.go @@ -344,9 +344,6 @@ func (r *Runner) experimentalServer(optsHook func(*source.Options)) jsonrpc2.Str options := func(o *source.Options) { optsHook(o) o.EnableAllExperiments() - // ExperimentalWorkspaceModule is not (as of writing) enabled by - // source.Options.EnableAllExperiments, but we want to test it. - o.ExperimentalWorkspaceModule = true } return lsprpc.NewStreamServer(cache.New(nil, nil), false, options) } diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 44334a0f2bf..11347e7bf80 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -56,14 +56,6 @@ var GeneratedAPIJSON = &APIJSON{ Status: "experimental", Hierarchy: "build", }, - { - Name: "experimentalWorkspaceModule", - Type: "bool", - Doc: "experimentalWorkspaceModule opts a user into the experimental support\nfor multi-module workspaces.\n\nDeprecated: this feature is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55331).\n", - Default: "false", - Status: "experimental", - Hierarchy: "build", - }, { Name: "experimentalPackageCacheKey", Type: "bool", @@ -726,12 +718,6 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Runs `go generate` for a given directory.", ArgDoc: "{\n\t// URI for the directory to generate.\n\t\"Dir\": string,\n\t// Whether to generate recursively (go generate ./...)\n\t\"Recursive\": bool,\n}", }, - { - Command: "gopls.generate_gopls_mod", - Title: "Generate gopls.mod", - Doc: "(Re)generate the gopls.mod file for a workspace.", - ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", - }, { Command: "gopls.go_get_package", Title: "go get a package", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index ba6c3f0da2c..ca3441a693b 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -268,13 +268,6 @@ type BuildOptions struct { // a go.mod file, narrowing the scope to that directory if it exists. ExpandWorkspaceToModule bool `status:"experimental"` - // ExperimentalWorkspaceModule opts a user into the experimental support - // for multi-module workspaces. - // - // Deprecated: this feature is deprecated and will be removed in a future - // version of gopls (https://go.dev/issue/55331). - ExperimentalWorkspaceModule bool `status:"experimental"` - // ExperimentalPackageCacheKey controls whether to use a coarser cache key // for package type information to increase cache hits. This setting removes // the user's environment, build flags, and working directory from the cache @@ -1100,12 +1093,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.ExperimentalPostfixCompletions) case "experimentalWorkspaceModule": - const msg = "experimentalWorkspaceModule has been replaced by go workspaces, " + - "and will be removed in a future version of gopls (https://go.dev/issue/55331) -- " + - "see https://github.com/golang/tools/blob/master/gopls/doc/workspace.md " + - "for information on setting up multi-module workspaces using go.work files" - result.softErrorf(msg) - result.setBool(&o.ExperimentalWorkspaceModule) + result.deprecated("") case "experimentalTemplateSupport": // TODO(pjw): remove after June 2022 result.deprecated("") diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 118638aabfe..bb6b7542964 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -15,7 +15,6 @@ import ( "go/token" "go/types" "io" - "strings" "golang.org/x/mod/modfile" "golang.org/x/mod/module" @@ -210,10 +209,6 @@ type Snapshot interface { // // A nil result may mean success, or context cancellation. GetCriticalError(ctx context.Context) *CriticalError - - // BuildGoplsMod generates a go.mod file for all modules in the workspace. - // It bypasses any existing gopls.mod. - BuildGoplsMod(ctx context.Context) (*modfile.File, error) } // SnapshotLabels returns a new slice of labels that should be used for events @@ -779,7 +774,7 @@ type Package interface { PkgPath() PackagePath GetTypesSizes() types.Sizes ForTest() string - Version() *module.Version // may differ from Metadata.Module.Version + Version() *module.Version // Results of parsing: FileSet() *token.FileSet @@ -856,28 +851,3 @@ const ( func AnalyzerErrorKind(name string) DiagnosticSource { return DiagnosticSource(name) } - -// WorkspaceModuleVersion is the nonexistent pseudoversion suffix used in the -// construction of the workspace module. It is exported so that we can make -// sure not to show this version to end users in error messages, to avoid -// confusion. -// The major version is not included, as that depends on the module path. -// -// If workspace module A is dependent on workspace module B, we need our -// nonexistent version to be greater than the version A mentions. -// Otherwise, the go command will try to update to that version. Use a very -// high minor version to make that more likely. -const workspaceModuleVersion = ".9999999.0-goplsworkspace" - -func IsWorkspaceModuleVersion(version string) bool { - return strings.HasSuffix(version, workspaceModuleVersion) -} - -func WorkspaceModuleVersion(majorVersion string) string { - // Use the highest compatible major version to avoid unwanted upgrades. - // See the comment on workspaceModuleVersion. - if majorVersion == "v0" { - majorVersion = "v1" - } - return majorVersion + workspaceModuleVersion -} diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 3225c15b22c..e9f1009478d 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -286,7 +286,6 @@ func DefaultOptions(o *source.Options) { o.InsertTextFormat = protocol.SnippetTextFormat o.CompletionBudget = time.Minute o.HierarchicalDocumentSymbolSupport = true - o.ExperimentalWorkspaceModule = true o.SemanticTokens = true o.InternalOptions.NewDiff = "both" } diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 734499cd63a..b03b96f0ad3 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -78,6 +78,8 @@ const ( // regression test for golang/go#39446. It also checks that these code lenses // only affect the diagnostics and contents of the containing go.mod file. func TestUpgradeCodelens(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work + const proxyWithLatest = ` -- golang.org/x/hello@v1.3.3/go.mod -- module golang.org/x/hello @@ -185,7 +187,7 @@ require golang.org/x/hello v1.2.3 }); err != nil { t.Fatal(err) } - env.Await(env.DoneWithChangeWatchedFiles()) + env.AfterChange() if got := env.BufferText("a/go.mod"); got != wantGoModA { t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) } @@ -201,7 +203,7 @@ require golang.org/x/hello v1.2.3 if vendoring { env.RunGoCommandInDir("a", "mod", "vendor") } - env.Await(env.DoneWithChangeWatchedFiles()) + env.AfterChange() env.OpenFile("a/go.mod") env.OpenFile("b/go.mod") env.ExecuteCodeLensCommand("a/go.mod", command.CheckUpgrades, nil) diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index fb101d366e2..e9cc524819c 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -25,6 +25,9 @@ import ( func TestBrokenWorkspace_DuplicateModules(t *testing.T) { testenv.NeedsGo1Point(t, 18) + // TODO(golang/go#57650): fix this feature. + t.Skip("we no longer detect duplicate modules") + // This proxy module content is replaced by the workspace, but is still // required for module resolution to function in the Go command. const proxy = ` diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go index 94aa8b73464..3f8bf6c6401 100644 --- a/gopls/internal/regtest/workspace/directoryfilters_test.go +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -10,6 +10,7 @@ import ( "testing" . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" ) // This file contains regression tests for the directoryFilters setting. @@ -198,6 +199,8 @@ func Goodbye() {} // Test for golang/go#52993: non-wildcard directoryFilters should still be // applied relative to the workspace folder, not the module root. func TestDirectoryFilters_MultiRootImportScanning(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work + const files = ` -- go.work -- go 1.18 diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go index 47405c3a145..218e20630a1 100644 --- a/gopls/internal/regtest/workspace/fromenv_test.go +++ b/gopls/internal/regtest/workspace/fromenv_test.go @@ -31,12 +31,19 @@ package b func _() { x := 1 // unused } +-- other/c/go.mod -- +module c.com + +go 1.18 +-- other/c/c.go -- +package c -- config/go.work -- go 1.18 use ( $SANDBOX_WORKDIR/work/a $SANDBOX_WORKDIR/work/b + $SANDBOX_WORKDIR/other/c ) ` @@ -45,15 +52,19 @@ use ( EnvVars{"GOWORK": "$SANDBOX_WORKDIR/config/go.work"}, ).Run(t, files, func(t *testing.T, env *Env) { // When we have an explicit GOWORK set, we should get a file watch request. + env.Await( + OnceMet( + InitialWorkspaceLoad, + FileWatchMatching(`other`), + FileWatchMatching(`config.go\.work`), + ), + ) env.Await(FileWatchMatching(`config.go\.work`)) // Even though work/b is not open, we should get its diagnostics as it is // included in the workspace. env.OpenFile("work/a/a.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - env.DiagnosticAtRegexpWithMessage("work/b/b.go", "x := 1", "not used"), - ), + env.AfterChange( + env.DiagnosticAtRegexpWithMessage("work/b/b.go", "x := 1", "not used"), ) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 81e3531c376..35ea6b99d85 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -466,8 +466,6 @@ func Hello() int { // This test confirms that a gopls workspace can recover from initialization // with one invalid module. func TestOneBrokenModule(t *testing.T) { - t.Skip("golang/go#55331: this test is temporarily broken as go.work handling tries to build the workspace module") - testenv.NeedsGo1Point(t, 18) // uses go.work const multiModule = ` -- go.work -- @@ -521,135 +519,6 @@ func Hello() int { }) } -func TestUseGoplsMod(t *testing.T) { - // This test validates certain functionality related to using a gopls.mod - // file to specify workspace modules. - const multiModule = ` --- moda/a/go.mod -- -module a.com - -require b.com v1.2.3 --- moda/a/go.sum -- -b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= -b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= --- moda/a/a.go -- -package a - -import ( - "b.com/b" -) - -func main() { - var x int - _ = b.Hello() -} --- modb/go.mod -- -module b.com - -require example.com v1.2.3 --- modb/go.sum -- -example.com v1.2.3 h1:veRD4tUnatQRgsULqULZPjeoBGFr2qBhevSCZllD2Ds= -example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= --- modb/b/b.go -- -package b - -func Hello() int { - var x int -} --- gopls.mod -- -module gopls-workspace - -require ( - a.com v0.0.0-goplsworkspace - b.com v1.2.3 -) - -replace a.com => $SANDBOX_WORKDIR/moda/a -` - WithOptions( - ProxyFiles(workspaceModuleProxy), - Modes(Experimental), - ).Run(t, multiModule, func(t *testing.T, env *Env) { - // Initially, the gopls.mod should cause only the a.com module to be - // loaded. Validate this by jumping to a definition in b.com and ensuring - // that we go to the module cache. - env.OpenFile("moda/a/a.go") - env.Await(env.DoneWithOpen()) - - // To verify which modules are loaded, we'll jump to the definition of - // b.Hello. - checkHelloLocation := func(want string) error { - location, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) - if !strings.HasSuffix(location, want) { - return fmt.Errorf("expected %s, got %v", want, location) - } - return nil - } - - // Initially this should be in the module cache, as b.com is not replaced. - if err := checkHelloLocation("b.com@v1.2.3/b/b.go"); err != nil { - t.Fatal(err) - } - - // Now, modify the gopls.mod file on disk to activate the b.com module in - // the workspace. - workdir := env.Sandbox.Workdir.RootURI().SpanURI().Filename() - env.WriteWorkspaceFile("gopls.mod", fmt.Sprintf(`module gopls-workspace - -require ( - a.com v1.9999999.0-goplsworkspace - b.com v1.9999999.0-goplsworkspace -) - -replace a.com => %s/moda/a -replace b.com => %s/modb -`, workdir, workdir)) - - // As of golang/go#54069, writing a gopls.mod to the workspace triggers a - // workspace reload. - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - env.DiagnosticAtRegexp("modb/b/b.go", "x"), - ), - ) - - // Jumping to definition should now go to b.com in the workspace. - if err := checkHelloLocation("modb/b/b.go"); err != nil { - t.Fatal(err) - } - - // Now, let's modify the gopls.mod *overlay* (not on disk), and verify that - // this change is only picked up once it is saved. - env.OpenFile("gopls.mod") - env.Await(env.DoneWithOpen()) - env.SetBufferContent("gopls.mod", fmt.Sprintf(`module gopls-workspace - -require ( - a.com v0.0.0-goplsworkspace -) - -replace a.com => %s/moda/a -`, workdir)) - - // Editing the gopls.mod removes modb from the workspace modules, and so - // should clear outstanding diagnostics... - env.Await(OnceMet( - env.DoneWithChange(), - EmptyDiagnostics("modb/go.mod"), - )) - // ...but does not yet cause a workspace reload, so we should still jump to modb. - if err := checkHelloLocation("modb/b/b.go"); err != nil { - t.Fatal(err) - } - // Saving should reload the workspace. - env.SaveBufferWithoutActions("gopls.mod") - if err := checkHelloLocation("b.com@v1.2.3/b/b.go"); err != nil { - t.Fatal(err) - } - }) -} - // TestBadGoWork exercises the panic from golang/vscode-go#2121. func TestBadGoWork(t *testing.T) { const files = ` @@ -664,6 +533,7 @@ module example.com/bar } func TestUseGoWork(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work // This test validates certain functionality related to using a go.work // file to specify workspace modules. const multiModule = ` @@ -809,6 +679,8 @@ use ( } func TestUseGoWorkDiagnosticMissingModule(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work + const files = ` -- go.work -- go 1.18 @@ -819,7 +691,7 @@ module example.com/bar ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("go.work") - env.Await( + env.AfterChange( env.DiagnosticAtRegexpWithMessage("go.work", "use", "directory ./foo does not contain a module"), ) // The following tests is a regression test against an issue where we weren't @@ -829,17 +701,18 @@ module example.com/bar // struct, and then set the content back to the old contents to make sure // the diagnostic still shows up. env.SetBufferContent("go.work", "go 1.18 \n\n use ./bar\n") - env.Await( + env.AfterChange( env.NoDiagnosticAtRegexp("go.work", "use"), ) env.SetBufferContent("go.work", "go 1.18 \n\n use ./foo\n") - env.Await( + env.AfterChange( env.DiagnosticAtRegexpWithMessage("go.work", "use", "directory ./foo does not contain a module"), ) }) } func TestUseGoWorkDiagnosticSyntaxError(t *testing.T) { + testenv.NeedsGo1Point(t, 18) const files = ` -- go.work -- go 1.18 @@ -849,7 +722,7 @@ replace ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("go.work") - env.Await( + env.AfterChange( env.DiagnosticAtRegexpWithMessage("go.work", "usa", "unknown directive: usa"), env.DiagnosticAtRegexpWithMessage("go.work", "replace", "usage: replace"), ) @@ -857,6 +730,8 @@ replace } func TestUseGoWorkHover(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + const files = ` -- go.work -- go 1.18 @@ -1155,6 +1030,7 @@ package main } func TestAddAndRemoveGoWork(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // Use a workspace with a module in the root directory to exercise the case // where a go.work is added to the existing root directory. This verifies // that we're detecting changes to the module source, not just the root From e956495a7123528842a925cba81a856c53f3960f Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 14:47:03 -0500 Subject: [PATCH 616/723] gopls/internal/regtest: eliminate DiagnosticsFor As noted in the associated TODO, DiagnosticsFor is racy. Remove it, replacing uses with the atomic ReadDiagnostics expectation. Also replace nearby Awaits with AfterChange, to make them fail eagerly. Change-Id: Idb5c2420cd0d9b28e701343bc8d2883eea4e144a Reviewed-on: https://go-review.googlesource.com/c/tools/+/461875 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/regtest/env.go | 12 --------- .../regtest/diagnostics/diagnostics_test.go | 26 +++++++++---------- .../regtest/diagnostics/undeclared_test.go | 23 ++++++++++------ .../internal/regtest/modfile/modfile_test.go | 8 +++--- .../regtest/template/template_test.go | 13 +++++++--- 5 files changed, 43 insertions(+), 39 deletions(-) diff --git a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go index 192e8ed2a80..c3f48bf16e9 100644 --- a/gopls/internal/lsp/regtest/env.go +++ b/gopls/internal/lsp/regtest/env.go @@ -302,18 +302,6 @@ func checkExpectations(s State, expectations []Expectation) (Verdict, string) { return finalVerdict, summary.String() } -// DiagnosticsFor returns the current diagnostics for the file. It is useful -// after waiting on AnyDiagnosticAtCurrentVersion, when the desired diagnostic -// is not simply described by DiagnosticAt. -// -// TODO(rfindley): this method is inherently racy. Replace usages of this -// method with the atomic OnceMet(..., ReadDiagnostics) pattern. -func (a *Awaiter) DiagnosticsFor(name string) *protocol.PublishDiagnosticsParams { - a.mu.Lock() - defer a.mu.Unlock() - return a.state.diagnostics[name] -} - func (e *Env) Await(expectations ...Expectation) { e.T.Helper() if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil { diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index c7f091cde6a..84bd5d483bb 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1280,23 +1280,23 @@ func main() {} Run(t, dir, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OpenFile("other.go") - x := env.Awaiter.DiagnosticsFor("main.go") - if x == nil { - t.Fatalf("expected 1 diagnostic, got none") - } - if len(x.Diagnostics) != 1 { - t.Fatalf("main.go, got %d diagnostics, expected 1", len(x.Diagnostics)) + var mainDiags, otherDiags protocol.PublishDiagnosticsParams + env.AfterChange( + ReadDiagnostics("main.go", &mainDiags), + ReadDiagnostics("other.go", &otherDiags), + ) + if len(mainDiags.Diagnostics) != 1 { + t.Fatalf("main.go, got %d diagnostics, expected 1", len(mainDiags.Diagnostics)) } - keep := x.Diagnostics[0] - y := env.Awaiter.DiagnosticsFor("other.go") - if len(y.Diagnostics) != 1 { - t.Fatalf("other.go: got %d diagnostics, expected 1", len(y.Diagnostics)) + keep := mainDiags.Diagnostics[0] + if len(otherDiags.Diagnostics) != 1 { + t.Fatalf("other.go: got %d diagnostics, expected 1", len(otherDiags.Diagnostics)) } - if len(y.Diagnostics[0].RelatedInformation) != 1 { - t.Fatalf("got %d RelatedInformations, expected 1", len(y.Diagnostics[0].RelatedInformation)) + if len(otherDiags.Diagnostics[0].RelatedInformation) != 1 { + t.Fatalf("got %d RelatedInformations, expected 1", len(otherDiags.Diagnostics[0].RelatedInformation)) } // check that the RelatedInformation matches the error from main.go - c := y.Diagnostics[0].RelatedInformation[0] + c := otherDiags.Diagnostics[0].RelatedInformation[0] if c.Location.Range != keep.Range { t.Errorf("locations don't match. Got %v expected %v", c.Location.Range, keep.Range) } diff --git a/gopls/internal/regtest/diagnostics/undeclared_test.go b/gopls/internal/regtest/diagnostics/undeclared_test.go index c3456fa2d58..bba72c47237 100644 --- a/gopls/internal/regtest/diagnostics/undeclared_test.go +++ b/gopls/internal/regtest/diagnostics/undeclared_test.go @@ -44,23 +44,30 @@ func _() int { // 'x' is undeclared, but still necessary. env.OpenFile("a/a.go") - env.Await(env.DiagnosticAtRegexp("a/a.go", "x")) - diags := env.Awaiter.DiagnosticsFor("a/a.go") - if got := len(diags.Diagnostics); got != 1 { + var adiags protocol.PublishDiagnosticsParams + env.AfterChange( + env.DiagnosticAtRegexp("a/a.go", "x"), + ReadDiagnostics("a/a.go", &adiags), + ) + env.Await() + if got := len(adiags.Diagnostics); got != 1 { t.Errorf("len(Diagnostics) = %d, want 1", got) } - if diag := diags.Diagnostics[0]; isUnnecessary(diag) { + if diag := adiags.Diagnostics[0]; isUnnecessary(diag) { t.Errorf("%v tagged unnecessary, want necessary", diag) } // 'y = y' is pointless, and should be detected as unnecessary. env.OpenFile("b/b.go") - env.Await(env.DiagnosticAtRegexp("b/b.go", "y = y")) - diags = env.Awaiter.DiagnosticsFor("b/b.go") - if got := len(diags.Diagnostics); got != 1 { + var bdiags protocol.PublishDiagnosticsParams + env.AfterChange( + env.DiagnosticAtRegexp("b/b.go", "y = y"), + ReadDiagnostics("b/b.go", &bdiags), + ) + if got := len(bdiags.Diagnostics); got != 1 { t.Errorf("len(Diagnostics) = %d, want 1", got) } - if diag := diags.Diagnostics[0]; !isUnnecessary(diag) { + if diag := bdiags.Diagnostics[0]; !isUnnecessary(diag) { t.Errorf("%v tagged necessary, want unnecessary", diag) } }) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 1d2ade2fc9a..e15fa4c5c4d 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -554,13 +554,15 @@ var _ = blah.Name }.Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.OpenFile("a/go.mod") - env.Await( + var modDiags protocol.PublishDiagnosticsParams + env.AfterChange( // We would like for the error to appear in the v2 module, but // as of writing non-workspace packages are not diagnosed. env.DiagnosticAtRegexpWithMessage("a/main.go", `"example.com/blah/v2"`, "cannot find module providing"), env.DiagnosticAtRegexpWithMessage("a/go.mod", `require example.com/blah/v2`, "cannot find module providing"), + ReadDiagnostics("a/go.mod", &modDiags), ) - env.ApplyQuickFixes("a/go.mod", env.Awaiter.DiagnosticsFor("a/go.mod").Diagnostics) + env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics) const want = `module mod.com go 1.12 @@ -571,7 +573,7 @@ require ( ) ` env.SaveBuffer("a/go.mod") - env.Await(EmptyDiagnostics("a/main.go")) + env.AfterChange(EmptyDiagnostics("a/main.go")) if got := env.BufferText("a/go.mod"); got != want { t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got)) } diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index 91c704de0d9..afe579797cb 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -70,8 +70,15 @@ Hello {{}} <-- missing body }, ).Run(t, files, func(t *testing.T, env *Env) { // TODO: can we move this diagnostic onto {{}}? - env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) - d := env.Awaiter.DiagnosticsFor("hello.tmpl").Diagnostics // issue 50786: check for Source + var diags protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"), + ReadDiagnostics("hello.tmpl", &diags), + ), + ) + d := diags.Diagnostics // issue 50786: check for Source if len(d) != 1 { t.Errorf("expected 1 diagnostic, got %d", len(d)) return @@ -91,7 +98,7 @@ Hello {{}} <-- missing body } env.WriteWorkspaceFile("hello.tmpl", "{{range .Planets}}\nHello {{.}}\n{{end}}") - env.Await(EmptyDiagnostics("hello.tmpl")) + env.AfterChange(EmptyDiagnostics("hello.tmpl")) }) } From 7ec05ac773d948198280f1fdd5223464100a5241 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 14:53:45 -0500 Subject: [PATCH 617/723] gopls/internal/regtest: eliminate NoDiagnostics It is confusing to have both NoDiagnostics and EmptyOrNoDiagnostics. From a user's perspective, it does not matter whether gopls sends no diagnostics or sends empty diagnostics, so favor EmptyOrNoDiagnostics. In a subsequent change, I will rename EmptyOrNoDiagnostics to just NoDiagnostics. Also, switch more Await expressions to eager failures along the way. Change-Id: Ib20b9ad7e7e7c819f793acb931681ec0dd4f76f7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461876 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- gopls/internal/lsp/regtest/expectation.go | 17 ----------------- .../regtest/diagnostics/builtin_test.go | 2 +- .../regtest/diagnostics/diagnostics_test.go | 8 ++++---- .../internal/regtest/template/template_test.go | 7 +++++-- gopls/internal/regtest/watch/watch_test.go | 6 +++--- .../regtest/workspace/directoryfilters_test.go | 14 +++++++++++--- .../regtest/workspace/workspace_test.go | 7 ++----- 7 files changed, 26 insertions(+), 35 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 3b623d06059..7fcaf51344b 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -749,23 +749,6 @@ func EmptyOrNoDiagnostics(name string) Expectation { } } -// NoDiagnostics asserts that no diagnostics are sent for the -// workspace-relative path name. It should be used primarily in conjunction -// with a OnceMet, as it has to check that all outstanding diagnostics have -// already been delivered. -func NoDiagnostics(name string) Expectation { - check := func(s State) Verdict { - if _, ok := s.diagnostics[name]; !ok { - return Met - } - return Unmet - } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("no diagnostics for %q", name), - } -} - // DiagnosticAtRegexp expects that there is a diagnostic entry at the start // position matching the regexp search string re in the buffer specified by // name. Note that this currently ignores the end position. diff --git a/gopls/internal/regtest/diagnostics/builtin_test.go b/gopls/internal/regtest/diagnostics/builtin_test.go index 8de47bcd946..ae775d9b362 100644 --- a/gopls/internal/regtest/diagnostics/builtin_test.go +++ b/gopls/internal/regtest/diagnostics/builtin_test.go @@ -32,7 +32,7 @@ const ( } env.Await(OnceMet( env.DoneWithOpen(), - NoDiagnostics("builtin.go"), + EmptyOrNoDiagnostics("builtin.go"), )) }) } diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 84bd5d483bb..9ef429cf082 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1092,7 +1092,7 @@ func main() { env.Await( OnceMet( env.DoneWithChange(), - NoDiagnostics("foo/foo.go"), + EmptyOrNoDiagnostics("foo/foo.go"), ), ) }) @@ -1208,7 +1208,7 @@ func main() { WorkspaceFolders("a"), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("main.go", "x"), ) }) @@ -1217,8 +1217,8 @@ func main() { Settings{"expandWorkspaceToModule": false}, ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") - env.Await( - NoDiagnostics("main.go"), + env.AfterChange( + EmptyOrNoDiagnostics("main.go"), ) }) } diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index afe579797cb..4e87d0461ef 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -121,8 +121,11 @@ B {{}} <-- missing body }, ).Run(t, files, func(t *testing.T, env *Env) { env.Await( - OnceMet(env.DiagnosticAtRegexp("a/a.tmpl", "()A")), - NoDiagnostics("b/b.tmpl"), + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.tmpl", "()A"), + EmptyOrNoDiagnostics("b/b.tmpl"), + ), ) }) } diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 99c76ff9341..a62b473b231 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -225,8 +225,8 @@ func _() {} Run(t, original, func(t *testing.T, env *Env) { env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) env.WriteWorkspaceFile("a/a.go", `package a; import "mod.com/c"; func _() { c.C() }`) - env.Await( - NoDiagnostics("a/a.go"), + env.AfterChange( + EmptyOrNoDiagnostics("a/a.go"), ) }) } @@ -490,7 +490,7 @@ func _() {} env.RemoveWorkspaceFile("a/a1.go") env.WriteWorkspaceFile("a/a2.go", "package a; func _() {};") env.AfterChange( - NoDiagnostics("main.go"), + EmptyOrNoDiagnostics("main.go"), ) }) } diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go index 3f8bf6c6401..0a1f596d26e 100644 --- a/gopls/internal/regtest/workspace/directoryfilters_test.go +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -53,7 +53,12 @@ const _ = Nonexistant WithOptions( Settings{"directoryFilters": []string{"-exclude"}}, ).Run(t, files, func(t *testing.T, env *Env) { - env.Await(NoDiagnostics("exclude/x.go")) + env.Await( + OnceMet( + InitialWorkspaceLoad, + EmptyOrNoDiagnostics("exclude/x.go"), + ), + ) }) } @@ -81,8 +86,11 @@ const X = 1 Settings{"directoryFilters": []string{"-exclude"}}, ).Run(t, files, func(t *testing.T, env *Env) { env.Await( - NoDiagnostics("exclude/exclude.go"), // filtered out - NoDiagnostics("include/include.go"), // successfully builds + OnceMet( + InitialWorkspaceLoad, + EmptyOrNoDiagnostics("exclude/exclude.go"), // filtered out + EmptyOrNoDiagnostics("include/include.go"), // successfully builds + ), ) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 35ea6b99d85..0028f843f9c 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -1172,11 +1172,8 @@ import ( ` Run(t, ws, func(t *testing.T, env *Env) { env.OpenFile("b/main.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - NoDiagnostics("a/main.go"), - ), + env.AfterChange( + EmptyOrNoDiagnostics("a/main.go"), ) env.OpenFile("a/main.go") env.Await( From 2be1a9a651318524d2de0a9fc38fcb424d2b34c1 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 14:56:23 -0500 Subject: [PATCH 618/723] gopls/internal/regtest: rename EmptyOrNoDiagnostics to NoDiagnostics Now that there is only one such expectation, give it the simpler name. Change-Id: Ib5b7b55d2f20f9d9a034bce5eb01304bcdc65a2a Reviewed-on: https://go-review.googlesource.com/c/tools/+/461877 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley --- gopls/internal/lsp/regtest/expectation.go | 6 ++--- .../regtest/diagnostics/builtin_test.go | 2 +- .../regtest/diagnostics/diagnostics_test.go | 12 +++++----- gopls/internal/regtest/misc/failures_test.go | 4 ++-- gopls/internal/regtest/misc/vuln_test.go | 6 ++--- .../internal/regtest/modfile/modfile_test.go | 4 ++-- .../regtest/template/template_test.go | 4 ++-- gopls/internal/regtest/watch/watch_test.go | 24 +++++++++---------- .../workspace/directoryfilters_test.go | 6 ++--- .../regtest/workspace/standalone_test.go | 4 ++-- .../regtest/workspace/workspace_test.go | 4 ++-- 11 files changed, 37 insertions(+), 39 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 7fcaf51344b..55280ad2fea 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -732,11 +732,9 @@ func EmptyDiagnostics(name string) Expectation { } } -// EmptyOrNoDiagnostics asserts that either no diagnostics are sent for the +// NoDiagnostics asserts that either no diagnostics are sent for the // workspace-relative path name, or empty diagnostics are sent. -// TODO(rFindley): this subtlety shouldn't be necessary. Gopls should always -// send at least one diagnostic set for open files. -func EmptyOrNoDiagnostics(name string) Expectation { +func NoDiagnostics(name string) Expectation { check := func(s State) Verdict { if diags := s.diagnostics[name]; diags == nil || len(diags.Diagnostics) == 0 { return Met diff --git a/gopls/internal/regtest/diagnostics/builtin_test.go b/gopls/internal/regtest/diagnostics/builtin_test.go index ae775d9b362..8de47bcd946 100644 --- a/gopls/internal/regtest/diagnostics/builtin_test.go +++ b/gopls/internal/regtest/diagnostics/builtin_test.go @@ -32,7 +32,7 @@ const ( } env.Await(OnceMet( env.DoneWithOpen(), - EmptyOrNoDiagnostics("builtin.go"), + NoDiagnostics("builtin.go"), )) }) } diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 9ef429cf082..6f91c00a01d 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -886,8 +886,8 @@ package foo_ env.Await( OnceMet( env.DoneWithChange(), - EmptyOrNoDiagnostics("foo/bar_test.go"), - EmptyOrNoDiagnostics("foo/foo.go"), + NoDiagnostics("foo/bar_test.go"), + NoDiagnostics("foo/foo.go"), ), ) }) @@ -1092,7 +1092,7 @@ func main() { env.Await( OnceMet( env.DoneWithChange(), - EmptyOrNoDiagnostics("foo/foo.go"), + NoDiagnostics("foo/foo.go"), ), ) }) @@ -1218,7 +1218,7 @@ func main() { ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.AfterChange( - EmptyOrNoDiagnostics("main.go"), + NoDiagnostics("main.go"), ) }) } @@ -1636,8 +1636,8 @@ const B = a.B env.RegexpReplace("b/b.go", `const B = a\.B`, "") env.SaveBuffer("b/b.go") env.Await( - EmptyOrNoDiagnostics("a/a.go"), - EmptyOrNoDiagnostics("b/b.go"), + NoDiagnostics("a/a.go"), + NoDiagnostics("b/b.go"), ) }) } diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index f9965149957..b3ff37a2d67 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -77,8 +77,8 @@ const a = 2 // Fix the error by editing the const name in b.go to `b`. env.RegexpReplace("b.go", "(a) = 2", "b") env.Await( - EmptyOrNoDiagnostics("a.go"), - EmptyOrNoDiagnostics("b.go"), + NoDiagnostics("a.go"), + NoDiagnostics("b.go"), ) }) } diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 45d52c3d88f..1b44caf3566 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -229,7 +229,7 @@ func main() { OnceMet( CompletedProgress(result.Token, nil), ShownMessage("Found GOSTDLIB"), - EmptyOrNoDiagnostics("go.mod"), + NoDiagnostics("go.mod"), ), ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ @@ -276,7 +276,7 @@ func main() { ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.AfterChange( - EmptyOrNoDiagnostics("go.mod"), + NoDiagnostics("go.mod"), // we don't publish diagnostics for standard library vulnerability yet. ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ @@ -868,7 +868,7 @@ func TestGovulncheckInfo(t *testing.T) { } env.ApplyCodeAction(reset) - env.Await(EmptyOrNoDiagnostics("go.mod")) + env.Await(NoDiagnostics("go.mod")) }) } diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index e15fa4c5c4d..2a178e49a29 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -1166,8 +1166,8 @@ func main() { ) env.ApplyQuickFixes("main.go", d.Diagnostics) env.Await( - EmptyOrNoDiagnostics("main.go"), - EmptyOrNoDiagnostics("go.mod"), + NoDiagnostics("main.go"), + NoDiagnostics("go.mod"), ) }) } diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index 4e87d0461ef..c82c8291066 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -124,7 +124,7 @@ B {{}} <-- missing body OnceMet( InitialWorkspaceLoad, env.DiagnosticAtRegexp("a/a.tmpl", "()A"), - EmptyOrNoDiagnostics("b/b.tmpl"), + NoDiagnostics("b/b.tmpl"), ), ) }) @@ -149,7 +149,7 @@ go 1.12 env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}") env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) env.RegexpReplace("hello.tmpl", "{{}}", "{{.}}") - env.Await(EmptyOrNoDiagnostics("hello.tmpl")) + env.Await(NoDiagnostics("hello.tmpl")) }) } diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index a62b473b231..5a036567377 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -139,7 +139,7 @@ func _() { }) env.Await( EmptyDiagnostics("a/a.go"), - EmptyOrNoDiagnostics("b/b.go"), + NoDiagnostics("b/b.go"), ) }) } @@ -226,7 +226,7 @@ func _() {} env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) env.WriteWorkspaceFile("a/a.go", `package a; import "mod.com/c"; func _() { c.C() }`) env.AfterChange( - EmptyOrNoDiagnostics("a/a.go"), + NoDiagnostics("a/a.go"), ) }) } @@ -341,12 +341,12 @@ func _() { env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - EmptyOrNoDiagnostics("a/a.go"), + NoDiagnostics("a/a.go"), ), ) env.WriteWorkspaceFile("b/b.go", newMethod) env.Await( - EmptyOrNoDiagnostics("a/a.go"), + NoDiagnostics("a/a.go"), ) }) }) @@ -360,9 +360,9 @@ func _() { env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - EmptyOrNoDiagnostics("a/a.go"), + NoDiagnostics("a/a.go"), ), - EmptyOrNoDiagnostics("b/b.go"), + NoDiagnostics("b/b.go"), ) }) }) @@ -490,7 +490,7 @@ func _() {} env.RemoveWorkspaceFile("a/a1.go") env.WriteWorkspaceFile("a/a2.go", "package a; func _() {};") env.AfterChange( - EmptyOrNoDiagnostics("main.go"), + NoDiagnostics("main.go"), ) }) } @@ -567,7 +567,7 @@ func main() { }) env.AfterChange( env.DoneWithChangeWatchedFiles(), - EmptyOrNoDiagnostics("main.go"), + NoDiagnostics("main.go"), ) }) } @@ -694,11 +694,11 @@ func TestAll(t *testing.T) { env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - EmptyOrNoDiagnostics("a/a.go"), + NoDiagnostics("a/a.go"), ), OnceMet( env.DoneWithChangeWatchedFiles(), - EmptyOrNoDiagnostics("a/a_test.go"), + NoDiagnostics("a/a_test.go"), ), ) // Now, add a new file to the test variant and use its symbol in the @@ -726,11 +726,11 @@ func TestSomething(t *testing.T) {} env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - EmptyOrNoDiagnostics("a/a_test.go"), + NoDiagnostics("a/a_test.go"), ), OnceMet( env.DoneWithChangeWatchedFiles(), - EmptyOrNoDiagnostics("a/a2_test.go"), + NoDiagnostics("a/a2_test.go"), ), ) }) diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go index 0a1f596d26e..69f91cd9083 100644 --- a/gopls/internal/regtest/workspace/directoryfilters_test.go +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -56,7 +56,7 @@ const _ = Nonexistant env.Await( OnceMet( InitialWorkspaceLoad, - EmptyOrNoDiagnostics("exclude/x.go"), + NoDiagnostics("exclude/x.go"), ), ) }) @@ -88,8 +88,8 @@ const X = 1 env.Await( OnceMet( InitialWorkspaceLoad, - EmptyOrNoDiagnostics("exclude/exclude.go"), // filtered out - EmptyOrNoDiagnostics("include/include.go"), // successfully builds + NoDiagnostics("exclude/exclude.go"), // filtered out + NoDiagnostics("include/include.go"), // successfully builds ), ) }) diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go index 82334583e71..698c8aac134 100644 --- a/gopls/internal/regtest/workspace/standalone_test.go +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -219,7 +219,7 @@ func main() {} OnceMet( env.DoneWithOpen(), env.DiagnosticAtRegexp("ignore.go", "package (main)"), - EmptyOrNoDiagnostics("standalone.go"), + NoDiagnostics("standalone.go"), ), ) @@ -236,7 +236,7 @@ func main() {} env.Await( OnceMet( env.DoneWithChange(), - EmptyOrNoDiagnostics("ignore.go"), + NoDiagnostics("ignore.go"), env.DiagnosticAtRegexp("standalone.go", "package (main)"), ), ) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 0028f843f9c..022b04f6fd5 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -652,7 +652,7 @@ use ( // This fails if guarded with a OnceMet(DoneWithSave(), ...), because it is // debounced (and therefore not synchronous with the change). - env.Await(EmptyOrNoDiagnostics("modb/go.mod")) + env.Await(NoDiagnostics("modb/go.mod")) // Test Formatting. env.SetBufferContent("go.work", `go 1.18 @@ -1173,7 +1173,7 @@ import ( Run(t, ws, func(t *testing.T, env *Env) { env.OpenFile("b/main.go") env.AfterChange( - EmptyOrNoDiagnostics("a/main.go"), + NoDiagnostics("a/main.go"), ) env.OpenFile("a/main.go") env.Await( From d19e3d18a3dfcd46fc59c1c786758204d5cc039d Mon Sep 17 00:00:00 2001 From: Dung Le Date: Thu, 12 Jan 2023 16:20:26 -0500 Subject: [PATCH 619/723] internal/regtest/bench: fix BenchmarkRename and add more benchmark tests for gopls Change-Id: I8863f3f5b5a50b92a3e844b4fec1902bda1061ce Reviewed-on: https://go-review.googlesource.com/c/tools/+/461802 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Dylan Le --- .../regtest/bench/editor_features_test.go | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/gopls/internal/regtest/bench/editor_features_test.go b/gopls/internal/regtest/bench/editor_features_test.go index 92df3446695..bba6baae1d7 100644 --- a/gopls/internal/regtest/bench/editor_features_test.go +++ b/gopls/internal/regtest/bench/editor_features_test.go @@ -47,9 +47,37 @@ func BenchmarkRename(b *testing.B) { b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := 1; i < b.N; i++ { pos := env.RegexpSearch("internal/imports/mod.go", "gopathwalk") newName := fmt.Sprintf("%s%d", "gopathwalk", i) env.Rename("internal/imports/mod.go", pos, newName) } } + +func BenchmarkFindAllImplementations(b *testing.B) { + env := benchmarkEnv(b) + + env.OpenFile("internal/imports/mod.go") + pos := env.RegexpSearch("internal/imports/mod.go", "initAllMods") + env.Await(env.DoneWithOpen()) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + env.Implementations("internal/imports/mod.go", pos) + } +} + +func BenchmarkHover(b *testing.B) { + env := benchmarkEnv(b) + + env.OpenFile("internal/imports/mod.go") + pos := env.RegexpSearch("internal/imports/mod.go", "bytes") + env.Await(env.DoneWithOpen()) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + env.Hover("internal/imports/mod.go", pos) + } +} From e81af27852c63b9432c0b5bb49707a7f207ef21b Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 10 Jan 2023 16:24:18 -0500 Subject: [PATCH 620/723] gopls: update golang.org/x/vuln@6ad3e3d govulncheck.DefaultCache interface was changed to return error. Update callsites. Notable changes: vulncheck performance improvements (golang/go#57357) https://go-review.googlesource.com/c/vuln/+/460422 https://go-review.googlesource.com/c/vuln/+/460420 store the vuln info cache under module cache directory https://go-review.googlesource.com/c/vuln/+/459036 https://go-review.googlesource.com/c/vuln/+/460421 Change-Id: Ie3816f31b4e1178bb2067cb09c1c726dd37fad55 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461417 Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/go.mod | 4 ++-- gopls/go.sum | 2 ++ gopls/internal/vulncheck/command.go | 12 ++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/gopls/go.mod b/gopls/go.mod index 2830c165bf3..3b86c1fe724 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -11,8 +11,8 @@ require ( golang.org/x/sync v0.1.0 golang.org/x/sys v0.4.0 golang.org/x/text v0.6.0 - golang.org/x/tools v0.4.0 - golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a + golang.org/x/tools v0.4.1-0.20221217013628-b4dfc36097e2 + golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815 gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.3.3 mvdan.cc/gofumpt v0.4.0 diff --git a/gopls/go.sum b/gopls/go.sum index 6f6866773d6..ba8eabfa058 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -76,6 +76,8 @@ golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a h1:KWIh6uTTw7r3PEz1N1OIEM8pr5bf1uP1n6JL5Ml56X8= golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a/go.mod h1:54iI0rrZVM8VdIvTrT/sdlVfMUJWOgvTRQN24CEtZk0= +golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815 h1:A9kONVi4+AnuOr1dopsibH6hLi1Huy54cbeJxnq4vmU= +golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815/go.mod h1:XJiVExZgoZfrrxoTeVsFYrSSk1snhfpOEC95JL+A4T0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index c45096689d0..1f171f09d62 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -70,8 +70,12 @@ func init() { return err } logf("Loaded %d packages and their dependencies", len(pkgs)) + cache, err := govulncheck.DefaultCache() + if err != nil { + return err + } cli, err := client.NewClient(findGOVULNDB(cfg.Env), client.Options{ - HTTPCache: govulncheck.DefaultCache(), + HTTPCache: cache, }) if err != nil { return err @@ -232,9 +236,13 @@ func vulnerablePackages(ctx context.Context, snapshot source.Snapshot, modfile s } // Request vuln entries from remote service. + fsCache, err := govulncheck.DefaultCache() + if err != nil { + return nil, err + } cli, err := client.NewClient( findGOVULNDB(snapshot.View().Options().EnvSlice()), - client.Options{HTTPCache: govulncheck.NewInMemoryCache(govulncheck.DefaultCache())}) + client.Options{HTTPCache: govulncheck.NewInMemoryCache(fsCache)}) if err != nil { return nil, err } From c9b82f2d1e1da7fccab401c6475d1b0e3fe23ff1 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 15:29:14 -0500 Subject: [PATCH 621/723] gopls/internal/regtest: eliminate EmptyDiagnostics It doesn't matter from the user's perspective whether gopls has sent empty diagnostics, or no diagnostics. As long as we use an AfterChange check (or have previously asserted that diagnostics are non-empty), there is no problem replacing the EmptyDiagnostics expectation with NoDiagnostics. Using this principle, eliminate uses of EmptyDiagnostics. Updates golang/go#39384 Change-Id: I5940c46bbb9943a930aeedbae74c3d3314b13574 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461801 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/regtest/expectation.go | 15 -- .../regtest/codelens/codelens_test.go | 15 +- .../regtest/codelens/gcdetails_test.go | 8 +- .../regtest/diagnostics/analysis_test.go | 13 +- .../regtest/diagnostics/diagnostics_test.go | 190 +++++++++--------- .../regtest/misc/configuration_test.go | 7 +- gopls/internal/regtest/misc/embed_test.go | 4 +- gopls/internal/regtest/misc/fix_test.go | 6 +- gopls/internal/regtest/misc/generate_test.go | 6 +- gopls/internal/regtest/misc/imports_test.go | 14 +- gopls/internal/regtest/misc/rename_test.go | 2 +- gopls/internal/regtest/misc/shared_test.go | 7 +- .../internal/regtest/modfile/modfile_test.go | 40 ++-- .../regtest/template/template_test.go | 17 +- gopls/internal/regtest/watch/watch_test.go | 101 +++++----- .../internal/regtest/workspace/broken_test.go | 13 +- .../regtest/workspace/metadata_test.go | 23 +-- .../regtest/workspace/workspace_test.go | 42 ++-- 18 files changed, 237 insertions(+), 286 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 55280ad2fea..37113861204 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -717,21 +717,6 @@ func NoOutstandingDiagnostics() Expectation { } } -// EmptyDiagnostics asserts that empty diagnostics are sent for the -// workspace-relative path name. -func EmptyDiagnostics(name string) Expectation { - check := func(s State) Verdict { - if diags := s.diagnostics[name]; diags != nil && len(diags.Diagnostics) == 0 { - return Met - } - return Unmet - } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("empty diagnostics for %q", name), - } -} - // NoDiagnostics asserts that either no diagnostics are sent for the // workspace-relative path name, or empty diagnostics are sent. func NoDiagnostics(name string) Expectation { diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index b03b96f0ad3..8ea5e9c6fcf 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -223,11 +223,11 @@ require golang.org/x/hello v1.2.3 env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades, nil) env.Await(env.DiagnosticAtRegexpWithMessage("b/go.mod", `require`, "can be upgraded")) env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics, nil) - env.Await(EmptyDiagnostics("b/go.mod")) + env.Await(NoDiagnostics("b/go.mod")) // Apply the diagnostics to a/go.mod. env.ApplyQuickFixes("a/go.mod", d.Diagnostics) - env.Await(env.DoneWithChangeWatchedFiles()) + env.AfterChange() if got := env.BufferText("a/go.mod"); got != wantGoModA { t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) } @@ -320,18 +320,19 @@ func Foo() { Run(t, workspace, func(t *testing.T, env *Env) { // Open the file. We have a nonexistant symbol that will break cgo processing. env.OpenFile("cgo.go") - env.Await(env.DiagnosticAtRegexpWithMessage("cgo.go", ``, "go list failed to return CompiledGoFiles")) + env.AfterChange( + env.DiagnosticAtRegexpWithMessage("cgo.go", ``, "go list failed to return CompiledGoFiles"), + ) // Fix the C function name. We haven't regenerated cgo, so nothing should be fixed. env.RegexpReplace("cgo.go", `int fortythree`, "int fortytwo") env.SaveBuffer("cgo.go") - env.Await(OnceMet( - env.DoneWithSave(), + env.AfterChange( env.DiagnosticAtRegexpWithMessage("cgo.go", ``, "go list failed to return CompiledGoFiles"), - )) + ) // Regenerate cgo, fixing the diagnostic. env.ExecuteCodeLensCommand("cgo.go", command.RegenerateCgo, nil) - env.Await(EmptyDiagnostics("cgo.go")) + env.Await(NoDiagnostics("cgo.go")) }) } diff --git a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go index fdf3ae26889..5f812f43478 100644 --- a/gopls/internal/regtest/codelens/gcdetails_test.go +++ b/gopls/internal/regtest/codelens/gcdetails_test.go @@ -68,18 +68,16 @@ func main() { // Editing a buffer should cause gc_details diagnostics to disappear, since // they only apply to saved buffers. env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, "\n\n")) - env.Await(EmptyDiagnostics("main.go")) + env.AfterChange(NoDiagnostics("main.go")) // Saving a buffer should re-format back to the original state, and // re-enable the gc_details diagnostics. env.SaveBuffer("main.go") - env.Await(DiagnosticAt("main.go", 5, 13)) + env.AfterChange(DiagnosticAt("main.go", 5, 13)) // Toggle the GC details code lens again so now it should be off. env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) - env.Await( - EmptyDiagnostics("main.go"), - ) + env.Await(NoDiagnostics("main.go")) }) } diff --git a/gopls/internal/regtest/diagnostics/analysis_test.go b/gopls/internal/regtest/diagnostics/analysis_test.go index 56ee23f3f85..bad4f655e11 100644 --- a/gopls/internal/regtest/diagnostics/analysis_test.go +++ b/gopls/internal/regtest/diagnostics/analysis_test.go @@ -38,17 +38,12 @@ func main() { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DoneWithOpen(), - env.DiagnosticAtRegexp("main.go", "2006-02-01"), - ReadDiagnostics("main.go", &d), - ), + env.AfterChange( + env.DiagnosticAtRegexp("main.go", "2006-02-01"), + ReadDiagnostics("main.go", &d), ) env.ApplyQuickFixes("main.go", d.Diagnostics) - env.Await( - EmptyDiagnostics("main.go"), - ) + env.AfterChange(NoDiagnostics("main.go")) }) } diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 6f91c00a01d..6631a9234ca 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -82,9 +82,7 @@ func m() { env.DiagnosticAtRegexp("main.go", "log"), ) env.SaveBuffer("main.go") - env.Await( - EmptyDiagnostics("main.go"), - ) + env.AfterChange(NoDiagnostics("main.go")) }) } @@ -118,13 +116,16 @@ const a = 2 func TestDiagnosticClearingOnEdit(t *testing.T) { Run(t, badPackage, func(t *testing.T, env *Env) { env.OpenFile("b.go") - env.Await(env.DiagnosticAtRegexp("a.go", "a = 1"), env.DiagnosticAtRegexp("b.go", "a = 2")) + env.AfterChange( + env.DiagnosticAtRegexp("a.go", "a = 1"), + env.DiagnosticAtRegexp("b.go", "a = 2"), + ) // Fix the error by editing the const name in b.go to `b`. env.RegexpReplace("b.go", "(a) = 2", "b") - env.Await( - EmptyDiagnostics("a.go"), - EmptyDiagnostics("b.go"), + env.AfterChange( + NoDiagnostics("a.go"), + NoDiagnostics("b.go"), ) }) } @@ -135,7 +136,10 @@ func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) { env.Await(env.DiagnosticAtRegexp("a.go", "a = 1"), env.DiagnosticAtRegexp("b.go", "a = 2")) env.RemoveWorkspaceFile("b.go") - env.Await(EmptyDiagnostics("a.go"), EmptyDiagnostics("b.go")) + env.Await( + NoDiagnostics("a.go"), + NoDiagnostics("b.go"), + ) }) } @@ -144,16 +148,16 @@ func TestDiagnosticClearingOnClose(t *testing.T) { env.CreateBuffer("c.go", `package consts const a = 3`) - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("a.go", "a = 1"), env.DiagnosticAtRegexp("b.go", "a = 2"), env.DiagnosticAtRegexp("c.go", "a = 3"), ) env.CloseBuffer("c.go") - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("a.go", "a = 1"), env.DiagnosticAtRegexp("b.go", "a = 2"), - EmptyDiagnostics("c.go"), + NoDiagnostics("c.go"), ) }) } @@ -171,14 +175,14 @@ func TestIssue37978(t *testing.T) { const a = http.MethodGet `, }) - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("c/c.go", "http.MethodGet"), ) // Save file, which will organize imports, adding the expected import. // Expect the diagnostics to clear. env.SaveBuffer("c/c.go") - env.Await( - EmptyDiagnostics("c/c.go"), + env.AfterChange( + NoDiagnostics("c/c.go"), ) }) } @@ -214,13 +218,13 @@ func TestDeleteTestVariant(t *testing.T) { Run(t, test38878, func(t *testing.T, env *Env) { env.Await(env.DiagnosticAtRegexp("a_test.go", `f\((3)\)`)) env.RemoveWorkspaceFile("a_test.go") - env.Await(EmptyDiagnostics("a_test.go")) + env.AfterChange(NoDiagnostics("a_test.go")) // Make sure the test variant has been removed from the workspace by // triggering a metadata load. env.OpenFile("a.go") env.RegexpReplace("a.go", `// import`, "import") - env.Await(env.DiagnosticAtRegexp("a.go", `"fmt"`)) + env.AfterChange(env.DiagnosticAtRegexp("a.go", `"fmt"`)) }) } @@ -260,15 +264,18 @@ func Hello() { t.Run("manual", func(t *testing.T) { Run(t, noMod, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), + ), ) env.CreateBuffer("go.mod", `module mod.com go 1.12 `) env.SaveBuffer("go.mod") - env.Await( - EmptyDiagnostics("main.go"), + env.AfterChange( + NoDiagnostics("main.go"), ) var d protocol.PublishDiagnosticsParams env.Await( @@ -288,8 +295,8 @@ func Hello() { env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), ) env.RunGoCommand("mod", "init", "mod.com") - env.Await( - EmptyDiagnostics("main.go"), + env.AfterChange( + NoDiagnostics("main.go"), env.DiagnosticAtRegexp("bob/bob.go", "x"), ) }) @@ -305,8 +312,8 @@ func Hello() { if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, true); err != nil { t.Fatal(err) } - env.Await( - EmptyDiagnostics("main.go"), + env.AfterChange( + NoDiagnostics("main.go"), env.DiagnosticAtRegexp("bob/bob.go", "x"), ) }) @@ -354,9 +361,9 @@ func TestHello(t *testing.T) { ) env.OpenFile("lib.go") env.RegexpReplace("lib.go", "_ = x", "var y int") - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("lib.go", "y int"), - EmptyDiagnostics("lib_test.go"), + NoDiagnostics("lib_test.go"), ) }) } @@ -375,16 +382,8 @@ func main() {} Run(t, packageChange, func(t *testing.T, env *Env) { env.OpenFile("a.go") env.RegexpReplace("a.go", "foo", "foox") - env.Await( - // When the bug reported in #38328 was present, we didn't get erroneous - // file diagnostics until after the didChange message generated by the - // package renaming was fully processed. Therefore, in order for this - // test to actually exercise the bug, we must wait until that work has - // completed. - OnceMet( - env.DoneWithChange(), - EmptyDiagnostics("a.go"), - ), + env.AfterChange( + NoDiagnostics("a.go"), ) }) } @@ -476,9 +475,9 @@ func _() { }, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - env.Await(env.DiagnosticAtRegexp("main.go", "fmt")) + env.AfterChange(env.DiagnosticAtRegexp("main.go", "fmt")) env.SaveBuffer("main.go") - env.Await(EmptyDiagnostics("main.go")) + env.AfterChange(NoDiagnostics("main.go")) }) } @@ -503,7 +502,7 @@ var X = 0 ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OrganizeImports("main.go") - env.Await(EmptyDiagnostics("main.go")) + env.AfterChange(NoDiagnostics("main.go")) }) } @@ -667,8 +666,8 @@ func main() { ) env.ApplyQuickFixes("main.go", d.Diagnostics) env.SaveBuffer("go.mod") - env.Await( - EmptyDiagnostics("main.go"), + env.AfterChange( + NoDiagnostics("main.go"), ) // Comment out the line that depends on conf and expect a // diagnostic and a fix to remove the import. @@ -678,22 +677,20 @@ func main() { ) env.SaveBuffer("main.go") // Expect a diagnostic and fix to remove the dependency in the go.mod. - env.Await(EmptyDiagnostics("main.go")) - env.Await( - OnceMet( - env.DiagnosticAtRegexpWithMessage("go.mod", "require github.com/ardanlabs/conf", "not used in this module"), - ReadDiagnostics("go.mod", &d), - ), + env.AfterChange( + NoDiagnostics("main.go"), + env.DiagnosticAtRegexpWithMessage("go.mod", "require github.com/ardanlabs/conf", "not used in this module"), + ReadDiagnostics("go.mod", &d), ) env.ApplyQuickFixes("go.mod", d.Diagnostics) env.SaveBuffer("go.mod") - env.Await( - EmptyDiagnostics("go.mod"), + env.AfterChange( + NoDiagnostics("go.mod"), ) // Uncomment the lines and expect a new diagnostic for the import. env.RegexpReplace("main.go", "//_ = conf.ErrHelpWanted", "_ = conf.ErrHelpWanted") env.SaveBuffer("main.go") - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`), ) }) @@ -728,8 +725,8 @@ func main() { ), ) env.ApplyQuickFixes("main.go", d.Diagnostics) - env.Await( - EmptyDiagnostics("main.go"), + env.AfterChange( + NoDiagnostics("main.go"), ) }) } @@ -754,20 +751,12 @@ func _() { env.OpenFile("a/a1.go") env.CreateBuffer("a/a2.go", ``) env.SaveBufferWithoutActions("a/a2.go") - // We can't use OnceMet here (at least, not easily) because the didSave - // races with the didChangeWatchedFiles. - // - // TODO(rfindley): add an AllOf expectation combinator, or an expectation - // that all notifications have been processed. - env.Await( - EmptyDiagnostics("a/a1.go"), + env.AfterChange( + NoDiagnostics("a/a1.go"), ) env.EditBuffer("a/a2.go", fake.NewEdit(0, 0, 0, 0, `package a`)) - env.Await( - OnceMet( - env.DoneWithChange(), - EmptyDiagnostics("a/a1.go"), - ), + env.AfterChange( + NoDiagnostics("a/a1.go"), ) }) } @@ -801,7 +790,7 @@ func TestHello(t *testing.T) { // Open the file, triggering the workspace load. // There are errors in the code to ensure all is working as expected. env.OpenFile("hello/hello.go") - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("hello/hello.go", "x"), env.DiagnosticAtRegexp("hello/hello_test.go", "x"), ) @@ -830,12 +819,12 @@ func TestHello(t *testing.T) { `)) // Expect a diagnostic for the missing import. Save, which should // trigger import organization. The diagnostic should clear. - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("hello/hello_x_test.go", "hello.Hello"), ) env.SaveBuffer("hello/hello_x_test.go") - env.Await( - EmptyDiagnostics("hello/hello_x_test.go"), + env.AfterChange( + NoDiagnostics("hello/hello_x_test.go"), ) }) } @@ -906,11 +895,9 @@ var _ = foo.Bar ` Run(t, ws, func(t *testing.T, env *Env) { env.OpenFile("_foo/x.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - EmptyDiagnostics("_foo/x.go"), - )) + env.AfterChange( + NoDiagnostics("_foo/x.go"), + ) }) } @@ -1031,10 +1018,10 @@ func TestDoIt(t *testing.T) { env.DiagnosticAtRegexp("p/x_test.go", "5"), ) env.RegexpReplace("p/p.go", "s string", "i int") - env.Await( - EmptyDiagnostics("main.go"), - EmptyDiagnostics("p/p_test.go"), - EmptyDiagnostics("p/x_test.go"), + env.AfterChange( + NoDiagnostics("main.go"), + NoDiagnostics("p/p_test.go"), + NoDiagnostics("p/x_test.go"), ) }) }) @@ -1249,15 +1236,15 @@ func main() { ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams - env.Await(OnceMet( + env.AfterChange( env.DiagnosticAtRegexpWithMessage("main.go", `t{"msg"}`, "redundant type"), ReadDiagnostics("main.go", &d), - )) + ) if tags := d.Diagnostics[0].Tags; len(tags) == 0 || tags[0] != protocol.Unnecessary { t.Errorf("wanted Unnecessary tag on diagnostic, got %v", tags) } env.ApplyQuickFixes("main.go", d.Diagnostics) - env.Await(EmptyDiagnostics("main.go")) + env.AfterChange(NoDiagnostics("main.go")) }) } @@ -1479,10 +1466,8 @@ package foo_ env.RegexpReplace("foo/foo_test.go", "_", "_t") env.Await(env.DoneWithChange()) env.RegexpReplace("foo/foo_test.go", "_t", "_test") - env.Await(env.DoneWithChange()) - - env.Await( - EmptyDiagnostics("foo/foo_test.go"), + env.AfterChange( + NoDiagnostics("foo/foo_test.go"), NoOutstandingWork(), ) }) @@ -1542,11 +1527,16 @@ func main() { } ` Run(t, mod, func(t *testing.T, env *Env) { - env.Await(FileWatchMatching("bob")) - env.RemoveWorkspaceFile("bob") env.Await( + OnceMet( + InitialWorkspaceLoad, + FileWatchMatching("bob"), + ), + ) + env.RemoveWorkspaceFile("bob") + env.AfterChange( env.DiagnosticAtRegexp("cmd/main.go", `"mod.com/bob"`), - EmptyDiagnostics("bob/bob.go"), + NoDiagnostics("bob/bob.go"), NoFileWatchMatching("bob"), ) }) @@ -1782,9 +1772,9 @@ var Bar = Foo Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo.go") - env.Await(env.DiagnosticAtRegexp("bar.go", `Foo`)) + env.AfterChange(env.DiagnosticAtRegexp("bar.go", `Foo`)) env.RegexpReplace("foo.go", `\+build`, "") - env.Await(EmptyDiagnostics("bar.go")) + env.AfterChange(NoDiagnostics("bar.go")) }) } @@ -1887,9 +1877,16 @@ package main const C = 0b10 ` Run(t, files, func(t *testing.T, env *Env) { - env.Await(env.DiagnosticAtRegexpWithMessage("main.go", `0b10`, "go1.13 or later")) + env.Await( + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexpWithMessage("main.go", `0b10`, "go1.13 or later"), + ), + ) env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.13\n") - env.Await(EmptyDiagnostics("main.go")) + env.AfterChange( + NoDiagnostics("main.go"), + ) }) } @@ -1938,15 +1935,15 @@ func F[T any](_ T) { var d protocol.PublishDiagnosticsParams env.Await( OnceMet( + InitialWorkspaceLoad, env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), ReadDiagnostics("main.go", &d), ), ) env.ApplyQuickFixes("main.go", d.Diagnostics) - - env.Await( - EmptyDiagnostics("main.go"), + env.AfterChange( + NoDiagnostics("main.go"), ) }) } @@ -1986,11 +1983,8 @@ func F[T any](_ T) { // Once the edit is applied, the problematic diagnostics should be // resolved. - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), // go.mod should have been quick-fixed - EmptyDiagnostics("main.go"), - ), + env.AfterChange( + NoDiagnostics("main.go"), ) }) } diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index 5bb2c8620a0..15aeeb42e30 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -35,14 +35,15 @@ var FooErr = errors.New("foo") ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - env.Await( - env.DoneWithOpen(), - EmptyDiagnostics("a/a.go"), + env.AfterChange( + NoDiagnostics("a/a.go"), ) cfg := env.Editor.Config() cfg.Settings = map[string]interface{}{ "staticcheck": true, } + // TODO(rfindley): support waiting on diagnostics following a configuration + // change. env.ChangeConfiguration(cfg) env.Await( DiagnosticAt("a/a.go", 5, 4), diff --git a/gopls/internal/regtest/misc/embed_test.go b/gopls/internal/regtest/misc/embed_test.go index 0fadda03e6e..0d6d2ddd1d1 100644 --- a/gopls/internal/regtest/misc/embed_test.go +++ b/gopls/internal/regtest/misc/embed_test.go @@ -28,8 +28,8 @@ var foo string ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("x.go") - env.Await(env.DiagnosticAtRegexpWithMessage("x.go", `NONEXISTENT`, "no matching files found")) + env.AfterChange(env.DiagnosticAtRegexpWithMessage("x.go", `NONEXISTENT`, "no matching files found")) env.RegexpReplace("x.go", `NONEXISTENT`, "x.go") - env.Await(EmptyDiagnostics("x.go")) + env.AfterChange(NoDiagnostics("x.go")) }) } diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index 8939e004dc7..9d51eefef7c 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -77,11 +77,11 @@ func Foo() error { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams - env.Await(OnceMet( + env.AfterChange( // The error message here changed in 1.18; "return values" covers both forms. env.DiagnosticAtRegexpWithMessage("main.go", `return`, "return values"), 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)) @@ -102,6 +102,6 @@ func Foo() error { t.Fatalf("expected fixall code action, got none") } env.ApplyQuickFixes("main.go", d.Diagnostics) - env.Await(EmptyDiagnostics("main.go")) + env.AfterChange(NoDiagnostics("main.go")) }) } diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go index ca3461d68fe..f6bbcd5afe4 100644 --- a/gopls/internal/regtest/misc/generate_test.go +++ b/gopls/internal/regtest/misc/generate_test.go @@ -64,10 +64,8 @@ func main() { ) env.RunGenerate("./lib1") env.RunGenerate("./lib2") - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - EmptyDiagnostics("main.go")), + env.AfterChange( + NoDiagnostics("main.go"), ) }) } diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index 221a6ed2520..9e77d549610 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -156,9 +156,9 @@ var _, _ = x.X, y.Y ProxyFiles(proxy), ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - env.Await(env.DiagnosticAtRegexp("main.go", `y.Y`)) + env.AfterChange(env.DiagnosticAtRegexp("main.go", `y.Y`)) env.SaveBuffer("main.go") - env.Await(EmptyDiagnostics("main.go")) + env.AfterChange(NoDiagnostics("main.go")) path, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `y.(Y)`)) if !strings.HasPrefix(path, filepath.ToSlash(modcache)) { t.Errorf("found module dependency outside of GOMODCACHE: got %v, wanted subdir of %v", path, filepath.ToSlash(modcache)) @@ -199,15 +199,15 @@ func TestA(t *testing.T) { Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") var d protocol.PublishDiagnosticsParams - env.Await( + env.AfterChange( OnceMet( env.DiagnosticAtRegexp("a/a.go", "os.Stat"), ReadDiagnostics("a/a.go", &d), ), ) env.ApplyQuickFixes("a/a.go", d.Diagnostics) - env.Await( - EmptyDiagnostics("a/a.go"), + env.AfterChange( + NoDiagnostics("a/a.go"), ) }) } @@ -249,11 +249,11 @@ func Test() { ` Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("caller/caller.go") - env.Await(env.DiagnosticAtRegexp("caller/caller.go", "a.Test")) + env.AfterChange(env.DiagnosticAtRegexp("caller/caller.go", "a.Test")) // Saving caller.go should trigger goimports, which should find a.Test in // the mod.com module, thanks to the go.work file. env.SaveBuffer("caller/caller.go") - env.Await(EmptyDiagnostics("caller/caller.go")) + env.AfterChange(NoDiagnostics("caller/caller.go")) }) } diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index ebbe5d465f7..ef94ae25639 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -414,7 +414,7 @@ package b // Moving x.go should make the diagnostic go away. env.RenameFile("a/x.go", "b/x.go") env.AfterChange( - EmptyDiagnostics("a/a.go"), // no more duplicate declarations + NoDiagnostics("a/a.go"), // no more duplicate declarations env.DiagnosticAtRegexp("b/b.go", "package"), // as package names mismatch ) diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index e47ca2959d1..23f7a42872e 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -64,11 +64,8 @@ func main() { } env1.RegexpReplace("main.go", "Printl", "Println") - env1.Await( - OnceMet( - env1.DoneWithChange(), - EmptyDiagnostics("main.go"), - ), + env1.AfterChange( + NoDiagnostics("main.go"), ) }) } diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 2a178e49a29..21fd417d930 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -497,10 +497,15 @@ func main() { {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, {"nested", WithOptions(ProxyFiles(proxy))}, }.Run(t, mod, func(t *testing.T, env *Env) { - env.Await(env.DiagnosticAtRegexp("a/go.mod", "require")) - env.RunGoCommandInDir("a", "mod", "tidy") env.Await( - EmptyDiagnostics("a/go.mod"), + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/go.mod", "require"), + ), + ) + env.RunGoCommandInDir("a", "mod", "tidy") + env.AfterChange( + NoDiagnostics("a/go.mod"), ) }) } @@ -573,7 +578,7 @@ require ( ) ` env.SaveBuffer("a/go.mod") - env.AfterChange(EmptyDiagnostics("a/main.go")) + env.AfterChange(NoDiagnostics("a/main.go")) if got := env.BufferText("a/go.mod"); got != want { t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got)) } @@ -629,7 +634,7 @@ func main() { env.ApplyCodeAction(qfs[0]) // Arbitrarily pick a single fix to apply. Applying all of them seems to cause trouble in this particular test. env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. env.AfterChange( - EmptyDiagnostics("a/go.mod"), + NoDiagnostics("a/go.mod"), env.DiagnosticAtRegexp("a/main.go", "x = "), ) }) @@ -758,8 +763,8 @@ func main() { } env.RunGoCommand("get", "example.com/blah@v1.2.3") env.RunGoCommand("mod", "tidy") - env.Await( - EmptyDiagnostics("main.go"), + env.AfterChange( + NoDiagnostics("main.go"), ) }) } @@ -887,16 +892,14 @@ func main() { ).Run(t, mod, func(t *testing.T, env *Env) { d := &protocol.PublishDiagnosticsParams{} env.OpenFile("go.mod") - env.Await( - OnceMet( - env.GoSumDiagnostic("go.mod", `example.com v1.2.3`), - ReadDiagnostics("go.mod", d), - ), + env.AfterChange( + env.GoSumDiagnostic("go.mod", `example.com v1.2.3`), + ReadDiagnostics("go.mod", d), ) env.ApplyQuickFixes("go.mod", d.Diagnostics) env.SaveBuffer("go.mod") // Save to trigger diagnostics. - env.Await( - EmptyDiagnostics("go.mod"), + env.AfterChange( + NoDiagnostics("go.mod"), ) }) } @@ -1182,9 +1185,14 @@ go foo package main ` Run(t, files, func(t *testing.T, env *Env) { - env.Await(env.DiagnosticAtRegexpWithMessage("go.mod", `go foo`, "invalid go version")) + env.Await( + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexpWithMessage("go.mod", `go foo`, "invalid go version"), + ), + ) env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.12\n") - env.Await(EmptyDiagnostics("go.mod")) + env.AfterChange(NoDiagnostics("go.mod")) }) } diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index c82c8291066..7ccf08d2db2 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -98,7 +98,7 @@ Hello {{}} <-- missing body } env.WriteWorkspaceFile("hello.tmpl", "{{range .Planets}}\nHello {{.}}\n{{end}}") - env.AfterChange(EmptyDiagnostics("hello.tmpl")) + env.AfterChange(NoDiagnostics("hello.tmpl")) }) } @@ -140,11 +140,8 @@ go 1.12 Run(t, files, func(t *testing.T, env *Env) { env.CreateBuffer("hello.tmpl", "") - env.Await( - OnceMet( - env.DoneWithOpen(), - EmptyDiagnostics("hello.tmpl"), // Don't get spurious errors for empty templates. - ), + env.AfterChange( + NoDiagnostics("hello.tmpl"), // Don't get spurious errors for empty templates. ) env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}") env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) @@ -167,11 +164,15 @@ Hello {{}} <-- missing body Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("hello.tmpl") - env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) + env.AfterChange( + env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"), + ) // Since we don't have templateExtensions configured, closing hello.tmpl // should make its diagnostics disappear. env.CloseBuffer("hello.tmpl") - env.Await(EmptyDiagnostics("hello.tmpl")) + env.AfterChange( + NoDiagnostics("hello.tmpl"), + ) }) } diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 5a036567377..70376a91810 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -45,7 +45,7 @@ func _() { ) env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) env.AfterChange( - EmptyDiagnostics("a/a.go"), + NoDiagnostics("a/a.go"), ) }) }) @@ -125,7 +125,10 @@ func _() { ` Run(t, pkg, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("a/a.go", "x"), + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "x"), + ), ) env.WriteWorkspaceFiles(map[string]string{ "b/b.go": `package b; func B() {};`, @@ -137,8 +140,8 @@ func _() { b.B() }`, }) - env.Await( - EmptyDiagnostics("a/a.go"), + env.AfterChange( + NoDiagnostics("a/a.go"), NoDiagnostics("b/b.go"), ) }) @@ -200,11 +203,14 @@ func _() { ` Run(t, missing, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""), + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""), + ), ) env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) - env.Await( - EmptyDiagnostics("a/a.go"), + env.AfterChange( + NoDiagnostics("a/a.go"), ) }) } @@ -247,11 +253,14 @@ func _() { ` Run(t, pkg, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("a/a.go", "hello"), + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "hello"), + ), ) env.WriteWorkspaceFile("a/a2.go", `package a; func hello() {};`) - env.Await( - EmptyDiagnostics("a/a.go"), + env.AfterChange( + NoDiagnostics("a/a.go"), ) }) } @@ -322,15 +331,12 @@ func _() { t.Run("method before implementation", func(t *testing.T) { Run(t, pkg, func(t *testing.T, env *Env) { env.WriteWorkspaceFile("b/b.go", newMethod) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - DiagnosticAt("a/a.go", 12, 12), - ), + env.AfterChange( + DiagnosticAt("a/a.go", 12, 12), ) env.WriteWorkspaceFile("a/a.go", implementation) - env.Await( - EmptyDiagnostics("a/a.go"), + env.AfterChange( + NoDiagnostics("a/a.go"), ) }) }) @@ -338,14 +344,11 @@ func _() { t.Run("implementation before method", func(t *testing.T) { Run(t, pkg, func(t *testing.T, env *Env) { env.WriteWorkspaceFile("a/a.go", implementation) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a.go"), - ), + env.AfterChange( + NoDiagnostics("a/a.go"), ) env.WriteWorkspaceFile("b/b.go", newMethod) - env.Await( + env.AfterChange( NoDiagnostics("a/a.go"), ) }) @@ -357,11 +360,8 @@ func _() { "a/a.go": implementation, "b/b.go": newMethod, }) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a.go"), - ), + env.AfterChange( + NoDiagnostics("a/a.go"), NoDiagnostics("b/b.go"), ) }) @@ -406,15 +406,12 @@ package a env.DiagnosticAtRegexp("a/a.go", "fmt"), ) env.SaveBuffer("a/a.go") - env.Await( - OnceMet( - env.DoneWithSave(), - // There should only be one log message containing - // a_unneeded.go, from the initial workspace load, which we - // check for earlier. If there are more, there's a bug. - LogMatching(protocol.Info, "a_unneeded.go", 1, false), - ), - EmptyDiagnostics("a/a.go"), + env.AfterChange( + // There should only be one log message containing + // a_unneeded.go, from the initial workspace load, which we + // check for earlier. If there are more, there's a bug. + LogMatching(protocol.Info, "a_unneeded.go", 1, false), + NoDiagnostics("a/a.go"), ) }) }) @@ -440,15 +437,12 @@ package a env.DiagnosticAtRegexp("a/a.go", "fmt"), ) env.SaveBuffer("a/a.go") - env.Await( - OnceMet( - env.DoneWithSave(), - // There should only be one log message containing - // a_unneeded.go, from the initial workspace load, which we - // check for earlier. If there are more, there's a bug. - LogMatching(protocol.Info, "a_unneeded.go", 1, false), - ), - EmptyDiagnostics("a/a.go"), + env.AfterChange( + // There should only be one log message containing + // a_unneeded.go, from the initial workspace load, which we + // check for earlier. If there are more, there's a bug. + LogMatching(protocol.Info, "a_unneeded.go", 1, false), + NoDiagnostics("a/a.go"), ) }) }) @@ -595,7 +589,7 @@ func main() { ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.AfterChange( - EmptyDiagnostics("main.go"), + NoDiagnostics("main.go"), ) if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, true); err != nil { t.Fatal(err) @@ -609,7 +603,7 @@ func main() { env.RegexpReplace("main.go", `"foo/blah"`, `"mod.com/foo/blah"`) env.AfterChange( - EmptyDiagnostics("main.go"), + NoDiagnostics("main.go"), ) }) } @@ -640,15 +634,12 @@ func main() { ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/main.go") env.RemoveWorkspaceFile("foo/go.mod") - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - env.DiagnosticAtRegexp("foo/main.go", `"mod.com/blah"`), - ), + env.AfterChange( + env.DiagnosticAtRegexp("foo/main.go", `"mod.com/blah"`), ) env.RegexpReplace("foo/main.go", `"mod.com/blah"`, `"foo/blah"`) - env.Await( - EmptyDiagnostics("foo/main.go"), + env.AfterChange( + NoDiagnostics("foo/main.go"), ) }) } diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index e9cc524819c..711d59617be 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -222,8 +222,8 @@ package b // workspace folder, therefore we can't invoke AfterChange here. env.ChangeWorkspaceFolders("a", "b") env.Await( - EmptyDiagnostics("a/a.go"), - EmptyDiagnostics("b/go.mod"), + NoDiagnostics("a/a.go"), + NoDiagnostics("b/go.mod"), NoOutstandingWork(), ) @@ -239,7 +239,7 @@ package b // (better) trying to get workspace packages for each open file. See // also golang/go#54261. env.OpenFile("b/b.go") - env.Await( + env.AfterChange( // TODO(rfindley): fix these missing diagnostics. // env.DiagnosticAtRegexp("a/a.go", "package a"), // env.DiagnosticAtRegexp("b/go.mod", "module b.com"), @@ -258,11 +258,8 @@ package b InGOPATH(), ).Run(t, modules, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - EmptyDiagnostics("a/a.go"), - ), + env.AfterChange( + NoDiagnostics("a/a.go"), NoOutstandingWork(), ) }) diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index 4620b93cbb9..c014f11e921 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -33,10 +33,9 @@ const C = 42 Run(t, src, func(t *testing.T, env *Env) { env.OpenFile("p.go") env.RegexpReplace("p.go", "\"fmt\"", "\"fmt\"\n)") - env.Await(OnceMet( - env.DoneWithChange(), - EmptyDiagnostics("p.go"), - )) + env.AfterChange( + NoDiagnostics("p.go"), + ) }) } @@ -90,23 +89,17 @@ func main() {} // But of course, this should not be necessary: we should invalidate stale // information when fresh metadata arrives. // env.RegexpReplace("foo.go", "package main", "package main // test") - env.Await( - OnceMet( - env.DoneWithChange(), - EmptyDiagnostics("foo.go"), - EmptyDiagnostics("bar.go"), - ), + env.AfterChange( + NoDiagnostics("foo.go"), + NoDiagnostics("bar.go"), ) // If instead of 'ignore' (which gopls treats as a standalone package) we // used a different build tag, we should get a warning about having no // packages for bar.go env.RegexpReplace("bar.go", "ignore", "excluded") - env.Await( - OnceMet( - env.DoneWithChange(), - env.DiagnosticAtRegexpWithMessage("bar.go", "package (main)", "No packages"), - ), + env.AfterChange( + env.DiagnosticAtRegexpWithMessage("bar.go", "package (main)", "No packages"), ) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 022b04f6fd5..a279b8e2a6b 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -151,12 +151,12 @@ func TestClearAnalysisDiagnostics(t *testing.T) { WorkspaceFolders("pkg/inner"), ).Run(t, workspaceModule, func(t *testing.T, env *Env) { env.OpenFile("pkg/main.go") - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("pkg/main2.go", "fmt.Print"), ) env.CloseBuffer("pkg/main.go") - env.Await( - EmptyDiagnostics("pkg/main2.go"), + env.AfterChange( + NoDiagnostics("pkg/main2.go"), ) }) } @@ -971,11 +971,9 @@ func main() { ).Run(t, files, func(t *testing.T, env *Env) { params := &protocol.PublishDiagnosticsParams{} env.OpenFile("b/go.mod") - env.Await( - OnceMet( - env.GoSumDiagnostic("b/go.mod", `example.com v1.2.3`), - ReadDiagnostics("b/go.mod", params), - ), + env.AfterChange( + env.GoSumDiagnostic("b/go.mod", `example.com v1.2.3`), + ReadDiagnostics("b/go.mod", params), ) for _, d := range params.Diagnostics { if !strings.Contains(d.Message, "go.sum is out of sync") { @@ -987,8 +985,8 @@ func main() { } env.ApplyQuickFixes("b/go.mod", []protocol.Diagnostic{d}) } - env.Await( - EmptyDiagnostics("b/go.mod"), + env.AfterChange( + NoDiagnostics("b/go.mod"), ) }) } @@ -1061,7 +1059,7 @@ func main() {} // Since b/main.go is not in the workspace, it should have a warning on its // package declaration. env.AfterChange( - EmptyDiagnostics("main.go"), + NoDiagnostics("main.go"), DiagnosticAt("b/main.go", 0, 0), ) env.WriteWorkspaceFile("go.work", `go 1.16 @@ -1087,7 +1085,7 @@ use ( env.OpenFile("b/main.go") env.AfterChange( - EmptyDiagnostics("main.go"), + NoDiagnostics("main.go"), DiagnosticAt("b/main.go", 0, 0), ) }) @@ -1128,9 +1126,9 @@ func (Server) Foo() {} env.DiagnosticAtRegexp("main_test.go", "otherConst"), ) env.RegexpReplace("other_test.go", "main", "main_test") - env.Await( - EmptyDiagnostics("other_test.go"), - EmptyDiagnostics("main_test.go"), + env.AfterChange( + NoDiagnostics("other_test.go"), + NoDiagnostics("main_test.go"), ) // This will cause a test failure if other_test.go is not in any package. @@ -1176,11 +1174,8 @@ import ( NoDiagnostics("a/main.go"), ) env.OpenFile("a/main.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - env.DiagnosticAtRegexpWithMessage("a/main.go", "V", "not used"), - ), + env.AfterChange( + env.DiagnosticAtRegexpWithMessage("a/main.go", "V", "not used"), ) env.CloseBuffer("a/main.go") @@ -1190,11 +1185,8 @@ import ( // TODO(rfindley): it should not be necessary to make another edit here. // Gopls should be smart enough to avoid diagnosing a. env.RegexpReplace("b/main.go", "package b", "package b // a package") - env.Await( - OnceMet( - env.DoneWithChange(), - EmptyDiagnostics("a/main.go"), - ), + env.AfterChange( + NoDiagnostics("a/main.go"), ) }) } From 331a1c68965eaef2ef44e7d94037cd4d8a7ea346 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 18:12:26 -0500 Subject: [PATCH 622/723] gopls/internal/regtest: add a simpler API for diagnostic expectations The number of different regtest expectations related to diagnostics has grown significantly over time. Start to consolidate to just two expectations: Diagnostics, which asserts on the existence of diagnostics, and NoMatchingDiagnostics, which asserts on the nonexistence of diagnostics. Both accept an argument list to filter the set of diagnostics under consideration. In a subsequent CL, NoMatchingDiagnostics will be renamed to NoDiagnostics, once the existing expectation with that name has been replaced. Use this to eliminate the following expectations: - DiagnosticAtRegexpFromSource -> Diagnostics(AtRegexp, FromSource) - NoDiagnosticAtRegexp -> NoMatchingDiagnostics(AtRegexp) - NoOutstandingDiagnostics -> NoMatchingDiagnostics Updates golang/go#39384 Change-Id: I518b14eb00c9fcf62388a03efb668899939a8f82 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461915 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/regtest/expectation.go | 163 ++++++++++++++---- .../regtest/codelens/codelens_test.go | 2 +- .../regtest/diagnostics/diagnostics_test.go | 2 +- .../internal/regtest/misc/staticcheck_test.go | 21 +-- .../internal/regtest/workspace/broken_test.go | 9 +- .../regtest/workspace/standalone_test.go | 42 +---- .../regtest/workspace/workspace_test.go | 6 +- 7 files changed, 152 insertions(+), 93 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 37113861204..a86dfe10219 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -700,23 +700,144 @@ func (e DiagnosticExpectation) Description() string { return desc } -// NoOutstandingDiagnostics asserts that the workspace has no outstanding -// diagnostic messages. -func NoOutstandingDiagnostics() Expectation { +// Diagnostics asserts that there is at least one diagnostic matching the given +// filters. +func Diagnostics(filters ...DiagnosticFilter) Expectation { check := func(s State) Verdict { - for _, diags := range s.diagnostics { - if len(diags.Diagnostics) > 0 { + diags := flattenDiagnostics(s) + for _, filter := range filters { + var filtered []flatDiagnostic + for _, d := range diags { + if filter.check(d.name, d.diag) { + filtered = append(filtered, d) + } + } + if len(filtered) == 0 { + // TODO(rfindley): if/when expectations describe their own failure, we + // can provide more useful information here as to which filter caused + // the failure. return Unmet } + diags = filtered + } + return Met + } + var descs []string + for _, filter := range filters { + descs = append(descs, filter.desc) + } + return SimpleExpectation{ + check: check, + description: "any diagnostics " + strings.Join(descs, ", "), + } +} + +// NoMatchingDiagnostics asserts that there are no diagnostics matching the +// given filters. Notably, if no filters are supplied this assertion checks +// that there are no diagnostics at all, for any file. +// +// TODO(rfindley): replace NoDiagnostics with this, and rename. +func NoMatchingDiagnostics(filters ...DiagnosticFilter) Expectation { + check := func(s State) Verdict { + diags := flattenDiagnostics(s) + for _, filter := range filters { + var filtered []flatDiagnostic + for _, d := range diags { + if filter.check(d.name, d.diag) { + filtered = append(filtered, d) + } + } + diags = filtered + } + if len(diags) > 0 { + return Unmet } return Met } + var descs []string + for _, filter := range filters { + descs = append(descs, filter.desc) + } return SimpleExpectation{ check: check, - description: "no outstanding diagnostics", + description: "no diagnostics " + strings.Join(descs, ", "), + } +} + +type flatDiagnostic struct { + name string + diag protocol.Diagnostic +} + +func flattenDiagnostics(state State) []flatDiagnostic { + var result []flatDiagnostic + for name, diags := range state.diagnostics { + for _, diag := range diags.Diagnostics { + result = append(result, flatDiagnostic{name, diag}) + } } + return result } +// -- Diagnostic filters -- + +// A DiagnosticFilter filters the set of diagnostics, for assertion with +// Diagnostics or NoMatchingDiagnostics. +type DiagnosticFilter struct { + desc string + check func(name string, _ protocol.Diagnostic) bool +} + +// ForFile filters to diagnostics matching the sandbox-relative file name. +func ForFile(name string) DiagnosticFilter { + return DiagnosticFilter{ + desc: fmt.Sprintf("for file %q", name), + check: func(diagName string, _ protocol.Diagnostic) bool { + return diagName == name + }, + } +} + +// FromSource filters to diagnostics matching the given diagnostics source. +func FromSource(source string) DiagnosticFilter { + return DiagnosticFilter{ + desc: fmt.Sprintf("with source %q", source), + check: func(_ string, d protocol.Diagnostic) bool { + return d.Source == source + }, + } +} + +// AtRegexp filters to diagnostics in the file with sandbox-relative path name, +// at the first position matching the given regexp pattern. +// +// TODO(rfindley): pass in the editor to expectations, so that they may depend +// on editor state and AtRegexp can be a function rather than a method. +func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { + pos := e.RegexpSearch(name, pattern) + return DiagnosticFilter{ + desc: fmt.Sprintf("at the first position matching %q in %q", pattern, name), + check: func(diagName string, d protocol.Diagnostic) bool { + // TODO(rfindley): just use protocol.Position for Pos, rather than + // duplicating. + return diagName == name && d.Range.Start.Line == uint32(pos.Line) && d.Range.Start.Character == uint32(pos.Column) + }, + } +} + +// WithMessageContaining filters to diagnostics whose message contains the +// given substring. +func WithMessageContaining(substring string) DiagnosticFilter { + return DiagnosticFilter{ + desc: fmt.Sprintf("with message containing %q", substring), + check: func(_ string, d protocol.Diagnostic) bool { + return strings.Contains(d.Message, substring) + }, + } +} + +// TODO(rfindley): eliminate all expectations below this point. + // NoDiagnostics asserts that either no diagnostics are sent for the // workspace-relative path name, or empty diagnostics are sent. func NoDiagnostics(name string) Expectation { @@ -749,43 +870,17 @@ func (e *Env) DiagnosticAtRegexpWithMessage(name, re, msg string) DiagnosticExpe return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true, message: msg} } -// DiagnosticAtRegexpFromSource expects a diagnostic at the first position -// matching re, from the given source. -func (e *Env) DiagnosticAtRegexpFromSource(name, re, source string) DiagnosticExpectation { - e.T.Helper() - pos := e.RegexpSearch(name, re) - return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true, source: source} -} - // DiagnosticAt asserts that there is a diagnostic entry at the position // specified by line and col, for the workdir-relative path name. func DiagnosticAt(name string, line, col int) DiagnosticExpectation { return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: true} } -// NoDiagnosticAtRegexp expects that there is no diagnostic entry at the start -// position matching the regexp search string re in the buffer specified by -// name. Note that this currently ignores the end position. -// This should only be used in combination with OnceMet for a given condition, -// otherwise it may always succeed. -func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation { - e.T.Helper() - pos := e.RegexpSearch(name, re) - return DiagnosticExpectation{path: name, pos: &pos, re: re, present: false} -} - -// NoDiagnosticWithMessage asserts that there is no diagnostic entry with the -// given message. -// -// This should only be used in combination with OnceMet for a given condition, -// otherwise it may always succeed. -func NoDiagnosticWithMessage(name, msg string) DiagnosticExpectation { - return DiagnosticExpectation{path: name, message: msg, present: false} -} - // GoSumDiagnostic asserts that a "go.sum is out of sync" diagnostic for the // given module (as formatted in a go.mod file, e.g. "example.com v1.0.0") is // present. +// +// TODO(rfindley): remove this. func (e *Env) GoSumDiagnostic(name, module string) Expectation { e.T.Helper() // In 1.16, go.sum diagnostics should appear on the relevant module. Earlier diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 8ea5e9c6fcf..13ba4e1c305 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -216,7 +216,7 @@ require golang.org/x/hello v1.2.3 // but there may be some subtlety in timing here, where this // should always succeed, but may not actually test the correct // behavior. - env.NoDiagnosticAtRegexp("b/go.mod", `require`), + NoMatchingDiagnostics(env.AtRegexp("b/go.mod", `require`)), ), ) // Check for upgrades in b/go.mod and then clear them. diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 6631a9234ca..b7ccb5bf8d4 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -1404,7 +1404,7 @@ func main() { env.Await( OnceMet( InitialWorkspaceLoad, - NoDiagnosticWithMessage("", "illegal character U+0023 '#'"), + NoMatchingDiagnostics(WithMessageContaining("illegal character U+0023 '#'")), ), ) }) diff --git a/gopls/internal/regtest/misc/staticcheck_test.go b/gopls/internal/regtest/misc/staticcheck_test.go index f2ba3ccd8a7..9242e194eb7 100644 --- a/gopls/internal/regtest/misc/staticcheck_test.go +++ b/gopls/internal/regtest/misc/staticcheck_test.go @@ -64,13 +64,13 @@ var FooErr error = errors.New("foo") Settings{"staticcheck": true}, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - env.Await( - env.DiagnosticAtRegexpFromSource("a/a.go", "sort.Slice", "sortslice"), - env.DiagnosticAtRegexpFromSource("a/a.go", "sort.Slice.(slice)", "SA1028"), - env.DiagnosticAtRegexpFromSource("a/a.go", "var (FooErr)", "ST1012"), - env.DiagnosticAtRegexpFromSource("a/a.go", `"12234"`, "SA1024"), - env.DiagnosticAtRegexpFromSource("a/a.go", "testGenerics.*(p P)", "SA4009"), - env.DiagnosticAtRegexpFromSource("a/a.go", "q = (&\\*p)", "SA4001"), + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "sort.Slice"), FromSource("sortslice")), + Diagnostics(env.AtRegexp("a/a.go", "sort.Slice.(slice)"), FromSource("SA1028")), + Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)"), FromSource("ST1012")), + Diagnostics(env.AtRegexp("a/a.go", `"12234"`), FromSource("SA1024")), + Diagnostics(env.AtRegexp("a/a.go", "testGenerics.*(p P)"), FromSource("SA4009")), + Diagnostics(env.AtRegexp("a/a.go", "q = (&\\*p)"), FromSource("SA4001")), ) }) } @@ -103,11 +103,8 @@ func Foo(enabled interface{}) { Settings{"staticcheck": true}, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("p.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - env.DiagnosticAtRegexpFromSource("p.go", ", (enabled)", "SA9008"), - ), + env.AfterChange( + Diagnostics(env.AtRegexp("p.go", ", (enabled)"), FromSource("SA9008")), ) }) } diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index 711d59617be..9a65030c715 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -160,16 +160,13 @@ const F = named.D - 3 Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("p/internal/bar/bar.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - env.DiagnosticAtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\""), - ), + env.AfterChange( + env.DiagnosticAtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\""), ) env.OpenFile("go.mod") env.RegexpReplace("go.mod", "mod.testx", "mod.test") env.SaveBuffer("go.mod") // saving triggers a reload - env.Await(NoOutstandingDiagnostics()) + env.AfterChange(NoMatchingDiagnostics()) }) } diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go index 698c8aac134..dffbbea4b70 100644 --- a/gopls/internal/regtest/workspace/standalone_test.go +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -74,29 +74,14 @@ func main() { } env.OpenFile("lib/lib.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - NoOutstandingDiagnostics(), - ), - ) + env.AfterChange(NoMatchingDiagnostics()) // Replacing C with D should not cause any workspace diagnostics, since we // haven't yet opened the standalone file. env.RegexpReplace("lib/lib.go", "C", "D") - env.Await( - OnceMet( - env.DoneWithChange(), - NoOutstandingDiagnostics(), - ), - ) + env.AfterChange(NoMatchingDiagnostics()) env.RegexpReplace("lib/lib.go", "D", "C") - env.Await( - OnceMet( - env.DoneWithChange(), - NoOutstandingDiagnostics(), - ), - ) + env.AfterChange(NoMatchingDiagnostics()) refs := env.References("lib/lib.go", env.RegexpSearch("lib/lib.go", "C")) checkLocations("References", refs, "lib/lib.go") @@ -106,12 +91,7 @@ func main() { // Opening the standalone file should not result in any diagnostics. env.OpenFile("lib/ignore.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - NoOutstandingDiagnostics(), - ), - ) + env.AfterChange(NoMatchingDiagnostics()) // Having opened the standalone file, we should find its symbols in the // workspace. @@ -151,21 +131,11 @@ func main() { // Renaming "lib.C" to "lib.D" should cause a diagnostic in the standalone // file. env.RegexpReplace("lib/lib.go", "C", "D") - env.Await( - OnceMet( - env.DoneWithChange(), - env.DiagnosticAtRegexp("lib/ignore.go", "lib.(C)"), - ), - ) + env.AfterChange(env.DiagnosticAtRegexp("lib/ignore.go", "lib.(C)")) // Undoing the replacement should fix diagnostics env.RegexpReplace("lib/lib.go", "D", "C") - env.Await( - OnceMet( - env.DoneWithChange(), - NoOutstandingDiagnostics(), - ), - ) + env.AfterChange(NoMatchingDiagnostics()) // Now that our workspace has no errors, we should be able to find // references and rename. diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index a279b8e2a6b..49e9e27226c 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -272,7 +272,7 @@ func Hello() int { env.AfterChange( env.DiagnosticAtRegexp("moda/a/a.go", "x"), env.DiagnosticAtRegexp("modb/b/b.go", "x"), - env.NoDiagnosticAtRegexp("moda/a/a.go", `"b.com/b"`), + NoMatchingDiagnostics(env.AtRegexp("moda/a/a.go", `"b.com/b"`)), ) }) } @@ -702,7 +702,7 @@ module example.com/bar // the diagnostic still shows up. env.SetBufferContent("go.work", "go 1.18 \n\n use ./bar\n") env.AfterChange( - env.NoDiagnosticAtRegexp("go.work", "use"), + NoMatchingDiagnostics(env.AtRegexp("go.work", "use")), ) env.SetBufferContent("go.work", "go 1.18 \n\n use ./foo\n") env.AfterChange( @@ -1069,7 +1069,7 @@ use ( b ) `) - env.AfterChange(NoOutstandingDiagnostics()) + env.AfterChange(NoMatchingDiagnostics()) // Removing the go.work file should put us back where we started. env.RemoveWorkspaceFile("go.work") From ab7b5b24f1236ceaca343bcf89f51a5d511a36bf Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 18:22:01 -0500 Subject: [PATCH 623/723] gopls/internal/regtest: eliminate GoSumDiagnostic Remove the redundant GoSumDiagnostic. Updates golang/go#39384 Change-Id: I742fbe5d32dd55288c7632bed335f0d33e1015d5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461916 gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- gopls/internal/lsp/regtest/expectation.go | 17 ----------------- gopls/internal/regtest/modfile/modfile_test.go | 10 ++++++++-- .../regtest/workspace/workspace_test.go | 5 ++++- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index a86dfe10219..43c9af22aef 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -13,7 +13,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp" "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/testenv" ) // An Expectation asserts that the state of the editor at a point in time @@ -875,19 +874,3 @@ func (e *Env) DiagnosticAtRegexpWithMessage(name, re, msg string) DiagnosticExpe func DiagnosticAt(name string, line, col int) DiagnosticExpectation { return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: true} } - -// GoSumDiagnostic asserts that a "go.sum is out of sync" diagnostic for the -// given module (as formatted in a go.mod file, e.g. "example.com v1.0.0") is -// present. -// -// TODO(rfindley): remove this. -func (e *Env) GoSumDiagnostic(name, module string) Expectation { - e.T.Helper() - // In 1.16, go.sum diagnostics should appear on the relevant module. Earlier - // errors have no information and appear on the module declaration. - if testenv.Go1Point() >= 16 { - return e.DiagnosticAtRegexpWithMessage(name, module, "go.sum is out of sync") - } else { - return e.DiagnosticAtRegexpWithMessage(name, `module`, "go.sum is out of sync") - } -} diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 21fd417d930..9c2ad63854f 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -893,7 +893,10 @@ func main() { d := &protocol.PublishDiagnosticsParams{} env.OpenFile("go.mod") env.AfterChange( - env.GoSumDiagnostic("go.mod", `example.com v1.2.3`), + Diagnostics( + env.AtRegexp("go.mod", `example.com v1.2.3`), + WithMessageContaining("go.sum is out of sync"), + ), ReadDiagnostics("go.mod", d), ) env.ApplyQuickFixes("go.mod", d.Diagnostics) @@ -1098,7 +1101,10 @@ func main() { params := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( - env.GoSumDiagnostic("go.mod", "example.com"), + Diagnostics( + env.AtRegexp("go.mod", `example.com`), + WithMessageContaining("go.sum is out of sync"), + ), ReadDiagnostics("go.mod", params), ), ) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 49e9e27226c..1708263b8e0 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -972,7 +972,10 @@ func main() { params := &protocol.PublishDiagnosticsParams{} env.OpenFile("b/go.mod") env.AfterChange( - env.GoSumDiagnostic("b/go.mod", `example.com v1.2.3`), + Diagnostics( + env.AtRegexp("go.mod", `example.com v1.2.3`), + WithMessageContaining("go.sum is out of sync"), + ), ReadDiagnostics("b/go.mod", params), ) for _, d := range params.Diagnostics { From 672a036fc157de6b0105673c2e0f2e841b047ad2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 18:52:54 -0500 Subject: [PATCH 624/723] gopls/internal/regtest: simplify OnceMet expressions with an env helper Simplify env.Await(OnceMet(...)) to env.OnceMet(...). Aside from avoiding boilerplate, this makes it easier to identify where we're still using Await. Updates golang/go#39384 Change-Id: I57a18242ce6b48e371e5ce4876ef01a6774fe15c Reviewed-on: https://go-review.googlesource.com/c/tools/+/461917 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/regtest/env.go | 12 + gopls/internal/lsp/regtest/expectation.go | 8 +- .../regtest/codelens/codelens_test.go | 18 +- .../regtest/codelens/gcdetails_test.go | 8 +- .../regtest/diagnostics/builtin_test.go | 5 +- .../regtest/diagnostics/diagnostics_test.go | 218 ++++++------------ .../regtest/diagnostics/invalidation_test.go | 35 +-- .../regtest/misc/configuration_test.go | 28 +-- gopls/internal/regtest/misc/imports_test.go | 6 +- gopls/internal/regtest/misc/rename_test.go | 10 +- gopls/internal/regtest/misc/vendor_test.go | 11 +- gopls/internal/regtest/misc/vuln_test.go | 58 ++--- .../internal/regtest/modfile/modfile_test.go | 106 ++++----- .../regtest/template/template_test.go | 20 +- gopls/internal/regtest/watch/watch_test.go | 72 ++---- .../workspace/directoryfilters_test.go | 18 +- .../regtest/workspace/fromenv_test.go | 10 +- .../regtest/workspace/metadata_test.go | 10 +- .../regtest/workspace/standalone_test.go | 18 +- .../regtest/workspace/workspace_test.go | 49 ++-- 20 files changed, 263 insertions(+), 457 deletions(-) diff --git a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go index c3f48bf16e9..73e8ef31cee 100644 --- a/gopls/internal/lsp/regtest/env.go +++ b/gopls/internal/lsp/regtest/env.go @@ -302,6 +302,12 @@ func checkExpectations(s State, expectations []Expectation) (Verdict, string) { return finalVerdict, summary.String() } +// Await blocks until the given expectations are all simultaneously met. +// +// Generally speaking Await should be avoided because it can block indefinitely +// if gopls ends up in a state where the expectations are never going to be +// met. Use AfterChange or OnceMet instead, so that the runner knows when to +// stop waiting. func (e *Env) Await(expectations ...Expectation) { e.T.Helper() if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil { @@ -309,6 +315,12 @@ func (e *Env) Await(expectations ...Expectation) { } } +// OnceMet blocks until precondition is met or unmeetable; if the precondition +// is met, it atomically checks that all expectations in mustMeets are met. +func (e *Env) OnceMet(precondition Expectation, mustMeets ...Expectation) { + e.Await(OnceMet(precondition, mustMeets...)) +} + // Await waits for all expectations to simultaneously be met. It should only be // called from the main test goroutine. func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error { diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 43c9af22aef..410e77873b5 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -291,11 +291,9 @@ func (e *Env) DoneDiagnosingChanges() Expectation { // expectations. func (e *Env) AfterChange(expectations ...Expectation) { e.T.Helper() - e.Await( - OnceMet( - e.DoneDiagnosingChanges(), - expectations..., - ), + e.OnceMet( + e.DoneDiagnosingChanges(), + expectations..., ) } diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 13ba4e1c305..56e6cca1623 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -208,16 +208,14 @@ require golang.org/x/hello v1.2.3 env.OpenFile("b/go.mod") env.ExecuteCodeLensCommand("a/go.mod", command.CheckUpgrades, nil) d := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - env.DiagnosticAtRegexpWithMessage("a/go.mod", `require`, "can be upgraded"), - ReadDiagnostics("a/go.mod", d), - // We do not want there to be a diagnostic for b/go.mod, - // but there may be some subtlety in timing here, where this - // should always succeed, but may not actually test the correct - // behavior. - NoMatchingDiagnostics(env.AtRegexp("b/go.mod", `require`)), - ), + env.OnceMet( + env.DiagnosticAtRegexpWithMessage("a/go.mod", `require`, "can be upgraded"), + ReadDiagnostics("a/go.mod", d), + // We do not want there to be a diagnostic for b/go.mod, + // but there may be some subtlety in timing here, where this + // should always succeed, but may not actually test the correct + // behavior. + NoMatchingDiagnostics(env.AtRegexp("b/go.mod", `require`)), ) // Check for upgrades in b/go.mod and then clear them. env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades, nil) diff --git a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go index 5f812f43478..1b2c3468bff 100644 --- a/gopls/internal/regtest/codelens/gcdetails_test.go +++ b/gopls/internal/regtest/codelens/gcdetails_test.go @@ -45,11 +45,9 @@ func main() { env.OpenFile("main.go") env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) d := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - DiagnosticAt("main.go", 5, 13), - ReadDiagnostics("main.go", d), - ), + env.OnceMet( + DiagnosticAt("main.go", 5, 13), + ReadDiagnostics("main.go", d), ) // Confirm that the diagnostics come from the gc details code lens. var found bool diff --git a/gopls/internal/regtest/diagnostics/builtin_test.go b/gopls/internal/regtest/diagnostics/builtin_test.go index 8de47bcd946..5baf1dcd0f2 100644 --- a/gopls/internal/regtest/diagnostics/builtin_test.go +++ b/gopls/internal/regtest/diagnostics/builtin_test.go @@ -30,9 +30,6 @@ const ( if !strings.HasSuffix(name, "builtin.go") { t.Fatalf("jumped to %q, want builtin.go", name) } - env.Await(OnceMet( - env.DoneWithOpen(), - NoDiagnostics("builtin.go"), - )) + env.AfterChange(NoDiagnostics("builtin.go")) }) } diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index b7ccb5bf8d4..62585b9c693 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -48,13 +48,8 @@ func TestDiagnosticErrorInEditedFile(t *testing.T) { // diagnostic. env.OpenFile("main.go") env.RegexpReplace("main.go", "Printl(n)", "") - env.Await( - // Once we have gotten diagnostics for the change above, we should - // satisfy the DiagnosticAtRegexp assertion. - OnceMet( - env.DoneWithChange(), - env.DiagnosticAtRegexp("main.go", "Printl"), - ), + env.AfterChange( + env.DiagnosticAtRegexp("main.go", "Printl"), // Assert that this test has sent no error logs to the client. This is not // strictly necessary for testing this regression, but is included here // as an example of using the NoErrorLogs() expectation. Feel free to @@ -235,9 +230,7 @@ func TestDeleteTestVariant_DiskOnly(t *testing.T) { env.OpenFile("a_test.go") env.Await(DiagnosticAt("a_test.go", 5, 3)) env.Sandbox.Workdir.RemoveFile(context.Background(), "a_test.go") - env.Await(OnceMet( - env.DoneWithChangeWatchedFiles(), - DiagnosticAt("a_test.go", 5, 3))) + env.AfterChange(DiagnosticAt("a_test.go", 5, 3)) }) } @@ -263,26 +256,20 @@ func Hello() { t.Run("manual", func(t *testing.T) { Run(t, noMod, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), ) env.CreateBuffer("go.mod", `module mod.com go 1.12 `) env.SaveBuffer("go.mod") + var d protocol.PublishDiagnosticsParams env.AfterChange( NoDiagnostics("main.go"), - ) - var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexp("bob/bob.go", "x"), - ReadDiagnostics("bob/bob.go", &d), - ), + env.DiagnosticAtRegexp("bob/bob.go", "x"), + ReadDiagnostics("bob/bob.go", &d), ) if len(d.Diagnostics) != 1 { t.Fatalf("expected 1 diagnostic, got %v", len(d.Diagnostics)) @@ -527,11 +514,9 @@ func _() { Run(t, generated, func(t *testing.T, env *Env) { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - DiagnosticAt("main.go", 5, 8), - ReadDiagnostics("main.go", &d), - ), + env.AfterChange( + DiagnosticAt("main.go", 5, 8), + ReadDiagnostics("main.go", &d), ) if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { t.Errorf("got quick fixes %v, wanted none", fixes) @@ -608,7 +593,7 @@ func main() { EnvVars{"GO111MODULE": "off"}, ).Run(t, collision, func(t *testing.T, env *Env) { env.OpenFile("x/x.go") - env.Await( + env.AfterChange( env.DiagnosticAtRegexpWithMessage("x/x.go", `^`, "found packages main (main.go) and x (x.go)"), env.DiagnosticAtRegexpWithMessage("x/main.go", `^`, "found packages main (main.go) and x (x.go)"), ) @@ -616,9 +601,9 @@ func main() { // We don't recover cleanly from the errors without good overlay support. if testenv.Go1Point() >= 16 { env.RegexpReplace("x/x.go", `package x`, `package main`) - env.Await(OnceMet( - env.DoneWithChange(), - env.DiagnosticAtRegexp("x/main.go", `fmt`))) + env.AfterChange( + env.DiagnosticAtRegexp("x/main.go", `fmt`), + ) } }) } @@ -658,11 +643,9 @@ func main() { env.OpenFile("go.mod") env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`), - ReadDiagnostics("main.go", &d), - ), + env.AfterChange( + env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`), + ReadDiagnostics("main.go", &d), ) env.ApplyQuickFixes("main.go", d.Diagnostics) env.SaveBuffer("go.mod") @@ -672,7 +655,7 @@ func main() { // Comment out the line that depends on conf and expect a // diagnostic and a fix to remove the import. env.RegexpReplace("main.go", "_ = conf.ErrHelpWanted", "//_ = conf.ErrHelpWanted") - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`), ) env.SaveBuffer("main.go") @@ -718,11 +701,9 @@ func main() { `) env.SaveBuffer("main.go") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexpWithMessage("main.go", `"github.com/ardanlabs/conf"`, "no required module"), - ReadDiagnostics("main.go", &d), - ), + env.AfterChange( + env.DiagnosticAtRegexpWithMessage("main.go", `"github.com/ardanlabs/conf"`, "no required module"), + ReadDiagnostics("main.go", &d), ) env.ApplyQuickFixes("main.go", d.Diagnostics) env.AfterChange( @@ -872,12 +853,9 @@ package foo_ Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("foo/bar_test.go") env.RegexpReplace("foo/bar_test.go", "package foo_", "package foo_test") - env.Await( - OnceMet( - env.DoneWithChange(), - NoDiagnostics("foo/bar_test.go"), - NoDiagnostics("foo/foo.go"), - ), + env.AfterChange( + NoDiagnostics("foo/bar_test.go"), + NoDiagnostics("foo/foo.go"), ) }) } @@ -1076,12 +1054,7 @@ func main() { }`) env.OpenFile("foo/foo_test.go") env.RegexpReplace("foo/foo_test.go", `package main`, `package foo`) - env.Await( - OnceMet( - env.DoneWithChange(), - NoDiagnostics("foo/foo.go"), - ), - ) + env.AfterChange(NoDiagnostics("foo/foo.go")) }) } @@ -1102,12 +1075,7 @@ func main() {} env.DoneWithOpen(), ) env.CloseBuffer("foo.go") - env.Await( - OnceMet( - env.DoneWithClose(), - NoLogMatching(protocol.Info, "packages=0"), - ), - ) + env.AfterChange(NoLogMatching(protocol.Info, "packages=0")) }) } @@ -1346,11 +1314,9 @@ func b(c bytes.Buffer) { Settings{"allExperiments": true}, ).Run(t, mod, func(t *testing.T, env *Env) { // Confirm that the setting doesn't cause any warnings. - env.Await( - OnceMet( - InitialWorkspaceLoad, - NoShownMessage(""), // empty substring to match any message - ), + env.OnceMet( + InitialWorkspaceLoad, + NoShownMessage(""), // empty substring to match any message ) }) } @@ -1401,11 +1367,9 @@ func main() { } ` Run(t, mod, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - NoMatchingDiagnostics(WithMessageContaining("illegal character U+0023 '#'")), - ), + env.OnceMet( + InitialWorkspaceLoad, + NoMatchingDiagnostics(WithMessageContaining("illegal character U+0023 '#'")), ) }) } @@ -1527,11 +1491,9 @@ func main() { } ` Run(t, mod, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - FileWatchMatching("bob"), - ), + env.OnceMet( + InitialWorkspaceLoad, + FileWatchMatching("bob"), ) env.RemoveWorkspaceFile("bob") env.AfterChange( @@ -1610,17 +1572,14 @@ const B = a.B Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("b/b.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - // 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. - // - // TODO(golang/go#52904): we should get *both* of these errors. - AnyOf( - env.DiagnosticAtRegexpWithMessage("a/a.go", `"mod.test/b"`, "import cycle"), - env.DiagnosticAtRegexpWithMessage("b/b.go", `"mod.test/a"`, "import cycle"), - ), + 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. + // + // TODO(golang/go#52904): we should get *both* of these errors. + AnyOf( + env.DiagnosticAtRegexpWithMessage("a/a.go", `"mod.test/b"`, "import cycle"), + env.DiagnosticAtRegexpWithMessage("b/b.go", `"mod.test/a"`, "import cycle"), ), ) env.RegexpReplace("b/b.go", `const B = a\.B`, "") @@ -1715,20 +1674,10 @@ func helloHelper() {} ).Run(t, nested, func(t *testing.T, env *Env) { // Expect a diagnostic in a nested module. env.OpenFile("nested/hello/hello.go") - didOpen := env.DoneWithOpen() - env.Await( - OnceMet( - didOpen, - env.DiagnosticAtRegexp("nested/hello/hello.go", "helloHelper"), - ), - OnceMet( - didOpen, - env.DiagnosticAtRegexpWithMessage("nested/hello/hello.go", "package hello", "nested module"), - ), - OnceMet( - didOpen, - OutstandingWork(lsp.WorkspaceLoadFailure, "nested module"), - ), + env.AfterChange( + env.DiagnosticAtRegexp("nested/hello/hello.go", "helloHelper"), + env.DiagnosticAtRegexpWithMessage("nested/hello/hello.go", "package hello", "nested module"), + OutstandingWork(lsp.WorkspaceLoadFailure, "nested module"), ) }) } @@ -1743,12 +1692,7 @@ func main() {} Run(t, nomod, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.RegexpReplace("main.go", "{}", "{ var x int; }") // simulate typing - env.Await( - OnceMet( - env.DoneWithChange(), - NoLogMatching(protocol.Info, "packages=1"), - ), - ) + env.AfterChange(NoLogMatching(protocol.Info, "packages=1")) }) } @@ -1808,11 +1752,9 @@ package main ) env.SetBufferContent("other.go", "package main\n\nasdf") // The new diagnostic in other.go should not suppress diagnostics in main.go. - env.Await( - OnceMet( - env.DiagnosticAtRegexpWithMessage("other.go", "asdf", "expected declaration"), - env.DiagnosticAtRegexp("main.go", "asdf"), - ), + env.AfterChange( + env.DiagnosticAtRegexpWithMessage("other.go", "asdf", "expected declaration"), + env.DiagnosticAtRegexp("main.go", "asdf"), ) }) } @@ -1831,11 +1773,8 @@ package main env.Await(env.DoneWithOpen()) env.RegexpReplace("go.mod", "module", "modul") env.SaveBufferWithoutActions("go.mod") - env.Await( - OnceMet( - env.DoneWithSave(), - NoLogMatching(protocol.Error, "initial workspace load failed"), - ), + env.AfterChange( + NoLogMatching(protocol.Error, "initial workspace load failed"), ) }) } @@ -1855,11 +1794,8 @@ func main() {} ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - env.Await( - OnceMet( - env.DoneWithOpen(), - LogMatching(protocol.Info, `.*query=\[builtin mod.com/...\].*`, 1, false), - ), + env.AfterChange( + LogMatching(protocol.Info, `.*query=\[builtin mod.com/...\].*`, 1, false), ) }) } @@ -1877,11 +1813,9 @@ package main const C = 0b10 ` Run(t, files, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexpWithMessage("main.go", `0b10`, "go1.13 or later"), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexpWithMessage("main.go", `0b10`, "go1.13 or later"), ) env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.13\n") env.AfterChange( @@ -1906,11 +1840,10 @@ func F[T C](_ T) { Run(t, files, func(t *testing.T, env *Env) { var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexp("main.go", `C`), - ReadDiagnostics("main.go", &d), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("main.go", `C`), + ReadDiagnostics("main.go", &d), ) if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { t.Errorf("got quick fixes %v, wanted none", fixes) @@ -1933,12 +1866,10 @@ func F[T any](_ T) { ` Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), - ReadDiagnostics("main.go", &d), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), + ReadDiagnostics("main.go", &d), ) env.ApplyQuickFixes("main.go", d.Diagnostics) @@ -1968,14 +1899,11 @@ func F[T any](_ T) { Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. var d protocol.PublishDiagnosticsParams - // Once the initial workspace load is complete, we should have a diagnostic - // because generics are not supported at 1.16. - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), - ReadDiagnostics("main.go", &d), - ), + // We should have a diagnostic because generics are not supported at 1.16. + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), + ReadDiagnostics("main.go", &d), ) // This diagnostic should have a quick fix to edit the go version. diff --git a/gopls/internal/regtest/diagnostics/invalidation_test.go b/gopls/internal/regtest/diagnostics/invalidation_test.go index 2f0b173160c..f5097f32d77 100644 --- a/gopls/internal/regtest/diagnostics/invalidation_test.go +++ b/gopls/internal/regtest/diagnostics/invalidation_test.go @@ -30,30 +30,21 @@ func _() { Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. env.OpenFile("main.go") var afterOpen protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DoneWithOpen(), - ReadDiagnostics("main.go", &afterOpen), - ), + env.AfterChange( + ReadDiagnostics("main.go", &afterOpen), ) env.CloseBuffer("main.go") var afterClose protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DoneWithClose(), - ReadDiagnostics("main.go", &afterClose), - ), + env.AfterChange( + ReadDiagnostics("main.go", &afterClose), ) if afterOpen.Version == afterClose.Version { t.Errorf("publishDiagnostics: got the same version after closing (%d) as after opening", afterOpen.Version) } env.OpenFile("main.go") var afterReopen protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DoneWithOpen(), - ReadDiagnostics("main.go", &afterReopen), - ), + env.AfterChange( + ReadDiagnostics("main.go", &afterReopen), ) if afterReopen.Version == afterClose.Version { t.Errorf("pubslishDiagnostics: got the same version after reopening (%d) as after closing", afterClose.Version) @@ -87,11 +78,8 @@ func _() { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DoneWithOpen(), - ReadDiagnostics("main.go", &d), - ), + env.AfterChange( + ReadDiagnostics("main.go", &d), ) if len(d.Diagnostics) != 1 { @@ -102,11 +90,8 @@ func _() { for i := 0; i < 5; i++ { before := d.Version env.RegexpReplace("main.go", "Irrelevant comment #.", fmt.Sprintf("Irrelevant comment #%d", i)) - env.Await( - OnceMet( - env.DoneWithChange(), - ReadDiagnostics("main.go", &d), - ), + env.AfterChange( + ReadDiagnostics("main.go", &d), ) if d.Version == before { diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index 15aeeb42e30..4164d6c0e1f 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -72,11 +72,9 @@ var FooErr = errors.New("foo") WithOptions( Settings{"staticcheck": true}, ).Run(t, files, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - ShownMessage("staticcheck is not supported"), - ), + env.OnceMet( + InitialWorkspaceLoad, + ShownMessage("staticcheck is not supported"), ) }) } @@ -87,11 +85,9 @@ func TestGofumptWarning(t *testing.T) { WithOptions( Settings{"gofumpt": true}, ).Run(t, "", func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - ShownMessage("gofumpt is not supported"), - ), + env.OnceMet( + InitialWorkspaceLoad, + ShownMessage("gofumpt is not supported"), ) }) } @@ -104,13 +100,11 @@ func TestDeprecatedSettings(t *testing.T) { "experimentalWorkspaceModule": true, }, ).Run(t, "", func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - ShownMessage("experimentalWorkspaceModule"), - ShownMessage("experimentalUseInvalidMetadata"), - ShownMessage("experimentalWatchedFileDelay"), - ), + env.OnceMet( + InitialWorkspaceLoad, + ShownMessage("experimentalWorkspaceModule"), + ShownMessage("experimentalUseInvalidMetadata"), + ShownMessage("experimentalWatchedFileDelay"), ) }) } diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index 9e77d549610..6558491df13 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -200,10 +200,8 @@ func TestA(t *testing.T) { env.OpenFile("a/a.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - OnceMet( - env.DiagnosticAtRegexp("a/a.go", "os.Stat"), - ReadDiagnostics("a/a.go", &d), - ), + env.DiagnosticAtRegexp("a/a.go", "os.Stat"), + ReadDiagnostics("a/a.go", &d), ) env.ApplyQuickFixes("a/a.go", d.Diagnostics) env.AfterChange( diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index ef94ae25639..b46b3d05458 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -403,12 +403,10 @@ package b // Rename files and verify that diagnostics are affected accordingly. // Initially, we should have diagnostics on both X's, for their duplicate declaration. - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "X"), - env.DiagnosticAtRegexp("a/x.go", "X"), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "X"), + env.DiagnosticAtRegexp("a/x.go", "X"), ) // Moving x.go should make the diagnostic go away. diff --git a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go index 3210461a326..06e34b55f5f 100644 --- a/gopls/internal/regtest/misc/vendor_test.go +++ b/gopls/internal/regtest/misc/vendor_test.go @@ -51,15 +51,14 @@ func _() { ).Run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) { env.OpenFile("a/a1.go") d := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - env.DiagnosticAtRegexpWithMessage("go.mod", "module mod.com", "Inconsistent vendoring"), - ReadDiagnostics("go.mod", d), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexpWithMessage("go.mod", "module mod.com", "Inconsistent vendoring"), + ReadDiagnostics("go.mod", d), ) env.ApplyQuickFixes("go.mod", d.Diagnostics) - env.Await( + env.AfterChange( env.DiagnosticAtRegexpWithMessage("a/a1.go", `q int`, "not used"), ) }) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 1b44caf3566..9556ab34ccd 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -225,12 +225,10 @@ func main() { Arguments: lens.Command.Arguments, }, &result) - env.Await( - OnceMet( - CompletedProgress(result.Token, nil), - ShownMessage("Found GOSTDLIB"), - NoDiagnostics("go.mod"), - ), + env.OnceMet( + CompletedProgress(result.Token, nil), + ShownMessage("Found GOSTDLIB"), + NoDiagnostics("go.mod"), ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ "go.mod": {IDs: []string{"GOSTDLIB"}, Mode: govulncheck.ModeGovulncheck}}) @@ -586,17 +584,13 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { var result command.RunVulncheckResult env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) gotDiagnostics := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - CompletedProgress(result.Token, nil), - ShownMessage("Found"), - ), + env.OnceMet( + CompletedProgress(result.Token, nil), + ShownMessage("Found"), ) - env.Await( - OnceMet( - env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"), - ReadDiagnostics("go.mod", gotDiagnostics), - ), + env.OnceMet( + env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"), + ReadDiagnostics("go.mod", gotDiagnostics), ) // We expect only one diagnostic for GO-2022-02. count := 0 @@ -636,18 +630,14 @@ func TestRunVulncheckWarning(t *testing.T) { var result command.RunVulncheckResult env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) gotDiagnostics := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - CompletedProgress(result.Token, nil), - ShownMessage("Found"), - ), + env.OnceMet( + CompletedProgress(result.Token, nil), + ShownMessage("Found"), ) // Vulncheck diagnostics asynchronous to the vulncheck command. - env.Await( - OnceMet( - env.DiagnosticAtRegexp("go.mod", `golang.org/amod`), - ReadDiagnostics("go.mod", gotDiagnostics), - ), + env.OnceMet( + env.DiagnosticAtRegexp("go.mod", `golang.org/amod`), + ReadDiagnostics("go.mod", gotDiagnostics), ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ @@ -806,19 +796,15 @@ func TestGovulncheckInfo(t *testing.T) { var result command.RunVulncheckResult env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) gotDiagnostics := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - CompletedProgress(result.Token, nil), - ShownMessage("No vulnerabilities found"), // only count affecting vulnerabilities. - ), + env.OnceMet( + CompletedProgress(result.Token, nil), + ShownMessage("No vulnerabilities found"), // only count affecting vulnerabilities. ) // Vulncheck diagnostics asynchronous to the vulncheck command. - env.Await( - OnceMet( - env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"), - ReadDiagnostics("go.mod", gotDiagnostics), - ), + env.OnceMet( + env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"), + ReadDiagnostics("go.mod", gotDiagnostics), ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{"go.mod": {IDs: []string{"GO-2022-02"}, Mode: govulncheck.ModeGovulncheck}}) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 9c2ad63854f..e870096a9eb 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -92,11 +92,8 @@ func main() { // modify the go.mod file. goModContent := env.ReadWorkspaceFile("a/go.mod") env.OpenFile("a/main.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) @@ -145,11 +142,8 @@ func main() { ) env.WriteWorkspaceFile("a/main.go", mainContent) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) @@ -189,11 +183,9 @@ require example.com v1.2.3 } env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexp("a/main.go", `"example.com/blah"`), - ReadDiagnostics("a/main.go", &d), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", `"example.com/blah"`), + ReadDiagnostics("a/main.go", &d), ) var goGetDiag protocol.Diagnostic for _, diag := range d.Diagnostics { @@ -238,11 +230,9 @@ require random.org v1.2.3 }.Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexp("a/main.go", `"random.org/blah"`), - ReadDiagnostics("a/main.go", &d), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", `"random.org/blah"`), + ReadDiagnostics("a/main.go", &d), ) var randomDiag protocol.Diagnostic for _, diag := range d.Diagnostics { @@ -294,11 +284,9 @@ require random.org v1.2.3 }.Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexp("a/main.go", `"random.org/blah"`), - ReadDiagnostics("a/main.go", &d), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", `"random.org/blah"`), + ReadDiagnostics("a/main.go", &d), ) var randomDiag protocol.Diagnostic for _, diag := range d.Diagnostics { @@ -345,11 +333,9 @@ require example.com v1.2.3 }.Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexp("a/go.mod", "// indirect"), - ReadDiagnostics("a/go.mod", &d), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/go.mod", "// indirect"), + ReadDiagnostics("a/go.mod", &d), ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) if got := env.BufferText("a/go.mod"); got != want { @@ -389,11 +375,9 @@ go 1.14 }.Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexp("a/go.mod", `require example.com`), - ReadDiagnostics("a/go.mod", &d), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/go.mod", `require example.com`), + ReadDiagnostics("a/go.mod", &d), ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) if got := env.BufferText("a/go.mod"); got != want { @@ -451,11 +435,9 @@ func _() { }.Run(t, repro, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - env.DiagnosticAtRegexp("a/main.go", `"github.com/esimov/caire"`), - ReadDiagnostics("a/main.go", &d), - ), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", `"github.com/esimov/caire"`), + ReadDiagnostics("a/main.go", &d), ) env.ApplyQuickFixes("a/main.go", d.Diagnostics) want := `module mod.com @@ -497,11 +479,9 @@ func main() { {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, {"nested", WithOptions(ProxyFiles(proxy))}, }.Run(t, mod, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/go.mod", "require"), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/go.mod", "require"), ) env.RunGoCommandInDir("a", "mod", "tidy") env.AfterChange( @@ -1000,11 +980,9 @@ func main() {} ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") d := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - env.DiagnosticAtRegexp("go.mod", "require hasdep.com v1.2.3"), - ReadDiagnostics("go.mod", d), - ), + env.AfterChange( + env.DiagnosticAtRegexp("go.mod", "require hasdep.com v1.2.3"), + ReadDiagnostics("go.mod", d), ) const want = `module mod.com @@ -1044,11 +1022,9 @@ func main() {} d := &protocol.PublishDiagnosticsParams{} env.OpenFile("go.mod") pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3") - env.Await( - OnceMet( - DiagnosticAt("go.mod", pos.Line, pos.Column), - ReadDiagnostics("go.mod", d), - ), + env.AfterChange( + DiagnosticAt("go.mod", pos.Line, pos.Column), + ReadDiagnostics("go.mod", d), ) const want = `module mod.com @@ -1099,14 +1075,12 @@ func main() { ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") params := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - Diagnostics( - env.AtRegexp("go.mod", `example.com`), - WithMessageContaining("go.sum is out of sync"), - ), - ReadDiagnostics("go.mod", params), + env.AfterChange( + Diagnostics( + env.AtRegexp("go.mod", `example.com`), + WithMessageContaining("go.sum is out of sync"), ), + ReadDiagnostics("go.mod", params), ) env.ApplyQuickFixes("go.mod", params.Diagnostics) const want = `example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= @@ -1191,11 +1165,9 @@ go foo package main ` Run(t, files, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexpWithMessage("go.mod", `go foo`, "invalid go version"), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexpWithMessage("go.mod", `go foo`, "invalid go version"), ) env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.12\n") env.AfterChange(NoDiagnostics("go.mod")) diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index 7ccf08d2db2..140ba3caf3b 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -71,12 +71,10 @@ Hello {{}} <-- missing body ).Run(t, files, func(t *testing.T, env *Env) { // TODO: can we move this diagnostic onto {{}}? var diags protocol.PublishDiagnosticsParams - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"), - ReadDiagnostics("hello.tmpl", &diags), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"), + ReadDiagnostics("hello.tmpl", &diags), ) d := diags.Diagnostics // issue 50786: check for Source if len(d) != 1 { @@ -120,12 +118,10 @@ B {{}} <-- missing body "templateExtensions": []string{"tmpl"}, }, ).Run(t, files, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.tmpl", "()A"), - NoDiagnostics("b/b.tmpl"), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.tmpl", "()A"), + NoDiagnostics("b/b.tmpl"), ) }) } diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 70376a91810..d2a203e8cfa 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -37,11 +37,9 @@ func _() { // diagnostics are updated. t.Run("unopened", func(t *testing.T) { Run(t, pkg, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "x"), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "x"), ) env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) env.AfterChange( @@ -124,11 +122,9 @@ func _() { } ` Run(t, pkg, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "x"), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "x"), ) env.WriteWorkspaceFiles(map[string]string{ "b/b.go": `package b; func B() {};`, @@ -202,11 +198,9 @@ func _() { } ` Run(t, missing, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""), ) env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) env.AfterChange( @@ -252,11 +246,9 @@ func _() { } ` Run(t, pkg, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "hello"), - ), + env.OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "hello"), ) env.WriteWorkspaceFile("a/a2.go", `package a; func hello() {};`) env.AfterChange( @@ -391,18 +383,15 @@ package a ).Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("a/a_unneeded.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - LogMatching(protocol.Info, "a_unneeded.go", 1, false), - ), + env.AfterChange( + LogMatching(protocol.Info, "a_unneeded.go", 1, false), ) // Close and delete the open file, mimicking what an editor would do. env.CloseBuffer("a/a_unneeded.go") env.RemoveWorkspaceFile("a/a_unneeded.go") env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") - env.Await( + env.AfterChange( env.DiagnosticAtRegexp("a/a.go", "fmt"), ) env.SaveBuffer("a/a.go") @@ -422,11 +411,8 @@ package a ).Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("a/a_unneeded.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - LogMatching(protocol.Info, "a_unneeded.go", 1, false), - ), + env.AfterChange( + LogMatching(protocol.Info, "a_unneeded.go", 1, false), ) // Delete and then close the file. @@ -682,15 +668,9 @@ func TestAll(t *testing.T) { } `, }) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a.go"), - ), - OnceMet( - env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a_test.go"), - ), + env.AfterChange( + NoDiagnostics("a/a.go"), + NoDiagnostics("a/a_test.go"), ) // Now, add a new file to the test variant and use its symbol in the // original test file. Expect no diagnostics. @@ -714,15 +694,9 @@ func hi() {} func TestSomething(t *testing.T) {} `, }) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a_test.go"), - ), - OnceMet( - env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a2_test.go"), - ), + env.AfterChange( + NoDiagnostics("a/a_test.go"), + NoDiagnostics("a/a2_test.go"), ) }) } diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go index 69f91cd9083..bf22b8012bd 100644 --- a/gopls/internal/regtest/workspace/directoryfilters_test.go +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -53,11 +53,9 @@ const _ = Nonexistant WithOptions( Settings{"directoryFilters": []string{"-exclude"}}, ).Run(t, files, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - NoDiagnostics("exclude/x.go"), - ), + env.OnceMet( + InitialWorkspaceLoad, + NoDiagnostics("exclude/x.go"), ) }) } @@ -85,12 +83,10 @@ const X = 1 WithOptions( Settings{"directoryFilters": []string{"-exclude"}}, ).Run(t, files, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - NoDiagnostics("exclude/exclude.go"), // filtered out - NoDiagnostics("include/include.go"), // successfully builds - ), + env.OnceMet( + InitialWorkspaceLoad, + NoDiagnostics("exclude/exclude.go"), // filtered out + NoDiagnostics("include/include.go"), // successfully builds ) }) } diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go index 218e20630a1..831fa771a46 100644 --- a/gopls/internal/regtest/workspace/fromenv_test.go +++ b/gopls/internal/regtest/workspace/fromenv_test.go @@ -52,12 +52,10 @@ use ( EnvVars{"GOWORK": "$SANDBOX_WORKDIR/config/go.work"}, ).Run(t, files, func(t *testing.T, env *Env) { // When we have an explicit GOWORK set, we should get a file watch request. - env.Await( - OnceMet( - InitialWorkspaceLoad, - FileWatchMatching(`other`), - FileWatchMatching(`config.go\.work`), - ), + env.OnceMet( + InitialWorkspaceLoad, + FileWatchMatching(`other`), + FileWatchMatching(`config.go\.work`), ) env.Await(FileWatchMatching(`config.go\.work`)) // Even though work/b is not open, we should get its diagnostics as it is diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index c014f11e921..de7dec9f6f2 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -71,12 +71,10 @@ func main() {} ).Run(t, src, func(t *testing.T, env *Env) { env.OpenFile("foo.go") env.OpenFile("bar.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - env.DiagnosticAtRegexp("foo.go", "func (main)"), - env.DiagnosticAtRegexp("bar.go", "func (main)"), - ), + env.OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("foo.go", "func (main)"), + env.DiagnosticAtRegexp("bar.go", "func (main)"), ) // Ignore bar.go. This should resolve diagnostics. diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go index dffbbea4b70..53b269e59e3 100644 --- a/gopls/internal/regtest/workspace/standalone_test.go +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -185,12 +185,9 @@ func main() {} env.OpenFile("ignore.go") env.OpenFile("standalone.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - env.DiagnosticAtRegexp("ignore.go", "package (main)"), - NoDiagnostics("standalone.go"), - ), + env.AfterChange( + env.DiagnosticAtRegexp("ignore.go", "package (main)"), + NoDiagnostics("standalone.go"), ) cfg := env.Editor.Config() @@ -203,12 +200,9 @@ func main() {} // diagnostice when configuration changes. env.RegexpReplace("ignore.go", "arbitrary", "meaningless") - env.Await( - OnceMet( - env.DoneWithChange(), - NoDiagnostics("ignore.go"), - env.DiagnosticAtRegexp("standalone.go", "package (main)"), - ), + env.AfterChange( + NoDiagnostics("ignore.go"), + env.DiagnosticAtRegexp("standalone.go", "package (main)"), ) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 1708263b8e0..720ec97ee0e 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -177,11 +177,8 @@ func TestReloadOnlyOnce(t *testing.T) { replace random.org => %s `, env.ReadWorkspaceFile("pkg/go.mod"), dir) env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace) - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - LogMatching(protocol.Info, `packages\.Load #\d+\n`, 2, false), - ), + env.AfterChange( + LogMatching(protocol.Info, `packages\.Load #\d+\n`, 2, false), ) }) } @@ -505,11 +502,8 @@ func Hello() int { ProxyFiles(workspaceModuleProxy), ).Run(t, multiModule, func(t *testing.T, env *Env) { env.OpenFile("modb/go.mod") - env.Await( - OnceMet( - env.DoneWithOpen(), - DiagnosticAt("modb/go.mod", 0, 0), - ), + env.AfterChange( + DiagnosticAt("modb/go.mod", 0, 0), ) env.RegexpReplace("modb/go.mod", "modul", "module") env.SaveBufferWithoutActions("modb/go.mod") @@ -612,11 +606,8 @@ use ( // As of golang/go#54069, writing go.work to the workspace triggers a // workspace reload. - env.Await( - OnceMet( - env.DoneWithChangeWatchedFiles(), - env.DiagnosticAtRegexp("modb/b/b.go", "x"), - ), + env.AfterChange( + env.DiagnosticAtRegexp("modb/b/b.go", "x"), ) // Jumping to definition should now go to b.com in the workspace. @@ -627,7 +618,7 @@ use ( // Now, let's modify the go.work *overlay* (not on disk), and verify that // this change is only picked up once it is saved. env.OpenFile("go.work") - env.Await(env.DoneWithOpen()) + env.AfterChange() env.SetBufferContent("go.work", `go 1.17 use ( @@ -910,16 +901,14 @@ func main() { WithOptions( ProxyFiles(proxy), ).Run(t, multiModule, func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - // TODO(rfindley): assert on the full set of diagnostics here. We - // should ensure that we don't have a diagnostic at b.Hi in a.go. - env.DiagnosticAtRegexp("moda/a/a.go", "x"), - env.DiagnosticAtRegexp("modb/b/b.go", "x"), - env.DiagnosticAtRegexp("modb/v2/b/b.go", "x"), - env.DiagnosticAtRegexp("modc/main.go", "x"), - ), + env.OnceMet( + InitialWorkspaceLoad, + // TODO(rfindley): assert on the full set of diagnostics here. We + // should ensure that we don't have a diagnostic at b.Hi in a.go. + env.DiagnosticAtRegexp("moda/a/a.go", "x"), + env.DiagnosticAtRegexp("modb/b/b.go", "x"), + env.DiagnosticAtRegexp("modb/v2/b/b.go", "x"), + env.DiagnosticAtRegexp("modc/main.go", "x"), ) }) } @@ -1203,11 +1192,9 @@ func TestOldGoNotification_SupportedVersion(t *testing.T) { } Run(t, "", func(t *testing.T, env *Env) { - env.Await( - OnceMet( - InitialWorkspaceLoad, - NoShownMessage("upgrade"), - ), + env.OnceMet( + InitialWorkspaceLoad, + NoShownMessage("upgrade"), ) }) } From 87092c8c8c89b9b7fa33baa772b00a3cc5caf303 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 21:05:03 -0500 Subject: [PATCH 625/723] gopls/internal/lsp/fake: use protocol types for the fake editor Especially with the recent consolidation of position mapping into protocol.Mapper, there is no need for the fake package to use anything but protocol representation for positions, ranges, and edits. Make this change, eliminating redundant types and conversions, and simplifying the code. Notably, the representation of buffers as slices of lines is no longer useful. It proved most convenient to instead store a protocol.Mapper. Updates golang/go#39384 Change-Id: I8c131e55c4c281bd1fc2db6e5620cdfae0ebdee5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461935 Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/fake/edit.go | 142 ++------- gopls/internal/lsp/fake/edit_test.go | 66 ++--- gopls/internal/lsp/fake/editor.go | 273 +++++++----------- gopls/internal/lsp/fake/editor_test.go | 37 +-- gopls/internal/lsp/fake/workdir.go | 22 +- gopls/internal/lsp/fake/workdir_test.go | 4 +- gopls/internal/lsp/protocol/mapper.go | 4 +- gopls/internal/lsp/protocol/span.go | 4 +- gopls/internal/lsp/regtest/expectation.go | 15 +- gopls/internal/lsp/regtest/wrappers.go | 30 +- .../internal/regtest/bench/completion_test.go | 30 +- .../internal/regtest/bench/didchange_test.go | 14 +- .../regtest/completion/completion18_test.go | 7 +- .../regtest/completion/completion_test.go | 25 +- .../regtest/diagnostics/diagnostics_test.go | 4 +- .../regtest/misc/call_hierarchy_test.go | 2 +- .../internal/regtest/misc/definition_test.go | 6 +- gopls/internal/regtest/misc/extract_test.go | 4 +- gopls/internal/regtest/misc/fix_test.go | 2 +- gopls/internal/regtest/misc/highlight_test.go | 3 +- gopls/internal/regtest/misc/rename_test.go | 4 +- gopls/internal/regtest/misc/vuln_test.go | 2 +- .../internal/regtest/modfile/modfile_test.go | 2 +- 23 files changed, 257 insertions(+), 445 deletions(-) diff --git a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go index 7f15a41c1cf..e7eeca14662 100644 --- a/gopls/internal/lsp/fake/edit.go +++ b/gopls/internal/lsp/fake/edit.go @@ -5,56 +5,14 @@ package fake import ( - "fmt" - "strings" - "unicode/utf8" - "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/diff" ) -// Pos represents a position in a text buffer. -// Both Line and Column are 0-indexed. -// Column counts runes. -type Pos struct { - Line, Column int -} - -func (p Pos) String() string { - return fmt.Sprintf("%v:%v", p.Line, p.Column) -} - -// Range corresponds to protocol.Range, but uses the editor friend Pos -// instead of UTF-16 oriented protocol.Position -type Range struct { - Start Pos - End Pos -} - -func (p Pos) ToProtocolPosition() protocol.Position { - return protocol.Position{ - Line: uint32(p.Line), - Character: uint32(p.Column), - } -} - -func fromProtocolPosition(pos protocol.Position) Pos { - return Pos{ - Line: int(pos.Line), - Column: int(pos.Character), - } -} - -// Edit represents a single (contiguous) buffer edit. -type Edit struct { - Start, End Pos - Text string -} - // Location is the editor friendly equivalent of protocol.Location type Location struct { Path string - Range Range + Range protocol.Range } // SymbolInformation is an editor friendly version of @@ -68,97 +26,49 @@ type SymbolInformation struct { // NewEdit creates an edit replacing all content between // (startLine, startColumn) and (endLine, endColumn) with text. -func NewEdit(startLine, startColumn, endLine, endColumn int, text string) Edit { - return Edit{ - Start: Pos{Line: startLine, Column: startColumn}, - End: Pos{Line: endLine, Column: endColumn}, - Text: text, - } -} - -func (e Edit) toProtocolChangeEvent() protocol.TextDocumentContentChangeEvent { - return protocol.TextDocumentContentChangeEvent{ - Range: &protocol.Range{ - Start: e.Start.ToProtocolPosition(), - End: e.End.ToProtocolPosition(), +func NewEdit(startLine, startColumn, endLine, endColumn uint32, text string) protocol.TextEdit { + return protocol.TextEdit{ + Range: protocol.Range{ + Start: protocol.Position{Line: startLine, Character: startColumn}, + End: protocol.Position{Line: endLine, Character: endColumn}, }, - Text: e.Text, + NewText: text, } } -func fromProtocolTextEdit(textEdit protocol.TextEdit) Edit { - return Edit{ - Start: fromProtocolPosition(textEdit.Range.Start), - End: fromProtocolPosition(textEdit.Range.End), - Text: textEdit.NewText, - } -} - -// inText reports whether p is a valid position in the text buffer. -func inText(p Pos, content []string) bool { - if p.Line < 0 || p.Line >= len(content) { - return false - } - // Note the strict right bound: the column indexes character _separators_, - // not characters. - if p.Column < 0 || p.Column > len([]rune(content[p.Line])) { - return false +func EditToChangeEvent(e protocol.TextEdit) protocol.TextDocumentContentChangeEvent { + var rng protocol.Range = e.Range + return protocol.TextDocumentContentChangeEvent{ + Range: &rng, + Text: e.NewText, } - return true } // applyEdits applies the edits to a file with the specified lines, // and returns a new slice containing the lines of the patched file. // It is a wrapper around diff.Apply; see that function for preconditions. -func applyEdits(lines []string, edits []Edit) ([]string, error) { - src := strings.Join(lines, "\n") - - // Build a table of byte offset of start of each line. - lineOffset := make([]int, len(lines)) - offset := 0 - for i, line := range lines { - lineOffset[i] = offset - offset += len(line) + len("\n") - } - - var posErr error - posToOffset := func(pos Pos) int { - // Convert pos.Column (runes) to a UTF-8 byte offset. - if pos.Line > len(lines) { - posErr = fmt.Errorf("bad line") - return 0 - } - if pos.Line == len(lines) { - if pos.Column > 0 { - posErr = fmt.Errorf("bad column") - } - return len(src) // EOF - } - offset := lineOffset[pos.Line] - for i := 0; i < pos.Column; i++ { - r, sz := utf8.DecodeRuneInString(src[offset:]) - if r == '\n' && posErr == nil { - posErr = fmt.Errorf("bad column") - } - offset += sz - } - return offset - } - +func applyEdits(mapper *protocol.Mapper, edits []protocol.TextEdit, windowsLineEndings bool) ([]byte, error) { // Convert fake.Edits to diff.Edits diffEdits := make([]diff.Edit, len(edits)) for i, edit := range edits { + start, end, err := mapper.RangeOffsets(edit.Range) + if err != nil { + return nil, err + } diffEdits[i] = diff.Edit{ - Start: posToOffset(edit.Start), - End: posToOffset(edit.End), - New: edit.Text, + Start: start, + End: end, + New: edit.NewText, } } - patched, err := diff.Apply(src, diffEdits) + patchedString, err := diff.Apply(string(mapper.Content), diffEdits) if err != nil { return nil, err } - - return strings.Split(patched, "\n"), posErr + patched := []byte(patchedString) + if windowsLineEndings { + patched = toWindowsLineEndings(patched) + } + return patched, nil } diff --git a/gopls/internal/lsp/fake/edit_test.go b/gopls/internal/lsp/fake/edit_test.go index fc4211739b0..97e2c73e42d 100644 --- a/gopls/internal/lsp/fake/edit_test.go +++ b/gopls/internal/lsp/fake/edit_test.go @@ -5,15 +5,16 @@ package fake import ( - "strings" "testing" + + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func TestApplyEdits(t *testing.T) { tests := []struct { label string content string - edits []Edit + edits []protocol.TextEdit want string wantErr bool }{ @@ -23,74 +24,56 @@ func TestApplyEdits(t *testing.T) { { label: "empty edit", content: "hello", - edits: []Edit{}, + edits: []protocol.TextEdit{}, want: "hello", }, { label: "unicode edit", content: "hello, 日本語", - edits: []Edit{{ - Start: Pos{Line: 0, Column: 7}, - End: Pos{Line: 0, Column: 10}, - Text: "world", - }}, + edits: []protocol.TextEdit{ + NewEdit(0, 7, 0, 10, "world"), + }, want: "hello, world", }, { label: "range edit", content: "ABC\nDEF\nGHI\nJKL", - edits: []Edit{{ - Start: Pos{Line: 1, Column: 1}, - End: Pos{Line: 2, Column: 3}, - Text: "12\n345", - }}, + edits: []protocol.TextEdit{ + NewEdit(1, 1, 2, 3, "12\n345"), + }, want: "ABC\nD12\n345\nJKL", }, { label: "regression test for issue #57627", content: "go 1.18\nuse moda/a", - edits: []Edit{ - { - Start: Pos{Line: 1, Column: 0}, - End: Pos{Line: 1, Column: 0}, - Text: "\n", - }, - { - Start: Pos{Line: 2, Column: 0}, - End: Pos{Line: 2, Column: 0}, - Text: "\n", - }, + edits: []protocol.TextEdit{ + NewEdit(1, 0, 1, 0, "\n"), + NewEdit(2, 0, 2, 0, "\n"), }, want: "go 1.18\n\nuse moda/a\n", }, { label: "end before start", content: "ABC\nDEF\nGHI\nJKL", - edits: []Edit{{ - End: Pos{Line: 1, Column: 1}, - Start: Pos{Line: 2, Column: 3}, - Text: "12\n345", - }}, + edits: []protocol.TextEdit{ + NewEdit(2, 3, 1, 1, "12\n345"), + }, wantErr: true, }, { label: "out of bounds line", content: "ABC\nDEF\nGHI\nJKL", - edits: []Edit{{ - Start: Pos{Line: 1, Column: 1}, - End: Pos{Line: 4, Column: 3}, - Text: "12\n345", - }}, + edits: []protocol.TextEdit{ + NewEdit(1, 1, 4, 3, "12\n345"), + }, wantErr: true, }, { label: "out of bounds column", content: "ABC\nDEF\nGHI\nJKL", - edits: []Edit{{ - Start: Pos{Line: 1, Column: 4}, - End: Pos{Line: 2, Column: 3}, - Text: "12\n345", - }}, + edits: []protocol.TextEdit{ + NewEdit(1, 4, 2, 3, "12\n345"), + }, wantErr: true, }, } @@ -98,15 +81,14 @@ func TestApplyEdits(t *testing.T) { for _, test := range tests { test := test t.Run(test.label, func(t *testing.T) { - lines := strings.Split(test.content, "\n") - newLines, err := applyEdits(lines, test.edits) + got, err := applyEdits(protocol.NewMapper("", []byte(test.content)), test.edits, false) if (err != nil) != test.wantErr { t.Errorf("got err %v, want error: %t", err, test.wantErr) } if err != nil { return } - if got := strings.Join(newLines, "\n"); got != test.want { + if got := string(got); got != test.want { t.Errorf("got %q, want %q", got, test.want) } }) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 25aa6d5446e..00bba62997c 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -5,7 +5,7 @@ package fake import ( - "bufio" + "bytes" "context" "errors" "fmt" @@ -60,19 +60,14 @@ type CallCounts struct { // buffer holds information about an open buffer in the editor. type buffer struct { - windowsLineEndings bool // use windows line endings when merging lines - version int // monotonic version; incremented on edits - path string // relative path in the workspace - lines []string // line content - dirty bool // if true, content is unsaved (TODO(rfindley): rename this field) + version int // monotonic version; incremented on edits + path string // relative path in the workspace + mapper *protocol.Mapper // buffer content + dirty bool // if true, content is unsaved (TODO(rfindley): rename this field) } func (b buffer) text() string { - eol := "\n" - if b.windowsLineEndings { - eol = "\r\n" - } - return strings.Join(b.lines, eol) + return string(b.mapper.Content) } // EditorConfig configures the editor's LSP session. This is similar to @@ -356,11 +351,11 @@ func (e *Editor) onFileChanges(ctx context.Context, evts []protocol.FileEvent) { continue // A race with some other operation. } // No need to update if the buffer content hasn't changed. - if content == buf.text() { + if string(content) == buf.text() { continue } // During shutdown, this call will fail. Ignore the error. - _ = e.setBufferContentLocked(ctx, path, false, lines(content), nil) + _ = e.setBufferContentLocked(ctx, path, false, content, nil) } } var matchedEvts []protocol.FileEvent @@ -392,16 +387,43 @@ func (e *Editor) OpenFile(ctx context.Context, path string) error { if err != nil { return err } + if e.Config().WindowsLineEndings { + content = toWindowsLineEndings(content) + } return e.createBuffer(ctx, path, false, content) } +// toWindowsLineEndings checks whether content has windows line endings. +// +// If so, it returns content unmodified. If not, it returns a new byte slice modified to use CRLF line endings. +func toWindowsLineEndings(content []byte) []byte { + abnormal := false + for i, b := range content { + if b == '\n' && (i == 0 || content[i-1] != '\r') { + abnormal = true + break + } + } + if !abnormal { + return content + } + var buf bytes.Buffer + for i, b := range content { + if b == '\n' && (i == 0 || content[i-1] != '\r') { + buf.WriteByte('\r') + } + buf.WriteByte(b) + } + return buf.Bytes() +} + // CreateBuffer creates a new unsaved buffer corresponding to the workdir path, // containing the given textual content. func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { - return e.createBuffer(ctx, path, true, content) + return e.createBuffer(ctx, path, true, []byte(content)) } -func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error { +func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content []byte) error { e.mu.Lock() if _, ok := e.buffers[path]; ok { @@ -409,12 +431,12 @@ func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, cont return fmt.Errorf("buffer %q already exists", path) } + uri := e.sandbox.Workdir.URI(path).SpanURI() buf := buffer{ - windowsLineEndings: e.config.WindowsLineEndings, - version: 1, - path: path, - lines: lines(content), - dirty: dirty, + version: 1, + path: path, + mapper: protocol.NewMapper(uri, content), + dirty: dirty, } e.buffers[path] = buf @@ -476,15 +498,6 @@ func languageID(p string, fileAssociations map[string]string) string { return "" } -// lines returns line-ending agnostic line representation of content. -func lines(content string) []string { - lines := strings.Split(content, "\n") - for i, l := range lines { - lines[i] = strings.TrimSuffix(l, "\r") - } - return lines -} - // CloseBuffer removes the current buffer (regardless of whether it is saved). func (e *Editor) CloseBuffer(ctx context.Context, path string) error { e.mu.Lock() @@ -578,40 +591,6 @@ func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) erro return nil } -// contentPosition returns the (Line, Column) position corresponding to offset -// in the buffer referenced by path. -// -// TODO(adonovan): offset is measured in runes, according to the test, -// but I can't imagine why that would be useful, and the only actual -// caller (regexpRange) seems to pass a byte offset. -// I would expect the implementation to be simply: -// -// prefix := content[:offset] -// line := strings.Count(prefix, "\n") // 0-based -// col := utf8.RuneCountInString(prefix[strings.LastIndex(prefix, "\n")+1:]) // 0-based, runes -// return Pos{Line: line, Column: col}, nil -func contentPosition(content string, offset int) (Pos, error) { - scanner := bufio.NewScanner(strings.NewReader(content)) - start := 0 - line := 0 - for scanner.Scan() { - end := start + len([]rune(scanner.Text())) + 1 - if offset < end { - return Pos{Line: line, Column: offset - start}, nil - } - start = end - line++ - } - if err := scanner.Err(); err != nil { - return Pos{}, fmt.Errorf("scanning content: %w", err) - } - // Scan() will drop the last line if it is empty. Correct for this. - if (strings.HasSuffix(content, "\n") || content == "") && offset == start { - return Pos{Line: line, Column: 0}, nil - } - return Pos{}, fmt.Errorf("position %d out of bounds in %q (line = %d, start = %d)", offset, content, line, start) -} - // ErrNoMatch is returned if a regexp search fails. var ( ErrNoMatch = errors.New("no match") @@ -620,16 +599,15 @@ var ( // regexpRange returns the start and end of the first occurrence of either re // or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match. -func regexpRange(content, re string) (Pos, Pos, error) { - content = normalizeEOL(content) +func regexpRange(mapper *protocol.Mapper, re string) (protocol.Range, error) { var start, end int rec, err := regexp.Compile(re) if err != nil { - return Pos{}, Pos{}, err + return protocol.Range{}, err } - indexes := rec.FindStringSubmatchIndex(content) + indexes := rec.FindSubmatchIndex(mapper.Content) if indexes == nil { - return Pos{}, Pos{}, ErrNoMatch + return protocol.Range{}, ErrNoMatch } switch len(indexes) { case 2: @@ -639,33 +617,21 @@ func regexpRange(content, re string) (Pos, Pos, error) { // one subgroup: return its range start, end = indexes[2], indexes[3] default: - return Pos{}, Pos{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1) - } - startPos, err := contentPosition(content, start) - if err != nil { - return Pos{}, Pos{}, err - } - endPos, err := contentPosition(content, end) - if err != nil { - return Pos{}, Pos{}, err + return protocol.Range{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1) } - return startPos, endPos, nil -} - -func normalizeEOL(content string) string { - return strings.Join(lines(content), "\n") + return mapper.OffsetRange(start, end) } // RegexpRange returns the first range in the buffer bufName matching re. See // RegexpSearch for more information on matching. -func (e *Editor) RegexpRange(bufName, re string) (Pos, Pos, error) { +func (e *Editor) RegexpRange(bufName, re string) (protocol.Range, error) { e.mu.Lock() defer e.mu.Unlock() buf, ok := e.buffers[bufName] if !ok { - return Pos{}, Pos{}, ErrUnknownBuffer + return protocol.Range{}, ErrUnknownBuffer } - return regexpRange(buf.text(), re) + return regexpRange(buf.mapper, re) } // RegexpSearch returns the position of the first match for re in the buffer @@ -675,9 +641,9 @@ func (e *Editor) RegexpRange(bufName, re string) (Pos, Pos, error) { // // It returns an error re is invalid, has more than one subgroup, or doesn't // match the buffer. -func (e *Editor) RegexpSearch(bufName, re string) (Pos, error) { - start, _, err := e.RegexpRange(bufName, re) - return start, err +func (e *Editor) RegexpSearch(bufName, re string) (protocol.Position, error) { + rng, err := e.RegexpRange(bufName, re) + return rng.Start, err } // RegexpReplace edits the buffer corresponding to path by replacing the first @@ -692,20 +658,23 @@ func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) er if !ok { return ErrUnknownBuffer } - content := buf.text() - start, end, err := regexpRange(content, re) + rng, err := regexpRange(buf.mapper, re) if err != nil { return err } - return e.editBufferLocked(ctx, path, []Edit{{ - Start: start, - End: end, - Text: replace, - }}) + edits := []protocol.TextEdit{{ + Range: rng, + NewText: replace, + }} + patched, err := applyEdits(buf.mapper, edits, e.config.WindowsLineEndings) + if err != nil { + return fmt.Errorf("editing %q: %v", path, err) + } + return e.setBufferContentLocked(ctx, path, true, patched, edits) } // EditBuffer applies the given test edits to the buffer identified by path. -func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error { +func (e *Editor) EditBuffer(ctx context.Context, path string, edits []protocol.TextEdit) error { e.mu.Lock() defer e.mu.Unlock() return e.editBufferLocked(ctx, path, edits) @@ -714,8 +683,7 @@ func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) erro func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error { e.mu.Lock() defer e.mu.Unlock() - lines := lines(content) - return e.setBufferContentLocked(ctx, path, true, lines, nil) + return e.setBufferContentLocked(ctx, path, true, []byte(content), nil) } // HasBuffer reports whether the file name is open in the editor. @@ -747,24 +715,24 @@ func (e *Editor) BufferVersion(name string) int { return e.buffers[name].version } -func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit) error { +func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []protocol.TextEdit) error { buf, ok := e.buffers[path] if !ok { return fmt.Errorf("unknown buffer %q", path) } - content, err := applyEdits(buf.lines, edits) + content, err := applyEdits(buf.mapper, edits, e.config.WindowsLineEndings) if err != nil { return fmt.Errorf("editing %q: %v; edits:\n%v", path, err, edits) } return e.setBufferContentLocked(ctx, path, true, content, edits) } -func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []string, fromEdits []Edit) error { +func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []byte, fromEdits []protocol.TextEdit) error { buf, ok := e.buffers[path] if !ok { return fmt.Errorf("unknown buffer %q", path) } - buf.lines = content + buf.mapper = protocol.NewMapper(buf.mapper.URI, content) buf.version++ buf.dirty = dirty e.buffers[path] = buf @@ -772,7 +740,7 @@ func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty // Otherwise, send the entire content. var evts []protocol.TextDocumentContentChangeEvent if len(fromEdits) == 1 { - evts = append(evts, fromEdits[0].toProtocolChangeEvent()) + evts = append(evts, EditToChangeEvent(fromEdits[0])) } else { evts = append(evts, protocol.TextDocumentContentChangeEvent{ Text: buf.text(), @@ -798,53 +766,52 @@ func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty // GoToDefinition jumps to the definition of the symbol at the given position // in an open buffer. It returns the path and position of the resulting jump. -func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) { +func (e *Editor) GoToDefinition(ctx context.Context, path string, pos protocol.Position) (string, protocol.Position, error) { if err := e.checkBufferPosition(path, pos); err != nil { - return "", Pos{}, err + return "", protocol.Position{}, err } params := &protocol.DefinitionParams{} params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos.ToProtocolPosition() + params.Position = pos resp, err := e.Server.Definition(ctx, params) if err != nil { - return "", Pos{}, fmt.Errorf("definition: %w", err) + return "", protocol.Position{}, fmt.Errorf("definition: %w", err) } return e.extractFirstPathAndPos(ctx, resp) } // GoToTypeDefinition jumps to the type definition of the symbol at the given position // in an open buffer. -func (e *Editor) GoToTypeDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) { +func (e *Editor) GoToTypeDefinition(ctx context.Context, path string, pos protocol.Position) (string, protocol.Position, error) { if err := e.checkBufferPosition(path, pos); err != nil { - return "", Pos{}, err + return "", protocol.Position{}, err } params := &protocol.TypeDefinitionParams{} params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos.ToProtocolPosition() + params.Position = pos resp, err := e.Server.TypeDefinition(ctx, params) if err != nil { - return "", Pos{}, fmt.Errorf("type definition: %w", err) + return "", protocol.Position{}, fmt.Errorf("type definition: %w", err) } return e.extractFirstPathAndPos(ctx, resp) } // extractFirstPathAndPos returns the path and the position of the first location. // It opens the file if needed. -func (e *Editor) extractFirstPathAndPos(ctx context.Context, locs []protocol.Location) (string, Pos, error) { +func (e *Editor) extractFirstPathAndPos(ctx context.Context, locs []protocol.Location) (string, protocol.Position, error) { if len(locs) == 0 { - return "", Pos{}, nil + return "", protocol.Position{}, nil } newPath := e.sandbox.Workdir.URIToPath(locs[0].URI) - newPos := fromProtocolPosition(locs[0].Range.Start) if !e.HasBuffer(newPath) { if err := e.OpenFile(ctx, newPath); err != nil { - return "", Pos{}, fmt.Errorf("OpenFile: %w", err) + return "", protocol.Position{}, fmt.Errorf("OpenFile: %w", err) } } - return newPath, newPos, nil + return newPath, locs[0].Range.Start, nil } // Symbol performs a workspace symbol search using query @@ -860,15 +827,9 @@ func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, for _, si := range resp { ploc := si.Location path := e.sandbox.Workdir.URIToPath(ploc.URI) - start := fromProtocolPosition(ploc.Range.Start) - end := fromProtocolPosition(ploc.Range.End) - rnge := Range{ - Start: start, - End: end, - } loc := Location{ Path: path, - Range: rnge, + Range: ploc.Range, } res = append(res, SymbolInformation{ Name: si.Name, @@ -915,8 +876,7 @@ func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction // Skip edits for old versions. continue } - edits := convertEdits(change.TextDocumentEdit.Edits) - if err := e.EditBuffer(ctx, path, edits); err != nil { + if err := e.EditBuffer(ctx, path, change.TextDocumentEdit.Edits); err != nil { return fmt.Errorf("editing buffer %q: %w", path, err) } } @@ -1011,14 +971,6 @@ func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCom return result, nil } -func convertEdits(protocolEdits []protocol.TextEdit) []Edit { - var edits []Edit - for _, lspEdit := range protocolEdits { - edits = append(edits, fromProtocolTextEdit(lspEdit)) - } - return edits -} - // FormatBuffer gofmts a Go file. func (e *Editor) FormatBuffer(ctx context.Context, path string) error { if e.Server == nil { @@ -1029,7 +981,7 @@ func (e *Editor) FormatBuffer(ctx context.Context, path string) error { e.mu.Unlock() params := &protocol.DocumentFormattingParams{} params.TextDocument.URI = e.sandbox.Workdir.URI(path) - resp, err := e.Server.Formatting(ctx, params) + edits, err := e.Server.Formatting(ctx, params) if err != nil { return fmt.Errorf("textDocument/formatting: %w", err) } @@ -1038,24 +990,22 @@ func (e *Editor) FormatBuffer(ctx context.Context, path string) error { if versionAfter := e.buffers[path].version; versionAfter != version { return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter) } - edits := convertEdits(resp) if len(edits) == 0 { return nil } return e.editBufferLocked(ctx, path, edits) } -func (e *Editor) checkBufferPosition(path string, pos Pos) error { +func (e *Editor) checkBufferPosition(path string, pos protocol.Position) error { e.mu.Lock() defer e.mu.Unlock() buf, ok := e.buffers[path] if !ok { return fmt.Errorf("buffer %q is not open", path) } - if !inText(pos, buf.lines) { - return fmt.Errorf("position %v is invalid in buffer %q", pos, path) - } - return nil + + _, err := buf.mapper.PositionOffset(pos) + return err } // RunGenerate runs `go generate` non-recursively in the workdir-relative dir @@ -1111,7 +1061,7 @@ func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens } // Completion executes a completion request on the server. -func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protocol.CompletionList, error) { +func (e *Editor) Completion(ctx context.Context, path string, pos protocol.Position) (*protocol.CompletionList, error) { if e.Server == nil { return nil, nil } @@ -1124,7 +1074,7 @@ func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protoco params := &protocol.CompletionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocument: e.TextDocumentIdentifier(path), - Position: pos.ToProtocolPosition(), + Position: pos, }, } completions, err := e.Server.Completion(ctx, params) @@ -1136,7 +1086,7 @@ func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protoco // AcceptCompletion accepts a completion for the given item at the given // position. -func (e *Editor) AcceptCompletion(ctx context.Context, path string, pos Pos, item protocol.CompletionItem) error { +func (e *Editor) AcceptCompletion(ctx context.Context, path string, pos protocol.Position, item protocol.CompletionItem) error { if e.Server == nil { return nil } @@ -1146,9 +1096,9 @@ func (e *Editor) AcceptCompletion(ctx context.Context, path string, pos Pos, ite if !ok { return fmt.Errorf("buffer %q is not open", path) } - return e.editBufferLocked(ctx, path, convertEdits(append([]protocol.TextEdit{ + return e.editBufferLocked(ctx, path, append([]protocol.TextEdit{ *item.TextEdit, - }, item.AdditionalTextEdits...))) + }, item.AdditionalTextEdits...)) } // Symbols executes a workspace/symbols request on the server. @@ -1184,7 +1134,7 @@ func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHi // References returns references to the object at (path, pos), as returned by // the connected LSP server. If no server is connected, it returns (nil, nil). -func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) { +func (e *Editor) References(ctx context.Context, path string, pos protocol.Position) ([]protocol.Location, error) { if e.Server == nil { return nil, nil } @@ -1197,7 +1147,7 @@ func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protoc params := &protocol.ReferenceParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocument: e.TextDocumentIdentifier(path), - Position: pos.ToProtocolPosition(), + Position: pos, }, Context: protocol.ReferenceContext{ IncludeDeclaration: true, @@ -1212,7 +1162,7 @@ func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protoc // Rename performs a rename of the object at (path, pos) to newName, using the // connected LSP server. If no server is connected, it returns nil. -func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName string) error { +func (e *Editor) Rename(ctx context.Context, path string, pos protocol.Position, newName string) error { if e.Server == nil { return nil } @@ -1220,14 +1170,14 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin // Verify that PrepareRename succeeds. prepareParams := &protocol.PrepareRenameParams{} prepareParams.TextDocument = e.TextDocumentIdentifier(path) - prepareParams.Position = pos.ToProtocolPosition() + prepareParams.Position = pos if _, err := e.Server.PrepareRename(ctx, prepareParams); err != nil { return fmt.Errorf("preparing rename: %v", err) } params := &protocol.RenameParams{ TextDocument: e.TextDocumentIdentifier(path), - Position: pos.ToProtocolPosition(), + Position: pos, NewName: newName, } wsEdits, err := e.Server.Rename(ctx, params) @@ -1245,7 +1195,7 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin // Implementations returns implementations for the object at (path, pos), as // returned by the connected LSP server. If no server is connected, it returns // (nil, nil). -func (e *Editor) Implementations(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) { +func (e *Editor) Implementations(ctx context.Context, path string, pos protocol.Position) ([]protocol.Location, error) { if e.Server == nil { return nil, nil } @@ -1258,7 +1208,7 @@ func (e *Editor) Implementations(ctx context.Context, path string, pos Pos) ([]p params := &protocol.ImplementationParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocument: e.TextDocumentIdentifier(path), - Position: pos.ToProtocolPosition(), + Position: pos, }, } return e.Server.Implementation(ctx, params) @@ -1362,8 +1312,7 @@ func (e *Editor) applyTextDocumentEdit(ctx context.Context, change protocol.Text return err } } - fakeEdits := convertEdits(change.Edits) - return e.EditBuffer(ctx, path, fakeEdits) + return e.EditBuffer(ctx, path, change.Edits) } // Config returns the current editor configuration. @@ -1466,22 +1415,22 @@ func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Rang } // Hover triggers a hover at the given position in an open buffer. -func (e *Editor) Hover(ctx context.Context, path string, pos Pos) (*protocol.MarkupContent, Pos, error) { +func (e *Editor) Hover(ctx context.Context, path string, pos protocol.Position) (*protocol.MarkupContent, protocol.Position, error) { if err := e.checkBufferPosition(path, pos); err != nil { - return nil, Pos{}, err + return nil, protocol.Position{}, err } params := &protocol.HoverParams{} params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos.ToProtocolPosition() + params.Position = pos resp, err := e.Server.Hover(ctx, params) if err != nil { - return nil, Pos{}, fmt.Errorf("hover: %w", err) + return nil, protocol.Position{}, fmt.Errorf("hover: %w", err) } if resp == nil { - return nil, Pos{}, nil + return nil, protocol.Position{}, nil } - return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil + return &resp.Contents, resp.Range.Start, nil } func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) { @@ -1493,7 +1442,7 @@ func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.Docu return e.Server.DocumentLink(ctx, params) } -func (e *Editor) DocumentHighlight(ctx context.Context, path string, pos Pos) ([]protocol.DocumentHighlight, error) { +func (e *Editor) DocumentHighlight(ctx context.Context, path string, pos protocol.Position) ([]protocol.DocumentHighlight, error) { if e.Server == nil { return nil, nil } @@ -1502,7 +1451,7 @@ func (e *Editor) DocumentHighlight(ctx context.Context, path string, pos Pos) ([ } params := &protocol.DocumentHighlightParams{} params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos.ToProtocolPosition() + params.Position = pos return e.Server.DocumentHighlight(ctx, params) } diff --git a/gopls/internal/lsp/fake/editor_test.go b/gopls/internal/lsp/fake/editor_test.go index 3ce5df6e08f..cc8a14744d2 100644 --- a/gopls/internal/lsp/fake/editor_test.go +++ b/gopls/internal/lsp/fake/editor_test.go @@ -7,32 +7,9 @@ package fake import ( "context" "testing" -) -func TestContentPosition(t *testing.T) { - content := "foo\n😀\nbar" - tests := []struct { - offset, wantLine, wantColumn int - }{ - {0, 0, 0}, - {3, 0, 3}, - {4, 1, 0}, - {5, 1, 1}, - {6, 2, 0}, - } - for _, test := range tests { - pos, err := contentPosition(content, test.offset) - if err != nil { - t.Fatal(err) - } - if pos.Line != test.wantLine { - t.Errorf("contentPosition(%q, %d): Line = %d, want %d", content, test.offset, pos.Line, test.wantLine) - } - if pos.Column != test.wantColumn { - t.Errorf("contentPosition(%q, %d): Column = %d, want %d", content, test.offset, pos.Column, test.wantColumn) - } - } -} + "golang.org/x/tools/gopls/internal/lsp/protocol" +) const exampleProgram = ` -- go.mod -- @@ -58,11 +35,13 @@ func TestClientEditing(t *testing.T) { if err := editor.OpenFile(ctx, "main.go"); err != nil { t.Fatal(err) } - if err := editor.EditBuffer(ctx, "main.go", []Edit{ + if err := editor.EditBuffer(ctx, "main.go", []protocol.TextEdit{ { - Start: Pos{5, 14}, - End: Pos{5, 26}, - Text: "Hola, mundo.", + Range: protocol.Range{ + Start: protocol.Position{Line: 5, Character: 14}, + End: protocol.Position{Line: 5, Character: 26}, + }, + NewText: "Hola, mundo.", }, }); err != nil { t.Fatal(err) diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index 19907ada655..2470e74e734 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -178,7 +178,7 @@ func toURI(fp string) protocol.DocumentURI { } // ReadFile reads a text file specified by a workdir-relative path. -func (w *Workdir) ReadFile(path string) (string, error) { +func (w *Workdir) ReadFile(path string) ([]byte, error) { backoff := 1 * time.Millisecond for { b, err := ioutil.ReadFile(w.AbsPath(path)) @@ -190,29 +190,31 @@ func (w *Workdir) ReadFile(path string) (string, error) { backoff *= 2 continue } - return "", err + return nil, err } - return string(b), nil + return b, nil } } -func (w *Workdir) RegexpRange(path, re string) (Pos, Pos, error) { +func (w *Workdir) RegexpRange(path, re string) (protocol.Range, error) { content, err := w.ReadFile(path) if err != nil { - return Pos{}, Pos{}, err + return protocol.Range{}, err } - return regexpRange(content, re) + mapper := protocol.NewMapper(w.URI(path).SpanURI(), content) + return regexpRange(mapper, re) } // RegexpSearch searches the file corresponding to path for the first position // matching re. -func (w *Workdir) RegexpSearch(path string, re string) (Pos, error) { +func (w *Workdir) RegexpSearch(path string, re string) (protocol.Position, error) { content, err := w.ReadFile(path) if err != nil { - return Pos{}, err + return protocol.Position{}, err } - start, _, err := regexpRange(content, re) - return start, err + mapper := protocol.NewMapper(w.URI(path).SpanURI(), content) + rng, err := regexpRange(mapper, re) + return rng.Start, err } // RemoveFile removes a workdir-relative file path and notifies watchers of the diff --git a/gopls/internal/lsp/fake/workdir_test.go b/gopls/internal/lsp/fake/workdir_test.go index c660a405cda..d036658ef2d 100644 --- a/gopls/internal/lsp/fake/workdir_test.go +++ b/gopls/internal/lsp/fake/workdir_test.go @@ -85,7 +85,7 @@ func TestWorkdir_ReadFile(t *testing.T) { t.Fatal(err) } want := "Hello World!\n" - if got != want { + if got := string(got); got != want { t.Errorf("reading workdir file, got %q, want %q", got, want) } } @@ -123,7 +123,7 @@ func TestWorkdir_WriteFile(t *testing.T) { t.Fatal(err) } want := "42" - if got != want { + if got := string(got); got != want { t.Errorf("ws.ReadFile(%q) = %q, want %q", test.path, got, want) } } diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go index 2a9df8a2f81..ae7218e2fae 100644 --- a/gopls/internal/lsp/protocol/mapper.go +++ b/gopls/internal/lsp/protocol/mapper.go @@ -185,7 +185,7 @@ func (m *Mapper) PointPosition(p span.Point) (Position, error) { return Position{}, fmt.Errorf("column is beyond end of line") } - char := utf16len(m.Content[offset:end]) + char := UTF16Len(m.Content[offset:end]) return Position{Line: uint32(line), Character: uint32(char)}, nil } if p.HasOffset() { @@ -247,7 +247,7 @@ func (m *Mapper) lineCol16(offset int) (int, int) { line, start, cr := m.line(offset) var col16 int if m.nonASCII { - col16 = utf16len(m.Content[start:offset]) + col16 = UTF16Len(m.Content[start:offset]) } else { col16 = offset - start } diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index 387c1d9535f..d484f8f7413 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -96,8 +96,8 @@ func (p Position) Format(f fmt.State, _ rune) { // -- implementation helpers -- -// utf16len returns the number of codes in the UTF-16 transcoding of s. -func utf16len(s []byte) int { +// UTF16Len returns the number of codes in the UTF-16 transcoding of s. +func UTF16Len(s []byte) int { var n int for len(s) > 0 { n++ diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 410e77873b5..8073561a37d 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -11,7 +11,6 @@ import ( "strings" "golang.org/x/tools/gopls/internal/lsp" - "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" ) @@ -624,7 +623,7 @@ func UnregistrationMatching(re string) SimpleExpectation { // of diagnostics for a file. type DiagnosticExpectation struct { // optionally, the position of the diagnostic and the regex used to calculate it. - pos *fake.Pos + pos *protocol.Position re string // optionally, the message that the diagnostic should contain. @@ -653,7 +652,7 @@ func (e DiagnosticExpectation) Check(s State) Verdict { found := false for _, d := range diags.Diagnostics { if e.pos != nil { - if d.Range.Start.Line != uint32(e.pos.Line) || d.Range.Start.Character != uint32(e.pos.Column) { + if d.Range.Start != *e.pos { continue } } @@ -683,7 +682,7 @@ func (e DiagnosticExpectation) Description() string { } desc += " diagnostic" if e.pos != nil { - desc += fmt.Sprintf(" at {line:%d, column:%d}", e.pos.Line, e.pos.Column) + desc += fmt.Sprintf(" at {line:%d, column:%d}", e.pos.Line, e.pos.Character) if e.re != "" { desc += fmt.Sprintf(" (location of %q)", e.re) } @@ -815,9 +814,7 @@ func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { return DiagnosticFilter{ desc: fmt.Sprintf("at the first position matching %q in %q", pattern, name), check: func(diagName string, d protocol.Diagnostic) bool { - // TODO(rfindley): just use protocol.Position for Pos, rather than - // duplicating. - return diagName == name && d.Range.Start.Line == uint32(pos.Line) && d.Range.Start.Character == uint32(pos.Column) + return diagName == name && d.Range.Start == pos }, } } @@ -869,6 +866,6 @@ func (e *Env) DiagnosticAtRegexpWithMessage(name, re, msg string) DiagnosticExpe // DiagnosticAt asserts that there is a diagnostic entry at the position // specified by line and col, for the workdir-relative path name. -func DiagnosticAt(name string, line, col int) DiagnosticExpectation { - return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: true} +func DiagnosticAt(name string, line, col uint32) DiagnosticExpectation { + return DiagnosticExpectation{path: name, pos: &protocol.Position{Line: line, Character: col}, present: true} } diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index fe80d89fc87..3699597bd71 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -30,7 +30,7 @@ func (e *Env) ReadWorkspaceFile(name string) string { if err != nil { e.T.Fatal(err) } - return content + return string(content) } // WriteWorkspaceFile writes a file to disk but does nothing in the editor. @@ -99,7 +99,7 @@ func (e *Env) CloseBuffer(name string) { } // EditBuffer applies edits to an editor buffer, calling t.Fatal on any error. -func (e *Env) EditBuffer(name string, edits ...fake.Edit) { +func (e *Env) EditBuffer(name string, edits ...protocol.TextEdit) { e.T.Helper() if err := e.Editor.EditBuffer(e.Ctx, name, edits); err != nil { e.T.Fatal(err) @@ -116,22 +116,22 @@ func (e *Env) SetBufferContent(name string, content string) { // RegexpRange returns the range of the first match for re in the buffer // specified by name, calling t.Fatal on any error. It first searches for the // position in open buffers, then in workspace files. -func (e *Env) RegexpRange(name, re string) (fake.Pos, fake.Pos) { +func (e *Env) RegexpRange(name, re string) protocol.Range { e.T.Helper() - start, end, err := e.Editor.RegexpRange(name, re) + rng, err := e.Editor.RegexpRange(name, re) if err == fake.ErrUnknownBuffer { - start, end, err = e.Sandbox.Workdir.RegexpRange(name, re) + rng, err = e.Sandbox.Workdir.RegexpRange(name, re) } if err != nil { e.T.Fatalf("RegexpRange: %v, %v", name, err) } - return start, end + return rng } // RegexpSearch returns the starting position of the first match for re in the // buffer specified by name, calling t.Fatal on any error. It first searches // for the position in open buffers, then in workspace files. -func (e *Env) RegexpSearch(name, re string) fake.Pos { +func (e *Env) RegexpSearch(name, re string) protocol.Position { e.T.Helper() pos, err := e.Editor.RegexpSearch(name, re) if err == fake.ErrUnknownBuffer { @@ -169,7 +169,7 @@ func (e *Env) SaveBufferWithoutActions(name string) { // GoToDefinition goes to definition in the editor, calling t.Fatal on any // error. It returns the path and position of the resulting jump. -func (e *Env) GoToDefinition(name string, pos fake.Pos) (string, fake.Pos) { +func (e *Env) GoToDefinition(name string, pos protocol.Position) (string, protocol.Position) { e.T.Helper() n, p, err := e.Editor.GoToDefinition(e.Ctx, name, pos) if err != nil { @@ -232,7 +232,7 @@ func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) []pr } // Hover in the editor, calling t.Fatal on any error. -func (e *Env) Hover(name string, pos fake.Pos) (*protocol.MarkupContent, fake.Pos) { +func (e *Env) Hover(name string, pos protocol.Position) (*protocol.MarkupContent, protocol.Position) { e.T.Helper() c, p, err := e.Editor.Hover(e.Ctx, name, pos) if err != nil { @@ -250,7 +250,7 @@ func (e *Env) DocumentLink(name string) []protocol.DocumentLink { return links } -func (e *Env) DocumentHighlight(name string, pos fake.Pos) []protocol.DocumentHighlight { +func (e *Env) DocumentHighlight(name string, pos protocol.Position) []protocol.DocumentHighlight { e.T.Helper() highlights, err := e.Editor.DocumentHighlight(e.Ctx, name, pos) if err != nil { @@ -405,7 +405,7 @@ func (e *Env) WorkspaceSymbol(sym string) []protocol.SymbolInformation { } // References wraps Editor.References, calling t.Fatal on any error. -func (e *Env) References(path string, pos fake.Pos) []protocol.Location { +func (e *Env) References(path string, pos protocol.Position) []protocol.Location { e.T.Helper() locations, err := e.Editor.References(e.Ctx, path, pos) if err != nil { @@ -415,7 +415,7 @@ func (e *Env) References(path string, pos fake.Pos) []protocol.Location { } // Rename wraps Editor.Rename, calling t.Fatal on any error. -func (e *Env) Rename(path string, pos fake.Pos, newName string) { +func (e *Env) Rename(path string, pos protocol.Position, newName string) { e.T.Helper() if err := e.Editor.Rename(e.Ctx, path, pos, newName); err != nil { e.T.Fatal(err) @@ -423,7 +423,7 @@ func (e *Env) Rename(path string, pos fake.Pos, newName string) { } // Implementations wraps Editor.Implementations, calling t.Fatal on any error. -func (e *Env) Implementations(path string, pos fake.Pos) []protocol.Location { +func (e *Env) Implementations(path string, pos protocol.Position) []protocol.Location { e.T.Helper() locations, err := e.Editor.Implementations(e.Ctx, path, pos) if err != nil { @@ -441,7 +441,7 @@ func (e *Env) RenameFile(oldPath, newPath string) { } // Completion executes a completion request on the server. -func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList { +func (e *Env) Completion(path string, pos protocol.Position) *protocol.CompletionList { e.T.Helper() completions, err := e.Editor.Completion(e.Ctx, path, pos) if err != nil { @@ -452,7 +452,7 @@ func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList { // AcceptCompletion accepts a completion for the given item at the given // position. -func (e *Env) AcceptCompletion(path string, pos fake.Pos, item protocol.CompletionItem) { +func (e *Env) AcceptCompletion(path string, pos protocol.Position, item protocol.CompletionItem) { e.T.Helper() if err := e.Editor.AcceptCompletion(e.Ctx, path, pos, item); err != nil { e.T.Fatal(err) diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go index 51860fdc765..aafc970f1c2 100644 --- a/gopls/internal/regtest/bench/completion_test.go +++ b/gopls/internal/regtest/bench/completion_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/fake" @@ -84,15 +85,16 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) { // endPosInBuffer returns the position for last character in the buffer for // the given file. -func endPosInBuffer(env *Env, name string) fake.Pos { +func endRangeInBuffer(env *Env, name string) protocol.Range { buffer := env.BufferText(name) lines := strings.Split(buffer, "\n") numLines := len(lines) - return fake.Pos{ - Line: numLines - 1, - Column: len([]rune(lines[numLines-1])), + end := protocol.Position{ + Line: uint32(numLines - 1), + Character: uint32(len([]rune(lines[numLines-1]))), } + return protocol.Range{Start: end, End: end} } // Benchmark struct completion in tools codebase. @@ -101,10 +103,9 @@ func BenchmarkStructCompletion(b *testing.B) { setup := func(env *Env) { env.OpenFile(file) - originalBuffer := env.BufferText(file) - env.EditBuffer(file, fake.Edit{ - End: endPosInBuffer(env, file), - Text: originalBuffer + "\nvar testVariable map[string]bool = Session{}.\n", + env.EditBuffer(file, protocol.TextEdit{ + Range: endRangeInBuffer(env, file), + NewText: "\nvar testVariable map[string]bool = Session{}.\n", }) } @@ -131,10 +132,9 @@ func BenchmarkSliceCompletion(b *testing.B) { setup := func(env *Env) { env.OpenFile(file) - originalBuffer := env.BufferText(file) - env.EditBuffer(file, fake.Edit{ - End: endPosInBuffer(env, file), - Text: originalBuffer + "\nvar testVariable []byte = \n", + env.EditBuffer(file, protocol.TextEdit{ + Range: endRangeInBuffer(env, file), + NewText: "\nvar testVariable []byte = \n", }) } @@ -156,9 +156,9 @@ func (c *completer) _() { setup := func(env *Env) { env.OpenFile(file) originalBuffer := env.BufferText(file) - env.EditBuffer(file, fake.Edit{ - End: endPosInBuffer(env, file), - Text: originalBuffer + fileContent, + env.EditBuffer(file, protocol.TextEdit{ + Range: endRangeInBuffer(env, file), + NewText: originalBuffer + fileContent, }) } diff --git a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go index 5fd5e9c577b..d309e3dcebf 100644 --- a/gopls/internal/regtest/bench/didchange_test.go +++ b/gopls/internal/regtest/bench/didchange_test.go @@ -8,7 +8,7 @@ import ( "fmt" "testing" - "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // BenchmarkDidChange benchmarks modifications of a single file by making @@ -23,15 +23,17 @@ func BenchmarkDidChange(b *testing.B) { env.Await(env.DoneWithOpen()) // Insert the text we'll be modifying at the top of the file. - env.EditBuffer(*file, fake.Edit{Text: "// __REGTEST_PLACEHOLDER_0__\n"}) + env.EditBuffer(*file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"}) b.ResetTimer() for i := 0; i < b.N; i++ { - env.EditBuffer(*file, fake.Edit{ - Start: fake.Pos{Line: 0, Column: 0}, - End: fake.Pos{Line: 1, Column: 0}, + env.EditBuffer(*file, protocol.TextEdit{ + Range: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 1, Character: 0}, + }, // Increment the placeholder text, to ensure cache misses. - Text: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1), + NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1), }) env.Await(env.StartedChange()) } diff --git a/gopls/internal/regtest/completion/completion18_test.go b/gopls/internal/regtest/completion/completion18_test.go index 936436b9296..b9edf06d5b1 100644 --- a/gopls/internal/regtest/completion/completion18_test.go +++ b/gopls/internal/regtest/completion/completion18_test.go @@ -10,6 +10,7 @@ package completion import ( "testing" + "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" ) @@ -42,7 +43,7 @@ func (s SyncMap[XX,string]) g(v UU) {} env.Await(env.DoneWithOpen()) for _, tst := range tests { pos := env.RegexpSearch("main.go", tst.pat) - pos.Column += len(tst.pat) + pos.Character += uint32(protocol.UTF16Len([]byte(tst.pat))) completions := env.Completion("main.go", pos) result := compareCompletionLabels(tst.want, completions.Items) if result != "" { @@ -95,7 +96,7 @@ func FuzzHex(f *testing.F) { tests := []struct { file string pat string - offset int // from the beginning of pat to what the user just typed + offset uint32 // UTF16 length from the beginning of pat to what the user just typed want []string }{ {"a_test.go", "f.Ad", 3, []string{"Add"}}, @@ -109,7 +110,7 @@ func FuzzHex(f *testing.F) { env.OpenFile(test.file) env.Await(env.DoneWithOpen()) pos := env.RegexpSearch(test.file, test.pat) - pos.Column += test.offset // character user just typed? will type? + pos.Character += test.offset // character user just typed? will type? completions := env.Completion(test.file, pos) result := compareCompletionLabels(test.want, completions.Items) if result != "" { diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index a4ac9ed8491..c8674f04b7b 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -9,12 +9,12 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/hooks" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" ) @@ -193,11 +193,7 @@ package } if tc.want != nil { - start, end := env.RegexpRange(tc.filename, tc.editRegexp) - expectedRng := protocol.Range{ - Start: start.ToProtocolPosition(), - End: end.ToProtocolPosition(), - } + expectedRng := env.RegexpRange(tc.filename, tc.editRegexp) for _, item := range completions.Items { gotRng := item.TextEdit.Range if expectedRng != gotRng { @@ -229,10 +225,7 @@ package ma want := []string{"ma", "ma_test", "main", "math", "math_test"} Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("math/add.go") - completions := env.Completion("math/add.go", fake.Pos{ - Line: 0, - Column: 10, - }) + completions := env.Completion("math/add.go", env.RegexpSearch("math/add.go", "package ma()")) diff := compareCompletionLabels(want, completions.Items) if diff != "" { @@ -484,7 +477,7 @@ func doit() { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("prog.go") pos := env.RegexpSearch("prog.go", "if fooF") - pos.Column += len("if fooF") + pos.Character += uint32(protocol.UTF16Len([]byte("if fooF"))) completions := env.Completion("prog.go", pos) diff := compareCompletionLabels([]string{"fooFunc"}, completions.Items) if diff != "" { @@ -494,7 +487,7 @@ func doit() { t.Errorf("expected Tags to show deprecation %#v", diff[0]) } pos = env.RegexpSearch("prog.go", "= badP") - pos.Column += len("= badP") + pos.Character += uint32(protocol.UTF16Len([]byte("= badP"))) completions = env.Completion("prog.go", pos) diff = compareCompletionLabels([]string{"badPi"}, completions.Items) if diff != "" { @@ -538,8 +531,8 @@ func main() { env.Await(env.DoneWithChange()) got := env.BufferText("main.go") want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:})\r\n}\r\n" - if got != want { - t.Errorf("unimported completion: got %q, want %q", got, want) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unimported completion (-want +got):\n%s", diff) } }) } @@ -584,7 +577,7 @@ package foo for _, tst := range tests { env.SetBufferContent(fname, "package foo\n"+tst.line) pos := env.RegexpSearch(fname, tst.pat) - pos.Column += len(tst.pat) + pos.Character += uint32(protocol.UTF16Len([]byte(tst.pat))) completions := env.Completion(fname, pos) result := compareCompletionLabels(tst.want, completions.Items) if result != "" { @@ -666,7 +659,7 @@ func Benchmark${1:Xxx}(b *testing.B) { env.SetBufferContent("foo_test.go", tst.before) pos := env.RegexpSearch("foo_test.go", tst.name) - pos.Column = len(tst.name) + pos.Character = uint32(protocol.UTF16Len([]byte(tst.name))) completions := env.Completion("foo_test.go", pos) if len(completions.Items) == 0 { t.Fatalf("no completion items") diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 62585b9c693..df456e75839 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -164,8 +164,8 @@ func TestIssue37978(t *testing.T) { env.CreateBuffer("c/c.go", "") // Write the file contents with a missing import. - env.EditBuffer("c/c.go", fake.Edit{ - Text: `package c + env.EditBuffer("c/c.go", protocol.TextEdit{ + NewText: `package c const a = http.MethodGet `, diff --git a/gopls/internal/regtest/misc/call_hierarchy_test.go b/gopls/internal/regtest/misc/call_hierarchy_test.go index ece05b0e614..b807ee991d7 100644 --- a/gopls/internal/regtest/misc/call_hierarchy_test.go +++ b/gopls/internal/regtest/misc/call_hierarchy_test.go @@ -27,7 +27,7 @@ package pkg var params protocol.CallHierarchyPrepareParams params.TextDocument.URI = env.Sandbox.Workdir.URI("p.go") - params.Position = pos.ToProtocolPosition() + params.Position = pos // Check that this doesn't panic. env.Editor.Server.PrepareCallHierarchy(env.Ctx, ¶ms) diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index a662e4c1061..0186d50b364 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -14,8 +14,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/tests/compare" - - "golang.org/x/tools/gopls/internal/lsp/fake" ) const internalDefinition = ` @@ -166,9 +164,9 @@ func main() {} ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"fmt"`)) - if !tt.wantDef && (file != "" || pos != (fake.Pos{})) { + if !tt.wantDef && (file != "" || pos != (protocol.Position{})) { t.Fatalf("expected no definition, got one: %s:%v", file, pos) - } else if tt.wantDef && file == "" && pos == (fake.Pos{}) { + } else if tt.wantDef && file == "" && pos == (protocol.Position{}) { t.Fatalf("expected definition, got none") } links := env.DocumentLink("main.go") diff --git a/gopls/internal/regtest/misc/extract_test.go b/gopls/internal/regtest/misc/extract_test.go index 816827716cb..f159e5e9f4d 100644 --- a/gopls/internal/regtest/misc/extract_test.go +++ b/gopls/internal/regtest/misc/extract_test.go @@ -29,8 +29,8 @@ func Foo() int { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - start := env.RegexpSearch("main.go", "a := 5").ToProtocolPosition() - end := env.RegexpSearch("main.go", "return a").ToProtocolPosition() + start := env.RegexpSearch("main.go", "a := 5") + end := env.RegexpSearch("main.go", "return a") actions, err := env.Editor.CodeAction(env.Ctx, "main.go", &protocol.Range{Start: start, End: end}, nil) if err != nil { diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index 9d51eefef7c..72c6231e9ed 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -34,7 +34,7 @@ func Foo() { ` Run(t, basic, func(t *testing.T, env *Env) { env.OpenFile("main.go") - pos := env.RegexpSearch("main.go", "Info{}").ToProtocolPosition() + pos := env.RegexpSearch("main.go", "Info{}") if err := env.Editor.RefactorRewrite(env.Ctx, "main.go", &protocol.Range{ Start: pos, End: pos, diff --git a/gopls/internal/regtest/misc/highlight_test.go b/gopls/internal/regtest/misc/highlight_test.go index 8e7ce525e35..587e7d6f2ab 100644 --- a/gopls/internal/regtest/misc/highlight_test.go +++ b/gopls/internal/regtest/misc/highlight_test.go @@ -8,7 +8,6 @@ import ( "sort" "testing" - "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" ) @@ -123,7 +122,7 @@ func main() {}` }) } -func checkHighlights(env *Env, file string, pos fake.Pos, highlightCount int) { +func checkHighlights(env *Env, file string, pos protocol.Position, highlightCount int) { t := env.T t.Helper() diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index b46b3d05458..b6a385d36f5 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -37,7 +37,7 @@ func main() { pos := env.RegexpSearch("main.go", `main`) tdpp := protocol.TextDocumentPositionParams{ TextDocument: env.Editor.TextDocumentIdentifier("main.go"), - Position: pos.ToProtocolPosition(), + Position: pos, } params := &protocol.PrepareRenameParams{ TextDocumentPositionParams: tdpp, @@ -145,7 +145,7 @@ func main() { pos := env.RegexpSearch("lib/a.go", "lib") tdpp := protocol.TextDocumentPositionParams{ TextDocument: env.Editor.TextDocumentIdentifier("lib/a.go"), - Position: pos.ToProtocolPosition(), + Position: pos, } params := &protocol.PrepareRenameParams{ TextDocumentPositionParams: tdpp, diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 9556ab34ccd..5cf42521c8e 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -869,7 +869,7 @@ func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagEx var diag *protocol.Diagnostic for _, g := range got.Diagnostics { g := g - if g.Range.Start == pos.ToProtocolPosition() && w.msg == g.Message { + if g.Range.Start == pos && w.msg == g.Message { modPathDiagnostics = append(modPathDiagnostics, g) diag = &g break diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index e870096a9eb..ab74595dc9d 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -1023,7 +1023,7 @@ func main() {} env.OpenFile("go.mod") pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3") env.AfterChange( - DiagnosticAt("go.mod", pos.Line, pos.Column), + DiagnosticAt("go.mod", pos.Line, pos.Character), ReadDiagnostics("go.mod", d), ) const want = `module mod.com From 27dfeb267f683cfdbcf1f5f92689e559b9e5a084 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 22:02:00 -0500 Subject: [PATCH 626/723] gopls/internal/regtest: replace NoDiagnostics with NoMatchingDiagnostics Replace uses of NoDiagnostics with the more flexible NoMatchingDiagnostics, and rename NoMatchingDiagnostics to NoDiagnostics. Updates golang/go#39384 Change-Id: I15b19ad6c9b58c1ae88ec1b444bb589002f75a80 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461936 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/regtest/expectation.go | 27 ++----- .../regtest/codelens/codelens_test.go | 6 +- .../regtest/codelens/gcdetails_test.go | 4 +- .../regtest/diagnostics/analysis_test.go | 2 +- .../regtest/diagnostics/builtin_test.go | 2 +- .../regtest/diagnostics/diagnostics_test.go | 80 +++++++++---------- .../regtest/misc/configuration_test.go | 2 +- gopls/internal/regtest/misc/embed_test.go | 2 +- gopls/internal/regtest/misc/failures_test.go | 4 +- gopls/internal/regtest/misc/fix_test.go | 2 +- gopls/internal/regtest/misc/generate_test.go | 2 +- gopls/internal/regtest/misc/imports_test.go | 6 +- gopls/internal/regtest/misc/rename_test.go | 2 +- gopls/internal/regtest/misc/shared_test.go | 2 +- gopls/internal/regtest/misc/vuln_test.go | 6 +- .../internal/regtest/modfile/modfile_test.go | 16 ++-- .../regtest/template/template_test.go | 10 +-- gopls/internal/regtest/watch/watch_test.go | 44 +++++----- .../internal/regtest/workspace/broken_test.go | 8 +- .../workspace/directoryfilters_test.go | 6 +- .../regtest/workspace/metadata_test.go | 6 +- .../regtest/workspace/standalone_test.go | 14 ++-- .../regtest/workspace/workspace_test.go | 24 +++--- 23 files changed, 130 insertions(+), 147 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 8073561a37d..66a59ee1325 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -728,12 +728,10 @@ func Diagnostics(filters ...DiagnosticFilter) Expectation { } } -// NoMatchingDiagnostics asserts that there are no diagnostics matching the -// given filters. Notably, if no filters are supplied this assertion checks -// that there are no diagnostics at all, for any file. -// -// TODO(rfindley): replace NoDiagnostics with this, and rename. -func NoMatchingDiagnostics(filters ...DiagnosticFilter) Expectation { +// NoDiagnostics asserts that there are no diagnostics matching the given +// filters. Notably, if no filters are supplied this assertion checks that +// there are no diagnostics at all, for any file. +func NoDiagnostics(filters ...DiagnosticFilter) Expectation { check := func(s State) Verdict { diags := flattenDiagnostics(s) for _, filter := range filters { @@ -778,7 +776,7 @@ func flattenDiagnostics(state State) []flatDiagnostic { // -- Diagnostic filters -- // A DiagnosticFilter filters the set of diagnostics, for assertion with -// Diagnostics or NoMatchingDiagnostics. +// Diagnostics or NoDiagnostics. type DiagnosticFilter struct { desc string check func(name string, _ protocol.Diagnostic) bool @@ -832,21 +830,6 @@ func WithMessageContaining(substring string) DiagnosticFilter { // TODO(rfindley): eliminate all expectations below this point. -// NoDiagnostics asserts that either no diagnostics are sent for the -// workspace-relative path name, or empty diagnostics are sent. -func NoDiagnostics(name string) Expectation { - check := func(s State) Verdict { - if diags := s.diagnostics[name]; diags == nil || len(diags.Diagnostics) == 0 { - return Met - } - return Unmet - } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("empty or no diagnostics for %q", name), - } -} - // DiagnosticAtRegexp expects that there is a diagnostic entry at the start // position matching the regexp search string re in the buffer specified by // name. Note that this currently ignores the end position. diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 56e6cca1623..09206509ae4 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -215,13 +215,13 @@ require golang.org/x/hello v1.2.3 // but there may be some subtlety in timing here, where this // should always succeed, but may not actually test the correct // behavior. - NoMatchingDiagnostics(env.AtRegexp("b/go.mod", `require`)), + NoDiagnostics(env.AtRegexp("b/go.mod", `require`)), ) // Check for upgrades in b/go.mod and then clear them. env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades, nil) env.Await(env.DiagnosticAtRegexpWithMessage("b/go.mod", `require`, "can be upgraded")) env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics, nil) - env.Await(NoDiagnostics("b/go.mod")) + env.Await(NoDiagnostics(ForFile("b/go.mod"))) // Apply the diagnostics to a/go.mod. env.ApplyQuickFixes("a/go.mod", d.Diagnostics) @@ -331,6 +331,6 @@ func Foo() { // Regenerate cgo, fixing the diagnostic. env.ExecuteCodeLensCommand("cgo.go", command.RegenerateCgo, nil) - env.Await(NoDiagnostics("cgo.go")) + env.Await(NoDiagnostics(ForFile("cgo.go"))) }) } diff --git a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go index 1b2c3468bff..4b740223d52 100644 --- a/gopls/internal/regtest/codelens/gcdetails_test.go +++ b/gopls/internal/regtest/codelens/gcdetails_test.go @@ -66,7 +66,7 @@ func main() { // Editing a buffer should cause gc_details diagnostics to disappear, since // they only apply to saved buffers. env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, "\n\n")) - env.AfterChange(NoDiagnostics("main.go")) + env.AfterChange(NoDiagnostics(ForFile("main.go"))) // Saving a buffer should re-format back to the original state, and // re-enable the gc_details diagnostics. @@ -75,7 +75,7 @@ func main() { // Toggle the GC details code lens again so now it should be off. env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) - env.Await(NoDiagnostics("main.go")) + env.Await(NoDiagnostics(ForFile("main.go"))) }) } diff --git a/gopls/internal/regtest/diagnostics/analysis_test.go b/gopls/internal/regtest/diagnostics/analysis_test.go index bad4f655e11..a038d15aeea 100644 --- a/gopls/internal/regtest/diagnostics/analysis_test.go +++ b/gopls/internal/regtest/diagnostics/analysis_test.go @@ -44,6 +44,6 @@ func main() { ) env.ApplyQuickFixes("main.go", d.Diagnostics) - env.AfterChange(NoDiagnostics("main.go")) + env.AfterChange(NoDiagnostics(ForFile("main.go"))) }) } diff --git a/gopls/internal/regtest/diagnostics/builtin_test.go b/gopls/internal/regtest/diagnostics/builtin_test.go index 5baf1dcd0f2..193bbe0a9e3 100644 --- a/gopls/internal/regtest/diagnostics/builtin_test.go +++ b/gopls/internal/regtest/diagnostics/builtin_test.go @@ -30,6 +30,6 @@ const ( if !strings.HasSuffix(name, "builtin.go") { t.Fatalf("jumped to %q, want builtin.go", name) } - env.AfterChange(NoDiagnostics("builtin.go")) + env.AfterChange(NoDiagnostics(ForFile("builtin.go"))) }) } diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index df456e75839..a7b5bb0be42 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -77,7 +77,7 @@ func m() { env.DiagnosticAtRegexp("main.go", "log"), ) env.SaveBuffer("main.go") - env.AfterChange(NoDiagnostics("main.go")) + env.AfterChange(NoDiagnostics(ForFile("main.go"))) }) } @@ -119,8 +119,8 @@ func TestDiagnosticClearingOnEdit(t *testing.T) { // Fix the error by editing the const name in b.go to `b`. env.RegexpReplace("b.go", "(a) = 2", "b") env.AfterChange( - NoDiagnostics("a.go"), - NoDiagnostics("b.go"), + NoDiagnostics(ForFile("a.go")), + NoDiagnostics(ForFile("b.go")), ) }) } @@ -132,8 +132,8 @@ func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) { env.RemoveWorkspaceFile("b.go") env.Await( - NoDiagnostics("a.go"), - NoDiagnostics("b.go"), + NoDiagnostics(ForFile("a.go")), + NoDiagnostics(ForFile("b.go")), ) }) } @@ -152,7 +152,7 @@ const a = 3`) env.AfterChange( env.DiagnosticAtRegexp("a.go", "a = 1"), env.DiagnosticAtRegexp("b.go", "a = 2"), - NoDiagnostics("c.go"), + NoDiagnostics(ForFile("c.go")), ) }) } @@ -177,7 +177,7 @@ const a = http.MethodGet // Expect the diagnostics to clear. env.SaveBuffer("c/c.go") env.AfterChange( - NoDiagnostics("c/c.go"), + NoDiagnostics(ForFile("c/c.go")), ) }) } @@ -213,7 +213,7 @@ func TestDeleteTestVariant(t *testing.T) { Run(t, test38878, func(t *testing.T, env *Env) { env.Await(env.DiagnosticAtRegexp("a_test.go", `f\((3)\)`)) env.RemoveWorkspaceFile("a_test.go") - env.AfterChange(NoDiagnostics("a_test.go")) + env.AfterChange(NoDiagnostics(ForFile("a_test.go"))) // Make sure the test variant has been removed from the workspace by // triggering a metadata load. @@ -267,7 +267,7 @@ func Hello() { env.SaveBuffer("go.mod") var d protocol.PublishDiagnosticsParams env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), env.DiagnosticAtRegexp("bob/bob.go", "x"), ReadDiagnostics("bob/bob.go", &d), ) @@ -283,7 +283,7 @@ func Hello() { ) env.RunGoCommand("mod", "init", "mod.com") env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), env.DiagnosticAtRegexp("bob/bob.go", "x"), ) }) @@ -300,7 +300,7 @@ func Hello() { t.Fatal(err) } env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), env.DiagnosticAtRegexp("bob/bob.go", "x"), ) }) @@ -350,7 +350,7 @@ func TestHello(t *testing.T) { env.RegexpReplace("lib.go", "_ = x", "var y int") env.AfterChange( env.DiagnosticAtRegexp("lib.go", "y int"), - NoDiagnostics("lib_test.go"), + NoDiagnostics(ForFile("lib_test.go")), ) }) } @@ -370,7 +370,7 @@ func main() {} env.OpenFile("a.go") env.RegexpReplace("a.go", "foo", "foox") env.AfterChange( - NoDiagnostics("a.go"), + NoDiagnostics(ForFile("a.go")), ) }) } @@ -464,7 +464,7 @@ func _() { env.OpenFile("main.go") env.AfterChange(env.DiagnosticAtRegexp("main.go", "fmt")) env.SaveBuffer("main.go") - env.AfterChange(NoDiagnostics("main.go")) + env.AfterChange(NoDiagnostics(ForFile("main.go"))) }) } @@ -489,7 +489,7 @@ var X = 0 ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OrganizeImports("main.go") - env.AfterChange(NoDiagnostics("main.go")) + env.AfterChange(NoDiagnostics(ForFile("main.go"))) }) } @@ -650,7 +650,7 @@ func main() { env.ApplyQuickFixes("main.go", d.Diagnostics) env.SaveBuffer("go.mod") env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) // Comment out the line that depends on conf and expect a // diagnostic and a fix to remove the import. @@ -661,14 +661,14 @@ func main() { env.SaveBuffer("main.go") // Expect a diagnostic and fix to remove the dependency in the go.mod. env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), env.DiagnosticAtRegexpWithMessage("go.mod", "require github.com/ardanlabs/conf", "not used in this module"), ReadDiagnostics("go.mod", &d), ) env.ApplyQuickFixes("go.mod", d.Diagnostics) env.SaveBuffer("go.mod") env.AfterChange( - NoDiagnostics("go.mod"), + NoDiagnostics(ForFile("go.mod")), ) // Uncomment the lines and expect a new diagnostic for the import. env.RegexpReplace("main.go", "//_ = conf.ErrHelpWanted", "_ = conf.ErrHelpWanted") @@ -707,7 +707,7 @@ func main() { ) env.ApplyQuickFixes("main.go", d.Diagnostics) env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } @@ -733,11 +733,11 @@ func _() { env.CreateBuffer("a/a2.go", ``) env.SaveBufferWithoutActions("a/a2.go") env.AfterChange( - NoDiagnostics("a/a1.go"), + NoDiagnostics(ForFile("a/a1.go")), ) env.EditBuffer("a/a2.go", fake.NewEdit(0, 0, 0, 0, `package a`)) env.AfterChange( - NoDiagnostics("a/a1.go"), + NoDiagnostics(ForFile("a/a1.go")), ) }) } @@ -805,7 +805,7 @@ func TestHello(t *testing.T) { ) env.SaveBuffer("hello/hello_x_test.go") env.AfterChange( - NoDiagnostics("hello/hello_x_test.go"), + NoDiagnostics(ForFile("hello/hello_x_test.go")), ) }) } @@ -854,8 +854,8 @@ package foo_ env.OpenFile("foo/bar_test.go") env.RegexpReplace("foo/bar_test.go", "package foo_", "package foo_test") env.AfterChange( - NoDiagnostics("foo/bar_test.go"), - NoDiagnostics("foo/foo.go"), + NoDiagnostics(ForFile("foo/bar_test.go")), + NoDiagnostics(ForFile("foo/foo.go")), ) }) } @@ -874,7 +874,7 @@ var _ = foo.Bar Run(t, ws, func(t *testing.T, env *Env) { env.OpenFile("_foo/x.go") env.AfterChange( - NoDiagnostics("_foo/x.go"), + NoDiagnostics(ForFile("_foo/x.go")), ) }) } @@ -997,9 +997,9 @@ func TestDoIt(t *testing.T) { ) env.RegexpReplace("p/p.go", "s string", "i int") env.AfterChange( - NoDiagnostics("main.go"), - NoDiagnostics("p/p_test.go"), - NoDiagnostics("p/x_test.go"), + NoDiagnostics(ForFile("main.go")), + NoDiagnostics(ForFile("p/p_test.go")), + NoDiagnostics(ForFile("p/x_test.go")), ) }) }) @@ -1054,7 +1054,7 @@ func main() { }`) env.OpenFile("foo/foo_test.go") env.RegexpReplace("foo/foo_test.go", `package main`, `package foo`) - env.AfterChange(NoDiagnostics("foo/foo.go")) + env.AfterChange(NoDiagnostics(ForFile("foo/foo.go"))) }) } @@ -1173,7 +1173,7 @@ func main() { ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } @@ -1212,7 +1212,7 @@ func main() { t.Errorf("wanted Unnecessary tag on diagnostic, got %v", tags) } env.ApplyQuickFixes("main.go", d.Diagnostics) - env.AfterChange(NoDiagnostics("main.go")) + env.AfterChange(NoDiagnostics(ForFile("main.go"))) }) } @@ -1369,7 +1369,7 @@ func main() { Run(t, mod, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - NoMatchingDiagnostics(WithMessageContaining("illegal character U+0023 '#'")), + NoDiagnostics(WithMessageContaining("illegal character U+0023 '#'")), ) }) } @@ -1431,7 +1431,7 @@ package foo_ env.Await(env.DoneWithChange()) env.RegexpReplace("foo/foo_test.go", "_t", "_test") env.AfterChange( - NoDiagnostics("foo/foo_test.go"), + NoDiagnostics(ForFile("foo/foo_test.go")), NoOutstandingWork(), ) }) @@ -1498,7 +1498,7 @@ func main() { env.RemoveWorkspaceFile("bob") env.AfterChange( env.DiagnosticAtRegexp("cmd/main.go", `"mod.com/bob"`), - NoDiagnostics("bob/bob.go"), + NoDiagnostics(ForFile("bob/bob.go")), NoFileWatchMatching("bob"), ) }) @@ -1585,8 +1585,8 @@ const B = a.B env.RegexpReplace("b/b.go", `const B = a\.B`, "") env.SaveBuffer("b/b.go") env.Await( - NoDiagnostics("a/a.go"), - NoDiagnostics("b/b.go"), + NoDiagnostics(ForFile("a/a.go")), + NoDiagnostics(ForFile("b/b.go")), ) }) } @@ -1718,7 +1718,7 @@ var Bar = Foo env.OpenFile("foo.go") env.AfterChange(env.DiagnosticAtRegexp("bar.go", `Foo`)) env.RegexpReplace("foo.go", `\+build`, "") - env.AfterChange(NoDiagnostics("bar.go")) + env.AfterChange(NoDiagnostics(ForFile("bar.go"))) }) } @@ -1819,7 +1819,7 @@ const C = 0b10 ) env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.13\n") env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } @@ -1874,7 +1874,7 @@ func F[T any](_ T) { env.ApplyQuickFixes("main.go", d.Diagnostics) env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } @@ -1912,7 +1912,7 @@ func F[T any](_ T) { // Once the edit is applied, the problematic diagnostics should be // resolved. env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index 4164d6c0e1f..6aea24a06d4 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -36,7 +36,7 @@ var FooErr = errors.New("foo") Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) cfg := env.Editor.Config() cfg.Settings = map[string]interface{}{ diff --git a/gopls/internal/regtest/misc/embed_test.go b/gopls/internal/regtest/misc/embed_test.go index 0d6d2ddd1d1..15943b8c05f 100644 --- a/gopls/internal/regtest/misc/embed_test.go +++ b/gopls/internal/regtest/misc/embed_test.go @@ -30,6 +30,6 @@ var foo string env.OpenFile("x.go") env.AfterChange(env.DiagnosticAtRegexpWithMessage("x.go", `NONEXISTENT`, "no matching files found")) env.RegexpReplace("x.go", `NONEXISTENT`, "x.go") - env.AfterChange(NoDiagnostics("x.go")) + env.AfterChange(NoDiagnostics(ForFile("x.go"))) }) } diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index b3ff37a2d67..f088a0e86bc 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -77,8 +77,8 @@ const a = 2 // Fix the error by editing the const name in b.go to `b`. env.RegexpReplace("b.go", "(a) = 2", "b") env.Await( - NoDiagnostics("a.go"), - NoDiagnostics("b.go"), + NoDiagnostics(ForFile("a.go")), + NoDiagnostics(ForFile("b.go")), ) }) } diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index 72c6231e9ed..f00a734a23f 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -102,6 +102,6 @@ func Foo() error { t.Fatalf("expected fixall code action, got none") } env.ApplyQuickFixes("main.go", d.Diagnostics) - env.AfterChange(NoDiagnostics("main.go")) + env.AfterChange(NoDiagnostics(ForFile("main.go"))) }) } diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go index f6bbcd5afe4..9fd448cd849 100644 --- a/gopls/internal/regtest/misc/generate_test.go +++ b/gopls/internal/regtest/misc/generate_test.go @@ -65,7 +65,7 @@ func main() { env.RunGenerate("./lib1") env.RunGenerate("./lib2") env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index 6558491df13..b9804d0c436 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -158,7 +158,7 @@ var _, _ = x.X, y.Y env.OpenFile("main.go") env.AfterChange(env.DiagnosticAtRegexp("main.go", `y.Y`)) env.SaveBuffer("main.go") - env.AfterChange(NoDiagnostics("main.go")) + env.AfterChange(NoDiagnostics(ForFile("main.go"))) path, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `y.(Y)`)) if !strings.HasPrefix(path, filepath.ToSlash(modcache)) { t.Errorf("found module dependency outside of GOMODCACHE: got %v, wanted subdir of %v", path, filepath.ToSlash(modcache)) @@ -205,7 +205,7 @@ func TestA(t *testing.T) { ) env.ApplyQuickFixes("a/a.go", d.Diagnostics) env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) }) } @@ -252,6 +252,6 @@ func Test() { // Saving caller.go should trigger goimports, which should find a.Test in // the mod.com module, thanks to the go.work file. env.SaveBuffer("caller/caller.go") - env.AfterChange(NoDiagnostics("caller/caller.go")) + env.AfterChange(NoDiagnostics(ForFile("caller/caller.go"))) }) } diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index b6a385d36f5..81b3de8125a 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -412,7 +412,7 @@ package b // Moving x.go should make the diagnostic go away. env.RenameFile("a/x.go", "b/x.go") env.AfterChange( - NoDiagnostics("a/a.go"), // no more duplicate declarations + NoDiagnostics(ForFile("a/a.go")), // no more duplicate declarations env.DiagnosticAtRegexp("b/b.go", "package"), // as package names mismatch ) diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index 23f7a42872e..84eb99f73de 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -65,7 +65,7 @@ func main() { env1.RegexpReplace("main.go", "Printl", "Println") env1.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 5cf42521c8e..b34ddfc0a12 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -228,7 +228,7 @@ func main() { env.OnceMet( CompletedProgress(result.Token, nil), ShownMessage("Found GOSTDLIB"), - NoDiagnostics("go.mod"), + NoDiagnostics(ForFile("go.mod")), ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ "go.mod": {IDs: []string{"GOSTDLIB"}, Mode: govulncheck.ModeGovulncheck}}) @@ -274,7 +274,7 @@ func main() { ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.AfterChange( - NoDiagnostics("go.mod"), + NoDiagnostics(ForFile("go.mod")), // we don't publish diagnostics for standard library vulnerability yet. ) testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ @@ -854,7 +854,7 @@ func TestGovulncheckInfo(t *testing.T) { } env.ApplyCodeAction(reset) - env.Await(NoDiagnostics("go.mod")) + env.Await(NoDiagnostics(ForFile("go.mod"))) }) } diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index ab74595dc9d..c5aed8a6ef6 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -485,7 +485,7 @@ func main() { ) env.RunGoCommandInDir("a", "mod", "tidy") env.AfterChange( - NoDiagnostics("a/go.mod"), + NoDiagnostics(ForFile("a/go.mod")), ) }) } @@ -558,7 +558,7 @@ require ( ) ` env.SaveBuffer("a/go.mod") - env.AfterChange(NoDiagnostics("a/main.go")) + env.AfterChange(NoDiagnostics(ForFile("a/main.go"))) if got := env.BufferText("a/go.mod"); got != want { t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got)) } @@ -614,7 +614,7 @@ func main() { env.ApplyCodeAction(qfs[0]) // Arbitrarily pick a single fix to apply. Applying all of them seems to cause trouble in this particular test. env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. env.AfterChange( - NoDiagnostics("a/go.mod"), + NoDiagnostics(ForFile("a/go.mod")), env.DiagnosticAtRegexp("a/main.go", "x = "), ) }) @@ -744,7 +744,7 @@ func main() { env.RunGoCommand("get", "example.com/blah@v1.2.3") env.RunGoCommand("mod", "tidy") env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } @@ -882,7 +882,7 @@ func main() { env.ApplyQuickFixes("go.mod", d.Diagnostics) env.SaveBuffer("go.mod") // Save to trigger diagnostics. env.AfterChange( - NoDiagnostics("go.mod"), + NoDiagnostics(ForFile("go.mod")), ) }) } @@ -1149,8 +1149,8 @@ func main() { ) env.ApplyQuickFixes("main.go", d.Diagnostics) env.Await( - NoDiagnostics("main.go"), - NoDiagnostics("go.mod"), + NoDiagnostics(ForFile("main.go")), + NoDiagnostics(ForFile("go.mod")), ) }) } @@ -1170,7 +1170,7 @@ package main env.DiagnosticAtRegexpWithMessage("go.mod", `go foo`, "invalid go version"), ) env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.12\n") - env.AfterChange(NoDiagnostics("go.mod")) + env.AfterChange(NoDiagnostics(ForFile("go.mod"))) }) } diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index 140ba3caf3b..f3453d72cf8 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -96,7 +96,7 @@ Hello {{}} <-- missing body } env.WriteWorkspaceFile("hello.tmpl", "{{range .Planets}}\nHello {{.}}\n{{end}}") - env.AfterChange(NoDiagnostics("hello.tmpl")) + env.AfterChange(NoDiagnostics(ForFile("hello.tmpl"))) }) } @@ -121,7 +121,7 @@ B {{}} <-- missing body env.OnceMet( InitialWorkspaceLoad, env.DiagnosticAtRegexp("a/a.tmpl", "()A"), - NoDiagnostics("b/b.tmpl"), + NoDiagnostics(ForFile("b/b.tmpl")), ) }) } @@ -137,12 +137,12 @@ go 1.12 Run(t, files, func(t *testing.T, env *Env) { env.CreateBuffer("hello.tmpl", "") env.AfterChange( - NoDiagnostics("hello.tmpl"), // Don't get spurious errors for empty templates. + NoDiagnostics(ForFile("hello.tmpl")), // Don't get spurious errors for empty templates. ) env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}") env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) env.RegexpReplace("hello.tmpl", "{{}}", "{{.}}") - env.Await(NoDiagnostics("hello.tmpl")) + env.Await(NoDiagnostics(ForFile("hello.tmpl"))) }) } @@ -167,7 +167,7 @@ Hello {{}} <-- missing body // should make its diagnostics disappear. env.CloseBuffer("hello.tmpl") env.AfterChange( - NoDiagnostics("hello.tmpl"), + NoDiagnostics(ForFile("hello.tmpl")), ) }) } diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index d2a203e8cfa..6a91a26213e 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -43,7 +43,7 @@ func _() { ) env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) }) }) @@ -137,8 +137,8 @@ func _() { }`, }) env.AfterChange( - NoDiagnostics("a/a.go"), - NoDiagnostics("b/b.go"), + NoDiagnostics(ForFile("a/a.go")), + NoDiagnostics(ForFile("b/b.go")), ) }) } @@ -204,7 +204,7 @@ func _() { ) env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) }) } @@ -226,7 +226,7 @@ func _() {} env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) env.WriteWorkspaceFile("a/a.go", `package a; import "mod.com/c"; func _() { c.C() }`) env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) }) } @@ -252,7 +252,7 @@ func _() { ) env.WriteWorkspaceFile("a/a2.go", `package a; func hello() {};`) env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) }) } @@ -328,7 +328,7 @@ func _() { ) env.WriteWorkspaceFile("a/a.go", implementation) env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) }) }) @@ -337,11 +337,11 @@ func _() { Run(t, pkg, func(t *testing.T, env *Env) { env.WriteWorkspaceFile("a/a.go", implementation) env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) env.WriteWorkspaceFile("b/b.go", newMethod) env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) }) }) @@ -353,8 +353,8 @@ func _() { "b/b.go": newMethod, }) env.AfterChange( - NoDiagnostics("a/a.go"), - NoDiagnostics("b/b.go"), + NoDiagnostics(ForFile("a/a.go")), + NoDiagnostics(ForFile("b/b.go")), ) }) }) @@ -400,7 +400,7 @@ package a // a_unneeded.go, from the initial workspace load, which we // check for earlier. If there are more, there's a bug. LogMatching(protocol.Info, "a_unneeded.go", 1, false), - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) }) }) @@ -428,7 +428,7 @@ package a // a_unneeded.go, from the initial workspace load, which we // check for earlier. If there are more, there's a bug. LogMatching(protocol.Info, "a_unneeded.go", 1, false), - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), ) }) }) @@ -470,7 +470,7 @@ func _() {} env.RemoveWorkspaceFile("a/a1.go") env.WriteWorkspaceFile("a/a2.go", "package a; func _() {};") env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } @@ -547,7 +547,7 @@ func main() { }) env.AfterChange( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } @@ -575,7 +575,7 @@ func main() { ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, true); err != nil { t.Fatal(err) @@ -589,7 +589,7 @@ func main() { env.RegexpReplace("main.go", `"foo/blah"`, `"mod.com/foo/blah"`) env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), ) }) } @@ -625,7 +625,7 @@ func main() { ) env.RegexpReplace("foo/main.go", `"mod.com/blah"`, `"foo/blah"`) env.AfterChange( - NoDiagnostics("foo/main.go"), + NoDiagnostics(ForFile("foo/main.go")), ) }) } @@ -669,8 +669,8 @@ func TestAll(t *testing.T) { `, }) env.AfterChange( - NoDiagnostics("a/a.go"), - NoDiagnostics("a/a_test.go"), + NoDiagnostics(ForFile("a/a.go")), + NoDiagnostics(ForFile("a/a_test.go")), ) // Now, add a new file to the test variant and use its symbol in the // original test file. Expect no diagnostics. @@ -695,8 +695,8 @@ func TestSomething(t *testing.T) {} `, }) env.AfterChange( - NoDiagnostics("a/a_test.go"), - NoDiagnostics("a/a2_test.go"), + NoDiagnostics(ForFile("a/a_test.go")), + NoDiagnostics(ForFile("a/a2_test.go")), ) }) } diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index 9a65030c715..0c718b339ea 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -166,7 +166,7 @@ const F = named.D - 3 env.OpenFile("go.mod") env.RegexpReplace("go.mod", "mod.testx", "mod.test") env.SaveBuffer("go.mod") // saving triggers a reload - env.AfterChange(NoMatchingDiagnostics()) + env.AfterChange(NoDiagnostics()) }) } @@ -219,8 +219,8 @@ package b // workspace folder, therefore we can't invoke AfterChange here. env.ChangeWorkspaceFolders("a", "b") env.Await( - NoDiagnostics("a/a.go"), - NoDiagnostics("b/go.mod"), + NoDiagnostics(ForFile("a/a.go")), + NoDiagnostics(ForFile("b/go.mod")), NoOutstandingWork(), ) @@ -256,7 +256,7 @@ package b ).Run(t, modules, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.AfterChange( - NoDiagnostics("a/a.go"), + NoDiagnostics(ForFile("a/a.go")), NoOutstandingWork(), ) }) diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go index bf22b8012bd..f7c7153ec0e 100644 --- a/gopls/internal/regtest/workspace/directoryfilters_test.go +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -55,7 +55,7 @@ const _ = Nonexistant ).Run(t, files, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - NoDiagnostics("exclude/x.go"), + NoDiagnostics(ForFile("exclude/x.go")), ) }) } @@ -85,8 +85,8 @@ const X = 1 ).Run(t, files, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - NoDiagnostics("exclude/exclude.go"), // filtered out - NoDiagnostics("include/include.go"), // successfully builds + NoDiagnostics(ForFile("exclude/exclude.go")), // filtered out + NoDiagnostics(ForFile("include/include.go")), // successfully builds ) }) } diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index de7dec9f6f2..4990a6659aa 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -34,7 +34,7 @@ const C = 42 env.OpenFile("p.go") env.RegexpReplace("p.go", "\"fmt\"", "\"fmt\"\n)") env.AfterChange( - NoDiagnostics("p.go"), + NoDiagnostics(ForFile("p.go")), ) }) } @@ -88,8 +88,8 @@ func main() {} // information when fresh metadata arrives. // env.RegexpReplace("foo.go", "package main", "package main // test") env.AfterChange( - NoDiagnostics("foo.go"), - NoDiagnostics("bar.go"), + NoDiagnostics(ForFile("foo.go")), + NoDiagnostics(ForFile("bar.go")), ) // If instead of 'ignore' (which gopls treats as a standalone package) we diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go index 53b269e59e3..7056f5464bb 100644 --- a/gopls/internal/regtest/workspace/standalone_test.go +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -74,14 +74,14 @@ func main() { } env.OpenFile("lib/lib.go") - env.AfterChange(NoMatchingDiagnostics()) + env.AfterChange(NoDiagnostics()) // Replacing C with D should not cause any workspace diagnostics, since we // haven't yet opened the standalone file. env.RegexpReplace("lib/lib.go", "C", "D") - env.AfterChange(NoMatchingDiagnostics()) + env.AfterChange(NoDiagnostics()) env.RegexpReplace("lib/lib.go", "D", "C") - env.AfterChange(NoMatchingDiagnostics()) + env.AfterChange(NoDiagnostics()) refs := env.References("lib/lib.go", env.RegexpSearch("lib/lib.go", "C")) checkLocations("References", refs, "lib/lib.go") @@ -91,7 +91,7 @@ func main() { // Opening the standalone file should not result in any diagnostics. env.OpenFile("lib/ignore.go") - env.AfterChange(NoMatchingDiagnostics()) + env.AfterChange(NoDiagnostics()) // Having opened the standalone file, we should find its symbols in the // workspace. @@ -135,7 +135,7 @@ func main() { // Undoing the replacement should fix diagnostics env.RegexpReplace("lib/lib.go", "D", "C") - env.AfterChange(NoMatchingDiagnostics()) + env.AfterChange(NoDiagnostics()) // Now that our workspace has no errors, we should be able to find // references and rename. @@ -187,7 +187,7 @@ func main() {} env.AfterChange( env.DiagnosticAtRegexp("ignore.go", "package (main)"), - NoDiagnostics("standalone.go"), + NoDiagnostics(ForFile("standalone.go")), ) cfg := env.Editor.Config() @@ -201,7 +201,7 @@ func main() {} env.RegexpReplace("ignore.go", "arbitrary", "meaningless") env.AfterChange( - NoDiagnostics("ignore.go"), + NoDiagnostics(ForFile("ignore.go")), env.DiagnosticAtRegexp("standalone.go", "package (main)"), ) }) diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 720ec97ee0e..acdeef6b179 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -156,7 +156,7 @@ func TestClearAnalysisDiagnostics(t *testing.T) { ) env.CloseBuffer("pkg/main.go") env.AfterChange( - NoDiagnostics("pkg/main2.go"), + NoDiagnostics(ForFile("pkg/main2.go")), ) }) } @@ -269,7 +269,7 @@ func Hello() int { env.AfterChange( env.DiagnosticAtRegexp("moda/a/a.go", "x"), env.DiagnosticAtRegexp("modb/b/b.go", "x"), - NoMatchingDiagnostics(env.AtRegexp("moda/a/a.go", `"b.com/b"`)), + NoDiagnostics(env.AtRegexp("moda/a/a.go", `"b.com/b"`)), ) }) } @@ -643,7 +643,7 @@ use ( // This fails if guarded with a OnceMet(DoneWithSave(), ...), because it is // debounced (and therefore not synchronous with the change). - env.Await(NoDiagnostics("modb/go.mod")) + env.Await(NoDiagnostics(ForFile("modb/go.mod"))) // Test Formatting. env.SetBufferContent("go.work", `go 1.18 @@ -693,7 +693,7 @@ module example.com/bar // the diagnostic still shows up. env.SetBufferContent("go.work", "go 1.18 \n\n use ./bar\n") env.AfterChange( - NoMatchingDiagnostics(env.AtRegexp("go.work", "use")), + NoDiagnostics(env.AtRegexp("go.work", "use")), ) env.SetBufferContent("go.work", "go 1.18 \n\n use ./foo\n") env.AfterChange( @@ -978,7 +978,7 @@ func main() { env.ApplyQuickFixes("b/go.mod", []protocol.Diagnostic{d}) } env.AfterChange( - NoDiagnostics("b/go.mod"), + NoDiagnostics(ForFile("b/go.mod")), ) }) } @@ -1051,7 +1051,7 @@ func main() {} // Since b/main.go is not in the workspace, it should have a warning on its // package declaration. env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), DiagnosticAt("b/main.go", 0, 0), ) env.WriteWorkspaceFile("go.work", `go 1.16 @@ -1061,7 +1061,7 @@ use ( b ) `) - env.AfterChange(NoMatchingDiagnostics()) + env.AfterChange(NoDiagnostics()) // Removing the go.work file should put us back where we started. env.RemoveWorkspaceFile("go.work") @@ -1077,7 +1077,7 @@ use ( env.OpenFile("b/main.go") env.AfterChange( - NoDiagnostics("main.go"), + NoDiagnostics(ForFile("main.go")), DiagnosticAt("b/main.go", 0, 0), ) }) @@ -1119,8 +1119,8 @@ func (Server) Foo() {} ) env.RegexpReplace("other_test.go", "main", "main_test") env.AfterChange( - NoDiagnostics("other_test.go"), - NoDiagnostics("main_test.go"), + NoDiagnostics(ForFile("other_test.go")), + NoDiagnostics(ForFile("main_test.go")), ) // This will cause a test failure if other_test.go is not in any package. @@ -1163,7 +1163,7 @@ import ( Run(t, ws, func(t *testing.T, env *Env) { env.OpenFile("b/main.go") env.AfterChange( - NoDiagnostics("a/main.go"), + NoDiagnostics(ForFile("a/main.go")), ) env.OpenFile("a/main.go") env.AfterChange( @@ -1178,7 +1178,7 @@ import ( // Gopls should be smart enough to avoid diagnosing a. env.RegexpReplace("b/main.go", "package b", "package b // a package") env.AfterChange( - NoDiagnostics("a/main.go"), + NoDiagnostics(ForFile("a/main.go")), ) }) } From 5d65394a9d6bc3b14c051a03d5b1a16e7d617aaa Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 22:11:31 -0500 Subject: [PATCH 627/723] gopls/internal/regtest: eliminate DiagnosticAt Replace the DiagnosticAt expectation with a filter. Updates golang/go#39384 Change-Id: I29e61a531beb2097a88266943917b0ae43630e3f Reviewed-on: https://go-review.googlesource.com/c/tools/+/461937 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro Reviewed-by: Alan Donovan --- gopls/internal/lsp/regtest/expectation.go | 18 ++++++++++++------ .../regtest/codelens/gcdetails_test.go | 4 ++-- .../regtest/diagnostics/diagnostics_test.go | 12 ++++++------ .../regtest/misc/configuration_test.go | 2 +- gopls/internal/regtest/modfile/modfile_test.go | 2 +- gopls/internal/regtest/watch/watch_test.go | 2 +- .../regtest/workspace/workspace_test.go | 6 +++--- 7 files changed, 26 insertions(+), 20 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 66a59ee1325..cd4ac61ce64 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -817,6 +817,18 @@ func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { } } +// AtPosition filters to diagnostics at location name:line:character, for a +// sandbox-relative path name. +func AtPosition(name string, line, character uint32) DiagnosticFilter { + pos := protocol.Position{Line: line, Character: character} + return DiagnosticFilter{ + desc: fmt.Sprintf("at %s:%d:%d", name, line, character), + check: func(diagName string, d protocol.Diagnostic) bool { + return diagName == name && d.Range.Start == pos + }, + } +} + // WithMessageContaining filters to diagnostics whose message contains the // given substring. func WithMessageContaining(substring string) DiagnosticFilter { @@ -846,9 +858,3 @@ func (e *Env) DiagnosticAtRegexpWithMessage(name, re, msg string) DiagnosticExpe pos := e.RegexpSearch(name, re) return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true, message: msg} } - -// DiagnosticAt asserts that there is a diagnostic entry at the position -// specified by line and col, for the workdir-relative path name. -func DiagnosticAt(name string, line, col uint32) DiagnosticExpectation { - return DiagnosticExpectation{path: name, pos: &protocol.Position{Line: line, Character: col}, present: true} -} diff --git a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go index 4b740223d52..e0642d65224 100644 --- a/gopls/internal/regtest/codelens/gcdetails_test.go +++ b/gopls/internal/regtest/codelens/gcdetails_test.go @@ -46,7 +46,7 @@ func main() { env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) d := &protocol.PublishDiagnosticsParams{} env.OnceMet( - DiagnosticAt("main.go", 5, 13), + Diagnostics(AtPosition("main.go", 5, 13)), ReadDiagnostics("main.go", d), ) // Confirm that the diagnostics come from the gc details code lens. @@ -71,7 +71,7 @@ func main() { // Saving a buffer should re-format back to the original state, and // re-enable the gc_details diagnostics. env.SaveBuffer("main.go") - env.AfterChange(DiagnosticAt("main.go", 5, 13)) + env.AfterChange(Diagnostics(AtPosition("main.go", 5, 13))) // Toggle the GC details code lens again so now it should be off. env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index a7b5bb0be42..6608d24be03 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -228,9 +228,9 @@ func TestDeleteTestVariant(t *testing.T) { func TestDeleteTestVariant_DiskOnly(t *testing.T) { Run(t, test38878, func(t *testing.T, env *Env) { env.OpenFile("a_test.go") - env.Await(DiagnosticAt("a_test.go", 5, 3)) + env.Await(Diagnostics(AtPosition("a_test.go", 5, 3))) env.Sandbox.Workdir.RemoveFile(context.Background(), "a_test.go") - env.AfterChange(DiagnosticAt("a_test.go", 5, 3)) + env.AfterChange(Diagnostics(AtPosition("a_test.go", 5, 3))) }) } @@ -343,8 +343,8 @@ func TestHello(t *testing.T) { Run(t, testPackage, func(t *testing.T, env *Env) { env.OpenFile("lib_test.go") env.Await( - DiagnosticAt("lib_test.go", 10, 2), - DiagnosticAt("lib_test.go", 11, 2), + Diagnostics(AtPosition("lib_test.go", 10, 2)), + Diagnostics(AtPosition("lib_test.go", 11, 2)), ) env.OpenFile("lib.go") env.RegexpReplace("lib.go", "_ = x", "var y int") @@ -515,7 +515,7 @@ func _() { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - DiagnosticAt("main.go", 5, 8), + Diagnostics(AtPosition("main.go", 5, 8)), ReadDiagnostics("main.go", &d), ) if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { @@ -1286,7 +1286,7 @@ func _() { ) env.OpenFile("a/a_exclude.go") env.Await( - DiagnosticAt("a/a_exclude.go", 2, 8), + Diagnostics(env.AtRegexp("a/a_exclude.go", "package (a)")), ) }) } diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index 6aea24a06d4..5af1311f838 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -46,7 +46,7 @@ var FooErr = errors.New("foo") // change. env.ChangeConfiguration(cfg) env.Await( - DiagnosticAt("a/a.go", 5, 4), + Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")), ) }) } diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index c5aed8a6ef6..b94894c79bf 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -1023,7 +1023,7 @@ func main() {} env.OpenFile("go.mod") pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3") env.AfterChange( - DiagnosticAt("go.mod", pos.Line, pos.Character), + Diagnostics(AtPosition("go.mod", pos.Line, pos.Character)), ReadDiagnostics("go.mod", d), ) const want = `module mod.com diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 6a91a26213e..b445f88b332 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -324,7 +324,7 @@ func _() { Run(t, pkg, func(t *testing.T, env *Env) { env.WriteWorkspaceFile("b/b.go", newMethod) env.AfterChange( - DiagnosticAt("a/a.go", 12, 12), + Diagnostics(AtPosition("a/a.go", 12, 12)), ) env.WriteWorkspaceFile("a/a.go", implementation) env.AfterChange( diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index acdeef6b179..0d000b877fd 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -503,7 +503,7 @@ func Hello() int { ).Run(t, multiModule, func(t *testing.T, env *Env) { env.OpenFile("modb/go.mod") env.AfterChange( - DiagnosticAt("modb/go.mod", 0, 0), + Diagnostics(AtPosition("modb/go.mod", 0, 0)), ) env.RegexpReplace("modb/go.mod", "modul", "module") env.SaveBufferWithoutActions("modb/go.mod") @@ -1052,7 +1052,7 @@ func main() {} // package declaration. env.AfterChange( NoDiagnostics(ForFile("main.go")), - DiagnosticAt("b/main.go", 0, 0), + Diagnostics(AtPosition("b/main.go", 0, 0)), ) env.WriteWorkspaceFile("go.work", `go 1.16 @@ -1078,7 +1078,7 @@ use ( env.AfterChange( NoDiagnostics(ForFile("main.go")), - DiagnosticAt("b/main.go", 0, 0), + Diagnostics(AtPosition("b/main.go", 0, 0)), ) }) } From bd48b9a5d7872f3d6b457d49038c4657c7443740 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 22:21:43 -0500 Subject: [PATCH 628/723] gopls/internal/regtest: eliminate DiagnosticsAtRegexpWithMessage Replace with assertions on filtered diagnostics. Updates golang/go#39384 Change-Id: If88e29a8241008dd778fbfabe37d840b48f17691 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461938 gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Robert Findley --- gopls/internal/lsp/regtest/expectation.go | 14 ++---- .../regtest/codelens/codelens_test.go | 8 ++-- .../regtest/diagnostics/diagnostics_test.go | 47 ++++++++++--------- gopls/internal/regtest/misc/embed_test.go | 7 ++- gopls/internal/regtest/misc/failures_test.go | 4 +- gopls/internal/regtest/misc/fix_test.go | 2 +- gopls/internal/regtest/misc/vendor_test.go | 4 +- .../internal/regtest/modfile/modfile_test.go | 19 ++++---- .../regtest/workspace/fromenv_test.go | 2 +- .../regtest/workspace/metadata_test.go | 2 +- .../regtest/workspace/workspace_test.go | 12 ++--- 11 files changed, 62 insertions(+), 59 deletions(-) diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index cd4ac61ce64..1c359252f0e 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -829,9 +829,9 @@ func AtPosition(name string, line, character uint32) DiagnosticFilter { } } -// WithMessageContaining filters to diagnostics whose message contains the -// given substring. -func WithMessageContaining(substring string) DiagnosticFilter { +// WithMessage filters to diagnostics whose message contains the given +// substring. +func WithMessage(substring string) DiagnosticFilter { return DiagnosticFilter{ desc: fmt.Sprintf("with message containing %q", substring), check: func(_ string, d protocol.Diagnostic) bool { @@ -850,11 +850,3 @@ func (e *Env) DiagnosticAtRegexp(name, re string) DiagnosticExpectation { pos := e.RegexpSearch(name, re) return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true} } - -// DiagnosticAtRegexpWithMessage is like DiagnosticAtRegexp, but it also -// checks for the content of the diagnostic message, -func (e *Env) DiagnosticAtRegexpWithMessage(name, re, msg string) DiagnosticExpectation { - e.T.Helper() - pos := e.RegexpSearch(name, re) - return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true, message: msg} -} diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index 09206509ae4..acd5652811e 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -209,7 +209,7 @@ require golang.org/x/hello v1.2.3 env.ExecuteCodeLensCommand("a/go.mod", command.CheckUpgrades, nil) d := &protocol.PublishDiagnosticsParams{} env.OnceMet( - env.DiagnosticAtRegexpWithMessage("a/go.mod", `require`, "can be upgraded"), + Diagnostics(env.AtRegexp("a/go.mod", `require`), WithMessage("can be upgraded")), ReadDiagnostics("a/go.mod", d), // We do not want there to be a diagnostic for b/go.mod, // but there may be some subtlety in timing here, where this @@ -219,7 +219,7 @@ require golang.org/x/hello v1.2.3 ) // Check for upgrades in b/go.mod and then clear them. env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades, nil) - env.Await(env.DiagnosticAtRegexpWithMessage("b/go.mod", `require`, "can be upgraded")) + env.Await(Diagnostics(env.AtRegexp("b/go.mod", `require`), WithMessage("can be upgraded"))) env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics, nil) env.Await(NoDiagnostics(ForFile("b/go.mod"))) @@ -319,14 +319,14 @@ func Foo() { // Open the file. We have a nonexistant symbol that will break cgo processing. env.OpenFile("cgo.go") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("cgo.go", ``, "go list failed to return CompiledGoFiles"), + Diagnostics(env.AtRegexp("cgo.go", ``), WithMessage("go list failed to return CompiledGoFiles")), ) // Fix the C function name. We haven't regenerated cgo, so nothing should be fixed. env.RegexpReplace("cgo.go", `int fortythree`, "int fortytwo") env.SaveBuffer("cgo.go") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("cgo.go", ``, "go list failed to return CompiledGoFiles"), + Diagnostics(env.AtRegexp("cgo.go", ``), WithMessage("go list failed to return CompiledGoFiles")), ) // Regenerate cgo, fixing the diagnostic. diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 6608d24be03..1c1168199c7 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -594,8 +594,8 @@ func main() { ).Run(t, collision, func(t *testing.T, env *Env) { env.OpenFile("x/x.go") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("x/x.go", `^`, "found packages main (main.go) and x (x.go)"), - env.DiagnosticAtRegexpWithMessage("x/main.go", `^`, "found packages main (main.go) and x (x.go)"), + Diagnostics(env.AtRegexp("x/x.go", `^`), WithMessage("found packages main (main.go) and x (x.go)")), + Diagnostics(env.AtRegexp("x/main.go", `^`), WithMessage("found packages main (main.go) and x (x.go)")), ) // We don't recover cleanly from the errors without good overlay support. @@ -662,7 +662,7 @@ func main() { // Expect a diagnostic and fix to remove the dependency in the go.mod. env.AfterChange( NoDiagnostics(ForFile("main.go")), - env.DiagnosticAtRegexpWithMessage("go.mod", "require github.com/ardanlabs/conf", "not used in this module"), + Diagnostics(env.AtRegexp("go.mod", "require github.com/ardanlabs/conf"), WithMessage("not used in this module")), ReadDiagnostics("go.mod", &d), ) env.ApplyQuickFixes("go.mod", d.Diagnostics) @@ -702,7 +702,7 @@ func main() { env.SaveBuffer("main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexpWithMessage("main.go", `"github.com/ardanlabs/conf"`, "no required module"), + Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`), WithMessage("no required module")), ReadDiagnostics("main.go", &d), ) env.ApplyQuickFixes("main.go", d.Diagnostics) @@ -1136,7 +1136,7 @@ func main() {} Run(t, pkgDefault, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.Await( - env.DiagnosticAtRegexpWithMessage("main.go", "default", "expected 'IDENT'"), + Diagnostics(env.AtRegexp("main.go", "default"), WithMessage("expected 'IDENT'")), ) }) } @@ -1205,7 +1205,7 @@ func main() { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexpWithMessage("main.go", `t{"msg"}`, "redundant type"), + Diagnostics(env.AtRegexp("main.go", `t{"msg"}`), WithMessage("redundant type")), ReadDiagnostics("main.go", &d), ) if tags := d.Diagnostics[0].Tags; len(tags) == 0 || tags[0] != protocol.Unnecessary { @@ -1369,7 +1369,7 @@ func main() { Run(t, mod, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - NoDiagnostics(WithMessageContaining("illegal character U+0023 '#'")), + NoDiagnostics(WithMessage("illegal character U+0023 '#'")), ) }) } @@ -1539,9 +1539,9 @@ import _ "mod.com/triple/a" ` Run(t, mod, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexpWithMessage("self/self.go", `_ "mod.com/self"`, "import cycle not allowed"), - env.DiagnosticAtRegexpWithMessage("double/a/a.go", `_ "mod.com/double/b"`, "import cycle not allowed"), - env.DiagnosticAtRegexpWithMessage("triple/a/a.go", `_ "mod.com/triple/b"`, "import cycle not allowed"), + Diagnostics(env.AtRegexp("self/self.go", `_ "mod.com/self"`), WithMessage("import cycle not allowed")), + Diagnostics(env.AtRegexp("double/a/a.go", `_ "mod.com/double/b"`), WithMessage("import cycle not allowed")), + Diagnostics(env.AtRegexp("triple/a/a.go", `_ "mod.com/triple/b"`), WithMessage("import cycle not allowed")), ) }) } @@ -1578,8 +1578,8 @@ const B = a.B // // TODO(golang/go#52904): we should get *both* of these errors. AnyOf( - env.DiagnosticAtRegexpWithMessage("a/a.go", `"mod.test/b"`, "import cycle"), - env.DiagnosticAtRegexpWithMessage("b/b.go", `"mod.test/a"`, "import cycle"), + Diagnostics(env.AtRegexp("a/a.go", `"mod.test/b"`), WithMessage("import cycle")), + Diagnostics(env.AtRegexp("b/b.go", `"mod.test/a"`), WithMessage("import cycle")), ), ) env.RegexpReplace("b/b.go", `const B = a\.B`, "") @@ -1607,7 +1607,7 @@ import ( t.Run("module", func(t *testing.T) { Run(t, mod, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexpWithMessage("main.go", `"nosuchpkg"`, `could not import nosuchpkg (no required module provides package "nosuchpkg"`), + Diagnostics(env.AtRegexp("main.go", `"nosuchpkg"`), WithMessage(`could not import nosuchpkg (no required module provides package "nosuchpkg"`)), ) }) }) @@ -1618,7 +1618,7 @@ import ( Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexpWithMessage("main.go", `"nosuchpkg"`, `cannot find package "nosuchpkg" in any of`), + Diagnostics(env.AtRegexp("main.go", `"nosuchpkg"`), WithMessage(`cannot find package "nosuchpkg" in any of`)), ) }) }) @@ -1676,7 +1676,7 @@ func helloHelper() {} env.OpenFile("nested/hello/hello.go") env.AfterChange( env.DiagnosticAtRegexp("nested/hello/hello.go", "helloHelper"), - env.DiagnosticAtRegexpWithMessage("nested/hello/hello.go", "package hello", "nested module"), + Diagnostics(env.AtRegexp("nested/hello/hello.go", "package hello"), WithMessage("nested module")), OutstandingWork(lsp.WorkspaceLoadFailure, "nested module"), ) }) @@ -1753,7 +1753,7 @@ package main env.SetBufferContent("other.go", "package main\n\nasdf") // The new diagnostic in other.go should not suppress diagnostics in main.go. env.AfterChange( - env.DiagnosticAtRegexpWithMessage("other.go", "asdf", "expected declaration"), + Diagnostics(env.AtRegexp("other.go", "asdf"), WithMessage("expected declaration")), env.DiagnosticAtRegexp("main.go", "asdf"), ) }) @@ -1815,7 +1815,7 @@ const C = 0b10 Run(t, files, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexpWithMessage("main.go", `0b10`, "go1.13 or later"), + Diagnostics(env.AtRegexp("main.go", `0b10`), WithMessage("go1.13 or later")), ) env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.13\n") env.AfterChange( @@ -1868,7 +1868,7 @@ func F[T any](_ T) { var d protocol.PublishDiagnosticsParams env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), + Diagnostics(env.AtRegexp("main.go", `T any`), WithMessage("type parameter")), ReadDiagnostics("main.go", &d), ) @@ -1902,7 +1902,7 @@ func F[T any](_ T) { // We should have a diagnostic because generics are not supported at 1.16. env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), + Diagnostics(env.AtRegexp("main.go", `T any`), WithMessage("type parameter")), ReadDiagnostics("main.go", &d), ) @@ -1955,8 +1955,11 @@ func MyPrintf(format string, args ...interface{}) { Run(t, src, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("a/a.go", "new.*Printf", - "format %d has arg \"s\" of wrong type string")) + Diagnostics( + env.AtRegexp("a/a.go", "new.*Printf"), + WithMessage("format %d has arg \"s\" of wrong type string"), + ), + ) }) } @@ -2005,7 +2008,7 @@ var _ = 1 / "" // type error var diags protocol.PublishDiagnosticsParams env.OpenFile("a/a.go") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("a/a.go", "mu2 := (mu)", "assignment copies lock value"), + Diagnostics(env.AtRegexp("a/a.go", "mu2 := (mu)"), WithMessage("assignment copies lock value")), ReadDiagnostics("a/a.go", &diags)) // Assert that there were no other diagnostics. diff --git a/gopls/internal/regtest/misc/embed_test.go b/gopls/internal/regtest/misc/embed_test.go index 15943b8c05f..021fbfcc06d 100644 --- a/gopls/internal/regtest/misc/embed_test.go +++ b/gopls/internal/regtest/misc/embed_test.go @@ -28,7 +28,12 @@ var foo string ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("x.go") - env.AfterChange(env.DiagnosticAtRegexpWithMessage("x.go", `NONEXISTENT`, "no matching files found")) + env.AfterChange( + Diagnostics( + env.AtRegexp("x.go", `NONEXISTENT`), + WithMessage("no matching files found"), + ), + ) env.RegexpReplace("x.go", `NONEXISTENT`, "x.go") env.AfterChange(NoDiagnostics(ForFile("x.go"))) }) diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index f088a0e86bc..aad8360b931 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -70,8 +70,8 @@ const a = 2 Run(t, badPackageDup, func(t *testing.T, env *Env) { env.OpenFile("b.go") env.Await( - env.DiagnosticAtRegexpWithMessage("b.go", `a = 2`, "a redeclared"), - env.DiagnosticAtRegexpWithMessage("a.go", `a = 1`, "other declaration"), + Diagnostics(env.AtRegexp("b.go", `a = 2`), WithMessage("a redeclared")), + Diagnostics(env.AtRegexp("a.go", `a = 1`), WithMessage("other declaration")), ) // Fix the error by editing the const name in b.go to `b`. diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index f00a734a23f..42096a8b360 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -79,7 +79,7 @@ func Foo() error { var d protocol.PublishDiagnosticsParams env.AfterChange( // The error message here changed in 1.18; "return values" covers both forms. - env.DiagnosticAtRegexpWithMessage("main.go", `return`, "return values"), + Diagnostics(env.AtRegexp("main.go", `return`), WithMessage("return values")), ReadDiagnostics("main.go", &d), ) codeActions := env.CodeAction("main.go", d.Diagnostics) diff --git a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go index 06e34b55f5f..4fcf1067a1e 100644 --- a/gopls/internal/regtest/misc/vendor_test.go +++ b/gopls/internal/regtest/misc/vendor_test.go @@ -53,13 +53,13 @@ func _() { d := &protocol.PublishDiagnosticsParams{} env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexpWithMessage("go.mod", "module mod.com", "Inconsistent vendoring"), + Diagnostics(env.AtRegexp("go.mod", "module mod.com"), WithMessage("Inconsistent vendoring")), ReadDiagnostics("go.mod", d), ) env.ApplyQuickFixes("go.mod", d.Diagnostics) env.AfterChange( - env.DiagnosticAtRegexpWithMessage("a/a1.go", `q int`, "not used"), + Diagnostics(env.AtRegexp("a/a1.go", `q int`), WithMessage("not used")), ) }) } diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index b94894c79bf..e88368a3b14 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -543,8 +543,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. - env.DiagnosticAtRegexpWithMessage("a/main.go", `"example.com/blah/v2"`, "cannot find module providing"), - env.DiagnosticAtRegexpWithMessage("a/go.mod", `require example.com/blah/v2`, "cannot find module providing"), + 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")), ReadDiagnostics("a/go.mod", &modDiags), ) env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics) @@ -604,7 +604,7 @@ func main() { d := protocol.PublishDiagnosticsParams{} env.AfterChange( // Make sure the diagnostic mentions the new version -- the old diagnostic is in the same place. - env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "example.com@v1.2.3"), + Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.3"), WithMessage("example.com@v1.2.3")), ReadDiagnostics("a/go.mod", &d), ) qfs := env.GetQuickFixes("a/go.mod", d.Diagnostics) @@ -793,7 +793,7 @@ func main() { ProxyFiles(workspaceProxy), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "is not used"), + Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.3"), WithMessage("is not used")), ) }) } @@ -875,7 +875,7 @@ func main() { env.AfterChange( Diagnostics( env.AtRegexp("go.mod", `example.com v1.2.3`), - WithMessageContaining("go.sum is out of sync"), + WithMessage("go.sum is out of sync"), ), ReadDiagnostics("go.mod", d), ) @@ -1078,7 +1078,7 @@ func main() { env.AfterChange( Diagnostics( env.AtRegexp("go.mod", `example.com`), - WithMessageContaining("go.sum is out of sync"), + WithMessage("go.sum is out of sync"), ), ReadDiagnostics("go.mod", params), ) @@ -1144,7 +1144,10 @@ func main() { env.OpenFile("main.go") d := &protocol.PublishDiagnosticsParams{} env.Await( - env.DiagnosticAtRegexpWithMessage("main.go", `"example.com/blah"`, `could not import example.com/blah (no required module provides package "example.com/blah")`), + Diagnostics( + env.AtRegexp("main.go", `"example.com/blah"`), + WithMessage(`could not import example.com/blah (no required module provides package "example.com/blah")`), + ), ReadDiagnostics("main.go", d), ) env.ApplyQuickFixes("main.go", d.Diagnostics) @@ -1167,7 +1170,7 @@ package main Run(t, files, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexpWithMessage("go.mod", `go foo`, "invalid go version"), + Diagnostics(env.AtRegexp("go.mod", `go foo`), WithMessage("invalid go version")), ) env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.12\n") env.AfterChange(NoDiagnostics(ForFile("go.mod"))) diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go index 831fa771a46..c05012d74c7 100644 --- a/gopls/internal/regtest/workspace/fromenv_test.go +++ b/gopls/internal/regtest/workspace/fromenv_test.go @@ -62,7 +62,7 @@ use ( // included in the workspace. env.OpenFile("work/a/a.go") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("work/b/b.go", "x := 1", "not used"), + Diagnostics(env.AtRegexp("work/b/b.go", "x := 1"), WithMessage("not used")), ) }) } diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index 4990a6659aa..31663fbeb07 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -97,7 +97,7 @@ func main() {} // packages for bar.go env.RegexpReplace("bar.go", "ignore", "excluded") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("bar.go", "package (main)", "No packages"), + Diagnostics(env.AtRegexp("bar.go", "package (main)"), WithMessage("No packages")), ) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 0d000b877fd..3932648bdc1 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -683,7 +683,7 @@ module example.com/bar Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("go.work") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("go.work", "use", "directory ./foo does not contain a module"), + Diagnostics(env.AtRegexp("go.work", "use"), WithMessage("directory ./foo does not contain a module")), ) // The following tests is a regression test against an issue where we weren't // copying the workFile struct field on workspace when a new one was created in @@ -697,7 +697,7 @@ module example.com/bar ) env.SetBufferContent("go.work", "go 1.18 \n\n use ./foo\n") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("go.work", "use", "directory ./foo does not contain a module"), + Diagnostics(env.AtRegexp("go.work", "use"), WithMessage("directory ./foo does not contain a module")), ) }) } @@ -714,8 +714,8 @@ replace Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("go.work") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("go.work", "usa", "unknown directive: usa"), - env.DiagnosticAtRegexpWithMessage("go.work", "replace", "usage: replace"), + Diagnostics(env.AtRegexp("go.work", "usa"), WithMessage("unknown directive: usa")), + Diagnostics(env.AtRegexp("go.work", "replace"), WithMessage("usage: replace")), ) }) } @@ -963,7 +963,7 @@ func main() { env.AfterChange( Diagnostics( env.AtRegexp("go.mod", `example.com v1.2.3`), - WithMessageContaining("go.sum is out of sync"), + WithMessage("go.sum is out of sync"), ), ReadDiagnostics("b/go.mod", params), ) @@ -1167,7 +1167,7 @@ import ( ) env.OpenFile("a/main.go") env.AfterChange( - env.DiagnosticAtRegexpWithMessage("a/main.go", "V", "not used"), + Diagnostics(env.AtRegexp("a/main.go", "V"), WithMessage("not used")), ) env.CloseBuffer("a/main.go") From 91b60705d66caf33c9c08752d5bc73fa78c01647 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 23:03:12 -0500 Subject: [PATCH 629/723] gopls/internal/regtest: eliminate DiagnosticAtRegexp Replace DiagnosticAtRegexp with diagnostic filters. This allowed eliminating DiagnosticExpectation, which was the only complicated implementation of the Expectation interface. Replace Expectation with the concrete (formerly named) SimpleExpectation. Updates golang/go#39384 Change-Id: I6716e869609dce9777025557494c8f81a606e4ff Reviewed-on: https://go-review.googlesource.com/c/tools/+/461939 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/regtest/env.go | 2 +- gopls/internal/lsp/regtest/expectation.go | 302 +++++------------- .../regtest/completion/completion_test.go | 2 +- .../regtest/diagnostics/analysis_test.go | 2 +- .../regtest/diagnostics/diagnostics_test.go | 99 +++--- .../regtest/diagnostics/undeclared_test.go | 4 +- gopls/internal/regtest/misc/generate_test.go | 2 +- gopls/internal/regtest/misc/imports_test.go | 6 +- gopls/internal/regtest/misc/rename_test.go | 16 +- .../regtest/misc/semantictokens_test.go | 4 +- gopls/internal/regtest/misc/shared_test.go | 4 +- gopls/internal/regtest/misc/vuln_test.go | 8 +- .../internal/regtest/modfile/modfile_test.go | 40 +-- .../regtest/template/template_test.go | 8 +- gopls/internal/regtest/watch/watch_test.go | 20 +- .../internal/regtest/workspace/broken_test.go | 12 +- .../workspace/directoryfilters_test.go | 2 +- .../regtest/workspace/metadata_test.go | 4 +- .../regtest/workspace/standalone_test.go | 6 +- .../regtest/workspace/workspace_test.go | 26 +- 20 files changed, 224 insertions(+), 345 deletions(-) diff --git a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go index 73e8ef31cee..ad606587b0a 100644 --- a/gopls/internal/lsp/regtest/env.go +++ b/gopls/internal/lsp/regtest/env.go @@ -297,7 +297,7 @@ func checkExpectations(s State, expectations []Expectation) (Verdict, string) { if v > finalVerdict { finalVerdict = v } - summary.WriteString(fmt.Sprintf("%v: %s\n", v, e.Description())) + summary.WriteString(fmt.Sprintf("%v: %s\n", v, e.Description)) } return finalVerdict, summary.String() } diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 1c359252f0e..d0896ff7e2d 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -14,17 +14,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" ) -// An Expectation asserts that the state of the editor at a point in time -// matches an expected condition. This is used for signaling in tests when -// certain conditions in the editor are met. -type Expectation interface { - // Check determines whether the state of the editor satisfies the - // expectation, returning the results that met the condition. - Check(State) Verdict - // Description is a human-readable description of the expectation. - Description() string -} - var ( // InitialWorkspaceLoad is an expectation that the workspace initial load has // completed. It is verified via workdone reporting. @@ -60,25 +49,15 @@ func (v Verdict) String() string { return fmt.Sprintf("unrecognized verdict %d", v) } -// SimpleExpectation holds an arbitrary check func, and implements the Expectation interface. -type SimpleExpectation struct { - check func(State) Verdict - description string -} - -// Check invokes e.check. -func (e SimpleExpectation) Check(s State) Verdict { - return e.check(s) -} - -// Description returns e.description. -func (e SimpleExpectation) Description() string { - return e.description +// Expectation holds an arbitrary check func, and implements the Expectation interface. +type Expectation struct { + Check func(State) Verdict + Description string } // OnceMet returns an Expectation that, once the precondition is met, asserts // that mustMeet is met. -func OnceMet(precondition Expectation, mustMeets ...Expectation) *SimpleExpectation { +func OnceMet(precondition Expectation, mustMeets ...Expectation) Expectation { check := func(s State) Verdict { switch pre := precondition.Check(s); pre { case Unmeetable: @@ -96,23 +75,23 @@ func OnceMet(precondition Expectation, mustMeets ...Expectation) *SimpleExpectat } } description := describeExpectations(mustMeets...) - return &SimpleExpectation{ - check: check, - description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description(), description), + return Expectation{ + Check: check, + Description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description, description), } } func describeExpectations(expectations ...Expectation) string { var descriptions []string for _, e := range expectations { - descriptions = append(descriptions, e.Description()) + descriptions = append(descriptions, e.Description) } return strings.Join(descriptions, "\n") } // AnyOf returns an expectation that is satisfied when any of the given // expectations is met. -func AnyOf(anyOf ...Expectation) *SimpleExpectation { +func AnyOf(anyOf ...Expectation) Expectation { check := func(s State) Verdict { for _, e := range anyOf { verdict := e.Check(s) @@ -123,9 +102,9 @@ func AnyOf(anyOf ...Expectation) *SimpleExpectation { return Unmet } description := describeExpectations(anyOf...) - return &SimpleExpectation{ - check: check, - description: fmt.Sprintf("Any of:\n%s", description), + return Expectation{ + Check: check, + Description: fmt.Sprintf("Any of:\n%s", description), } } @@ -139,7 +118,7 @@ func AnyOf(anyOf ...Expectation) *SimpleExpectation { // why an expectation failed. This should allow us to significantly improve // test output: we won't need to summarize state at all, as the verdict // explanation itself should describe clearly why the expectation not met. -func AllOf(allOf ...Expectation) *SimpleExpectation { +func AllOf(allOf ...Expectation) Expectation { check := func(s State) Verdict { verdict := Met for _, e := range allOf { @@ -150,15 +129,15 @@ func AllOf(allOf ...Expectation) *SimpleExpectation { return verdict } description := describeExpectations(allOf...) - return &SimpleExpectation{ - check: check, - description: fmt.Sprintf("All of:\n%s", description), + return Expectation{ + Check: check, + Description: fmt.Sprintf("All of:\n%s", description), } } // ReadDiagnostics is an 'expectation' that is used to read diagnostics // atomically. It is intended to be used with 'OnceMet'. -func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) *SimpleExpectation { +func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation { check := func(s State) Verdict { diags, ok := s.diagnostics[fileName] if !ok { @@ -167,29 +146,29 @@ func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) * *into = *diags return Met } - return &SimpleExpectation{ - check: check, - description: fmt.Sprintf("read diagnostics for %q", fileName), + return Expectation{ + Check: check, + Description: fmt.Sprintf("read diagnostics for %q", fileName), } } // NoOutstandingWork asserts that there is no work initiated using the LSP // $/progress API that has not completed. -func NoOutstandingWork() SimpleExpectation { +func NoOutstandingWork() Expectation { check := func(s State) Verdict { if len(s.outstandingWork()) == 0 { return Met } return Unmet } - return SimpleExpectation{ - check: check, - description: "no outstanding work", + return Expectation{ + Check: check, + Description: "no outstanding work", } } // NoShownMessage asserts that the editor has not received a ShowMessage. -func NoShownMessage(subString string) SimpleExpectation { +func NoShownMessage(subString string) Expectation { check := func(s State) Verdict { for _, m := range s.showMessage { if strings.Contains(m.Message, subString) { @@ -198,15 +177,15 @@ func NoShownMessage(subString string) SimpleExpectation { } return Met } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("no ShowMessage received containing %q", subString), + return Expectation{ + Check: check, + Description: fmt.Sprintf("no ShowMessage received containing %q", subString), } } // ShownMessage asserts that the editor has received a ShowMessageRequest // containing the given substring. -func ShownMessage(containing string) SimpleExpectation { +func ShownMessage(containing string) Expectation { check := func(s State) Verdict { for _, m := range s.showMessage { if strings.Contains(m.Message, containing) { @@ -215,15 +194,15 @@ func ShownMessage(containing string) SimpleExpectation { } return Unmet } - return SimpleExpectation{ - check: check, - description: "received ShowMessage", + return Expectation{ + Check: check, + Description: "received ShowMessage", } } // ShowMessageRequest asserts that the editor has received a ShowMessageRequest // with an action item that has the given title. -func ShowMessageRequest(title string) SimpleExpectation { +func ShowMessageRequest(title string) Expectation { check := func(s State) Verdict { if len(s.showMessageRequest) == 0 { return Unmet @@ -238,9 +217,9 @@ func ShowMessageRequest(title string) SimpleExpectation { } return Unmet } - return SimpleExpectation{ - check: check, - description: "received ShowMessageRequest", + return Expectation{ + Check: check, + Description: "received ShowMessageRequest", } } @@ -348,16 +327,16 @@ func (e *Env) DoneWithClose() Expectation { // StartedWork expect a work item to have been started >= atLeast times. // // See CompletedWork. -func StartedWork(title string, atLeast uint64) SimpleExpectation { +func StartedWork(title string, atLeast uint64) Expectation { check := func(s State) Verdict { if s.startedWork()[title] >= atLeast { return Met } return Unmet } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast), + return Expectation{ + Check: check, + Description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast), } } @@ -365,7 +344,7 @@ func StartedWork(title string, atLeast uint64) SimpleExpectation { // // Since the Progress API doesn't include any hidden metadata, we must use the // progress notification title to identify the work we expect to be completed. -func CompletedWork(title string, count uint64, atLeast bool) SimpleExpectation { +func CompletedWork(title string, count uint64, atLeast bool) Expectation { check := func(s State) Verdict { completed := s.completedWork() if completed[title] == count || atLeast && completed[title] > count { @@ -377,9 +356,9 @@ func CompletedWork(title string, count uint64, atLeast bool) SimpleExpectation { if atLeast { desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count) } - return SimpleExpectation{ - check: check, - description: desc, + return Expectation{ + Check: check, + Description: desc, } } @@ -396,7 +375,7 @@ type WorkStatus struct { // // If the token is not a progress token that the client has seen, this // expectation is Unmeetable. -func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) SimpleExpectation { +func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) Expectation { check := func(s State) Verdict { work, ok := s.work[token] if !ok { @@ -412,16 +391,16 @@ func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) SimpleExp return Unmet } desc := fmt.Sprintf("completed work for token %v", token) - return SimpleExpectation{ - check: check, - description: desc, + return Expectation{ + Check: check, + Description: desc, } } // OutstandingWork expects a work item to be outstanding. The given title must // be an exact match, whereas the given msg must only be contained in the work // item's message. -func OutstandingWork(title, msg string) SimpleExpectation { +func OutstandingWork(title, msg string) Expectation { check := func(s State) Verdict { for _, work := range s.work { if work.complete { @@ -433,32 +412,15 @@ func OutstandingWork(title, msg string) SimpleExpectation { } return Unmet } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("outstanding work: %q containing %q", title, msg), + return Expectation{ + Check: check, + Description: fmt.Sprintf("outstanding work: %q containing %q", title, msg), } } -// LogExpectation is an expectation on the log messages received by the editor -// from gopls. -type LogExpectation struct { - check func([]*protocol.LogMessageParams) Verdict - description string -} - -// Check implements the Expectation interface. -func (e LogExpectation) Check(s State) Verdict { - return e.check(s.logs) -} - -// Description implements the Expectation interface. -func (e LogExpectation) Description() string { - return e.description -} - // NoErrorLogs asserts that the client has not received any log messages of // error severity. -func NoErrorLogs() LogExpectation { +func NoErrorLogs() Expectation { return NoLogMatching(protocol.Error, "") } @@ -468,14 +430,14 @@ func NoErrorLogs() LogExpectation { // The count argument specifies the expected number of matching logs. If // atLeast is set, this is a lower bound, otherwise there must be exactly cound // matching logs. -func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) LogExpectation { +func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) Expectation { rec, err := regexp.Compile(re) if err != nil { panic(err) } - check := func(msgs []*protocol.LogMessageParams) Verdict { + check := func(state State) Verdict { var found int - for _, msg := range msgs { + for _, msg := range state.logs { if msg.Type == typ && rec.Match([]byte(msg.Message)) { found++ } @@ -490,16 +452,16 @@ func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) L if atLeast { desc = fmt.Sprintf("log message matching %q expected at least %v times", re, count) } - return LogExpectation{ - check: check, - description: desc, + return Expectation{ + Check: check, + Description: desc, } } // NoLogMatching asserts that the client has not received a log message // of type typ matching the regexp re. If re is an empty string, any log // message is considered a match. -func NoLogMatching(typ protocol.MessageType, re string) LogExpectation { +func NoLogMatching(typ protocol.MessageType, re string) Expectation { var r *regexp.Regexp if re != "" { var err error @@ -508,8 +470,8 @@ func NoLogMatching(typ protocol.MessageType, re string) LogExpectation { panic(err) } } - check := func(msgs []*protocol.LogMessageParams) Verdict { - for _, msg := range msgs { + check := func(state State) Verdict { + for _, msg := range state.logs { if msg.Type != typ { continue } @@ -519,25 +481,25 @@ func NoLogMatching(typ protocol.MessageType, re string) LogExpectation { } return Met } - return LogExpectation{ - check: check, - description: fmt.Sprintf("no log message matching %q", re), + return Expectation{ + Check: check, + Description: fmt.Sprintf("no log message matching %q", re), } } // FileWatchMatching expects that a file registration matches re. -func FileWatchMatching(re string) SimpleExpectation { - return SimpleExpectation{ - check: checkFileWatch(re, Met, Unmet), - description: fmt.Sprintf("file watch matching %q", re), +func FileWatchMatching(re string) Expectation { + return Expectation{ + Check: checkFileWatch(re, Met, Unmet), + Description: fmt.Sprintf("file watch matching %q", re), } } // NoFileWatchMatching expects that no file registration matches re. -func NoFileWatchMatching(re string) SimpleExpectation { - return SimpleExpectation{ - check: checkFileWatch(re, Unmet, Met), - description: fmt.Sprintf("no file watch matching %q", re), +func NoFileWatchMatching(re string) Expectation { + return Expectation{ + Check: checkFileWatch(re, Unmet, Met), + Description: fmt.Sprintf("no file watch matching %q", re), } } @@ -581,7 +543,7 @@ func jsonProperty(obj interface{}, path ...string) interface{} { // TODO(rfindley): remove this once TestWatchReplaceTargets has been revisited. // // Deprecated: use (No)FileWatchMatching -func RegistrationMatching(re string) SimpleExpectation { +func RegistrationMatching(re string) Expectation { rec := regexp.MustCompile(re) check := func(s State) Verdict { for _, p := range s.registrations { @@ -593,15 +555,15 @@ func RegistrationMatching(re string) SimpleExpectation { } return Unmet } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("registration matching %q", re), + return Expectation{ + Check: check, + Description: fmt.Sprintf("registration matching %q", re), } } // UnregistrationMatching asserts that the client has received an // unregistration whose ID matches the given regexp. -func UnregistrationMatching(re string) SimpleExpectation { +func UnregistrationMatching(re string) Expectation { rec := regexp.MustCompile(re) check := func(s State) Verdict { for _, p := range s.unregistrations { @@ -613,89 +575,12 @@ func UnregistrationMatching(re string) SimpleExpectation { } return Unmet } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("unregistration matching %q", re), + return Expectation{ + Check: check, + Description: fmt.Sprintf("unregistration matching %q", re), } } -// A DiagnosticExpectation is a condition that must be met by the current set -// of diagnostics for a file. -type DiagnosticExpectation struct { - // optionally, the position of the diagnostic and the regex used to calculate it. - pos *protocol.Position - re string - - // optionally, the message that the diagnostic should contain. - message string - - // whether the expectation is that the diagnostic is present, or absent. - present bool - - // path is the scratch workdir-relative path to the file being asserted on. - path string - - // optionally, the diagnostic source - source string -} - -// Check implements the Expectation interface. -func (e DiagnosticExpectation) Check(s State) Verdict { - diags, ok := s.diagnostics[e.path] - if !ok { - if !e.present { - return Met - } - return Unmet - } - - found := false - for _, d := range diags.Diagnostics { - if e.pos != nil { - if d.Range.Start != *e.pos { - continue - } - } - if e.message != "" { - if !strings.Contains(d.Message, e.message) { - continue - } - } - if e.source != "" && e.source != d.Source { - continue - } - found = true - break - } - - if found == e.present { - return Met - } - return Unmet -} - -// Description implements the Expectation interface. -func (e DiagnosticExpectation) Description() string { - desc := e.path + ":" - if !e.present { - desc += " no" - } - desc += " diagnostic" - if e.pos != nil { - desc += fmt.Sprintf(" at {line:%d, column:%d}", e.pos.Line, e.pos.Character) - if e.re != "" { - desc += fmt.Sprintf(" (location of %q)", e.re) - } - } - if e.message != "" { - desc += fmt.Sprintf(" with message %q", e.message) - } - if e.source != "" { - desc += fmt.Sprintf(" from source %q", e.source) - } - return desc -} - // Diagnostics asserts that there is at least one diagnostic matching the given // filters. func Diagnostics(filters ...DiagnosticFilter) Expectation { @@ -722,9 +607,9 @@ func Diagnostics(filters ...DiagnosticFilter) Expectation { for _, filter := range filters { descs = append(descs, filter.desc) } - return SimpleExpectation{ - check: check, - description: "any diagnostics " + strings.Join(descs, ", "), + return Expectation{ + Check: check, + Description: "any diagnostics " + strings.Join(descs, ", "), } } @@ -752,9 +637,9 @@ func NoDiagnostics(filters ...DiagnosticFilter) Expectation { for _, filter := range filters { descs = append(descs, filter.desc) } - return SimpleExpectation{ - check: check, - description: "no diagnostics " + strings.Join(descs, ", "), + return Expectation{ + Check: check, + Description: "no diagnostics " + strings.Join(descs, ", "), } } @@ -839,14 +724,3 @@ func WithMessage(substring string) DiagnosticFilter { }, } } - -// TODO(rfindley): eliminate all expectations below this point. - -// DiagnosticAtRegexp expects that there is a diagnostic entry at the start -// position matching the regexp search string re in the buffer specified by -// name. Note that this currently ignores the end position. -func (e *Env) DiagnosticAtRegexp(name, re string) DiagnosticExpectation { - e.T.Helper() - pos := e.RegexpSearch(name, re) - return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true} -} diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index c8674f04b7b..2f9b550ed28 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -319,7 +319,7 @@ func _() { // Await the diagnostics to add example.com/blah to the go.mod file. env.Await( - env.DiagnosticAtRegexp("main.go", `"example.com/blah"`), + Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), ) }) } diff --git a/gopls/internal/regtest/diagnostics/analysis_test.go b/gopls/internal/regtest/diagnostics/analysis_test.go index a038d15aeea..308c25f13f6 100644 --- a/gopls/internal/regtest/diagnostics/analysis_test.go +++ b/gopls/internal/regtest/diagnostics/analysis_test.go @@ -39,7 +39,7 @@ func main() { var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("main.go", "2006-02-01"), + Diagnostics(env.AtRegexp("main.go", "2006-02-01")), ReadDiagnostics("main.go", &d), ) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 1c1168199c7..4ce509c87ac 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -49,7 +49,7 @@ func TestDiagnosticErrorInEditedFile(t *testing.T) { env.OpenFile("main.go") env.RegexpReplace("main.go", "Printl(n)", "") env.AfterChange( - env.DiagnosticAtRegexp("main.go", "Printl"), + Diagnostics(env.AtRegexp("main.go", "Printl")), // Assert that this test has sent no error logs to the client. This is not // strictly necessary for testing this regression, but is included here // as an example of using the NoErrorLogs() expectation. Feel free to @@ -74,7 +74,7 @@ func m() { } `) env.Await( - env.DiagnosticAtRegexp("main.go", "log"), + Diagnostics(env.AtRegexp("main.go", "log")), ) env.SaveBuffer("main.go") env.AfterChange(NoDiagnostics(ForFile("main.go"))) @@ -88,7 +88,7 @@ const Foo = "abc ` Run(t, brokenFile, func(t *testing.T, env *Env) { env.CreateBuffer("broken.go", brokenFile) - env.Await(env.DiagnosticAtRegexp("broken.go", "\"abc")) + env.Await(Diagnostics(env.AtRegexp("broken.go", "\"abc"))) }) } @@ -112,8 +112,8 @@ func TestDiagnosticClearingOnEdit(t *testing.T) { Run(t, badPackage, func(t *testing.T, env *Env) { env.OpenFile("b.go") env.AfterChange( - env.DiagnosticAtRegexp("a.go", "a = 1"), - env.DiagnosticAtRegexp("b.go", "a = 2"), + Diagnostics(env.AtRegexp("a.go", "a = 1")), + Diagnostics(env.AtRegexp("b.go", "a = 2")), ) // Fix the error by editing the const name in b.go to `b`. @@ -128,7 +128,10 @@ func TestDiagnosticClearingOnEdit(t *testing.T) { func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) { Run(t, badPackage, func(t *testing.T, env *Env) { env.OpenFile("a.go") - env.Await(env.DiagnosticAtRegexp("a.go", "a = 1"), env.DiagnosticAtRegexp("b.go", "a = 2")) + env.Await( + Diagnostics(env.AtRegexp("a.go", "a = 1")), + Diagnostics(env.AtRegexp("b.go", "a = 2")), + ) env.RemoveWorkspaceFile("b.go") env.Await( @@ -144,14 +147,14 @@ func TestDiagnosticClearingOnClose(t *testing.T) { const a = 3`) env.AfterChange( - env.DiagnosticAtRegexp("a.go", "a = 1"), - env.DiagnosticAtRegexp("b.go", "a = 2"), - env.DiagnosticAtRegexp("c.go", "a = 3"), + Diagnostics(env.AtRegexp("a.go", "a = 1")), + Diagnostics(env.AtRegexp("b.go", "a = 2")), + Diagnostics(env.AtRegexp("c.go", "a = 3")), ) env.CloseBuffer("c.go") env.AfterChange( - env.DiagnosticAtRegexp("a.go", "a = 1"), - env.DiagnosticAtRegexp("b.go", "a = 2"), + Diagnostics(env.AtRegexp("a.go", "a = 1")), + Diagnostics(env.AtRegexp("b.go", "a = 2")), NoDiagnostics(ForFile("c.go")), ) }) @@ -171,7 +174,7 @@ const a = http.MethodGet `, }) env.AfterChange( - env.DiagnosticAtRegexp("c/c.go", "http.MethodGet"), + Diagnostics(env.AtRegexp("c/c.go", "http.MethodGet")), ) // Save file, which will organize imports, adding the expected import. // Expect the diagnostics to clear. @@ -211,7 +214,7 @@ func TestA(t *testing.T) { // not break the workspace. func TestDeleteTestVariant(t *testing.T) { Run(t, test38878, func(t *testing.T, env *Env) { - env.Await(env.DiagnosticAtRegexp("a_test.go", `f\((3)\)`)) + env.Await(Diagnostics(env.AtRegexp("a_test.go", `f\((3)\)`))) env.RemoveWorkspaceFile("a_test.go") env.AfterChange(NoDiagnostics(ForFile("a_test.go"))) @@ -219,7 +222,7 @@ func TestDeleteTestVariant(t *testing.T) { // triggering a metadata load. env.OpenFile("a.go") env.RegexpReplace("a.go", `// import`, "import") - env.AfterChange(env.DiagnosticAtRegexp("a.go", `"fmt"`)) + env.AfterChange(Diagnostics(env.AtRegexp("a.go", `"fmt"`))) }) } @@ -258,7 +261,7 @@ func Hello() { Run(t, noMod, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), + Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), ) env.CreateBuffer("go.mod", `module mod.com @@ -268,7 +271,7 @@ func Hello() { var d protocol.PublishDiagnosticsParams env.AfterChange( NoDiagnostics(ForFile("main.go")), - env.DiagnosticAtRegexp("bob/bob.go", "x"), + Diagnostics(env.AtRegexp("bob/bob.go", "x")), ReadDiagnostics("bob/bob.go", &d), ) if len(d.Diagnostics) != 1 { @@ -279,12 +282,12 @@ func Hello() { t.Run("initialized", func(t *testing.T) { Run(t, noMod, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), + Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), ) env.RunGoCommand("mod", "init", "mod.com") env.AfterChange( NoDiagnostics(ForFile("main.go")), - env.DiagnosticAtRegexp("bob/bob.go", "x"), + Diagnostics(env.AtRegexp("bob/bob.go", "x")), ) }) }) @@ -294,14 +297,14 @@ func Hello() { Modes(Default), ).Run(t, noMod, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), + Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), ) if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, true); err != nil { t.Fatal(err) } env.AfterChange( NoDiagnostics(ForFile("main.go")), - env.DiagnosticAtRegexp("bob/bob.go", "x"), + Diagnostics(env.AtRegexp("bob/bob.go", "x")), ) }) }) @@ -349,7 +352,7 @@ func TestHello(t *testing.T) { env.OpenFile("lib.go") env.RegexpReplace("lib.go", "_ = x", "var y int") env.AfterChange( - env.DiagnosticAtRegexp("lib.go", "y int"), + Diagnostics(env.AtRegexp("lib.go", "y int")), NoDiagnostics(ForFile("lib_test.go")), ) }) @@ -418,7 +421,7 @@ func TestResolveDiagnosticWithDownload(t *testing.T) { // Check that gopackages correctly loaded this dependency. We should get a // diagnostic for the wrong formatting type. // TODO: we should be able to easily also match the diagnostic message. - env.Await(env.DiagnosticAtRegexp("print.go", "fmt.Printf")) + env.Await(Diagnostics(env.AtRegexp("print.go", "fmt.Printf"))) }) } @@ -441,7 +444,7 @@ func Hello() { ` Run(t, adHoc, func(t *testing.T, env *Env) { env.OpenFile("b/b.go") - env.Await(env.DiagnosticAtRegexp("b/b.go", "x")) + env.Await(Diagnostics(env.AtRegexp("b/b.go", "x"))) }) } @@ -462,7 +465,7 @@ func _() { }, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - env.AfterChange(env.DiagnosticAtRegexp("main.go", "fmt")) + env.AfterChange(Diagnostics(env.AtRegexp("main.go", "fmt"))) env.SaveBuffer("main.go") env.AfterChange(NoDiagnostics(ForFile("main.go"))) }) @@ -602,7 +605,7 @@ func main() { if testenv.Go1Point() >= 16 { env.RegexpReplace("x/x.go", `package x`, `package main`) env.AfterChange( - env.DiagnosticAtRegexp("x/main.go", `fmt`), + Diagnostics(env.AtRegexp("x/main.go", `fmt`)), ) } }) @@ -644,7 +647,7 @@ func main() { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`), + Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)), ReadDiagnostics("main.go", &d), ) env.ApplyQuickFixes("main.go", d.Diagnostics) @@ -656,7 +659,7 @@ func main() { // diagnostic and a fix to remove the import. env.RegexpReplace("main.go", "_ = conf.ErrHelpWanted", "//_ = conf.ErrHelpWanted") env.AfterChange( - env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`), + Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)), ) env.SaveBuffer("main.go") // Expect a diagnostic and fix to remove the dependency in the go.mod. @@ -674,7 +677,7 @@ func main() { env.RegexpReplace("main.go", "//_ = conf.ErrHelpWanted", "_ = conf.ErrHelpWanted") env.SaveBuffer("main.go") env.AfterChange( - env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`), + Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)), ) }) } @@ -772,8 +775,8 @@ func TestHello(t *testing.T) { // There are errors in the code to ensure all is working as expected. env.OpenFile("hello/hello.go") env.AfterChange( - env.DiagnosticAtRegexp("hello/hello.go", "x"), - env.DiagnosticAtRegexp("hello/hello_test.go", "x"), + Diagnostics(env.AtRegexp("hello/hello.go", "x")), + Diagnostics(env.AtRegexp("hello/hello_test.go", "x")), ) // Create an empty file with the intention of making it an x test. @@ -801,7 +804,7 @@ func TestHello(t *testing.T) { // Expect a diagnostic for the missing import. Save, which should // trigger import organization. The diagnostic should clear. env.AfterChange( - env.DiagnosticAtRegexp("hello/hello_x_test.go", "hello.Hello"), + Diagnostics(env.AtRegexp("hello/hello_x_test.go", "hello.Hello")), ) env.SaveBuffer("hello/hello_x_test.go") env.AfterChange( @@ -834,7 +837,7 @@ func TestX(t *testing.T) { } `) env.Await( - env.DiagnosticAtRegexp("foo/bar_test.go", "x"), + Diagnostics(env.AtRegexp("foo/bar_test.go", "x")), ) }) } @@ -915,7 +918,7 @@ const C = a.A // We should still get diagnostics for files that exist. env.RegexpReplace("b/b.go", `a.A`, "a.Nonexistant") - env.Await(env.DiagnosticAtRegexp("b/b.go", `Nonexistant`)) + env.Await(Diagnostics(env.AtRegexp("b/b.go", `Nonexistant`))) }) } @@ -961,7 +964,7 @@ func main() { Run(t, mod, func(t *testing.T, env *Env) { writeGoVim(env, "p/p.go", p) writeGoVim(env, "main.go", main) - env.Await(env.DiagnosticAtRegexp("main.go", "5")) + env.Await(Diagnostics(env.AtRegexp("main.go", "5"))) }) }) @@ -991,9 +994,9 @@ func TestDoIt(t *testing.T) { } `) env.Await( - env.DiagnosticAtRegexp("main.go", "5"), - env.DiagnosticAtRegexp("p/p_test.go", "5"), - env.DiagnosticAtRegexp("p/x_test.go", "5"), + Diagnostics(env.AtRegexp("main.go", "5")), + Diagnostics(env.AtRegexp("p/p_test.go", "5")), + Diagnostics(env.AtRegexp("p/x_test.go", "5")), ) env.RegexpReplace("p/p.go", "s string", "i int") env.AfterChange( @@ -1024,7 +1027,7 @@ func _() { ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.Await( - env.DiagnosticAtRegexp("a/a.go", "x"), + Diagnostics(env.AtRegexp("a/a.go", "x")), ) }) } @@ -1116,7 +1119,7 @@ func main() { } `)) env.Await( - env.DiagnosticAtRegexp("main.go", "x"), + Diagnostics(env.AtRegexp("main.go", "x")), ) }) } @@ -1164,7 +1167,7 @@ func main() { ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.AfterChange( - env.DiagnosticAtRegexp("main.go", "x"), + Diagnostics(env.AtRegexp("main.go", "x")), ) }) WithOptions( @@ -1282,7 +1285,7 @@ func _() { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.Await( - env.DiagnosticAtRegexp("a/a.go", "x"), + Diagnostics(env.AtRegexp("a/a.go", "x")), ) env.OpenFile("a/a_exclude.go") env.Await( @@ -1497,7 +1500,7 @@ func main() { ) env.RemoveWorkspaceFile("bob") env.AfterChange( - env.DiagnosticAtRegexp("cmd/main.go", `"mod.com/bob"`), + Diagnostics(env.AtRegexp("cmd/main.go", `"mod.com/bob"`)), NoDiagnostics(ForFile("bob/bob.go")), NoFileWatchMatching("bob"), ) @@ -1675,7 +1678,7 @@ func helloHelper() {} // Expect a diagnostic in a nested module. env.OpenFile("nested/hello/hello.go") env.AfterChange( - env.DiagnosticAtRegexp("nested/hello/hello.go", "helloHelper"), + Diagnostics(env.AtRegexp("nested/hello/hello.go", "helloHelper")), Diagnostics(env.AtRegexp("nested/hello/hello.go", "package hello"), WithMessage("nested module")), OutstandingWork(lsp.WorkspaceLoadFailure, "nested module"), ) @@ -1716,7 +1719,7 @@ var Bar = Foo Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo.go") - env.AfterChange(env.DiagnosticAtRegexp("bar.go", `Foo`)) + env.AfterChange(Diagnostics(env.AtRegexp("bar.go", `Foo`))) env.RegexpReplace("foo.go", `\+build`, "") env.AfterChange(NoDiagnostics(ForFile("bar.go"))) }) @@ -1747,14 +1750,14 @@ package main env.OpenFile("main.go") env.OpenFile("other.go") env.Await( - env.DiagnosticAtRegexp("main.go", "asdf"), - env.DiagnosticAtRegexp("main.go", "fdas"), + Diagnostics(env.AtRegexp("main.go", "asdf")), + Diagnostics(env.AtRegexp("main.go", "fdas")), ) env.SetBufferContent("other.go", "package main\n\nasdf") // The new diagnostic in other.go should not suppress diagnostics in main.go. env.AfterChange( Diagnostics(env.AtRegexp("other.go", "asdf"), WithMessage("expected declaration")), - env.DiagnosticAtRegexp("main.go", "asdf"), + Diagnostics(env.AtRegexp("main.go", "asdf")), ) }) } @@ -1842,7 +1845,7 @@ func F[T C](_ T) { var d protocol.PublishDiagnosticsParams env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("main.go", `C`), + Diagnostics(env.AtRegexp("main.go", `C`)), ReadDiagnostics("main.go", &d), ) if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { diff --git a/gopls/internal/regtest/diagnostics/undeclared_test.go b/gopls/internal/regtest/diagnostics/undeclared_test.go index bba72c47237..a6289c9745a 100644 --- a/gopls/internal/regtest/diagnostics/undeclared_test.go +++ b/gopls/internal/regtest/diagnostics/undeclared_test.go @@ -46,7 +46,7 @@ func _() int { env.OpenFile("a/a.go") var adiags protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("a/a.go", "x"), + Diagnostics(env.AtRegexp("a/a.go", "x")), ReadDiagnostics("a/a.go", &adiags), ) env.Await() @@ -61,7 +61,7 @@ func _() int { env.OpenFile("b/b.go") var bdiags protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("b/b.go", "y = y"), + Diagnostics(env.AtRegexp("b/b.go", "y = y")), ReadDiagnostics("b/b.go", &bdiags), ) if got := len(bdiags.Diagnostics); got != 1 { diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go index 9fd448cd849..69a28e23132 100644 --- a/gopls/internal/regtest/misc/generate_test.go +++ b/gopls/internal/regtest/misc/generate_test.go @@ -60,7 +60,7 @@ func main() { Run(t, generatedWorkspace, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("main.go", "lib1.(Answer)"), + Diagnostics(env.AtRegexp("main.go", "lib1.(Answer)")), ) env.RunGenerate("./lib1") env.RunGenerate("./lib2") diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index b9804d0c436..513171a1e69 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -156,7 +156,7 @@ var _, _ = x.X, y.Y ProxyFiles(proxy), ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - env.AfterChange(env.DiagnosticAtRegexp("main.go", `y.Y`)) + env.AfterChange(Diagnostics(env.AtRegexp("main.go", `y.Y`))) env.SaveBuffer("main.go") env.AfterChange(NoDiagnostics(ForFile("main.go"))) path, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `y.(Y)`)) @@ -200,7 +200,7 @@ func TestA(t *testing.T) { env.OpenFile("a/a.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("a/a.go", "os.Stat"), + Diagnostics(env.AtRegexp("a/a.go", "os.Stat")), ReadDiagnostics("a/a.go", &d), ) env.ApplyQuickFixes("a/a.go", d.Diagnostics) @@ -247,7 +247,7 @@ func Test() { ` Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("caller/caller.go") - env.AfterChange(env.DiagnosticAtRegexp("caller/caller.go", "a.Test")) + env.AfterChange(Diagnostics(env.AtRegexp("caller/caller.go", "a.Test"))) // Saving caller.go should trigger goimports, which should find a.Test in // the mod.com module, thanks to the go.work file. diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 81b3de8125a..86fd567cde7 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -405,15 +405,15 @@ package b // Initially, we should have diagnostics on both X's, for their duplicate declaration. env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "X"), - env.DiagnosticAtRegexp("a/x.go", "X"), + Diagnostics(env.AtRegexp("a/a.go", "X")), + Diagnostics(env.AtRegexp("a/x.go", "X")), ) // Moving x.go should make the diagnostic go away. env.RenameFile("a/x.go", "b/x.go") env.AfterChange( - NoDiagnostics(ForFile("a/a.go")), // no more duplicate declarations - env.DiagnosticAtRegexp("b/b.go", "package"), // as package names mismatch + NoDiagnostics(ForFile("a/a.go")), // no more duplicate declarations + Diagnostics(env.AtRegexp("b/b.go", "package")), // as package names mismatch ) // Renaming should also work on open buffers. @@ -422,15 +422,15 @@ package b // Moving x.go back to a/ should cause the diagnostics to reappear. env.RenameFile("b/x.go", "a/x.go") env.AfterChange( - env.DiagnosticAtRegexp("a/a.go", "X"), - env.DiagnosticAtRegexp("a/x.go", "X"), + Diagnostics(env.AtRegexp("a/a.go", "X")), + Diagnostics(env.AtRegexp("a/x.go", "X")), ) // Renaming the entire directory should move both the open and closed file. env.RenameFile("a", "x") env.AfterChange( - env.DiagnosticAtRegexp("x/a.go", "X"), - env.DiagnosticAtRegexp("x/x.go", "X"), + Diagnostics(env.AtRegexp("x/a.go", "X")), + Diagnostics(env.AtRegexp("x/x.go", "X")), ) // As a sanity check, verify that x/x.go is open. diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index 422147cb33e..e083faa3912 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -93,7 +93,9 @@ func Add[T int](target T, l []T) []T { Settings{"semanticTokens": true}, ).Run(t, src, func(t *testing.T, env *Env) { env.OpenFile("main.go") - env.AfterChange(env.DiagnosticAtRegexp("main.go", "for range")) + env.AfterChange( + Diagnostics(env.AtRegexp("main.go", "for range")), + ) p := &protocol.SemanticTokensParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: env.Sandbox.Workdir.URI("main.go"), diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index 84eb99f73de..de44167c291 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -54,8 +54,8 @@ func main() { env2.RegexpReplace("main.go", "\\)\n(})", "") // Now check that we got different diagnostics in each environment. - env1.Await(env1.DiagnosticAtRegexp("main.go", "Printl")) - env2.Await(env2.DiagnosticAtRegexp("main.go", "$")) + env1.Await(Diagnostics(env1.AtRegexp("main.go", "Printl"))) + env2.Await(Diagnostics(env2.AtRegexp("main.go", "$"))) // Now close editor #2, and verify that operation in editor #1 is // unaffected. diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index b34ddfc0a12..9d732727ea9 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -485,7 +485,7 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { gotDiagnostics := &protocol.PublishDiagnosticsParams{} env.AfterChange( - env.DiagnosticAtRegexp("go.mod", `golang.org/amod`), + Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)), ReadDiagnostics("go.mod", gotDiagnostics), ) @@ -589,7 +589,7 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { ShownMessage("Found"), ) env.OnceMet( - env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"), + Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")), ReadDiagnostics("go.mod", gotDiagnostics), ) // We expect only one diagnostic for GO-2022-02. @@ -636,7 +636,7 @@ func TestRunVulncheckWarning(t *testing.T) { ) // Vulncheck diagnostics asynchronous to the vulncheck command. env.OnceMet( - env.DiagnosticAtRegexp("go.mod", `golang.org/amod`), + Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)), ReadDiagnostics("go.mod", gotDiagnostics), ) @@ -803,7 +803,7 @@ func TestGovulncheckInfo(t *testing.T) { // Vulncheck diagnostics asynchronous to the vulncheck command. env.OnceMet( - env.DiagnosticAtRegexp("go.mod", "golang.org/bmod"), + Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")), ReadDiagnostics("go.mod", gotDiagnostics), ) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index e88368a3b14..d3df2155b2b 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -93,7 +93,7 @@ func main() { goModContent := env.ReadWorkspaceFile("a/go.mod") env.OpenFile("a/main.go") env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), + Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) @@ -102,7 +102,7 @@ func main() { // Confirm that the go.mod file still does not change. env.SaveBuffer("a/main.go") env.Await( - env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), + Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) @@ -143,7 +143,7 @@ func main() { env.WriteWorkspaceFile("a/main.go", mainContent) env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), + Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) @@ -184,7 +184,7 @@ require example.com v1.2.3 env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", `"example.com/blah"`), + Diagnostics(env.AtRegexp("a/main.go", `"example.com/blah"`)), ReadDiagnostics("a/main.go", &d), ) var goGetDiag protocol.Diagnostic @@ -231,7 +231,7 @@ require random.org v1.2.3 env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", `"random.org/blah"`), + Diagnostics(env.AtRegexp("a/main.go", `"random.org/blah"`)), ReadDiagnostics("a/main.go", &d), ) var randomDiag protocol.Diagnostic @@ -285,7 +285,7 @@ require random.org v1.2.3 env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", `"random.org/blah"`), + Diagnostics(env.AtRegexp("a/main.go", `"random.org/blah"`)), ReadDiagnostics("a/main.go", &d), ) var randomDiag protocol.Diagnostic @@ -334,7 +334,7 @@ require example.com v1.2.3 env.OpenFile("a/go.mod") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("a/go.mod", "// indirect"), + Diagnostics(env.AtRegexp("a/go.mod", "// indirect")), ReadDiagnostics("a/go.mod", &d), ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) @@ -376,7 +376,7 @@ go 1.14 env.OpenFile("a/go.mod") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("a/go.mod", `require example.com`), + Diagnostics(env.AtRegexp("a/go.mod", `require example.com`)), ReadDiagnostics("a/go.mod", &d), ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) @@ -436,7 +436,7 @@ func _() { env.OpenFile("a/main.go") var d protocol.PublishDiagnosticsParams env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", `"github.com/esimov/caire"`), + Diagnostics(env.AtRegexp("a/main.go", `"github.com/esimov/caire"`)), ReadDiagnostics("a/main.go", &d), ) env.ApplyQuickFixes("a/main.go", d.Diagnostics) @@ -481,7 +481,7 @@ func main() { }.Run(t, mod, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/go.mod", "require"), + Diagnostics(env.AtRegexp("a/go.mod", "require")), ) env.RunGoCommandInDir("a", "mod", "tidy") env.AfterChange( @@ -596,7 +596,7 @@ func main() { runner.Run(t, unknown, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") env.Await( - env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"), + Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.2")), ) env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. @@ -615,7 +615,7 @@ func main() { env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. env.AfterChange( NoDiagnostics(ForFile("a/go.mod")), - env.DiagnosticAtRegexp("a/main.go", "x = "), + Diagnostics(env.AtRegexp("a/main.go", "x = ")), ) }) }) @@ -645,17 +645,17 @@ func main() { runner.Run(t, known, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", "x = "), + Diagnostics(env.AtRegexp("a/main.go", "x = ")), ) env.RegexpReplace("a/go.mod", "v1.2.3", "v1.2.2") env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk env.AfterChange( - env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"), + Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.2")), ) env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk env.AfterChange( - env.DiagnosticAtRegexp("a/main.go", "x = "), + Diagnostics(env.AtRegexp("a/main.go", "x = ")), ) }) }) @@ -706,7 +706,7 @@ func main() { }.Run(t, module, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") env.Await( - env.DiagnosticAtRegexp("a/go.mod", "require example.com v1.2.3"), + Diagnostics(env.AtRegexp("a/go.mod", "require example.com v1.2.3")), ) }) } @@ -735,7 +735,7 @@ func main() { env.OpenFile("main.go") original := env.ReadWorkspaceFile("go.mod") env.Await( - env.DiagnosticAtRegexp("main.go", `"example.com/blah"`), + Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), ) got := env.ReadWorkspaceFile("go.mod") if got != original { @@ -820,7 +820,7 @@ func main() { Settings{"buildFlags": []string{"-tags", "bob"}}, ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("main.go", `"example.com/blah"`), + Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), ) }) } @@ -840,7 +840,7 @@ func main() {} env.OpenFile("go.mod") env.RegexpReplace("go.mod", "module", "modul") env.Await( - env.DiagnosticAtRegexp("go.mod", "modul"), + Diagnostics(env.AtRegexp("go.mod", "modul")), ) }) } @@ -981,7 +981,7 @@ func main() {} env.OpenFile("go.mod") d := &protocol.PublishDiagnosticsParams{} env.AfterChange( - env.DiagnosticAtRegexp("go.mod", "require hasdep.com v1.2.3"), + Diagnostics(env.AtRegexp("go.mod", "require hasdep.com v1.2.3")), ReadDiagnostics("go.mod", d), ) const want = `module mod.com diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index f3453d72cf8..c81bf1a1184 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -73,7 +73,7 @@ Hello {{}} <-- missing body var diags protocol.PublishDiagnosticsParams env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"), + Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}")), ReadDiagnostics("hello.tmpl", &diags), ) d := diags.Diagnostics // issue 50786: check for Source @@ -120,7 +120,7 @@ B {{}} <-- missing body ).Run(t, files, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.tmpl", "()A"), + Diagnostics(env.AtRegexp("a/a.tmpl", "()A")), NoDiagnostics(ForFile("b/b.tmpl")), ) }) @@ -140,7 +140,7 @@ go 1.12 NoDiagnostics(ForFile("hello.tmpl")), // Don't get spurious errors for empty templates. ) env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}") - env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) + env.Await(Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}"))) env.RegexpReplace("hello.tmpl", "{{}}", "{{.}}") env.Await(NoDiagnostics(ForFile("hello.tmpl"))) }) @@ -161,7 +161,7 @@ Hello {{}} <-- missing body Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("hello.tmpl") env.AfterChange( - env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}"), + Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}")), ) // Since we don't have templateExtensions configured, closing hello.tmpl // should make its diagnostics disappear. diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index b445f88b332..908648396ef 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -39,7 +39,7 @@ func _() { Run(t, pkg, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "x"), + Diagnostics(env.AtRegexp("a/a.go", "x")), ) env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) env.AfterChange( @@ -59,7 +59,7 @@ func _() { env.AfterChange() env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) env.AfterChange( - env.DiagnosticAtRegexp("a/a.go", "x"), + Diagnostics(env.AtRegexp("a/a.go", "x")), ) }) }) @@ -92,7 +92,7 @@ func _() { env.Await(env.DoneWithOpen()) env.WriteWorkspaceFile("b/b.go", `package b; func B() {};`) env.Await( - env.DiagnosticAtRegexp("a/a.go", "b.B"), + Diagnostics(env.AtRegexp("a/a.go", "b.B")), ) }) } @@ -124,7 +124,7 @@ func _() { Run(t, pkg, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "x"), + Diagnostics(env.AtRegexp("a/a.go", "x")), ) env.WriteWorkspaceFiles(map[string]string{ "b/b.go": `package b; func B() {};`, @@ -170,7 +170,7 @@ func _() { env.Await(env.DoneWithOpen()) env.RemoveWorkspaceFile("b/b.go") env.Await( - env.DiagnosticAtRegexp("a/a.go", "\"mod.com/b\""), + Diagnostics(env.AtRegexp("a/a.go", "\"mod.com/b\"")), ) }) } @@ -200,7 +200,7 @@ func _() { Run(t, missing, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""), + Diagnostics(env.AtRegexp("a/a.go", "\"mod.com/c\"")), ) env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) env.AfterChange( @@ -248,7 +248,7 @@ func _() { Run(t, pkg, func(t *testing.T, env *Env) { env.OnceMet( InitialWorkspaceLoad, - env.DiagnosticAtRegexp("a/a.go", "hello"), + Diagnostics(env.AtRegexp("a/a.go", "hello")), ) env.WriteWorkspaceFile("a/a2.go", `package a; func hello() {};`) env.AfterChange( @@ -392,7 +392,7 @@ package a env.RemoveWorkspaceFile("a/a_unneeded.go") env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") env.AfterChange( - env.DiagnosticAtRegexp("a/a.go", "fmt"), + Diagnostics(env.AtRegexp("a/a.go", "fmt")), ) env.SaveBuffer("a/a.go") env.AfterChange( @@ -420,7 +420,7 @@ package a env.CloseBuffer("a/a_unneeded.go") env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") env.Await( - env.DiagnosticAtRegexp("a/a.go", "fmt"), + Diagnostics(env.AtRegexp("a/a.go", "fmt")), ) env.SaveBuffer("a/a.go") env.AfterChange( @@ -621,7 +621,7 @@ func main() { env.OpenFile("foo/main.go") env.RemoveWorkspaceFile("foo/go.mod") env.AfterChange( - env.DiagnosticAtRegexp("foo/main.go", `"mod.com/blah"`), + Diagnostics(env.AtRegexp("foo/main.go", `"mod.com/blah"`)), ) env.RegexpReplace("foo/main.go", `"mod.com/blah"`, `"foo/blah"`) env.AfterChange( diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index 0c718b339ea..5c5a68ae1dd 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -161,7 +161,7 @@ const F = named.D - 3 Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("p/internal/bar/bar.go") env.AfterChange( - env.DiagnosticAtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\""), + Diagnostics(env.AtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\"")), ) env.OpenFile("go.mod") env.RegexpReplace("go.mod", "mod.testx", "mod.test") @@ -207,8 +207,8 @@ package b env.OpenFile("a/empty.go") env.OpenFile("b/go.mod") env.AfterChange( - env.DiagnosticAtRegexp("a/a.go", "package a"), - env.DiagnosticAtRegexp("b/go.mod", "module b.com"), + Diagnostics(env.AtRegexp("a/a.go", "package a")), + Diagnostics(env.AtRegexp("b/go.mod", "module b.com")), OutstandingWork(lsp.WorkspaceLoadFailure, msg), ) @@ -238,9 +238,9 @@ package b env.OpenFile("b/b.go") env.AfterChange( // TODO(rfindley): fix these missing diagnostics. - // env.DiagnosticAtRegexp("a/a.go", "package a"), - // env.DiagnosticAtRegexp("b/go.mod", "module b.com"), - env.DiagnosticAtRegexp("b/b.go", "package b"), + // Diagnostics(env.AtRegexp("a/a.go", "package a")), + // Diagnostics(env.AtRegexp("b/go.mod", "module b.com")), + Diagnostics(env.AtRegexp("b/b.go", "package b")), OutstandingWork(lsp.WorkspaceLoadFailure, msg), ) }) diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go index f7c7153ec0e..5db8d897cf8 100644 --- a/gopls/internal/regtest/workspace/directoryfilters_test.go +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -135,7 +135,7 @@ package exclude ProxyFiles(proxy), Settings{"directoryFilters": []string{"-exclude"}}, ).Run(t, files, func(t *testing.T, env *Env) { - env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`)) + env.Await(Diagnostics(env.AtRegexp("include/include.go", `exclude.(X)`))) }) } diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index 31663fbeb07..11dfe670c42 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -73,8 +73,8 @@ func main() {} env.OpenFile("bar.go") env.OnceMet( env.DoneWithOpen(), - env.DiagnosticAtRegexp("foo.go", "func (main)"), - env.DiagnosticAtRegexp("bar.go", "func (main)"), + Diagnostics(env.AtRegexp("foo.go", "func (main)")), + Diagnostics(env.AtRegexp("bar.go", "func (main)")), ) // Ignore bar.go. This should resolve diagnostics. diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go index 7056f5464bb..d7f03469fc6 100644 --- a/gopls/internal/regtest/workspace/standalone_test.go +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -131,7 +131,7 @@ func main() { // Renaming "lib.C" to "lib.D" should cause a diagnostic in the standalone // file. env.RegexpReplace("lib/lib.go", "C", "D") - env.AfterChange(env.DiagnosticAtRegexp("lib/ignore.go", "lib.(C)")) + env.AfterChange(Diagnostics(env.AtRegexp("lib/ignore.go", "lib.(C)"))) // Undoing the replacement should fix diagnostics env.RegexpReplace("lib/lib.go", "D", "C") @@ -186,7 +186,7 @@ func main() {} env.OpenFile("standalone.go") env.AfterChange( - env.DiagnosticAtRegexp("ignore.go", "package (main)"), + Diagnostics(env.AtRegexp("ignore.go", "package (main)")), NoDiagnostics(ForFile("standalone.go")), ) @@ -202,7 +202,7 @@ func main() {} env.AfterChange( NoDiagnostics(ForFile("ignore.go")), - env.DiagnosticAtRegexp("standalone.go", "package (main)"), + Diagnostics(env.AtRegexp("standalone.go", "package (main)")), ) }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 3932648bdc1..0573455f396 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -152,7 +152,7 @@ func TestClearAnalysisDiagnostics(t *testing.T) { ).Run(t, workspaceModule, func(t *testing.T, env *Env) { env.OpenFile("pkg/main.go") env.AfterChange( - env.DiagnosticAtRegexp("pkg/main2.go", "fmt.Print"), + Diagnostics(env.AtRegexp("pkg/main2.go", "fmt.Print")), ) env.CloseBuffer("pkg/main.go") env.AfterChange( @@ -267,8 +267,8 @@ func Hello() int { env.RunGoCommand("work", "init") env.RunGoCommand("work", "use", "-r", ".") env.AfterChange( - env.DiagnosticAtRegexp("moda/a/a.go", "x"), - env.DiagnosticAtRegexp("modb/b/b.go", "x"), + Diagnostics(env.AtRegexp("moda/a/a.go", "x")), + Diagnostics(env.AtRegexp("modb/b/b.go", "x")), NoDiagnostics(env.AtRegexp("moda/a/a.go", `"b.com/b"`)), ) }) @@ -324,7 +324,7 @@ func main() { ProxyFiles(proxy), ).Run(t, multiModule, func(t *testing.T, env *Env) { env.Await( - env.DiagnosticAtRegexp("main.go", "x"), + Diagnostics(env.AtRegexp("main.go", "x")), ) }) } @@ -452,7 +452,7 @@ func Hello() int { } `, }) - env.AfterChange(env.DiagnosticAtRegexp("modb/b/b.go", "x")) + env.AfterChange(Diagnostics(env.AtRegexp("modb/b/b.go", "x"))) got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) if want := "modb/b/b.go"; !strings.HasSuffix(got, want) { t.Errorf("expected %s, got %v", want, original) @@ -508,7 +508,7 @@ func Hello() int { env.RegexpReplace("modb/go.mod", "modul", "module") env.SaveBufferWithoutActions("modb/go.mod") env.Await( - env.DiagnosticAtRegexp("modb/b/b.go", "x"), + Diagnostics(env.AtRegexp("modb/b/b.go", "x")), ) }) } @@ -607,7 +607,7 @@ use ( // As of golang/go#54069, writing go.work to the workspace triggers a // workspace reload. env.AfterChange( - env.DiagnosticAtRegexp("modb/b/b.go", "x"), + Diagnostics(env.AtRegexp("modb/b/b.go", "x")), ) // Jumping to definition should now go to b.com in the workspace. @@ -905,10 +905,10 @@ func main() { InitialWorkspaceLoad, // TODO(rfindley): assert on the full set of diagnostics here. We // should ensure that we don't have a diagnostic at b.Hi in a.go. - env.DiagnosticAtRegexp("moda/a/a.go", "x"), - env.DiagnosticAtRegexp("modb/b/b.go", "x"), - env.DiagnosticAtRegexp("modb/v2/b/b.go", "x"), - env.DiagnosticAtRegexp("modc/main.go", "x"), + Diagnostics(env.AtRegexp("moda/a/a.go", "x")), + Diagnostics(env.AtRegexp("modb/b/b.go", "x")), + Diagnostics(env.AtRegexp("modb/v2/b/b.go", "x")), + Diagnostics(env.AtRegexp("modc/main.go", "x")), ) }) } @@ -1114,8 +1114,8 @@ func (Server) Foo() {} // as invalid. So we need to wait for the metadata of main_test.go to be // updated before moving other_test.go back to the main_test package. env.Await( - env.DiagnosticAtRegexp("other_test.go", "Server"), - env.DiagnosticAtRegexp("main_test.go", "otherConst"), + Diagnostics(env.AtRegexp("other_test.go", "Server")), + Diagnostics(env.AtRegexp("main_test.go", "otherConst")), ) env.RegexpReplace("other_test.go", "main", "main_test") env.AfterChange( From 9ba8bb1a1238e96143a9d2287fc4c9a4e9bad164 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 23:37:30 -0500 Subject: [PATCH 630/723] gopls/internal/regtest: clean up workspace symbol helpers Consolidate the redundant env.Symbol and env.WorkspaceSymbol wrappers, and eliminate the unnecessary wrapper types. Updates golang/go#39384 Change-Id: Ibe3b7ca89c531a914e5044a7dc45ac30e7210f1b Reviewed-on: https://go-review.googlesource.com/c/tools/+/461897 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/fake/edit.go | 15 ------------- gopls/internal/lsp/fake/editor.go | 22 ++----------------- gopls/internal/lsp/regtest/wrappers.go | 16 +++----------- .../regtest/bench/workspace_symbols_test.go | 4 ++-- .../regtest/misc/workspace_symbol_test.go | 16 +++++++------- .../workspace/directoryfilters_test.go | 4 ++-- .../regtest/workspace/standalone_test.go | 4 ++-- 7 files changed, 19 insertions(+), 62 deletions(-) diff --git a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go index e7eeca14662..0b688e7d716 100644 --- a/gopls/internal/lsp/fake/edit.go +++ b/gopls/internal/lsp/fake/edit.go @@ -9,21 +9,6 @@ import ( "golang.org/x/tools/internal/diff" ) -// Location is the editor friendly equivalent of protocol.Location -type Location struct { - Path string - Range protocol.Range -} - -// SymbolInformation is an editor friendly version of -// protocol.SymbolInformation, with location information transformed to byte -// offsets. Field names correspond to the protocol type. -type SymbolInformation struct { - Name string - Kind protocol.SymbolKind - Location Location -} - // NewEdit creates an edit replacing all content between // (startLine, startColumn) and (endLine, endColumn) with text. func NewEdit(startLine, startColumn, endLine, endColumn uint32, text string) protocol.TextEdit { diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 00bba62997c..7a61785298c 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -815,29 +815,11 @@ func (e *Editor) extractFirstPathAndPos(ctx context.Context, locs []protocol.Loc } // Symbol performs a workspace symbol search using query -func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, error) { +func (e *Editor) Symbol(ctx context.Context, query string) ([]protocol.SymbolInformation, error) { params := &protocol.WorkspaceSymbolParams{} params.Query = query - resp, err := e.Server.Symbol(ctx, params) - if err != nil { - return nil, fmt.Errorf("symbol: %w", err) - } - var res []SymbolInformation - for _, si := range resp { - ploc := si.Location - path := e.sandbox.Workdir.URIToPath(ploc.URI) - loc := Location{ - Path: path, - Range: ploc.Range, - } - res = append(res, SymbolInformation{ - Name: si.Name, - Kind: si.Kind, - Location: loc, - }) - } - return res, nil + return e.Server.Symbol(ctx, params) } // OrganizeImports requests and performs the source.organizeImports codeAction. diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index 3699597bd71..03f3551ee5f 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -178,16 +178,6 @@ func (e *Env) GoToDefinition(name string, pos protocol.Position) (string, protoc return n, p } -// Symbol returns symbols matching query -func (e *Env) Symbol(query string) []fake.SymbolInformation { - e.T.Helper() - r, err := e.Editor.Symbol(e.Ctx, query) - if err != nil { - e.T.Fatal(err) - } - return r -} - // FormatBuffer formats the editor buffer, calling t.Fatal on any error. func (e *Env) FormatBuffer(name string) { e.T.Helper() @@ -394,10 +384,10 @@ func (e *Env) InlayHints(path string) []protocol.InlayHint { return hints } -// WorkspaceSymbol calls workspace/symbol -func (e *Env) WorkspaceSymbol(sym string) []protocol.SymbolInformation { +// Symbol calls workspace/symbol +func (e *Env) Symbol(query string) []protocol.SymbolInformation { e.T.Helper() - ans, err := e.Editor.Symbols(e.Ctx, sym) + ans, err := e.Editor.Symbols(e.Ctx, query) if err != nil { e.T.Fatal(err) } diff --git a/gopls/internal/regtest/bench/workspace_symbols_test.go b/gopls/internal/regtest/bench/workspace_symbols_test.go index fccc8182997..a540dfd2cd0 100644 --- a/gopls/internal/regtest/bench/workspace_symbols_test.go +++ b/gopls/internal/regtest/bench/workspace_symbols_test.go @@ -18,7 +18,7 @@ func BenchmarkWorkspaceSymbols(b *testing.B) { env := benchmarkEnv(b) // Make an initial symbol query to warm the cache. - symbols := env.WorkspaceSymbol(*symbolQuery) + symbols := env.Symbol(*symbolQuery) if testing.Verbose() { fmt.Println("Results:") @@ -30,6 +30,6 @@ func BenchmarkWorkspaceSymbols(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - env.WorkspaceSymbol(*symbolQuery) + env.Symbol(*symbolQuery) } } diff --git a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go index eb150f6094e..33b8147afe4 100644 --- a/gopls/internal/regtest/misc/workspace_symbol_test.go +++ b/gopls/internal/regtest/misc/workspace_symbol_test.go @@ -34,7 +34,7 @@ const C2 = "exclude.go" Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a.go") - syms := env.WorkspaceSymbol("C") + syms := env.Symbol("C") if got, want := len(syms), 1; got != want { t.Errorf("got %d symbols, want %d", got, want) } @@ -42,7 +42,7 @@ const C2 = "exclude.go" // Opening up an ignored file will result in an overlay with missing // metadata, but this shouldn't break workspace symbols requests. env.OpenFile("exclude.go") - syms = env.WorkspaceSymbol("C") + syms = env.Symbol("C") if got, want := len(syms), 1; got != want { t.Errorf("got %d symbols, want %d", got, want) } @@ -78,7 +78,7 @@ const ( "Fooey", // shorter than Fooest, Foobar "Fooest", } - got := env.WorkspaceSymbol("Foo") + got := env.Symbol("Foo") compareSymbols(t, got, want) }) } @@ -102,11 +102,11 @@ const ( WithOptions( Settings{"symbolMatcher": symbolMatcher}, ).Run(t, files, func(t *testing.T, env *Env) { - compareSymbols(t, env.WorkspaceSymbol("ABC"), []string{"ABC", "AxxBxxCxx"}) - compareSymbols(t, env.WorkspaceSymbol("'ABC"), []string{"ABC"}) - compareSymbols(t, env.WorkspaceSymbol("^mod.com"), []string{"mod.com/a.ABC", "mod.com/a.AxxBxxCxx"}) - compareSymbols(t, env.WorkspaceSymbol("^mod.com Axx"), []string{"mod.com/a.AxxBxxCxx"}) - compareSymbols(t, env.WorkspaceSymbol("C$"), []string{"ABC"}) + compareSymbols(t, env.Symbol("ABC"), []string{"ABC", "AxxBxxCxx"}) + compareSymbols(t, env.Symbol("'ABC"), []string{"ABC"}) + compareSymbols(t, env.Symbol("^mod.com"), []string{"mod.com/a.ABC", "mod.com/a.AxxBxxCxx"}) + compareSymbols(t, env.Symbol("^mod.com Axx"), []string{"mod.com/a.AxxBxxCxx"}) + compareSymbols(t, env.Symbol("C$"), []string{"ABC"}) }) } diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go index 5db8d897cf8..6e2a15557fd 100644 --- a/gopls/internal/regtest/workspace/directoryfilters_test.go +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -27,7 +27,7 @@ func TestDirectoryFilters(t *testing.T) { "directoryFilters": []string{"-inner"}, }, ).Run(t, workspaceModule, func(t *testing.T, env *Env) { - syms := env.WorkspaceSymbol("Hi") + syms := env.Symbol("Hi") sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) for _, s := range syms { if strings.Contains(s.ContainerName, "inner") { @@ -149,7 +149,7 @@ func TestDirectoryFilters_Wildcard(t *testing.T) { "directoryFilters": filters, }, ).Run(t, workspaceModule, func(t *testing.T, env *Env) { - syms := env.WorkspaceSymbol("Bye") + syms := env.Symbol("Bye") sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) for _, s := range syms { if strings.Contains(s.ContainerName, "bye") { diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go index d7f03469fc6..c9618eb3aeb 100644 --- a/gopls/internal/regtest/workspace/standalone_test.go +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -54,7 +54,7 @@ func main() { ).Run(t, files, func(t *testing.T, env *Env) { // Initially, gopls should not know about the standalone file as it hasn't // been opened. Therefore, we should only find one symbol 'C'. - syms := env.WorkspaceSymbol("C") + syms := env.Symbol("C") if got, want := len(syms), 1; got != want { t.Errorf("got %d symbols, want %d", got, want) } @@ -95,7 +95,7 @@ func main() { // Having opened the standalone file, we should find its symbols in the // workspace. - syms = env.WorkspaceSymbol("C") + syms = env.Symbol("C") if got, want := len(syms), 2; got != want { t.Fatalf("got %d symbols, want %d", got, want) } From 1e819a3cf3ded3a6efd6b5df42ec269728c5f134 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 13 Jan 2023 10:12:54 -0500 Subject: [PATCH 631/723] gopls/internal/regtest: follow-ups to review comments from earlier CLs For convenience, this CL addresses review comments from earlier CLs in the stack. Change-Id: I28581c77170aa5e3978b28c4c7c7c85e6cbf506f Reviewed-on: https://go-review.googlesource.com/c/tools/+/461943 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/fake/edit.go | 4 ++- gopls/internal/lsp/fake/editor.go | 4 +-- gopls/internal/lsp/regtest/env.go | 15 +++++---- gopls/internal/lsp/regtest/expectation.go | 33 ++++++++++++++++--- .../regtest/misc/workspace_symbol_test.go | 14 ++++---- 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go index 0b688e7d716..5fd65b0c855 100644 --- a/gopls/internal/lsp/fake/edit.go +++ b/gopls/internal/lsp/fake/edit.go @@ -9,8 +9,10 @@ import ( "golang.org/x/tools/internal/diff" ) -// NewEdit creates an edit replacing all content between +// NewEdit creates an edit replacing all content between the 0-based // (startLine, startColumn) and (endLine, endColumn) with text. +// +// Columns measure UTF-16 codes. func NewEdit(startLine, startColumn, endLine, endColumn uint32, text string) protocol.TextEdit { return protocol.TextEdit{ Range: protocol.Range{ diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 7a61785298c..c9214a8aa6d 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -816,9 +816,7 @@ func (e *Editor) extractFirstPathAndPos(ctx context.Context, locs []protocol.Loc // Symbol performs a workspace symbol search using query func (e *Editor) Symbol(ctx context.Context, query string) ([]protocol.SymbolInformation, error) { - params := &protocol.WorkspaceSymbolParams{} - params.Query = query - + params := &protocol.WorkspaceSymbolParams{Query: query} return e.Server.Symbol(ctx, params) } diff --git a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go index ad606587b0a..67e72072477 100644 --- a/gopls/internal/lsp/regtest/env.go +++ b/gopls/internal/lsp/regtest/env.go @@ -297,17 +297,17 @@ func checkExpectations(s State, expectations []Expectation) (Verdict, string) { if v > finalVerdict { finalVerdict = v } - summary.WriteString(fmt.Sprintf("%v: %s\n", v, e.Description)) + fmt.Fprintf(&summary, "%v: %s\n", v, e.Description) } return finalVerdict, summary.String() } // Await blocks until the given expectations are all simultaneously met. // -// Generally speaking Await should be avoided because it can block indefinitely -// if gopls ends up in a state where the expectations are never going to be -// met. Use AfterChange or OnceMet instead, so that the runner knows when to -// stop waiting. +// Generally speaking Await should be avoided because it blocks indefinitely if +// gopls ends up in a state where the expectations are never going to be met. +// Use AfterChange or OnceMet instead, so that the runner knows when to stop +// waiting. func (e *Env) Await(expectations ...Expectation) { e.T.Helper() if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil { @@ -315,8 +315,9 @@ func (e *Env) Await(expectations ...Expectation) { } } -// OnceMet blocks until precondition is met or unmeetable; if the precondition -// is met, it atomically checks that all expectations in mustMeets are met. +// OnceMet blocks until the precondition is met by the state or becomes +// unmeetable. If it was met, OnceMet checks that the state meets all +// expectations in mustMeets. func (e *Env) OnceMet(precondition Expectation, mustMeets ...Expectation) { e.Await(OnceMet(precondition, mustMeets...)) } diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index d0896ff7e2d..6d3f3340a31 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -49,9 +49,25 @@ func (v Verdict) String() string { return fmt.Sprintf("unrecognized verdict %d", v) } -// Expectation holds an arbitrary check func, and implements the Expectation interface. +// An Expectation is an expected property of the state of the LSP client. +// The Check function reports whether the property is met. +// +// Expectations are combinators. By composing them, tests may express +// complex expectations in terms of simpler ones. +// +// TODO(rfindley): as expectations are combined, it becomes harder to identify +// why they failed. A better signature for Check would be +// +// func(State) (Verdict, string) +// +// returning a reason for the verdict that can be composed similarly to +// descriptions. type Expectation struct { - Check func(State) Verdict + Check func(State) Verdict + + // Description holds a noun-phrase identifying what the expectation checks. + // + // TODO(rfindley): revisit existing descriptions to ensure they compose nicely. Description string } @@ -135,8 +151,11 @@ func AllOf(allOf ...Expectation) Expectation { } } -// ReadDiagnostics is an 'expectation' that is used to read diagnostics -// atomically. It is intended to be used with 'OnceMet'. +// ReadDiagnostics is an Expectation that stores the current diagnostics for +// fileName in into, whenever it is evaluated. +// +// It can be used in combination with OnceMet or AfterChange to capture the +// state of diagnostics when other expectations are satisfied. func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation { check := func(s State) Verdict { diags, ok := s.diagnostics[fileName] @@ -695,7 +714,7 @@ func FromSource(source string) DiagnosticFilter { func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { pos := e.RegexpSearch(name, pattern) return DiagnosticFilter{ - desc: fmt.Sprintf("at the first position matching %q in %q", pattern, name), + desc: fmt.Sprintf("at the first position matching %#q in %q", pattern, name), check: func(diagName string, d protocol.Diagnostic) bool { return diagName == name && d.Range.Start == pos }, @@ -704,6 +723,10 @@ func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { // AtPosition filters to diagnostics at location name:line:character, for a // sandbox-relative path name. +// +// Line and character are 0-based, and character measures UTF-16 codes. +// +// Note: prefer the more readable AtRegexp. func AtPosition(name string, line, character uint32) DiagnosticFilter { pos := protocol.Position{Line: line, Character: character} return DiagnosticFilter{ diff --git a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go index 33b8147afe4..a492e1d4985 100644 --- a/gopls/internal/regtest/misc/workspace_symbol_test.go +++ b/gopls/internal/regtest/misc/workspace_symbol_test.go @@ -79,7 +79,7 @@ const ( "Fooest", } got := env.Symbol("Foo") - compareSymbols(t, got, want) + compareSymbols(t, got, want...) }) } @@ -102,15 +102,15 @@ const ( WithOptions( Settings{"symbolMatcher": symbolMatcher}, ).Run(t, files, func(t *testing.T, env *Env) { - compareSymbols(t, env.Symbol("ABC"), []string{"ABC", "AxxBxxCxx"}) - compareSymbols(t, env.Symbol("'ABC"), []string{"ABC"}) - compareSymbols(t, env.Symbol("^mod.com"), []string{"mod.com/a.ABC", "mod.com/a.AxxBxxCxx"}) - compareSymbols(t, env.Symbol("^mod.com Axx"), []string{"mod.com/a.AxxBxxCxx"}) - compareSymbols(t, env.Symbol("C$"), []string{"ABC"}) + compareSymbols(t, env.Symbol("ABC"), "ABC", "AxxBxxCxx") + compareSymbols(t, env.Symbol("'ABC"), "ABC") + compareSymbols(t, env.Symbol("^mod.com"), "mod.com/a.ABC", "mod.com/a.AxxBxxCxx") + compareSymbols(t, env.Symbol("^mod.com Axx"), "mod.com/a.AxxBxxCxx") + compareSymbols(t, env.Symbol("C$"), "ABC") }) } -func compareSymbols(t *testing.T, got []protocol.SymbolInformation, want []string) { +func compareSymbols(t *testing.T, got []protocol.SymbolInformation, want ...string) { t.Helper() if len(got) != len(want) { t.Errorf("got %d symbols, want %d", len(got), len(want)) From 03eac8179ffe6c450672516551213b3426ba06a6 Mon Sep 17 00:00:00 2001 From: Michael Pratt Date: Fri, 13 Jan 2023 10:09:50 -0500 Subject: [PATCH 632/723] go/expect: remove testdata go.mod to go.fake.mod go.mod defines a module boundary, making the go/expect test fail when run as a dependency of another module because the testdata is missing. Rename go.mod to go.fake.mod to keep it in the parent module. go/expect will still parse it as a mod file. Fixes golang/go#57783. Change-Id: Ib17af058e26042bf5148e9aa11fc3d7f9b9c6e7f Reviewed-on: https://go-review.googlesource.com/c/tools/+/461942 Auto-Submit: Michael Pratt Reviewed-by: Bryan Mills Run-TryBot: Michael Pratt Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- go/expect/expect_test.go | 2 +- go/expect/testdata/go.fake.mod | 9 +++++++++ go/expect/testdata/go.mod | 5 ----- 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 go/expect/testdata/go.fake.mod delete mode 100644 go/expect/testdata/go.mod diff --git a/go/expect/expect_test.go b/go/expect/expect_test.go index bd25ef831e2..e9ae40f7e09 100644 --- a/go/expect/expect_test.go +++ b/go/expect/expect_test.go @@ -43,7 +43,7 @@ func TestMarker(t *testing.T) { }, }, { - filename: "testdata/go.mod", + filename: "testdata/go.fake.mod", expectNotes: 2, expectMarkers: map[string]string{ "αMarker": "αfake1α", diff --git a/go/expect/testdata/go.fake.mod b/go/expect/testdata/go.fake.mod new file mode 100644 index 00000000000..ca84fcee9f3 --- /dev/null +++ b/go/expect/testdata/go.fake.mod @@ -0,0 +1,9 @@ +// This file is named go.fake.mod so it does not define a real module, which +// would make the contents of this directory unavailable to the test when run +// from outside the repository. + +module αfake1α //@mark(αMarker, "αfake1α") + +go 1.14 + +require golang.org/modfile v0.0.0 //@mark(βMarker, "require golang.org/modfile v0.0.0") diff --git a/go/expect/testdata/go.mod b/go/expect/testdata/go.mod deleted file mode 100644 index d0323eae6a1..00000000000 --- a/go/expect/testdata/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module αfake1α //@mark(αMarker, "αfake1α") - -go 1.14 - -require golang.org/modfile v0.0.0 //@mark(βMarker, "require golang.org/modfile v0.0.0") From e035d0c426c8338563a8171efe196487cb8dcadb Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Sat, 14 Jan 2023 11:37:19 +0700 Subject: [PATCH 633/723] go/ssa: fix phi node type in slice to array conversion The slice to array conversion is implemented as: if val == nil && len(typ) == 0 { ptr = &[0]T{} } else { ptr = SliceToArrayPointer(val) } v = *ptr There's a Phi node to combine values in two branches. But that phi node must have type *[N]T, but is currently marked [N]T, causing the panic on dereference operation. Fixes golang/go#57790 Change-Id: I245823729b704dd80020fae37cc948e410f3d8a7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462175 gopls-CI: kokoro Reviewed-by: Tim King Run-TryBot: Cuong Manh Le Reviewed-by: Alan Donovan Auto-Submit: Cuong Manh Le TryBot-Result: Gopher Robot --- go/ssa/emit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/ssa/emit.go b/go/ssa/emit.go index e7cd6261dcd..f52d87a16a1 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -525,7 +525,7 @@ func emitSliceToArray(f *Function, val Value, typ types.Type) Value { phi := &Phi{Edges: []Value{zero, ptr}, Comment: "slicetoarray"} phi.pos = val.Pos() - phi.setType(typ) + phi.setType(ptype) x := f.emit(phi) unOp := &UnOp{Op: token.MUL, X: x} unOp.setType(typ) From 8e9496736149dcd86fcd8362f3e095ed557f1a17 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Mon, 16 Jan 2023 08:23:56 -0500 Subject: [PATCH 634/723] cmd/fiximports: do not assume go list -json unmarshals into build.Package Work planned for golang/go#56986 adds fields to go list -json and build.Package of different types, but this program assumes that you can unmarshal go list -json output into a build.Package. We've never promised that, and I'm unaware of any other programs that do it, so fix this program. (If many programs made this assumption, we'd want to consider whether they need to stay compatible, but if it's just this old program, it's not worth fixing.) Change-Id: I8b402bb3e3db7ecff961e1f033f6c5cf967b8578 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461658 Reviewed-by: Alan Donovan Run-TryBot: Russ Cox TryBot-Result: Gopher Robot Auto-Submit: Russ Cox gopls-CI: kokoro --- cmd/fiximports/main.go | 43 +++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/cmd/fiximports/main.go b/cmd/fiximports/main.go index 82e5fb57296..515d4a6ca92 100644 --- a/cmd/fiximports/main.go +++ b/cmd/fiximports/main.go @@ -72,11 +72,9 @@ import ( "flag" "fmt" "go/ast" - "go/build" "go/format" "go/parser" "go/token" - exec "golang.org/x/sys/execabs" "io" "io/ioutil" "log" @@ -86,6 +84,8 @@ import ( "sort" "strconv" "strings" + + exec "golang.org/x/sys/execabs" ) // flags @@ -137,16 +137,16 @@ type canonicalName struct{ path, name string } // Invariant: a false result implies an error was already printed. func fiximports(packages ...string) bool { // importedBy is the transpose of the package import graph. - importedBy := make(map[string]map[*build.Package]bool) + importedBy := make(map[string]map[*listPackage]bool) // addEdge adds an edge to the import graph. - addEdge := func(from *build.Package, to string) { + addEdge := func(from *listPackage, to string) { if to == "C" || to == "unsafe" { return // fake } pkgs := importedBy[to] if pkgs == nil { - pkgs = make(map[*build.Package]bool) + pkgs = make(map[*listPackage]bool) importedBy[to] = pkgs } pkgs[from] = true @@ -162,7 +162,7 @@ func fiximports(packages ...string) bool { // packageName maps each package's path to its name. packageName := make(map[string]string) for _, p := range pkgs { - packageName[p.ImportPath] = p.Package.Name + packageName[p.ImportPath] = p.Name } // canonical maps each non-canonical package path to @@ -207,21 +207,21 @@ func fiximports(packages ...string) bool { } for _, imp := range p.Imports { - addEdge(&p.Package, imp) + addEdge(p, imp) } for _, imp := range p.TestImports { - addEdge(&p.Package, imp) + addEdge(p, imp) } for _, imp := range p.XTestImports { - addEdge(&p.Package, imp) + addEdge(p, imp) } // Does package have an explicit import comment? if p.ImportComment != "" { if p.ImportComment != p.ImportPath { canonical[p.ImportPath] = canonicalName{ - path: p.Package.ImportComment, - name: p.Package.Name, + path: p.ImportComment, + name: p.Name, } } } else { @@ -273,7 +273,7 @@ func fiximports(packages ...string) bool { // Find all clients (direct importers) of canonical packages. // These are the packages that need fixing up. - clients := make(map[*build.Package]bool) + clients := make(map[*listPackage]bool) for path := range canonical { for client := range importedBy[path] { clients[client] = true @@ -350,7 +350,7 @@ func fiximports(packages ...string) bool { } // Invariant: false result => error already printed. -func rewritePackage(client *build.Package, canonical map[string]canonicalName) bool { +func rewritePackage(client *listPackage, canonical map[string]canonicalName) bool { ok := true used := make(map[string]bool) @@ -450,11 +450,20 @@ func rewriteFile(filename string, canonical map[string]canonicalName, used map[s return nil } -// listPackage is a copy of cmd/go/list.Package. -// It has more fields than build.Package and we need some of them. +// listPackage corresponds to the output of go list -json, +// but only the fields we need. type listPackage struct { - build.Package - Error *packageError // error loading package + Name string + Dir string + ImportPath string + GoFiles []string + TestGoFiles []string + XTestGoFiles []string + Imports []string + TestImports []string + XTestImports []string + ImportComment string + Error *packageError // error loading package } // A packageError describes an error loading information about a package. From 6f095b4f2ae61fc6789e92f8b43cf0541e23531c Mon Sep 17 00:00:00 2001 From: Zvonimir Pavlinovic Date: Thu, 12 Jan 2023 06:21:35 -0800 Subject: [PATCH 635/723] go/callgraph/vta: add flows for receiver function types When creating type propagation graph, we don't add edges between the interface object i in i.Foo() and receiver objects of callees provided by the initial call graph. The intuition is that receiver objects are of concrete type so nothing can flow to them. This is not true when the receiver type of a callee is a named function type. If value of such type flows to i, then it should flow to the receiver r. Then, the callee can in principle invoke r(). We currently resolve r() to an empty set of callees. The fix is to add edges between i and r when r is a named function type. Fixes golang/go#57756 Change-Id: I25427e29bad35db3ae4fe4919a72155d84c7a9f3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461604 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Tim King Run-TryBot: Zvonimir Pavlinovic --- go/callgraph/vta/graph.go | 30 ++++++--- .../vta/testdata/src/callgraph_issue_57756.go | 67 +++++++++++++++++++ go/callgraph/vta/vta_test.go | 1 + 3 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 go/callgraph/vta/testdata/src/callgraph_issue_57756.go diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 2ad0f89fd83..ebb497149e1 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -327,14 +327,16 @@ func (b *builder) instr(instr ssa.Instruction) { // change type command a := A(b) results in a and b being the // same value. For concrete type A, there is no interesting flow. // - // Note: When A is an interface, most interface casts are handled + // When A is an interface, most interface casts are handled // by the ChangeInterface instruction. The relevant case here is // when converting a pointer to an interface type. This can happen // when the underlying interfaces have the same method set. - // type I interface{ foo() } - // type J interface{ foo() } - // var b *I - // a := (*J)(b) + // + // type I interface{ foo() } + // type J interface{ foo() } + // var b *I + // a := (*J)(b) + // // When this happens we add flows between a <--> b. b.addInFlowAliasEdges(b.nodeFromVal(i), b.nodeFromVal(i.X)) case *ssa.TypeAssert: @@ -588,14 +590,22 @@ func addArgumentFlows(b *builder, c ssa.CallInstruction, f *ssa.Function) { return } cc := c.Common() + if cc.Method != nil { + // In principle we don't add interprocedural flows for receiver + // objects. At a call site, the receiver object is interface + // while the callee object is concrete. The flow from interface + // to concrete type in general does not make sense. The exception + // is when the concrete type is a named function type (see #57756). + // + // The flow other way around would bake in information from the + // initial call graph. + if isFunction(f.Params[0].Type()) { + b.addInFlowEdge(b.nodeFromVal(cc.Value), b.nodeFromVal(f.Params[0])) + } + } offset := 0 if cc.Method != nil { - // We don't add interprocedural flows for receiver objects. - // At a call site, the receiver object is interface while the - // callee object is concrete. The flow from interface to - // concrete type does not make sense. The flow other way around - // would bake in information from the initial call graph. offset = 1 } for i, v := range cc.Args { diff --git a/go/callgraph/vta/testdata/src/callgraph_issue_57756.go b/go/callgraph/vta/testdata/src/callgraph_issue_57756.go new file mode 100644 index 00000000000..e18f16eba01 --- /dev/null +++ b/go/callgraph/vta/testdata/src/callgraph_issue_57756.go @@ -0,0 +1,67 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// go:build ignore + +package testdata + +// Test that the values of a named function type are correctly +// flowing from interface objects i in i.Foo() to the receiver +// parameters of callees of i.Foo(). + +type H func() + +func (h H) Do() { + h() +} + +type I interface { + Do() +} + +func Bar() I { + return H(func() {}) +} + +func For(g G) { + b := Bar() + b.Do() + + g[0] = b + g.Goo() +} + +type G []I + +func (g G) Goo() { + g[0].Do() +} + +// Relevant SSA: +// func Bar$1(): +// return +// +// func Bar() I: +// t0 = changetype H <- func() (Bar$1) +// t1 = make I <- H (t0) +// +// func For(): +// t0 = Bar() +// t1 = invoke t0.Do() +// t2 = &g[0:int] +// *t2 = t0 +// t3 = (G).Goo(g) +// +// func (h H) Do(): +// t0 = h() +// +// func (g G) Goo(): +// t0 = &g[0:int] +// t1 = *t0 +// t2 = invoke t1.Do() + +// WANT: +// For: (G).Goo(g) -> G.Goo; Bar() -> Bar; invoke t0.Do() -> H.Do +// H.Do: h() -> Bar$1 +// G.Goo: invoke t1.Do() -> H.Do diff --git a/go/callgraph/vta/vta_test.go b/go/callgraph/vta/vta_test.go index 4cd26a54aba..549c4af4529 100644 --- a/go/callgraph/vta/vta_test.go +++ b/go/callgraph/vta/vta_test.go @@ -26,6 +26,7 @@ func TestVTACallGraph(t *testing.T) { "testdata/src/callgraph_fields.go", "testdata/src/callgraph_field_funcs.go", "testdata/src/callgraph_recursive_types.go", + "testdata/src/callgraph_issue_57756.go", } { t.Run(file, func(t *testing.T) { prog, want, err := testProg(file, ssa.BuilderMode(0)) From d34a055ab1aa3868cca11a7880ae54ad459d9d33 Mon Sep 17 00:00:00 2001 From: Tim King Date: Mon, 16 Jan 2023 18:20:27 -0800 Subject: [PATCH 636/723] go/ssa: sanity check the types of phi nodes Adds a sanity check that the types of phi edges are types.Identical to the Phi instruction. This would have caught golang/go#57790. Change-Id: I9818fc47fda368bfc009bfe4de66e23de759f6ca Reviewed-on: https://go-review.googlesource.com/c/tools/+/462050 gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Tim King TryBot-Result: Gopher Robot --- go/ssa/sanity.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 3fb3f394e87..994e2631f16 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -108,6 +108,9 @@ func (s *sanity) checkInstr(idx int, instr Instruction) { for i, e := range instr.Edges { if e == nil { s.errorf("phi node '%s' has no value for edge #%d from %s", instr.Comment, i, s.block.Preds[i]) + } else if !types.Identical(instr.typ, e.Type()) { + s.errorf("phi node '%s' has a different type (%s) for edge #%d from %s (%s)", + instr.Comment, instr.Type(), i, s.block.Preds[i], e.Type()) } } } From 5c176b1de44b3941fd964b684643dfda6147edfa Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 17 Jan 2023 12:24:46 -0500 Subject: [PATCH 637/723] internal/robustio: skip os.Link test on android https://pkg.go.dev/internal/testenv#HasLink says: // From Android release M (Marshmallow), hard linking files is blocked // and an attempt to call link() on a file will return EACCES. // - https://code.google.com/p/android-developer-preview/issues/detail?id=3150 return runtime.GOOS != "plan9" && runtime.GOOS != "android" Fixes golang/go#57844 Change-Id: I92594daf99d3a0e74507f7f3ab806e3ed5af533f Reviewed-on: https://go-review.googlesource.com/c/tools/+/462415 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Bryan Mills --- internal/robustio/robustio_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/robustio/robustio_test.go b/internal/robustio/robustio_test.go index ed49bff7318..af53282200a 100644 --- a/internal/robustio/robustio_test.go +++ b/internal/robustio/robustio_test.go @@ -56,8 +56,10 @@ func TestFileID(t *testing.T) { if realID != symlinkID { t.Errorf("realID %+v != symlinkID %+v", realID, symlinkID) } + } - // Two hard-linked files have the same ID. + // Two hard-linked files have the same ID. + if runtime.GOOS != "plan9" && runtime.GOOS != "android" { hardlink := filepath.Join(t.TempDir(), "hardlink") if err := os.Link(real, hardlink); err != nil { t.Fatal(err) From d7fc4e7114a478d21092b3ab0ce243607ce74c34 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Tue, 6 Dec 2022 09:17:36 -0500 Subject: [PATCH 638/723] gopls: new LSP stub generator This is a complete rewrite of the stub generator to replace the one written in Typescript. It uses the json 'language-independent' specification of the protocol. To make correctness checking easier, it generates files that are drop-in replacements for the existing files. Diffing the old and new generated files will just show comments and whitespace diffs for tsclient.go and tsserver.go. The diffs for tsjon.go don't matter, as exsting gopls code doesn't use anything in that file. Aside from comments and whitespace, every line in the old tsprotcol.go is in the new one; the new one has more types. (The sole exception is one of the fields of _InterfaceParams, which is not used.) In particular, after previous unpleasant experience, all '*' and ',omitempty' occurrences match in struct fields. Further description is in README.md. Change-Id: Ia32f901102e3cfbce00261544eb463285a8b5ff4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/455475 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Peter Weinberger --- .../internal/lsp/protocol/generate/README.md | 138 ++ .../internal/lsp/protocol/generate/compare.go | 10 - gopls/internal/lsp/protocol/generate/data.go | 104 -- gopls/internal/lsp/protocol/generate/doc.go | 32 - .../lsp/protocol/generate/generate.go | 99 +- gopls/internal/lsp/protocol/generate/main.go | 351 +++- .../lsp/protocol/generate/main_test.go | 26 +- .../internal/lsp/protocol/generate/naming.go | 64 - .../internal/lsp/protocol/generate/output.go | 412 ++++- gopls/internal/lsp/protocol/generate/parse.go | 174 -- .../internal/lsp/protocol/generate/tables.go | 422 +++++ .../lsp/protocol/generate/typenames.go | 184 +++ gopls/internal/lsp/protocol/generate/types.go | 78 +- .../lsp/protocol/generate/utilities.go | 55 - .../lsp/protocol/typescript/README.md | 55 - .../internal/lsp/protocol/typescript/code.ts | 1450 ----------------- .../lsp/protocol/typescript/tsconfig.json | 29 - .../internal/lsp/protocol/typescript/util.ts | 254 --- 18 files changed, 1601 insertions(+), 2336 deletions(-) create mode 100644 gopls/internal/lsp/protocol/generate/README.md delete mode 100644 gopls/internal/lsp/protocol/generate/compare.go delete mode 100644 gopls/internal/lsp/protocol/generate/data.go delete mode 100644 gopls/internal/lsp/protocol/generate/doc.go delete mode 100644 gopls/internal/lsp/protocol/generate/naming.go delete mode 100644 gopls/internal/lsp/protocol/generate/parse.go create mode 100644 gopls/internal/lsp/protocol/generate/tables.go create mode 100644 gopls/internal/lsp/protocol/generate/typenames.go delete mode 100644 gopls/internal/lsp/protocol/generate/utilities.go delete mode 100644 gopls/internal/lsp/protocol/typescript/README.md delete mode 100644 gopls/internal/lsp/protocol/typescript/code.ts delete mode 100644 gopls/internal/lsp/protocol/typescript/tsconfig.json delete mode 100644 gopls/internal/lsp/protocol/typescript/util.ts diff --git a/gopls/internal/lsp/protocol/generate/README.md b/gopls/internal/lsp/protocol/generate/README.md new file mode 100644 index 00000000000..7c34c623ea2 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/README.md @@ -0,0 +1,138 @@ +# LSP Support for gopls + +## The protocol + +The LSP protocol exchanges json-encoded messages between the client and the server. +(gopls is the server.) The messages are either Requests, which require Responses, or +Notifications, which generate no response. Each Request or Notification has a method name +such as "textDocument/hover" that indicates its meaning and determines which function in the server will handle it. +The protocol is described in a +[web page](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/), +in words, and in a json file (metaModel.json) available either linked towards the bottom of the +web page, or in the vscode-languageserver-node repository. This code uses the latter so the +exact version can be tied to a githash. Download the repository with + +`git clone https://github.com/microsoft/vscode-languageserver-node.git` + +The specification has five sections +1. Requests, which describe the Request and Response types for request methods (e.g., *textDocument/didChange*), +2. Notifications, which describe the Request types for notification methods, +3. Structures, which describe named struct-like types, +4. TypeAliases, which describe type aliases, +5. Enumerations, which describe named constants. + +Requests and Notifications are tagged with a Method (e.g., `"textDocument/hover"`). +The specification does not specify the names of the functions that handle the messages. These +names are specified by the `methodNames` map. Enumerations generate Go `const`s, but +in Typescript they are scoped to namespaces, while in Go they are scoped to a package, so the Go names +may need to be modified to avoid name collisions. (See the `disambiguate` map, and its use.) + +Finally, the specified types are Typescript types, which are quite different from Go types. + +### Optionality +The specification can mark fields in structs as Optional. The client distinguishes between missing +fields and `null` fields in some cases. The Go translation for an optional type +should be making sure the field's value +can be `nil`, and adding the json tag `,omitempty`. The former condition would be satisfied by +adding `*` to the field's type if the type is not a reference type. + +### Types +The specification uses a number of different types, only a few of which correspond directly to Go types. +The specification's types are "base", "reference", "map", "literal", "stringLiteral", "tuple", "and", "or". +The "base" types correspond directly to Go types, although some Go types needs to be chosen for `URI` and `DocumentUri`. (The "base" types`RegExp`, `BooleanLiteral`, `NumericLiteral` never occur.) + +"reference" types are the struct-like types in the Structures section of the specification. The given +names are suitable for Go to use, except the code needs to change names like `_Initialze` to `XInitialize` so +they are exported for json marshaling and unmarshaling. + +"map" types are just like Go. (The key type in all of them is `DocumentUri`.) + +"stringLiteral" types are types whose type name and value are a single string. The chosen Go equivalent +is to make the type `string` and the value a constant. (The alternative would be to generate a new +named type, which seemed redundant.) + +"literal" types are like Go anonymous structs, so they have to be given a name. (All instances +of the remaining types have to be given names. One approach is to construct the name from the components +of the type, but this leads to misleading punning, and is unstable if components are added. The other approach +is to construct the name from the context of the definition, that is, from the types it is defined within. +For instance `Lit__InitializeParams_clientInfo` is the "literal" type at the +`clientInfo` field in the `_InitializeParams` +struct. Although this choice is sensitive to the ordering of the components, the code uses this approach, +presuming that reordering components is an unlikely protocol change.) + +"tuple" types are generated as Go structs. (There is only one, with two `uint32` fields.) + +"and" types are Go structs with embedded type names. (There is only one, `And_Param_workspace_configuration`.) + +"or" types are the most complicated. There are a lot of them and there is no simple Go equivalent. +They are defined as structs with a single `Value interface{}` field and custom json marshaling +and unmarshaling code. Users can assign anything to `Value` but the type will be checked, and +correctly marshaled, by the custom marshaling code. The unmarshaling code checks types, so `Value` +will have one of the permitted types. (`nil` is always allowed.) There are about 40 "or" types that +have a single non-null component, and these are converted to the component type. + +## Processing +The code parses the json specification file, and scans all the types. It assigns names, as described +above, to the types that are unnamed in the specification, and constructs Go equivalents as required. +(Most of this code is in typenames.go.) + +There are four output files. tsclient.go and tsserver.go contain the definition and implementation +of the `protocol.Client` and `protocol.Server` types and the code that dispatches on the Method +of the Request or Notification. tsjson.go contains the custom marshaling and unmarshaling code. +And tsprotocol.go contains the type and const definitions. + +### Accommodating gopls +As the code generates output, mostly in generateoutput.go and main.go, +it makes adjustments so that no changes are required to the existing Go code. +(Organizing the computation this way makes the code's structure simpler, but results in +a lot of unused types.) +There are three major classes of these adjustments, and leftover special cases. + +The first major +adjustment is to change generated type names to the ones gopls expects. Some of these don't change the +semantics of the type, just the name. +But for historical reasons a lot of them replace "or" types by a single +component of the type. (Until fairly recently Go only saw or used only one of components.) +The `goplsType` map in tables.go controls this process. + +The second major adjustment is to the types of fields of structs, which is done using the +`renameProp` map in tables.go. + +The third major adjustment handles optionality, controlling `*` and `,omitempty` placement when +the default rules don't match what gopls is expecting. (The map is `goplsStar`, also in tables.go) +(If the intermediate components in expressions of the form `A.B.C.S` were optional, the code would need +a lot of useless checking for nils. Typescript has a language construct to avoid most checks.) + +Then there are some additional special cases. There are a few places with adjustments to avoid +recursive types. For instance `LSPArray` is `[]LSPAny`, but `LSPAny` is an "or" type including `LSPArray`. +The solution is to make `LSPAny` an `interface{}`. Another instance is `_InitializeParams.trace` +whose type is an "or" of 3 stringLiterals, which just becomes a `string`. + +### Checking +`TestAll(t *testing.T)` checks that there are no unexpected fields in the json specification. + +While the code is executing, it checks that all the entries in the maps in tables.go are used. +It also checks that the entries in `renameProp` and `goplsStar` are not redundant. + +As a one-time check on the first release of this code, diff-ing the existing and generated tsclient.go +and tsserver.go code results in only whitespace and comment diffs. The existing and generated +tsprotocol.go differ in whitespace and comments, and in a substantial number of new type definitions +that the older, more heuristic, code did not generate. (And the unused type `_InitializeParams` differs +slightly between the new and the old, and is not worth fixing.) + +### Some history +The original stub code was written by hand, but with the protocol under active development, that +couldn't last. The web page existed before the json specification, but it lagged the implementation +and was hard to process by machine. So the earlier version of the generating code was written in Typescript, and +used the Typescript compiler's API to parse the protocol code in the repository. +It then used a set of heuristics +to pick out the elements of the protocol, and another set of overlapping heuristics to create the Go code. +The output was functional, but idiosyncratic, and the code was fragile and barely maintainable. + +### The future +Most of the adjustments using the maps in tables.go could be removed by making changes, mostly to names, +in the gopls code. Using more "or" types in gopls requires more elaborate, but stereotyped, changes. +But even without all the adjustments, making this its own module would face problems; a number of +dependencies would have to be factored out. And, it is fragile. The custom unmarshaling code knows what +types it expects. A design that return an 'any' on unexpected types would match the json +'ignore unexpected values' philosophy better, but the the Go code would need extra checking. diff --git a/gopls/internal/lsp/protocol/generate/compare.go b/gopls/internal/lsp/protocol/generate/compare.go deleted file mode 100644 index d341307821d..00000000000 --- a/gopls/internal/lsp/protocol/generate/compare.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.19 -// +build go1.19 - -package main - -// compare the generated files in two directories diff --git a/gopls/internal/lsp/protocol/generate/data.go b/gopls/internal/lsp/protocol/generate/data.go deleted file mode 100644 index 435f594bcf7..00000000000 --- a/gopls/internal/lsp/protocol/generate/data.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.19 -// +build go1.19 - -package main - -// various data tables - -// methodNames is a map from the method to the name of the function that handles it -var methodNames = map[string]string{ - "$/cancelRequest": "CancelRequest", - "$/logTrace": "LogTrace", - "$/progress": "Progress", - "$/setTrace": "SetTrace", - "callHierarchy/incomingCalls": "IncomingCalls", - "callHierarchy/outgoingCalls": "OutgoingCalls", - "client/registerCapability": "RegisterCapability", - "client/unregisterCapability": "UnregisterCapability", - "codeAction/resolve": "ResolveCodeAction", - "codeLens/resolve": "ResolveCodeLens", - "completionItem/resolve": "ResolveCompletionItem", - "documentLink/resolve": "ResolveDocumentLink", - "exit": "Exit", - "initialize": "Initialize", - "initialized": "Initialized", - "inlayHint/resolve": "Resolve", - "notebookDocument/didChange": "DidChangeNotebookDocument", - "notebookDocument/didClose": "DidCloseNotebookDocument", - "notebookDocument/didOpen": "DidOpenNotebookDocument", - "notebookDocument/didSave": "DidSaveNotebookDocument", - "shutdown": "Shutdown", - "telemetry/event": "Event", - "textDocument/codeAction": "CodeAction", - "textDocument/codeLens": "CodeLens", - "textDocument/colorPresentation": "ColorPresentation", - "textDocument/completion": "Completion", - "textDocument/declaration": "Declaration", - "textDocument/definition": "Definition", - "textDocument/diagnostic": "Diagnostic", - "textDocument/didChange": "DidChange", - "textDocument/didClose": "DidClose", - "textDocument/didOpen": "DidOpen", - "textDocument/didSave": "DidSave", - "textDocument/documentColor": "DocumentColor", - "textDocument/documentHighlight": "DocumentHighlight", - "textDocument/documentLink": "DocumentLink", - "textDocument/documentSymbol": "DocumentSymbol", - "textDocument/foldingRange": "FoldingRange", - "textDocument/formatting": "Formatting", - "textDocument/hover": "Hover", - "textDocument/implementation": "Implementation", - "textDocument/inlayHint": "InlayHint", - "textDocument/inlineValue": "InlineValue", - "textDocument/linkedEditingRange": "LinkedEditingRange", - "textDocument/moniker": "Moniker", - "textDocument/onTypeFormatting": "OnTypeFormatting", - "textDocument/prepareCallHierarchy": "PrepareCallHierarchy", - "textDocument/prepareRename": "PrepareRename", - "textDocument/prepareTypeHierarchy": "PrepareTypeHierarchy", - "textDocument/publishDiagnostics": "PublishDiagnostics", - "textDocument/rangeFormatting": "RangeFormatting", - "textDocument/references": "References", - "textDocument/rename": "Rename", - "textDocument/selectionRange": "SelectionRange", - "textDocument/semanticTokens/full": "SemanticTokensFull", - "textDocument/semanticTokens/full/delta": "SemanticTokensFullDelta", - "textDocument/semanticTokens/range": "SemanticTokensRange", - "textDocument/signatureHelp": "SignatureHelp", - "textDocument/typeDefinition": "TypeDefinition", - "textDocument/willSave": "WillSave", - "textDocument/willSaveWaitUntil": "WillSaveWaitUntil", - "typeHierarchy/subtypes": "Subtypes", - "typeHierarchy/supertypes": "Supertypes", - "window/logMessage": "LogMessage", - "window/showDocument": "ShowDocument", - "window/showMessage": "ShowMessage", - "window/showMessageRequest": "ShowMessageRequest", - "window/workDoneProgress/cancel": "WorkDoneProgressCancel", - "window/workDoneProgress/create": "WorkDoneProgressCreate", - "workspace/applyEdit": "ApplyEdit", - "workspace/codeLens/refresh": "CodeLensRefresh", - "workspace/configuration": "Configuration", - "workspace/diagnostic": "DiagnosticWorkspace", - "workspace/diagnostic/refresh": "DiagnosticRefresh", - "workspace/didChangeConfiguration": "DidChangeConfiguration", - "workspace/didChangeWatchedFiles": "DidChangeWatchedFiles", - "workspace/didChangeWorkspaceFolders": "DidChangeWorkspaceFolders", - "workspace/didCreateFiles": "DidCreateFiles", - "workspace/didDeleteFiles": "DidDeleteFiles", - "workspace/didRenameFiles": "DidRenameFiles", - "workspace/executeCommand": "ExecuteCommand", - "workspace/inlayHint/refresh": "InlayHintRefresh", - "workspace/inlineValue/refresh": "InlineValueRefresh", - "workspace/semanticTokens/refresh": "SemanticTokensRefresh", - "workspace/symbol": "Symbol", - "workspace/willCreateFiles": "WillCreateFiles", - "workspace/willDeleteFiles": "WillDeleteFiles", - "workspace/willRenameFiles": "WillRenameFiles", - "workspace/workspaceFolders": "WorkspaceFolders", - "workspaceSymbol/resolve": "ResolveWorkspaceSymbol", -} diff --git a/gopls/internal/lsp/protocol/generate/doc.go b/gopls/internal/lsp/protocol/generate/doc.go deleted file mode 100644 index 74685559c8e..00000000000 --- a/gopls/internal/lsp/protocol/generate/doc.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.19 -// +build go1.19 - -/* -GenLSP generates the files tsprotocol.go, tsclient.go, -tsserver.go, tsjson.go that support the language server protocol -for gopls. - -Usage: - - go run . [flags] - -The flags are: - - -d - The directory containing the vscode-languageserver-node repository. - (git clone https://github.com/microsoft/vscode-languageserver-node.git). - If not specified, the default is $HOME/vscode-languageserver-node. - - -o - The directory to write the generated files to. It must exist. - The default is "gen". - - -c - Compare the generated files to the files in the specified directory. - If this flag is not specified, no comparison is done. -*/ -package main diff --git a/gopls/internal/lsp/protocol/generate/generate.go b/gopls/internal/lsp/protocol/generate/generate.go index 86c332856af..3746228275d 100644 --- a/gopls/internal/lsp/protocol/generate/generate.go +++ b/gopls/internal/lsp/protocol/generate/generate.go @@ -7,4 +7,101 @@ package main -// generate the Go code +import ( + "bytes" + "fmt" + "log" + "strings" +) + +// a newType is a type that needs a name and a definition +// These are the various types that the json specification doesn't name +type newType struct { + name string + properties Properties // for struct/literal types + items []*Type // for other types ("and", "tuple") + line int + kind string // Or, And, Tuple, Lit, Map + typ *Type +} + +func generateDoc(out *bytes.Buffer, doc string) { + if doc == "" { + return + } + + if !strings.Contains(doc, "\n") { + fmt.Fprintf(out, "// %s\n", doc) + return + } + out.WriteString("/*\n") + for _, line := range strings.Split(doc, "\n") { + fmt.Fprintf(out, " * %s\n", line) + } + out.WriteString(" */\n") +} + +// decide if a property is optional, and if it needs a * +// return ",omitempty" if it is optional, and "*" if it needs a pointer +func propStar(name string, t NameType, gotype string) (string, string) { + var opt, star string + if t.Optional { + star = "*" + opt = ",omitempty" + } + if strings.HasPrefix(gotype, "[]") || strings.HasPrefix(gotype, "map[") { + star = "" // passed by reference, so no need for * + } else { + switch gotype { + case "bool", "uint32", "int32", "string", "interface{}": + star = "" // gopls compatibility if t.Optional + } + } + ostar, oopt := star, opt + if newStar, ok := goplsStar[prop{name, t.Name}]; ok { + switch newStar { + case nothing: + star, opt = "", "" + case wantStar: + star, opt = "*", "" + case wantOpt: + star, opt = "", ",omitempty" + case wantOptStar: + star, opt = "*", ",omitempty" + } + if star == ostar && opt == oopt { // no change + log.Printf("goplsStar[ {%q, %q} ](%d) useless %s/%s %s/%s", name, t.Name, t.Line, ostar, star, oopt, opt) + } + usedGoplsStar[prop{name, t.Name}] = true + } + + return opt, star +} + +func goName(s string) string { + // Go naming conventions + if strings.HasSuffix(s, "Id") { + s = s[:len(s)-len("Id")] + "ID" + } else if strings.HasSuffix(s, "Uri") { + s = s[:len(s)-3] + "URI" + } else if s == "uri" { + s = "URI" + } else if s == "id" { + s = "ID" + } + + // renames for temporary GOPLS compatibility + if news := goplsType[s]; news != "" { + usedGoplsType[s] = true + s = news + } + // Names beginning _ are not exported + if strings.HasPrefix(s, "_") { + s = strings.Replace(s, "_", "X", 1) + } + if s != "string" { // base types are unchanged (textDocuemnt/diagnostic) + // Title is deprecated, but a) s is only one word, b) replacement is too heavy-weight + s = strings.Title(s) + } + return s +} diff --git a/gopls/internal/lsp/protocol/generate/main.go b/gopls/internal/lsp/protocol/generate/main.go index 38d25705d32..94af77b5b16 100644 --- a/gopls/internal/lsp/protocol/generate/main.go +++ b/gopls/internal/lsp/protocol/generate/main.go @@ -8,86 +8,335 @@ package main import ( + "bytes" + "encoding/json" "flag" "fmt" + "go/format" "log" "os" + "path/filepath" + "strings" + "time" ) var ( // git clone https://github.com/microsoft/vscode-languageserver-node.git repodir = flag.String("d", "", "directory of vscode-languageserver-node") outputdir = flag.String("o", "gen", "output directory") - cmpolder = flag.String("c", "", "directory of older generated code") + // PJW: not for real code + cmpdir = flag.String("c", "", "directory of earlier code") + doboth = flag.String("b", "", "generate and compare") ) func main() { log.SetFlags(log.Lshortfile) // log file name and line number, not time flag.Parse() + processinline() +} + +func processinline() { if *repodir == "" { - *repodir = fmt.Sprintf("%s/vscode-languageserver-node", os.Getenv("HOME")) + *repodir = filepath.Join(os.Getenv("HOME"), "vscode-languageserver-node") + } + + model := parse(filepath.Join(*repodir, "protocol/metaModel.json")) + + findTypeNames(model) + generateOutput(model) + + fileHdr = fileHeader(model) + + // write the files + writeclient() + writeserver() + writeprotocol() + writejsons() + + checkTables() +} + +// common file header for output files +var fileHdr string + +func writeclient() { + out := new(bytes.Buffer) + fmt.Fprintln(out, fileHdr) + out.WriteString( + `import ( + "context" + "encoding/json" + + "golang.org/x/tools/internal/jsonrpc2" +) +`) + out.WriteString("type Client interface {\n") + for _, k := range cdecls.keys() { + out.WriteString(cdecls[k]) + } + out.WriteString("}\n\n") + out.WriteString("func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) {\n") + out.WriteString("\tswitch r.Method() {\n") + for _, k := range ccases.keys() { + out.WriteString(ccases[k]) + } + out.WriteString(("\tdefault:\n\t\treturn false, nil\n\t}\n}\n\n")) + for _, k := range cfuncs.keys() { + out.WriteString(cfuncs[k]) + } + + x, err := format.Source(out.Bytes()) + if err != nil { + os.WriteFile("/tmp/a.go", out.Bytes(), 0644) + log.Fatalf("tsclient.go: %v", err) + } + + if err := os.WriteFile(filepath.Join(*outputdir, "tsclient.go"), x, 0644); err != nil { + log.Fatalf("%v writing tsclient.go", err) + } +} + +func writeserver() { + out := new(bytes.Buffer) + fmt.Fprintln(out, fileHdr) + out.WriteString( + `import ( + "context" + "encoding/json" + + "golang.org/x/tools/internal/jsonrpc2" +) +`) + out.WriteString("type Server interface {\n") + for _, k := range sdecls.keys() { + out.WriteString(sdecls[k]) + } + out.WriteString(` NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) +} + +func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { + switch r.Method() { +`) + for _, k := range scases.keys() { + out.WriteString(scases[k]) + } + out.WriteString(("\tdefault:\n\t\treturn false, nil\n\t}\n}\n\n")) + for _, k := range sfuncs.keys() { + out.WriteString(sfuncs[k]) + } + out.WriteString(`func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { + var result interface{} + if err := s.sender.Call(ctx, method, params, &result); err != nil { + return nil, err + } + return result, nil +} +`) + + x, err := format.Source(out.Bytes()) + if err != nil { + os.WriteFile("/tmp/a.go", out.Bytes(), 0644) + log.Fatalf("tsserver.go: %v", err) + } + + if err := os.WriteFile(filepath.Join(*outputdir, "tsserver.go"), x, 0644); err != nil { + log.Fatalf("%v writing tsserver.go", err) + } +} + +func writeprotocol() { + out := new(bytes.Buffer) + fmt.Fprintln(out, fileHdr) + out.WriteString("import \"encoding/json\"\n\n") + + // The followiing are unneeded, but make the new code a superset of the old + hack := func(newer, existing string) { + if _, ok := types[existing]; !ok { + log.Fatalf("types[%q] not found", existing) + } + types[newer] = strings.Replace(types[existing], existing, newer, 1) + } + hack("ConfigurationParams", "ParamConfiguration") + hack("InitializeParams", "ParamInitialize") + hack("PreviousResultId", "PreviousResultID") + hack("WorkspaceFoldersServerCapabilities", "WorkspaceFolders5Gn") + hack("_InitializeParams", "XInitializeParams") + // and some aliases to make the new code contain the old + types["PrepareRename2Gn"] = "type PrepareRename2Gn = Msg_PrepareRename2Gn // (alias) line 13927\n" + types["PrepareRenameResult"] = "type PrepareRenameResult = Msg_PrepareRename2Gn // (alias) line 13927\n" + for _, k := range types.keys() { + if k == "WatchKind" { + types[k] = "type WatchKind = uint32 // line 13505" // strict gopls compatibility neads the '=' + } + out.WriteString(types[k]) + } + + out.WriteString("\nconst (\n") + for _, k := range consts.keys() { + out.WriteString(consts[k]) + } + out.WriteString(")\n\n") + x, err := format.Source(out.Bytes()) + if err != nil { + os.WriteFile("/tmp/a.go", out.Bytes(), 0644) + log.Fatalf("tsprotocol.go: %v", err) + } + if err := os.WriteFile(filepath.Join(*outputdir, "tsprotocol.go"), x, 0644); err != nil { + log.Fatalf("%v writing tsprotocol.go", err) + } +} + +func writejsons() { + out := new(bytes.Buffer) + fmt.Fprintln(out, fileHdr) + out.WriteString("import \"encoding/json\"\n\n") + out.WriteString("import \"errors\"\n") + out.WriteString("import \"fmt\"\n") + + for _, k := range jsons.keys() { + out.WriteString(jsons[k]) + } + x, err := format.Source(out.Bytes()) + if err != nil { + os.WriteFile("/tmp/a.go", out.Bytes(), 0644) + log.Fatalf("tsjson.go: %v", err) } - spec := parse(*repodir) + if err := os.WriteFile(filepath.Join(*outputdir, "tsjson.go"), x, 0644); err != nil { + log.Fatalf("%v writing tsjson.go", err) + } +} + +// create the common file header for the output files +func fileHeader(model Model) string { + fname := filepath.Join(*repodir, ".git", "HEAD") + buf, err := os.ReadFile(fname) + if err != nil { + log.Fatal(err) + } + buf = bytes.TrimSpace(buf) + var githash string + if len(buf) == 40 { + githash = string(buf[:40]) + } else if bytes.HasPrefix(buf, []byte("ref: ")) { + fname = filepath.Join(*repodir, ".git", string(buf[5:])) + buf, err = os.ReadFile(fname) + if err != nil { + log.Fatal(err) + } + githash = string(buf[:40]) + } else { + log.Fatalf("githash cannot be recovered from %s", fname) + } + + format := `// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated for LSP. DO NOT EDIT. - // index the information in the specification - spec.indexRPCInfo() // messages - spec.indexDefInfo() // named types +package protocol +// Code generated from version %s of protocol/metaModel.json. +// git hash %s (as of %s) + + +` + now := time.Now().Format("2006-01-02") + return fmt.Sprintf(format, model.Version.Version, githash, now) +} + +func parse(fname string) Model { + buf, err := os.ReadFile(fname) + if err != nil { + log.Fatal(err) + } + buf = addLineNumbers(buf) + var model Model + if err := json.Unmarshal(buf, &model); err != nil { + log.Fatal(err) + } + return model } -func (s *spec) indexRPCInfo() { - for _, r := range s.model.Requests { - r := r - s.byMethod[r.Method] = &r +// Type.Value has to be treated specially for literals and maps +func (t *Type) UnmarshalJSON(data []byte) error { + // First unmarshal only the unambiguous fields. + var x struct { + Kind string `json:"kind"` + Items []*Type `json:"items"` + Element *Type `json:"element"` + Name string `json:"name"` + Key *Type `json:"key"` + Value any `json:"value"` + Line int `json:"line"` + } + if err := json.Unmarshal(data, &x); err != nil { + return err + } + *t = Type{ + Kind: x.Kind, + Items: x.Items, + Element: x.Element, + Name: x.Name, + Value: x.Value, + Line: x.Line, } - for _, n := range s.model.Notifications { - n := n - if n.Method == "$/cancelRequest" { - // viewed as too confusing to generate - continue + + // Then unmarshal the 'value' field based on the kind. + // This depends on Unmarshal ignoring fields it doesn't know about. + switch x.Kind { + case "map": + var x struct { + Key *Type `json:"key"` + Value *Type `json:"value"` + } + if err := json.Unmarshal(data, &x); err != nil { + return fmt.Errorf("Type.kind=map: %v", err) } - s.byMethod[n.Method] = &n + t.Key = x.Key + t.Value = x.Value + + case "literal": + var z struct { + Value ParseLiteral `json:"value"` + } + + if err := json.Unmarshal(data, &z); err != nil { + return fmt.Errorf("Type.kind=literal: %v", err) + } + t.Value = z.Value + + case "base", "reference", "array", "and", "or", "tuple", + "stringLiteral": + // no-op. never seen integerLiteral or booleanLiteral. + + default: + return fmt.Errorf("cannot decode Type.kind %q: %s", x.Kind, data) } + return nil } -func (sp *spec) indexDefInfo() { - for _, s := range sp.model.Structures { - s := s - sp.byName[s.Name] = &s - } - for _, e := range sp.model.Enumerations { - e := e - sp.byName[e.Name] = &e - } - for _, ta := range sp.model.TypeAliases { - ta := ta - sp.byName[ta.Name] = &ta - } - - // some Structure and TypeAlias names need to be changed for Go - // so byName contains the name used in the .json file, and - // the Name field contains the Go version of the name. - v := sp.model.Structures - for i, s := range v { - switch s.Name { - case "_InitializeParams": // _ is not upper case - v[i].Name = "XInitializeParams" - case "ConfigurationParams": // gopls compatibility - v[i].Name = "ParamConfiguration" - case "InitializeParams": // gopls compatibility - v[i].Name = "ParamInitialize" - case "PreviousResultId": // Go naming convention - v[i].Name = "PreviousResultID" - case "WorkspaceFoldersServerCapabilities": // gopls compatibility - v[i].Name = "WorkspaceFolders5Gn" +// which table entries were not used +func checkTables() { + for k := range disambiguate { + if !usedDisambiguate[k] { + log.Printf("disambiguate[%v] unused", k) + } + } + for k := range renameProp { + if !usedRenameProp[k] { + log.Printf("renameProp {%q, %q} unused", k[0], k[1]) + } + } + for k := range goplsStar { + if !usedGoplsStar[k] { + log.Printf("goplsStar {%q, %q} unused", k[0], k[1]) } } - w := sp.model.TypeAliases - for i, t := range w { - switch t.Name { - case "PrepareRenameResult": // gopls compatibility - w[i].Name = "PrepareRename2Gn" + for k := range goplsType { + if !usedGoplsType[k] { + log.Printf("unused goplsType[%q]->%s", k, goplsType[k]) } } } diff --git a/gopls/internal/lsp/protocol/generate/main_test.go b/gopls/internal/lsp/protocol/generate/main_test.go index d986b59cee9..f887066ee2d 100644 --- a/gopls/internal/lsp/protocol/generate/main_test.go +++ b/gopls/internal/lsp/protocol/generate/main_test.go @@ -15,33 +15,30 @@ import ( "testing" ) -// this is not a test, but an easy way to invoke the debugger -func TestAll(t *testing.T) { - t.Skip("run by hand") - log.SetFlags(log.Lshortfile) - main() -} +// These tests require the result of +//"git clone https://github.com/microsoft/vscode-languageserver-node" in the HOME directory -// this is not a test, but an easy way to invoke the debugger -func TestCompare(t *testing.T) { - t.Skip("run by hand") +// this is not a test, but a way to get code coverage, +// (in vscode, just run the test with "go.coverOnSingleTest": true) +func TestAll(t *testing.T) { + t.Skip("needs vscode-languageserver-node repository") log.SetFlags(log.Lshortfile) - *cmpolder = "../lsp/gen" // instead use a directory containing the older generated files main() } // check that the parsed file includes all the information // from the json file. This test will fail if the spec // introduces new fields. (one can test this test by -// commenting out some special handling in parse.go.) +// commenting out the version field in Model.) func TestParseContents(t *testing.T) { - t.Skip("run by hand") + t.Skip("needs vscode-languageserver-node repository") log.SetFlags(log.Lshortfile) // compute our parse of the specification dir := os.Getenv("HOME") + "/vscode-languageserver-node" - v := parse(dir) - out, err := json.Marshal(v.model) + fname := dir + "/protocol/metaModel.json" + v := parse(fname) + out, err := json.Marshal(v) if err != nil { t.Fatal(err) } @@ -51,7 +48,6 @@ func TestParseContents(t *testing.T) { } // process the json file - fname := dir + "/protocol/metaModel.json" buf, err := os.ReadFile(fname) if err != nil { t.Fatalf("could not read metaModel.json: %v", err) diff --git a/gopls/internal/lsp/protocol/generate/naming.go b/gopls/internal/lsp/protocol/generate/naming.go deleted file mode 100644 index 9d9201a49d9..00000000000 --- a/gopls/internal/lsp/protocol/generate/naming.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.19 -// +build go1.19 - -package main - -// assign names to types. many types come with names, but names -// have to be provided for "or", "and", "tuple", and "literal" types. -// Only one tuple type occurs, so it poses no problem. Otherwise -// the name cannot depend on the ordering of the components, as permuting -// them doesn't change the type. One possibility is to build the name -// of the type out of the names of its components, done in an -// earlier version of this code, but rejected by code reviewers. -// (the name would change if the components changed.) -// An alternate is to use the definition context, which is what is done here -// and works for the existing code. However, it cannot work in general. -// (This easiest case is an "or" type with two "literal" components. -// The components will get the same name, as their definition contexts -// are identical.) spec.byName contains enough information to detect -// such cases. (Note that sometimes giving the same name to different -// types is correct, for instance when they involve stringLiterals.) - -import ( - "strings" -) - -// stacks contain information about the ancestry of a type -// (spaces and initial capital letters are treated specially in stack.name()) -type stack []string - -func (s stack) push(v string) stack { - return append(s, v) -} - -func (s stack) pop() { - s = s[:len(s)-1] -} - -// generate a type name from the stack that contains its ancestry -// -// For instance, ["Result textDocument/implementation"] becomes "_textDocument_implementation" -// which, after being returned, becomes "Or_textDocument_implementation", -// which will become "[]Location" eventually (for gopls compatibility). -func (s stack) name(prefix string) string { - var nm string - var seen int - // use the most recent 2 entries, if there are 2, - // or just the only one. - for i := len(s) - 1; i >= 0 && seen < 2; i-- { - x := s[i] - if x[0] <= 'Z' && x[0] >= 'A' { - // it may contain a message - if idx := strings.Index(x, " "); idx >= 0 { - x = prefix + strings.Replace(x[idx+1:], "/", "_", -1) - } - nm += x - seen++ - } - } - return nm -} diff --git a/gopls/internal/lsp/protocol/generate/output.go b/gopls/internal/lsp/protocol/generate/output.go index 14a04864e85..04f1080c195 100644 --- a/gopls/internal/lsp/protocol/generate/output.go +++ b/gopls/internal/lsp/protocol/generate/output.go @@ -7,4 +7,414 @@ package main -// Write the output +import ( + "bytes" + "fmt" + "log" + "sort" + "strings" +) + +var ( + // tsclient.go has 3 sections + cdecls = make(sortedMap[string]) + ccases = make(sortedMap[string]) + cfuncs = make(sortedMap[string]) + // tsserver.go has 3 sections + sdecls = make(sortedMap[string]) + scases = make(sortedMap[string]) + sfuncs = make(sortedMap[string]) + // tsprotocol.go has 2 sections + types = make(sortedMap[string]) + consts = make(sortedMap[string]) + // tsjson has 1 section + jsons = make(sortedMap[string]) +) + +func generateOutput(model Model) { + for _, r := range model.Requests { + genDecl(r.Method, r.Params, r.Result, r.Direction) + genCase(r.Method, r.Params, r.Result, r.Direction) + genFunc(r.Method, r.Params, r.Result, r.Direction, false) + } + for _, n := range model.Notifications { + if n.Method == "$/cancelRequest" { + continue // handled internally by jsonrpc2 + } + genDecl(n.Method, n.Params, nil, n.Direction) + genCase(n.Method, n.Params, nil, n.Direction) + genFunc(n.Method, n.Params, nil, n.Direction, true) + } + genStructs(model) + genAliases(model) + genGenTypes() // generate the unnamed types + genConsts(model) + genMarshal() +} + +func genDecl(method string, param, result *Type, dir string) { + fname := methodNames[method] + p := "" + if notNil(param) { + p = ", *" + goplsName(param) + } + ret := "error" + if notNil(result) { + tp := goplsName(result) + if !hasNilValue(tp) { + tp = "*" + tp + } + ret = fmt.Sprintf("(%s, error)", tp) + } + // special gopls compatibility case (PJW: still needed?) + switch method { + case "workspace/configuration": + // was And_Param_workspace_configuration, but the type substitution doesn't work, + // as ParamConfiguration is embedded in And_Param_workspace_configuration + p = ", *ParamConfiguration" + ret = "([]LSPAny, error)" + } + msg := fmt.Sprintf("\t%s(context.Context%s) %s // %s\n", fname, p, ret, method) + switch dir { + case "clientToServer": + sdecls[method] = msg + case "serverToClient": + cdecls[method] = msg + case "both": + sdecls[method] = msg + cdecls[method] = msg + default: + log.Fatalf("impossible direction %q", dir) + } +} + +func genCase(method string, param, result *Type, dir string) { + out := new(bytes.Buffer) + fmt.Fprintf(out, "\tcase %q:\n", method) + var p string + fname := methodNames[method] + if notNil(param) { + nm := goplsName(param) + if method == "workspace/configuration" { // gopls compatibility + // was And_Param_workspace_configuration, which contains ParamConfiguration + // so renaming the type leads to circular definitions + nm = "ParamConfiguration" // gopls compatibility + } + fmt.Fprintf(out, "\t\tvar params %s\n", nm) + fmt.Fprintf(out, "\t\tif err := json.Unmarshal(r.Params(), ¶ms); err != nil {\n") + fmt.Fprintf(out, "\t\t\treturn true, sendParseError(ctx, reply, err)\n\t\t}\n") + p = ", ¶ms" + } + if notNil(result) { + fmt.Fprintf(out, "\t\tresp, err := %%s.%s(ctx%s)\n", fname, p) + out.WriteString("\t\tif err != nil {\n") + out.WriteString("\t\t\treturn true, reply(ctx, nil, err)\n") + out.WriteString("\t\t}\n") + out.WriteString("\t\treturn true, reply(ctx, resp, nil)\n") + } else { + fmt.Fprintf(out, "\t\terr := %%s.%s(ctx%s)\n", fname, p) + out.WriteString("\t\treturn true, reply(ctx, nil, err)\n") + } + msg := out.String() + switch dir { + case "clientToServer": + scases[method] = fmt.Sprintf(msg, "server") + case "serverToClient": + ccases[method] = fmt.Sprintf(msg, "client") + case "both": + scases[method] = fmt.Sprintf(msg, "server") + ccases[method] = fmt.Sprintf(msg, "client") + default: + log.Fatalf("impossible direction %q", dir) + } +} + +func genFunc(method string, param, result *Type, dir string, isnotify bool) { + out := new(bytes.Buffer) + var p, r string + var goResult string + if notNil(param) { + p = ", params *" + goplsName(param) + } + if notNil(result) { + goResult = goplsName(result) + if !hasNilValue(goResult) { + goResult = "*" + goResult + } + r = fmt.Sprintf("(%s, error)", goResult) + } else { + r = "error" + } + // special gopls compatibility case + switch method { + case "workspace/configuration": + // was And_Param_workspace_configuration, but the type substitution doesn't work, + // as ParamConfiguration is embedded in And_Param_workspace_configuration + p = ", params *ParamConfiguration" + r = "([]LSPAny, error)" + goResult = "[]LSPAny" + } + fname := methodNames[method] + fmt.Fprintf(out, "func (s *%%sDispatcher) %s(ctx context.Context%s) %s {\n", + fname, p, r) + + if !notNil(result) { + if isnotify { + if notNil(param) { + fmt.Fprintf(out, "\treturn s.sender.Notify(ctx, %q, params)\n", method) + } else { + fmt.Fprintf(out, "\treturn s.sender.Notify(ctx, %q, nil)\n", method) + } + } else { + if notNil(param) { + fmt.Fprintf(out, "\treturn s.sender.Call(ctx, %q, params, nil)\n", method) + } else { + fmt.Fprintf(out, "\treturn s.sender.Call(ctx, %q, nil, nil)\n", method) + } + } + } else { + fmt.Fprintf(out, "\tvar result %s\n", goResult) + if isnotify { + if notNil(param) { + fmt.Fprintf(out, "\ts.sender.Notify(ctx, %q, params)\n", method) + } else { + fmt.Fprintf(out, "\t\tif err := s.sender.Notify(ctx, %q, nil); err != nil {\n", method) + } + } else { + if notNil(param) { + fmt.Fprintf(out, "\t\tif err := s.sender.Call(ctx, %q, params, &result); err != nil {\n", method) + } else { + fmt.Fprintf(out, "\t\tif err := s.sender.Call(ctx, %q, nil, &result); err != nil {\n", method) + } + } + fmt.Fprintf(out, "\t\treturn nil, err\n\t}\n\treturn result, nil\n") + } + out.WriteString("}\n") + msg := out.String() + switch dir { + case "clientToServer": + sfuncs[method] = fmt.Sprintf(msg, "server") + case "serverToClient": + cfuncs[method] = fmt.Sprintf(msg, "client") + case "both": + sfuncs[method] = fmt.Sprintf(msg, "server") + cfuncs[method] = fmt.Sprintf(msg, "client") + default: + log.Fatalf("impossible direction %q", dir) + } +} + +func genStructs(model Model) { + structures := make(map[string]*Structure) // for expanding Extends + for _, s := range model.Structures { + structures[s.Name] = s + } + for _, s := range model.Structures { + out := new(bytes.Buffer) + generateDoc(out, s.Documentation) + nm := goName(s.Name) + if nm == "string" { // an unacceptable strut name + // a weird case, and needed only so the generated code contains the old gopls code + nm = "DocumentDiagnosticParams" + } + fmt.Fprintf(out, "type %s struct { // line %d\n", nm, s.Line) + // for gpls compatibilitye, embed most extensions, but expand the rest some day + props := append([]NameType{}, s.Properties...) + if s.Name == "SymbolInformation" { // but expand this one + for _, ex := range s.Extends { + fmt.Fprintf(out, "\t// extends %s\n", ex.Name) + props = append(props, structures[ex.Name].Properties...) + } + genProps(out, props, nm) + } else { + genProps(out, props, nm) + for _, ex := range s.Extends { + fmt.Fprintf(out, "\t%s\n", goName(ex.Name)) + } + } + for _, ex := range s.Mixins { + fmt.Fprintf(out, "\t%s\n", goName(ex.Name)) + } + out.WriteString("}\n") + types[nm] = out.String() + } + // base types + types["DocumentURI"] = "type DocumentURI string\n" + types["URI"] = "type URI = string\n" + + types["LSPAny"] = "type LSPAny = interface{}\n" + // A special case, the only previously existing Or type + types["DocumentDiagnosticReport"] = "type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) line 13909\n" + +} + +func genProps(out *bytes.Buffer, props []NameType, name string) { + for _, p := range props { + tp := goplsName(p.Type) + if newNm, ok := renameProp[prop{name, p.Name}]; ok { + usedRenameProp[prop{name, p.Name}] = true + if tp == newNm { + log.Printf("renameProp useless {%q, %q} for %s", name, p.Name, tp) + } + tp = newNm + } + // it's a pointer if it is optional, or for gopls compatibility + opt, star := propStar(name, p, tp) + json := fmt.Sprintf(" `json:\"%s%s\"`", p.Name, opt) + generateDoc(out, p.Documentation) + fmt.Fprintf(out, "\t%s %s%s %s\n", goName(p.Name), star, tp, json) + } +} + +func genAliases(model Model) { + for _, ta := range model.TypeAliases { + out := new(bytes.Buffer) + generateDoc(out, ta.Documentation) + nm := goName(ta.Name) + if nm != ta.Name { + continue // renamed the type, e.g., "DocumentDiagnosticReport", an or-type to "string" + } + tp := goplsName(ta.Type) + fmt.Fprintf(out, "type %s = %s // (alias) line %d\n", nm, tp, ta.Line) + types[nm] = out.String() + } +} + +func genGenTypes() { + for _, nt := range genTypes { + out := new(bytes.Buffer) + nm := goplsName(nt.typ) + switch nt.kind { + case "literal": + fmt.Fprintf(out, "// created for Literal (%s)\n", nt.name) + fmt.Fprintf(out, "type %s struct { // line %d\n", nm, nt.line+1) + genProps(out, nt.properties, nt.name) // systematic name, not gopls name; is this a good choice? + case "or": + if !strings.HasPrefix(nm, "Or") { + // It was replaced by a narrower type defined elsewhere + continue + } + names := []string{} + for _, t := range nt.items { + if notNil(t) { + names = append(names, goplsName(t)) + } + } + sort.Strings(names) + fmt.Fprintf(out, "// created for Or %v\n", names) + fmt.Fprintf(out, "type %s struct { // line %d\n", nm, nt.line+1) + fmt.Fprintf(out, "\tValue interface{} `json:\"value\"`\n") + case "and": + fmt.Fprintf(out, "// created for And\n") + fmt.Fprintf(out, "type %s struct { // line %d\n", nm, nt.line+1) + for _, x := range nt.items { + nm := goplsName(x) + fmt.Fprintf(out, "\t%s\n", nm) + } + case "tuple": // there's only this one + nt.name = "UIntCommaUInt" + fmt.Fprintf(out, "//created for Tuple\ntype %s struct { // line %d\n", nm, nt.line+1) + fmt.Fprintf(out, "\tFld0 uint32 `json:\"fld0\"`\n") + fmt.Fprintf(out, "\tFld1 uint32 `json:\"fld1\"`\n") + default: + log.Fatalf("%s not handled", nt.kind) + } + out.WriteString("}\n") + types[nm] = out.String() + } +} +func genConsts(model Model) { + for _, e := range model.Enumerations { + out := new(bytes.Buffer) + generateDoc(out, e.Documentation) + tp := goplsName(e.Type) + nm := goName(e.Name) + fmt.Fprintf(out, "type %s %s // line %d\n", nm, tp, e.Line) + types[nm] = out.String() + vals := new(bytes.Buffer) + generateDoc(vals, e.Documentation) + for _, v := range e.Values { + generateDoc(vals, v.Documentation) + nm := goName(v.Name) + more, ok := disambiguate[e.Name] + if ok { + usedDisambiguate[e.Name] = true + nm = more.prefix + nm + more.suffix + nm = goName(nm) // stringType + } + var val string + switch v := v.Value.(type) { + case string: + val = fmt.Sprintf("%q", v) + case float64: + val = fmt.Sprintf("%d", int(v)) + default: + log.Fatalf("impossible type %T", v) + } + fmt.Fprintf(vals, "\t%s %s = %s // line %d\n", nm, e.Name, val, v.Line) + } + consts[nm] = vals.String() + } +} +func genMarshal() { + for _, nt := range genTypes { + nm := goplsName(nt.typ) + if !strings.HasPrefix(nm, "Or") { + continue + } + names := []string{} + for _, t := range nt.items { + if notNil(t) { + names = append(names, goplsName(t)) + } + } + sort.Strings(names) + var buf bytes.Buffer + fmt.Fprintf(&buf, "// from line %d\n", nt.line) + fmt.Fprintf(&buf, "func (t %s) MarshalJSON() ([]byte, error) {\n", nm) + buf.WriteString("\tswitch x := t.Value.(type){\n") + for _, nmx := range names { + fmt.Fprintf(&buf, "\tcase %s:\n", nmx) + fmt.Fprintf(&buf, "\t\treturn json.Marshal(x)\n") + } + buf.WriteString("\tcase nil:\n\t\treturn []byte(\"null\"), nil\n\t}\n") + fmt.Fprintf(&buf, "\treturn nil, fmt.Errorf(\"type %%T not one of %v\", t)\n", names) + buf.WriteString("}\n\n") + + fmt.Fprintf(&buf, "func (t *%s) UnmarshalJSON(x []byte) error {\n", nm) + buf.WriteString("\tif string(x) == \"null\" {\n\t\tt.Value = nil\n\t\t\treturn nil\n\t}\n") + for i, nmx := range names { + fmt.Fprintf(&buf, "\tvar h%d %s\n", i, nmx) + fmt.Fprintf(&buf, "\tif err := json.Unmarshal(x, &h%d); err == nil {\n\t\tt.Value = h%d\n\t\t\treturn nil\n\t\t}\n", i, i) + } + fmt.Fprintf(&buf, "return errors.New(\"unmarshal failed to match one of %v\")\n", names) + buf.WriteString("}\n\n") + jsons[nm] = buf.String() + } +} + +func goplsName(t *Type) string { + nm := typeNames[t] + // translate systematic name to gopls name + if newNm, ok := goplsType[nm]; ok { + usedGoplsType[nm] = true + nm = newNm + } + return nm +} + +func notNil(t *Type) bool { // shutdwon is the special case that needs this + return t != nil && (t.Kind != "base" || t.Name != "null") +} + +func hasNilValue(t string) bool { + // this may be unreliable, and need a supplementary table + if strings.HasPrefix(t, "[]") || strings.HasPrefix(t, "*") { + return true + } + if t == "interface{}" || t == "any" { + return true + } + // that's all the cases that occur currently + return false +} diff --git a/gopls/internal/lsp/protocol/generate/parse.go b/gopls/internal/lsp/protocol/generate/parse.go deleted file mode 100644 index 9f8067eff4d..00000000000 --- a/gopls/internal/lsp/protocol/generate/parse.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.19 -// +build go1.19 - -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "time" -) - -// a spec contains the specification of the protocol, and derived information. -type spec struct { - model *Model - - // combined Requests and Notifications, indexed by method (e.g., "textDocument/didOpen") - byMethod sortedMap[Message] - - // Structures, Enumerations, and TypeAliases, indexed by name used in - // the .json specification file - // (Some Structure and Enumeration names need to be changed for Go, - // such as _Initialize) - byName sortedMap[Defined] - - // computed type information - nameToTypes sortedMap[[]*Type] // all the uses of a type name - - // remember which types are in a union type - orTypes sortedMap[sortedMap[bool]] - - // information about the version of vscode-languageclient-node - githash string - modTime time.Time -} - -// parse the specification file and return a spec. -// (TestParseContents checks that the parse gets all the fields of the specification) -func parse(dir string) *spec { - fname := filepath.Join(dir, "protocol", "metaModel.json") - buf, err := os.ReadFile(fname) - if err != nil { - log.Fatalf("could not read metaModel.json: %v", err) - } - // line numbers in the .json file occur as comments in tsprotocol.go - newbuf := addLineNumbers(buf) - var v Model - if err := json.Unmarshal(newbuf, &v); err != nil { - log.Fatalf("could not unmarshal metaModel.json: %v", err) - } - - ans := &spec{ - model: &v, - byMethod: make(sortedMap[Message]), - byName: make(sortedMap[Defined]), - nameToTypes: make(sortedMap[[]*Type]), - orTypes: make(sortedMap[sortedMap[bool]]), - } - ans.githash, ans.modTime = gitInfo(dir) - return ans -} - -// gitInfo returns the git hash and modtime of the repository. -func gitInfo(dir string) (string, time.Time) { - fname := dir + "/.git/HEAD" - buf, err := os.ReadFile(fname) - if err != nil { - log.Fatal(err) - } - buf = bytes.TrimSpace(buf) - var githash string - if len(buf) == 40 { - githash = string(buf[:40]) - } else if bytes.HasPrefix(buf, []byte("ref: ")) { - fname = dir + "/.git/" + string(buf[5:]) - buf, err = os.ReadFile(fname) - if err != nil { - log.Fatal(err) - } - githash = string(buf[:40]) - } else { - log.Fatalf("githash cannot be recovered from %s", fname) - } - loadTime := time.Now() - return githash, loadTime -} - -// addLineNumbers adds a "line" field to each object in the JSON. -func addLineNumbers(buf []byte) []byte { - var ans []byte - // In the specification .json file, the delimiter '{' is - // always followed by a newline. There are other {s embedded in strings. - // json.Token does not return \n, or :, or , so using it would - // require parsing the json to reconstruct the missing information. - for linecnt, i := 1, 0; i < len(buf); i++ { - ans = append(ans, buf[i]) - switch buf[i] { - case '{': - if buf[i+1] == '\n' { - ans = append(ans, fmt.Sprintf(`"line": %d, `, linecnt)...) - // warning: this would fail if the spec file had - // `"value": {\n}`, but it does not, as comma is a separator. - } - case '\n': - linecnt++ - } - } - return ans -} - -// Type.Value has to be treated specially for literals and maps -func (t *Type) UnmarshalJSON(data []byte) error { - // First unmarshal only the unambiguous fields. - var x struct { - Kind string `json:"kind"` - Items []*Type `json:"items"` - Element *Type `json:"element"` - Name string `json:"name"` - Key *Type `json:"key"` - Value any `json:"value"` - Line int `json:"line"` - } - if err := json.Unmarshal(data, &x); err != nil { - return err - } - *t = Type{ - Kind: x.Kind, - Items: x.Items, - Element: x.Element, - Name: x.Name, - Value: x.Value, - Line: x.Line, - } - - // Then unmarshal the 'value' field based on the kind. - // This depends on Unmarshal ignoring fields it doesn't know about. - switch x.Kind { - case "map": - var x struct { - Key *Type `json:"key"` - Value *Type `json:"value"` - } - if err := json.Unmarshal(data, &x); err != nil { - return fmt.Errorf("Type.kind=map: %v", err) - } - t.Key = x.Key - t.Value = x.Value - - case "literal": - var z struct { - Value ParseLiteral `json:"value"` - } - - if err := json.Unmarshal(data, &z); err != nil { - return fmt.Errorf("Type.kind=literal: %v", err) - } - t.Value = z.Value - - case "base", "reference", "array", "and", "or", "tuple", - "stringLiteral": - // nop. never seen integerLiteral or booleanLiteral. - - default: - return fmt.Errorf("cannot decode Type.kind %q: %s", x.Kind, data) - } - return nil -} diff --git a/gopls/internal/lsp/protocol/generate/tables.go b/gopls/internal/lsp/protocol/generate/tables.go new file mode 100644 index 00000000000..93a04367370 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/tables.go @@ -0,0 +1,422 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// prop combines the name of a property with the name of the structure it is in. +type prop [2]string + +const ( + nothing = iota + wantStar + wantOpt + wantOptStar +) + +// goplsStar records the optionality of each field in the protocol. +var goplsStar = map[prop]int{ + {"ClientCapabilities", "textDocument"}: wantOpt, + {"ClientCapabilities", "window"}: wantOpt, + {"ClientCapabilities", "workspace"}: wantOpt, + {"CodeAction", "edit"}: wantOpt, + {"CodeAction", "kind"}: wantOpt, + {"CodeActionClientCapabilities", "codeActionLiteralSupport"}: wantOpt, + {"CodeActionContext", "triggerKind"}: wantOpt, + {"CodeLens", "command"}: wantOpt, + {"CompletionClientCapabilities", "completionItem"}: wantOpt, + {"CompletionClientCapabilities", "insertTextMode"}: wantOpt, + {"CompletionItem", "insertTextFormat"}: wantOpt, + {"CompletionItem", "insertTextMode"}: wantOpt, + {"CompletionItem", "kind"}: wantOpt, + {"CompletionParams", "context"}: wantOpt, + {"Diagnostic", "severity"}: wantOpt, + {"DidSaveTextDocumentParams", "text"}: wantOptStar, + {"DocumentHighlight", "kind"}: wantOpt, + {"FileOperationPattern", "matches"}: wantOpt, + {"FileSystemWatcher", "kind"}: wantOpt, + {"Hover", "range"}: wantOpt, + {"InitializeResult", "serverInfo"}: wantOpt, + {"InlayHint", "kind"}: wantOpt, + {"InlayHint", "position"}: wantStar, + + {"Lit_CompletionClientCapabilities_completionItem", "commitCharactersSupport"}: nothing, + {"Lit_CompletionClientCapabilities_completionItem", "deprecatedSupport"}: nothing, + {"Lit_CompletionClientCapabilities_completionItem", "documentationFormat"}: nothing, + {"Lit_CompletionClientCapabilities_completionItem", "insertReplaceSupport"}: nothing, + {"Lit_CompletionClientCapabilities_completionItem", "insertTextModeSupport"}: nothing, + {"Lit_CompletionClientCapabilities_completionItem", "labelDetailsSupport"}: nothing, + {"Lit_CompletionClientCapabilities_completionItem", "preselectSupport"}: nothing, + {"Lit_CompletionClientCapabilities_completionItem", "resolveSupport"}: nothing, + {"Lit_CompletionClientCapabilities_completionItem", "snippetSupport"}: nothing, + {"Lit_CompletionClientCapabilities_completionItem", "tagSupport"}: nothing, + {"Lit_CompletionClientCapabilities_completionItemKind", "valueSet"}: nothing, + {"Lit_CompletionClientCapabilities_completionList", "itemDefaults"}: nothing, + {"Lit_CompletionList_itemDefaults", "commitCharacters"}: nothing, + {"Lit_CompletionList_itemDefaults", "data"}: nothing, + {"Lit_CompletionList_itemDefaults", "editRange"}: nothing, + {"Lit_CompletionList_itemDefaults", "insertTextFormat"}: nothing, + {"Lit_CompletionList_itemDefaults", "insertTextMode"}: nothing, + {"Lit_CompletionOptions_completionItem", "labelDetailsSupport"}: nothing, + {"Lit_DocumentSymbolClientCapabilities_symbolKind", "valueSet"}: nothing, + {"Lit_FoldingRangeClientCapabilities_foldingRange", "collapsedText"}: nothing, + {"Lit_FoldingRangeClientCapabilities_foldingRangeKind", "valueSet"}: nothing, + {"Lit_InitializeResult_serverInfo", "version"}: nothing, + {"Lit_NotebookDocumentChangeEvent_cells", "data"}: nothing, + {"Lit_NotebookDocumentChangeEvent_cells", "structure"}: nothing, + {"Lit_NotebookDocumentChangeEvent_cells", "textContent"}: nothing, + {"Lit_NotebookDocumentChangeEvent_cells_structure", "didClose"}: nothing, + {"Lit_NotebookDocumentChangeEvent_cells_structure", "didOpen"}: nothing, + {"Lit_NotebookDocumentFilter_Item0", "pattern"}: nothing, + {"Lit_NotebookDocumentFilter_Item0", "scheme"}: nothing, + {"Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0", "cells"}: nothing, + {"Lit_SemanticTokensClientCapabilities_requests", "full"}: nothing, + {"Lit_SemanticTokensClientCapabilities_requests", "range"}: nothing, + {"Lit_SemanticTokensClientCapabilities_requests_full_Item1", "delta"}: nothing, + {"Lit_SemanticTokensOptions_full_Item1", "delta"}: nothing, + {"Lit_ServerCapabilities_workspace", "fileOperations"}: nothing, + {"Lit_ServerCapabilities_workspace", "workspaceFolders"}: nothing, + + {"Lit_ShowMessageRequestClientCapabilities_messageActionItem", "additionalPropertiesSupport"}: nothing, + {"Lit_SignatureHelpClientCapabilities_signatureInformation", "activeParameterSupport"}: nothing, + {"Lit_SignatureHelpClientCapabilities_signatureInformation", "documentationFormat"}: nothing, + {"Lit_SignatureHelpClientCapabilities_signatureInformation", "parameterInformation"}: nothing, + {"Lit_SignatureHelpClientCapabilities_signatureInformation_parameterInformation", "labelOffsetSupport"}: nothing, + + {"Lit_TextDocumentContentChangeEvent_Item0", "range"}: wantStar, + {"Lit_TextDocumentContentChangeEvent_Item0", "rangeLength"}: nothing, + {"Lit_TextDocumentFilter_Item0", "pattern"}: nothing, + {"Lit_TextDocumentFilter_Item0", "scheme"}: nothing, + {"Lit_TextDocumentFilter_Item1", "language"}: nothing, + {"Lit_TextDocumentFilter_Item1", "pattern"}: nothing, + + {"Lit_WorkspaceEditClientCapabilities_changeAnnotationSupport", "groupsOnLabel"}: nothing, + {"Lit_WorkspaceSymbolClientCapabilities_symbolKind", "valueSet"}: nothing, + {"Lit__InitializeParams_clientInfo", "version"}: nothing, + + {"Moniker", "kind"}: wantOpt, + {"PartialResultParams", "partialResultToken"}: wantOpt, + {"ResourceOperation", "annotationId"}: wantOpt, + {"ServerCapabilities", "completionProvider"}: wantOpt, + {"ServerCapabilities", "documentLinkProvider"}: wantOpt, + {"ServerCapabilities", "executeCommandProvider"}: wantOpt, + {"ServerCapabilities", "positionEncoding"}: wantOpt, + {"ServerCapabilities", "signatureHelpProvider"}: wantOpt, + {"ServerCapabilities", "workspace"}: wantOpt, + {"TextDocumentClientCapabilities", "codeAction"}: wantOpt, + {"TextDocumentClientCapabilities", "completion"}: wantOpt, + {"TextDocumentClientCapabilities", "documentSymbol"}: wantOpt, + {"TextDocumentClientCapabilities", "foldingRange"}: wantOpt, + {"TextDocumentClientCapabilities", "hover"}: wantOpt, + {"TextDocumentClientCapabilities", "publishDiagnostics"}: wantOpt, + {"TextDocumentClientCapabilities", "rename"}: wantOpt, + {"TextDocumentClientCapabilities", "semanticTokens"}: wantOpt, + {"TextDocumentSyncOptions", "change"}: wantOpt, + {"TextDocumentSyncOptions", "save"}: wantOpt, + {"WorkDoneProgressParams", "workDoneToken"}: wantOpt, + {"WorkspaceClientCapabilities", "didChangeConfiguration"}: wantOpt, + {"WorkspaceClientCapabilities", "didChangeWatchedFiles"}: wantOpt, + {"WorkspaceEditClientCapabilities", "failureHandling"}: wantOpt, + {"XInitializeParams", "clientInfo"}: wantOpt, +} + +// keep track of which entries in goplsStar are used +var usedGoplsStar = make(map[prop]bool) + +// For gopls compatibility, use a different, typically more restrictive, type for some fields. +var renameProp = map[prop]string{ + {"CancelParams", "id"}: "interface{}", + {"Command", "arguments"}: "[]json.RawMessage", + {"CompletionItem", "documentation"}: "string", + {"CompletionItem", "textEdit"}: "TextEdit", + {"Diagnostic", "code"}: "interface{}", + + {"DocumentDiagnosticReportPartialResult", "relatedDocuments"}: "map[DocumentURI]interface{}", + + {"ExecuteCommandParams", "arguments"}: "[]json.RawMessage", + {"FoldingRange", "kind"}: "string", + {"Hover", "contents"}: "MarkupContent", + {"InlayHint", "label"}: "[]InlayHintLabelPart", + + {"Lit_NotebookDocumentChangeEvent_cells", "textContent"}: "[]FTextContentPCells", + {"Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0", "cells"}: "[]FCellsPNotebookSelector", + + {"Lit_SemanticTokensClientCapabilities_requests", "full"}: "interface{}", + {"Lit_SemanticTokensClientCapabilities_requests", "range"}: "bool", + {"NotebookCellTextDocumentFilter", "notebook"}: "NotebookDocumentFilter", + {"RelatedFullDocumentDiagnosticReport", "relatedDocuments"}: "map[DocumentURI]interface{}", + {"RelatedUnchangedDocumentDiagnosticReport", "relatedDocuments"}: "map[DocumentURI]interface{}", + + {"RenameClientCapabilities", "prepareSupportDefaultBehavior"}: "interface{}", + + {"SemanticTokensClientCapabilities", "formats"}: "[]string", + {"SemanticTokensOptions", "full"}: "bool", + {"SemanticTokensOptions", "range"}: "interface{}", + {"ServerCapabilities", "callHierarchyProvider"}: "interface{}", + {"ServerCapabilities", "codeActionProvider"}: "interface{}", + {"ServerCapabilities", "colorProvider"}: "interface{}", + {"ServerCapabilities", "declarationProvider"}: "bool", + {"ServerCapabilities", "definitionProvider"}: "bool", + {"ServerCapabilities", "diagnosticProvider"}: "interface{}", + {"ServerCapabilities", "documentFormattingProvider"}: "bool", + {"ServerCapabilities", "documentHighlightProvider"}: "bool", + {"ServerCapabilities", "documentRangeFormattingProvider"}: "bool", + {"ServerCapabilities", "documentSymbolProvider"}: "bool", + {"ServerCapabilities", "foldingRangeProvider"}: "interface{}", + {"ServerCapabilities", "hoverProvider"}: "bool", + {"ServerCapabilities", "implementationProvider"}: "interface{}", + {"ServerCapabilities", "inlayHintProvider"}: "interface{}", + {"ServerCapabilities", "inlineValueProvider"}: "interface{}", + {"ServerCapabilities", "linkedEditingRangeProvider"}: "interface{}", + {"ServerCapabilities", "monikerProvider"}: "interface{}", + {"ServerCapabilities", "notebookDocumentSync"}: "interface{}", + {"ServerCapabilities", "referencesProvider"}: "bool", + {"ServerCapabilities", "renameProvider"}: "interface{}", + {"ServerCapabilities", "selectionRangeProvider"}: "interface{}", + {"ServerCapabilities", "semanticTokensProvider"}: "interface{}", + {"ServerCapabilities", "textDocumentSync"}: "interface{}", + {"ServerCapabilities", "typeDefinitionProvider"}: "interface{}", + {"ServerCapabilities", "typeHierarchyProvider"}: "interface{}", + {"ServerCapabilities", "workspaceSymbolProvider"}: "bool", + {"SignatureInformation", "documentation"}: "string", + {"TextDocumentEdit", "edits"}: "[]TextEdit", + {"TextDocumentSyncOptions", "save"}: "SaveOptions", + {"WorkspaceEdit", "documentChanges"}: "[]DocumentChanges", +} + +// which entries of renameProp were used +var usedRenameProp = make(map[prop]bool) + +type adjust struct { + prefix, suffix string +} + +// disambiguate specifies prefixes or suffixes to add to all values of +// some enum types to avoid name conflicts +var disambiguate = map[string]adjust{ + "CodeActionTriggerKind": {"CodeAction", ""}, + "CompletionItemKind": {"", "Completion"}, + "CompletionItemTag": {"Compl", ""}, + "DiagnosticSeverity": {"Severity", ""}, + "DocumentDiagnosticReportKind": {"Diagnostic", ""}, + "FileOperationPatternKind": {"", "Pattern"}, + "InsertTextFormat": {"", "TextFormat"}, + "SemanticTokenModifiers": {"Mod", ""}, + "SemanticTokenTypes": {"", "Type"}, + "SignatureHelpTriggerKind": {"Sig", ""}, + "SymbolTag": {"", "Symbol"}, + "WatchKind": {"Watch", ""}, +} + +// which entries of disambiguate got used +var usedDisambiguate = make(map[string]bool) + +// for gopls compatibility, replace generated type names with existing ones +var goplsType = map[string]string{ + "And_RegOpt_textDocument_colorPresentation": "WorkDoneProgressOptionsAndTextDocumentRegistrationOptions", + "ConfigurationParams": "ParamConfiguration", + "DocumentDiagnosticParams": "string", + "DocumentDiagnosticReport": "string", + "DocumentUri": "DocumentURI", + "InitializeParams": "ParamInitialize", + "LSPAny": "interface{}", + + "Lit_CodeActionClientCapabilities_codeActionLiteralSupport": "PCodeActionLiteralSupportPCodeAction", + "Lit_CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind": "FCodeActionKindPCodeActionLiteralSupport", + + "Lit_CodeActionClientCapabilities_resolveSupport": "PResolveSupportPCodeAction", + "Lit_CodeAction_disabled": "PDisabledMsg_textDocument_codeAction", + "Lit_CompletionClientCapabilities_completionItem": "PCompletionItemPCompletion", + "Lit_CompletionClientCapabilities_completionItemKind": "PCompletionItemKindPCompletion", + + "Lit_CompletionClientCapabilities_completionItem_insertTextModeSupport": "FInsertTextModeSupportPCompletionItem", + + "Lit_CompletionClientCapabilities_completionItem_resolveSupport": "FResolveSupportPCompletionItem", + "Lit_CompletionClientCapabilities_completionItem_tagSupport": "FTagSupportPCompletionItem", + + "Lit_CompletionClientCapabilities_completionList": "PCompletionListPCompletion", + "Lit_CompletionList_itemDefaults": "PItemDefaultsMsg_textDocument_completion", + "Lit_CompletionList_itemDefaults_editRange_Item1": "FEditRangePItemDefaults", + "Lit_CompletionOptions_completionItem": "PCompletionItemPCompletionProvider", + "Lit_DocumentSymbolClientCapabilities_symbolKind": "PSymbolKindPDocumentSymbol", + "Lit_DocumentSymbolClientCapabilities_tagSupport": "PTagSupportPDocumentSymbol", + "Lit_FoldingRangeClientCapabilities_foldingRange": "PFoldingRangePFoldingRange", + "Lit_FoldingRangeClientCapabilities_foldingRangeKind": "PFoldingRangeKindPFoldingRange", + "Lit_GeneralClientCapabilities_staleRequestSupport": "PStaleRequestSupportPGeneral", + "Lit_InitializeResult_serverInfo": "PServerInfoMsg_initialize", + "Lit_InlayHintClientCapabilities_resolveSupport": "PResolveSupportPInlayHint", + "Lit_MarkedString_Item1": "Msg_MarkedString", + "Lit_NotebookDocumentChangeEvent_cells": "PCellsPChange", + "Lit_NotebookDocumentChangeEvent_cells_structure": "FStructurePCells", + "Lit_NotebookDocumentChangeEvent_cells_textContent_Elem": "FTextContentPCells", + "Lit_NotebookDocumentFilter_Item0": "Msg_NotebookDocumentFilter", + + "Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0": "PNotebookSelectorPNotebookDocumentSync", + + "Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0_cells_Elem": "FCellsPNotebookSelector", + "Lit_PrepareRenameResult_Item1": "Msg_PrepareRename2Gn", + + "Lit_PublishDiagnosticsClientCapabilities_tagSupport": "PTagSupportPPublishDiagnostics", + "Lit_SemanticTokensClientCapabilities_requests": "PRequestsPSemanticTokens", + "Lit_SemanticTokensClientCapabilities_requests_full_Item1": "FFullPRequests", + "Lit_SemanticTokensClientCapabilities_requests_range_Item1": "FRangePRequests", + + "Lit_SemanticTokensOptions_full_Item1": "PFullESemanticTokensOptions", + "Lit_SemanticTokensOptions_range_Item1": "PRangeESemanticTokensOptions", + "Lit_ServerCapabilities_workspace": "Workspace6Gn", + + "Lit_ShowMessageRequestClientCapabilities_messageActionItem": "PMessageActionItemPShowMessage", + "Lit_SignatureHelpClientCapabilities_signatureInformation": "PSignatureInformationPSignatureHelp", + + "Lit_SignatureHelpClientCapabilities_signatureInformation_parameterInformation": "FParameterInformationPSignatureInformation", + + "Lit_TextDocumentContentChangeEvent_Item0": "Msg_TextDocumentContentChangeEvent", + "Lit_TextDocumentFilter_Item0": "Msg_TextDocumentFilter", + "Lit_TextDocumentFilter_Item1": "Msg_TextDocumentFilter", + "Lit_WorkspaceEditClientCapabilities_changeAnnotationSupport": "PChangeAnnotationSupportPWorkspaceEdit", + "Lit_WorkspaceSymbolClientCapabilities_resolveSupport": "PResolveSupportPSymbol", + "Lit_WorkspaceSymbolClientCapabilities_symbolKind": "PSymbolKindPSymbol", + "Lit_WorkspaceSymbolClientCapabilities_tagSupport": "PTagSupportPSymbol", + "Lit_WorkspaceSymbol_location_Item1": "PLocationMsg_workspace_symbol", + "Lit__InitializeParams_clientInfo": "Msg_XInitializeParams_clientInfo", + "Or_CompletionList_itemDefaults_editRange": "OrFEditRangePItemDefaults", + "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_NotebookDocumentFilter": "Msg_NotebookDocumentFilter", + "Or_NotebookDocumentSyncOptions_notebookSelector_Elem": "PNotebookSelectorPNotebookDocumentSync", + + "Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0_notebook": "OrFNotebookPNotebookSelector", + + "Or_ParameterInformation_documentation": "string", + "Or_ParameterInformation_label": "string", + "Or_PrepareRenameResult": "Msg_PrepareRename2Gn", + "Or_ProgressToken": "interface{}", + "Or_Result_textDocument_completion": "CompletionList", + "Or_Result_textDocument_declaration": "Or_textDocument_declaration", + "Or_Result_textDocument_definition": "[]Location", + "Or_Result_textDocument_documentSymbol": "[]interface{}", + "Or_Result_textDocument_implementation": "[]Location", + "Or_Result_textDocument_semanticTokens_full_delta": "interface{}", + "Or_Result_textDocument_typeDefinition": "[]Location", + "Or_Result_workspace_symbol": "[]SymbolInformation", + "Or_TextDocumentContentChangeEvent": "Msg_TextDocumentContentChangeEvent", + "Or_TextDocumentFilter": "Msg_TextDocumentFilter", + "Or_WorkspaceFoldersServerCapabilities_changeNotifications": "string", + "Or_WorkspaceSymbol_location": "OrPLocation_workspace_symbol", + "PrepareRenameResult": "PrepareRename2Gn", + "Tuple_ParameterInformation_label_Item1": "UIntCommaUInt", + "WorkspaceFoldersServerCapabilities": "WorkspaceFolders5Gn", + "[]LSPAny": "[]interface{}", + "[]Or_NotebookDocumentSyncOptions_notebookSelector_Elem": "[]PNotebookSelectorPNotebookDocumentSync", + "[]Or_Result_textDocument_codeAction_Item0_Elem": "[]CodeAction", + "[]PreviousResultId": "[]PreviousResultID", + "[]uinteger": "[]uint32", + "boolean": "bool", + "decimal": "float64", + "integer": "int32", + "map[DocumentUri][]TextEdit": "map[DocumentURI][]TextEdit", + "uinteger": "uint32", +} + +var usedGoplsType = make(map[string]bool) + +// methodNames is a map from the method to the name of the function that handles it +var methodNames = map[string]string{ + "$/cancelRequest": "CancelRequest", + "$/logTrace": "LogTrace", + "$/progress": "Progress", + "$/setTrace": "SetTrace", + "callHierarchy/incomingCalls": "IncomingCalls", + "callHierarchy/outgoingCalls": "OutgoingCalls", + "client/registerCapability": "RegisterCapability", + "client/unregisterCapability": "UnregisterCapability", + "codeAction/resolve": "ResolveCodeAction", + "codeLens/resolve": "ResolveCodeLens", + "completionItem/resolve": "ResolveCompletionItem", + "documentLink/resolve": "ResolveDocumentLink", + "exit": "Exit", + "initialize": "Initialize", + "initialized": "Initialized", + "inlayHint/resolve": "Resolve", + "notebookDocument/didChange": "DidChangeNotebookDocument", + "notebookDocument/didClose": "DidCloseNotebookDocument", + "notebookDocument/didOpen": "DidOpenNotebookDocument", + "notebookDocument/didSave": "DidSaveNotebookDocument", + "shutdown": "Shutdown", + "telemetry/event": "Event", + "textDocument/codeAction": "CodeAction", + "textDocument/codeLens": "CodeLens", + "textDocument/colorPresentation": "ColorPresentation", + "textDocument/completion": "Completion", + "textDocument/declaration": "Declaration", + "textDocument/definition": "Definition", + "textDocument/diagnostic": "Diagnostic", + "textDocument/didChange": "DidChange", + "textDocument/didClose": "DidClose", + "textDocument/didOpen": "DidOpen", + "textDocument/didSave": "DidSave", + "textDocument/documentColor": "DocumentColor", + "textDocument/documentHighlight": "DocumentHighlight", + "textDocument/documentLink": "DocumentLink", + "textDocument/documentSymbol": "DocumentSymbol", + "textDocument/foldingRange": "FoldingRange", + "textDocument/formatting": "Formatting", + "textDocument/hover": "Hover", + "textDocument/implementation": "Implementation", + "textDocument/inlayHint": "InlayHint", + "textDocument/inlineValue": "InlineValue", + "textDocument/linkedEditingRange": "LinkedEditingRange", + "textDocument/moniker": "Moniker", + "textDocument/onTypeFormatting": "OnTypeFormatting", + "textDocument/prepareCallHierarchy": "PrepareCallHierarchy", + "textDocument/prepareRename": "PrepareRename", + "textDocument/prepareTypeHierarchy": "PrepareTypeHierarchy", + "textDocument/publishDiagnostics": "PublishDiagnostics", + "textDocument/rangeFormatting": "RangeFormatting", + "textDocument/references": "References", + "textDocument/rename": "Rename", + "textDocument/selectionRange": "SelectionRange", + "textDocument/semanticTokens/full": "SemanticTokensFull", + "textDocument/semanticTokens/full/delta": "SemanticTokensFullDelta", + "textDocument/semanticTokens/range": "SemanticTokensRange", + "textDocument/signatureHelp": "SignatureHelp", + "textDocument/typeDefinition": "TypeDefinition", + "textDocument/willSave": "WillSave", + "textDocument/willSaveWaitUntil": "WillSaveWaitUntil", + "typeHierarchy/subtypes": "Subtypes", + "typeHierarchy/supertypes": "Supertypes", + "window/logMessage": "LogMessage", + "window/showDocument": "ShowDocument", + "window/showMessage": "ShowMessage", + "window/showMessageRequest": "ShowMessageRequest", + "window/workDoneProgress/cancel": "WorkDoneProgressCancel", + "window/workDoneProgress/create": "WorkDoneProgressCreate", + "workspace/applyEdit": "ApplyEdit", + "workspace/codeLens/refresh": "CodeLensRefresh", + "workspace/configuration": "Configuration", + "workspace/diagnostic": "DiagnosticWorkspace", + "workspace/diagnostic/refresh": "DiagnosticRefresh", + "workspace/didChangeConfiguration": "DidChangeConfiguration", + "workspace/didChangeWatchedFiles": "DidChangeWatchedFiles", + "workspace/didChangeWorkspaceFolders": "DidChangeWorkspaceFolders", + "workspace/didCreateFiles": "DidCreateFiles", + "workspace/didDeleteFiles": "DidDeleteFiles", + "workspace/didRenameFiles": "DidRenameFiles", + "workspace/executeCommand": "ExecuteCommand", + "workspace/inlayHint/refresh": "InlayHintRefresh", + "workspace/inlineValue/refresh": "InlineValueRefresh", + "workspace/semanticTokens/refresh": "SemanticTokensRefresh", + "workspace/symbol": "Symbol", + "workspace/willCreateFiles": "WillCreateFiles", + "workspace/willDeleteFiles": "WillDeleteFiles", + "workspace/willRenameFiles": "WillRenameFiles", + "workspace/workspaceFolders": "WorkspaceFolders", + "workspaceSymbol/resolve": "ResolveWorkspaceSymbol", +} diff --git a/gopls/internal/lsp/protocol/generate/typenames.go b/gopls/internal/lsp/protocol/generate/typenames.go new file mode 100644 index 00000000000..237d19e4f06 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/typenames.go @@ -0,0 +1,184 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import ( + "fmt" + "log" + "strings" +) + +var typeNames = make(map[*Type]string) +var genTypes = make(map[*Type]*newType) + +func findTypeNames(model Model) { + for _, s := range model.Structures { + for _, e := range s.Extends { + nameType(e, nil) // all references + } + for _, m := range s.Mixins { + nameType(m, nil) // all references + } + for _, p := range s.Properties { + nameType(p.Type, []string{s.Name, p.Name}) + } + } + for _, t := range model.Enumerations { + nameType(t.Type, []string{t.Name}) + } + for _, t := range model.TypeAliases { + nameType(t.Type, []string{t.Name}) + } + for _, r := range model.Requests { + nameType(r.Params, []string{"Param", r.Method}) + nameType(r.Result, []string{"Result", r.Method}) + nameType(r.RegistrationOptions, []string{"RegOpt", r.Method}) + } + for _, n := range model.Notifications { + nameType(n.Params, []string{"Param", n.Method}) + nameType(n.RegistrationOptions, []string{"RegOpt", n.Method}) + } +} + +// nameType populates typeNames[t] with the computed name of the type. +// path is the list of enclosing constructs in the JSON model. +func nameType(t *Type, path []string) string { + if t == nil || typeNames[t] != "" { + return "" + } + switch t.Kind { + case "base": + typeNames[t] = t.Name + return t.Name + case "reference": + typeNames[t] = t.Name + return t.Name + case "array": + nm := "[]" + nameType(t.Element, append(path, "Elem")) + typeNames[t] = nm + return nm + case "map": + key := nameType(t.Key, nil) // never a generated type + value := nameType(t.Value.(*Type), append(path, "Value")) + nm := "map[" + key + "]" + value + typeNames[t] = nm + return nm + // generated types + case "and": + nm := nameFromPath("And", path) + typeNames[t] = nm + for _, it := range t.Items { + nameType(it, append(path, "Item")) + } + genTypes[t] = &newType{ + name: nm, + typ: t, + kind: "and", + items: t.Items, + line: t.Line, + } + return nm + case "literal": + nm := nameFromPath("Lit", path) + typeNames[t] = nm + for _, p := range t.Value.(ParseLiteral).Properties { + nameType(p.Type, append(path, p.Name)) + } + genTypes[t] = &newType{ + name: nm, + typ: t, + kind: "literal", + properties: t.Value.(ParseLiteral).Properties, + line: t.Line, + } + return nm + case "tuple": + nm := nameFromPath("Tuple", path) + typeNames[t] = nm + for _, it := range t.Items { + nameType(it, append(path, "Item")) + } + genTypes[t] = &newType{ + name: nm, + typ: t, + kind: "tuple", + items: t.Items, + line: t.Line, + } + return nm + case "or": + nm := nameFromPath("Or", path) + typeNames[t] = nm + for i, it := range t.Items { + // these names depend on the ordering within the "or" type + nameType(it, append(path, fmt.Sprintf("Item%d", i))) + } + // this code handles an "or" of stringLiterals (_InitializeParams.trace) + names := make(map[string]int) + msg := "" + for _, it := range t.Items { + if line, ok := names[typeNames[it]]; ok { + // duplicate component names are bad + msg += fmt.Sprintf("lines %d %d dup, %s for %s\n", line, it.Line, typeNames[it], nm) + } + names[typeNames[it]] = t.Line + } + // this code handles an "or" of stringLiterals (_InitializeParams.trace) + if len(names) == 1 { + var solekey string + for k := range names { + solekey = k // the sole name + } + if solekey == "string" { // _InitializeParams.trace + typeNames[t] = "string" + return "string" + } + // otherwise unexpected + log.Printf("unexpected: single-case 'or' type has non-string key %s: %s", nm, solekey) + log.Fatal(msg) + } else if len(names) == 2 { + // if one of the names is null, just use the other, rather than generating an "or". + // This removes about 40 types from the generated code. An entry in goplsStar + // could be added to handle the null case, if necessary. + newNm := "" + sawNull := false + for k := range names { + if k == "null" { + sawNull = true + } else { + newNm = k + } + } + if sawNull { + typeNames[t] = newNm + return newNm + } + } + genTypes[t] = &newType{ + name: nm, + typ: t, + kind: "or", + items: t.Items, + line: t.Line, + } + return nm + case "stringLiteral": // a single type, like 'kind' or 'rename' + typeNames[t] = "string" + return "string" + default: + log.Fatalf("nameType: %T unexpected, line:%d path:%v", t, t.Line, path) + panic("unreachable in nameType") + } +} + +func nameFromPath(prefix string, path []string) string { + nm := prefix + "_" + strings.Join(path, "_") + // methods have slashes + nm = strings.ReplaceAll(nm, "/", "_") + return nm +} diff --git a/gopls/internal/lsp/protocol/generate/types.go b/gopls/internal/lsp/protocol/generate/types.go index e8abd600815..c509031fabf 100644 --- a/gopls/internal/lsp/protocol/generate/types.go +++ b/gopls/internal/lsp/protocol/generate/types.go @@ -7,17 +7,20 @@ package main -import "sort" +import ( + "fmt" + "sort" +) // Model contains the parsed version of the spec type Model struct { - Version Metadata `json:"metaData"` - Requests []Request `json:"requests"` - Notifications []Notification `json:"notifications"` - Structures []Structure `json:"structures"` - Enumerations []Enumeration `json:"enumerations"` - TypeAliases []TypeAlias `json:"typeAliases"` - Line int `json:"line"` + Version Metadata `json:"metaData"` + Requests []*Request `json:"requests"` + Notifications []*Notification `json:"notifications"` + Structures []*Structure `json:"structures"` + Enumerations []*Enumeration `json:"enumerations"` + TypeAliases []*TypeAlias `json:"typeAliases"` + Line int `json:"line"` } // Metadata is information about the version of the spec @@ -99,33 +102,6 @@ type NameValue struct { Line int `json:"line"` } -// common to Request and Notification -type Message interface { - direction() string -} - -func (r Request) direction() string { - return r.Direction -} - -func (n Notification) direction() string { - return n.Direction -} - -// A Defined is one of Structure, Enumeration, TypeAlias, for type checking -type Defined interface { - tag() -} - -func (s Structure) tag() { -} - -func (e Enumeration) tag() { -} - -func (ta TypeAlias) tag() { -} - // A Type is the parsed version of an LSP type from the spec, // or a Type the code constructs type Type struct { @@ -135,11 +111,7 @@ type Type struct { Name string `json:"name"` // "base", "reference" Key *Type `json:"key"` // "map" Value any `json:"value"` // "map", "stringLiteral", "literal" - // used to tie generated code to the specification - Line int `json:"line"` - - name string // these are generated names, like Uint32 - typeName string // these are actual type names, like uint32 + Line int `json:"line"` // JSON source line } // ParsedLiteral is Type.Value when Type.Kind is "literal" @@ -158,9 +130,33 @@ type NameType struct { Line int `json:"line"` } -// Properties are the collection of structure elements +// Properties are the collection of structure fields type Properties []NameType +// addLineNumbers adds a "line" field to each object in the JSON. +func addLineNumbers(buf []byte) []byte { + var ans []byte + // In the specification .json file, the delimiter '{' is + // always followed by a newline. There are other {s embedded in strings. + // json.Token does not return \n, or :, or , so using it would + // require parsing the json to reconstruct the missing information. + // TODO(pjw): should linecnt start at 1 (editor) or 0 (compatibility)? + for linecnt, i := 0, 0; i < len(buf); i++ { + ans = append(ans, buf[i]) + switch buf[i] { + case '{': + if buf[i+1] == '\n' { + ans = append(ans, fmt.Sprintf(`"line": %d, `, linecnt)...) + // warning: this would fail if the spec file had + // `"value": {\n}`, but it does not, as comma is a separator. + } + case '\n': + linecnt++ + } + } + return ans +} + type sortedMap[T any] map[string]T func (s sortedMap[T]) keys() []string { diff --git a/gopls/internal/lsp/protocol/generate/utilities.go b/gopls/internal/lsp/protocol/generate/utilities.go deleted file mode 100644 index b091a0d145f..00000000000 --- a/gopls/internal/lsp/protocol/generate/utilities.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.19 -// +build go1.19 - -package main - -import ( - "fmt" - "log" - "runtime" - "strings" - "time" -) - -// goName returns the Go version of a name. -func goName(s string) string { - if s == "" { - return s // doesn't happen - } - s = strings.ToUpper(s[:1]) + s[1:] - if rest := strings.TrimSuffix(s, "Uri"); rest != s { - s = rest + "URI" - } - if rest := strings.TrimSuffix(s, "Id"); rest != s { - s = rest + "ID" - } - return s -} - -// the common header for all generated files -func (s *spec) createHeader() string { - format := `// Copyright 2022 The Go Authors. All rights reserved. - // Use of this source code is governed by a BSD-style - // license that can be found in the LICENSE file. - - // Code generated for LSP. DO NOT EDIT. - - package protocol - - // Code generated from version %s of protocol/metaModel.json. - // git hash %s (as of %s) - - ` - hdr := fmt.Sprintf(format, s.model.Version.Version, s.githash, s.modTime.Format(time.ANSIC)) - return hdr -} - -// useful in debugging -func here() { - _, f, l, _ := runtime.Caller(1) - log.Printf("here: %s:%d", f, l) -} diff --git a/gopls/internal/lsp/protocol/typescript/README.md b/gopls/internal/lsp/protocol/typescript/README.md deleted file mode 100644 index 74bcd1883d1..00000000000 --- a/gopls/internal/lsp/protocol/typescript/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Generate Go types and signatures for the LSP protocol - -## Setup - -Make sure `node` and `tsc` are installed and in your PATH. There are detailed instructions below. -(`tsc -v` should be at least `4.2.4`.) -Get the typescript code for the jsonrpc protocol with - -`git clone git@github.com:microsoft vscode-languageserver-node.git` or -`git clone https://github.com/microsoft/vscode-languageserver-node.git` - -`util.ts` expects it to be in your HOME directory - -If you want to reproduce the existing files you need to be on a branch with the same git hash that `util.ts` expects, for instance, `git checkout 7b90c29` - -## Usage - -Code is generated and normalized by - -`tsc && node code.js && gofmt -w ts*.go` - -(`code.ts` imports `util.ts`.) This generates 3 files in the current directory, `tsprotocol.go` -containing type definitions, and `tsserver.go`, `tsclient.go` containing API stubs. - -## Notes - -1. `code.ts` and `util.ts` use the Typescript compiler's API, which is [introduced](https://github.com/Microsoft/TypeScript/wiki/Architectural-Overview) in their wiki. -2. Because the Typescript and Go type systems are incompatible, `code.ts` and `util.ts` are filled with heuristics and special cases. Therefore they are tied to a specific commit of `vscode-languageserver-node`. The hash code of the commit is included in the header of -the generated files and stored in the variable `gitHash` in `go.ts`. It is checked (see `git()` in `util.ts`) on every execution. -3. Generating the `ts*.go` files is only semi-automated. Please file an issue if the released version is too far behind. -4. For the impatient, first change `gitHash` by hand (`git()` shows how to find the hash). - 1. Then try to run `code.ts`. This will likely fail because the heuristics don't cover some new case. For instance, some simple type like `string` might have changed to a union type `string | [number,number]`. Another example is that some generated formal parameter may have anonymous structure type, which is essentially unusable. - 2. Next step is to move the generated code to `internal/lsp/protocol` and try to build `gopls` and its tests. This will likely fail because types have changed. Generally the fixes are fairly easy. Then run all the tests. - 3. Since there are not adequate integration tests, the next step is to run `gopls`. - -## Detailed instructions for installing node and typescript - -(The instructions are somewhat different for Linux and MacOS. They install some things locally, so `$PATH` needs to be changed.) - -1. For Linux, it is possible to build node from scratch, but if there's a package manager, that's simpler. - 1. To use the Ubuntu package manager - 1. `sudo apt update` (if you can't `sudo` then these instructions are not helpful) - 2. `sudo apt install nodejs` (this may install `/usr/bin/nodejs` rather than `/usr/bin/node`. For me, `/usr/bin/nodejs` pointed to an actual executable `/etc/alternatives/nodejs`, which should be copied to `/usr/bin/node`) - 3. `sudo apt intall npm` - 1. To build from scratch - 1. Go to the [node site](https://nodejs.org), and download the one recommended for most users, and then you're on your own. (It's got binaries in it. Untar the file somewhere and put its `bin` directory in your path, perhaps?) -2. The Mac is easier. Download the macOS installer from [nodejs](https://nodejs.org), click on it, and let it install. -3. (There's a good chance that soon you will be asked to upgrade your new npm. `sudo npm install -g npm` is the command.) -4. For either system, node and nvm should now be available. Running `node -v` and `npm -v` should produce version numbers. -5. `npm install typescript` - 1. This may give warning messages that indicate you've failed to set up a project. Ignore them. - 2. Your home directory will now have new directories `.npm` and `node_modules` (and a `package_lock.json` file) - 3. The typescript executable `tsc` will be in `node_modules/.bin`, so put that directory in your path. - 4. `tsc -v` should print "Version 4.2.4" (or later). If not you may (as I did) have an obsolete tsc earlier in your path. -6. `npm install @types/node` (Without this there will be many incomprehensible typescript error messages.) diff --git a/gopls/internal/lsp/protocol/typescript/code.ts b/gopls/internal/lsp/protocol/typescript/code.ts deleted file mode 100644 index 1eefa55903a..00000000000 --- a/gopls/internal/lsp/protocol/typescript/code.ts +++ /dev/null @@ -1,1450 +0,0 @@ -/* eslint-disable no-useless-return */ -// read files from vscode-languageserver-node, and generate Go rpc stubs -// and data definitions. (and maybe someday unmarshaling code) - -// The output is 3 files, tsprotocol.go contains the type definitions -// while tsclient.go and tsserver.go contain the LSP API and stub. An LSP server -// uses both APIs. To read the code, start in this file's main() function. - -// The code is rich in heuristics and special cases, some of which are to avoid -// extensive changes to gopls, and some of which are due to the mismatch between -// typescript and Go types. In particular, there is no Go equivalent to union -// types, so each case ought to be considered separately. The Go equivalent of A -// & B could frequently be struct{A;B;}, or it could be the equivalent type -// listing all the members of A and B. Typically the code uses the former, but -// especially if A and B have elements with the same name, it does a version of -// the latter. ClientCapabilities has to be expanded, and ServerCapabilities is -// expanded to make the generated code easier to read. - -// for us typescript ignorati, having an import makes this file a module -import * as fs from 'fs'; -import * as ts from 'typescript'; -import * as u from './util'; -import { constName, getComments, goName, loc, strKind } from './util'; - -var program: ts.Program; - -function parse() { - // this won't complain if some fnames don't exist - program = ts.createProgram( - u.fnames, - { target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS }); - program.getTypeChecker(); // finish type checking and assignment -} - -// ----- collecting information for RPCs -let req = new Map(); // requests -let not = new Map(); // notifications -let ptypes = new Map(); // req, resp types -let receives = new Map(); // who receives it -let rpcTypes = new Set(); // types seen in the rpcs - -function findRPCs(node: ts.Node) { - if (!ts.isModuleDeclaration(node)) { - return; - } - if (!ts.isIdentifier(node.name)) { - throw new Error( - `expected Identifier, got ${strKind(node.name)} at ${loc(node)}`); - } - let reqnot = req; - let v = node.name.getText(); - if (v.endsWith('Notification')) reqnot = not; - else if (!v.endsWith('Request')) return; - - if (!ts.isModuleBlock(node.body)) { - throw new Error( - `expected ModuleBlock got ${strKind(node.body)} at ${loc(node)}`); - } - let x: ts.ModuleBlock = node.body; - // The story is to expect const method = 'textDocument/implementation' - // const type = new ProtocolRequestType<...>(method) - // but the method may be an explicit string - let rpc: string = ''; - let newNode: ts.NewExpression; - for (let i = 0; i < x.statements.length; i++) { - const uu = x.statements[i]; - if (!ts.isVariableStatement(uu)) continue; - const dl: ts.VariableDeclarationList = uu.declarationList; - if (dl.declarations.length != 1) - throw new Error(`expected a single decl at ${loc(dl)}`); - const decl: ts.VariableDeclaration = dl.declarations[0]; - const name = decl.name.getText(); - // we want the initializers - if (name == 'method') { // mostly StringLiteral but NoSubstitutionTemplateLiteral in protocol.semanticTokens.ts - if (!ts.isStringLiteral(decl.initializer)) { - if (!ts.isNoSubstitutionTemplateLiteral(decl.initializer)) { - console.log(`81: ${decl.initializer.getText()}`); - throw new Error(`expect StringLiteral at ${loc(decl)} got ${strKind(decl.initializer)}`); - } - } - rpc = decl.initializer.getText(); - } - else if (name == 'type') { // NewExpression - if (!ts.isNewExpression(decl.initializer)) - throw new Error(`89 expected new at ${loc(decl)}`); - const nn: ts.NewExpression = decl.initializer; - newNode = nn; - const mtd = nn.arguments[0]; - if (ts.isStringLiteral(mtd)) rpc = mtd.getText(); - switch (nn.typeArguments.length) { - case 1: // exit - ptypes.set(rpc, [nn.typeArguments[0], null]); - break; - case 2: // notifications - ptypes.set(rpc, [nn.typeArguments[0], null]); - break; - case 4: // request with no parameters - ptypes.set(rpc, [null, nn.typeArguments[0]]); - break; - case 5: // request req, resp, partial(?) - ptypes.set(rpc, [nn.typeArguments[0], nn.typeArguments[1]]); - break; - default: - throw new Error(`${nn.typeArguments?.length} at ${loc(nn)}`); - } - } - } - if (rpc == '') throw new Error(`112 no name found at ${loc(x)}`); - // remember the implied types - const [a, b] = ptypes.get(rpc); - const add = function (n: ts.Node) { - rpcTypes.add(goName(n.getText())); - }; - underlying(a, add); - underlying(b, add); - rpc = rpc.substring(1, rpc.length - 1); // 'exit' - reqnot.set(rpc, newNode); -} - -function setReceives() { - // mark them all as server, then adjust the client ones. - // it would be nice to have some independent check on this - // (this logic fails if the server ever sends $/canceRequest - // or $/progress) - req.forEach((_, k) => { receives.set(k, 'server'); }); - not.forEach((_, k) => { receives.set(k, 'server'); }); - receives.set('window/showMessage', 'client'); - receives.set('window/showMessageRequest', 'client'); - receives.set('window/logMessage', 'client'); - receives.set('telemetry/event', 'client'); - receives.set('client/registerCapability', 'client'); - receives.set('client/unregisterCapability', 'client'); - receives.set('workspace/workspaceFolders', 'client'); - receives.set('workspace/configuration', 'client'); - receives.set('workspace/applyEdit', 'client'); - receives.set('textDocument/publishDiagnostics', 'client'); - receives.set('window/workDoneProgress/create', 'client'); - receives.set('window/showDocument', 'client'); - receives.set('$/progress', 'client'); - // a small check - receives.forEach((_, k) => { - if (!req.get(k) && !not.get(k)) throw new Error(`145 missing ${k}}`); - if (req.get(k) && not.get(k)) throw new Error(`146 dup ${k}`); - }); -} - -type DataKind = 'module' | 'interface' | 'alias' | 'enum' | 'class'; - -interface Data { - kind: DataKind; - me: ts.Node; // root node for this type - name: string; // Go name - origname: string; // their name - generics: ts.NodeArray; - as: ts.NodeArray; // inheritance - // Interface - properties: ts.NodeArray - alias: ts.TypeNode; // type alias - // module - statements: ts.NodeArray; - enums: ts.NodeArray; - // class - members: ts.NodeArray; -} -function newData(n: ts.Node, nm: string, k: DataKind, origname: string): Data { - return { - kind: k, - me: n, name: goName(nm), origname: origname, - generics: ts.factory.createNodeArray(), - as: ts.factory.createNodeArray(), - properties: ts.factory.createNodeArray(), alias: undefined, - statements: ts.factory.createNodeArray(), - enums: ts.factory.createNodeArray(), - members: ts.factory.createNodeArray(), - }; -} - -// for debugging, produce a skeleton description -function strData(d: Data): string { - if (!d) { return 'nil'; } - const f = function (na: ts.NodeArray): number { - return na.length; - }; - const nm = d.name == d.origname ? `${d.name}` : `${d.name}/${d.origname}`; - return `g:${f(d.generics)} a:${f(d.as)} p:${f(d.properties)} s:${f(d.statements)} e:${f(d.enums)} m:${f(d.members)} a:${d.alias !== undefined} D(${nm}) k:${d.kind}`; -} - -let data = new Map(); // parsed data types -let seenTypes = new Map(); // type names we've seen -let extraTypes = new Map(); // to avoid struct params - -function setData(nm: string, d: Data) { - const v = data.get(nm); - if (!v) { - data.set(nm, d); - return; - } - // if there are multiple definitions of the same name, decide what to do. - // For now the choices are only aliases and modules - // alias is preferred unless the constant values are needed - if (nm === 'PrepareSupportDefaultBehavior') { - // want the alias, as we're going to change the type and can't afford a constant - if (d.kind === 'alias') data.set(nm, d); - else if (v.kind == 'alias') data.set(nm, v); - else throw new Error(`208 ${d.kind} ${v.kind}`); - return; - } - if (nm === 'CodeActionKind') { - // want the module, need the constants - if (d.kind === 'module') data.set(nm, d); - else if (v.kind === 'module') data.set(nm, v); - else throw new Error(`215 ${d.kind} ${v.kind}`); - } - if (v.kind === 'alias' && d.kind !== 'alias') return; - if (d.kind === 'alias' && v.kind !== 'alias') { - data.set(nm, d); - return; - } - if (v.kind === 'alias' && d.kind === 'alias') return; - // protocol/src/common/protocol.foldingRange.ts 44: 1 (39: 2) and - // types/src/main.ts 397: 1 (392: 2) - // for FoldingRangeKind - if (d.me.getText() === v.me.getText()) return; - // error messages for an unexpected case - console.log(`228 ${strData(v)} ${loc(v.me)} for`); - console.log(`229 ${v.me.getText().replace(/\n/g, '\\n')}`); - console.log(`230 ${strData(d)} ${loc(d.me)}`); - console.log(`231 ${d.me.getText().replace(/\n/g, '\\n')}`); - throw new Error(`232 setData found ${v.kind} for ${d.kind}`); -} - -// look at top level data definitions -function genTypes(node: ts.Node) { - // Ignore top-level items that can't produce output - if (ts.isExpressionStatement(node) || ts.isFunctionDeclaration(node) || - ts.isImportDeclaration(node) || ts.isVariableStatement(node) || - ts.isExportDeclaration(node) || ts.isEmptyStatement(node) || - ts.isExportAssignment(node) || ts.isImportEqualsDeclaration(node) || - ts.isBlock(node) || node.kind == ts.SyntaxKind.EndOfFileToken) { - return; - } - if (ts.isInterfaceDeclaration(node)) { - const v: ts.InterfaceDeclaration = node; - // need to check the members, many of which are disruptive - let mems: ts.PropertySignature[] = []; - const f = function (t: ts.TypeElement) { - if (ts.isPropertySignature(t)) { - mems.push(t); - } else if (ts.isMethodSignature(t) || ts.isCallSignatureDeclaration(t)) { - return; - } else if (ts.isIndexSignatureDeclaration(t)) { - // probably safe to ignore these - // [key: string]: boolean | number | string | undefined; - // and InitializeResult: [custom: string]: any;] - } else - throw new Error(`259 unexpected ${strKind(t)}`); - }; - v.members.forEach(f); - if (mems.length == 0 && !v.heritageClauses && - v.name.getText() != 'InitializedParams') { - return; // Don't seem to need any of these [Logger, PipTransport, ...] - } - // Found one we want - let x = newData(v, goName(v.name.getText()), 'interface', v.name.getText()); - x.properties = ts.factory.createNodeArray(mems); - if (v.typeParameters) x.generics = v.typeParameters; - if (v.heritageClauses) x.as = v.heritageClauses; - if (x.generics.length > 1) { // Unneeded - // Item interface Item... - return; - } - if (data.has(x.name)) { // modifying one we've seen - x = dataChoose(x, data.get(x.name)); - } - setData(x.name, x); - } else if (ts.isTypeAliasDeclaration(node)) { - const v: ts.TypeAliasDeclaration = node; - let x = newData(v, v.name.getText(), 'alias', v.name.getText()); - x.alias = v.type; - // if type is a union of constants, we (mostly) don't want it - // (at the top level) - // Unfortunately this is false for TraceValues - if (ts.isUnionTypeNode(v.type) && - v.type.types.every((n: ts.TypeNode) => ts.isLiteralTypeNode(n))) { - if (x.name != 'TraceValues') return; - } - if (v.typeParameters) { - x.generics = v.typeParameters; - } - if (data.has(x.name)) x = dataChoose(x, data.get(x.name)); - if (x.generics.length > 1) { - return; - } - setData(x.name, x); - } else if (ts.isModuleDeclaration(node)) { - const v: ts.ModuleDeclaration = node; - if (!ts.isModuleBlock(v.body)) { - throw new Error(`${loc(v)} not ModuleBlock, but ${strKind(v.body)}`); - } - const b: ts.ModuleBlock = v.body; - var s: ts.Statement[] = []; - // we don't want most of these - const fx = function (x: ts.Statement) { - if (ts.isFunctionDeclaration(x)) { - return; - } - if (ts.isTypeAliasDeclaration(x) || ts.isModuleDeclaration(x)) { - return; - } - if (!ts.isVariableStatement(x)) - throw new Error( - `315 expected VariableStatment ${loc(x)} ${strKind(x)} ${x.getText()}`); - if (hasNewExpression(x)) { - return; - } - s.push(x); - }; - b.statements.forEach(fx); - if (s.length == 0) { - return; - } - let m = newData(node, v.name.getText(), 'module', v.name.getText()); - m.statements = ts.factory.createNodeArray(s); - if (data.has(m.name)) m = dataChoose(m, data.get(m.name)); - setData(m.name, m); - } else if (ts.isEnumDeclaration(node)) { - const nm = node.name.getText(); - let v = newData(node, nm, 'enum', node.name.getText()); - v.enums = node.members; - if (data.has(nm)) { - v = dataChoose(v, data.get(nm)); - } - setData(nm, v); - } else if (ts.isClassDeclaration(node)) { - const v: ts.ClassDeclaration = node; - var d: ts.PropertyDeclaration[] = []; - const wanted = function (c: ts.ClassElement): string { - if (ts.isConstructorDeclaration(c)) { - return ''; - } - if (ts.isMethodDeclaration(c)) { - return ''; - } - if (ts.isGetAccessor(c)) { - return ''; - } - if (ts.isSetAccessor(c)) { - return ''; - } - if (ts.isPropertyDeclaration(c)) { - d.push(c); - return strKind(c); - } - throw new Error(`Class decl ${strKind(c)} `); - }; - v.members.forEach((c) => wanted(c)); - if (d.length == 0) { - return; - } // don't need it - let c = newData(v, v.name.getText(), 'class', v.name.getText()); - c.members = ts.factory.createNodeArray(d); - if (v.typeParameters) { - c.generics = v.typeParameters; - } - if (c.generics.length > 1) { - return; - } - if (v.heritageClauses) { - c.as = v.heritageClauses; - } - if (data.has(c.name)) - throw new Error(`Class dup ${loc(c.me)} and ${loc(data.get(c.name).me)}`); - setData(c.name, c); - } else { - throw new Error(`378 unexpected ${strKind(node)} ${loc(node)} `); - } -} - -// Typescript can accumulate, but this chooses one or the other -function dataChoose(a: Data, b: Data): Data { - // maybe they are textually identical? (e.g., FoldingRangeKind) - const [at, bt] = [a.me.getText(), b.me.getText()]; - if (at == bt) { - return a; - } - switch (a.name) { - case 'InitializeError': - case 'CompletionItemTag': - case 'SymbolTag': - case 'CodeActionKind': - case 'Integer': - case 'Uinteger': - case 'Decimal': - // want the Module, if anything - return a.statements.length > 0 ? a : b; - case 'CancellationToken': - case 'CancellationStrategy': - // want the Interface - return a.properties.length > 0 ? a : b; - case 'TextDocumentContentChangeEvent': // almost the same - case 'TokenFormat': - case 'PrepareSupportDefaultBehavior': - return a; - } - console.log( - `409 ${strKind(a.me)} ${strKind(b.me)} ${a.name} ${loc(a.me)} ${loc(b.me)}`); - throw new Error(`410 Fix dataChoose for ${a.name}`); -} - -// is a node an ancestor of a NewExpression -function hasNewExpression(n: ts.Node): boolean { - let ans = false; - n.forEachChild((n: ts.Node) => { - if (ts.isNewExpression(n)) ans = true; - }); - return ans; -} - -function checkOnce() { - // Data for all the rpc types? - rpcTypes.forEach(s => { - if (!data.has(s)) throw new Error(`checkOnce, ${s}?`); - }); -} - -// helper function to find underlying types -// eslint-disable-next-line no-unused-vars -function underlying(n: ts.Node | undefined, f: (n: ts.Node) => void) { - if (!n) return; - const ff = function (n: ts.Node) { - underlying(n, f); - }; - if (ts.isIdentifier(n)) { - f(n); - } else if ( - n.kind == ts.SyntaxKind.StringKeyword || - n.kind == ts.SyntaxKind.NumberKeyword || - n.kind == ts.SyntaxKind.AnyKeyword || - n.kind == ts.SyntaxKind.UnknownKeyword || - n.kind == ts.SyntaxKind.NullKeyword || - n.kind == ts.SyntaxKind.BooleanKeyword || - n.kind == ts.SyntaxKind.ObjectKeyword || - n.kind == ts.SyntaxKind.VoidKeyword) { - // nothing to do - } else if (ts.isTypeReferenceNode(n)) { - f(n.typeName); - } else if (ts.isArrayTypeNode(n)) { - underlying(n.elementType, f); - } else if (ts.isHeritageClause(n)) { - n.types.forEach(ff); - } else if (ts.isExpressionWithTypeArguments(n)) { - underlying(n.expression, f); - } else if (ts.isPropertySignature(n)) { - underlying(n.type, f); - } else if (ts.isTypeLiteralNode(n)) { - n.members.forEach(ff); - } else if (ts.isUnionTypeNode(n) || ts.isIntersectionTypeNode(n)) { - n.types.forEach(ff); - } else if (ts.isIndexSignatureDeclaration(n)) { - underlying(n.type, f); - } else if (ts.isParenthesizedTypeNode(n)) { - underlying(n.type, f); - } else if ( - ts.isLiteralTypeNode(n) || ts.isVariableStatement(n) || - ts.isTupleTypeNode(n)) { - // we only see these in moreTypes, but they are handled elsewhere - } else if (ts.isEnumMember(n)) { - if (ts.isStringLiteral(n.initializer)) return; - throw new Error(`472 EnumMember ${strKind(n.initializer)} ${n.name.getText()}`); - } else { - throw new Error(`474 saw ${strKind(n)} in underlying. ${n.getText()} at ${loc(n)}`); - } -} - -// find all the types implied by seenTypes. -// Simplest way to the transitive closure is to stabilize the size of seenTypes -// but it is slow -function moreTypes() { - const extra = function (s: string) { - if (!data.has(s)) throw new Error(`moreTypes needs ${s}`); - seenTypes.set(s, data.get(s)); - }; - rpcTypes.forEach(extra); // all the types needed by the rpcs - // needed in enums.go (or elsewhere) - extra('InitializeError'); - extra('WatchKind'); - extra('FoldingRangeKind'); - // not sure why these weren't picked up - extra('DidChangeWatchedFilesRegistrationOptions'); - extra('WorkDoneProgressBegin'); - extra('WorkDoneProgressReport'); - extra('WorkDoneProgressEnd'); - let old = 0; - do { - old = seenTypes.size; - - const m = new Map(); - const add = function (n: ts.Node) { - const nm = goName(n.getText()); - if (seenTypes.has(nm) || m.has(nm)) return; - if (data.get(nm)) { - m.set(nm, data.get(nm)); - } - }; - // expect all the heritage clauses have single Identifiers - const h = function (n: ts.Node) { - underlying(n, add); - }; - const f = function (x: ts.NodeArray) { - x.forEach(h); - }; - seenTypes.forEach((d: Data) => d && f(d.as)); - // find the types in the properties - seenTypes.forEach((d: Data) => d && f(d.properties)); - // and in the alias and in the statements and in the enums - seenTypes.forEach((d: Data) => d && underlying(d.alias, add)); - seenTypes.forEach((d: Data) => d && f(d.statements)); - seenTypes.forEach((d: Data) => d && f(d.enums)); - m.forEach((d, k) => seenTypes.set(k, d)); - } - while (seenTypes.size != old) - ; -} - -function cleanData() { // middle pass - // seenTypes contains all the top-level types. - seenTypes.forEach((d) => { - if (d.kind == 'alias') mergeAlias(d); - }); -} - -function sameType(a: ts.TypeNode, b: ts.TypeNode): boolean { - if (a.kind !== b.kind) return false; - if (a.kind === ts.SyntaxKind.BooleanKeyword) return true; - if (a.kind === ts.SyntaxKind.StringKeyword) return true; - if (ts.isTypeReferenceNode(a) && ts.isTypeReferenceNode(b) && - a.typeName.getText() === b.typeName.getText()) return true; - if (ts.isArrayTypeNode(a) && ts.isArrayTypeNode(b)) return sameType(a.elementType, b.elementType); - if (ts.isTypeLiteralNode(a) && ts.isTypeLiteralNode(b)) { - if (a.members.length !== b.members.length) return false; - if (a.members.length === 1) return a.members[0].name.getText() === b.members[0].name.getText(); - if (loc(a) === loc(b)) return true; - } - throw new Error(`544 sameType? ${strKind(a)} ${strKind(b)} ${a.getText()}`); -} -type CreateMutable = { - -readonly [Property in keyof Type]: Type[Property]; -}; -type propMap = Map; -function propMapSet(pm: propMap, name: string, v: ts.PropertySignature) { - if (!pm.get(name)) { - try { getComments(v); } catch (e) { console.log(`552 ${name} ${e}`); } - pm.set(name, v); - return; - } - const a = pm.get(name).type; - const b = v.type; - if (sameType(a, b)) { - return; - } - if (ts.isTypeReferenceNode(a) && ts.isTypeLiteralNode(b)) { - const x = mergeTypeRefLit(a, b); - const fake: CreateMutable = v; - fake['type'] = x; - check(fake as ts.PropertySignature, '565'); - pm.set(name, fake as ts.PropertySignature); - return; - } - if (ts.isTypeLiteralNode(a) && ts.isTypeLiteralNode(b)) { - const x = mergeTypeLitLit(a, b); - const fake: CreateMutable = v; - fake['type'] = x; - check(fake as ts.PropertySignature, '578'); - pm.set(name, fake as ts.PropertySignature); - return; - } - console.log(`577 ${pm.get(name).getText()}\n${v.getText()}`); - throw new Error(`578 should merge ${strKind(a)} and ${strKind(b)} for ${name}`); -} -function addToProperties(pm: propMap, tn: ts.TypeNode | undefined, prefix = '') { - if (!tn) return; - if (ts.isTypeReferenceNode(tn)) { - const d = seenTypes.get(goName(tn.typeName.getText())); - if (tn.typeName.getText() === 'T') return; - if (!d) throw new Error(`584 ${tn.typeName.getText()} not found`); - if (d.properties.length === 0 && d.alias === undefined) return; - if (d.alias !== undefined) { - if (ts.isIntersectionTypeNode(d.alias)) { - d.alias.types.forEach((tn) => addToProperties(pm, tn, prefix)); // prefix? - return; - } - } - d.properties.forEach((ps) => { - const name = `${prefix}.${ps.name.getText()}`; - propMapSet(pm, name, ps); - addToProperties(pm, ps.type, name); - }); - } else if (strKind(tn) === 'TypeLiteral') { - if (!ts.isTypeLiteralNode(tn)) new Error(`599 ${strKind(tn)}`); - tn.forEachChild((child: ts.Node) => { - if (ts.isPropertySignature(child)) { - const name = `${prefix}.${child.name.getText()}`; - propMapSet(pm, name, child); - addToProperties(pm, child.type, name); - } else if (!ts.isIndexSignatureDeclaration(child)) { - // ignoring IndexSignatures, seen as relatedDocument in - // RelatedFullDocumentDiagnosticReport - throw new Error(`608 ${strKind(child)} ${loc(child)}`); - } - }); - } -} -function deepProperties(d: Data): propMap | undefined { - let properties: propMap = new Map(); - if (!d.alias || !ts.isIntersectionTypeNode(d.alias)) return undefined; - d.alias.types.forEach((ts) => addToProperties(properties, ts)); - return properties; -} - -function mergeAlias(d: Data) { - const props = deepProperties(d); - if (!props) return; // nothing merged - // now each element of props should have length 1 - // change d to merged, toss its alias field, fill in its properties - const v: ts.PropertySignature[] = []; - props.forEach((ps, nm) => { - const xlen = nm.split('.').length; - if (xlen !== 2) return; // not top-level - v.push(ps); - }); - d.kind = 'interface'; - d.alias = undefined; - d.properties = ts.factory.createNodeArray(v); -} - -function mergeTypeLitLit(a: ts.TypeLiteralNode, b: ts.TypeLiteralNode): ts.TypeLiteralNode { - const v = new Map(); // avoid duplicates - a.members.forEach((te) => v.set(te.name.getText(), te)); - b.members.forEach((te) => v.set(te.name.getText(), te)); - const x: ts.TypeElement[] = []; - v.forEach((te) => x.push(te)); - const fake: CreateMutable = a; - fake['members'] = ts.factory.createNodeArray(x); - check(fake as ts.TypeLiteralNode, '643'); - return fake as ts.TypeLiteralNode; -} - -function mergeTypeRefLit(a: ts.TypeReferenceNode, b: ts.TypeLiteralNode): ts.TypeLiteralNode { - const d = seenTypes.get(goName(a.typeName.getText())); - if (!d) throw new Error(`644 name ${a.typeName.getText()} not found`); - const typ = d.me; - if (!ts.isInterfaceDeclaration(typ)) throw new Error(`646 got ${strKind(typ)} not InterfaceDecl`); - const v = new Map(); // avoid duplicates - typ.members.forEach((te) => v.set(te.name.getText(), te)); - b.members.forEach((te) => v.set(te.name.getText(), te)); - const x: ts.TypeElement[] = []; - v.forEach((te) => x.push(te)); - - const w = ts.factory.createNodeArray(x); - const fk: CreateMutable = b; - fk['members'] = w; - (fk['members'] as { pos: number })['pos'] = b.members.pos; - (fk['members'] as { end: number })['end'] = b.members.end; - check(fk as ts.TypeLiteralNode, '662'); - return fk as ts.TypeLiteralNode; -} - -// check that constructed nodes still have associated text -function check(n: ts.Node, loc: string) { - try { getComments(n); } catch (e) { console.log(`check at ${loc} ${e}`); } - try { n.getText(); } catch (e) { console.log(`text check at ${loc}`); } -} - -let typesOut = new Array(); -let constsOut = new Array(); - -// generate Go types -function toGo(d: Data, nm: string) { - if (!d) return; // this is probably a generic T - if (d.name.startsWith('Inner') || d.name === 'WindowClientCapabilities') return; // removed by alias processing - if (d.name === 'Integer' || d.name === 'Uinteger') return; // unneeded - switch (d.kind) { - case 'alias': - goTypeAlias(d, nm); break; - case 'module': goModule(d, nm); break; - case 'enum': goEnum(d, nm); break; - case 'interface': goInterface(d, nm); break; - default: - throw new Error( - `672: more cases in toGo ${nm} ${d.kind}`); - } -} - -// these fields need a * and are not covered by the code -// that calls isStructType. -var starred: [string, string][] = [ - ['TextDocumentContentChangeEvent', 'range'], ['CodeAction', 'command'], - ['CodeAction', 'disabled'], - ['DidSaveTextDocumentParams', 'text'], ['CompletionItem', 'command'], - ['Diagnostic', 'codeDescription'] -]; - -// generate Go code for an interface -function goInterface(d: Data, nm: string) { - let ans = `type ${goName(nm)} struct {\n`; - - // generate the code for each member - const g = function (n: ts.PropertySignature) { - if (!ts.isPropertySignature(n)) - throw new Error(`expected PropertySignature got ${strKind(n)} `); - ans = ans.concat(getComments(n)); - const json = u.JSON(n); - let gt = goType(n.type, n.name.getText()); - if (gt == d.name) gt = '*' + gt; // avoid recursive types (SelectionRange) - // there are several cases where a * is needed - // (putting * in front of too many things breaks uses of CodeActionKind) - starred.forEach(([a, b]) => { - if (d.name == a && n.name.getText() == b) { - gt = '*' + gt; - } - }); - ans = ans.concat(`${goName(n.name.getText())} ${gt}`, json, '\n'); - }; - d.properties.forEach(g); - // heritage clauses become embedded types - // check they are all Identifiers - const f = function (n: ts.ExpressionWithTypeArguments) { - if (!ts.isIdentifier(n.expression)) - throw new Error(`Interface ${nm} heritage ${strKind(n.expression)} `); - if (n.expression.getText() === 'Omit') return; // Type modification type - ans = ans.concat(goName(n.expression.getText()), '\n'); - }; - d.as.forEach((n: ts.HeritageClause) => n.types.forEach(f)); - ans = ans.concat('}\n'); - typesOut.push(getComments(d.me)); - typesOut.push(ans); -} - -// generate Go code for a module (const declarations) -// Generates type definitions, and named constants -function goModule(d: Data, nm: string) { - if (d.generics.length > 0 || d.as.length > 0) { - throw new Error(`743 goModule: unexpected for ${nm} - `); - } - // all the statements should be export const : value - // or value = value - // They are VariableStatements with x.declarationList having a single - // VariableDeclaration - let isNumeric = false; - const f = function (n: ts.Statement, i: number) { - if (!ts.isVariableStatement(n)) { - throw new Error(`753 ${nm} ${i} expected VariableStatement, - got ${strKind(n)}`); - } - const c = getComments(n); - const v = n.declarationList.declarations[0]; // only one - - if (!v.initializer) - throw new Error(`760 no initializer ${nm} ${i} ${v.name.getText()}`); - isNumeric = strKind(v.initializer) == 'NumericLiteral'; - if (c != '') constsOut.push(c); // no point if there are no comments - // There are duplicates. - const cname = constName(goName(v.name.getText()), nm); - let val = v.initializer.getText(); - val = val.split('\'').join('"'); // useless work for numbers - constsOut.push(`${cname} ${nm} = ${val}`); - }; - d.statements.forEach(f); - typesOut.push(getComments(d.me)); - // Or should they be type aliases? - typesOut.push(`type ${nm} ${isNumeric ? 'float64' : 'string'}`); -} - -// generate Go code for an enum. Both types and named constants -function goEnum(d: Data, nm: string) { - let isNumeric = false; - const f = function (v: ts.EnumMember, j: number) { // same as goModule - if (!v.initializer) - throw new Error(`goEnum no initializer ${nm} ${j} ${v.name.getText()}`); - isNumeric = strKind(v.initializer) == 'NumericLiteral'; - const c = getComments(v); - const cname = constName(goName(v.name.getText()), nm); - let val = v.initializer.getText(); - val = val.split('\'').join('"'); // replace quotes. useless work for numbers - constsOut.push(`${c}${cname} ${nm} = ${val}`); - }; - d.enums.forEach(f); - typesOut.push(getComments(d.me)); - // Or should they be type aliases? - typesOut.push(`type ${nm} ${isNumeric ? 'float64' : 'string'}`); -} - -// generate code for a type alias -function goTypeAlias(d: Data, nm: string) { - if (d.as.length != 0 || d.generics.length != 0) { - if (nm != 'ServerCapabilities') - throw new Error(`${nm} has extra fields(${d.as.length},${d.generics.length}) ${d.me.getText()}`); - } - typesOut.push(getComments(d.me)); - // d.alias doesn't seem to have comments - let aliasStr = goName(nm) == 'DocumentURI' ? ' ' : ' = '; - if (nm == 'PrepareSupportDefaultBehavior') { - // code-insiders is sending a bool, not a number. PJW: check this after Feb/2021 - // (and gopls never looks at it anyway) - typesOut.push(`type ${goName(nm)}${aliasStr}interface{}\n`); - return; - } - typesOut.push(`type ${goName(nm)}${aliasStr}${goType(d.alias, nm)}\n`); -} - -// return a go type and maybe an assocated javascript tag -function goType(n: ts.TypeNode | undefined, nm: string): string { - if (!n) throw new Error(`goType undefined for ${nm}`); - if (n.getText() == 'T') return 'interface{}'; // should check it's generic - if (ts.isTypeReferenceNode(n)) { - // DocumentDiagnosticReportKind.unChanged (or .new) value is "new" or "unChanged" - if (n.getText().startsWith('DocumentDiagnostic')) return 'string'; - switch (n.getText()) { - case 'integer': return 'int32'; - case 'uinteger': return 'uint32'; - default: return goName(n.typeName.getText()); // avoid - } - } else if (ts.isUnionTypeNode(n)) { - return goUnionType(n, nm); - } else if (ts.isIntersectionTypeNode(n)) { - return goIntersectionType(n, nm); - } else if (strKind(n) == 'StringKeyword') { - return 'string'; - } else if (strKind(n) == 'NumberKeyword') { - return 'float64'; - } else if (strKind(n) == 'BooleanKeyword') { - return 'bool'; - } else if (strKind(n) == 'AnyKeyword' || strKind(n) == 'UnknownKeyword') { - return 'interface{}'; - } else if (strKind(n) == 'NullKeyword') { - return 'nil'; - } else if (strKind(n) == 'VoidKeyword' || strKind(n) == 'NeverKeyword') { - return 'void'; - } else if (strKind(n) == 'ObjectKeyword') { - return 'interface{}'; - } else if (ts.isArrayTypeNode(n)) { - if (nm === 'arguments') { - // Command and ExecuteCommandParams - return '[]json.RawMessage'; - } - return `[]${goType(n.elementType, nm)}`; - } else if (ts.isParenthesizedTypeNode(n)) { - return goType(n.type, nm); - } else if (ts.isLiteralTypeNode(n)) { - return strKind(n.literal) == 'StringLiteral' ? 'string' : 'float64'; - } else if (ts.isTypeLiteralNode(n)) { - // these are anonymous structs - const v = goTypeLiteral(n, nm); - return v; - } else if (ts.isTupleTypeNode(n)) { - if (n.getText() == '[number, number]') return '[]float64'; - throw new Error(`goType unexpected Tuple ${n.getText()}`); - } - throw new Error(`${strKind(n)} goType unexpected ${n.getText()} for ${nm}`); -} - -// The choice is uniform interface{}, or some heuristically assigned choice, -// or some better sytematic idea I haven't thought of. Using interface{} -// is, in practice, impossibly complex in the existing code. -function goUnionType(n: ts.UnionTypeNode, nm: string): string { - let help = `/*${n.getText()}*/`; // show the original as a comment - // There are some bad cases with newlines: - // range?: boolean | {\n }; - // full?: boolean | {\n /**\n * The server supports deltas for full documents.\n */\n delta?: boolean;\n } - // These are handled specially: - if (nm == 'range') help = help.replace(/\n/, ''); - if (nm == 'full' && help.indexOf('\n') != -1) { - help = '/*boolean | */'; - } - // handle all the special cases - switch (n.types.length) { - case 2: { - const a = strKind(n.types[0]); - const b = strKind(n.types[1]); - if (a == 'NumberKeyword' && b == 'StringKeyword') { // ID - return `interface{} ${help}`; - } - // for null, b is not useful (LiternalType) - if (n.types[1].getText() === 'null') { - if (nm == 'textDocument/codeAction') { - // (Command | CodeAction)[] | null - return `[]CodeAction ${help}`; - } - let v = goType(n.types[0], 'a'); - return `${v} ${help}`; - } - if (a == 'BooleanKeyword') { // usually want bool - if (nm == 'codeActionProvider') return `interface{} ${help}`; - if (nm == 'renameProvider') return `interface{} ${help}`; - if (nm == 'full') return `interface{} ${help}`; // there's a struct - if (nm == 'save') return `${goType(n.types[1], '680')} ${help}`; - return `${goType(n.types[0], 'b')} ${help}`; - } - if (b == 'ArrayType') return `${goType(n.types[1], 'c')} ${help}`; - if (help.includes('InsertReplaceEdit') && n.types[0].getText() == 'TextEdit') { - return `*TextEdit ${help}`; - } - if (a == 'TypeReference') { - if (nm == 'edits') return `${goType(n.types[0], '901')} ${help}`; - if (a == b) return `interface{} ${help}`; - if (nm == 'code') return `interface{} ${help}`; - if (nm == 'editRange') return `${goType(n.types[0], '904')} ${help}`; - if (nm === 'location') return `${goType(n.types[0], '905')} ${help}`; - } - if (a == 'StringKeyword') return `string ${help}`; - if (a == 'TypeLiteral' && nm == 'TextDocumentContentChangeEvent') { - return `${goType(n.types[0], nm)}`; - } - if (a == 'TypeLiteral' && b === 'TypeLiteral') { - // DocumentDiagnosticReport - // the first one includes the second one - return `${goType(n.types[0], '9d')}`; - } - throw new Error(`911 ${nm}: a:${a}/${goType(n.types[0], '9a')} b:${b}/${goType(n.types[1], '9b')} ${loc(n)}`); - } - case 3: { - const aa = strKind(n.types[0]); - const bb = strKind(n.types[1]); - const cc = strKind(n.types[2]); - if (nm === 'workspace/symbol') return `${goType(n.types[0], '930')} ${help}`; - if (nm == 'DocumentFilter' || nm == 'NotebookDocumentFilter' || nm == 'TextDocumentFilter') { - // not really a union. the first is enough, up to a missing - // omitempty but avoid repetitious comments - return `${goType(n.types[0], 'g')}`; - } - if (nm == 'textDocument/documentSymbol') { - return `[]interface{} ${help}`; - } - if (aa == 'TypeReference' && bb == 'ArrayType' && (cc == 'NullKeyword' || cc === 'LiteralType')) { - return `${goType(n.types[0], 'd')} ${help}`; - } - if (aa == 'TypeReference' && bb == aa && cc == 'ArrayType') { - // should check that this is Hover.Contents - return `${goType(n.types[0], 'e')} ${help}`; - } - if (aa == 'ArrayType' && bb == 'TypeReference' && (cc == 'NullKeyword' || cc === 'LiteralType')) { - // check this is nm == 'textDocument/completion' - return `${goType(n.types[1], 'f')} ${help}`; - } - if (aa == 'LiteralType' && bb == aa && cc == aa) return `string ${help}`; - // keep this for diagnosing unexpected interface{} results - // console.log(`931, interface{} for ${aa}/${goType(n.types[0], 'g')},${bb}/${goType(n.types[1], 'h')},${cc}/${goType(n.types[2], 'i')} ${nm}`); - break; - } - case 4: - if (nm == 'documentChanges') return `TextDocumentEdit ${help} `; - if (nm == 'textDocument/prepareRename') { - // these names have to be made unique - const genName = `${goName("prepareRename")}${extraTypes.size}Gn`; - extraTypes.set(genName, [`Range Range \`json:"range"\` - Placeholder string \`json:"placeholder"\``]); - return `${genName} ${help} `; - } - break; - case 8: // LSPany - break; - default: - throw new Error(`957 goUnionType len=${n.types.length} nm=${nm} ${n.getText()}`); - } - - // Result will be interface{} with a comment - let isLiteral = true; - let literal = 'string'; - let res = 'interface{} /* '; - n.types.forEach((v: ts.TypeNode, i: number) => { - // might get an interface inside: - // (Command | CodeAction)[] | null - let m = goType(v, nm); - if (m.indexOf('interface') != -1) { - // avoid nested comments - m = m.split(' ')[0]; - } - m = m.split('\n').join('; '); // sloppy: struct{; - res = res.concat(`${i == 0 ? '' : ' | '}`, m); - if (!ts.isLiteralTypeNode(v)) isLiteral = false; - else literal = strKind(v.literal) == 'StringLiteral' ? 'string' : 'number'; - }); - if (!isLiteral) { - return res + '*/'; - } - // I don't think we get here - // trace?: 'off' | 'messages' | 'verbose' should get string - return `${literal} /* ${n.getText()} */`; -} - -// some of the intersection types A&B are ok as struct{A;B;} and some -// could be expanded, and ClientCapabilites has to be expanded, -// at least for workspace. It's possible to check algorithmically, -// but much simpler just to check explicitly. -function goIntersectionType(n: ts.IntersectionTypeNode, nm: string): string { - if (nm == 'ClientCapabilities') return expandIntersection(n); - //if (nm == 'ServerCapabilities') return expandIntersection(n); // save for later consideration - let inner = ''; - n.types.forEach( - (t: ts.TypeNode) => { inner = inner.concat(goType(t, nm), '\n'); }); - return `struct{ \n${inner}} `; -} - -// for each of the intersected types, extract its components (each will -// have a Data with properties) extract the properties, and keep track -// of them by name. The names that occur once can be output. The names -// that occur more than once need to be combined. -function expandIntersection(n: ts.IntersectionTypeNode): string { - const bad = function (n: ts.Node, s: string) { - return new Error(`expandIntersection ${strKind(n)} ${s}`); - }; - let props = new Map(); - for (const tp of n.types) { - if (!ts.isTypeReferenceNode(tp)) throw bad(tp, 'A'); - const d = data.get(goName(tp.typeName.getText())); - for (const p of d.properties) { - if (!ts.isPropertySignature(p)) throw bad(p, 'B'); - let v = props.get(p.name.getText()) || []; - v.push(p); - props.set(p.name.getText(), v); - } - } - let ans = 'struct {\n'; - for (const [k, v] of Array.from(props)) { - if (v.length == 1) { - const a = v[0]; - ans = ans.concat(getComments(a)); - ans = ans.concat(`${goName(k)} ${goType(a.type, k)} ${u.JSON(a)}\n`); - continue; - } - ans = ans.concat(`${goName(k)} struct {\n`); - for (let i = 0; i < v.length; i++) { - const a = v[i]; - if (ts.isTypeReferenceNode(a.type)) { - ans = ans.concat(getComments(a)); - ans = ans.concat(goName(a.type.typeName.getText()), '\n'); - } else if (ts.isTypeLiteralNode(a.type)) { - if (a.type.members.length != 1) throw bad(a.type, 'C'); - const b = a.type.members[0]; - if (!ts.isPropertySignature(b)) throw bad(b, 'D'); - ans = ans.concat(getComments(b)); - ans = ans.concat( - goName(b.name.getText()), ' ', goType(b.type, 'a'), u.JSON(b), '\n'); - } else { - throw bad(a.type, `E ${a.getText()} in ${goName(k)} at ${loc(a)}`); - } - } - ans = ans.concat('}\n'); - } - ans = ans.concat('}\n'); - return ans; -} - -// Does it make sense to use a pointer? -function isStructType(te: ts.TypeNode): boolean { - switch (strKind(te)) { - case 'UnionType': // really need to know which type will be chosen - case 'BooleanKeyword': - case 'StringKeyword': - case 'ArrayType': - return false; - case 'TypeLiteral': return false; // true makes for difficult compound constants - // but think more carefully to understands why starred is needed. - case 'TypeReference': { - if (!ts.isTypeReferenceNode(te)) throw new Error(`1047 impossible ${strKind(te)}`); - const d = seenTypes.get(goName(te.typeName.getText())); - if (d === undefined || d.properties.length == 0) return false; - if (d.properties.length > 1) return true; - // alias or interface with a single property (The alias is Uinteger, which we ignore later) - if (d.alias) return false; - const x = d.properties[0].type; - return isStructType(x); - } - default: throw new Error(`1055 indirectable> ${strKind(te)}`); - } -} - -function goTypeLiteral(n: ts.TypeLiteralNode, nm: string): string { - let ans: string[] = []; // in case we generate a new extra type - let res = 'struct{\n'; // the actual answer usually - const g = function (nx: ts.TypeElement) { - // add the json, as in goInterface(). Strange inside union types. - if (ts.isPropertySignature(nx)) { - let json = u.JSON(nx); - let typ = goType(nx.type, nx.name.getText()); - // }/*\n*/`json:v` is not legal, the comment is a newline - if (typ.includes('\n') && typ.indexOf('*/') === typ.length - 2) { - typ = typ.replace(/\n\t*/g, ' '); - } - const v = getComments(nx) || ''; - starred.forEach(([a, b]) => { - if (a != nm || b != typ.toLowerCase()) return; - typ = '*' + typ; - json = json.substring(0, json.length - 2) + ',omitempty"`'; - }); - if (typ[0] !== '*' && isStructType(nx.type)) typ = '*' + typ; - res = res.concat(`${v} ${goName(nx.name.getText())} ${typ}`, json, '\n'); - ans.push(`${v}${goName(nx.name.getText())} ${typ} ${json}\n`); - } else if (ts.isIndexSignatureDeclaration(nx)) { - const comment = nx.getText().replace(/[/]/g, ''); - if (nx.getText() == '[uri: string]: TextEdit[];') { - res = 'map[string][]TextEdit'; - } else if (nx.getText().startsWith('[id: ChangeAnnotationIdentifier]')) { - res = 'map[string]ChangeAnnotationIdentifier'; - } else if (nx.getText().startsWith('[uri: string')) { - res = 'map[string]interface{}'; - } else if (nx.getText().startsWith('[uri: DocumentUri')) { - res = 'map[DocumentURI][]TextEdit'; - } else if (nx.getText().startsWith('[key: string')) { - res = 'map[string]interface{}'; - } else { - throw new Error(`1100 handle ${nx.getText()} ${loc(nx)}`); - } - res += ` /*${comment}*/`; - ans.push(res); - return; - } else - throw new Error(`TypeLiteral had ${strKind(nx)}`); - }; - n.members.forEach(g); - // for some the generated type is wanted, for others it's not needed - if (!nm.startsWith('workspace')) { - if (res.startsWith('struct')) return res + '}'; // map[] is special - return res; - } - // these names have to be made unique - const genName = `${goName(nm)}${extraTypes.size}Gn`; - extraTypes.set(genName, ans); - return genName; -} - -// print all the types and constants and extra types -function outputTypes() { - // generate go types alphabeticaly - let v = Array.from(seenTypes.keys()); - v.sort(); - v.forEach((x) => toGo(seenTypes.get(x), x)); - u.prgo(u.computeHeader(true)); - u.prgo('import "encoding/json"\n\n'); - typesOut.forEach((s) => { - u.prgo(s); - // it's more convenient not to have to think about trailing newlines - // when generating types, but doc comments can't have an extra \n - if (s.indexOf('/**') < 0) u.prgo('\n'); - }); - u.prgo('\nconst (\n'); - constsOut.forEach((s) => { - u.prgo(s); - u.prgo('\n'); - }); - u.prgo(')\n'); - u.prgo('// Types created to name formal parameters and embedded structs\n'); - extraTypes.forEach((v, k) => { - u.prgo(` type ${k} struct {\n`); - v.forEach((s) => { - u.prgo(s); - u.prgo('\n'); - }); - u.prgo('}\n'); - }); -} - -// client and server ------------------ - -interface side { - methods: string[]; - cases: string[]; - calls: string[]; - name: string; // client or server - goName: string; // Client or Server - outputFile?: string; - fd?: number -} -let client: side = { - methods: [], - cases: [], - calls: [], - name: 'client', - goName: 'Client', -}; -let server: side = { - methods: [], - cases: [], - calls: [], - name: 'server', - goName: 'Server', -}; - -// commonly used output -const notNil = `if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) -}`; - -// Go code for notifications. Side is client or server, m is the request -// method -function goNot(side: side, m: string) { - if (m == '$/cancelRequest') return; // handled specially in protocol.go - const n = not.get(m); - const a = goType(n.typeArguments[0], m); - const nm = methodName(m); - side.methods.push(sig(nm, a, '')); - const caseHdr = ` case "${m}": // notif`; - let case1 = notNil; - if (a != '' && a != 'void') { - case1 = `var params ${a} - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - } - err:= ${side.name}.${nm}(ctx, ¶ms) - return true, reply(ctx, nil, err)`; - } else { - case1 = `err := ${side.name}.${nm}(ctx) - return true, reply(ctx, nil, err)`; - } - side.cases.push(`${caseHdr}\n${case1}`); - - const arg3 = a == '' || a == 'void' ? 'nil' : 'params'; - side.calls.push(` - func (s *${side.name}Dispatcher) ${sig(nm, a, '', true)} { - return s.sender.Notify(ctx, "${m}", ${arg3}) - }`); -} - -// Go code for requests. -function goReq(side: side, m: string) { - const n = req.get(m); - const nm = methodName(m); - let a = goType(n.typeArguments[0], m); - let b = goType(n.typeArguments[1], m); - if (n.getText().includes('Type0')) { - b = a; - a = ''; // workspace/workspaceFolders and shutdown - } - u.prb(`${side.name} req ${a != ''}, ${b != ''} ${nm} ${m} ${loc(n)} `); - side.methods.push(sig(nm, a, b)); - - const caseHdr = `case "${m}": // req`; - let case1 = notNil; - if (a != '') { - if (extraTypes.has('Param' + nm)) a = 'Param' + nm; - case1 = `var params ${a} - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - }`; - if (a === 'ParamInitialize') { - case1 = `var params ${a} - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - if _, ok := err.(*json.UnmarshalTypeError); !ok { - return true, sendParseError(ctx, reply, err) - } - }`; - } - } - const arg2 = a == '' ? '' : ', ¶ms'; - // if case2 is not explicitly typed string, typescript makes it a union of strings - let case2: string = `if err := ${side.name}.${nm}(ctx${arg2}); err != nil { - event.Error(ctx, "", err) - }`; - if (b != '' && b != 'void') { - case2 = `resp, err := ${side.name}.${nm}(ctx${arg2}) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil)`; - } else { // response is nil - case2 = `err := ${side.name}.${nm}(ctx${arg2}) - return true, reply(ctx, nil, err)`; - } - - side.cases.push(`${caseHdr}\n${case1}\n${case2}`); - - const callHdr = `func (s *${side.name}Dispatcher) ${sig(nm, a, b, true)} {`; - let callBody = `return s.sender.Call(ctx, "${m}", nil, nil)\n}`; - if (b != '' && b != 'void') { - const p2 = a == '' ? 'nil' : 'params'; - const returnType = indirect(b) ? `*${b}` : b; - callBody = `var result ${returnType} - if err := s.sender.Call(ctx, "${m}", ${p2}, &result); err != nil { - return nil, err - } - return result, nil - }`; - } else if (a != '') { - callBody = `return s.sender.Call(ctx, "${m}", params, nil) // Call, not Notify - }`; - } - side.calls.push(`${callHdr}\n${callBody}\n`); -} - -// make sure method names are unique -let seenNames = new Set(); -function methodName(m: string): string { - let i = m.indexOf('/'); - let s = m.substring(i + 1); - let x = s[0].toUpperCase() + s.substring(1); - for (let j = x.indexOf('/'); j >= 0; j = x.indexOf('/')) { - let suffix = x.substring(j + 1); - suffix = suffix[0].toUpperCase() + suffix.substring(1); - let prefix = x.substring(0, j); - x = prefix + suffix; - } - if (seenNames.has(x)) { - // various Resolve and Diagnostic - x += m[0].toUpperCase() + m.substring(1, i); - } - seenNames.add(x); - return x; -} - -// used in sig and in goReq -function indirect(s: string): boolean { - if (s == '' || s == 'void') return false; - const skip = (x: string) => s.startsWith(x); - if (skip('[]') || skip('interface') || skip('Declaration') || - skip('Definition') || skip('DocumentSelector')) - return false; - return true; -} - -// Go signatures for methods. -function sig(nm: string, a: string, b: string, names?: boolean): string { - if (a.indexOf('struct') != -1) { - const v = a.split('\n'); - extraTypes.set(`Param${nm}`, v.slice(1, v.length - 1)); - a = 'Param' + nm; - } - if (a == 'void') - a = ''; - else if (a != '') { - if (names) - a = ', params *' + a; - else - a = ', *' + a; - } - let ret = 'error'; - if (b != '' && b != 'void') { - // avoid * when it is senseless - if (indirect(b)) b = '*' + b; - ret = `(${b}, error)`; - } - let start = `${nm}(`; - if (names) { - start = start + 'ctx '; - } - return `${start}context.Context${a}) ${ret}`; -} - -// write the request/notification code -function output(side: side) { - // make sure the output file exists - if (!side.outputFile) { - side.outputFile = `ts${side.name}.go`; - side.fd = fs.openSync(side.outputFile, 'w'); - } - const f = function (s: string) { - fs.writeSync(side.fd!, s); - fs.writeSync(side.fd!, '\n'); - }; - f(u.computeHeader(false)); - f(` - import ( - "context" - "encoding/json" - - "golang.org/x/tools/internal/jsonrpc2" - ) - `); - const a = side.name[0].toUpperCase() + side.name.substring(1); - f(`type ${a} interface {`); - side.methods.forEach((v) => { f(v); }); - f('}\n'); - f(`func ${side.name}Dispatch(ctx context.Context, ${side.name} ${a}, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { - switch r.Method() {`); - side.cases.forEach((v) => { f(v); }); - f(` - default: - return false, nil - } - }`); - side.calls.forEach((v) => { f(v); }); -} - -// Handling of non-standard requests, so we can add gopls-specific calls. -function nonstandardRequests() { - server.methods.push( - 'NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error)'); - server.calls.push( - `func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { - var result interface{} - if err := s.sender.Call(ctx, method, params, &result); err != nil { - return nil, err - } - return result, nil - } - `); -} - -// ----- remember it's a scripting language -function main() { - if (u.gitHash != u.git()) { - throw new Error( - `git hash mismatch, wanted\n${u.gitHash} but source is at\n${u.git()}`); - } - u.createOutputFiles(); - parse(); - u.printAST(program); - // find the Requests and Nofificatations - for (const sourceFile of program.getSourceFiles()) { - if (!sourceFile.isDeclarationFile) { - ts.forEachChild(sourceFile, findRPCs); - } - } - // separate RPCs into client and server - setReceives(); - // visit every sourceFile collecting top-level type definitions - for (const sourceFile of program.getSourceFiles()) { - if (!sourceFile.isDeclarationFile) { - ts.forEachChild(sourceFile, genTypes); - } - } - // check that each thing occurs exactly once, and put pointers into - // seenTypes - checkOnce(); - // for each of Client and Server there are 3 parts to the output: - // 1. type X interface {methods} - // 2. func (h *serverHandler) Deliver(...) { switch r.method } - // 3. func (x *xDispatcher) Method(ctx, parm) - not.forEach( // notifications - (v, k) => { - receives.get(k) == 'client' ? goNot(client, k) : goNot(server, k); - }); - req.forEach( // requests - (v, k) => { - receives.get(k) == 'client' ? goReq(client, k) : goReq(server, k); - }); - nonstandardRequests(); - // find all the types implied by seenTypes and rpcs to try to avoid - // generating types that aren't used - moreTypes(); - // do merging - cleanData(); - // and print the Go code - outputTypes(); - console.log(`seen ${seenTypes.size + extraTypes.size}`); - output(client); - output(server); -} - -main(); diff --git a/gopls/internal/lsp/protocol/typescript/tsconfig.json b/gopls/internal/lsp/protocol/typescript/tsconfig.json deleted file mode 100644 index 14cfe0c7ee9..00000000000 --- a/gopls/internal/lsp/protocol/typescript/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - "isolatedModules": true, - "moduleResolution": "node", - "lib":["ES2020"], - "sourceMap": true, // sourceMap or inlineSourceMap? and see inlineSources - "target": "ES5", - - "noFallthroughCasesInSwitch": false, // there is one legitimate on - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": false, - "noEmitOnError": true, - - // "extendedDiagnostics": true, // for occasional amusement - - // "strict": true, // too many undefineds in types, etc - "alwaysStrict": true, - "noImplicitAny": true, - "noImplicitThis": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, - "strictNullChecks": false, // doesn't like arrray access, among other things. - //"strictPropertyInitialization": true, // needs strictNullChecks - }, - "files": ["./code.ts", "./util.ts"] -} diff --git a/gopls/internal/lsp/protocol/typescript/util.ts b/gopls/internal/lsp/protocol/typescript/util.ts deleted file mode 100644 index 9475b26a157..00000000000 --- a/gopls/internal/lsp/protocol/typescript/util.ts +++ /dev/null @@ -1,254 +0,0 @@ - -// for us typescript ignorati, having an import makes this file a module -import * as fs from 'fs'; -import * as process from 'process'; -import * as ts from 'typescript'; - -// This file contains various utilities having to do with producing strings -// and managing output - -// ------ create files -let dir = process.env['HOME']; -const srcDir = '/vscode-languageserver-node'; -export const fnames = [ - `${dir}${srcDir}/protocol/src/common/protocol.ts`, - `${dir}/${srcDir}/protocol/src/browser/main.ts`, `${dir}${srcDir}/types/src/main.ts`, - `${dir}${srcDir}/jsonrpc/src/node/main.ts` -]; -export const gitHash = '696f9285bf849b73745682fdb1c1feac73eb8772'; -let outFname = 'tsprotocol.go'; -let fda: number, fdb: number, fde: number; // file descriptors - -export function createOutputFiles() { - fda = fs.openSync('/tmp/ts-a', 'w'); // dump of AST - fdb = fs.openSync('/tmp/ts-b', 'w'); // unused, for debugging - fde = fs.openSync(outFname, 'w'); // generated Go -} -export function pra(s: string) { - return (fs.writeSync(fda, s)); -} -export function prb(s: string) { - return (fs.writeSync(fdb, s)); -} -export function prgo(s: string) { - return (fs.writeSync(fde, s)); -} - -// Get the hash value of the git commit -export function git(): string { - let a = fs.readFileSync(`${dir}${srcDir}/.git/HEAD`).toString(); - // ref: refs/heads/foo, or a hash like - // cc12d1a1c7df935012cdef5d085cdba04a7c8ebe - if (a.charAt(a.length - 1) == '\n') { - a = a.substring(0, a.length - 1); - } - if (a.length == 40) { - return a; // a hash - } - if (a.substring(0, 5) == 'ref: ') { - const fname = `${dir}${srcDir}/.git/` + a.substring(5); - let b = fs.readFileSync(fname).toString(); - if (b.length == 41) { - return b.substring(0, 40); - } - } - throw new Error('failed to find the git commit hash'); -} - -// Produce a header for Go output files -export function computeHeader(pkgDoc: boolean): string { - let lastMod = 0; - let lastDate = new Date(); - for (const f of fnames) { - const st = fs.statSync(f); - if (st.mtimeMs > lastMod) { - lastMod = st.mtimeMs; - lastDate = st.mtime; - } - } - const cp = `// 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. - - `; - const a = - '// Package protocol contains data types and code for LSP json rpcs\n' + - '// generated automatically from vscode-languageserver-node\n' + - `// commit: ${gitHash}\n` + - `// last fetched ${lastDate}\n`; - const b = 'package protocol\n'; - const c = '\n// Code generated (see typescript/README.md) DO NOT EDIT.\n\n'; - if (pkgDoc) { - return cp + c + a + b; - } - else { - return cp + c+ b + a; - } -} - -// Turn a typescript name into an exportable Go name, and appease lint -export function goName(s: string): string { - let ans = s; - if (s.charAt(0) == '_') { - // in the end, none of these are emitted. - ans = 'Inner' + s.substring(1); - } - else { ans = s.substring(0, 1).toUpperCase() + s.substring(1); } - ans = ans.replace(/Uri$/, 'URI'); - ans = ans.replace(/Id$/, 'ID'); - return ans; -} - -// Generate JSON tag for a struct field -export function JSON(n: ts.PropertySignature): string { - const json = `\`json:"${n.name.getText()}${n.questionToken !== undefined ? ',omitempty' : ''}"\``; - return json; -} - -// Generate modifying prefixes and suffixes to ensure -// consts are unique. (Go consts are package-level, but Typescript's are -// not.) Use suffixes to minimize changes to gopls. -export function constName(nm: string, type: string): string { - let pref = new Map([ - ['DiagnosticSeverity', 'Severity'], ['WatchKind', 'Watch'], - ['SignatureHelpTriggerKind', 'Sig'], ['CompletionItemTag', 'Compl'], - ['Integer', 'INT_'], ['Uinteger', 'UINT_'], ['CodeActionTriggerKind', 'CodeAction'] - ]); // typeName->prefix - let suff = new Map([ - ['CompletionItemKind', 'Completion'], ['InsertTextFormat', 'TextFormat'], - ['SymbolTag', 'Symbol'], ['FileOperationPatternKind', 'Op'], - ]); - let ans = nm; - if (pref.get(type)) ans = pref.get(type) + ans; - if (suff.has(type)) ans = ans + suff.get(type); - return ans; -} - -// Find the comments associated with an AST node -export function getComments(node: ts.Node): string { - const sf = node.getSourceFile(); - const start = node.getStart(sf, false); - const starta = node.getStart(sf, true); - const x = sf.text.substring(starta, start); - return x; -} - - -// --------- printing the AST, for debugging - -export function printAST(program: ts.Program) { - // dump the ast, for debugging - const f = function (n: ts.Node) { - describe(n, pra); - }; - for (const sourceFile of program.getSourceFiles()) { - if (!sourceFile.isDeclarationFile) { - // walk the tree to do stuff - ts.forEachChild(sourceFile, f); - } - } - pra('\n'); - for (const key of Object.keys(seenThings).sort()) { - pra(`${key}: ${seenThings.get(key)} \n`); - } -} - -// Used in printing the AST -let seenThings = new Map(); -function seenAdd(x: string) { - const u = seenThings.get(x); - seenThings.set(x, u === undefined ? 1 : u + 1); -} - -// eslint-disable-next-line no-unused-vars -function describe(node: ts.Node, pr: (_: string) => any) { - if (node === undefined) { - return; - } - let indent = ''; - - function f(n: ts.Node) { - seenAdd(kinds(n)); - if (ts.isIdentifier(n)) { - pr(`${indent} ${loc(n)} ${strKind(n)} ${n.text} \n`); - } - else if (ts.isPropertySignature(n) || ts.isEnumMember(n)) { - pra(`${indent} ${loc(n)} ${strKind(n)} \n`); - } - else if (ts.isTypeLiteralNode(n)) { - let m = n.members; - pr(`${indent} ${loc(n)} ${strKind(n)} ${m.length} \n`); - } - else if (ts.isStringLiteral(n)) { - pr(`${indent} ${loc(n)} ${strKind(n)} ${n.text} \n`); - } - else { pr(`${indent} ${loc(n)} ${strKind(n)} \n`); } - indent += ' .'; - ts.forEachChild(n, f); - indent = indent.slice(0, indent.length - 2); - } - f(node); -} - - -// For debugging, say where an AST node is in a file -export function loc(node: ts.Node | undefined): string { - if (!node) throw new Error('loc called with undefined (cannot happen!)'); - const sf = node.getSourceFile(); - const start = node.getStart(); - const x = sf.getLineAndCharacterOfPosition(start); - const full = node.getFullStart(); - const y = sf.getLineAndCharacterOfPosition(full); - let fn = sf.fileName; - const n = fn.search(/-node./); - fn = fn.substring(n + 6); - return `${fn} ${x.line + 1}: ${x.character + 1} (${y.line + 1}: ${y.character + 1})`; -} - -// --- various string stuff - -// return a string of the kinds of the immediate descendants -// as part of printing the AST tree -function kinds(n: ts.Node): string { - let res = 'Seen ' + strKind(n); - function f(n: ts.Node): void { res += ' ' + strKind(n); } - ts.forEachChild(n, f); - return res; -} - -// What kind of AST node is it? This would just be typescript's -// SyntaxKind[n.kind] except that the default names for some nodes -// are misleading -export function strKind(n: ts.Node | undefined): string { - if (n == null || n == undefined) { - return 'null'; - } - return kindToStr(n.kind); -} - -function kindToStr(k: ts.SyntaxKind): string { - const x = ts.SyntaxKind[k]; - // some of these have two names - switch (x) { - default: - return x; - case 'FirstAssignment': - return 'EqualsToken'; - case 'FirstBinaryOperator': - return 'LessThanToken'; - case 'FirstCompoundAssignment': - return 'PlusEqualsToken'; - case 'FirstContextualKeyword': - return 'AbstractKeyword'; - case 'FirstLiteralToken': - return 'NumericLiteral'; - case 'FirstNode': - return 'QualifiedName'; - case 'FirstTemplateToken': - return 'NoSubstitutionTemplateLiteral'; - case 'LastTemplateToken': - return 'TemplateTail'; - case 'FirstTypeNode': - return 'TypePredicate'; - } -} From 8aba49bb5ea283ececf5e8a1652b5e9705115d37 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 9 Jan 2023 13:44:29 -0500 Subject: [PATCH 639/723] internal/gcimporter: preserve column and line in shallow iexport Historically, export data has discarded source position information. Originally not even line numbers were available; at some point line and column numbers were stored, up to some fixed limit, but more recently column numbers were again discarded. Offset information has always been incorrect. gopls is moving toward greater reliance on incremental operation using a file-based cache of export data and other serializable records that are similar in character. It is critical that it be able to accurately recover file position information for dependencies. This change causes the iexport function to encode each object's token.Pos as a pair (file, offset), where file indicates the token.File and offset is a byte offset within the file. The token.File is serialized as a filename and a delta-encoded line-offset table. (We discard the lineInfos table that supports //line directives because gopls no longer uses it.) The iimport function constructs a token.File and calls SetLines, and then all token.Pos values work exactly as they would with source. This causes about a 74% increase in size of the shallow export data for the standard library: was 564KB, now 982KB. token.File has a SetLines method but no GetLines. This change must therefore resort to... unorthodox methods to retrieve the field. Suggestions welcome. This alternative encoding is enabled by using "shallow" mode, which is effectively a license for gopls to do whatever it wants. Again, suggestions welcome. Updates golang/go#57708 Change-Id: I028ed669161e38a9a4672dd8d9cadb268a0cdd07 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461215 Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- internal/gcimporter/iexport.go | 108 +++++++++++++++++++++++++++- internal/gcimporter/iimport.go | 49 ++++++++++++- internal/gcimporter/shallow_test.go | 30 ++++++++ 3 files changed, 184 insertions(+), 3 deletions(-) diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 61e1dd2ad6a..93a184676c4 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -21,6 +21,8 @@ import ( "sort" "strconv" "strings" + "sync" + "unsafe" "golang.org/x/tools/internal/typeparams" ) @@ -100,6 +102,7 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, ver shallow: shallow, allPkgs: map[*types.Package]bool{}, stringIndex: map[string]uint64{}, + fileIndex: map[*token.File]uint64{}, declIndex: map[types.Object]uint64{}, tparamNames: map[types.Object]string{}, typIndex: map[types.Type]uint64{}, @@ -163,11 +166,17 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, ver } hdr.uint64(uint64(p.version)) hdr.uint64(uint64(p.strings.Len())) + if p.shallow { + hdr.uint64(uint64(p.files.Len())) + } hdr.uint64(dataLen) // Flush output. io.Copy(out, &hdr) io.Copy(out, &p.strings) + if p.shallow { + io.Copy(out, &p.files) + } io.Copy(out, &p.data0) return nil @@ -255,6 +264,11 @@ type iexporter struct { strings intWriter stringIndex map[string]uint64 + // In shallow mode, object positions are encoded as (file, offset). + // Each file is recorded as a line-number table. + files intWriter + fileIndex map[*token.File]uint64 + data0 intWriter declIndex map[types.Object]uint64 tparamNames map[types.Object]string // typeparam->exported name @@ -286,6 +300,35 @@ func (p *iexporter) stringOff(s string) uint64 { return off } +// fileOff returns the offset of the token.File encoding. +// If not already present, it's added to the end. +func (p *iexporter) fileOff(file *token.File) uint64 { + off, ok := p.fileIndex[file] + if !ok { + off = uint64(p.files.Len()) + p.fileIndex[file] = off + + p.files.uint64(p.stringOff(file.Name())) + p.files.uint64(uint64(file.Size())) + + // Delta-encode the line offsets, omitting the initial zero. + // (An empty file has an empty lines array.) + // + // TODO(adonovan): opt: use a two-pass approach that + // first gathers the set of Pos values and then + // encodes only the information necessary for them. + // This would allow us to discard the lines after the + // last object of interest and to run-length encode the + // trivial lines between lines with needed positions. + lines := getLines(file) + p.files.uint64(uint64(len(lines))) + for i := 1; i < len(lines); i++ { + p.files.uint64(uint64(lines[i] - lines[i-1])) + } + } + return off +} + // pushDecl adds n to the declaration work queue, if not already present. func (p *iexporter) pushDecl(obj types.Object) { // Package unsafe is known to the compiler and predeclared. @@ -464,13 +507,29 @@ func (w *exportWriter) tag(tag byte) { } func (w *exportWriter) pos(pos token.Pos) { - if w.p.version >= iexportVersionPosCol { + if w.p.shallow { + w.posV2(pos) + } else if w.p.version >= iexportVersionPosCol { w.posV1(pos) } else { w.posV0(pos) } } +// posV2 encoding (used only in shallow mode) records positions as +// (file, offset), where file is the index in the token.File table +// (which records the file name and newline offsets) and offset is a +// byte offset. It effectively ignores //line directives. +func (w *exportWriter) posV2(pos token.Pos) { + if pos == token.NoPos { + w.uint64(0) + return + } + file := w.p.fset.File(pos) // fset must be non-nil + w.uint64(1 + w.p.fileOff(file)) + w.uint64(uint64(file.Offset(pos))) +} + func (w *exportWriter) posV1(pos token.Pos) { if w.p.fset == nil { w.int64(0) @@ -1060,3 +1119,50 @@ func (q *objQueue) popHead() types.Object { q.head++ return obj } + +// getLines returns the table of line-start offsets from a token.File. +func getLines(file *token.File) []int { + // Use this variant once proposal #57708 is implemented: + // + // if file, ok := file.(interface{ Lines() []int }); ok { + // return file.Lines() + // } + + // This declaration must match that of token.File. + // This creates a risk of dependency skew. + // For now we check that the size of the two + // declarations is the same, on the (fragile) assumption + // that future changes would add fields. + type tokenFile119 struct { + _ string + _ int + _ int + mu sync.Mutex // we're not complete monsters + lines []int + _ []struct{} + } + type tokenFile118 struct { + _ *token.FileSet // deleted in go1.19 + tokenFile119 + } + + type uP = unsafe.Pointer + switch unsafe.Sizeof(*file) { + case unsafe.Sizeof(tokenFile118{}): + var ptr *tokenFile118 + *(*uP)(uP(&ptr)) = uP(file) + ptr.mu.Lock() + defer ptr.mu.Unlock() + return ptr.lines + + case unsafe.Sizeof(tokenFile119{}): + var ptr *tokenFile119 + *(*uP)(uP(&ptr)) = uP(file) + ptr.mu.Lock() + defer ptr.mu.Unlock() + return ptr.lines + + default: + panic("unexpected token.File size") + } +} diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index a1c46965350..cb75952c0c6 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -137,12 +137,18 @@ func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data } sLen := int64(r.uint64()) + var fLen int64 + if insert != nil { + // shallow mode uses a different position encoding + fLen = int64(r.uint64()) + } dLen := int64(r.uint64()) whence, _ := r.Seek(0, io.SeekCurrent) stringData := data[whence : whence+sLen] - declData := data[whence+sLen : whence+sLen+dLen] - r.Seek(sLen+dLen, io.SeekCurrent) + fileData := data[whence+sLen : whence+sLen+fLen] + declData := data[whence+sLen+fLen : whence+sLen+fLen+dLen] + r.Seek(sLen+fLen+dLen, io.SeekCurrent) p := iimporter{ version: int(version), @@ -151,6 +157,8 @@ func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data stringData: stringData, stringCache: make(map[uint64]string), + fileData: fileData, + fileCache: make(map[uint64]*token.File), pkgCache: make(map[uint64]*types.Package), declData: declData, @@ -280,6 +288,8 @@ type iimporter struct { stringData []byte stringCache map[uint64]string + fileData []byte + fileCache map[uint64]*token.File pkgCache map[uint64]*types.Package declData []byte @@ -352,6 +362,29 @@ func (p *iimporter) stringAt(off uint64) string { return s } +func (p *iimporter) fileAt(off uint64) *token.File { + file, ok := p.fileCache[off] + if !ok { + rd := intReader{bytes.NewReader(p.fileData[off:]), p.ipath} + filename := p.stringAt(rd.uint64()) + size := int(rd.uint64()) + file = p.fake.fset.AddFile(filename, -1, size) + + if n := int(rd.uint64()); n > 0 { + lines := make([]int, n) // initial element always implicitly zero + for i := 1; i < n; i++ { + lines[i] = lines[i-1] + int(rd.uint64()) + } + if !file.SetLines(lines) { + errorf("SetLines failed") // can't happen + } + } + + p.fileCache[off] = file + } + return file +} + func (p *iimporter) pkgAt(off uint64) *types.Package { if pkg, ok := p.pkgCache[off]; ok { return pkg @@ -645,6 +678,9 @@ func (r *importReader) qualifiedIdent() (*types.Package, string) { } func (r *importReader) pos() token.Pos { + if r.p.insert != nil { // shallow mode + return r.posv2() + } if r.p.version >= iexportVersionPosCol { r.posv1() } else { @@ -681,6 +717,15 @@ func (r *importReader) posv1() { } } +func (r *importReader) posv2() token.Pos { + file := r.uint64() + if file == 0 { + return token.NoPos + } + tf := r.p.fileAt(file - 1) + return tf.Pos(int(r.uint64())) +} + func (r *importReader) typ() types.Type { return r.p.typAt(r.uint64(), nil) } diff --git a/internal/gcimporter/shallow_test.go b/internal/gcimporter/shallow_test.go index 3d8c86a1545..443bb30149e 100644 --- a/internal/gcimporter/shallow_test.go +++ b/internal/gcimporter/shallow_test.go @@ -10,6 +10,8 @@ import ( "go/parser" "go/token" "go/types" + "os" + "strings" "testing" "golang.org/x/sync/errgroup" @@ -153,6 +155,7 @@ func typecheck(t *testing.T, ppkg *packages.Package) { // Type-check the syntax trees. tpkg, _ := cfg.Check(ppkg.PkgPath, fset, syntax, nil) + postTypeCheck(t, fset, tpkg) // Save the export data. data, err := gcimporter.IExportShallow(fset, tpkg) @@ -161,3 +164,30 @@ func typecheck(t *testing.T, ppkg *packages.Package) { } ppkg.ExportFile = string(data) } + +// postTypeCheck is called after a package is type checked. +// We use it to assert additional correctness properties, +// for example, that the apparent location of "fmt.Println" +// corresponds to its source location: in other words, +// export+import preserves high-fidelity positions. +func postTypeCheck(t *testing.T, fset *token.FileSet, pkg *types.Package) { + if pkg.Path() == "fmt" { + obj := pkg.Scope().Lookup("Println") + posn := fset.Position(obj.Pos()) + data, err := os.ReadFile(posn.Filename) + if err != nil { + t.Errorf("can't read source file declaring fmt.Println: %v", err) + return + } + // Check line and column. + line := strings.Split(string(data), "\n")[posn.Line-1] + + if id := line[posn.Column-1 : posn.Column-1+len(obj.Name())]; id != "Println" { + t.Errorf("%+v: expected declaration of fmt.Println at this line, column; got %q", posn, line) + } + // Check offset. + if id := string(data[posn.Offset : posn.Offset+len(obj.Name())]); id != "Println" { + t.Errorf("%+v: expected declaration of fmt.Println at this offset; got %q", posn, id) + } + } +} From d958e854808e53978bb3f600901fcb7fa3f71163 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 13 Jan 2023 11:44:12 -0500 Subject: [PATCH 640/723] internal/gcimporter: use two-level file index This change introduces a two-level index of files, as a precursor to an optimization in which only the line number information for the necessary positions is recorded. The optimization naturally requires two passes over the data, which means we can't emit the file information in one gulp. Change-Id: Ia8e015c8b19cbf6074661ec345c7360a325d1054 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462095 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- internal/gcimporter/iexport.go | 85 ++++++++++++++++++++++------------ internal/gcimporter/iimport.go | 22 ++++++--- 2 files changed, 70 insertions(+), 37 deletions(-) diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 93a184676c4..fa6fa3d9cdd 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -102,7 +102,6 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, ver shallow: shallow, allPkgs: map[*types.Package]bool{}, stringIndex: map[string]uint64{}, - fileIndex: map[*token.File]uint64{}, declIndex: map[types.Object]uint64{}, tparamNames: map[types.Object]string{}, typIndex: map[types.Type]uint64{}, @@ -141,6 +140,34 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, ver p.doDecl(p.declTodo.popHead()) } + // Produce index of offset of each file record in files. + var files intWriter + var fileOffset []uint64 // fileOffset[i] is offset in files of file encoded as i + if p.shallow { + fileOffset = make([]uint64, len(p.fileInfos)) + for i, info := range p.fileInfos { + fileOffset[i] = uint64(files.Len()) + + files.uint64(p.stringOff(info.file.Name())) + files.uint64(uint64(info.file.Size())) + + // Delta-encode the line offsets, omitting the initial zero. + // (An empty file has an empty lines array.) + // + // TODO(adonovan): opt: use a two-pass approach that + // first gathers the set of Pos values and then + // encodes only the information necessary for them. + // This would allow us to discard the lines after the + // last object of interest and to run-length encode the + // trivial lines between lines with needed positions. + lines := getLines(info.file) + files.uint64(uint64(len(lines))) + for i := 1; i < len(lines); i++ { + files.uint64(uint64(lines[i] - lines[i-1])) + } + } + } + // Append indices to data0 section. dataLen := uint64(p.data0.Len()) w := p.newWriter() @@ -167,7 +194,11 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, ver hdr.uint64(uint64(p.version)) hdr.uint64(uint64(p.strings.Len())) if p.shallow { - hdr.uint64(uint64(p.files.Len())) + hdr.uint64(uint64(files.Len())) + hdr.uint64(uint64(len(fileOffset))) + for _, offset := range fileOffset { + hdr.uint64(offset) + } } hdr.uint64(dataLen) @@ -175,7 +206,7 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, ver io.Copy(out, &hdr) io.Copy(out, &p.strings) if p.shallow { - io.Copy(out, &p.files) + io.Copy(out, &files) } io.Copy(out, &p.data0) @@ -266,8 +297,9 @@ type iexporter struct { // In shallow mode, object positions are encoded as (file, offset). // Each file is recorded as a line-number table. - files intWriter - fileIndex map[*token.File]uint64 + // Only the lines of needed positions are saved faithfully. + fileInfo map[*token.File]uint64 // value is index in fileInfos + fileInfos []*filePositions data0 intWriter declIndex map[types.Object]uint64 @@ -277,6 +309,11 @@ type iexporter struct { indent int // for tracing support } +type filePositions struct { + file *token.File + needed []token.Pos // unordered list of needed positions +} + func (p *iexporter) trace(format string, args ...interface{}) { if !trace { // Call sites should also be guarded, but having this check here allows @@ -300,33 +337,21 @@ func (p *iexporter) stringOff(s string) uint64 { return off } -// fileOff returns the offset of the token.File encoding. -// If not already present, it's added to the end. -func (p *iexporter) fileOff(file *token.File) uint64 { - off, ok := p.fileIndex[file] +// fileIndex returns the index of the token.File. +func (p *iexporter) fileIndex(file *token.File, pos token.Pos) uint64 { + index, ok := p.fileInfo[file] if !ok { - off = uint64(p.files.Len()) - p.fileIndex[file] = off - - p.files.uint64(p.stringOff(file.Name())) - p.files.uint64(uint64(file.Size())) - - // Delta-encode the line offsets, omitting the initial zero. - // (An empty file has an empty lines array.) - // - // TODO(adonovan): opt: use a two-pass approach that - // first gathers the set of Pos values and then - // encodes only the information necessary for them. - // This would allow us to discard the lines after the - // last object of interest and to run-length encode the - // trivial lines between lines with needed positions. - lines := getLines(file) - p.files.uint64(uint64(len(lines))) - for i := 1; i < len(lines); i++ { - p.files.uint64(uint64(lines[i] - lines[i-1])) + index = uint64(len(p.fileInfo)) + p.fileInfos = append(p.fileInfos, &filePositions{file: file}) + if p.fileInfo == nil { + p.fileInfo = make(map[*token.File]uint64) } + p.fileInfo[file] = index } - return off + // Record each needed position. + info := p.fileInfos[index] + info.needed = append(info.needed, pos) + return index } // pushDecl adds n to the declaration work queue, if not already present. @@ -526,7 +551,7 @@ func (w *exportWriter) posV2(pos token.Pos) { return } file := w.p.fset.File(pos) // fset must be non-nil - w.uint64(1 + w.p.fileOff(file)) + w.uint64(1 + w.p.fileIndex(file, pos)) w.uint64(uint64(file.Offset(pos))) } diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index cb75952c0c6..df381d35b46 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -138,9 +138,14 @@ func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data sLen := int64(r.uint64()) var fLen int64 + var fileOffset []uint64 if insert != nil { - // shallow mode uses a different position encoding + // Shallow mode uses a different position encoding. fLen = int64(r.uint64()) + fileOffset = make([]uint64, r.uint64()) + for i := range fileOffset { + fileOffset[i] = r.uint64() + } } dLen := int64(r.uint64()) @@ -157,8 +162,9 @@ func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data stringData: stringData, stringCache: make(map[uint64]string), + fileOffset: fileOffset, fileData: fileData, - fileCache: make(map[uint64]*token.File), + fileCache: make([]*token.File, len(fileOffset)), pkgCache: make(map[uint64]*types.Package), declData: declData, @@ -288,8 +294,9 @@ type iimporter struct { stringData []byte stringCache map[uint64]string + fileOffset []uint64 // fileOffset[i] is offset in fileData for info about file encoded as i fileData []byte - fileCache map[uint64]*token.File + fileCache []*token.File // memoized decoding of file encoded as i pkgCache map[uint64]*types.Package declData []byte @@ -362,9 +369,10 @@ func (p *iimporter) stringAt(off uint64) string { return s } -func (p *iimporter) fileAt(off uint64) *token.File { - file, ok := p.fileCache[off] - if !ok { +func (p *iimporter) fileAt(index uint64) *token.File { + file := p.fileCache[index] + if file == nil { + off := p.fileOffset[index] rd := intReader{bytes.NewReader(p.fileData[off:]), p.ipath} filename := p.stringAt(rd.uint64()) size := int(rd.uint64()) @@ -380,7 +388,7 @@ func (p *iimporter) fileAt(off uint64) *token.File { } } - p.fileCache[off] = file + p.fileCache[index] = file } return file } From f3e53e5cc3bae1d9d27cec1ce5cc983fef5a6061 Mon Sep 17 00:00:00 2001 From: kouler Date: Sun, 8 Jan 2023 13:06:38 +0000 Subject: [PATCH 641/723] internal/jsonrpc2_v2: fix typos Change-Id: I96ad3f8fdb675bdb249934d92fd5d01d28c7dd6b GitHub-Last-Rev: 969dfc7945b6199ee4b28a04c48e8bbe45f4df7b GitHub-Pull-Request: golang/tools#421 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461075 Reviewed-by: Cherry Mui Run-TryBot: Bryan Mills Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Bryan Mills --- internal/jsonrpc2_v2/conn.go | 4 ++-- internal/jsonrpc2_v2/serve.go | 2 +- internal/jsonrpc2_v2/serve_pre116.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 60afa7060e4..04d1445cc92 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -156,11 +156,11 @@ func (c *Connection) updateInFlight(f func(*inFlightState)) { } } -// idle reports whether the connction is in a state with no pending calls or +// idle reports whether the connection is in a state with no pending calls or // notifications. // // If idle returns true, the readIncoming goroutine may still be running, -// but no other goroutines are doing work on behalf of the connnection. +// but no other goroutines are doing work on behalf of the connection. func (s *inFlightState) idle() bool { return len(s.outgoingCalls) == 0 && s.outgoingNotifications == 0 && s.incoming == 0 && !s.handlerRunning } diff --git a/internal/jsonrpc2_v2/serve.go b/internal/jsonrpc2_v2/serve.go index 7df785655d7..5e082735469 100644 --- a/internal/jsonrpc2_v2/serve.go +++ b/internal/jsonrpc2_v2/serve.go @@ -183,7 +183,7 @@ func (l *idleListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { // In theory the timeout could have raced with an unrelated error return // from Accept. However, ErrIdleTimeout is arguably still valid (since we // would have closed due to the timeout independent of the error), and the - // harm from returning a spurious ErrIdleTimeout is negliglible anyway. + // harm from returning a spurious ErrIdleTimeout is negligible anyway. } return nil, ErrIdleTimeout diff --git a/internal/jsonrpc2_v2/serve_pre116.go b/internal/jsonrpc2_v2/serve_pre116.go index 14afa834962..a1801d8a200 100644 --- a/internal/jsonrpc2_v2/serve_pre116.go +++ b/internal/jsonrpc2_v2/serve_pre116.go @@ -19,10 +19,10 @@ var errClosed = errors.New("use of closed network connection") // isErrClosed reports whether err ends in the same string as errClosed. func isErrClosed(err error) bool { // As of Go 1.16, this could be 'errors.Is(err, net.ErrClosing)', but - // unfortunately gopls still requires compatiblity with + // unfortunately gopls still requires compatibility with // (otherwise-unsupported) older Go versions. // - // In the meantime, this error strirng has not changed on any supported Go + // In the meantime, this error string has not changed on any supported Go // version, and is not expected to change in the future. // This is not ideal, but since the worst that could happen here is some // superfluous logging, it is acceptable. From f46e4185aa5ac36b323581af555cb8e6126a01c5 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 17 Jan 2023 18:53:22 -0500 Subject: [PATCH 642/723] gopls/internal/lsp/source: include ITVs in global references The global search for references should include intermediate test variants (ITVs), because the rdeps of every variant are disjoint. The existing comment was mistaken. The local search does not need them for the valid reason given in the existing comment. The previous call to RemoveIntermediateTestVariants had not quite the intended effect because we forgot to use its result. (D'oh.) The effect of the change in the test was to expose the bug by creating an ordinary test variant. Updates golang/go#57795 Change-Id: I1852aebf81ae0d169191834eb9a880d0ba90bc6e Reviewed-on: https://go-review.googlesource.com/c/tools/+/462497 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/source/references2.go | 30 +++++++++---------- .../internal/regtest/misc/references_test.go | 3 ++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index f24c07f1bc3..87347d485a9 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -282,18 +282,6 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp return nil, err } - // We want the ordinary importable package, - // plus any test-augmented variants, since - // declarations in _test.go files may change - // the reference of a selection, or even a - // field into a method or vice versa. - // - // But we don't need intermediate test variants, - // as both their local and global references - // will be covered already by equivalent (though - // not types.Identical) objects in other variants. - RemoveIntermediateTestVariants(metas) - // The search functions will call report(loc) for each hit. var ( refsMu sync.Mutex @@ -337,6 +325,18 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp // local group.Go(func() error { + // We want the ordinary importable package, + // plus any test-augmented variants, since + // declarations in _test.go files may change + // the reference of a selection, or even a + // field into a method or vice versa. + // + // But we don't need intermediate test variants, + // as their local references will be covered + // already by other variants. + if m.IsIntermediateTestVariant() { + return nil + } return localReferences(ctx, snapshot, declURI, declPosn.Offset, m, report) }) @@ -346,9 +346,9 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp // global group.Go(func() error { - // Compute the global-scope query for each variant - // of the declaring package in parallel. - // We may assume the rdeps of each variant are disjoint. + // Compute the global-scope query for every variant + // of the declaring package in parallel, + // as the rdeps of each variant are disjoint. rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, transitive) if err != nil { return err diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index e5ee019857f..bdaf610b86f 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -200,6 +200,9 @@ func _() { _ = bar.Blah } +-- foo/foo_test.go -- +package foo + -- bar/bar.go -- package bar From f6ea009c5f495984cd59a7db8271d5c46dd36137 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 17 Jan 2023 17:27:37 -0500 Subject: [PATCH 643/723] gopls/internal/lsp: remove the experimentalWatchedFileDelay setting See the associated issue for more details. Fixes golang/go#55332 Change-Id: I062423c090838c87dbb0c2d21ab448f6c72399c6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462496 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- gopls/doc/settings.md | 17 ------- gopls/internal/lsp/server.go | 11 ----- gopls/internal/lsp/source/api_json.go | 8 --- gopls/internal/lsp/source/options.go | 17 +------ gopls/internal/lsp/text_synchronization.go | 57 +--------------------- 5 files changed, 2 insertions(+), 108 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 3df086cf82b..1b8f6d779a0 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -358,23 +358,6 @@ This option must be set to a valid duration string, for example `"250ms"`. Default: `"250ms"`. -##### **experimentalWatchedFileDelay** *time.Duration* - -**This setting is experimental and may be deleted.** - -experimentalWatchedFileDelay controls the amount of time that gopls waits -for additional workspace/didChangeWatchedFiles notifications to arrive, -before processing all such notifications in a single batch. This is -intended for use by LSP clients that don't support their own batching of -file system notifications. - -This option must be set to a valid duration string, for example `"100ms"`. - -Deprecated: this setting is deprecated and will be removed in a future -version of gopls (https://go.dev/issue/55332) - -Default: `"0s"`. - #### Documentation ##### **hoverKind** *enum* diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go index 7e70d0a76ab..82d90dcf4f9 100644 --- a/gopls/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -33,7 +33,6 @@ func NewServer(session *cache.Session, client protocol.ClientCloser) *Server { diagnosticsSema: make(chan struct{}, concurrentAnalyses), progress: progress.NewTracker(client), diagDebouncer: newDebouncer(), - watchedFileDebouncer: newDebouncer(), } } @@ -106,22 +105,12 @@ type Server struct { // diagDebouncer is used for debouncing diagnostics. diagDebouncer *debouncer - // watchedFileDebouncer is used for batching didChangeWatchedFiles notifications. - watchedFileDebouncer *debouncer - fileChangeMu sync.Mutex - pendingOnDiskChanges []*pendingModificationSet - // When the workspace fails to load, we show its status through a progress // report with an error message. criticalErrorStatusMu sync.Mutex criticalErrorStatus *progress.WorkDone } -type pendingModificationSet struct { - diagnoseDone chan struct{} - changes []source.FileModification -} - func (s *Server) workDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error { return s.progress.Cancel(params.Token) } diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 11347e7bf80..0d24ececa1e 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -516,14 +516,6 @@ var GeneratedAPIJSON = &APIJSON{ Status: "advanced", Hierarchy: "ui.diagnostic", }, - { - Name: "experimentalWatchedFileDelay", - Type: "time.Duration", - Doc: "experimentalWatchedFileDelay controls the amount of time that gopls waits\nfor additional workspace/didChangeWatchedFiles notifications to arrive,\nbefore processing all such notifications in a single batch. This is\nintended for use by LSP clients that don't support their own batching of\nfile system notifications.\n\nThis option must be set to a valid duration string, for example `\"100ms\"`.\n\nDeprecated: this setting is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55332)\n", - Default: "\"0s\"", - Status: "experimental", - Hierarchy: "ui.diagnostic", - }, { Name: "hints", Type: "map[string]bool", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index ca3441a693b..01feb210d7a 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -434,18 +434,6 @@ type DiagnosticOptions struct { // // This option must be set to a valid duration string, for example `"250ms"`. DiagnosticsDelay time.Duration `status:"advanced"` - - // ExperimentalWatchedFileDelay controls the amount of time that gopls waits - // for additional workspace/didChangeWatchedFiles notifications to arrive, - // before processing all such notifications in a single batch. This is - // intended for use by LSP clients that don't support their own batching of - // file system notifications. - // - // This option must be set to a valid duration string, for example `"100ms"`. - // - // Deprecated: this setting is deprecated and will be removed in a future - // version of gopls (https://go.dev/issue/55332) - ExperimentalWatchedFileDelay time.Duration `status:"experimental"` } type InlayHintOptions struct { @@ -1120,10 +1108,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setDuration(&o.DiagnosticsDelay) case "experimentalWatchedFileDelay": - const msg = "experimentalWatchedFileDelay is deprecated, and will " + - "be removed in a future version of gopls (https://go.dev/issue/55332)" - result.softErrorf(msg) - result.setDuration(&o.ExperimentalWatchedFileDelay) + result.deprecated("") case "experimentalPackageCacheKey": result.setBool(&o.ExperimentalPackageCacheKey) diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go index e70bf9eb5b6..77e081a8a08 100644 --- a/gopls/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -10,14 +10,11 @@ import ( "errors" "fmt" "path/filepath" - "time" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/xcontext" ) // ModificationSource identifies the originating cause of a file modification. @@ -220,59 +217,7 @@ func (s *Server) didModifyFiles(ctx context.Context, modifications []source.File } onDisk := cause == FromDidChangeWatchedFiles - delay := s.session.Options().ExperimentalWatchedFileDelay - s.fileChangeMu.Lock() - defer s.fileChangeMu.Unlock() - if !onDisk || delay == 0 { - // No delay: process the modifications immediately. - return s.processModifications(ctx, modifications, onDisk, diagnoseDone) - } - // Debounce and batch up pending modifications from watched files. - pending := &pendingModificationSet{ - diagnoseDone: diagnoseDone, - changes: modifications, - } - // Invariant: changes appended to s.pendingOnDiskChanges are eventually - // handled in the order they arrive. This guarantee is only partially - // enforced here. Specifically: - // 1. s.fileChangesMu ensures that the append below happens in the order - // notifications were received, so that the changes within each batch are - // ordered properly. - // 2. The debounced func below holds s.fileChangesMu while processing all - // changes in s.pendingOnDiskChanges, ensuring that no batches are - // processed out of order. - // 3. Session.ExpandModificationsToDirectories and Session.DidModifyFiles - // process changes in order. - s.pendingOnDiskChanges = append(s.pendingOnDiskChanges, pending) - ctx = xcontext.Detach(ctx) - okc := s.watchedFileDebouncer.debounce("", 0, time.After(delay)) - go func() { - if ok := <-okc; !ok { - return - } - s.fileChangeMu.Lock() - var allChanges []source.FileModification - // For accurate progress notifications, we must notify all goroutines - // waiting for the diagnose pass following a didChangeWatchedFiles - // notification. This is necessary for regtest assertions. - var dones []chan struct{} - for _, pending := range s.pendingOnDiskChanges { - allChanges = append(allChanges, pending.changes...) - dones = append(dones, pending.diagnoseDone) - } - - allDone := make(chan struct{}) - if err := s.processModifications(ctx, allChanges, onDisk, allDone); err != nil { - event.Error(ctx, "processing delayed file changes", err) - } - s.pendingOnDiskChanges = nil - s.fileChangeMu.Unlock() - <-allDone - for _, done := range dones { - close(done) - } - }() - return nil + return s.processModifications(ctx, modifications, onDisk, diagnoseDone) } // processModifications update server state to reflect file changes, and From 5bedd861b12ec5276c0877b760e82f0469c7bf79 Mon Sep 17 00:00:00 2001 From: Fumitoshi Ukai Date: Wed, 18 Jan 2023 01:04:09 +0000 Subject: [PATCH 644/723] cmd/digraph: use ReadString rather than bufio.Scanner if an input line is too long (more than bufio.MaxScanTokenSize), bufio.Scanner returns bufio.ErrToolong. Use bufio's ReadString instead. Fixes golang.org/go#57807 Change-Id: If4165213668a468b844ee6a0ae84263475504208 GitHub-Last-Rev: e7057d029edcf3b51b1b54c820a2eafc1566de0f GitHub-Pull-Request: golang/tools#423 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462053 Run-TryBot: Ian Lance Taylor Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Ian Lance Taylor Reviewed-by: Bryan Mills gopls-CI: kokoro --- cmd/digraph/digraph.go | 21 +++++++++++++++------ cmd/digraph/digraph_test.go | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/cmd/digraph/digraph.go b/cmd/digraph/digraph.go index 69d84ad5012..0e50ad18dcb 100644 --- a/cmd/digraph/digraph.go +++ b/cmd/digraph/digraph.go @@ -351,20 +351,29 @@ func parse(rd io.Reader) (graph, error) { g := make(graph) var linenum int - in := bufio.NewScanner(rd) - for in.Scan() { + // We avoid bufio.Scanner as it imposes a (configurable) limit + // on line length, whereas Reader.ReadString does not. + in := bufio.NewReader(rd) + for { linenum++ + line, err := in.ReadString('\n') + eof := false + if err == io.EOF { + eof = true + } else if err != nil { + return nil, err + } // Split into words, honoring double-quotes per Go spec. - words, err := split(in.Text()) + words, err := split(line) if err != nil { return nil, fmt.Errorf("at line %d: %v", linenum, err) } if len(words) > 0 { g.addEdges(words[0], words[1:]...) } - } - if err := in.Err(); err != nil { - return nil, err + if eof { + break + } } return g, nil } diff --git a/cmd/digraph/digraph_test.go b/cmd/digraph/digraph_test.go index cff46735b2d..60b8e75eb72 100644 --- a/cmd/digraph/digraph_test.go +++ b/cmd/digraph/digraph_test.go @@ -45,6 +45,7 @@ e e {"scss", g2, "sccs", nil, "c d\ne\n"}, {"scc", g2, "scc", []string{"d"}, "c\nd\n"}, {"succs", g2, "succs", []string{"a"}, "b\nc\n"}, + {"succs-long-token", g2 + "x " + strings.Repeat("x", 96*1024), "succs", []string{"x"}, strings.Repeat("x", 96*1024) + "\n"}, {"preds", g2, "preds", []string{"c"}, "a\nd\n"}, {"preds multiple args", g2, "preds", []string{"c", "d"}, "a\nb\nc\nd\n"}, } { From f0e2d5c52e777589d1375465c4b2816266af60b4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 13 Jan 2023 15:12:19 -0500 Subject: [PATCH 645/723] internal/gcimporter: discard position info for unneeded lines This change implements an optimization in the size of the shallow export data: instead of saving the entire line start offset table of each file, it saves a sparse representation of each file capable of restoring accurate position information for the entirety of each line that contains an encoded token.Pos, but no others. This reduces the total size of export data for the standard library to 665KB, down 31% from 957KB, and only 18% more than the original baseline of 564KB. The shallow_test asserts (for a couple of specific objects) that the end positions may be derived from the start positions either before or after converting token.Pos to token.Position. Change-Id: I5c2a1648398d0bee979dc36ba6224072ea183bec Reviewed-on: https://go-review.googlesource.com/c/tools/+/462096 gopls-CI: kokoro Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- internal/gcimporter/iexport.go | 87 ++++++++++++++++++++--------- internal/gcimporter/iimport.go | 53 +++++++++++++----- internal/gcimporter/shallow_test.go | 67 ++++++++++++++++------ 3 files changed, 150 insertions(+), 57 deletions(-) diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index fa6fa3d9cdd..5e01184ae72 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -147,24 +147,7 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, ver fileOffset = make([]uint64, len(p.fileInfos)) for i, info := range p.fileInfos { fileOffset[i] = uint64(files.Len()) - - files.uint64(p.stringOff(info.file.Name())) - files.uint64(uint64(info.file.Size())) - - // Delta-encode the line offsets, omitting the initial zero. - // (An empty file has an empty lines array.) - // - // TODO(adonovan): opt: use a two-pass approach that - // first gathers the set of Pos values and then - // encodes only the information necessary for them. - // This would allow us to discard the lines after the - // last object of interest and to run-length encode the - // trivial lines between lines with needed positions. - lines := getLines(info.file) - files.uint64(uint64(len(lines))) - for i := 1; i < len(lines); i++ { - files.uint64(uint64(lines[i] - lines[i-1])) - } + p.encodeFile(&files, info.file, info.needed) } } @@ -213,6 +196,55 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, ver return nil } +// encodeFile writes to w a representation of the file sufficient to +// faithfully restore position information about all needed offsets. +// Mutates the needed array. +func (p *iexporter) encodeFile(w *intWriter, file *token.File, needed []uint64) { + _ = needed[0] // precondition: needed is non-empty + + w.uint64(p.stringOff(file.Name())) + + size := uint64(file.Size()) + w.uint64(size) + + // Sort the set of needed offsets. Duplicates are harmless. + sort.Slice(needed, func(i, j int) bool { return needed[i] < needed[j] }) + + lines := getLines(file) // byte offset of each line start + w.uint64(uint64(len(lines))) + + // Rather than record the entire array of line start offsets, + // we save only a sparse list of (index, offset) pairs for + // the start of each line that contains a needed position. + var sparse [][2]int // (index, offset) pairs +outer: + for i, lineStart := range lines { + lineEnd := size + if i < len(lines)-1 { + lineEnd = uint64(lines[i+1]) + } + // Does this line contains a needed offset? + if needed[0] < lineEnd { + sparse = append(sparse, [2]int{i, lineStart}) + for needed[0] < lineEnd { + needed = needed[1:] + if len(needed) == 0 { + break outer + } + } + } + } + + // Delta-encode the columns. + w.uint64(uint64(len(sparse))) + var prev [2]int + for _, pair := range sparse { + w.uint64(uint64(pair[0] - prev[0])) + w.uint64(uint64(pair[1] - prev[1])) + prev = pair + } +} + // writeIndex writes out an object index. mainIndex indicates whether // we're writing out the main index, which is also read by // non-compiler tools and includes a complete package description @@ -311,7 +343,7 @@ type iexporter struct { type filePositions struct { file *token.File - needed []token.Pos // unordered list of needed positions + needed []uint64 // unordered list of needed file offsets } func (p *iexporter) trace(format string, args ...interface{}) { @@ -337,8 +369,8 @@ func (p *iexporter) stringOff(s string) uint64 { return off } -// fileIndex returns the index of the token.File. -func (p *iexporter) fileIndex(file *token.File, pos token.Pos) uint64 { +// fileIndex returns the index of the token.File and the byte offset of pos within it. +func (p *iexporter) fileIndexAndOffset(file *token.File, pos token.Pos) (uint64, uint64) { index, ok := p.fileInfo[file] if !ok { index = uint64(len(p.fileInfo)) @@ -348,10 +380,12 @@ func (p *iexporter) fileIndex(file *token.File, pos token.Pos) uint64 { } p.fileInfo[file] = index } - // Record each needed position. + // Record each needed offset. info := p.fileInfos[index] - info.needed = append(info.needed, pos) - return index + offset := uint64(file.Offset(pos)) + info.needed = append(info.needed, offset) + + return index, offset } // pushDecl adds n to the declaration work queue, if not already present. @@ -551,8 +585,9 @@ func (w *exportWriter) posV2(pos token.Pos) { return } file := w.p.fset.File(pos) // fset must be non-nil - w.uint64(1 + w.p.fileIndex(file, pos)) - w.uint64(uint64(file.Offset(pos))) + index, offset := w.p.fileIndexAndOffset(file, pos) + w.uint64(1 + index) + w.uint64(offset) } func (w *exportWriter) posV1(pos token.Pos) { diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index df381d35b46..448f903e86a 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -373,22 +373,47 @@ func (p *iimporter) fileAt(index uint64) *token.File { file := p.fileCache[index] if file == nil { off := p.fileOffset[index] - rd := intReader{bytes.NewReader(p.fileData[off:]), p.ipath} - filename := p.stringAt(rd.uint64()) - size := int(rd.uint64()) - file = p.fake.fset.AddFile(filename, -1, size) - - if n := int(rd.uint64()); n > 0 { - lines := make([]int, n) // initial element always implicitly zero - for i := 1; i < n; i++ { - lines[i] = lines[i-1] + int(rd.uint64()) - } - if !file.SetLines(lines) { - errorf("SetLines failed") // can't happen - } + file = p.decodeFile(intReader{bytes.NewReader(p.fileData[off:]), p.ipath}) + p.fileCache[index] = file + } + return file +} + +func (p *iimporter) decodeFile(rd intReader) *token.File { + filename := p.stringAt(rd.uint64()) + size := int(rd.uint64()) + file := p.fake.fset.AddFile(filename, -1, size) + + // SetLines requires a nondecreasing sequence. + // Because it is common for clients to derive the interval + // [start, start+len(name)] from a start position, and we + // want to ensure that the end offset is on the same line, + // we fill in the gaps of the sparse encoding with values + // that strictly increase by the largest possible amount. + // This allows us to avoid having to record the actual end + // offset of each needed line. + + lines := make([]int, int(rd.uint64())) + var index, offset int + for i, n := 0, int(rd.uint64()); i < n; i++ { + index += int(rd.uint64()) + offset += int(rd.uint64()) + lines[index] = offset + + // Ensure monotonicity between points. + for j := index - 1; j > 0 && lines[j] == 0; j-- { + lines[j] = lines[j+1] - 1 } + } - p.fileCache[index] = file + // Ensure monotonicity after last point. + for j := len(lines) - 1; j > 0 && lines[j] == 0; j-- { + size-- + lines[j] = size + } + + if !file.SetLines(lines) { + errorf("SetLines failed: %d", lines) // can't happen } return file } diff --git a/internal/gcimporter/shallow_test.go b/internal/gcimporter/shallow_test.go index 443bb30149e..429c34b3dd7 100644 --- a/internal/gcimporter/shallow_test.go +++ b/internal/gcimporter/shallow_test.go @@ -171,23 +171,56 @@ func typecheck(t *testing.T, ppkg *packages.Package) { // corresponds to its source location: in other words, // export+import preserves high-fidelity positions. func postTypeCheck(t *testing.T, fset *token.FileSet, pkg *types.Package) { - if pkg.Path() == "fmt" { - obj := pkg.Scope().Lookup("Println") - posn := fset.Position(obj.Pos()) - data, err := os.ReadFile(posn.Filename) - if err != nil { - t.Errorf("can't read source file declaring fmt.Println: %v", err) - return - } - // Check line and column. - line := strings.Split(string(data), "\n")[posn.Line-1] + // We hard-code a few interesting test-case objects. + var obj types.Object + switch pkg.Path() { + case "fmt": + // func fmt.Println + obj = pkg.Scope().Lookup("Println") + case "net/http": + // method (*http.Request).ParseForm + req := pkg.Scope().Lookup("Request") + obj, _, _ = types.LookupFieldOrMethod(req.Type(), true, pkg, "ParseForm") + default: + return + } + if obj == nil { + t.Errorf("object not found in package %s", pkg.Path()) + return + } - if id := line[posn.Column-1 : posn.Column-1+len(obj.Name())]; id != "Println" { - t.Errorf("%+v: expected declaration of fmt.Println at this line, column; got %q", posn, line) - } - // Check offset. - if id := string(data[posn.Offset : posn.Offset+len(obj.Name())]); id != "Println" { - t.Errorf("%+v: expected declaration of fmt.Println at this offset; got %q", posn, id) - } + // Now check the source fidelity of the object's position. + posn := fset.Position(obj.Pos()) + data, err := os.ReadFile(posn.Filename) + if err != nil { + t.Errorf("can't read source file declaring %v: %v", obj, err) + return + } + + // Check line and column denote a source interval containing the object's identifier. + line := strings.Split(string(data), "\n")[posn.Line-1] + + if id := line[posn.Column-1 : posn.Column-1+len(obj.Name())]; id != obj.Name() { + t.Errorf("%+v: expected declaration of %v at this line, column; got %q", posn, obj, line) + } + + // Check offset. + if id := string(data[posn.Offset : posn.Offset+len(obj.Name())]); id != obj.Name() { + t.Errorf("%+v: expected declaration of %v at this offset; got %q", posn, obj, id) + } + + // Check commutativity of Position() and start+len(name) operations: + // Position(startPos+len(name)) == Position(startPos) + len(name). + // This important property is a consequence of the way in which the + // decoder fills the gaps in the sparse line-start offset table. + endPosn := fset.Position(obj.Pos() + token.Pos(len(obj.Name()))) + wantEndPosn := token.Position{ + Filename: posn.Filename, + Offset: posn.Offset + len(obj.Name()), + Line: posn.Line, + Column: posn.Column + len(obj.Name()), + } + if endPosn != wantEndPosn { + t.Errorf("%+v: expected end Position of %v here; was at %+v", wantEndPosn, obj, endPosn) } } From 957bec5ae5bdd0549c2ffd4c8d07a39f61f471e5 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Mon, 16 Jan 2023 07:55:22 -0500 Subject: [PATCH 646/723] gopls/protocol: new versions of generated LSP files These are the files generated by the new code in protocol/generate. They are drop-in replacements for the existing files, requiring no changes to the rest of gopls. Change-Id: Ib1a40cb1c63acab3d1e0a3f264241ff0cc3b0f54 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462235 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Peter Weinberger Reviewed-by: Robert Findley --- gopls/internal/lsp/protocol/tsclient.go | 67 +- gopls/internal/lsp/protocol/tsjson.go | 1585 ++++++++++++++++++++- gopls/internal/lsp/protocol/tsprotocol.go | 630 ++++++-- gopls/internal/lsp/protocol/tsserver.go | 307 ++-- 4 files changed, 2276 insertions(+), 313 deletions(-) diff --git a/gopls/internal/lsp/protocol/tsclient.go b/gopls/internal/lsp/protocol/tsclient.go index b8f11b50f37..116a81b4d5c 100644 --- a/gopls/internal/lsp/protocol/tsclient.go +++ b/gopls/internal/lsp/protocol/tsclient.go @@ -1,12 +1,13 @@ -// Copyright 2019-2022 The Go Authors. All rights reserved. +// Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Code generated for LSP. DO NOT EDIT. + package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) -// Code generated; DO NOT EDIT. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14) import ( "context" @@ -41,49 +42,49 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, return true, sendParseError(ctx, reply, err) } err := client.LogTrace(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "$/progress": var params ProgressParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.Progress(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "client/registerCapability": var params RegistrationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.RegisterCapability(ctx, ¶ms) - return true, reply(ctx, nil, err) // 155 + return true, reply(ctx, nil, err) case "client/unregisterCapability": var params UnregistrationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.UnregisterCapability(ctx, ¶ms) - return true, reply(ctx, nil, err) // 155 + return true, reply(ctx, nil, err) case "telemetry/event": var params interface{} if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.Event(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "textDocument/publishDiagnostics": var params PublishDiagnosticsParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.PublishDiagnostics(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "window/logMessage": var params LogMessageParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.LogMessage(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "window/showDocument": var params ShowDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -93,14 +94,14 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "window/showMessage": var params ShowMessageParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.ShowMessage(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "window/showMessageRequest": var params ShowMessageRequestParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -110,14 +111,14 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "window/workDoneProgress/create": var params WorkDoneProgressCreateParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.WorkDoneProgressCreate(ctx, ¶ms) - return true, reply(ctx, nil, err) // 155 + return true, reply(ctx, nil, err) case "workspace/applyEdit": var params ApplyWorkspaceEditParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -127,10 +128,10 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "workspace/codeLens/refresh": err := client.CodeLensRefresh(ctx) - return true, reply(ctx, nil, err) // 170 + return true, reply(ctx, nil, err) case "workspace/configuration": var params ParamConfiguration if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -140,13 +141,13 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "workspace/workspaceFolders": resp, err := client.WorkspaceFolders(ctx) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 165 + return true, reply(ctx, resp, nil) default: return false, nil } @@ -154,66 +155,66 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, func (s *clientDispatcher) LogTrace(ctx context.Context, params *LogTraceParams) error { return s.sender.Notify(ctx, "$/logTrace", params) -} // 244 +} func (s *clientDispatcher) Progress(ctx context.Context, params *ProgressParams) error { return s.sender.Notify(ctx, "$/progress", params) -} // 244 +} func (s *clientDispatcher) RegisterCapability(ctx context.Context, params *RegistrationParams) error { return s.sender.Call(ctx, "client/registerCapability", params, nil) -} // 194 +} func (s *clientDispatcher) UnregisterCapability(ctx context.Context, params *UnregistrationParams) error { return s.sender.Call(ctx, "client/unregisterCapability", params, nil) -} // 194 +} func (s *clientDispatcher) Event(ctx context.Context, params *interface{}) error { return s.sender.Notify(ctx, "telemetry/event", params) -} // 244 +} func (s *clientDispatcher) PublishDiagnostics(ctx context.Context, params *PublishDiagnosticsParams) error { return s.sender.Notify(ctx, "textDocument/publishDiagnostics", params) -} // 244 +} func (s *clientDispatcher) LogMessage(ctx context.Context, params *LogMessageParams) error { return s.sender.Notify(ctx, "window/logMessage", params) -} // 244 +} func (s *clientDispatcher) ShowDocument(ctx context.Context, params *ShowDocumentParams) (*ShowDocumentResult, error) { var result *ShowDocumentResult if err := s.sender.Call(ctx, "window/showDocument", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *clientDispatcher) ShowMessage(ctx context.Context, params *ShowMessageParams) error { return s.sender.Notify(ctx, "window/showMessage", params) -} // 244 +} func (s *clientDispatcher) ShowMessageRequest(ctx context.Context, params *ShowMessageRequestParams) (*MessageActionItem, error) { var result *MessageActionItem if err := s.sender.Call(ctx, "window/showMessageRequest", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *clientDispatcher) WorkDoneProgressCreate(ctx context.Context, params *WorkDoneProgressCreateParams) error { return s.sender.Call(ctx, "window/workDoneProgress/create", params, nil) -} // 194 +} func (s *clientDispatcher) ApplyEdit(ctx context.Context, params *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) { var result *ApplyWorkspaceEditResult if err := s.sender.Call(ctx, "workspace/applyEdit", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *clientDispatcher) CodeLensRefresh(ctx context.Context) error { return s.sender.Call(ctx, "workspace/codeLens/refresh", nil, nil) -} // 209 +} func (s *clientDispatcher) Configuration(ctx context.Context, params *ParamConfiguration) ([]LSPAny, error) { var result []LSPAny if err := s.sender.Call(ctx, "workspace/configuration", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *clientDispatcher) WorkspaceFolders(ctx context.Context) ([]WorkspaceFolder, error) { var result []WorkspaceFolder if err := s.sender.Call(ctx, "workspace/workspaceFolders", nil, &result); err != nil { return nil, err } return result, nil -} // 204 +} diff --git a/gopls/internal/lsp/protocol/tsjson.go b/gopls/internal/lsp/protocol/tsjson.go index 3c9781ade0b..a0f11c61238 100644 --- a/gopls/internal/lsp/protocol/tsjson.go +++ b/gopls/internal/lsp/protocol/tsjson.go @@ -1,17 +1,20 @@ -// Copyright 2019-2022 The Go Authors. All rights reserved. +// Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Code generated for LSP. DO NOT EDIT. + package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) -// Code generated; DO NOT EDIT. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14) import "encoding/json" + import "errors" import "fmt" +// from line 4790 func (t OrFEditRangePItemDefaults) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case FEditRangePItemDefaults: @@ -42,6 +45,7 @@ func (t *OrFEditRangePItemDefaults) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [FEditRangePItemDefaults Range]") } +// from line 9837 func (t OrFNotebookPNotebookSelector) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case NotebookDocumentFilter: @@ -72,6 +76,7 @@ func (t *OrFNotebookPNotebookSelector) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [NotebookDocumentFilter string]") } +// from line 5539 func (t OrPLocation_workspace_symbol) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case Location: @@ -102,6 +107,7 @@ func (t *OrPLocation_workspace_symbol) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [Location PLocationMsg_workspace_symbol]") } +// from line 4185 func (t OrPSection_workspace_didChangeConfiguration) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case []string: @@ -132,6 +138,7 @@ func (t *OrPSection_workspace_didChangeConfiguration) UnmarshalJSON(x []byte) er return errors.New("unmarshal failed to match one of [[]string string]") } +// from line 7080 func (t OrPTooltipPLabel) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case MarkupContent: @@ -162,6 +169,7 @@ func (t *OrPTooltipPLabel) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [MarkupContent string]") } +// from line 3721 func (t OrPTooltip_textDocument_inlayHint) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case MarkupContent: @@ -192,6 +200,100 @@ func (t *OrPTooltip_textDocument_inlayHint) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [MarkupContent string]") } +// from line 6203 +func (t Or_CancelParams_id) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case int32: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [int32 string]", t) +} + +func (t *Or_CancelParams_id) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 int32 + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [int32 string]") +} + +// from line 4604 +func (t Or_CompletionItem_documentation) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case MarkupContent: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +} + +func (t *Or_CompletionItem_documentation) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 MarkupContent + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [MarkupContent string]") +} + +// from line 4686 +func (t Or_CompletionItem_textEdit) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case InsertReplaceEdit: + return json.Marshal(x) + case TextEdit: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [InsertReplaceEdit TextEdit]", t) +} + +func (t *Or_CompletionItem_textEdit) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 InsertReplaceEdit + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 TextEdit + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [InsertReplaceEdit TextEdit]") +} + +// from line 13779 func (t Or_Definition) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case Location: @@ -222,6 +324,38 @@ func (t *Or_Definition) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [Location []Location]") } +// from line 8567 +func (t Or_Diagnostic_code) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case int32: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [int32 string]", t) +} + +func (t *Or_Diagnostic_code) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 int32 + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [int32 string]") +} + +// from line 13911 func (t Or_DocumentDiagnosticReport) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case RelatedFullDocumentDiagnosticReport: @@ -252,6 +386,38 @@ func (t *Or_DocumentDiagnosticReport) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]") } +// from line 3844 +func (t Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case FullDocumentDiagnosticReport: + return json.Marshal(x) + case UnchangedDocumentDiagnosticReport: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]", t) +} + +func (t *Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 FullDocumentDiagnosticReport + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 UnchangedDocumentDiagnosticReport + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]") +} + +// from line 14120 func (t Or_DocumentFilter) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case NotebookCellTextDocumentFilter: @@ -282,6 +448,76 @@ func (t *Or_DocumentFilter) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [NotebookCellTextDocumentFilter TextDocumentFilter]") } +// from line 4912 +func (t Or_Hover_contents) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case MarkedString: + return json.Marshal(x) + case MarkupContent: + return json.Marshal(x) + case []MarkedString: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [MarkedString MarkupContent []MarkedString]", t) +} + +func (t *Or_Hover_contents) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 MarkedString + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 MarkupContent + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 []MarkedString + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [MarkedString MarkupContent []MarkedString]") +} + +// from line 3680 +func (t Or_InlayHint_label) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case []InlayHintLabelPart: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [[]InlayHintLabelPart string]", t) +} + +func (t *Or_InlayHint_label) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 []InlayHintLabelPart + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [[]InlayHintLabelPart string]") +} + +// from line 13889 func (t Or_InlineValue) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case InlineValueEvaluatableExpression: @@ -319,6 +555,7 @@ func (t *Or_InlineValue) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]") } +// from line 14086 func (t Or_MarkedString) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case Msg_MarkedString: @@ -349,66 +586,1376 @@ func (t *Or_MarkedString) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [Msg_MarkedString string]") } -func (t Or_RelativePattern_baseUri) MarshalJSON() ([]byte, error) { +// from line 10144 +func (t Or_NotebookCellTextDocumentFilter_notebook) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { - case URI: + case NotebookDocumentFilter: return json.Marshal(x) - case WorkspaceFolder: + case string: return json.Marshal(x) case nil: return []byte("null"), nil } - return nil, fmt.Errorf("type %T not one of [URI WorkspaceFolder]", t) + return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) } -func (t *Or_RelativePattern_baseUri) UnmarshalJSON(x []byte) error { +func (t *Or_NotebookCellTextDocumentFilter_notebook) UnmarshalJSON(x []byte) error { if string(x) == "null" { t.Value = nil return nil } - var h0 URI + var h0 NotebookDocumentFilter if err := json.Unmarshal(x, &h0); err == nil { t.Value = h0 return nil } - var h1 WorkspaceFolder + var h1 string if err := json.Unmarshal(x, &h1); err == nil { t.Value = h1 return nil } - return errors.New("unmarshal failed to match one of [URI WorkspaceFolder]") + return errors.New("unmarshal failed to match one of [NotebookDocumentFilter string]") } -func (t Or_WorkspaceDocumentDiagnosticReport) MarshalJSON() ([]byte, error) { +// from line 9883 +func (t Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { - case WorkspaceFullDocumentDiagnosticReport: + case NotebookDocumentFilter: return json.Marshal(x) - case WorkspaceUnchangedDocumentDiagnosticReport: + case string: return json.Marshal(x) case nil: return []byte("null"), nil } - return nil, fmt.Errorf("type %T not one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]", t) + return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) } -func (t *Or_WorkspaceDocumentDiagnosticReport) UnmarshalJSON(x []byte) error { +func (t *Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook) UnmarshalJSON(x []byte) error { if string(x) == "null" { t.Value = nil return nil } - var h0 WorkspaceFullDocumentDiagnosticReport + var h0 NotebookDocumentFilter if err := json.Unmarshal(x, &h0); err == nil { t.Value = h0 return nil } - var h1 WorkspaceUnchangedDocumentDiagnosticReport + var h1 string if err := json.Unmarshal(x, &h1); err == nil { t.Value = h1 return nil } - return errors.New("unmarshal failed to match one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]") + return errors.New("unmarshal failed to match one of [NotebookDocumentFilter string]") +} + +// from line 7173 +func (t Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case FullDocumentDiagnosticReport: + return json.Marshal(x) + case UnchangedDocumentDiagnosticReport: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]", t) +} + +func (t *Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 FullDocumentDiagnosticReport + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 UnchangedDocumentDiagnosticReport + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]") +} + +// from line 7212 +func (t Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case FullDocumentDiagnosticReport: + return json.Marshal(x) + case UnchangedDocumentDiagnosticReport: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]", t) +} + +func (t *Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 FullDocumentDiagnosticReport + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 UnchangedDocumentDiagnosticReport + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]") +} + +// from line 10767 +func (t Or_RelativePattern_baseUri) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case URI: + return json.Marshal(x) + case WorkspaceFolder: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [URI WorkspaceFolder]", t) +} + +func (t *Or_RelativePattern_baseUri) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 URI + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 WorkspaceFolder + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [URI WorkspaceFolder]") +} + +// from line 1379 +func (t Or_Result_textDocument_codeAction_Item0_Elem) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case CodeAction: + return json.Marshal(x) + case Command: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [CodeAction Command]", t) +} + +func (t *Or_Result_textDocument_codeAction_Item0_Elem) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 CodeAction + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 Command + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [CodeAction Command]") +} + +// from line 12223 +func (t Or_SemanticTokensClientCapabilities_requests_full) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case FFullPRequests: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [FFullPRequests bool]", t) +} + +func (t *Or_SemanticTokensClientCapabilities_requests_full) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 FFullPRequests + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [FFullPRequests bool]") +} + +// from line 12203 +func (t Or_SemanticTokensClientCapabilities_requests_range) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case FRangePRequests: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [FRangePRequests bool]", t) +} + +func (t *Or_SemanticTokensClientCapabilities_requests_range) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 FRangePRequests + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [FRangePRequests bool]") +} + +// from line 6584 +func (t Or_SemanticTokensOptions_full) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case PFullESemanticTokensOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [PFullESemanticTokensOptions bool]", t) +} + +func (t *Or_SemanticTokensOptions_full) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 PFullESemanticTokensOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [PFullESemanticTokensOptions bool]") +} + +// from line 6564 +func (t Or_SemanticTokensOptions_range) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case PRangeESemanticTokensOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [PRangeESemanticTokensOptions bool]", t) +} + +func (t *Or_SemanticTokensOptions_range) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 PRangeESemanticTokensOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [PRangeESemanticTokensOptions bool]") +} + +// from line 8247 +func (t Or_ServerCapabilities_callHierarchyProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case CallHierarchyOptions: + return json.Marshal(x) + case CallHierarchyRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [CallHierarchyOptions CallHierarchyRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_callHierarchyProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 CallHierarchyOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 CallHierarchyRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [CallHierarchyOptions CallHierarchyRegistrationOptions bool]") +} + +// from line 8055 +func (t Or_ServerCapabilities_codeActionProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case CodeActionOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [CodeActionOptions bool]", t) +} + +func (t *Or_ServerCapabilities_codeActionProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 CodeActionOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [CodeActionOptions bool]") +} + +// from line 8091 +func (t Or_ServerCapabilities_colorProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case DocumentColorOptions: + return json.Marshal(x) + case DocumentColorRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [DocumentColorOptions DocumentColorRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_colorProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 DocumentColorOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 DocumentColorRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [DocumentColorOptions DocumentColorRegistrationOptions bool]") +} + +// from line 7917 +func (t Or_ServerCapabilities_declarationProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case DeclarationOptions: + return json.Marshal(x) + case DeclarationRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [DeclarationOptions DeclarationRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_declarationProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 DeclarationOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 DeclarationRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [DeclarationOptions DeclarationRegistrationOptions bool]") +} + +// from line 7939 +func (t Or_ServerCapabilities_definitionProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case DefinitionOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [DefinitionOptions bool]", t) +} + +func (t *Or_ServerCapabilities_definitionProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 DefinitionOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [DefinitionOptions bool]") +} + +// from line 8404 +func (t Or_ServerCapabilities_diagnosticProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case DiagnosticOptions: + return json.Marshal(x) + case DiagnosticRegistrationOptions: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [DiagnosticOptions DiagnosticRegistrationOptions]", t) +} + +func (t *Or_ServerCapabilities_diagnosticProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 DiagnosticOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 DiagnosticRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [DiagnosticOptions DiagnosticRegistrationOptions]") +} + +// from line 8131 +func (t Or_ServerCapabilities_documentFormattingProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case DocumentFormattingOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [DocumentFormattingOptions bool]", t) +} + +func (t *Or_ServerCapabilities_documentFormattingProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 DocumentFormattingOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [DocumentFormattingOptions bool]") +} + +// from line 8019 +func (t Or_ServerCapabilities_documentHighlightProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case DocumentHighlightOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [DocumentHighlightOptions bool]", t) +} + +func (t *Or_ServerCapabilities_documentHighlightProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 DocumentHighlightOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [DocumentHighlightOptions bool]") +} + +// from line 8149 +func (t Or_ServerCapabilities_documentRangeFormattingProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case DocumentRangeFormattingOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [DocumentRangeFormattingOptions bool]", t) +} + +func (t *Or_ServerCapabilities_documentRangeFormattingProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 DocumentRangeFormattingOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [DocumentRangeFormattingOptions bool]") +} + +// from line 8037 +func (t Or_ServerCapabilities_documentSymbolProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case DocumentSymbolOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [DocumentSymbolOptions bool]", t) +} + +func (t *Or_ServerCapabilities_documentSymbolProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 DocumentSymbolOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [DocumentSymbolOptions bool]") +} + +// from line 8194 +func (t Or_ServerCapabilities_foldingRangeProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case FoldingRangeOptions: + return json.Marshal(x) + case FoldingRangeRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [FoldingRangeOptions FoldingRangeRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_foldingRangeProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 FoldingRangeOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 FoldingRangeRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [FoldingRangeOptions FoldingRangeRegistrationOptions bool]") +} + +// from line 7890 +func (t Or_ServerCapabilities_hoverProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case HoverOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [HoverOptions bool]", t) +} + +func (t *Or_ServerCapabilities_hoverProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 HoverOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [HoverOptions bool]") +} + +// from line 7979 +func (t Or_ServerCapabilities_implementationProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case ImplementationOptions: + return json.Marshal(x) + case ImplementationRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [ImplementationOptions ImplementationRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_implementationProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 ImplementationOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 ImplementationRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [ImplementationOptions ImplementationRegistrationOptions bool]") +} + +// from line 8381 +func (t Or_ServerCapabilities_inlayHintProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case InlayHintOptions: + return json.Marshal(x) + case InlayHintRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [InlayHintOptions InlayHintRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_inlayHintProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 InlayHintOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 InlayHintRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [InlayHintOptions InlayHintRegistrationOptions bool]") +} + +// from line 8358 +func (t Or_ServerCapabilities_inlineValueProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case InlineValueOptions: + return json.Marshal(x) + case InlineValueRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [InlineValueOptions InlineValueRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_inlineValueProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 InlineValueOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 InlineValueRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [InlineValueOptions InlineValueRegistrationOptions bool]") +} + +// from line 8270 +func (t Or_ServerCapabilities_linkedEditingRangeProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case LinkedEditingRangeOptions: + return json.Marshal(x) + case LinkedEditingRangeRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_linkedEditingRangeProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 LinkedEditingRangeOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 LinkedEditingRangeRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool]") +} + +// from line 8312 +func (t Or_ServerCapabilities_monikerProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case MonikerOptions: + return json.Marshal(x) + case MonikerRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [MonikerOptions MonikerRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_monikerProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 MonikerOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 MonikerRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [MonikerOptions MonikerRegistrationOptions bool]") +} + +// from line 7862 +func (t Or_ServerCapabilities_notebookDocumentSync) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case NotebookDocumentSyncOptions: + return json.Marshal(x) + case NotebookDocumentSyncRegistrationOptions: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions]", t) +} + +func (t *Or_ServerCapabilities_notebookDocumentSync) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 NotebookDocumentSyncOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 NotebookDocumentSyncRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions]") +} + +// from line 8001 +func (t Or_ServerCapabilities_referencesProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case ReferenceOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [ReferenceOptions bool]", t) +} + +func (t *Or_ServerCapabilities_referencesProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 ReferenceOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [ReferenceOptions bool]") +} + +// from line 8176 +func (t Or_ServerCapabilities_renameProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case RenameOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [RenameOptions bool]", t) +} + +func (t *Or_ServerCapabilities_renameProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 RenameOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [RenameOptions bool]") +} + +// from line 8216 +func (t Or_ServerCapabilities_selectionRangeProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case SelectionRangeOptions: + return json.Marshal(x) + case SelectionRangeRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [SelectionRangeOptions SelectionRangeRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_selectionRangeProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 SelectionRangeOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 SelectionRangeRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [SelectionRangeOptions SelectionRangeRegistrationOptions bool]") +} + +// from line 8293 +func (t Or_ServerCapabilities_semanticTokensProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case SemanticTokensOptions: + return json.Marshal(x) + case SemanticTokensRegistrationOptions: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [SemanticTokensOptions SemanticTokensRegistrationOptions]", t) +} + +func (t *Or_ServerCapabilities_semanticTokensProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 SemanticTokensOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 SemanticTokensRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [SemanticTokensOptions SemanticTokensRegistrationOptions]") +} + +// from line 7844 +func (t Or_ServerCapabilities_textDocumentSync) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case TextDocumentSyncKind: + return json.Marshal(x) + case TextDocumentSyncOptions: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [TextDocumentSyncKind TextDocumentSyncOptions]", t) +} + +func (t *Or_ServerCapabilities_textDocumentSync) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 TextDocumentSyncKind + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 TextDocumentSyncOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [TextDocumentSyncKind TextDocumentSyncOptions]") +} + +// from line 7957 +func (t Or_ServerCapabilities_typeDefinitionProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case TypeDefinitionOptions: + return json.Marshal(x) + case TypeDefinitionRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_typeDefinitionProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 TypeDefinitionOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 TypeDefinitionRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool]") +} + +// from line 8335 +func (t Or_ServerCapabilities_typeHierarchyProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case TypeHierarchyOptions: + return json.Marshal(x) + case TypeHierarchyRegistrationOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool]", t) +} + +func (t *Or_ServerCapabilities_typeHierarchyProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 TypeHierarchyOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 TypeHierarchyRegistrationOptions + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 bool + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool]") +} + +// from line 8113 +func (t Or_ServerCapabilities_workspaceSymbolProvider) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case WorkspaceSymbolOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [WorkspaceSymbolOptions bool]", t) +} + +func (t *Or_ServerCapabilities_workspaceSymbolProvider) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 WorkspaceSymbolOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [WorkspaceSymbolOptions bool]") +} + +// from line 8861 +func (t Or_SignatureInformation_documentation) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case MarkupContent: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +} + +func (t *Or_SignatureInformation_documentation) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 MarkupContent + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [MarkupContent string]") +} + +// from line 6697 +func (t Or_TextDocumentEdit_edits_Elem) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case AnnotatedTextEdit: + return json.Marshal(x) + case TextEdit: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [AnnotatedTextEdit TextEdit]", t) +} + +func (t *Or_TextDocumentEdit_edits_Elem) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 AnnotatedTextEdit + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 TextEdit + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [AnnotatedTextEdit TextEdit]") +} + +// from line 9803 +func (t Or_TextDocumentSyncOptions_save) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case SaveOptions: + return json.Marshal(x) + case bool: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [SaveOptions bool]", t) +} + +func (t *Or_TextDocumentSyncOptions_save) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 SaveOptions + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 bool + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [SaveOptions bool]") +} + +// from line 14012 +func (t Or_WorkspaceDocumentDiagnosticReport) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case WorkspaceFullDocumentDiagnosticReport: + return json.Marshal(x) + case WorkspaceUnchangedDocumentDiagnosticReport: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]", t) +} + +func (t *Or_WorkspaceDocumentDiagnosticReport) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 WorkspaceFullDocumentDiagnosticReport + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 WorkspaceUnchangedDocumentDiagnosticReport + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]") +} + +// from line 3241 +func (t Or_WorkspaceEdit_documentChanges_Elem) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case CreateFile: + return json.Marshal(x) + case DeleteFile: + return json.Marshal(x) + case RenameFile: + return json.Marshal(x) + case TextDocumentEdit: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [CreateFile DeleteFile RenameFile TextDocumentEdit]", t) +} + +func (t *Or_WorkspaceEdit_documentChanges_Elem) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 CreateFile + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 DeleteFile + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 RenameFile + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + var h3 TextDocumentEdit + if err := json.Unmarshal(x, &h3); err == nil { + t.Value = h3 + return nil + } + return errors.New("unmarshal failed to match one of [CreateFile DeleteFile RenameFile TextDocumentEdit]") } +// from line 256 func (t Or_textDocument_declaration) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case Declaration: diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go index 8272d7e80b3..becb255b1fb 100644 --- a/gopls/internal/lsp/protocol/tsprotocol.go +++ b/gopls/internal/lsp/protocol/tsprotocol.go @@ -1,15 +1,22 @@ -// Copyright 2019-2022 The Go Authors. All rights reserved. +// Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Code generated for LSP. DO NOT EDIT. + package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) -// Code generated; DO NOT EDIT. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14) import "encoding/json" +// created for And +type And_Param_workspace_configuration struct { // line 141 + ParamConfiguration + PartialResultParams +} + /* * A special text edit with an additional change annotation. * @@ -393,6 +400,8 @@ type CodeActionContext struct { // line 9052 */ TriggerKind CodeActionTriggerKind `json:"triggerKind,omitempty"` } + +// A set of predefined code action kinds type CodeActionKind string // line 13352 // Provider options for a [CodeActionRequest](#CodeActionRequest). type CodeActionOptions struct { // line 9091 @@ -430,6 +439,12 @@ type CodeActionRegistrationOptions struct { // line 5495 TextDocumentRegistrationOptions CodeActionOptions } + +/* + * The reason why code actions were requested. + * + * @since 3.17.0 + */ type CodeActionTriggerKind uint32 // line 13632 /* * Structure to capture a description for an error code. @@ -773,6 +788,8 @@ type CompletionItem struct { // line 4550 */ Data interface{} `json:"data,omitempty"` } + +// The kind of a completion entry. type CompletionItemKind uint32 // line 13160 /* * Additional details for a completion item label. @@ -791,6 +808,13 @@ type CompletionItemLabelDetails struct { // line 8671 */ Description string `json:"description,omitempty"` } + +/* + * Completion item tags are extra annotations that tweak the rendering of a completion + * item. + * + * @since 3.15.0 + */ type CompletionItemTag uint32 // line 13270 /* * Represents a collection of [completion items](#CompletionItem) to be presented @@ -880,6 +904,8 @@ type CompletionRegistrationOptions struct { // line 4875 TextDocumentRegistrationOptions CompletionOptions } + +// How a completion was triggered type CompletionTriggerKind uint32 // line 13581 type ConfigurationItem struct { // line 6401 // The scope to get the configuration section for. @@ -1156,8 +1182,15 @@ type DiagnosticRelatedInformation struct { // line 10067 type DiagnosticServerCancellationData struct { // line 3863 RetriggerRequest bool `json:"retriggerRequest"` } + +// The diagnostic's severity. type DiagnosticSeverity uint32 // line 13530 -type DiagnosticTag uint32 // line 13560 +/* + * The diagnostic tags. + * + * @since 3.15.0 + */ +type DiagnosticTag uint32 // line 13560 /* * Workspace client capabilities specific to diagnostic pull requests. * @@ -1379,18 +1412,13 @@ type DocumentDiagnosticParams struct { // line 3790 WorkDoneProgressParams PartialResultParams } - +type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) line 13909 /* - * The result of a document diagnostic pull request. A report can - * either be a full report containing all diagnostics for the - * requested document or an unchanged report indicating that nothing - * has changed in terms of diagnostics in comparison to the last - * pull request. + * The document diagnostic report kinds. * * @since 3.17.0 */ -type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) line 13909 -type DocumentDiagnosticReportKind string // line 12748 +type DocumentDiagnosticReportKind string // line 12748 /* * A partial result for a document diagnostic report. * @@ -1450,6 +1478,8 @@ type DocumentHighlightClientCapabilities struct { // line 11650 // Whether document highlight supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } + +// A document highlight kind. type DocumentHighlightKind uint32 // line 13327 // Provider options for a [DocumentHighlightRequest](#DocumentHighlightRequest). type DocumentHighlightOptions struct { // line 8975 @@ -1701,8 +1731,10 @@ type DocumentSymbolRegistrationOptions struct { // line 5312 TextDocumentRegistrationOptions DocumentSymbolOptions } -type DocumentURI string // line 0 -type ErrorCodes int32 // line 12769 +type DocumentURI string + +// Predefined error codes. +type ErrorCodes int32 // line 12769 // The client capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). type ExecuteCommandClientCapabilities struct { // line 10988 // Execute command supports dynamic registration. @@ -1743,12 +1775,12 @@ type ExecutionSummary struct { // line 10188 Success bool `json:"success,omitempty"` } -// created for Literal +// created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0_cells_Elem) type FCellsPNotebookSelector struct { // line 9857 Language string `json:"language"` } -// created for Literal +// created for Literal (Lit_CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind) type FCodeActionKindPCodeActionLiteralSupport struct { // line 11768 /* * The code action kind values the client supports. When this @@ -1759,14 +1791,14 @@ type FCodeActionKindPCodeActionLiteralSupport struct { // line 11768 ValueSet []CodeActionKind `json:"valueSet"` } -// created for Literal -type FEditRangePItemDefaults struct { // line 4797 +// created for Literal (Lit_CompletionList_itemDefaults_editRange_Item1) +type FEditRangePItemDefaults struct { // line 4798 Insert Range `json:"insert"` Replace Range `json:"replace"` } -// created for Literal -type FFullPRequests struct { // line 12230 +// created for Literal (Lit_SemanticTokensClientCapabilities_requests_full_Item1) +type FFullPRequests struct { // line 12231 /* * The client will send the `textDocument/semanticTokens/full/delta` request if * the server provides a corresponding handler. @@ -1774,12 +1806,12 @@ type FFullPRequests struct { // line 12230 Delta bool `json:"delta"` } -// created for Literal +// created for Literal (Lit_CompletionClientCapabilities_completionItem_insertTextModeSupport) type FInsertTextModeSupportPCompletionItem struct { // line 11321 ValueSet []InsertTextMode `json:"valueSet"` } -// created for Literal +// created for Literal (Lit_SignatureHelpClientCapabilities_signatureInformation_parameterInformation) type FParameterInformationPSignatureInformation struct { // line 11487 /* * The client supports processing label offsets instead of a @@ -1790,17 +1822,17 @@ type FParameterInformationPSignatureInformation struct { // line 11487 LabelOffsetSupport bool `json:"labelOffsetSupport"` } -// created for Literal -type FRangePRequests struct { // line 12210 +// created for Literal (Lit_SemanticTokensClientCapabilities_requests_range_Item1) +type FRangePRequests struct { // line 12211 } -// created for Literal +// created for Literal (Lit_CompletionClientCapabilities_completionItem_resolveSupport) type FResolveSupportPCompletionItem struct { // line 11297 // The properties that a client can resolve lazily. Properties []string `json:"properties"` } -// created for Literal +// created for Literal (Lit_NotebookDocumentChangeEvent_cells_structure) type FStructurePCells struct { // line 7492 // The change to the cell array. Array NotebookCellArrayChange `json:"array"` @@ -1810,19 +1842,20 @@ type FStructurePCells struct { // line 7492 DidClose []TextDocumentIdentifier `json:"didClose"` } -// created for Literal +// created for Literal (Lit_CompletionClientCapabilities_completionItem_tagSupport) type FTagSupportPCompletionItem struct { // line 11263 // The tags supported by the client. ValueSet []CompletionItemTag `json:"valueSet"` } -// created for Literal +// created for Literal (Lit_NotebookDocumentChangeEvent_cells_textContent_Elem) type FTextContentPCells struct { // line 7550 Document VersionedTextDocumentIdentifier `json:"document"` Changes []TextDocumentContentChangeEvent `json:"changes"` } type FailureHandlingKind string // line 13719 -type FileChangeType uint32 // line 13480 +// The file event type +type FileChangeType uint32 // line 13480 /* * Represents information on a file/folder create. * @@ -1935,6 +1968,13 @@ type FileOperationPattern struct { // line 9509 // Additional options used during matching. Options *FileOperationPatternOptions `json:"options,omitempty"` } + +/* + * A pattern kind describing if a glob pattern matches a file a folder or + * both. + * + * @since 3.16.0 + */ type FileOperationPatternKind string // line 13653 /* * Matching options for the file operation pattern. @@ -2049,6 +2089,8 @@ type FoldingRangeClientCapabilities struct { // line 12004 */ FoldingRange *PFoldingRangePFoldingRange `json:"foldingRange,omitempty"` } + +// A set of predefined range kinds. type FoldingRangeKind string // line 12841 type FoldingRangeOptions struct { // line 6486 WorkDoneProgressOptions @@ -2332,6 +2374,12 @@ type InlayHintClientCapabilities struct { // line 12395 */ ResolveSupport *PResolveSupportPInlayHint `json:"resolveSupport,omitempty"` } + +/* + * Inlay hint kinds. + * + * @since 3.17.0 + */ type InlayHintKind uint32 // line 13059 /* * An inlay hint label part allows for interactive and composite labels @@ -2575,17 +2623,21 @@ type InsertReplaceEdit struct { // line 8696 // The range if the replace is requested. Replace Range `json:"replace"` } + +/* + * Defines whether the insert text in a completion item should be interpreted as + * plain text or a snippet. + */ type InsertTextFormat uint32 // line 13286 -type InsertTextMode uint32 // line 13306 /* - * The LSP any type. - * Please note that strictly speaking a property with the value `undefined` - * can't be converted into JSON preserving the property name. However for - * convenience it is allowed and assumed that all these properties are - * optional as well. - * @since 3.17.0 + * How whitespace and indentation is handled during completion + * item insertion. + * + * @since 3.16.0 */ -type LSPAny = interface{} // (alias) line 13817 +type InsertTextMode uint32 // line 13306 +type LSPAny = interface{} + /* * LSP arrays. * @since 3.17.0 @@ -2644,6 +2696,64 @@ type LinkedEditingRanges struct { // line 3150 WordPattern string `json:"wordPattern,omitempty"` } +// created for Literal (Lit_NotebookDocumentFilter_Item1) +type Lit_NotebookDocumentFilter_Item1 struct { // line 14302 + // The type of the enclosing notebook. + NotebookType string `json:"notebookType,omitempty"` + // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + Scheme string `json:"scheme"` + // A glob pattern. + Pattern string `json:"pattern,omitempty"` +} + +// created for Literal (Lit_NotebookDocumentFilter_Item2) +type Lit_NotebookDocumentFilter_Item2 struct { // line 14335 + // The type of the enclosing notebook. + NotebookType string `json:"notebookType,omitempty"` + // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + Scheme string `json:"scheme,omitempty"` + // A glob pattern. + Pattern string `json:"pattern"` +} + +// created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1) +type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1 struct { // line 9878 + /* + * The notebook to be synced If a string + * value is provided it matches against the + * notebook type. '*' matches every notebook. + */ + Notebook *Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook `json:"notebook,omitempty"` + // The cells of the matching notebook to be synced. + Cells []Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_cells_Elem `json:"cells"` +} + +// created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_cells_Elem) +type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_cells_Elem struct { // line 9904 + Language string `json:"language"` +} + +// created for Literal (Lit_PrepareRenameResult_Item2) +type Lit_PrepareRenameResult_Item2 struct { // line 13958 + DefaultBehavior bool `json:"defaultBehavior"` +} + +// created for Literal (Lit_TextDocumentContentChangeEvent_Item1) +type Lit_TextDocumentContentChangeEvent_Item1 struct { // line 14066 + // The new text of the whole document. + Text string `json:"text"` +} + +// created for Literal (Lit_TextDocumentFilter_Item2) +type Lit_TextDocumentFilter_Item2 struct { // line 14226 + // A language id, like `typescript`. + Language string `json:"language,omitempty"` + // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + Scheme string `json:"scheme,omitempty"` + // A glob pattern, like `*.{ts,js}`. + Pattern string `json:"pattern"` +} + /* * Represents a location inside a resource, such as a line * inside a text file. @@ -2743,8 +2853,7 @@ type MarkedString = Or_MarkedString // (alias) line 14084 * '```typescript', * 'someCode();', * '```' - * ].join('\ - * ') + * ].join('\n') * }; * ``` * @@ -2757,11 +2866,21 @@ type MarkupContent struct { // line 7118 // The content itself Value string `json:"value"` } + +/* + * Describes the content type that a client supports in various + * result literals like `Hover`, `ParameterInfo` or `CompletionItem`. + * + * Please note that `MarkupKinds` must not start with a `$`. This kinds + * are reserved for internal usage. + */ type MarkupKind string // line 13433 type MessageActionItem struct { // line 4260 // A short title like 'Retry', 'Open Log' etc. Title string `json:"title"` } + +// The message type type MessageType uint32 // line 13080 /* * Moniker definition to match LSIF 0.5 moniker definition. @@ -2795,6 +2914,12 @@ type MonikerClientCapabilities struct { // line 12347 */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } + +/* + * The moniker kind. + * + * @since 3.16.0 + */ type MonikerKind string // line 13033 type MonikerOptions struct { // line 6931 WorkDoneProgressOptions @@ -2809,14 +2934,14 @@ type MonikerRegistrationOptions struct { // line 3400 MonikerOptions } -// created for Literal -type Msg_MarkedString struct { // line 14093 +// created for Literal (Lit_MarkedString_Item1) +type Msg_MarkedString struct { // line 14094 Language string `json:"language"` Value string `json:"value"` } -// created for Literal -type Msg_NotebookDocumentFilter struct { // line 14268 +// created for Literal (Lit_NotebookDocumentFilter_Item0) +type Msg_NotebookDocumentFilter struct { // line 14269 // The type of the enclosing notebook. NotebookType string `json:"notebookType"` // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. @@ -2825,14 +2950,14 @@ type Msg_NotebookDocumentFilter struct { // line 14268 Pattern string `json:"pattern"` } -// created for Literal -type Msg_PrepareRename2Gn struct { // line 13936 +// created for Literal (Lit_PrepareRenameResult_Item1) +type Msg_PrepareRename2Gn struct { // line 13937 Range Range `json:"range"` Placeholder string `json:"placeholder"` } -// created for Literal -type Msg_TextDocumentContentChangeEvent struct { // line 14033 +// created for Literal (Lit_TextDocumentContentChangeEvent_Item0) +type Msg_TextDocumentContentChangeEvent struct { // line 14034 // The range of the document that changed. Range *Range `json:"range"` /* @@ -2845,8 +2970,8 @@ type Msg_TextDocumentContentChangeEvent struct { // line 14033 Text string `json:"text"` } -// created for Literal -type Msg_TextDocumentFilter struct { // line 14159 +// created for Literal (Lit_TextDocumentFilter_Item1) +type Msg_TextDocumentFilter struct { // line 14193 // A language id, like `typescript`. Language string `json:"language"` // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. @@ -2855,7 +2980,7 @@ type Msg_TextDocumentFilter struct { // line 14159 Pattern string `json:"pattern"` } -// created for Literal +// created for Literal (Lit__InitializeParams_clientInfo) type Msg_XInitializeParams_clientInfo struct { // line 7678 // The name of the client as defined by the client. Name string `json:"name"` @@ -2907,6 +3032,12 @@ type NotebookCellArrayChange struct { // line 9665 // The new cells, if any Cells []NotebookCell `json:"cells,omitempty"` } + +/* + * A notebook cell kind. + * + * @since 3.17.0 + */ type NotebookCellKind uint32 // line 13674 /* * A notebook cell text document filter denotes a cell text @@ -3070,12 +3201,12 @@ type OptionalVersionedTextDocumentIdentifier struct { // line 9363 TextDocumentIdentifier } -// created for Or [Range FEditRangePItemDefaults] +// created for Or [FEditRangePItemDefaults Range] type OrFEditRangePItemDefaults struct { // line 4791 Value interface{} `json:"value"` } -// created for Or [string NotebookDocumentFilter] +// created for Or [NotebookDocumentFilter string] type OrFNotebookPNotebookSelector struct { // line 9838 Value interface{} `json:"value"` } @@ -3085,62 +3216,292 @@ type OrPLocation_workspace_symbol struct { // line 5540 Value interface{} `json:"value"` } -// created for Or [string []string] +// created for Or [[]string string] type OrPSection_workspace_didChangeConfiguration struct { // line 4186 Value interface{} `json:"value"` } -// created for Or [string MarkupContent] +// created for Or [MarkupContent string] type OrPTooltipPLabel struct { // line 7081 Value interface{} `json:"value"` } -// created for Or [string MarkupContent] +// created for Or [MarkupContent string] type OrPTooltip_textDocument_inlayHint struct { // line 3722 Value interface{} `json:"value"` } +// created for Or [int32 string] +type Or_CancelParams_id struct { // line 6204 + Value interface{} `json:"value"` +} + +// created for Or [MarkupContent string] +type Or_CompletionItem_documentation struct { // line 4605 + Value interface{} `json:"value"` +} + +// created for Or [InsertReplaceEdit TextEdit] +type Or_CompletionItem_textEdit struct { // line 4687 + Value interface{} `json:"value"` +} + // created for Or [Location []Location] type Or_Definition struct { // line 13780 Value interface{} `json:"value"` } +// created for Or [int32 string] +type Or_Diagnostic_code struct { // line 8568 + Value interface{} `json:"value"` +} + // created for Or [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport] type Or_DocumentDiagnosticReport struct { // line 13912 Value interface{} `json:"value"` } -// created for Or [TextDocumentFilter NotebookCellTextDocumentFilter] +// created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] +type Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value struct { // line 3845 + Value interface{} `json:"value"` +} + +// created for Or [NotebookCellTextDocumentFilter TextDocumentFilter] type Or_DocumentFilter struct { // line 14121 Value interface{} `json:"value"` } -// created for Or [InlineValueText InlineValueVariableLookup InlineValueEvaluatableExpression] +// created for Or [MarkedString MarkupContent []MarkedString] +type Or_Hover_contents struct { // line 4913 + Value interface{} `json:"value"` +} + +// created for Or [[]InlayHintLabelPart string] +type Or_InlayHint_label struct { // line 3681 + Value interface{} `json:"value"` +} + +// created for Or [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup] type Or_InlineValue struct { // line 13890 Value interface{} `json:"value"` } -// created for Or [string Msg_MarkedString] +// created for Or [Msg_MarkedString string] type Or_MarkedString struct { // line 14087 Value interface{} `json:"value"` } -// created for Or [WorkspaceFolder URI] +// created for Or [NotebookDocumentFilter string] +type Or_NotebookCellTextDocumentFilter_notebook struct { // line 10145 + Value interface{} `json:"value"` +} + +// created for Or [NotebookDocumentFilter string] +type Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook struct { // line 9884 + Value interface{} `json:"value"` +} + +// created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] +type Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value struct { // line 7174 + Value interface{} `json:"value"` +} + +// created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] +type Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value struct { // line 7213 + Value interface{} `json:"value"` +} + +// created for Or [URI WorkspaceFolder] type Or_RelativePattern_baseUri struct { // line 10768 Value interface{} `json:"value"` } +// created for Or [CodeAction Command] +type Or_Result_textDocument_codeAction_Item0_Elem struct { // line 1380 + Value interface{} `json:"value"` +} + +// created for Or [FFullPRequests bool] +type Or_SemanticTokensClientCapabilities_requests_full struct { // line 12224 + Value interface{} `json:"value"` +} + +// created for Or [FRangePRequests bool] +type Or_SemanticTokensClientCapabilities_requests_range struct { // line 12204 + Value interface{} `json:"value"` +} + +// created for Or [PFullESemanticTokensOptions bool] +type Or_SemanticTokensOptions_full struct { // line 6585 + Value interface{} `json:"value"` +} + +// created for Or [PRangeESemanticTokensOptions bool] +type Or_SemanticTokensOptions_range struct { // line 6565 + Value interface{} `json:"value"` +} + +// created for Or [CallHierarchyOptions CallHierarchyRegistrationOptions bool] +type Or_ServerCapabilities_callHierarchyProvider struct { // line 8248 + Value interface{} `json:"value"` +} + +// created for Or [CodeActionOptions bool] +type Or_ServerCapabilities_codeActionProvider struct { // line 8056 + Value interface{} `json:"value"` +} + +// created for Or [DocumentColorOptions DocumentColorRegistrationOptions bool] +type Or_ServerCapabilities_colorProvider struct { // line 8092 + Value interface{} `json:"value"` +} + +// created for Or [DeclarationOptions DeclarationRegistrationOptions bool] +type Or_ServerCapabilities_declarationProvider struct { // line 7918 + Value interface{} `json:"value"` +} + +// created for Or [DefinitionOptions bool] +type Or_ServerCapabilities_definitionProvider struct { // line 7940 + Value interface{} `json:"value"` +} + +// created for Or [DiagnosticOptions DiagnosticRegistrationOptions] +type Or_ServerCapabilities_diagnosticProvider struct { // line 8405 + Value interface{} `json:"value"` +} + +// created for Or [DocumentFormattingOptions bool] +type Or_ServerCapabilities_documentFormattingProvider struct { // line 8132 + Value interface{} `json:"value"` +} + +// created for Or [DocumentHighlightOptions bool] +type Or_ServerCapabilities_documentHighlightProvider struct { // line 8020 + Value interface{} `json:"value"` +} + +// created for Or [DocumentRangeFormattingOptions bool] +type Or_ServerCapabilities_documentRangeFormattingProvider struct { // line 8150 + Value interface{} `json:"value"` +} + +// created for Or [DocumentSymbolOptions bool] +type Or_ServerCapabilities_documentSymbolProvider struct { // line 8038 + Value interface{} `json:"value"` +} + +// created for Or [FoldingRangeOptions FoldingRangeRegistrationOptions bool] +type Or_ServerCapabilities_foldingRangeProvider struct { // line 8195 + Value interface{} `json:"value"` +} + +// created for Or [HoverOptions bool] +type Or_ServerCapabilities_hoverProvider struct { // line 7891 + Value interface{} `json:"value"` +} + +// created for Or [ImplementationOptions ImplementationRegistrationOptions bool] +type Or_ServerCapabilities_implementationProvider struct { // line 7980 + Value interface{} `json:"value"` +} + +// created for Or [InlayHintOptions InlayHintRegistrationOptions bool] +type Or_ServerCapabilities_inlayHintProvider struct { // line 8382 + Value interface{} `json:"value"` +} + +// created for Or [InlineValueOptions InlineValueRegistrationOptions bool] +type Or_ServerCapabilities_inlineValueProvider struct { // line 8359 + Value interface{} `json:"value"` +} + +// created for Or [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool] +type Or_ServerCapabilities_linkedEditingRangeProvider struct { // line 8271 + Value interface{} `json:"value"` +} + +// created for Or [MonikerOptions MonikerRegistrationOptions bool] +type Or_ServerCapabilities_monikerProvider struct { // line 8313 + Value interface{} `json:"value"` +} + +// created for Or [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions] +type Or_ServerCapabilities_notebookDocumentSync struct { // line 7863 + Value interface{} `json:"value"` +} + +// created for Or [ReferenceOptions bool] +type Or_ServerCapabilities_referencesProvider struct { // line 8002 + Value interface{} `json:"value"` +} + +// created for Or [RenameOptions bool] +type Or_ServerCapabilities_renameProvider struct { // line 8177 + Value interface{} `json:"value"` +} + +// created for Or [SelectionRangeOptions SelectionRangeRegistrationOptions bool] +type Or_ServerCapabilities_selectionRangeProvider struct { // line 8217 + Value interface{} `json:"value"` +} + +// created for Or [SemanticTokensOptions SemanticTokensRegistrationOptions] +type Or_ServerCapabilities_semanticTokensProvider struct { // line 8294 + Value interface{} `json:"value"` +} + +// created for Or [TextDocumentSyncKind TextDocumentSyncOptions] +type Or_ServerCapabilities_textDocumentSync struct { // line 7845 + Value interface{} `json:"value"` +} + +// created for Or [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool] +type Or_ServerCapabilities_typeDefinitionProvider struct { // line 7958 + Value interface{} `json:"value"` +} + +// created for Or [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool] +type Or_ServerCapabilities_typeHierarchyProvider struct { // line 8336 + Value interface{} `json:"value"` +} + +// created for Or [WorkspaceSymbolOptions bool] +type Or_ServerCapabilities_workspaceSymbolProvider struct { // line 8114 + Value interface{} `json:"value"` +} + +// created for Or [MarkupContent string] +type Or_SignatureInformation_documentation struct { // line 8862 + Value interface{} `json:"value"` +} + +// created for Or [AnnotatedTextEdit TextEdit] +type Or_TextDocumentEdit_edits_Elem struct { // line 6698 + Value interface{} `json:"value"` +} + +// created for Or [SaveOptions bool] +type Or_TextDocumentSyncOptions_save struct { // line 9804 + Value interface{} `json:"value"` +} + // created for Or [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport] type Or_WorkspaceDocumentDiagnosticReport struct { // line 14013 Value interface{} `json:"value"` } -// created for Or [Declaration []DeclarationLink ] +// created for Or [CreateFile DeleteFile RenameFile TextDocumentEdit] +type Or_WorkspaceEdit_documentChanges_Elem struct { // line 3242 + Value interface{} `json:"value"` +} + +// created for Or [Declaration []DeclarationLink] type Or_textDocument_declaration struct { // line 257 Value interface{} `json:"value"` } -// created for Literal +// created for Literal (Lit_NotebookDocumentChangeEvent_cells) type PCellsPChange struct { // line 7486 /* * Changes to the cell structure to add or @@ -3156,17 +3517,17 @@ type PCellsPChange struct { // line 7486 TextContent []FTextContentPCells `json:"textContent"` } -// created for Literal +// created for Literal (Lit_WorkspaceEditClientCapabilities_changeAnnotationSupport) type PChangeAnnotationSupportPWorkspaceEdit struct { // line 10842 /* * Whether the client groups edits with equal labels into tree nodes, - * for instance all edits labelled with \"Changes in Strings\" would + * for instance all edits labelled with "Changes in Strings" would * be a tree node. */ GroupsOnLabel bool `json:"groupsOnLabel"` } -// created for Literal +// created for Literal (Lit_CodeActionClientCapabilities_codeActionLiteralSupport) type PCodeActionLiteralSupportPCodeAction struct { // line 11762 /* * The code action kind is support with the following value @@ -3175,7 +3536,7 @@ type PCodeActionLiteralSupportPCodeAction struct { // line 11762 CodeActionKind FCodeActionKindPCodeActionLiteralSupport `json:"codeActionKind"` } -// created for Literal +// created for Literal (Lit_CompletionClientCapabilities_completionItemKind) type PCompletionItemKindPCompletion struct { // line 11360 /* * The completion item kind values the client supports. When this @@ -3190,7 +3551,7 @@ type PCompletionItemKindPCompletion struct { // line 11360 ValueSet []CompletionItemKind `json:"valueSet"` } -// created for Literal +// created for Literal (Lit_CompletionClientCapabilities_completionItem) type PCompletionItemPCompletion struct { // line 11209 /* * Client supports snippets as insert text. @@ -3253,7 +3614,7 @@ type PCompletionItemPCompletion struct { // line 11209 LabelDetailsSupport bool `json:"labelDetailsSupport"` } -// created for Literal +// created for Literal (Lit_CompletionOptions_completionItem) type PCompletionItemPCompletionProvider struct { // line 8767 /* * The server has support for completion item label @@ -3265,7 +3626,7 @@ type PCompletionItemPCompletionProvider struct { // line 8767 LabelDetailsSupport bool `json:"labelDetailsSupport"` } -// created for Literal +// created for Literal (Lit_CompletionClientCapabilities_completionList) type PCompletionListPCompletion struct { // line 11402 /* * The client supports the following itemDefaults on @@ -3280,7 +3641,7 @@ type PCompletionListPCompletion struct { // line 11402 ItemDefaults []string `json:"itemDefaults"` } -// created for Literal +// created for Literal (Lit_CodeAction_disabled) type PDisabledMsg_textDocument_codeAction struct { // line 5446 /* * Human readable description of why the code action is currently disabled. @@ -3290,7 +3651,7 @@ type PDisabledMsg_textDocument_codeAction struct { // line 5446 Reason string `json:"reason"` } -// created for Literal +// created for Literal (Lit_FoldingRangeClientCapabilities_foldingRangeKind) type PFoldingRangeKindPFoldingRange struct { // line 12037 /* * The folding range kind values the client supports. When this @@ -3301,7 +3662,7 @@ type PFoldingRangeKindPFoldingRange struct { // line 12037 ValueSet []FoldingRangeKind `json:"valueSet"` } -// created for Literal +// created for Literal (Lit_FoldingRangeClientCapabilities_foldingRange) type PFoldingRangePFoldingRange struct { // line 12062 /* * If set, the client signals that it supports setting collapsedText on @@ -3312,13 +3673,13 @@ type PFoldingRangePFoldingRange struct { // line 12062 CollapsedText bool `json:"collapsedText"` } -// created for Literal -type PFullESemanticTokensOptions struct { // line 6591 +// created for Literal (Lit_SemanticTokensOptions_full_Item1) +type PFullESemanticTokensOptions struct { // line 6592 // The server supports deltas for full documents. Delta bool `json:"delta"` } -// created for Literal +// created for Literal (Lit_CompletionList_itemDefaults) type PItemDefaultsMsg_textDocument_completion struct { // line 4772 /* * A default commit character set. @@ -3352,12 +3713,12 @@ type PItemDefaultsMsg_textDocument_completion struct { // line 4772 Data interface{} `json:"data"` } -// created for Literal -type PLocationMsg_workspace_symbol struct { // line 5546 +// created for Literal (Lit_WorkspaceSymbol_location_Item1) +type PLocationMsg_workspace_symbol struct { // line 5547 URI DocumentURI `json:"uri"` } -// created for Literal +// created for Literal (Lit_ShowMessageRequestClientCapabilities_messageActionItem) type PMessageActionItemPShowMessage struct { // line 12490 /* * Whether the client supports additional attributes which @@ -3367,8 +3728,8 @@ type PMessageActionItemPShowMessage struct { // line 12490 AdditionalPropertiesSupport bool `json:"additionalPropertiesSupport"` } -// created for Literal -type PNotebookSelectorPNotebookDocumentSync struct { // line 9831 +// created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0) +type PNotebookSelectorPNotebookDocumentSync struct { // line 9832 /* * The notebook to be synced If a string * value is provided it matches against the @@ -3379,11 +3740,11 @@ type PNotebookSelectorPNotebookDocumentSync struct { // line 9831 Cells []FCellsPNotebookSelector `json:"cells"` } -// created for Literal -type PRangeESemanticTokensOptions struct { // line 6571 +// created for Literal (Lit_SemanticTokensOptions_range_Item1) +type PRangeESemanticTokensOptions struct { // line 6572 } -// created for Literal +// created for Literal (Lit_SemanticTokensClientCapabilities_requests) type PRequestsPSemanticTokens struct { // line 12198 /* * The client will send the `textDocument/semanticTokens/range` request if @@ -3397,19 +3758,19 @@ type PRequestsPSemanticTokens struct { // line 12198 Full interface{} `json:"full"` } -// created for Literal +// created for Literal (Lit_CodeActionClientCapabilities_resolveSupport) type PResolveSupportPCodeAction struct { // line 11827 // The properties that a client can resolve lazily. Properties []string `json:"properties"` } -// created for Literal +// created for Literal (Lit_InlayHintClientCapabilities_resolveSupport) type PResolveSupportPInlayHint struct { // line 12410 // The properties that a client can resolve lazily. Properties []string `json:"properties"` } -// created for Literal +// created for Literal (Lit_WorkspaceSymbolClientCapabilities_resolveSupport) type PResolveSupportPSymbol struct { // line 10964 /* * The properties that a client can resolve lazily. Usually @@ -3418,7 +3779,7 @@ type PResolveSupportPSymbol struct { // line 10964 Properties []string `json:"properties"` } -// created for Literal +// created for Literal (Lit_InitializeResult_serverInfo) type PServerInfoMsg_initialize struct { // line 4118 // The name of the server as defined by the server. Name string `json:"name"` @@ -3426,7 +3787,7 @@ type PServerInfoMsg_initialize struct { // line 4118 Version string `json:"version"` } -// created for Literal +// created for Literal (Lit_SignatureHelpClientCapabilities_signatureInformation) type PSignatureInformationPSignatureHelp struct { // line 11469 /* * Client supports the following content formats for the documentation @@ -3444,7 +3805,7 @@ type PSignatureInformationPSignatureHelp struct { // line 11469 ActiveParameterSupport bool `json:"activeParameterSupport"` } -// created for Literal +// created for Literal (Lit_GeneralClientCapabilities_staleRequestSupport) type PStaleRequestSupportPGeneral struct { // line 10696 // The client will actively cancel the request. Cancel bool `json:"cancel"` @@ -3456,7 +3817,7 @@ type PStaleRequestSupportPGeneral struct { // line 10696 RetryOnContentModified []string `json:"retryOnContentModified"` } -// created for Literal +// created for Literal (Lit_DocumentSymbolClientCapabilities_symbolKind) type PSymbolKindPDocumentSymbol struct { // line 11680 /* * The symbol kind values the client supports. When this @@ -3471,7 +3832,7 @@ type PSymbolKindPDocumentSymbol struct { // line 11680 ValueSet []SymbolKind `json:"valueSet"` } -// created for Literal +// created for Literal (Lit_WorkspaceSymbolClientCapabilities_symbolKind) type PSymbolKindPSymbol struct { // line 10916 /* * The symbol kind values the client supports. When this @@ -3486,19 +3847,19 @@ type PSymbolKindPSymbol struct { // line 10916 ValueSet []SymbolKind `json:"valueSet"` } -// created for Literal +// created for Literal (Lit_DocumentSymbolClientCapabilities_tagSupport) type PTagSupportPDocumentSymbol struct { // line 11713 // The tags supported by the client. ValueSet []SymbolTag `json:"valueSet"` } -// created for Literal +// created for Literal (Lit_PublishDiagnosticsClientCapabilities_tagSupport) type PTagSupportPPublishDiagnostics struct { // line 12113 // The tags supported by the client. ValueSet []DiagnosticTag `json:"valueSet"` } -// created for Literal +// created for Literal (Lit_WorkspaceSymbolClientCapabilities_tagSupport) type PTagSupportPSymbol struct { // line 10940 // The tags supported by the client. ValueSet []SymbolTag `json:"valueSet"` @@ -3580,9 +3941,7 @@ type Pattern = string // (alias) line 14372 * usually on the server side. * * Positions are line end character agnostic. So you can not specify a position - * that denotes `\\r|\ - * ` or `\ - * |` where `|` represents the character offset. + * that denotes `\r|\n` or `\n|` where `|` represents the character offset. * * @since 3.17.0 - support for negotiated position encoding. */ @@ -3605,6 +3964,12 @@ type Position struct { // line 6506 */ Character uint32 `json:"character"` } + +/* + * A set of predefined position encoding kinds. + * + * @since 3.17.0 + */ type PositionEncodingKind string // line 13453 type PrepareRename2Gn = Msg_PrepareRename2Gn // (alias) line 13927 type PrepareRenameParams struct { // line 5944 @@ -3612,7 +3977,7 @@ type PrepareRenameParams struct { // line 5944 WorkDoneProgressParams } type PrepareRenameResult = Msg_PrepareRename2Gn // (alias) line 13927 -type PrepareSupportDefaultBehavior interface{} // line 13748 +type PrepareSupportDefaultBehavior uint32 // line 13748 /* * A previous result id in a workspace pull request. * @@ -3985,8 +4350,23 @@ type SelectionRangeRegistrationOptions struct { // line 2614 TextDocumentRegistrationOptions StaticRegistrationOptions } + +/* + * A set of predefined token modifiers. This set is not fixed + * an clients can specify additional token types via the + * corresponding client capabilities. + * + * @since 3.16.0 + */ type SemanticTokenModifiers string // line 12696 -type SemanticTokenTypes string // line 12589 +/* + * A set of predefined token types. This set is not fixed + * an clients can specify additional token types via the + * corresponding client capabilities. + * + * @since 3.16.0 + */ +type SemanticTokenTypes string // line 12589 // @since 3.16.0 type SemanticTokens struct { // line 2902 /* @@ -4483,6 +4863,12 @@ type SignatureHelpRegistrationOptions struct { // line 5024 TextDocumentRegistrationOptions SignatureHelpOptions } + +/* + * How a signature help was triggered. + * + * @since 3.15.0 + */ type SignatureHelpTriggerKind uint32 // line 13606 /* * Represents the signature of something callable. A signature @@ -4529,6 +4915,7 @@ type StaticRegistrationOptions struct { // line 6348 * interfaces etc. */ type SymbolInformation struct { // line 5202 + // extends BaseSymbolInformation /* * Indicates if this symbol is deprecated. * @@ -4565,8 +4952,15 @@ type SymbolInformation struct { // line 5202 */ ContainerName string `json:"containerName,omitempty"` } + +// A symbol kind. type SymbolKind uint32 // line 12867 -type SymbolTag uint32 // line 12981 +/* + * Symbol tags are extra annotations that tweak the rendering of a symbol. + * + * @since 3.16 + */ +type SymbolTag uint32 // line 12981 // Describe options to be used when registered for text document change events. type TextDocumentChangeRegistrationOptions struct { // line 4334 // How documents are synced to the server. @@ -4780,6 +5174,8 @@ type TextDocumentRegistrationOptions struct { // line 2390 */ DocumentSelector DocumentSelector `json:"documentSelector"` } + +// Represents reasons why a text document is saved. type TextDocumentSaveReason uint32 // line 13135 // Save registration options. type TextDocumentSaveRegistrationOptions struct { // line 4391 @@ -4800,6 +5196,11 @@ type TextDocumentSyncClientCapabilities struct { // line 11153 // The client supports did save notifications. DidSave bool `json:"didSave,omitempty"` } + +/* + * Defines how the host (editor) should sync + * document changes to the language server. + */ type TextDocumentSyncKind uint32 // line 13110 type TextDocumentSyncOptions struct { // line 9762 /* @@ -4968,11 +5369,12 @@ type TypeHierarchySupertypesParams struct { // line 3531 } // created for Tuple -type UIntCommaUInt struct { // line 10101 +type UIntCommaUInt struct { // line 10102 Fld0 uint32 `json:"fld0"` Fld1 uint32 `json:"fld1"` } -type URI = string // (alias) line 0 +type URI = string + /* * A diagnostic report indicating that the last returned * report is still accurate. @@ -4993,6 +5395,12 @@ type UnchangedDocumentDiagnosticReport struct { // line 7275 */ ResultID string `json:"resultId"` } + +/* + * Moniker uniqueness level to define scope of the moniker. + * + * @since 3.16.0 + */ type UniquenessLevel string // line 12997 // General parameters to unregister a request or notification. type Unregistration struct { // line 7633 @@ -5026,8 +5434,7 @@ type VersionedTextDocumentIdentifier struct { // line 8465 Version int32 `json:"version"` TextDocumentIdentifier } -type WatchKind = uint32 // line 13505 -// The parameters sent in a will save text document notification. +type WatchKind = uint32 // line 13505// The parameters sent in a will save text document notification. type WillSaveTextDocumentParams struct { // line 4406 // The document that will be saved. TextDocument TextDocumentIdentifier `json:"textDocument"` @@ -5066,7 +5473,7 @@ type WorkDoneProgressBegin struct { // line 6059 * Mandatory title of the progress operation. Used to briefly inform about * the kind of operation being performed. * - * Examples: \"Indexing\" or \"Linking dependencies\". + * Examples: "Indexing" or "Linking dependencies". */ Title string `json:"title"` /* @@ -5079,7 +5486,7 @@ type WorkDoneProgressBegin struct { // line 6059 * Optional, more detailed associated progress message. Contains * complementary information to the `title`. * - * Examples: \"3/25 files\", \"project/src/module2\", \"node_modules/some_dep\". + * Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". * If unset, the previous progress message (if any) is still valid. */ Message string `json:"message,omitempty"` @@ -5135,7 +5542,7 @@ type WorkDoneProgressReport struct { // line 6106 * Optional, more detailed associated progress message. Contains * complementary information to the `title`. * - * Examples: \"3/25 files\", \"project/src/module2\", \"node_modules/some_dep\". + * Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". * If unset, the previous progress message (if any) is still valid. */ Message string `json:"message,omitempty"` @@ -5150,7 +5557,7 @@ type WorkDoneProgressReport struct { // line 6106 Percentage uint32 `json:"percentage,omitempty"` } -// created for Literal +// created for Literal (Lit_ServerCapabilities_workspace) type Workspace6Gn struct { // line 8424 /* * The server supports workspace folder. @@ -5570,6 +5977,7 @@ type XInitializeParams struct { // line 7655 InitializationOptions interface{} `json:"initializationOptions,omitempty"` // The initial trace setting. If omitted trace is disabled ('off'). Trace string `json:"trace,omitempty"` + WorkDoneProgressParams } // The initialize parameters @@ -5587,7 +5995,7 @@ type _InitializeParams struct { // line 7655 * * @since 3.15.0 */ - ClientInfo *Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` + ClientInfo Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` /* * The locale the client is currently showing the user interface * in. This must not necessarily be the locale of the operating @@ -5620,6 +6028,7 @@ type _InitializeParams struct { // line 7655 InitializationOptions interface{} `json:"initializationOptions,omitempty"` // The initial trace setting. If omitted trace is disabled ('off'). Trace string `json:"trace,omitempty"` + WorkDoneProgressParams } const ( @@ -5998,6 +6407,11 @@ const ( * encoding-agnostic representation of character offsets. */ UTF32 PositionEncodingKind = "utf-32" // line 13470 + /* + * The client's default behavior is to select the identifier + * according the to language's syntax rule. + */ + Identifier PrepareSupportDefaultBehavior = 1 // line 13755 // Supports creating new files and folders. Create ResourceOperationKind = "create" // line 13702 // Supports renaming existing files and folders. diff --git a/gopls/internal/lsp/protocol/tsserver.go b/gopls/internal/lsp/protocol/tsserver.go index a93ee80082b..6446a014564 100644 --- a/gopls/internal/lsp/protocol/tsserver.go +++ b/gopls/internal/lsp/protocol/tsserver.go @@ -1,12 +1,13 @@ -// Copyright 2019-2022 The Go Authors. All rights reserved. +// Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Code generated for LSP. DO NOT EDIT. + package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) -// Code generated; DO NOT EDIT. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14) import ( "context" @@ -102,14 +103,14 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, return true, sendParseError(ctx, reply, err) } err := server.Progress(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "$/setTrace": var params SetTraceParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.SetTrace(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "callHierarchy/incomingCalls": var params CallHierarchyIncomingCallsParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -119,7 +120,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "callHierarchy/outgoingCalls": var params CallHierarchyOutgoingCallsParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -129,7 +130,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "codeAction/resolve": var params CodeAction if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -139,7 +140,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "codeLens/resolve": var params CodeLens if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -149,7 +150,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "completionItem/resolve": var params CompletionItem if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -159,7 +160,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "documentLink/resolve": var params DocumentLink if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -169,10 +170,10 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "exit": err := server.Exit(ctx) - return true, reply(ctx, nil, err) // 236 + return true, reply(ctx, nil, err) case "initialize": var params ParamInitialize if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -182,14 +183,14 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "initialized": var params InitializedParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.Initialized(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "inlayHint/resolve": var params InlayHint if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -199,38 +200,38 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "notebookDocument/didChange": var params DidChangeNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidChangeNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "notebookDocument/didClose": var params DidCloseNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidCloseNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "notebookDocument/didOpen": var params DidOpenNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidOpenNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "notebookDocument/didSave": var params DidSaveNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidSaveNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "shutdown": err := server.Shutdown(ctx) - return true, reply(ctx, nil, err) // 176 + return true, reply(ctx, nil, err) case "textDocument/codeAction": var params CodeActionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -240,7 +241,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/codeLens": var params CodeLensParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -250,7 +251,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/colorPresentation": var params ColorPresentationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -260,7 +261,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/completion": var params CompletionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -270,7 +271,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/declaration": var params DeclarationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -280,7 +281,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/definition": var params DefinitionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -290,7 +291,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/diagnostic": var params string if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -300,35 +301,35 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/didChange": var params DidChangeTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidChange(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "textDocument/didClose": var params DidCloseTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidClose(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "textDocument/didOpen": var params DidOpenTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidOpen(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "textDocument/didSave": var params DidSaveTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidSave(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "textDocument/documentColor": var params DocumentColorParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -338,7 +339,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/documentHighlight": var params DocumentHighlightParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -348,7 +349,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/documentLink": var params DocumentLinkParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -358,7 +359,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/documentSymbol": var params DocumentSymbolParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -368,7 +369,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/foldingRange": var params FoldingRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -378,7 +379,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/formatting": var params DocumentFormattingParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -388,7 +389,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/hover": var params HoverParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -398,7 +399,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/implementation": var params ImplementationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -408,7 +409,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/inlayHint": var params InlayHintParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -418,7 +419,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/inlineValue": var params InlineValueParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -428,7 +429,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/linkedEditingRange": var params LinkedEditingRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -438,7 +439,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/moniker": var params MonikerParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -448,7 +449,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/onTypeFormatting": var params DocumentOnTypeFormattingParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -458,7 +459,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/prepareCallHierarchy": var params CallHierarchyPrepareParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -468,7 +469,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/prepareRename": var params PrepareRenameParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -478,7 +479,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/prepareTypeHierarchy": var params TypeHierarchyPrepareParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -488,7 +489,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/rangeFormatting": var params DocumentRangeFormattingParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -498,7 +499,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/references": var params ReferenceParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -508,7 +509,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/rename": var params RenameParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -518,7 +519,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/selectionRange": var params SelectionRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -528,7 +529,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/semanticTokens/full": var params SemanticTokensParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -538,7 +539,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/semanticTokens/full/delta": var params SemanticTokensDeltaParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -548,7 +549,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/semanticTokens/range": var params SemanticTokensRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -558,7 +559,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/signatureHelp": var params SignatureHelpParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -568,7 +569,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/typeDefinition": var params TypeDefinitionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -578,14 +579,14 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "textDocument/willSave": var params WillSaveTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.WillSave(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "textDocument/willSaveWaitUntil": var params WillSaveTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -595,7 +596,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "typeHierarchy/subtypes": var params TypeHierarchySubtypesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -605,7 +606,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "typeHierarchy/supertypes": var params TypeHierarchySupertypesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -615,14 +616,14 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "window/workDoneProgress/cancel": var params WorkDoneProgressCancelParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.WorkDoneProgressCancel(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "workspace/diagnostic": var params WorkspaceDiagnosticParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -632,52 +633,52 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "workspace/diagnostic/refresh": err := server.DiagnosticRefresh(ctx) - return true, reply(ctx, nil, err) // 170 + return true, reply(ctx, nil, err) case "workspace/didChangeConfiguration": var params DidChangeConfigurationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidChangeConfiguration(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "workspace/didChangeWatchedFiles": var params DidChangeWatchedFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidChangeWatchedFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "workspace/didChangeWorkspaceFolders": var params DidChangeWorkspaceFoldersParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidChangeWorkspaceFolders(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "workspace/didCreateFiles": var params CreateFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidCreateFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "workspace/didDeleteFiles": var params DeleteFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidDeleteFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "workspace/didRenameFiles": var params RenameFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidRenameFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) // 231 + return true, reply(ctx, nil, err) case "workspace/executeCommand": var params ExecuteCommandParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -687,16 +688,16 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "workspace/inlayHint/refresh": err := server.InlayHintRefresh(ctx) - return true, reply(ctx, nil, err) // 170 + return true, reply(ctx, nil, err) case "workspace/inlineValue/refresh": err := server.InlineValueRefresh(ctx) - return true, reply(ctx, nil, err) // 170 + return true, reply(ctx, nil, err) case "workspace/semanticTokens/refresh": err := server.SemanticTokensRefresh(ctx) - return true, reply(ctx, nil, err) // 170 + return true, reply(ctx, nil, err) case "workspace/symbol": var params WorkspaceSymbolParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -706,7 +707,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "workspace/willCreateFiles": var params CreateFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -716,7 +717,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "workspace/willDeleteFiles": var params DeleteFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -726,7 +727,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "workspace/willRenameFiles": var params RenameFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -736,7 +737,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) case "workspaceSymbol/resolve": var params WorkspaceSymbol if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -746,7 +747,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) // 146 + return true, reply(ctx, resp, nil) default: return false, nil } @@ -754,429 +755,429 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, func (s *serverDispatcher) Progress(ctx context.Context, params *ProgressParams) error { return s.sender.Notify(ctx, "$/progress", params) -} // 244 +} func (s *serverDispatcher) SetTrace(ctx context.Context, params *SetTraceParams) error { return s.sender.Notify(ctx, "$/setTrace", params) -} // 244 +} func (s *serverDispatcher) IncomingCalls(ctx context.Context, params *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) { var result []CallHierarchyIncomingCall if err := s.sender.Call(ctx, "callHierarchy/incomingCalls", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) OutgoingCalls(ctx context.Context, params *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) { var result []CallHierarchyOutgoingCall if err := s.sender.Call(ctx, "callHierarchy/outgoingCalls", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) ResolveCodeAction(ctx context.Context, params *CodeAction) (*CodeAction, error) { var result *CodeAction if err := s.sender.Call(ctx, "codeAction/resolve", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) ResolveCodeLens(ctx context.Context, params *CodeLens) (*CodeLens, error) { var result *CodeLens if err := s.sender.Call(ctx, "codeLens/resolve", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) ResolveCompletionItem(ctx context.Context, params *CompletionItem) (*CompletionItem, error) { var result *CompletionItem if err := s.sender.Call(ctx, "completionItem/resolve", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) ResolveDocumentLink(ctx context.Context, params *DocumentLink) (*DocumentLink, error) { var result *DocumentLink if err := s.sender.Call(ctx, "documentLink/resolve", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Exit(ctx context.Context) error { return s.sender.Notify(ctx, "exit", nil) -} // 249 +} func (s *serverDispatcher) Initialize(ctx context.Context, params *ParamInitialize) (*InitializeResult, error) { var result *InitializeResult if err := s.sender.Call(ctx, "initialize", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Initialized(ctx context.Context, params *InitializedParams) error { return s.sender.Notify(ctx, "initialized", params) -} // 244 +} func (s *serverDispatcher) Resolve(ctx context.Context, params *InlayHint) (*InlayHint, error) { var result *InlayHint if err := s.sender.Call(ctx, "inlayHint/resolve", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) DidChangeNotebookDocument(ctx context.Context, params *DidChangeNotebookDocumentParams) error { return s.sender.Notify(ctx, "notebookDocument/didChange", params) -} // 244 +} func (s *serverDispatcher) DidCloseNotebookDocument(ctx context.Context, params *DidCloseNotebookDocumentParams) error { return s.sender.Notify(ctx, "notebookDocument/didClose", params) -} // 244 +} func (s *serverDispatcher) DidOpenNotebookDocument(ctx context.Context, params *DidOpenNotebookDocumentParams) error { return s.sender.Notify(ctx, "notebookDocument/didOpen", params) -} // 244 +} func (s *serverDispatcher) DidSaveNotebookDocument(ctx context.Context, params *DidSaveNotebookDocumentParams) error { return s.sender.Notify(ctx, "notebookDocument/didSave", params) -} // 244 +} func (s *serverDispatcher) Shutdown(ctx context.Context) error { return s.sender.Call(ctx, "shutdown", nil, nil) -} // 209 +} func (s *serverDispatcher) CodeAction(ctx context.Context, params *CodeActionParams) ([]CodeAction, error) { var result []CodeAction if err := s.sender.Call(ctx, "textDocument/codeAction", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) CodeLens(ctx context.Context, params *CodeLensParams) ([]CodeLens, error) { var result []CodeLens if err := s.sender.Call(ctx, "textDocument/codeLens", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) ColorPresentation(ctx context.Context, params *ColorPresentationParams) ([]ColorPresentation, error) { var result []ColorPresentation if err := s.sender.Call(ctx, "textDocument/colorPresentation", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Completion(ctx context.Context, params *CompletionParams) (*CompletionList, error) { var result *CompletionList if err := s.sender.Call(ctx, "textDocument/completion", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Declaration(ctx context.Context, params *DeclarationParams) (*Or_textDocument_declaration, error) { var result *Or_textDocument_declaration if err := s.sender.Call(ctx, "textDocument/declaration", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Definition(ctx context.Context, params *DefinitionParams) ([]Location, error) { var result []Location if err := s.sender.Call(ctx, "textDocument/definition", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Diagnostic(ctx context.Context, params *string) (*string, error) { var result *string if err := s.sender.Call(ctx, "textDocument/diagnostic", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) DidChange(ctx context.Context, params *DidChangeTextDocumentParams) error { return s.sender.Notify(ctx, "textDocument/didChange", params) -} // 244 +} func (s *serverDispatcher) DidClose(ctx context.Context, params *DidCloseTextDocumentParams) error { return s.sender.Notify(ctx, "textDocument/didClose", params) -} // 244 +} func (s *serverDispatcher) DidOpen(ctx context.Context, params *DidOpenTextDocumentParams) error { return s.sender.Notify(ctx, "textDocument/didOpen", params) -} // 244 +} func (s *serverDispatcher) DidSave(ctx context.Context, params *DidSaveTextDocumentParams) error { return s.sender.Notify(ctx, "textDocument/didSave", params) -} // 244 +} func (s *serverDispatcher) DocumentColor(ctx context.Context, params *DocumentColorParams) ([]ColorInformation, error) { var result []ColorInformation if err := s.sender.Call(ctx, "textDocument/documentColor", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) DocumentHighlight(ctx context.Context, params *DocumentHighlightParams) ([]DocumentHighlight, error) { var result []DocumentHighlight if err := s.sender.Call(ctx, "textDocument/documentHighlight", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) DocumentLink(ctx context.Context, params *DocumentLinkParams) ([]DocumentLink, error) { var result []DocumentLink if err := s.sender.Call(ctx, "textDocument/documentLink", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) DocumentSymbol(ctx context.Context, params *DocumentSymbolParams) ([]interface{}, error) { var result []interface{} if err := s.sender.Call(ctx, "textDocument/documentSymbol", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) FoldingRange(ctx context.Context, params *FoldingRangeParams) ([]FoldingRange, error) { var result []FoldingRange if err := s.sender.Call(ctx, "textDocument/foldingRange", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Formatting(ctx context.Context, params *DocumentFormattingParams) ([]TextEdit, error) { var result []TextEdit if err := s.sender.Call(ctx, "textDocument/formatting", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Hover(ctx context.Context, params *HoverParams) (*Hover, error) { var result *Hover if err := s.sender.Call(ctx, "textDocument/hover", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Implementation(ctx context.Context, params *ImplementationParams) ([]Location, error) { var result []Location if err := s.sender.Call(ctx, "textDocument/implementation", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) InlayHint(ctx context.Context, params *InlayHintParams) ([]InlayHint, error) { var result []InlayHint if err := s.sender.Call(ctx, "textDocument/inlayHint", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) InlineValue(ctx context.Context, params *InlineValueParams) ([]InlineValue, error) { var result []InlineValue if err := s.sender.Call(ctx, "textDocument/inlineValue", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) LinkedEditingRange(ctx context.Context, params *LinkedEditingRangeParams) (*LinkedEditingRanges, error) { var result *LinkedEditingRanges if err := s.sender.Call(ctx, "textDocument/linkedEditingRange", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Moniker(ctx context.Context, params *MonikerParams) ([]Moniker, error) { var result []Moniker if err := s.sender.Call(ctx, "textDocument/moniker", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) OnTypeFormatting(ctx context.Context, params *DocumentOnTypeFormattingParams) ([]TextEdit, error) { var result []TextEdit if err := s.sender.Call(ctx, "textDocument/onTypeFormatting", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) PrepareCallHierarchy(ctx context.Context, params *CallHierarchyPrepareParams) ([]CallHierarchyItem, error) { var result []CallHierarchyItem if err := s.sender.Call(ctx, "textDocument/prepareCallHierarchy", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) PrepareRename(ctx context.Context, params *PrepareRenameParams) (*PrepareRename2Gn, error) { var result *PrepareRename2Gn if err := s.sender.Call(ctx, "textDocument/prepareRename", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) PrepareTypeHierarchy(ctx context.Context, params *TypeHierarchyPrepareParams) ([]TypeHierarchyItem, error) { var result []TypeHierarchyItem if err := s.sender.Call(ctx, "textDocument/prepareTypeHierarchy", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) RangeFormatting(ctx context.Context, params *DocumentRangeFormattingParams) ([]TextEdit, error) { var result []TextEdit if err := s.sender.Call(ctx, "textDocument/rangeFormatting", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) References(ctx context.Context, params *ReferenceParams) ([]Location, error) { var result []Location if err := s.sender.Call(ctx, "textDocument/references", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Rename(ctx context.Context, params *RenameParams) (*WorkspaceEdit, error) { var result *WorkspaceEdit if err := s.sender.Call(ctx, "textDocument/rename", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) SelectionRange(ctx context.Context, params *SelectionRangeParams) ([]SelectionRange, error) { var result []SelectionRange if err := s.sender.Call(ctx, "textDocument/selectionRange", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) SemanticTokensFull(ctx context.Context, params *SemanticTokensParams) (*SemanticTokens, error) { var result *SemanticTokens if err := s.sender.Call(ctx, "textDocument/semanticTokens/full", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) SemanticTokensFullDelta(ctx context.Context, params *SemanticTokensDeltaParams) (interface{}, error) { var result interface{} if err := s.sender.Call(ctx, "textDocument/semanticTokens/full/delta", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) SemanticTokensRange(ctx context.Context, params *SemanticTokensRangeParams) (*SemanticTokens, error) { var result *SemanticTokens if err := s.sender.Call(ctx, "textDocument/semanticTokens/range", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) SignatureHelp(ctx context.Context, params *SignatureHelpParams) (*SignatureHelp, error) { var result *SignatureHelp if err := s.sender.Call(ctx, "textDocument/signatureHelp", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) TypeDefinition(ctx context.Context, params *TypeDefinitionParams) ([]Location, error) { var result []Location if err := s.sender.Call(ctx, "textDocument/typeDefinition", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) WillSave(ctx context.Context, params *WillSaveTextDocumentParams) error { return s.sender.Notify(ctx, "textDocument/willSave", params) -} // 244 +} func (s *serverDispatcher) WillSaveWaitUntil(ctx context.Context, params *WillSaveTextDocumentParams) ([]TextEdit, error) { var result []TextEdit if err := s.sender.Call(ctx, "textDocument/willSaveWaitUntil", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Subtypes(ctx context.Context, params *TypeHierarchySubtypesParams) ([]TypeHierarchyItem, error) { var result []TypeHierarchyItem if err := s.sender.Call(ctx, "typeHierarchy/subtypes", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) Supertypes(ctx context.Context, params *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) { var result []TypeHierarchyItem if err := s.sender.Call(ctx, "typeHierarchy/supertypes", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) WorkDoneProgressCancel(ctx context.Context, params *WorkDoneProgressCancelParams) error { return s.sender.Notify(ctx, "window/workDoneProgress/cancel", params) -} // 244 +} func (s *serverDispatcher) DiagnosticWorkspace(ctx context.Context, params *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) { var result *WorkspaceDiagnosticReport if err := s.sender.Call(ctx, "workspace/diagnostic", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) DiagnosticRefresh(ctx context.Context) error { return s.sender.Call(ctx, "workspace/diagnostic/refresh", nil, nil) -} // 209 +} func (s *serverDispatcher) DidChangeConfiguration(ctx context.Context, params *DidChangeConfigurationParams) error { return s.sender.Notify(ctx, "workspace/didChangeConfiguration", params) -} // 244 +} func (s *serverDispatcher) DidChangeWatchedFiles(ctx context.Context, params *DidChangeWatchedFilesParams) error { return s.sender.Notify(ctx, "workspace/didChangeWatchedFiles", params) -} // 244 +} func (s *serverDispatcher) DidChangeWorkspaceFolders(ctx context.Context, params *DidChangeWorkspaceFoldersParams) error { return s.sender.Notify(ctx, "workspace/didChangeWorkspaceFolders", params) -} // 244 +} func (s *serverDispatcher) DidCreateFiles(ctx context.Context, params *CreateFilesParams) error { return s.sender.Notify(ctx, "workspace/didCreateFiles", params) -} // 244 +} func (s *serverDispatcher) DidDeleteFiles(ctx context.Context, params *DeleteFilesParams) error { return s.sender.Notify(ctx, "workspace/didDeleteFiles", params) -} // 244 +} func (s *serverDispatcher) DidRenameFiles(ctx context.Context, params *RenameFilesParams) error { return s.sender.Notify(ctx, "workspace/didRenameFiles", params) -} // 244 +} func (s *serverDispatcher) ExecuteCommand(ctx context.Context, params *ExecuteCommandParams) (interface{}, error) { var result interface{} if err := s.sender.Call(ctx, "workspace/executeCommand", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) InlayHintRefresh(ctx context.Context) error { return s.sender.Call(ctx, "workspace/inlayHint/refresh", nil, nil) -} // 209 +} func (s *serverDispatcher) InlineValueRefresh(ctx context.Context) error { return s.sender.Call(ctx, "workspace/inlineValue/refresh", nil, nil) -} // 209 +} func (s *serverDispatcher) SemanticTokensRefresh(ctx context.Context) error { return s.sender.Call(ctx, "workspace/semanticTokens/refresh", nil, nil) -} // 209 +} func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) { var result []SymbolInformation if err := s.sender.Call(ctx, "workspace/symbol", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) WillCreateFiles(ctx context.Context, params *CreateFilesParams) (*WorkspaceEdit, error) { var result *WorkspaceEdit if err := s.sender.Call(ctx, "workspace/willCreateFiles", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) WillDeleteFiles(ctx context.Context, params *DeleteFilesParams) (*WorkspaceEdit, error) { var result *WorkspaceEdit if err := s.sender.Call(ctx, "workspace/willDeleteFiles", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) WillRenameFiles(ctx context.Context, params *RenameFilesParams) (*WorkspaceEdit, error) { var result *WorkspaceEdit if err := s.sender.Call(ctx, "workspace/willRenameFiles", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) ResolveWorkspaceSymbol(ctx context.Context, params *WorkspaceSymbol) (*WorkspaceSymbol, error) { var result *WorkspaceSymbol if err := s.sender.Call(ctx, "workspaceSymbol/resolve", params, &result); err != nil { return nil, err } return result, nil -} // 169 +} func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { var result interface{} if err := s.sender.Call(ctx, method, params, &result); err != nil { From 2fa6ca1e245fb760cb11dac2d3b31587ff8621e2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 29 Dec 2022 13:04:08 -0500 Subject: [PATCH 647/723] gopls/internal/lsp/source/impls: a new "implementations" index This change introduces a new algorithm for computing "implementations" that does not require type-checker data structures at search time. The algorithm uses a string representation of method IDs and types and a technique similar to a Bloom filter to quickly perform subset checks. See methodsets package for details. This string-based check currently has some false positives (e.g. for anonymous interface types, which are exceedingly rare) or type parameters. We could fall back to the type checker in such cases, or perhaps refine the string representation. (It's worth noting that the current implementation doesn't handle type parameters very well either.) The index is currently a set of data structures computed immediately after type checking, but they could be easily serialized into a file-based cache. Also, improve the output of the relevant tests. Change-Id: I9e879443312929bffd6d5311b3657535ad64dd5c Reviewed-on: https://go-review.googlesource.com/c/tools/+/460275 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/cache/check.go | 5 + gopls/internal/lsp/cache/pkg.go | 10 +- gopls/internal/lsp/lsp_test.go | 52 +-- gopls/internal/lsp/protocol/mapper.go | 23 +- gopls/internal/lsp/source/identifier.go | 2 +- gopls/internal/lsp/source/implementation.go | 62 +-- gopls/internal/lsp/source/implementation2.go | 385 ++++++++++++++++ .../lsp/source/methodsets/methodsets.go | 425 ++++++++++++++++++ gopls/internal/lsp/source/references2.go | 8 +- gopls/internal/lsp/source/source_test.go | 40 +- gopls/internal/lsp/source/util.go | 3 + gopls/internal/lsp/source/view.go | 2 + .../testdata/implementation/implementation.go | 2 + .../implementation/implementation_generics.go | 16 + .../implementation/other/other_generics.go | 16 + .../internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- gopls/internal/lsp/tests/util.go | 27 ++ .../internal/regtest/misc/references_test.go | 18 +- 19 files changed, 964 insertions(+), 136 deletions(-) create mode 100644 gopls/internal/lsp/source/implementation2.go create mode 100644 gopls/internal/lsp/source/methodsets/methodsets.go create mode 100644 gopls/internal/lsp/testdata/implementation/implementation_generics.go create mode 100644 gopls/internal/lsp/testdata/implementation/other/other_generics.go diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 01259583ce2..d7b06851a2a 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -22,6 +22,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/methodsets" "golang.org/x/tools/gopls/internal/lsp/source/xrefs" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" @@ -488,6 +489,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil // Don't type check Unsafe: it's unnecessary, and doing so exposes a data // race to Unsafe.completed. pkg.types = types.Unsafe + pkg.methodsets = methodsets.NewIndex(pkg.fset, pkg.types) return pkg, nil } @@ -569,6 +571,9 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil // Type checking errors are handled via the config, so ignore them here. _ = check.Files(files) // 50us-15ms, depending on size of package + // Build global index of method sets for 'implementations' queries. + pkg.methodsets = methodsets.NewIndex(pkg.fset, pkg.types) + // If the context was cancelled, we may have returned a ton of transient // errors to the type checker. Swallow them. if ctx.Err() != nil { diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index e640191d26e..0a986dec7c4 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/methodsets" "golang.org/x/tools/gopls/internal/lsp/source/xrefs" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/memoize" @@ -45,7 +46,8 @@ type pkg struct { hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors xrefs []byte // serializable index of outbound cross-references - analyses memoize.Store // maps analyzer.Name to Promise[actionResult] + analyses memoize.Store // maps analyzer.Name to Promise[actionResult] + methodsets *methodsets.Index // index of method sets of package-level types } func (p *pkg) String() string { return string(p.ID()) } @@ -184,3 +186,9 @@ func (p *pkg) ReferencesTo(pkgPath PackagePath, objPath objectpath.Path) []proto // For now we just hang it off the pkg. return xrefs.Lookup(p.m, p.xrefs, pkgPath, objPath) } + +func (p *pkg) MethodSetsIndex() *methodsets.Index { + // TODO(adonovan): In future, p.methodsets will be retrieved from a + // section of the cache file produced by type checking. + return p.methodsets +} diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 1138c36344b..d7acc1c553a 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -745,51 +745,35 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { } } -func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) { +func (r *runner) Implementation(t *testing.T, spn span.Span, wantSpans []span.Span) { sm, err := r.data.Mapper(spn.URI()) if err != nil { t.Fatal(err) } loc, err := sm.SpanLocation(spn) if err != nil { - t.Fatalf("failed for %v: %v", spn, err) - } - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - } - var locs []protocol.Location - params := &protocol.ImplementationParams{ - TextDocumentPositionParams: tdpp, + t.Fatal(err) } - locs, err = r.server.Implementation(r.ctx, params) + gotImpls, err := r.server.Implementation(r.ctx, &protocol.ImplementationParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, + Position: loc.Range.Start, + }, + }) if err != nil { - t.Fatalf("failed for %v: %v", spn, err) + t.Fatalf("Server.Implementation(%s): %v", spn, err) } - if len(locs) != len(impls) { - t.Fatalf("got %d locations for implementation, expected %d", len(locs), len(impls)) + gotLocs, err := tests.LocationsToSpans(r.data, gotImpls) + if err != nil { + t.Fatal(err) } - - var results []span.Span - for i := range locs { - locURI := locs[i].URI.SpanURI() - lm, err := r.data.Mapper(locURI) - if err != nil { - t.Fatal(err) - } - imp, err := lm.LocationSpan(locs[i]) - if err != nil { - t.Fatalf("failed for %v: %v", locs[i], err) - } - results = append(results, imp) + sanitize := func(s string) string { + return strings.ReplaceAll(s, r.data.Config.Dir, "gopls/internal/lsp/testdata") } - span.SortSpans(results) // to make tests - span.SortSpans(impls) // deterministic - - for i := range results { - if results[i] != impls[i] { - t.Errorf("for %dth implementation of %v got %v want %v", i, spn, results[i], impls[i]) - } + want := sanitize(tests.SortAndFormatSpans(wantSpans)) + got := sanitize(tests.SortAndFormatSpans(gotLocs)) + if got != want { + t.Errorf("implementations(%s):\n%s", sanitize(fmt.Sprint(spn)), diff.Unified("want", "got", want, got)) } } diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go index ae7218e2fae..e1e2578bf34 100644 --- a/gopls/internal/lsp/protocol/mapper.go +++ b/gopls/internal/lsp/protocol/mapper.go @@ -141,7 +141,7 @@ func (m *Mapper) SpanLocation(s span.Span) (Location, error) { if err != nil { return Location{}, err } - return Location{URI: URIFromSpanURI(s.URI()), Range: rng}, nil + return m.RangeLocation(rng), nil } // SpanRange converts a (UTF-8) span to a protocol (UTF-16) range. @@ -196,6 +196,15 @@ func (m *Mapper) PointPosition(p span.Point) (Position, error) { // -- conversions from byte offsets -- +// OffsetLocation converts a byte-offset interval to a protocol (UTF-16) location. +func (m *Mapper) OffsetLocation(start, end int) (Location, error) { + rng, err := m.OffsetRange(start, end) + if err != nil { + return Location{}, err + } + return m.RangeLocation(rng), nil +} + // OffsetRange converts a byte-offset interval to a protocol (UTF-16) range. func (m *Mapper) OffsetRange(start, end int) (Range, error) { if start > end { @@ -411,7 +420,7 @@ func (m *Mapper) PosLocation(tf *token.File, start, end token.Pos) (Location, er if err != nil { return Location{}, err } - return Location{URI: URIFromSpanURI(m.URI), Range: rng}, nil + return m.RangeLocation(rng), nil } // PosPosition converts a token range to a protocol (UTF-16) range. @@ -428,6 +437,11 @@ func (m *Mapper) NodeRange(tf *token.File, node ast.Node) (Range, error) { return m.PosRange(tf, node.Pos(), node.End()) } +// RangeLocation pairs a protocol Range with its URI, in a Location. +func (m *Mapper) RangeLocation(rng Range) Location { + return Location{URI: URIFromSpanURI(m.URI), Range: rng} +} + // -- MappedRange -- // OffsetMappedRange returns a MappedRange for the given byte offsets. @@ -472,10 +486,7 @@ func (mr MappedRange) Range() Range { // Location returns the range in protocol location (UTF-16) form. func (mr MappedRange) Location() Location { - return Location{ - URI: URIFromSpanURI(mr.URI()), - Range: mr.Range(), - } + return mr.Mapper.RangeLocation(mr.Range()) } // Span returns the range in span (UTF-8) form. diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 519cc9a4712..39743b2a507 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -216,7 +216,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa return nil, err } // Look up "error" and then navigate to its only method. - // The Error method does not appear in the builtin package's scope.log.Pri + // The Error method does not appear in the builtin package's scope. const errorName = "error" builtinObj := builtin.File.Scope.Lookup(errorName) if builtinObj == nil { diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 1c80096bc25..be29973ae8c 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -11,49 +11,26 @@ import ( "go/ast" "go/token" "go/types" - "sort" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source/methodsets" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" ) -func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) ([]protocol.Location, error) { - ctx, done := event.Start(ctx, "source.Implementation") - defer done() - - impls, err := implementations(ctx, snapshot, f, pp) - if err != nil { - return nil, err - } - var locations []protocol.Location - for _, impl := range impls { - if impl.pkg == nil || len(impl.pkg.CompiledGoFiles()) == 0 { - continue - } - rng, err := objToMappedRange(impl.pkg, impl.obj) - if err != nil { - return nil, err - } - locations = append(locations, rng.Location()) - } - sort.Slice(locations, func(i, j int) bool { - li, lj := locations[i], locations[j] - if li.URI == lj.URI { - return protocol.CompareRange(li.Range, lj.Range) < 0 - } - return li.URI < lj.URI - }) - return locations, nil -} - var ErrNotAType = errors.New("not a type name or method") // implementations returns the concrete implementations of the specified // interface, or the interfaces implemented by the specified concrete type. // It populates only the definition-related fields of qualifiedObject. // (Arguably it should return a smaller data type.) +// +// This is the legacy implementation using the type-check-the-world +// strategy. It is still needed by the 'references' command for now, +// but we are moving one step at a time. See implementation2.go for +// the incremental algorithm used by the 'implementations' command. +// TODO(adonovan): eliminate this implementation. func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) ([]qualifiedObject, error) { // Find all named types, even local types // (which can have methods due to promotion). @@ -106,10 +83,10 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. case *types.Func: queryMethod = obj if recv := obj.Type().(*types.Signature).Recv(); recv != nil { - queryType = ensurePointer(recv.Type()) + queryType = methodsets.EnsurePointer(recv.Type()) } case *types.TypeName: - queryType = ensurePointer(obj.Type()) + queryType = methodsets.EnsurePointer(obj.Type()) } if queryType == nil { @@ -124,7 +101,7 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. for _, named := range allNamed { var ( candObj types.Object = named.Obj() - candType = ensurePointer(named) + candType = methodsets.EnsurePointer(named) ) if !concreteImplementsIntf(candType, queryType) { @@ -179,7 +156,7 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. // concreteImplementsIntf returns true if a is an interface type implemented by // concrete type b, or vice versa. func concreteImplementsIntf(a, b types.Type) bool { - aIsIntf, bIsIntf := IsInterface(a), IsInterface(b) + aIsIntf, bIsIntf := types.IsInterface(a), types.IsInterface(b) // Make sure exactly one is an interface type. if aIsIntf == bIsIntf { @@ -191,20 +168,15 @@ func concreteImplementsIntf(a, b types.Type) bool { a, b = b, a } + // TODO(adonovan): this should really use GenericAssignableTo + // to report (e.g.) "ArrayList[T] implements List[T]", but + // GenericAssignableTo doesn't work correctly on pointers to + // generic named types. Thus the legacy implementation and the + // "local" part of implementation2 fail to report generics. + // The global algorithm based on subsets does the right thing. return types.AssignableTo(a, b) } -// ensurePointer wraps T in a *types.Pointer if T is a named, non-interface -// type. This is useful to make sure you consider a named type's full method -// set. -func ensurePointer(T types.Type) types.Type { - if _, ok := T.(*types.Named); ok && !IsInterface(T) { - return types.NewPointer(T) - } - - return T -} - // A qualifiedObject is the result of resolving a reference from an // identifier to an object. type qualifiedObject struct { diff --git a/gopls/internal/lsp/source/implementation2.go b/gopls/internal/lsp/source/implementation2.go new file mode 100644 index 00000000000..4a8e829a887 --- /dev/null +++ b/gopls/internal/lsp/source/implementation2.go @@ -0,0 +1,385 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +// This file defines the new implementation of the 'implementation' +// operator that does not require type-checker data structures for an +// unbounded number of packages. +// +// TODO(adonovan): +// - Audit to ensure robustness in face of type errors. +// - Support 'error' and 'error.Error', which were also lacking from the old implementation. +// - Eliminate false positives due to 'tricky' cases of the global algorithm. +// - Ensure we have test coverage of: +// type aliases +// nil, PkgName, Builtin (all errors) +// any (empty result) +// method of unnamed interface type (e.g. var x interface { f() }) +// (the global algorithm may find implementations of this type +// but will not include it in the index.) + +import ( + "context" + "fmt" + "go/ast" + "go/token" + "go/types" + "reflect" + "sort" + "strings" + "sync" + + "golang.org/x/sync/errgroup" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source/methodsets" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" +) + +// Implementation returns a new sorted array of locations of +// declarations of types that implement (or are implemented by) the +// type referred to at the given position. +// +// 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 Snapshot, f FileHandle, pp protocol.Position) ([]protocol.Location, error) { + ctx, done := event.Start(ctx, "source.Implementation") + defer done() + + locs, err := implementations2(ctx, snapshot, f, pp) + if err != nil { + return nil, err + } + + // Sort and de-duplicate locations. + sort.Slice(locs, func(i, j int) bool { + return protocol.CompareLocation(locs[i], locs[j]) < 0 + }) + out := locs[:0] + for _, loc := range locs { + if len(out) == 0 || out[len(out)-1] != loc { + out = append(out, loc) + } + } + locs = out + + return locs, nil +} + +func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.Location, error) { + + // Type-check the query package, find the query identifier, + // and locate the type/method declaration it refers to. + declPosn, methodID, err := typeDeclPosition(ctx, snapshot, fh.URI(), pp) + if err != nil { + return nil, err + } + + // Type-check the declaring package (incl. variants) for use + // by the "local" search, which uses type information to + // enumerate all types within the package that satisfy the + // query type, even those defined local to a function. + declURI := span.URIFromPath(declPosn.Filename) + declMetas, err := snapshot.MetadataForFile(ctx, declURI) + if err != nil { + return nil, err + } + if len(declMetas) == 0 { + return nil, fmt.Errorf("no packages for file %s", declURI) + } + ids := make([]PackageID, len(declMetas)) + for i, m := range declMetas { + ids[i] = m.ID + } + localPkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, ids...) + if err != nil { + return nil, err + } + // The narrowest package will do, since the local search is based + // on position and the global search is based on fingerprint. + // (Neither is based on object identity.) + declPkg := localPkgs[0] + declFile, err := declPkg.File(declURI) + if err != nil { + return nil, err // "can't happen" + } + + // Find declaration of corresponding object + // in this package based on (URI, offset). + pos, err := safetoken.Pos(declFile.Tok, declPosn.Offset) + if err != nil { + return nil, err + } + // TODO(adonovan): simplify: use objectsAt? + path := pathEnclosingObjNode(declFile.File, pos) + if path == nil { + return nil, ErrNoIdentFound // checked earlier + } + id, ok := path[0].(*ast.Ident) + if !ok { + return nil, ErrNoIdentFound // checked earlier + } + obj := declPkg.GetTypesInfo().ObjectOf(id) // may be nil + + // Is the selected identifier a type name or method? + // (For methods, report the corresponding method names.) + var queryType types.Type + switch obj := obj.(type) { + case *types.TypeName: + queryType = obj.Type() + case *types.Func: + // For methods, use the receiver type, which may be anonymous. + if recv := obj.Type().(*types.Signature).Recv(); recv != nil { + queryType = recv.Type() + } + default: + return nil, fmt.Errorf("%s is not a type or method", id.Name) + } + if queryType == nil { + return nil, ErrNotAType + } + + // Compute the method-set fingerprint used as a key to the global search. + key, hasMethods := methodsets.KeyOf(queryType) + if !hasMethods { + // A type with no methods yields an empty result. + // (No point reporting that every type satisfies 'any'.) + return nil, nil + } + + // The global search needs to look at every package in the workspace; + // see package ./methodsets. + // + // For now we do all the type checking before beginning the search. + // TODO(adonovan): opt: search in parallel topological order + // so that we can overlap index lookup with typechecking. + // I suspect a number of algorithms on the result of TypeCheck could + // be optimized by being applied as soon as each package is available. + globalMetas, err := snapshot.AllMetadata(ctx) + if err != nil { + return nil, err + } + globalIDs := make([]PackageID, 0, len(globalMetas)) + for _, m := range globalMetas { + if m.PkgPath == declPkg.PkgPath() { + continue // declaring package is handled by local implementation + } + globalIDs = append(globalIDs, m.ID) + } + globalPkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, globalIDs...) + if err != nil { + return nil, err + } + + // Search local and global packages in parallel. + var ( + group errgroup.Group + locsMu sync.Mutex + locs []protocol.Location + ) + // local search + for _, localPkg := range localPkgs { + localPkg := localPkg + group.Go(func() error { + localLocs, err := localImplementations(ctx, snapshot, localPkg, queryType, methodID) + if err != nil { + return err + } + locsMu.Lock() + locs = append(locs, localLocs...) + locsMu.Unlock() + return nil + }) + } + // global search + for _, globalPkg := range globalPkgs { + globalPkg := globalPkg + group.Go(func() error { + for _, loc := range globalPkg.MethodSetsIndex().Search(key, methodID) { + loc := loc + // Map offsets to protocol.Locations in parallel (may involve I/O). + group.Go(func() error { + ploc, err := offsetToLocation(ctx, snapshot, loc.Filename, loc.Start, loc.End) + if err != nil { + return err + } + locsMu.Lock() + locs = append(locs, ploc) + locsMu.Unlock() + return nil + }) + } + return nil + }) + } + if err := group.Wait(); err != nil { + return nil, err + } + + return locs, nil +} + +// offsetToLocation converts an offset-based position to a protocol.Location, +// which requires reading the file. +func offsetToLocation(ctx context.Context, snapshot Snapshot, filename string, start, end int) (protocol.Location, error) { + uri := span.URIFromPath(filename) + fh, err := snapshot.GetFile(ctx, uri) + if err != nil { + return protocol.Location{}, err // cancelled, perhaps + } + content, err := fh.Read() + if err != nil { + return protocol.Location{}, err // nonexistent or deleted ("can't happen") + } + m := protocol.NewMapper(uri, content) + return m.OffsetLocation(start, end) +} + +// typeDeclPosition returns the position of the declaration of the +// type referred to at (uri, ppos). If it refers to a method, the +// function returns the method's receiver type and ID. +func typeDeclPosition(ctx context.Context, snapshot Snapshot, uri span.URI, ppos protocol.Position) (token.Position, string, error) { + var noPosn token.Position + + pkg, pgf, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, WidestPackage) + if err != nil { + return noPosn, "", err + } + pos, err := pgf.PositionPos(ppos) + if err != nil { + return noPosn, "", err + } + + // This function inherits the limitation of its predecessor in + // requiring the selection to be an identifier (of a type or + // method). But there's no fundamental reason why one could + // not pose this query about any selected piece of syntax that + // has a type and thus a method set. + // (If LSP was more thorough about passing text selections as + // intervals to queries, you could ask about the method set of a + // subexpression such as x.f().) + + // TODO(adonovan): simplify: use objectsAt? + path := pathEnclosingObjNode(pgf.File, pos) + if path == nil { + return noPosn, "", ErrNoIdentFound + } + id, ok := path[0].(*ast.Ident) + if !ok { + return noPosn, "", ErrNoIdentFound + } + + // Is the object a type or method? Reject other kinds. + var methodID string + obj := pkg.GetTypesInfo().ObjectOf(id) + switch obj := obj.(type) { + case *types.TypeName: + // ok + case *types.Func: + if obj.Type().(*types.Signature).Recv() == nil { + return noPosn, "", fmt.Errorf("%s is a function, not a method", id.Name) + } + methodID = obj.Id() + case nil: + return noPosn, "", fmt.Errorf("%s denotes unknown object", id.Name) + default: + // e.g. *types.Var -> "var". + kind := strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types.")) + return noPosn, "", fmt.Errorf("%s is a %s, not a type", id.Name, kind) + } + + declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) + return declPosn, methodID, nil +} + +// localImplementations searches within pkg for declarations of all +// types that are assignable to/from the query type, and returns a new +// unordered array of their locations. +// +// If methodID is non-empty, the function instead returns the location +// of each type's method (if any) of that ID. +// +// ("Local" refers to the search within the same package, but this +// function's results may include type declarations that are local to +// a function body. The global search index excludes such types +// because reliably naming such types is hard.) +func localImplementations(ctx context.Context, snapshot Snapshot, pkg Package, queryType types.Type, methodID string) ([]protocol.Location, error) { + queryType = methodsets.EnsurePointer(queryType) + + // Scan through all type declarations in the syntax. + var locs []protocol.Location + var methodLocs []methodsets.Location + for _, pgf := range pkg.CompiledGoFiles() { + ast.Inspect(pgf.File, func(n ast.Node) bool { + spec, ok := n.(*ast.TypeSpec) + if !ok { + return true // not a type declaration + } + def := pkg.GetTypesInfo().Defs[spec.Name] + if def == nil { + return true // "can't happen" for types + } + if def.(*types.TypeName).IsAlias() { + return true // skip type aliases to avoid duplicate reporting + } + candidateType := methodsets.EnsurePointer(def.Type()) + + // The historical behavior enshrined by this + // function rejects cases where both are + // (nontrivial) interface types? + // That seems like useful information. + // TODO(adonovan): UX: report I/I pairs too? + // The same question appears in the global algorithm (methodsets). + if !concreteImplementsIntf(candidateType, queryType) { + return true // not assignable + } + + // Ignore types with empty method sets. + // (No point reporting that every type satisfies 'any'.) + mset := types.NewMethodSet(candidateType) + if mset.Len() == 0 { + return true + } + + if methodID == "" { + // Found matching type. + locs = append(locs, mustLocation(pgf, spec.Name)) + return true + } + + // Find corresponding method. + // + // We can't use LookupFieldOrMethod because it requires + // the methodID's types.Package, which we don't know. + // We could recursively search pkg.Imports for it, + // but it's easier to walk the method set. + for i := 0; i < mset.Len(); i++ { + method := mset.At(i).Obj() + if method.Id() == methodID { + posn := safetoken.StartPosition(pkg.FileSet(), method.Pos()) + methodLocs = append(methodLocs, methodsets.Location{ + Filename: posn.Filename, + Start: posn.Offset, + End: posn.Offset + len(method.Name()), + }) + break + } + } + return true + }) + } + + // Finally convert method positions to protocol form by reading the files. + for _, mloc := range methodLocs { + loc, err := offsetToLocation(ctx, snapshot, mloc.Filename, mloc.Start, mloc.End) + if err != nil { + return nil, err + } + locs = append(locs, loc) + } + + return locs, nil +} diff --git a/gopls/internal/lsp/source/methodsets/methodsets.go b/gopls/internal/lsp/source/methodsets/methodsets.go new file mode 100644 index 00000000000..81d5d0070da --- /dev/null +++ b/gopls/internal/lsp/source/methodsets/methodsets.go @@ -0,0 +1,425 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package methodsets defines an incremental, serializable index of +// method-set information that allows efficient 'implements' queries +// across packages of the workspace without using the type checker. +// +// This package provides only the "global" (all workspace) search; the +// "local" search within a given package uses a different +// implementation based on type-checker data structures for a single +// package plus variants; see ../implementation2.go. +// The local algorithm is more precise as it tests function-local types too. +// +// A global index of function-local types is challenging since they +// may reference other local types, for which we would need to invent +// stable names, an unsolved problem described in passing in Go issue +// 57497. The global algorithm also does not index anonymous interface +// types, even outside function bodies. +// +// Consequently, global results are not symmetric: applying the +// operation twice may not get you back where you started. +package methodsets + +// DESIGN +// +// See https://go.dev/cl/452060 for a minimal exposition of the algorithm. +// +// For each method, we compute a fingerprint: a string representing +// the method name and type such that equal fingerprint strings mean +// identical method types. +// +// For efficiency, the fingerprint is reduced to a single bit +// of a uint64, so that the method set can be represented as +// the union of those method bits (a uint64 bitmask). +// Assignability thus reduces to a subset check on bitmasks +// followed by equality checks on fingerprints. +// +// In earlier experiments, using 128-bit masks instead of 64 reduced +// the number of candidates by about 2x. Using (like a Bloom filter) a +// different hash function to compute a second 64-bit mask and +// performing a second mask test reduced it by about 4x. +// Neither had much effect on the running time, presumably because a +// single 64-bit mask is quite effective. See CL 452060 for details. + +import ( + "fmt" + "go/token" + "go/types" + "hash/crc32" + "strconv" + "strings" + + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/internal/typeparams" +) + +// An Index records the non-empty method sets of all package-level +// types in a package in a form that permits assignability queries +// without the type checker. +type Index struct { + pkg gobPackage +} + +// NewIndex returns a new index of method-set information for all +// package-level types in the specified package. +func NewIndex(fset *token.FileSet, pkg *types.Package) *Index { + return new(indexBuilder).build(fset, pkg) +} + +// A Location records the extent of an identifier in byte-offset form. +// +// Conversion to protocol (UTF-16) form is done by the caller after a +// search, not during index construction. +// TODO(adonovan): opt: reconsider this choice, if FileHandles, not +// ParsedGoFiles were to provide ColumnMapper-like functionality. +// (Column mapping is currently associated with parsing, +// but non-parsed and even non-Go files need it too.) +// Since type checking requires reading (but not parsing) all +// dependencies' Go files, we could do the conversion at type-checking +// time at little extra cost in that case. +type Location struct { + Filename string + Start, End int // byte offsets +} + +// A Key represents the method set of a given type in a form suitable +// to pass to the (*Index).Search method of many different Indexes. +type Key struct { + mset gobMethodSet // note: lacks position information +} + +// KeyOf returns the search key for the method sets of a given type. +// It returns false if the type has no methods. +func KeyOf(t types.Type) (Key, bool) { + mset := methodSetInfo(func(types.Object) (_ gobPosition) { return }, t, gobPosition{}) + if mset.Mask == 0 { + return Key{}, false // no methods + } + return Key{mset}, true +} + +// Search reports each type that implements (or is implemented by) the +// type that produced the search key. If methodID is nonempty, only +// that method of each type is reported. +// The result is the location of each type or method. +func (index *Index) Search(key Key, methodID string) []Location { + var locs []Location + for _, candidate := range index.pkg.MethodSets { + // Traditionally this feature doesn't report + // interface/interface elements of the relation. + // I think that's a mistake. + // TODO(adonovan): UX: change it, here and in the local implementation. + if candidate.IsInterface && key.mset.IsInterface { + continue + } + if !satisfies(candidate, key.mset) && !satisfies(key.mset, candidate) { + continue + } + + if candidate.Tricky { + // If any interface method is tricky then extra + // checking may be needed to eliminate a false positive. + // TODO(adonovan): implement it. + } + + if methodID == "" { + locs = append(locs, index.location(candidate.Posn)) + } else { + for _, m := range candidate.Methods { + // Here we exploit knowledge of the shape of the fingerprint string. + if strings.HasPrefix(m.Fingerprint, methodID) && + m.Fingerprint[len(methodID)] == '(' { + locs = append(locs, index.location(m.Posn)) + break + } + } + } + } + return locs +} + +// satisfies does a fast check for whether x satisfies y. +func satisfies(x, y gobMethodSet) bool { + return y.IsInterface && x.Mask&y.Mask == y.Mask && subset(y, x) +} + +// subset reports whether method set x is a subset of y. +func subset(x, y gobMethodSet) bool { +outer: + for _, mx := range x.Methods { + for _, my := range y.Methods { + if mx.Sum == my.Sum && mx.Fingerprint == my.Fingerprint { + continue outer // found; try next x method + } + } + return false // method of x not found in y + } + return true // all methods of x found in y +} + +func (index *Index) location(posn gobPosition) Location { + return Location{ + Filename: index.pkg.Filenames[posn.File], + Start: posn.Offset, + End: posn.Offset + posn.Len, + } +} + +// An indexBuilder builds an index for a single package. +type indexBuilder struct { + gobPackage + filenameIndex map[string]int +} + +// build adds to the index all package-level named types of the specified package. +func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index { + // We ignore aliases, though in principle they could define a + // struct{...} or interface{...} type, or an instantiation of + // a generic, that has a novel method set. + scope := pkg.Scope() + for _, name := range scope.Names() { + if tname, ok := scope.Lookup(name).(*types.TypeName); ok && !tname.IsAlias() { + b.add(fset, tname) + } + } + + return &Index{pkg: b.gobPackage} +} + +func (b *indexBuilder) add(fset *token.FileSet, tname *types.TypeName) { + objectPos := func(obj types.Object) gobPosition { + posn := safetoken.StartPosition(fset, obj.Pos()) + return gobPosition{b.fileIndex(posn.Filename), posn.Offset, len(obj.Name())} + } + if mset := methodSetInfo(objectPos, tname.Type(), objectPos(tname)); mset.Mask != 0 { + // Only record types with non-trivial method sets. + b.MethodSets = append(b.MethodSets, mset) + } +} + +// fileIndex returns a small integer that encodes the file name. +func (b *indexBuilder) fileIndex(filename string) int { + i, ok := b.filenameIndex[filename] + if !ok { + i = len(b.Filenames) + if b.filenameIndex == nil { + b.filenameIndex = make(map[string]int) + } + b.filenameIndex[filename] = i + b.Filenames = append(b.Filenames, filename) + } + return i +} + +// methodSetInfo returns the method-set fingerprint +// of a type and records its position (typePosn) +// and the position of each of its methods m, +// as provided by objectPos(m). +func methodSetInfo(objectPos func(types.Object) gobPosition, t types.Type, typePosn gobPosition) gobMethodSet { + // For non-interface types, use *T + // (if T is not already a pointer) + // since it may have more methods. + mset := types.NewMethodSet(EnsurePointer(t)) + + // Convert the method set into a compact summary. + var mask uint64 + tricky := false + methods := make([]gobMethod, mset.Len()) + for i := 0; i < mset.Len(); i++ { + m := mset.At(i).Obj().(*types.Func) + fp, isTricky := fingerprint(m) + if isTricky { + tricky = true + } + sum := crc32.ChecksumIEEE([]byte(fp)) + methods[i] = gobMethod{fp, sum, objectPos(m)} + mask |= 1 << uint64(((sum>>24)^(sum>>16)^(sum>>8)^sum)&0x3f) + } + return gobMethodSet{typePosn, types.IsInterface(t), tricky, mask, methods} +} + +// EnsurePointer wraps T in a types.Pointer if T is a named, non-interface type. +// This is useful to make sure you consider a named type's full method set. +func EnsurePointer(T types.Type) types.Type { + if _, ok := T.(*types.Named); ok && !types.IsInterface(T) { + return types.NewPointer(T) + } + + return T +} + +// fingerprint returns an encoding of a method signature such that two +// methods with equal encodings have identical types, except for a few +// tricky types whose encodings may spuriously match and whose exact +// identity computation requires the type checker to eliminate false +// positives (which are rare). The boolean result indicates whether +// the result was one of these tricky types. +// +// In the standard library, 99.8% of package-level types have a +// non-tricky method-set. The most common exceptions are due to type +// parameters. +// +// The fingerprint string starts with method.Id() + "(". +func fingerprint(method *types.Func) (string, bool) { + var buf strings.Builder + tricky := false + var fprint func(t types.Type) + fprint = func(t types.Type) { + switch t := t.(type) { + case *types.Named: + tname := t.Obj() + if tname.Pkg() != nil { + buf.WriteString(strconv.Quote(tname.Pkg().Path())) + buf.WriteByte('.') + } else if tname.Name() != "error" { + panic(tname) // error is the only named type with no package + } + buf.WriteString(tname.Name()) + + case *types.Array: + fmt.Fprintf(&buf, "[%d]", t.Len()) + fprint(t.Elem()) + + case *types.Slice: + buf.WriteString("[]") + fprint(t.Elem()) + + case *types.Pointer: + buf.WriteByte('*') + fprint(t.Elem()) + + case *types.Map: + buf.WriteString("map[") + fprint(t.Key()) + buf.WriteByte(']') + fprint(t.Elem()) + + case *types.Chan: + switch t.Dir() { + case types.SendRecv: + buf.WriteString("chan ") + case types.SendOnly: + buf.WriteString("<-chan ") + case types.RecvOnly: + buf.WriteString("chan<- ") + } + fprint(t.Elem()) + + case *types.Tuple: + buf.WriteByte('(') + for i := 0; i < t.Len(); i++ { + if i > 0 { + buf.WriteByte(',') + } + fprint(t.At(i).Type()) + } + buf.WriteByte(')') + + case *types.Basic: + // Use canonical names for uint8 and int32 aliases. + switch t.Kind() { + case types.Byte: + buf.WriteString("byte") + case types.Rune: + buf.WriteString("rune") + default: + buf.WriteString(t.String()) + } + + case *types.Signature: + buf.WriteString("func") + fprint(t.Params()) + if t.Variadic() { + buf.WriteString("...") // not quite Go syntax + } + fprint(t.Results()) + + case *types.Struct: + // Non-empty unnamed struct types in method + // signatures are vanishingly rare. + buf.WriteString("struct{") + for i := 0; i < t.NumFields(); i++ { + if i > 0 { + buf.WriteByte(';') + } + f := t.Field(i) + // This isn't quite right for embedded type aliases. + // (See types.TypeString(StructType) and #44410 for context.) + // But this is vanishingly rare. + if !f.Embedded() { + buf.WriteString(f.Id()) + buf.WriteByte(' ') + } + fprint(f.Type()) + if tag := t.Tag(i); tag != "" { + buf.WriteByte(' ') + buf.WriteString(strconv.Quote(tag)) + } + } + buf.WriteString("}") + + case *types.Interface: + if t.NumMethods() == 0 { + buf.WriteString("any") // common case + } else { + // Interface assignability is particularly + // tricky due to the possibility of recursion. + tricky = true + // We could still give more disambiguating precision + // than "..." if we wanted to. + buf.WriteString("interface{...}") + } + + case *typeparams.TypeParam: + tricky = true + // TODO(adonovan): refine this by adding a numeric suffix + // indicating the index among the receiver type's parameters. + buf.WriteByte('?') + + default: // incl. *types.Union + panic(t) + } + } + + buf.WriteString(method.Id()) // e.g. "pkg.Type" + sig := method.Type().(*types.Signature) + fprint(sig.Params()) + fprint(sig.Results()) + return buf.String(), tricky +} + +// -- serial format of index -- + +// The cost of gob encoding and decoding for most packages in x/tools +// is under 50us, with occasional peaks of around 1-3ms. +// The encoded indexes are around 1KB-50KB. + +// A gobPackage records the method set of each package-level type for a single package. +type gobPackage struct { + Filenames []string // see gobPosition.File + MethodSets []gobMethodSet +} + +// A gobMethodSet records the method set of a single type. +type gobMethodSet struct { + Posn gobPosition + IsInterface bool + Tricky bool // at least one method is tricky; assignability requires go/types + Mask uint64 // mask with 1 bit from each of methods[*].sum + Methods []gobMethod +} + +// A gobMethod records the name, type, and position of a single method. +type gobMethod struct { + Fingerprint string // string of form "methodID(params...)(results)" + Sum uint32 // checksum of fingerprint + Posn gobPosition // location of method declaration +} + +// A gobPosition records the file, offset, and length of an identifier. +type gobPosition struct { + File int // index into Index.filenames + Offset, Len int // in bytes +} diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index 87347d485a9..9ebeace6563 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -186,7 +186,7 @@ func packageReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pkg } } - // Find interal "references" to the package from + // Find internal "references" to the package from // of each package declaration in the target package itself. // // The widest package (possibly a test variant) has the @@ -474,11 +474,13 @@ func globalReferences(ctx context.Context, snapshot Snapshot, m *Metadata, pkgPa } // mustLocation reports the location interval a syntax node, -// which must belong to m.File! Safe for use only by references2. +// which must belong to m.File. +// +// Safe for use only by references2 and implementations2. func mustLocation(pgf *ParsedGoFile, n ast.Node) protocol.Location { loc, err := pgf.PosLocation(n.Pos(), n.End()) if err != nil { - panic(err) // can't happen in references2 + panic(err) // can't happen in references2 or implementations2 } return loc } diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 02625256be0..97f602f2958 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -571,45 +571,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { } func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) { - sm, err := r.data.Mapper(spn.URI()) - if err != nil { - t.Fatal(err) - } - loc, err := sm.SpanLocation(spn) - if err != nil { - t.Fatalf("failed for %v: %v", spn, err) - } - fh, err := r.snapshot.GetFile(r.ctx, spn.URI()) - if err != nil { - t.Fatal(err) - } - locs, err := source.Implementation(r.ctx, r.snapshot, fh, loc.Range.Start) - if err != nil { - t.Fatalf("failed for %v: %v", spn, err) - } - if len(locs) != len(impls) { - t.Fatalf("got %d locations for implementation, expected %d", len(locs), len(impls)) - } - var results []span.Span - for i := range locs { - locURI := locs[i].URI.SpanURI() - lm, err := r.data.Mapper(locURI) - if err != nil { - t.Fatal(err) - } - imp, err := lm.LocationSpan(locs[i]) - if err != nil { - t.Fatalf("failed for %v: %v", locs[i], err) - } - results = append(results, imp) - } - span.SortSpans(results) // to make tests - span.SortSpans(impls) // deterministic - for i := range results { - if results[i] != impls[i] { - t.Errorf("for %dth implementation of %v got %v want %v", i, spn, results[i], impls[i]) - } - } + // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go } func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) { diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 27ae8ff5c92..57b14646c50 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -165,6 +165,9 @@ func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) { } // IsInterface returns if a types.Type is an interface +// +// This function accepts nil, unlike types.IsInterface. +// TODO(adonovan): that's confusing! Eliminate this wrapper. func IsInterface(T types.Type) bool { return T != nil && types.IsInterface(T) } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index bb6b7542964..0a96eb376db 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -24,6 +24,7 @@ import ( "golang.org/x/tools/gopls/internal/govulncheck" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source/methodsets" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event/label" "golang.org/x/tools/internal/event/tag" @@ -793,6 +794,7 @@ type Package interface { HasTypeErrors() bool DiagnosticsForFile(uri span.URI) []*Diagnostic // new array of list/parse/type errors ReferencesTo(PackagePath, objectpath.Path) []protocol.Location // new sorted array of xrefs + MethodSetsIndex() *methodsets.Index } // A CriticalError is a workspace-wide error that generally prevents gopls from diff --git a/gopls/internal/lsp/testdata/implementation/implementation.go b/gopls/internal/lsp/testdata/implementation/implementation.go index b817319d5ef..5f124750e1b 100644 --- a/gopls/internal/lsp/testdata/implementation/implementation.go +++ b/gopls/internal/lsp/testdata/implementation/implementation.go @@ -29,3 +29,5 @@ type cryer int //@implementations("cryer", Cryer) func (cryer) Cry(other.CryType) {} //@mark(CryImpl, "Cry"),implementations("Cry", Cry) type Empty interface{} //@implementations("Empty") + +var _ interface{ Joke() } //@implementations("Joke", ImpJoker) diff --git a/gopls/internal/lsp/testdata/implementation/implementation_generics.go b/gopls/internal/lsp/testdata/implementation/implementation_generics.go new file mode 100644 index 00000000000..1f02d166b1e --- /dev/null +++ b/gopls/internal/lsp/testdata/implementation/implementation_generics.go @@ -0,0 +1,16 @@ +//go:build go1.18 +// +build go1.18 + +package implementation + +// -- generics -- + +type GenIface[T any] interface { //@mark(GenIface, "GenIface"),implementations("GenIface", GC) + F(int, string, T) //@mark(GenIfaceF, "F"),implementations("F", GCF) +} + +type GenConc[U any] int //@mark(GenConc, "GenConc"),implementations("GenConc", GI) + +func (GenConc[V]) F(int, string, V) {} //@mark(GenConcF, "F"),implementations("F", GIF) + +type GenConcString struct{ GenConc[string] } //@mark(GenConcString, "GenConcString"),implementations(GenConcString, GIString) diff --git a/gopls/internal/lsp/testdata/implementation/other/other_generics.go b/gopls/internal/lsp/testdata/implementation/other/other_generics.go new file mode 100644 index 00000000000..4b4c29f7d4a --- /dev/null +++ b/gopls/internal/lsp/testdata/implementation/other/other_generics.go @@ -0,0 +1,16 @@ +//go:build go1.18 +// +build go1.18 + +package other + +// -- generics (limited support) -- + +type GI[T any] interface { //@mark(GI, "GI"),implementations("GI", GenConc) + F(int, string, T) //@mark(GIF, "F"),implementations("F", GenConcF) +} + +type GIString GI[string] //@mark(GIString, "GIString"),implementations("GIString", GenConcString) + +type GC[U any] int //@mark(GC, "GC"),implementations("GC", GenIface) + +func (GC[V]) F(int, string, V) {} //@mark(GCF, "F"),implementations("F", GenIfaceF) diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index f68800875e3..e44ce18c049 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -27,6 +27,6 @@ SymbolsCount = 1 WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 -ImplementationsCount = 14 +ImplementationsCount = 15 SelectionRangesCount = 3 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 3a931ac3c75..80a5d23c7c6 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -27,6 +27,6 @@ SymbolsCount = 2 WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 -ImplementationsCount = 14 +ImplementationsCount = 25 SelectionRangesCount = 3 diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index 3d4c58c5d6e..454710b56a0 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -547,3 +547,30 @@ func StripSubscripts(s string) string { } return string(runes) } + +// LocationsToSpans converts protocol location into span form for testing. +func LocationsToSpans(data *Data, locs []protocol.Location) ([]span.Span, error) { + spans := make([]span.Span, len(locs)) + for i, loc := range locs { + m, err := data.Mapper(loc.URI.SpanURI()) + if err != nil { + return nil, err + } + spn, err := m.LocationSpan(loc) + if err != nil { + return nil, fmt.Errorf("failed for %v: %w", loc, err) + } + spans[i] = spn + } + return spans, nil +} + +// SortAndFormatSpans sorts and formats a list of spans for use in an assertion. +func SortAndFormatSpans(spans []span.Span) string { + span.SortSpans(spans) + var buf strings.Builder + for _, spn := range spans { + fmt.Fprintf(&buf, "%v\n", spn) + } + return buf.String() +} diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index bdaf610b86f..19704ffbf87 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -194,7 +194,8 @@ import "foo.mod/bar" const Foo = 42 type T int -type Interface interface{ M() } +type InterfaceM interface{ M() } +type InterfaceF interface{ F() } func _() { _ = bar.Blah @@ -203,6 +204,9 @@ func _() { -- foo/foo_test.go -- package foo +type Fer struct{} +func (Fer) F() {} + -- bar/bar.go -- package bar @@ -277,17 +281,21 @@ func _() { re string wantImpls []string }{ - // Interface is implemented both in foo.mod/bar [foo.mod/bar.test] (which + // InterfaceM is implemented both in foo.mod/bar [foo.mod/bar.test] (which // doesn't import foo), and in foo.mod/bar_test [foo.mod/bar.test], which // imports the test variant of foo. - {"Interface", []string{"bar/bar_test.go", "bar/bar_x_test.go"}}, + {"InterfaceM", []string{"bar/bar_test.go", "bar/bar_x_test.go"}}, + + // A search within the ordinary package to should find implementations + // (Fer) within the augmented test package. + {"InterfaceF", []string{"foo/foo_test.go"}}, } for _, test := range implTests { pos := env.RegexpSearch("foo/foo.go", test.re) - refs := env.Implementations("foo/foo.go", pos) + impls := env.Implementations("foo/foo.go", pos) - got := fileLocations(refs) + got := fileLocations(impls) if diff := cmp.Diff(test.wantImpls, got); diff != "" { t.Errorf("Implementations(%q) returned unexpected diff (-want +got):\n%s", test.re, diff) } From 9ff31a5780b2d96e9f596fe09dc13f1510a84542 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Wed, 18 Jan 2023 10:11:20 -0500 Subject: [PATCH 648/723] x/tools/go/analysis/passes/printf: revert URL in error message Revert CL 422854, which added a URL to this one error message, shortly after the Go 1.19 release, which means there is still time to keep it from ending up in Go 1.20. % go test # runtime/metrics_test ./description_test.go:61:4: (*testing.common).Errorf format %s has arg samples[0].Value.Kind() of wrong type runtime/metrics.ValueKind, see also https://pkg.go.dev/fmt#hdr-Printing After this CL the ", see also" will not print. The URL is undoubtedly helpful in some cases, but it makes the error much longer and reduces the signal-to-noise ratio. It is also not clear why this one error deserves special treatment. We should have a general solution for getting users to details about the specific errors being printed, not URLs in every message. This reverts commit 88d981ef8f8158b15938bd1bc77e47cea16fe5f0. Change-Id: Ib49e1fae94ba837f432d8a65e38e657cfa522668 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462438 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Russ Cox Reviewed-by: Robert Findley Auto-Submit: Russ Cox --- go/analysis/passes/printf/printf.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 3ac4fcaa28e..daaf709a449 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -910,7 +910,7 @@ func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, state *formatState) (o if reason != "" { details = " (" + reason + ")" } - pass.ReportRangef(call, "%s format %s has arg %s of wrong type %s%s, see also https://pkg.go.dev/fmt#hdr-Printing", state.name, state.format, analysisutil.Format(pass.Fset, arg), typeString, details) + pass.ReportRangef(call, "%s format %s has arg %s of wrong type %s%s", state.name, state.format, analysisutil.Format(pass.Fset, arg), typeString, details) return false } if v.typ&argString != 0 && v.verb != 'T' && !bytes.Contains(state.flags, []byte{'#'}) { From 3e6f71bba4359aeb7a301d361ee3cf95e8799599 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 17 Jan 2023 15:44:09 -0500 Subject: [PATCH 649/723] gopls/internal/regtest: use AfterChange in more places Update all the places (I think) where we await diagnostics unbounded, except for a couple that can't yet be made eager due to lacking instrumentation (for example, around didChangeConfiguration). Updates golang/go#39384 Change-Id: Ib00d28ce0350737d39a123fd200fd435cc89f967 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462495 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley --- .../regtest/completion/completion_test.go | 6 +- .../regtest/diagnostics/diagnostics_test.go | 91 +++++++++++-------- .../regtest/diagnostics/undeclared_test.go | 1 - gopls/internal/regtest/misc/failures_test.go | 4 +- gopls/internal/regtest/misc/generate_test.go | 3 +- gopls/internal/regtest/misc/shared_test.go | 4 +- .../internal/regtest/modfile/modfile_test.go | 36 ++++---- gopls/internal/regtest/watch/watch_test.go | 10 +- .../regtest/workspace/workspace_test.go | 8 +- 9 files changed, 86 insertions(+), 77 deletions(-) diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 2f9b550ed28..6d0bfc56844 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -173,9 +173,7 @@ package Run(t, files, func(t *testing.T, env *Env) { if tc.content != nil { env.WriteWorkspaceFile(tc.filename, *tc.content) - env.Await( - env.DoneWithChangeWatchedFiles(), - ) + env.Await(env.DoneWithChangeWatchedFiles()) } env.OpenFile(tc.filename) completions := env.Completion(tc.filename, env.RegexpSearch(tc.filename, tc.triggerRegexp)) @@ -318,7 +316,7 @@ func _() { env.AcceptCompletion("main.go", pos, item) // Await the diagnostics to add example.com/blah to the go.mod file. - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), ) }) diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 4ce509c87ac..800274b5c29 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -73,9 +73,7 @@ func m() { log.Println() } `) - env.Await( - Diagnostics(env.AtRegexp("main.go", "log")), - ) + env.AfterChange(Diagnostics(env.AtRegexp("main.go", "log"))) env.SaveBuffer("main.go") env.AfterChange(NoDiagnostics(ForFile("main.go"))) }) @@ -88,7 +86,7 @@ const Foo = "abc ` Run(t, brokenFile, func(t *testing.T, env *Env) { env.CreateBuffer("broken.go", brokenFile) - env.Await(Diagnostics(env.AtRegexp("broken.go", "\"abc"))) + env.AfterChange(Diagnostics(env.AtRegexp("broken.go", "\"abc"))) }) } @@ -128,13 +126,13 @@ func TestDiagnosticClearingOnEdit(t *testing.T) { func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) { Run(t, badPackage, func(t *testing.T, env *Env) { env.OpenFile("a.go") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a.go", "a = 1")), Diagnostics(env.AtRegexp("b.go", "a = 2")), ) env.RemoveWorkspaceFile("b.go") - env.Await( + env.AfterChange( NoDiagnostics(ForFile("a.go")), NoDiagnostics(ForFile("b.go")), ) @@ -214,7 +212,7 @@ func TestA(t *testing.T) { // not break the workspace. func TestDeleteTestVariant(t *testing.T) { Run(t, test38878, func(t *testing.T, env *Env) { - env.Await(Diagnostics(env.AtRegexp("a_test.go", `f\((3)\)`))) + env.AfterChange(Diagnostics(env.AtRegexp("a_test.go", `f\((3)\)`))) env.RemoveWorkspaceFile("a_test.go") env.AfterChange(NoDiagnostics(ForFile("a_test.go"))) @@ -231,7 +229,7 @@ func TestDeleteTestVariant(t *testing.T) { func TestDeleteTestVariant_DiskOnly(t *testing.T) { Run(t, test38878, func(t *testing.T, env *Env) { env.OpenFile("a_test.go") - env.Await(Diagnostics(AtPosition("a_test.go", 5, 3))) + env.AfterChange(Diagnostics(AtPosition("a_test.go", 5, 3))) env.Sandbox.Workdir.RemoveFile(context.Background(), "a_test.go") env.AfterChange(Diagnostics(AtPosition("a_test.go", 5, 3))) }) @@ -281,7 +279,8 @@ func Hello() { }) t.Run("initialized", func(t *testing.T) { Run(t, noMod, func(t *testing.T, env *Env) { - env.Await( + env.OnceMet( + InitialWorkspaceLoad, Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), ) env.RunGoCommand("mod", "init", "mod.com") @@ -296,7 +295,8 @@ func Hello() { WithOptions( Modes(Default), ).Run(t, noMod, func(t *testing.T, env *Env) { - env.Await( + env.OnceMet( + InitialWorkspaceLoad, Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), ) if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, true); err != nil { @@ -345,7 +345,7 @@ func TestHello(t *testing.T) { Run(t, testPackage, func(t *testing.T, env *Env) { env.OpenFile("lib_test.go") - env.Await( + env.AfterChange( Diagnostics(AtPosition("lib_test.go", 10, 2)), Diagnostics(AtPosition("lib_test.go", 11, 2)), ) @@ -420,8 +420,12 @@ func TestResolveDiagnosticWithDownload(t *testing.T) { env.OpenFile("print.go") // Check that gopackages correctly loaded this dependency. We should get a // diagnostic for the wrong formatting type. - // TODO: we should be able to easily also match the diagnostic message. - env.Await(Diagnostics(env.AtRegexp("print.go", "fmt.Printf"))) + env.AfterChange( + Diagnostics( + env.AtRegexp("print.go", "fmt.Printf"), + WithMessage("wrong type int"), + ), + ) }) } @@ -444,7 +448,9 @@ func Hello() { ` Run(t, adHoc, func(t *testing.T, env *Env) { env.OpenFile("b/b.go") - env.Await(Diagnostics(env.AtRegexp("b/b.go", "x"))) + env.AfterChange( + Diagnostics(env.AtRegexp("b/b.go", "x")), + ) }) } @@ -542,13 +548,13 @@ func f() { ` Run(t, noModule, func(t *testing.T, env *Env) { env.OpenFile("a.go") - env.Await( + env.AfterChange( // Expect the adHocPackagesWarning. OutstandingWork(lsp.WorkspaceLoadFailure, "outside of a module"), ) // Deleting the import dismisses the warning. env.RegexpReplace("a.go", `import "mod.com/hello"`, "") - env.Await( + env.AfterChange( NoOutstandingWork(), ) }) @@ -564,7 +570,8 @@ hi mom WithOptions( EnvVars{"GO111MODULE": go111module}, ).Run(t, files, func(t *testing.T, env *Env) { - env.Await( + env.OnceMet( + InitialWorkspaceLoad, NoOutstandingWork(), ) }) @@ -836,7 +843,7 @@ func TestX(t *testing.T) { var x int } `) - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("foo/bar_test.go", "x")), ) }) @@ -918,7 +925,9 @@ const C = a.A // We should still get diagnostics for files that exist. env.RegexpReplace("b/b.go", `a.A`, "a.Nonexistant") - env.Await(Diagnostics(env.AtRegexp("b/b.go", `Nonexistant`))) + env.AfterChange( + Diagnostics(env.AtRegexp("b/b.go", `Nonexistant`)), + ) }) } @@ -964,7 +973,9 @@ func main() { Run(t, mod, func(t *testing.T, env *Env) { writeGoVim(env, "p/p.go", p) writeGoVim(env, "main.go", main) - env.Await(Diagnostics(env.AtRegexp("main.go", "5"))) + env.AfterChange( + Diagnostics(env.AtRegexp("main.go", "5")), + ) }) }) @@ -993,7 +1004,7 @@ func TestDoIt(t *testing.T) { p.DoIt(5) } `) - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("main.go", "5")), Diagnostics(env.AtRegexp("p/p_test.go", "5")), Diagnostics(env.AtRegexp("p/x_test.go", "5")), @@ -1026,7 +1037,7 @@ func _() { WorkspaceFolders(), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a/a.go", "x")), ) }) @@ -1074,9 +1085,7 @@ func main() {} ` Run(t, basic, func(t *testing.T, env *Env) { env.Editor.CreateBuffer(env.Ctx, "foo.go", `package main`) - env.Await( - env.DoneWithOpen(), - ) + env.AfterChange() env.CloseBuffer("foo.go") env.AfterChange(NoLogMatching(protocol.Info, "packages=0")) }) @@ -1118,7 +1127,7 @@ func main() { var x int } `)) - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("main.go", "x")), ) }) @@ -1138,8 +1147,11 @@ func main() {} ` Run(t, pkgDefault, func(t *testing.T, env *Env) { env.OpenFile("main.go") - env.Await( - Diagnostics(env.AtRegexp("main.go", "default"), WithMessage("expected 'IDENT'")), + env.AfterChange( + Diagnostics( + env.AtRegexp("main.go", "default"), + WithMessage("expected 'IDENT'"), + ), ) }) } @@ -1284,11 +1296,11 @@ func _() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a/a.go", "x")), ) env.OpenFile("a/a_exclude.go") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a/a_exclude.go", "package (a)")), ) }) @@ -1453,7 +1465,7 @@ package main ` Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - env.Await( + env.AfterChange( OutstandingWork(lsp.WorkspaceLoadFailure, "unknown directive"), ) env.EditBuffer("go.mod", fake.NewEdit(0, 0, 3, 0, `module mod.com @@ -1463,12 +1475,12 @@ go 1.hello // As of golang/go#42529, go.mod changes do not reload the workspace until // they are saved. env.SaveBufferWithoutActions("go.mod") - env.Await( + env.AfterChange( OutstandingWork(lsp.WorkspaceLoadFailure, "invalid go version"), ) env.RegexpReplace("go.mod", "go 1.hello", "go 1.12") env.SaveBufferWithoutActions("go.mod") - env.Await( + env.AfterChange( NoOutstandingWork(), ) }) @@ -1541,7 +1553,8 @@ package c import _ "mod.com/triple/a" ` Run(t, mod, func(t *testing.T, env *Env) { - env.Await( + env.OnceMet( + InitialWorkspaceLoad, Diagnostics(env.AtRegexp("self/self.go", `_ "mod.com/self"`), WithMessage("import cycle not allowed")), Diagnostics(env.AtRegexp("double/a/a.go", `_ "mod.com/double/b"`), WithMessage("import cycle not allowed")), Diagnostics(env.AtRegexp("triple/a/a.go", `_ "mod.com/triple/b"`), WithMessage("import cycle not allowed")), @@ -1587,7 +1600,7 @@ const B = a.B ) env.RegexpReplace("b/b.go", `const B = a\.B`, "") env.SaveBuffer("b/b.go") - env.Await( + env.AfterChange( NoDiagnostics(ForFile("a/a.go")), NoDiagnostics(ForFile("b/b.go")), ) @@ -1609,7 +1622,8 @@ import ( ` t.Run("module", func(t *testing.T) { Run(t, mod, func(t *testing.T, env *Env) { - env.Await( + env.OnceMet( + InitialWorkspaceLoad, Diagnostics(env.AtRegexp("main.go", `"nosuchpkg"`), WithMessage(`could not import nosuchpkg (no required module provides package "nosuchpkg"`)), ) }) @@ -1620,7 +1634,8 @@ import ( EnvVars{"GO111MODULE": "off"}, Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { - env.Await( + env.OnceMet( + InitialWorkspaceLoad, Diagnostics(env.AtRegexp("main.go", `"nosuchpkg"`), WithMessage(`cannot find package "nosuchpkg" in any of`)), ) }) @@ -1749,7 +1764,7 @@ package main Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OpenFile("other.go") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("main.go", "asdf")), Diagnostics(env.AtRegexp("main.go", "fdas")), ) diff --git a/gopls/internal/regtest/diagnostics/undeclared_test.go b/gopls/internal/regtest/diagnostics/undeclared_test.go index a6289c9745a..ac5f598cc48 100644 --- a/gopls/internal/regtest/diagnostics/undeclared_test.go +++ b/gopls/internal/regtest/diagnostics/undeclared_test.go @@ -49,7 +49,6 @@ func _() int { Diagnostics(env.AtRegexp("a/a.go", "x")), ReadDiagnostics("a/a.go", &adiags), ) - env.Await() if got := len(adiags.Diagnostics); got != 1 { t.Errorf("len(Diagnostics) = %d, want 1", got) } diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index aad8360b931..9ab456d64c4 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -69,14 +69,14 @@ const a = 2 Run(t, badPackageDup, func(t *testing.T, env *Env) { env.OpenFile("b.go") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("b.go", `a = 2`), WithMessage("a redeclared")), Diagnostics(env.AtRegexp("a.go", `a = 1`), WithMessage("other declaration")), ) // Fix the error by editing the const name in b.go to `b`. env.RegexpReplace("b.go", "(a) = 2", "b") - env.Await( + env.AfterChange( NoDiagnostics(ForFile("a.go")), NoDiagnostics(ForFile("b.go")), ) diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go index 69a28e23132..85dd9a732bb 100644 --- a/gopls/internal/regtest/misc/generate_test.go +++ b/gopls/internal/regtest/misc/generate_test.go @@ -59,7 +59,8 @@ func main() { ` Run(t, generatedWorkspace, func(t *testing.T, env *Env) { - env.Await( + env.OnceMet( + InitialWorkspaceLoad, Diagnostics(env.AtRegexp("main.go", "lib1.(Answer)")), ) env.RunGenerate("./lib1") diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index de44167c291..eaa4c7e2fc0 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -54,8 +54,8 @@ func main() { env2.RegexpReplace("main.go", "\\)\n(})", "") // Now check that we got different diagnostics in each environment. - env1.Await(Diagnostics(env1.AtRegexp("main.go", "Printl"))) - env2.Await(Diagnostics(env2.AtRegexp("main.go", "$"))) + env1.AfterChange(Diagnostics(env1.AtRegexp("main.go", "Printl"))) + env2.AfterChange(Diagnostics(env2.AtRegexp("main.go", "$"))) // Now close editor #2, and verify that operation in editor #1 is // unaffected. diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index d3df2155b2b..f44eefa70f9 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -101,7 +101,7 @@ func main() { // Save the buffer, which will format and organize imports. // Confirm that the go.mod file still does not change. env.SaveBuffer("a/main.go") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { @@ -124,22 +124,14 @@ func main() { // // If this proves insufficient, env.RemoveWorkspaceFile can be updated to // retry file lock errors on windows. - env.Await( - env.DoneWithOpen(), - env.DoneWithSave(), - env.DoneWithChangeWatchedFiles(), - ) + env.AfterChange() env.RemoveWorkspaceFile("a/main.go") // TODO(rfindley): awaiting here shouldn't really be necessary. We should // be consistent eventually. // // Probably this was meant to exercise a race with the change below. - env.Await( - env.DoneWithOpen(), - env.DoneWithSave(), - env.DoneWithChangeWatchedFiles(), - ) + env.AfterChange() env.WriteWorkspaceFile("a/main.go", mainContent) env.AfterChange( @@ -595,7 +587,7 @@ func main() { t.Run("bad", func(t *testing.T) { runner.Run(t, unknown, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.2")), ) env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") @@ -705,7 +697,7 @@ func main() { {"nested", WithOptions(ProxyFiles(badProxy))}, }.Run(t, module, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a/go.mod", "require example.com v1.2.3")), ) }) @@ -734,7 +726,7 @@ func main() { ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") original := env.ReadWorkspaceFile("go.mod") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), ) got := env.ReadWorkspaceFile("go.mod") @@ -792,8 +784,11 @@ func main() { WithOptions( ProxyFiles(workspaceProxy), ).Run(t, mod, func(t *testing.T, env *Env) { - env.Await( - Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.3"), WithMessage("is not used")), + env.AfterChange( + Diagnostics( + env.AtRegexp("a/go.mod", "example.com v1.2.3"), + WithMessage("is not used"), + ), ) }) } @@ -819,7 +814,8 @@ func main() { ProxyFiles(workspaceProxy), Settings{"buildFlags": []string{"-tags", "bob"}}, ).Run(t, mod, func(t *testing.T, env *Env) { - env.Await( + env.OnceMet( + InitialWorkspaceLoad, Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), ) }) @@ -839,7 +835,7 @@ func main() {} Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.RegexpReplace("go.mod", "module", "modul") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("go.mod", "modul")), ) }) @@ -1143,7 +1139,7 @@ func main() { ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") d := &protocol.PublishDiagnosticsParams{} - env.Await( + env.AfterChange( Diagnostics( env.AtRegexp("main.go", `"example.com/blah"`), WithMessage(`could not import example.com/blah (no required module provides package "example.com/blah")`), @@ -1151,7 +1147,7 @@ func main() { ReadDiagnostics("main.go", d), ) env.ApplyQuickFixes("main.go", d.Diagnostics) - env.Await( + env.AfterChange( NoDiagnostics(ForFile("main.go")), NoDiagnostics(ForFile("go.mod")), ) diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index 908648396ef..edb479a9cf2 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -89,9 +89,9 @@ func _() { ` Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - env.Await(env.DoneWithOpen()) + env.AfterChange() env.WriteWorkspaceFile("b/b.go", `package b; func B() {};`) - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a/a.go", "b.B")), ) }) @@ -167,9 +167,9 @@ func _() { ` Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - env.Await(env.DoneWithOpen()) + env.AfterChange() env.RemoveWorkspaceFile("b/b.go") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a/a.go", "\"mod.com/b\"")), ) }) @@ -419,7 +419,7 @@ package a env.RemoveWorkspaceFile("a/a_unneeded.go") env.CloseBuffer("a/a_unneeded.go") env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("a/a.go", "fmt")), ) env.SaveBuffer("a/a.go") diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 0573455f396..3699fa823aa 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -199,8 +199,7 @@ func TestWatchReplaceTargets(t *testing.T) { replace random.org => %s `, env.ReadWorkspaceFile("pkg/go.mod"), dir) env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace) - env.Await( - env.DoneWithChangeWatchedFiles(), + env.AfterChange( UnregistrationMatching("didChangeWatchedFiles"), RegistrationMatching("didChangeWatchedFiles"), ) @@ -323,7 +322,8 @@ func main() { WithOptions( ProxyFiles(proxy), ).Run(t, multiModule, func(t *testing.T, env *Env) { - env.Await( + env.OnceMet( + InitialWorkspaceLoad, Diagnostics(env.AtRegexp("main.go", "x")), ) }) @@ -507,7 +507,7 @@ func Hello() int { ) env.RegexpReplace("modb/go.mod", "modul", "module") env.SaveBufferWithoutActions("modb/go.mod") - env.Await( + env.AfterChange( Diagnostics(env.AtRegexp("modb/b/b.go", "x")), ) }) From 7a7b699cd867b46efcb9864c4051e708d001c32f Mon Sep 17 00:00:00 2001 From: thepudds Date: Wed, 18 Jan 2023 16:39:23 +0000 Subject: [PATCH 650/723] go/analysis/passes/loopclosure: avoid panic in new parallel subtest check when t.Run has single argument The new Go 1.20 parallel subtest check in the loopclosure pass can panic in the rare case of t.Run with a single argument: fn := func() (string, func(t *testing.T)) { return "", nil } t.Run(fn()) A real-world example: https://github.com/go-spatial/geom/blob/3cd2f5a9a082dd4f827c9f9b69ba5d736d2dcb12/planar/planar_test.go#L118 This has been present for multiple months without seeming to be reported, so presumably this is a rare pattern. Avoid that panic by checking t.Run has an expected number of arguments. Fixes golang/go#57908 Change-Id: I5cde60edf624e16bb9f534e435bcd55e63a15647 GitHub-Last-Rev: 24e65a4a561477c6f24c2be91738451bdf912e33 GitHub-Pull-Request: golang/tools#424 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462282 Run-TryBot: thepudds Reviewed-by: Russ Cox Auto-Submit: Russ Cox TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Bryan Mills --- go/analysis/passes/loopclosure/loopclosure.go | 5 +++++ .../passes/loopclosure/testdata/src/subtests/subtest.go | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 5291d1b2cd0..ae5b4151dbe 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -303,6 +303,11 @@ func parallelSubtest(info *types.Info, call *ast.CallExpr) []ast.Stmt { return nil } + if len(call.Args) != 2 { + // Ignore calls such as t.Run(fn()). + return nil + } + lit, _ := call.Args[1].(*ast.FuncLit) if lit == nil { return nil diff --git a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go index c95fa1f0b1e..50283ec6152 100644 --- a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go +++ b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go @@ -140,6 +140,10 @@ func _(t *testing.T) { t.Parallel() println(test) // want "loop variable test captured by func literal" }) + + // Check that we do not have problems when t.Run has a single argument. + fn := func() (string, func(t *testing.T)) { return "", nil } + t.Run(fn()) } } From 9682b0d473741bd4b0b403887d16e1b7b1e09e83 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 18 Jan 2023 10:02:11 -0500 Subject: [PATCH 651/723] gopls/internal/lsp/source: delete IsInterface It differs from types.IsInterface, which is confusing. The nil check has been moved to the caller, where applicable. Change-Id: I07c5a4988b0d0f2d3cf01df7336268192cc74a34 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462439 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Alan Donovan --- gopls/internal/lsp/source/completion/completion.go | 2 +- gopls/internal/lsp/source/completion/literal.go | 6 +++--- gopls/internal/lsp/source/rename.go | 2 +- gopls/internal/lsp/source/rename_check.go | 8 ++++---- gopls/internal/lsp/source/util.go | 8 -------- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 92ef40533c0..8aace97feb5 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -2922,7 +2922,7 @@ func (c *completer) matchingTypeName(cand *candidate) bool { return true } - if !source.IsInterface(t) && typeMatches(types.NewPointer(t)) { + if !types.IsInterface(t) && typeMatches(types.NewPointer(t)) { if c.inference.typeName.compLitType { // If we are completing a composite literal type as in // "foo<>{}", to make a pointer we must prepend "&". diff --git a/gopls/internal/lsp/source/completion/literal.go b/gopls/internal/lsp/source/completion/literal.go index 2975c8fe592..870b97ab27b 100644 --- a/gopls/internal/lsp/source/completion/literal.go +++ b/gopls/internal/lsp/source/completion/literal.go @@ -129,7 +129,7 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im // Add a literal completion for a signature type that implements // an interface. For example, offer "http.HandlerFunc()" when // expected type is "http.Handler". - if source.IsInterface(expType) { + if expType != nil && types.IsInterface(expType) { c.basicLiteral(t, snip.Clone(), typeName, float64(score), addlEdits) } case *types.Basic: @@ -137,7 +137,7 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im // expected interface (e.g. named string type http.Dir // implements http.FileSystem), or are identical to our expected // type (i.e. yielding a type conversion such as "float64()"). - if source.IsInterface(expType) || types.Identical(expType, literalType) { + if expType != nil && (types.IsInterface(expType) || types.Identical(expType, literalType)) { c.basicLiteral(t, snip.Clone(), typeName, float64(score), addlEdits) } } @@ -159,7 +159,7 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im } // If prefix matches "func", client may want a function literal. - if score := c.matcher.Score("func"); !cand.hasMod(reference) && score > 0 && !source.IsInterface(expType) { + if score := c.matcher.Score("func"); !cand.hasMod(reference) && score > 0 && (expType == nil || !types.IsInterface(expType)) { switch t := literalType.Underlying().(type) { case *types.Signature: c.functionLiteral(t, float64(score)) diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index fe2439cc8fe..1d2a03b0f13 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -605,7 +605,7 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO for _, ref := range refs { if obj, ok := ref.obj.(*types.Func); ok { recv := obj.Type().(*types.Signature).Recv() - if recv != nil && IsInterface(recv.Type().Underlying()) { + if recv != nil && types.IsInterface(recv.Type().Underlying()) { r.changeMethods = true break } diff --git a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go index ce625dedd63..0285fc0c9de 100644 --- a/gopls/internal/lsp/source/rename_check.go +++ b/gopls/internal/lsp/source/rename_check.go @@ -552,7 +552,7 @@ func (r *renamer) checkMethod(from *types.Func) { // Check for conflict at point of declaration. // Check to ensure preservation of assignability requirements. R := recv(from).Type() - if IsInterface(R) { + if types.IsInterface(R) { // Abstract method // declaration @@ -569,7 +569,7 @@ func (r *renamer) checkMethod(from *types.Func) { for _, pkg := range r.packages { // Start with named interface types (better errors) for _, obj := range pkg.GetTypesInfo().Defs { - if obj, ok := obj.(*types.TypeName); ok && IsInterface(obj.Type()) { + if obj, ok := obj.(*types.TypeName); ok && types.IsInterface(obj.Type()) { f, _, _ := types.LookupFieldOrMethod( obj.Type(), false, from.Pkg(), from.Name()) if f == nil { @@ -641,7 +641,7 @@ func (r *renamer) checkMethod(from *types.Func) { // yields abstract method I.f. This can make error // messages less than obvious. // - if !IsInterface(key.RHS) { + if !types.IsInterface(key.RHS) { // The logic below was derived from checkSelections. rtosel := rmethods.Lookup(from.Pkg(), r.to) @@ -716,7 +716,7 @@ func (r *renamer) checkMethod(from *types.Func) { // for key := range r.satisfy() { // key = (lhs, rhs) where lhs is always an interface. - if IsInterface(key.RHS) { + if types.IsInterface(key.RHS) { continue } rsel := r.msets.MethodSet(key.RHS).Lookup(from.Pkg(), from.Name()) diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 57b14646c50..ec4c6bdbea3 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -164,14 +164,6 @@ func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) { return nil, -1 } -// IsInterface returns if a types.Type is an interface -// -// This function accepts nil, unlike types.IsInterface. -// TODO(adonovan): that's confusing! Eliminate this wrapper. -func IsInterface(T types.Type) bool { - return T != nil && types.IsInterface(T) -} - // FormatNode returns the "pretty-print" output for an ast node. func FormatNode(fset *token.FileSet, n ast.Node) string { var buf strings.Builder From 561a9be6790a1ca809a22901771203c0d0f438ef Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 18 Jan 2023 14:43:08 -0500 Subject: [PATCH 652/723] gopls/internal/lsp/filecache: actually delete files This change fixes an embarrassing blunder: the filename we gave to os.Rename was absolutized twice (goplsDir+goplsDir+path), so of course it was not found. The error was rightly ignored, but this meant the bug was undetected. CI builder machines filled their disks. Also, this change causes filecache's GC to delete files older than maxAge as soon as it encounters them, instead of in the second pass over the sorted list of all files in the cache. This should allow short-lived processes (e.g. tests) to make progress on garbage collection. Though this now seems like a distinctly third-order effect compared to... not deleting files at all. Also: - don't delay between stats after deleting files based on age. - reduce the statDelay to 100us (was 1ms). Scanning a file tree on macOS is already very slow, at least on my Google-issued machine. - reduce maxAge to 5 days (was 7), which should still tide most users over a long weekend. Fixes golang/go#57900 Change-Id: I053f2891d6c52c94f4d5dd18903280dff2282eab Reviewed-on: https://go-review.googlesource.com/c/tools/+/462597 Reviewed-by: Bryan Mills TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan --- gopls/internal/lsp/filecache/filecache.go | 55 ++++++++++++++++------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go index 12ba1ea5221..f5f1da6a8ca 100644 --- a/gopls/internal/lsp/filecache/filecache.go +++ b/gopls/internal/lsp/filecache/filecache.go @@ -243,9 +243,22 @@ func hashExecutable() (hash [32]byte, err error) { // process, possibly running a different version of gopls, possibly // running concurrently. func gc(goplsDir string) { - const period = 1 * time.Minute // period between collections - const statDelay = 1 * time.Millisecond // delay between stats to smooth out I/O - const maxAge = 7 * 24 * time.Hour // max time since last access before file is deleted + const period = 1 * time.Minute // period between collections + const statDelay = 100 * time.Microsecond // delay between stats to smooth out I/O + const maxAge = 5 * 24 * time.Hour // max time since last access before file is deleted + + // The macOS filesystem is strikingly slow, at least on some machines. + // /usr/bin/find achieves only about 25,000 stats per second + // at full speed (no pause between items), meaning a large + // cache may take several minutes to scan. + // We must ensure that short-lived processes (crucially, + // tests) are able to make progress sweeping garbage. + // + // (gopls' caches should never actually get this big in + // practise: the example mentioned above resulted from a bug + // that caused filecache to fail to delete any files.) + + const debug = false for { // Enumerate all files in the cache. @@ -259,9 +272,21 @@ func gc(goplsDir string) { // TODO(adonovan): opt: also collect empty directories, // as they typically occupy around 1KB. if err == nil && !stat.IsDir() { - files = append(files, item{path, stat}) - total += stat.Size() - time.Sleep(statDelay) + // Unconditionally delete files we haven't used in ages. + // (We do this here, not in the second loop, so that we + // perform age-based collection even in short-lived processes.) + age := time.Since(stat.ModTime()) + if age > maxAge { + if debug { + log.Printf("age: deleting stale file %s (%dB, age %v)", + path, stat.Size(), age) + } + os.Remove(path) // ignore error + } else { + files = append(files, item{path, stat}) + total += stat.Size() + time.Sleep(statDelay) + } } return nil }) @@ -272,18 +297,18 @@ func gc(goplsDir string) { }) // Delete oldest files until we're under budget. - // Unconditionally delete files we haven't used in ages. budget := atomic.LoadInt64(&budget) for _, file := range files { - age := time.Since(file.stat.ModTime()) - if total > budget || age > maxAge { - if false { // debugging - log.Printf("deleting stale file %s (%dB, age %v)", - file.path, file.stat.Size(), age) - } - os.Remove(filepath.Join(goplsDir, file.path)) // ignore error - total -= file.stat.Size() + if total < budget { + break + } + if debug { + age := time.Since(file.stat.ModTime()) + log.Printf("budget: deleting stale file %s (%dB, age %v)", + file.path, file.stat.Size(), age) } + os.Remove(file.path) // ignore error + total -= file.stat.Size() } time.Sleep(period) From bb7c440efb2b3f94e9c598584cdc94a5f4e5c909 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 12 Jan 2023 15:26:01 -0500 Subject: [PATCH 653/723] gopls/internal/lsp/filecache: use file locking, not rename After various dismal experiments, I'm not optimistic that Windows provides any means to rename a file atomically. This change causes filecache to use file locking, on all platforms, to ensure that cache entries are updated atomically. The implementation of locking was plundered wholesale from $GOROOT/src/cmd/go/internal/lockedfile, modified only as needed to get it to compile outside GOROOT. The change causes the (new) BenchmarkUncontendedGet to increase to 32us, up from 16us, on macOS. Probably still good enough, and preferable to two different implementations. Fixes golang/go#57747 Change-Id: Icd373e99481789056d70c02cbcd554bd59eb3101 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461800 TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan Reviewed-by: Bryan Mills gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley --- gopls/internal/lsp/filecache/filecache.go | 65 ++--- .../internal/lsp/filecache/filecache_test.go | 26 ++ .../lockedfile/internal/filelock/filelock.go | 99 +++++++ .../internal/filelock/filelock_fcntl.go | 215 ++++++++++++++ .../internal/filelock/filelock_other.go | 37 +++ .../internal/filelock/filelock_plan9.go | 37 +++ .../internal/filelock/filelock_test.go | 209 ++++++++++++++ .../internal/filelock/filelock_unix.go | 45 +++ .../internal/filelock/filelock_windows.go | 67 +++++ internal/lockedfile/lockedfile.go | 187 ++++++++++++ internal/lockedfile/lockedfile_filelock.go | 66 +++++ internal/lockedfile/lockedfile_plan9.go | 95 ++++++ internal/lockedfile/lockedfile_test.go | 270 ++++++++++++++++++ internal/lockedfile/mutex.go | 67 +++++ internal/lockedfile/transform_test.go | 106 +++++++ 15 files changed, 1554 insertions(+), 37 deletions(-) create mode 100644 internal/lockedfile/internal/filelock/filelock.go create mode 100644 internal/lockedfile/internal/filelock/filelock_fcntl.go create mode 100644 internal/lockedfile/internal/filelock/filelock_other.go create mode 100644 internal/lockedfile/internal/filelock/filelock_plan9.go create mode 100644 internal/lockedfile/internal/filelock/filelock_test.go create mode 100644 internal/lockedfile/internal/filelock/filelock_unix.go create mode 100644 internal/lockedfile/internal/filelock/filelock_windows.go create mode 100644 internal/lockedfile/lockedfile.go create mode 100644 internal/lockedfile/lockedfile_filelock.go create mode 100644 internal/lockedfile/lockedfile_plan9.go create mode 100644 internal/lockedfile/lockedfile_test.go create mode 100644 internal/lockedfile/mutex.go create mode 100644 internal/lockedfile/transform_test.go diff --git a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go index f5f1da6a8ca..44485cb80ac 100644 --- a/gopls/internal/lsp/filecache/filecache.go +++ b/gopls/internal/lsp/filecache/filecache.go @@ -18,12 +18,13 @@ package filecache import ( + "bytes" "crypto/sha256" + "encoding/binary" "errors" "fmt" "io" "log" - "math/rand" "os" "path/filepath" "sort" @@ -31,7 +32,7 @@ import ( "sync/atomic" "time" - "golang.org/x/tools/internal/robustio" + "golang.org/x/tools/internal/lockedfile" ) // Get retrieves from the cache and returns a newly allocated @@ -40,7 +41,7 @@ import ( // Get returns ErrNotFound if the value was not found. func Get(kind string, key [32]byte) ([]byte, error) { name := filename(kind, key) - data, err := robustio.ReadFile(name) + data, err := lockedfile.Read(name) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil, ErrNotFound @@ -48,6 +49,16 @@ func Get(kind string, key [32]byte) ([]byte, error) { return nil, err } + // Verify that the Write was complete + // by checking the recorded length. + if len(data) < 8 { + return nil, ErrNotFound // cache entry is incomplete + } + if length := binary.LittleEndian.Uint64(data); int(length) != len(data)-8 { + return nil, ErrNotFound // cache entry is incomplete (or too long!) + } + data = data[8:] + // Update file time for use by LRU eviction. // (This turns every read into a write operation. // If this is a performance problem, we should @@ -76,41 +87,21 @@ func Set(kind string, key [32]byte, value []byte) error { return err } - // The sequence below uses rename to achieve atomic cache - // updates even with concurrent processes. - var cause error - for try := 0; try < 3; try++ { - tmpname := fmt.Sprintf("%s.tmp.%d", name, rand.Int()) - tmp, err := os.OpenFile(tmpname, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) - if err != nil { - if os.IsExist(err) { - // Create raced with another thread (or stale file). - // Try again. - cause = err - continue - } - return err - } + // In the unlikely event of a short write (e.g. ENOSPC) + // followed by process termination (e.g. a power cut), we + // don't want a reader to see a short file, so we record + // the expected length first and verify it in Get. + var length [8]byte + binary.LittleEndian.PutUint64(length[:], uint64(len(value))) + header := bytes.NewReader(length[:]) + payload := bytes.NewReader(value) - _, err = tmp.Write(value) - if closeErr := tmp.Close(); err == nil { - err = closeErr // prefer error from write over close - } - if err != nil { - os.Remove(tmp.Name()) // ignore error - return err - } - - err = robustio.Rename(tmp.Name(), name) - if err == nil { - return nil // success - } - cause = err - - // Rename raced with another thread. Try again. - os.Remove(tmp.Name()) // ignore error - } - return cause + // Windows doesn't support atomic rename--we tried MoveFile, + // MoveFileEx, ReplaceFileEx, and SetFileInformationByHandle + // of RenameFileInfo, all to no avail--so instead we use + // advisory file locking, which is only about 2x slower even + // on POSIX platforms with atomic rename. + return lockedfile.Write(name, io.MultiReader(header, payload), 0600) } var budget int64 = 1e9 // 1GB diff --git a/gopls/internal/lsp/filecache/filecache_test.go b/gopls/internal/lsp/filecache/filecache_test.go index d1947b781e6..c96cb16eb99 100644 --- a/gopls/internal/lsp/filecache/filecache_test.go +++ b/gopls/internal/lsp/filecache/filecache_test.go @@ -187,3 +187,29 @@ func uniqueKey() (key [32]byte) { } return } + +func BenchmarkUncontendedGet(b *testing.B) { + const kind = "BenchmarkUncontendedGet" + key := uniqueKey() + + var value [8192]byte + if _, err := mathrand.Read(value[:]); err != nil { + b.Fatalf("rand: %v", err) + } + if err := filecache.Set(kind, key, value[:]); err != nil { + b.Fatal(err) + } + b.ResetTimer() + + var group errgroup.Group + group.SetLimit(50) + for i := 0; i < b.N; i++ { + group.Go(func() error { + _, err := filecache.Get(kind, key) + return err + }) + } + if err := group.Wait(); err != nil { + b.Fatal(err) + } +} diff --git a/internal/lockedfile/internal/filelock/filelock.go b/internal/lockedfile/internal/filelock/filelock.go new file mode 100644 index 00000000000..05f27c321a8 --- /dev/null +++ b/internal/lockedfile/internal/filelock/filelock.go @@ -0,0 +1,99 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package filelock provides a platform-independent API for advisory file +// locking. Calls to functions in this package on platforms that do not support +// advisory locks will return errors for which IsNotSupported returns true. +package filelock + +import ( + "errors" + "io/fs" + "os" +) + +// A File provides the minimal set of methods required to lock an open file. +// File implementations must be usable as map keys. +// The usual implementation is *os.File. +type File interface { + // Name returns the name of the file. + Name() string + + // Fd returns a valid file descriptor. + // (If the File is an *os.File, it must not be closed.) + Fd() uintptr + + // Stat returns the FileInfo structure describing file. + Stat() (fs.FileInfo, error) +} + +// Lock places an advisory write lock on the file, blocking until it can be +// locked. +// +// If Lock returns nil, no other process will be able to place a read or write +// lock on the file until this process exits, closes f, or calls Unlock on it. +// +// If f's descriptor is already read- or write-locked, the behavior of Lock is +// unspecified. +// +// Closing the file may or may not release the lock promptly. Callers should +// ensure that Unlock is always called when Lock succeeds. +func Lock(f File) error { + return lock(f, writeLock) +} + +// RLock places an advisory read lock on the file, blocking until it can be locked. +// +// If RLock returns nil, no other process will be able to place a write lock on +// the file until this process exits, closes f, or calls Unlock on it. +// +// If f is already read- or write-locked, the behavior of RLock is unspecified. +// +// Closing the file may or may not release the lock promptly. Callers should +// ensure that Unlock is always called if RLock succeeds. +func RLock(f File) error { + return lock(f, readLock) +} + +// Unlock removes an advisory lock placed on f by this process. +// +// The caller must not attempt to unlock a file that is not locked. +func Unlock(f File) error { + return unlock(f) +} + +// String returns the name of the function corresponding to lt +// (Lock, RLock, or Unlock). +func (lt lockType) String() string { + switch lt { + case readLock: + return "RLock" + case writeLock: + return "Lock" + default: + return "Unlock" + } +} + +// IsNotSupported returns a boolean indicating whether the error is known to +// report that a function is not supported (possibly for a specific input). +// It is satisfied by ErrNotSupported as well as some syscall errors. +func IsNotSupported(err error) bool { + return isNotSupported(underlyingError(err)) +} + +var ErrNotSupported = errors.New("operation not supported") + +// underlyingError returns the underlying error for known os error types. +func underlyingError(err error) error { + switch err := err.(type) { + case *fs.PathError: + return err.Err + case *os.LinkError: + return err.Err + case *os.SyscallError: + return err.Err + } + return err +} diff --git a/internal/lockedfile/internal/filelock/filelock_fcntl.go b/internal/lockedfile/internal/filelock/filelock_fcntl.go new file mode 100644 index 00000000000..30985191072 --- /dev/null +++ b/internal/lockedfile/internal/filelock/filelock_fcntl.go @@ -0,0 +1,215 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build aix || (solaris && !illumos) +// +build aix solaris,!illumos + +// This code implements the filelock API using POSIX 'fcntl' locks, which attach +// to an (inode, process) pair rather than a file descriptor. To avoid unlocking +// files prematurely when the same file is opened through different descriptors, +// we allow only one read-lock at a time. +// +// Most platforms provide some alternative API, such as an 'flock' system call +// or an F_OFD_SETLK command for 'fcntl', that allows for better concurrency and +// does not require per-inode bookkeeping in the application. + +package filelock + +import ( + "errors" + "io" + "io/fs" + "math/rand" + "sync" + "syscall" + "time" +) + +type lockType int16 + +const ( + readLock lockType = syscall.F_RDLCK + writeLock lockType = syscall.F_WRLCK +) + +type inode = uint64 // type of syscall.Stat_t.Ino + +type inodeLock struct { + owner File + queue []<-chan File +} + +var ( + mu sync.Mutex + inodes = map[File]inode{} + locks = map[inode]inodeLock{} +) + +func lock(f File, lt lockType) (err error) { + // POSIX locks apply per inode and process, and the lock for an inode is + // released when *any* descriptor for that inode is closed. So we need to + // synchronize access to each inode internally, and must serialize lock and + // unlock calls that refer to the same inode through different descriptors. + fi, err := f.Stat() + if err != nil { + return err + } + ino := fi.Sys().(*syscall.Stat_t).Ino + + mu.Lock() + if i, dup := inodes[f]; dup && i != ino { + mu.Unlock() + return &fs.PathError{ + Op: lt.String(), + Path: f.Name(), + Err: errors.New("inode for file changed since last Lock or RLock"), + } + } + inodes[f] = ino + + var wait chan File + l := locks[ino] + if l.owner == f { + // This file already owns the lock, but the call may change its lock type. + } else if l.owner == nil { + // No owner: it's ours now. + l.owner = f + } else { + // Already owned: add a channel to wait on. + wait = make(chan File) + l.queue = append(l.queue, wait) + } + locks[ino] = l + mu.Unlock() + + if wait != nil { + wait <- f + } + + // Spurious EDEADLK errors arise on platforms that compute deadlock graphs at + // the process, rather than thread, level. Consider processes P and Q, with + // threads P.1, P.2, and Q.3. The following trace is NOT a deadlock, but will be + // reported as a deadlock on systems that consider only process granularity: + // + // P.1 locks file A. + // Q.3 locks file B. + // Q.3 blocks on file A. + // P.2 blocks on file B. (This is erroneously reported as a deadlock.) + // P.1 unlocks file A. + // Q.3 unblocks and locks file A. + // Q.3 unlocks files A and B. + // P.2 unblocks and locks file B. + // P.2 unlocks file B. + // + // These spurious errors were observed in practice on AIX and Solaris in + // cmd/go: see https://golang.org/issue/32817. + // + // We work around this bug by treating EDEADLK as always spurious. If there + // really is a lock-ordering bug between the interacting processes, it will + // become a livelock instead, but that's not appreciably worse than if we had + // a proper flock implementation (which generally does not even attempt to + // diagnose deadlocks). + // + // In the above example, that changes the trace to: + // + // P.1 locks file A. + // Q.3 locks file B. + // Q.3 blocks on file A. + // P.2 spuriously fails to lock file B and goes to sleep. + // P.1 unlocks file A. + // Q.3 unblocks and locks file A. + // Q.3 unlocks files A and B. + // P.2 wakes up and locks file B. + // P.2 unlocks file B. + // + // We know that the retry loop will not introduce a *spurious* livelock + // because, according to the POSIX specification, EDEADLK is only to be + // returned when “the lock is blocked by a lock from another process”. + // If that process is blocked on some lock that we are holding, then the + // resulting livelock is due to a real deadlock (and would manifest as such + // when using, for example, the flock implementation of this package). + // If the other process is *not* blocked on some other lock that we are + // holding, then it will eventually release the requested lock. + + nextSleep := 1 * time.Millisecond + const maxSleep = 500 * time.Millisecond + for { + err = setlkw(f.Fd(), lt) + if err != syscall.EDEADLK { + break + } + time.Sleep(nextSleep) + + nextSleep += nextSleep + if nextSleep > maxSleep { + nextSleep = maxSleep + } + // Apply 10% jitter to avoid synchronizing collisions when we finally unblock. + nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep)) + } + + if err != nil { + unlock(f) + return &fs.PathError{ + Op: lt.String(), + Path: f.Name(), + Err: err, + } + } + + return nil +} + +func unlock(f File) error { + var owner File + + mu.Lock() + ino, ok := inodes[f] + if ok { + owner = locks[ino].owner + } + mu.Unlock() + + if owner != f { + panic("unlock called on a file that is not locked") + } + + err := setlkw(f.Fd(), syscall.F_UNLCK) + + mu.Lock() + l := locks[ino] + if len(l.queue) == 0 { + // No waiters: remove the map entry. + delete(locks, ino) + } else { + // The first waiter is sending us their file now. + // Receive it and update the queue. + l.owner = <-l.queue[0] + l.queue = l.queue[1:] + locks[ino] = l + } + delete(inodes, f) + mu.Unlock() + + return err +} + +// setlkw calls FcntlFlock with F_SETLKW for the entire file indicated by fd. +func setlkw(fd uintptr, lt lockType) error { + for { + err := syscall.FcntlFlock(fd, syscall.F_SETLKW, &syscall.Flock_t{ + Type: int16(lt), + Whence: io.SeekStart, + Start: 0, + Len: 0, // All bytes. + }) + if err != syscall.EINTR { + return err + } + } +} + +func isNotSupported(err error) bool { + return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported +} diff --git a/internal/lockedfile/internal/filelock/filelock_other.go b/internal/lockedfile/internal/filelock/filelock_other.go new file mode 100644 index 00000000000..1333edf7514 --- /dev/null +++ b/internal/lockedfile/internal/filelock/filelock_other.go @@ -0,0 +1,37 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !(darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd) && !plan9 && !windows +// +build !darwin,!dragonfly,!freebsd,!illumos,!linux,!netbsd,!openbsd,!plan9,!windows + +package filelock + +import "io/fs" + +type lockType int8 + +const ( + readLock = iota + 1 + writeLock +) + +func lock(f File, lt lockType) error { + return &fs.PathError{ + Op: lt.String(), + Path: f.Name(), + Err: ErrNotSupported, + } +} + +func unlock(f File) error { + return &fs.PathError{ + Op: "Unlock", + Path: f.Name(), + Err: ErrNotSupported, + } +} + +func isNotSupported(err error) bool { + return err == ErrNotSupported +} diff --git a/internal/lockedfile/internal/filelock/filelock_plan9.go b/internal/lockedfile/internal/filelock/filelock_plan9.go new file mode 100644 index 00000000000..908afb6c8cb --- /dev/null +++ b/internal/lockedfile/internal/filelock/filelock_plan9.go @@ -0,0 +1,37 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build plan9 +// +build plan9 + +package filelock + +import "io/fs" + +type lockType int8 + +const ( + readLock = iota + 1 + writeLock +) + +func lock(f File, lt lockType) error { + return &fs.PathError{ + Op: lt.String(), + Path: f.Name(), + Err: ErrNotSupported, + } +} + +func unlock(f File) error { + return &fs.PathError{ + Op: "Unlock", + Path: f.Name(), + Err: ErrNotSupported, + } +} + +func isNotSupported(err error) bool { + return err == ErrNotSupported +} diff --git a/internal/lockedfile/internal/filelock/filelock_test.go b/internal/lockedfile/internal/filelock/filelock_test.go new file mode 100644 index 00000000000..224feda93e3 --- /dev/null +++ b/internal/lockedfile/internal/filelock/filelock_test.go @@ -0,0 +1,209 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !js && !plan9 +// +build !js,!plan9 + +package filelock_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + "time" + + "golang.org/x/tools/internal/lockedfile/internal/filelock" +) + +func lock(t *testing.T, f *os.File) { + t.Helper() + err := filelock.Lock(f) + t.Logf("Lock(fd %d) = %v", f.Fd(), err) + if err != nil { + t.Fail() + } +} + +func rLock(t *testing.T, f *os.File) { + t.Helper() + err := filelock.RLock(f) + t.Logf("RLock(fd %d) = %v", f.Fd(), err) + if err != nil { + t.Fail() + } +} + +func unlock(t *testing.T, f *os.File) { + t.Helper() + err := filelock.Unlock(f) + t.Logf("Unlock(fd %d) = %v", f.Fd(), err) + if err != nil { + t.Fail() + } +} + +func mustTempFile(t *testing.T) (f *os.File, remove func()) { + t.Helper() + + base := filepath.Base(t.Name()) + f, err := os.CreateTemp("", base) + if err != nil { + t.Fatalf(`os.CreateTemp("", %q) = %v`, base, err) + } + t.Logf("fd %d = %s", f.Fd(), f.Name()) + + return f, func() { + f.Close() + os.Remove(f.Name()) + } +} + +func mustOpen(t *testing.T, name string) *os.File { + t.Helper() + + f, err := os.OpenFile(name, os.O_RDWR, 0) + if err != nil { + t.Fatalf("os.Open(%q) = %v", name, err) + } + + t.Logf("fd %d = os.Open(%q)", f.Fd(), name) + return f +} + +const ( + quiescent = 10 * time.Millisecond + probablyStillBlocked = 10 * time.Second +) + +func mustBlock(t *testing.T, op string, f *os.File) (wait func(*testing.T)) { + t.Helper() + + desc := fmt.Sprintf("%s(fd %d)", op, f.Fd()) + + done := make(chan struct{}) + go func() { + t.Helper() + switch op { + case "Lock": + lock(t, f) + case "RLock": + rLock(t, f) + default: + panic("invalid op: " + op) + } + close(done) + }() + + select { + case <-done: + t.Fatalf("%s unexpectedly did not block", desc) + return nil + + case <-time.After(quiescent): + t.Logf("%s is blocked (as expected)", desc) + return func(t *testing.T) { + t.Helper() + select { + case <-time.After(probablyStillBlocked): + t.Fatalf("%s is unexpectedly still blocked", desc) + case <-done: + } + } + } +} + +func TestLockExcludesLock(t *testing.T) { + t.Parallel() + + f, remove := mustTempFile(t) + defer remove() + + other := mustOpen(t, f.Name()) + defer other.Close() + + lock(t, f) + lockOther := mustBlock(t, "Lock", other) + unlock(t, f) + lockOther(t) + unlock(t, other) +} + +func TestLockExcludesRLock(t *testing.T) { + t.Parallel() + + f, remove := mustTempFile(t) + defer remove() + + other := mustOpen(t, f.Name()) + defer other.Close() + + lock(t, f) + rLockOther := mustBlock(t, "RLock", other) + unlock(t, f) + rLockOther(t) + unlock(t, other) +} + +func TestRLockExcludesOnlyLock(t *testing.T) { + t.Parallel() + + f, remove := mustTempFile(t) + defer remove() + rLock(t, f) + + f2 := mustOpen(t, f.Name()) + defer f2.Close() + + doUnlockTF := false + switch runtime.GOOS { + case "aix", "solaris": + // When using POSIX locks (as on Solaris), we can't safely read-lock the + // same inode through two different descriptors at the same time: when the + // first descriptor is closed, the second descriptor would still be open but + // silently unlocked. So a second RLock must block instead of proceeding. + lockF2 := mustBlock(t, "RLock", f2) + unlock(t, f) + lockF2(t) + default: + rLock(t, f2) + doUnlockTF = true + } + + other := mustOpen(t, f.Name()) + defer other.Close() + lockOther := mustBlock(t, "Lock", other) + + unlock(t, f2) + if doUnlockTF { + unlock(t, f) + } + lockOther(t) + unlock(t, other) +} + +func TestLockNotDroppedByExecCommand(t *testing.T) { + f, remove := mustTempFile(t) + defer remove() + + lock(t, f) + + other := mustOpen(t, f.Name()) + defer other.Close() + + // Some kinds of file locks are dropped when a duplicated or forked file + // descriptor is unlocked. Double-check that the approach used by os/exec does + // not accidentally drop locks. + cmd := exec.Command(os.Args[0], "-test.run=^$") + if err := cmd.Run(); err != nil { + t.Fatalf("exec failed: %v", err) + } + + lockOther := mustBlock(t, "Lock", other) + unlock(t, f) + lockOther(t) + unlock(t, other) +} diff --git a/internal/lockedfile/internal/filelock/filelock_unix.go b/internal/lockedfile/internal/filelock/filelock_unix.go new file mode 100644 index 00000000000..878a1e770d4 --- /dev/null +++ b/internal/lockedfile/internal/filelock/filelock_unix.go @@ -0,0 +1,45 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd +// +build darwin dragonfly freebsd illumos linux netbsd openbsd + +package filelock + +import ( + "io/fs" + "syscall" +) + +type lockType int16 + +const ( + readLock lockType = syscall.LOCK_SH + writeLock lockType = syscall.LOCK_EX +) + +func lock(f File, lt lockType) (err error) { + for { + err = syscall.Flock(int(f.Fd()), int(lt)) + if err != syscall.EINTR { + break + } + } + if err != nil { + return &fs.PathError{ + Op: lt.String(), + Path: f.Name(), + Err: err, + } + } + return nil +} + +func unlock(f File) error { + return lock(f, syscall.LOCK_UN) +} + +func isNotSupported(err error) bool { + return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported +} diff --git a/internal/lockedfile/internal/filelock/filelock_windows.go b/internal/lockedfile/internal/filelock/filelock_windows.go new file mode 100644 index 00000000000..3273a818272 --- /dev/null +++ b/internal/lockedfile/internal/filelock/filelock_windows.go @@ -0,0 +1,67 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows +// +build windows + +package filelock + +import ( + "io/fs" + + "golang.org/x/sys/windows" +) + +type lockType uint32 + +const ( + readLock lockType = 0 + writeLock lockType = windows.LOCKFILE_EXCLUSIVE_LOCK +) + +const ( + reserved = 0 + allBytes = ^uint32(0) +) + +func lock(f File, lt lockType) error { + // Per https://golang.org/issue/19098, “Programs currently expect the Fd + // method to return a handle that uses ordinary synchronous I/O.” + // However, LockFileEx still requires an OVERLAPPED structure, + // which contains the file offset of the beginning of the lock range. + // We want to lock the entire file, so we leave the offset as zero. + ol := new(windows.Overlapped) + + err := windows.LockFileEx(windows.Handle(f.Fd()), uint32(lt), reserved, allBytes, allBytes, ol) + if err != nil { + return &fs.PathError{ + Op: lt.String(), + Path: f.Name(), + Err: err, + } + } + return nil +} + +func unlock(f File) error { + ol := new(windows.Overlapped) + err := windows.UnlockFileEx(windows.Handle(f.Fd()), reserved, allBytes, allBytes, ol) + if err != nil { + return &fs.PathError{ + Op: "Unlock", + Path: f.Name(), + Err: err, + } + } + return nil +} + +func isNotSupported(err error) bool { + switch err { + case windows.ERROR_NOT_SUPPORTED, windows.ERROR_CALL_NOT_IMPLEMENTED, ErrNotSupported: + return true + default: + return false + } +} diff --git a/internal/lockedfile/lockedfile.go b/internal/lockedfile/lockedfile.go new file mode 100644 index 00000000000..82e1a89675e --- /dev/null +++ b/internal/lockedfile/lockedfile.go @@ -0,0 +1,187 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package lockedfile creates and manipulates files whose contents should only +// change atomically. +package lockedfile + +import ( + "fmt" + "io" + "io/fs" + "os" + "runtime" +) + +// A File is a locked *os.File. +// +// Closing the file releases the lock. +// +// If the program exits while a file is locked, the operating system releases +// the lock but may not do so promptly: callers must ensure that all locked +// files are closed before exiting. +type File struct { + osFile + closed bool +} + +// osFile embeds a *os.File while keeping the pointer itself unexported. +// (When we close a File, it must be the same file descriptor that we opened!) +type osFile struct { + *os.File +} + +// OpenFile is like os.OpenFile, but returns a locked file. +// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked; +// otherwise, it is read-locked. +func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) { + var ( + f = new(File) + err error + ) + f.osFile.File, err = openFile(name, flag, perm) + if err != nil { + return nil, err + } + + // Although the operating system will drop locks for open files when the go + // command exits, we want to hold locks for as little time as possible, and we + // especially don't want to leave a file locked after we're done with it. Our + // Close method is what releases the locks, so use a finalizer to report + // missing Close calls on a best-effort basis. + runtime.SetFinalizer(f, func(f *File) { + panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", f.Name())) + }) + + return f, nil +} + +// Open is like os.Open, but returns a read-locked file. +func Open(name string) (*File, error) { + return OpenFile(name, os.O_RDONLY, 0) +} + +// Create is like os.Create, but returns a write-locked file. +func Create(name string) (*File, error) { + return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) +} + +// Edit creates the named file with mode 0666 (before umask), +// but does not truncate existing contents. +// +// If Edit succeeds, methods on the returned File can be used for I/O. +// The associated file descriptor has mode O_RDWR and the file is write-locked. +func Edit(name string) (*File, error) { + return OpenFile(name, os.O_RDWR|os.O_CREATE, 0666) +} + +// Close unlocks and closes the underlying file. +// +// Close may be called multiple times; all calls after the first will return a +// non-nil error. +func (f *File) Close() error { + if f.closed { + return &fs.PathError{ + Op: "close", + Path: f.Name(), + Err: fs.ErrClosed, + } + } + f.closed = true + + err := closeFile(f.osFile.File) + runtime.SetFinalizer(f, nil) + return err +} + +// Read opens the named file with a read-lock and returns its contents. +func Read(name string) ([]byte, error) { + f, err := Open(name) + if err != nil { + return nil, err + } + defer f.Close() + + return io.ReadAll(f) +} + +// Write opens the named file (creating it with the given permissions if needed), +// then write-locks it and overwrites it with the given content. +func Write(name string, content io.Reader, perm fs.FileMode) (err error) { + f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + return err + } + + _, err = io.Copy(f, content) + if closeErr := f.Close(); err == nil { + err = closeErr + } + return err +} + +// Transform invokes t with the result of reading the named file, with its lock +// still held. +// +// If t returns a nil error, Transform then writes the returned contents back to +// the file, making a best effort to preserve existing contents on error. +// +// t must not modify the slice passed to it. +func Transform(name string, t func([]byte) ([]byte, error)) (err error) { + f, err := Edit(name) + if err != nil { + return err + } + defer f.Close() + + old, err := io.ReadAll(f) + if err != nil { + return err + } + + new, err := t(old) + if err != nil { + return err + } + + if len(new) > len(old) { + // The overall file size is increasing, so write the tail first: if we're + // about to run out of space on the disk, we would rather detect that + // failure before we have overwritten the original contents. + if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil { + // Make a best effort to remove the incomplete tail. + f.Truncate(int64(len(old))) + return err + } + } + + // We're about to overwrite the old contents. In case of failure, make a best + // effort to roll back before we close the file. + defer func() { + if err != nil { + if _, err := f.WriteAt(old, 0); err == nil { + f.Truncate(int64(len(old))) + } + } + }() + + if len(new) >= len(old) { + if _, err := f.WriteAt(new[:len(old)], 0); err != nil { + return err + } + } else { + if _, err := f.WriteAt(new, 0); err != nil { + return err + } + // The overall file size is decreasing, so shrink the file to its final size + // after writing. We do this after writing (instead of before) so that if + // the write fails, enough filesystem space will likely still be reserved + // to contain the previous contents. + if err := f.Truncate(int64(len(new))); err != nil { + return err + } + } + + return nil +} diff --git a/internal/lockedfile/lockedfile_filelock.go b/internal/lockedfile/lockedfile_filelock.go new file mode 100644 index 00000000000..7c71672c811 --- /dev/null +++ b/internal/lockedfile/lockedfile_filelock.go @@ -0,0 +1,66 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !plan9 +// +build !plan9 + +package lockedfile + +import ( + "io/fs" + "os" + + "golang.org/x/tools/internal/lockedfile/internal/filelock" +) + +func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) { + // On BSD systems, we could add the O_SHLOCK or O_EXLOCK flag to the OpenFile + // call instead of locking separately, but we have to support separate locking + // calls for Linux and Windows anyway, so it's simpler to use that approach + // consistently. + + f, err := os.OpenFile(name, flag&^os.O_TRUNC, perm) + if err != nil { + return nil, err + } + + switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) { + case os.O_WRONLY, os.O_RDWR: + err = filelock.Lock(f) + default: + err = filelock.RLock(f) + } + if err != nil { + f.Close() + return nil, err + } + + if flag&os.O_TRUNC == os.O_TRUNC { + if err := f.Truncate(0); err != nil { + // The documentation for os.O_TRUNC says “if possible, truncate file when + // opened”, but doesn't define “possible” (golang.org/issue/28699). + // We'll treat regular files (and symlinks to regular files) as “possible” + // and ignore errors for the rest. + if fi, statErr := f.Stat(); statErr != nil || fi.Mode().IsRegular() { + filelock.Unlock(f) + f.Close() + return nil, err + } + } + } + + return f, nil +} + +func closeFile(f *os.File) error { + // Since locking syscalls operate on file descriptors, we must unlock the file + // while the descriptor is still valid — that is, before the file is closed — + // and avoid unlocking files that are already closed. + err := filelock.Unlock(f) + + if closeErr := f.Close(); err == nil { + err = closeErr + } + return err +} diff --git a/internal/lockedfile/lockedfile_plan9.go b/internal/lockedfile/lockedfile_plan9.go new file mode 100644 index 00000000000..40871e610cd --- /dev/null +++ b/internal/lockedfile/lockedfile_plan9.go @@ -0,0 +1,95 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build plan9 +// +build plan9 + +package lockedfile + +import ( + "io/fs" + "math/rand" + "os" + "strings" + "time" +) + +// Opening an exclusive-use file returns an error. +// The expected error strings are: +// +// - "open/create -- file is locked" (cwfs, kfs) +// - "exclusive lock" (fossil) +// - "exclusive use file already open" (ramfs) +var lockedErrStrings = [...]string{ + "file is locked", + "exclusive lock", + "exclusive use file already open", +} + +// Even though plan9 doesn't support the Lock/RLock/Unlock functions to +// manipulate already-open files, IsLocked is still meaningful: os.OpenFile +// itself may return errors that indicate that a file with the ModeExclusive bit +// set is already open. +func isLocked(err error) bool { + s := err.Error() + + for _, frag := range lockedErrStrings { + if strings.Contains(s, frag) { + return true + } + } + + return false +} + +func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) { + // Plan 9 uses a mode bit instead of explicit lock/unlock syscalls. + // + // Per http://man.cat-v.org/plan_9/5/stat: “Exclusive use files may be open + // for I/O by only one fid at a time across all clients of the server. If a + // second open is attempted, it draws an error.” + // + // So we can try to open a locked file, but if it fails we're on our own to + // figure out when it becomes available. We'll use exponential backoff with + // some jitter and an arbitrary limit of 500ms. + + // If the file was unpacked or created by some other program, it might not + // have the ModeExclusive bit set. Set it before we call OpenFile, so that we + // can be confident that a successful OpenFile implies exclusive use. + if fi, err := os.Stat(name); err == nil { + if fi.Mode()&fs.ModeExclusive == 0 { + if err := os.Chmod(name, fi.Mode()|fs.ModeExclusive); err != nil { + return nil, err + } + } + } else if !os.IsNotExist(err) { + return nil, err + } + + nextSleep := 1 * time.Millisecond + const maxSleep = 500 * time.Millisecond + for { + f, err := os.OpenFile(name, flag, perm|fs.ModeExclusive) + if err == nil { + return f, nil + } + + if !isLocked(err) { + return nil, err + } + + time.Sleep(nextSleep) + + nextSleep += nextSleep + if nextSleep > maxSleep { + nextSleep = maxSleep + } + // Apply 10% jitter to avoid synchronizing collisions. + nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep)) + } +} + +func closeFile(f *os.File) error { + return f.Close() +} diff --git a/internal/lockedfile/lockedfile_test.go b/internal/lockedfile/lockedfile_test.go new file mode 100644 index 00000000000..572178d0d32 --- /dev/null +++ b/internal/lockedfile/lockedfile_test.go @@ -0,0 +1,270 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// js does not support inter-process file locking. +// +//go:build !js +// +build !js + +package lockedfile_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "golang.org/x/tools/internal/lockedfile" +) + +func mustTempDir(t *testing.T) (dir string, remove func()) { + t.Helper() + + dir, err := os.MkdirTemp("", filepath.Base(t.Name())) + if err != nil { + t.Fatal(err) + } + return dir, func() { os.RemoveAll(dir) } +} + +const ( + quiescent = 10 * time.Millisecond + probablyStillBlocked = 10 * time.Second +) + +func mustBlock(t *testing.T, desc string, f func()) (wait func(*testing.T)) { + t.Helper() + + done := make(chan struct{}) + go func() { + f() + close(done) + }() + + select { + case <-done: + t.Fatalf("%s unexpectedly did not block", desc) + return nil + + case <-time.After(quiescent): + return func(t *testing.T) { + t.Helper() + select { + case <-time.After(probablyStillBlocked): + t.Fatalf("%s is unexpectedly still blocked after %v", desc, probablyStillBlocked) + case <-done: + } + } + } +} + +func TestMutexExcludes(t *testing.T) { + t.Parallel() + + dir, remove := mustTempDir(t) + defer remove() + + path := filepath.Join(dir, "lock") + + mu := lockedfile.MutexAt(path) + t.Logf("mu := MutexAt(_)") + + unlock, err := mu.Lock() + if err != nil { + t.Fatalf("mu.Lock: %v", err) + } + t.Logf("unlock, _ := mu.Lock()") + + mu2 := lockedfile.MutexAt(mu.Path) + t.Logf("mu2 := MutexAt(mu.Path)") + + wait := mustBlock(t, "mu2.Lock()", func() { + unlock2, err := mu2.Lock() + if err != nil { + t.Errorf("mu2.Lock: %v", err) + return + } + t.Logf("unlock2, _ := mu2.Lock()") + t.Logf("unlock2()") + unlock2() + }) + + t.Logf("unlock()") + unlock() + wait(t) +} + +func TestReadWaitsForLock(t *testing.T) { + t.Parallel() + + dir, remove := mustTempDir(t) + defer remove() + + path := filepath.Join(dir, "timestamp.txt") + + f, err := lockedfile.Create(path) + if err != nil { + t.Fatalf("Create: %v", err) + } + defer f.Close() + + const ( + part1 = "part 1\n" + part2 = "part 2\n" + ) + _, err = f.WriteString(part1) + if err != nil { + t.Fatalf("WriteString: %v", err) + } + t.Logf("WriteString(%q) = ", part1) + + wait := mustBlock(t, "Read", func() { + b, err := lockedfile.Read(path) + if err != nil { + t.Errorf("Read: %v", err) + return + } + + const want = part1 + part2 + got := string(b) + if got == want { + t.Logf("Read(_) = %q", got) + } else { + t.Errorf("Read(_) = %q, _; want %q", got, want) + } + }) + + _, err = f.WriteString(part2) + if err != nil { + t.Errorf("WriteString: %v", err) + } else { + t.Logf("WriteString(%q) = ", part2) + } + f.Close() + + wait(t) +} + +func TestCanLockExistingFile(t *testing.T) { + t.Parallel() + + dir, remove := mustTempDir(t) + defer remove() + path := filepath.Join(dir, "existing.txt") + + if err := os.WriteFile(path, []byte("ok"), 0777); err != nil { + t.Fatalf("os.WriteFile: %v", err) + } + + f, err := lockedfile.Edit(path) + if err != nil { + t.Fatalf("first Edit: %v", err) + } + + wait := mustBlock(t, "Edit", func() { + other, err := lockedfile.Edit(path) + if err != nil { + t.Errorf("second Edit: %v", err) + } + other.Close() + }) + + f.Close() + wait(t) +} + +// TestSpuriousEDEADLK verifies that the spurious EDEADLK reported in +// https://golang.org/issue/32817 no longer occurs. +func TestSpuriousEDEADLK(t *testing.T) { + // P.1 locks file A. + // Q.3 locks file B. + // Q.3 blocks on file A. + // P.2 blocks on file B. (Spurious EDEADLK occurs here.) + // P.1 unlocks file A. + // Q.3 unblocks and locks file A. + // Q.3 unlocks files A and B. + // P.2 unblocks and locks file B. + // P.2 unlocks file B. + + dirVar := t.Name() + "DIR" + + if dir := os.Getenv(dirVar); dir != "" { + // Q.3 locks file B. + b, err := lockedfile.Edit(filepath.Join(dir, "B")) + if err != nil { + t.Fatal(err) + } + defer b.Close() + + if err := os.WriteFile(filepath.Join(dir, "locked"), []byte("ok"), 0666); err != nil { + t.Fatal(err) + } + + // Q.3 blocks on file A. + a, err := lockedfile.Edit(filepath.Join(dir, "A")) + // Q.3 unblocks and locks file A. + if err != nil { + t.Fatal(err) + } + defer a.Close() + + // Q.3 unlocks files A and B. + return + } + + dir, remove := mustTempDir(t) + defer remove() + + // P.1 locks file A. + a, err := lockedfile.Edit(filepath.Join(dir, "A")) + if err != nil { + t.Fatal(err) + } + + cmd := exec.Command(os.Args[0], "-test.run="+t.Name()) + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", dirVar, dir)) + + qDone := make(chan struct{}) + waitQ := mustBlock(t, "Edit A and B in subprocess", func() { + out, err := cmd.CombinedOutput() + if err != nil { + t.Errorf("%v:\n%s", err, out) + } + close(qDone) + }) + + // Wait until process Q has either failed or locked file B. + // Otherwise, P.2 might not block on file B as intended. +locked: + for { + if _, err := os.Stat(filepath.Join(dir, "locked")); !os.IsNotExist(err) { + break locked + } + select { + case <-qDone: + break locked + case <-time.After(1 * time.Millisecond): + } + } + + waitP2 := mustBlock(t, "Edit B", func() { + // P.2 blocks on file B. (Spurious EDEADLK occurs here.) + b, err := lockedfile.Edit(filepath.Join(dir, "B")) + // P.2 unblocks and locks file B. + if err != nil { + t.Error(err) + return + } + // P.2 unlocks file B. + b.Close() + }) + + // P.1 unlocks file A. + a.Close() + + waitQ(t) + waitP2(t) +} diff --git a/internal/lockedfile/mutex.go b/internal/lockedfile/mutex.go new file mode 100644 index 00000000000..180a36c6201 --- /dev/null +++ b/internal/lockedfile/mutex.go @@ -0,0 +1,67 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lockedfile + +import ( + "fmt" + "os" + "sync" +) + +// A Mutex provides mutual exclusion within and across processes by locking a +// well-known file. Such a file generally guards some other part of the +// filesystem: for example, a Mutex file in a directory might guard access to +// the entire tree rooted in that directory. +// +// Mutex does not implement sync.Locker: unlike a sync.Mutex, a lockedfile.Mutex +// can fail to lock (e.g. if there is a permission error in the filesystem). +// +// Like a sync.Mutex, a Mutex may be included as a field of a larger struct but +// must not be copied after first use. The Path field must be set before first +// use and must not be change thereafter. +type Mutex struct { + Path string // The path to the well-known lock file. Must be non-empty. + mu sync.Mutex // A redundant mutex. The race detector doesn't know about file locking, so in tests we may need to lock something that it understands. +} + +// MutexAt returns a new Mutex with Path set to the given non-empty path. +func MutexAt(path string) *Mutex { + if path == "" { + panic("lockedfile.MutexAt: path must be non-empty") + } + return &Mutex{Path: path} +} + +func (mu *Mutex) String() string { + return fmt.Sprintf("lockedfile.Mutex(%s)", mu.Path) +} + +// Lock attempts to lock the Mutex. +// +// If successful, Lock returns a non-nil unlock function: it is provided as a +// return-value instead of a separate method to remind the caller to check the +// accompanying error. (See https://golang.org/issue/20803.) +func (mu *Mutex) Lock() (unlock func(), err error) { + if mu.Path == "" { + panic("lockedfile.Mutex: missing Path during Lock") + } + + // We could use either O_RDWR or O_WRONLY here. If we choose O_RDWR and the + // file at mu.Path is write-only, the call to OpenFile will fail with a + // permission error. That's actually what we want: if we add an RLock method + // in the future, it should call OpenFile with O_RDONLY and will require the + // files must be readable, so we should not let the caller make any + // assumptions about Mutex working with write-only files. + f, err := OpenFile(mu.Path, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return nil, err + } + mu.mu.Lock() + + return func() { + mu.mu.Unlock() + f.Close() + }, nil +} diff --git a/internal/lockedfile/transform_test.go b/internal/lockedfile/transform_test.go new file mode 100644 index 00000000000..cebbf4101cb --- /dev/null +++ b/internal/lockedfile/transform_test.go @@ -0,0 +1,106 @@ +// 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. + +// js does not support inter-process file locking. +// +//go:build !js +// +build !js + +package lockedfile_test + +import ( + "bytes" + "encoding/binary" + "math/rand" + "path/filepath" + "testing" + "time" + + "golang.org/x/tools/internal/lockedfile" +) + +func isPowerOf2(x int) bool { + return x > 0 && x&(x-1) == 0 +} + +func roundDownToPowerOf2(x int) int { + if x <= 0 { + panic("nonpositive x") + } + bit := 1 + for x != bit { + x = x &^ bit + bit <<= 1 + } + return x +} + +func TestTransform(t *testing.T) { + dir, remove := mustTempDir(t) + defer remove() + path := filepath.Join(dir, "blob.bin") + + const maxChunkWords = 8 << 10 + buf := make([]byte, 2*maxChunkWords*8) + for i := uint64(0); i < 2*maxChunkWords; i++ { + binary.LittleEndian.PutUint64(buf[i*8:], i) + } + if err := lockedfile.Write(path, bytes.NewReader(buf[:8]), 0666); err != nil { + t.Fatal(err) + } + + var attempts int64 = 128 + if !testing.Short() { + attempts *= 16 + } + const parallel = 32 + + var sem = make(chan bool, parallel) + + for n := attempts; n > 0; n-- { + sem <- true + go func() { + defer func() { <-sem }() + + time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond) + chunkWords := roundDownToPowerOf2(rand.Intn(maxChunkWords) + 1) + offset := rand.Intn(chunkWords) + + err := lockedfile.Transform(path, func(data []byte) (chunk []byte, err error) { + chunk = buf[offset*8 : (offset+chunkWords)*8] + + if len(data)&^7 != len(data) { + t.Errorf("read %d bytes, but each write is an integer multiple of 8 bytes", len(data)) + return chunk, nil + } + + words := len(data) / 8 + if !isPowerOf2(words) { + t.Errorf("read %d 8-byte words, but each write is a power-of-2 number of words", words) + return chunk, nil + } + + u := binary.LittleEndian.Uint64(data) + for i := 1; i < words; i++ { + next := binary.LittleEndian.Uint64(data[i*8:]) + if next != u+1 { + t.Errorf("wrote sequential integers, but read integer out of sequence at offset %d", i) + return chunk, nil + } + u = next + } + + return chunk, nil + }) + + if err != nil { + t.Errorf("unexpected error from Transform: %v", err) + } + }() + } + + for n := parallel; n > 0; n-- { + sem <- true + } +} From afea27258597edccd7de3017d62243e70a27c529 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 19 Jan 2023 11:33:09 -0500 Subject: [PATCH 654/723] gopls/internal/lsp/source: make implementations2 work on embedded fields Previously, the operation on struct{T} would fail with the error "T is a var, not a type". I don't like the way that each new testcase in the marker tests incurs a quadratic logical complexity. Convenient though it is to edit testdata/*.go files, it tends to encourage a kind of sprawl. Do we have a precedent for self-contained marker tests that use a txtar literal? Updates golang/go#57898 Change-Id: I39a5cb1d96452b621bc6661094f1d735f0e32d3f Reviewed-on: https://go-review.googlesource.com/c/tools/+/462658 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/source/implementation2.go | 8 +++++++- .../lsp/testdata/implementation/implementation.go | 6 +++++- gopls/internal/lsp/testdata/summary.txt.golden | 2 +- gopls/internal/lsp/testdata/summary_go1.18.txt.golden | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/gopls/internal/lsp/source/implementation2.go b/gopls/internal/lsp/source/implementation2.go index 4a8e829a887..7ca2e087e96 100644 --- a/gopls/internal/lsp/source/implementation2.go +++ b/gopls/internal/lsp/source/implementation2.go @@ -274,7 +274,13 @@ func typeDeclPosition(ctx context.Context, snapshot Snapshot, uri span.URI, ppos // Is the object a type or method? Reject other kinds. var methodID string - obj := pkg.GetTypesInfo().ObjectOf(id) + obj := pkg.GetTypesInfo().Uses[id] + if obj == nil { + // Check uses first (unlike ObjectOf) so that T in + // struct{T} is treated as a reference to a type, + // not a declaration of a field. + obj = pkg.GetTypesInfo().Defs[id] + } switch obj := obj.(type) { case *types.TypeName: // ok diff --git a/gopls/internal/lsp/testdata/implementation/implementation.go b/gopls/internal/lsp/testdata/implementation/implementation.go index 5f124750e1b..4c1a22dd4f0 100644 --- a/gopls/internal/lsp/testdata/implementation/implementation.go +++ b/gopls/internal/lsp/testdata/implementation/implementation.go @@ -12,7 +12,7 @@ type ImpS struct{} //@ImpS,implementations("ImpS", Laugher, OtherLaugher) func (ImpS) Laugh() { //@mark(LaughS, "Laugh"),implementations("Laugh", Laugh, OtherLaugh) } -type Laugher interface { //@Laugher,implementations("Laugher", ImpP, OtherImpP, ImpS, OtherImpS) +type Laugher interface { //@Laugher,implementations("Laugher", ImpP, OtherImpP, ImpS, OtherImpS, embedsImpP) Laugh() //@Laugh,implementations("Laugh", LaughP, OtherLaughP, LaughS, OtherLaughS) } @@ -31,3 +31,7 @@ func (cryer) Cry(other.CryType) {} //@mark(CryImpl, "Cry"),implementations("Cry" type Empty interface{} //@implementations("Empty") var _ interface{ Joke() } //@implementations("Joke", ImpJoker) + +type embedsImpP struct { //@embedsImpP + ImpP //@implementations("ImpP", Laugher, OtherLaugher) +} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index e44ce18c049..e0982417c81 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -27,6 +27,6 @@ SymbolsCount = 1 WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 -ImplementationsCount = 15 +ImplementationsCount = 16 SelectionRangesCount = 3 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 80a5d23c7c6..b402eef2e63 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -27,6 +27,6 @@ SymbolsCount = 2 WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 -ImplementationsCount = 25 +ImplementationsCount = 26 SelectionRangesCount = 3 From 33d416e14897fedf26be93292d66c75eaed04d89 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 19 Jan 2023 13:31:56 -0500 Subject: [PATCH 655/723] gopls/internal/lsp: add missing comments on 3x tests.Test impls Also, use top-down order for some decls. Updates golang/go#54845 Change-Id: I5d732593fb709c02300b46a444360c4909b92773 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462659 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan --- gopls/internal/lsp/cmd/test/cmdtest.go | 24 ++++++++++++++---------- gopls/internal/lsp/lsp_test.go | 22 +++++++++++++--------- gopls/internal/lsp/source/source_test.go | 4 ++++ gopls/internal/lsp/tests/tests.go | 17 +++++++++++------ 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/gopls/internal/lsp/cmd/test/cmdtest.go b/gopls/internal/lsp/cmd/test/cmdtest.go index 16497093883..a95875d8f90 100644 --- a/gopls/internal/lsp/cmd/test/cmdtest.go +++ b/gopls/internal/lsp/cmd/test/cmdtest.go @@ -28,14 +28,9 @@ import ( "golang.org/x/tools/internal/tool" ) -type runner struct { - data *tests.Data - ctx context.Context - options func(*source.Options) - normalizers []tests.Normalizer - remote string -} - +// TestCommandLine runs the marker tests in files beneath testdata/ using +// implementations of each of the marker operations (e.g. @hover) that +// fork+exec the gopls command. func TestCommandLine(t *testing.T, testdata string, options func(*source.Options)) { // On Android, the testdata directory is not copied to the runner. if runtime.GOOS == "android" { @@ -43,19 +38,28 @@ func TestCommandLine(t *testing.T, testdata string, options func(*source.Options } tests.RunTests(t, testdata, false, func(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) - ts := NewTestServer(ctx, options) + ts := newTestServer(ctx, options) tests.Run(t, NewRunner(datum, ctx, ts.Addr, options), datum) cmd.CloseTestConnections(ctx) }) } -func NewTestServer(ctx context.Context, options func(*source.Options)) *servertest.TCPServer { +func newTestServer(ctx context.Context, options func(*source.Options)) *servertest.TCPServer { ctx = debug.WithInstance(ctx, "", "") cache := cache.New(nil, nil) ss := lsprpc.NewStreamServer(cache, false, options) return servertest.NewTCPServer(ctx, ss, nil) } +// runner implements tests.Tests by fork+execing the gopls command. +type runner struct { + data *tests.Data + ctx context.Context + options func(*source.Options) + normalizers []tests.Normalizer + remote string +} + func NewRunner(data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner { return &runner{ data: data, diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index d7acc1c553a..efb10be5589 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -43,19 +43,13 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +// TestLSP runs the marker tests in files beneath testdata/ using +// implementations of each of the marker operations (e.g. @hover) that +// make LSP RPCs (e.g. textDocument/hover) to a gopls server. func TestLSP(t *testing.T) { tests.RunTests(t, "testdata", true, testLSP) } -type runner struct { - server *Server - data *tests.Data - diagnostics map[span.URI][]*source.Diagnostic - ctx context.Context - normalizers []tests.Normalizer - editRecv chan map[span.URI]string -} - func testLSP(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) @@ -110,6 +104,16 @@ func testLSP(t *testing.T, datum *tests.Data) { tests.Run(t, r, datum) } +// runner implements tests.Tests by making LSP RPCs to a gopls server. +type runner struct { + server *Server + data *tests.Data + diagnostics map[span.URI][]*source.Diagnostic + ctx context.Context + normalizers []tests.Normalizer + editRecv chan map[span.URI]string +} + // testClient stubs any client functions that may be called by LSP functions. type testClient struct { protocol.Client diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go index 97f602f2958..2959a59cba8 100644 --- a/gopls/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -33,10 +33,14 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +// TestSource runs the marker tests in files beneath testdata/ using +// implementations of each of the marker operations (e.g. @hover) that +// make direct calls to the server logic (such as source.Hover). func TestSource(t *testing.T) { tests.RunTests(t, "../testdata", true, testSource) } +// runner implements tests.Tests by making direct calls to the source package. type runner struct { session *cache.Session view *cache.View diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index e9f1009478d..d9a55c1594c 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -143,12 +143,17 @@ type Data struct { mappers map[span.URI]*protocol.Mapper } -// TODO(adonovan): there are multiple implementations of this (undocumented) -// interface, each of which must implement similar semantics. For example: -// - *runner in ../cmd/test/check.go -// - *runner in ../source/source_test.go -// - *runner in ../lsp_test.go -// Can we avoid this duplication? +// The Tests interface abstracts a set of implementations of marker +// test operators (such as @hover) appearing +// +// There are three implementations: +// - *runner in ../cmd/test/check.go, which runs the command-line tool (e.g. "gopls hover") +// - *runner in ../source/source_test.go, which makes direct calls (e.g. to source.Hover) +// - *runner in ../lsp_test.go, which makes LSP requests (textDocument/hover) to a gopls server. +// +// Not all implementations implement all methods. +// +// TODO(adonovan): reduce duplication; see https://github.com/golang/go/issues/54845. type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) CodeLens(*testing.T, span.URI, []protocol.CodeLens) From bcc7794c084e0fae686602a9d827752af568f98d Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Sat, 14 Jan 2023 17:13:29 -0500 Subject: [PATCH 656/723] go/analysis/passes/directive: add directive analyzer The directive analyzer is a generalized version of the buildtag analyzer, meant to apply checks about any directives (//go:... lines) found in source code. For now it only checks the placement of //go:debug lines. For golang/go#56986. Change-Id: I2bd3d743c44554711ada90f6ee53b6195dc55bcb Reviewed-on: https://go-review.googlesource.com/c/tools/+/462216 Run-TryBot: Russ Cox gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Tim King Auto-Submit: Russ Cox --- go/analysis/passes/buildtag/buildtag.go | 2 +- go/analysis/passes/buildtag/buildtag_old.go | 2 +- go/analysis/passes/directive/directive.go | 216 ++++++++++++++++++ .../passes/directive/directive_test.go | 39 ++++ .../directive/testdata/src/a/badspace.go | 11 + .../directive/testdata/src/a/misplaced.go | 10 + .../directive/testdata/src/a/misplaced.s | 19 ++ .../testdata/src/a/misplaced_test.go | 10 + .../passes/directive/testdata/src/a/p.go | 11 + gopls/doc/analyzers.md | 22 +- gopls/internal/lsp/source/api_json.go | 14 +- gopls/internal/lsp/source/options.go | 2 + internal/gcimporter/testdata/a/a.go | 14 ++ .../gcimporter/testdata/issue51836/a/a.go | 8 + 14 files changed, 375 insertions(+), 5 deletions(-) create mode 100644 go/analysis/passes/directive/directive.go create mode 100644 go/analysis/passes/directive/directive_test.go create mode 100644 go/analysis/passes/directive/testdata/src/a/badspace.go create mode 100644 go/analysis/passes/directive/testdata/src/a/misplaced.go create mode 100644 go/analysis/passes/directive/testdata/src/a/misplaced.s create mode 100644 go/analysis/passes/directive/testdata/src/a/misplaced_test.go create mode 100644 go/analysis/passes/directive/testdata/src/a/p.go create mode 100644 internal/gcimporter/testdata/a/a.go create mode 100644 internal/gcimporter/testdata/issue51836/a/a.go diff --git a/go/analysis/passes/buildtag/buildtag.go b/go/analysis/passes/buildtag/buildtag.go index c4407ad91fe..775e507a346 100644 --- a/go/analysis/passes/buildtag/buildtag.go +++ b/go/analysis/passes/buildtag/buildtag.go @@ -20,7 +20,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" ) -const Doc = "check that +build tags are well-formed and correctly located" +const Doc = "check //go:build and // +build directives" var Analyzer = &analysis.Analyzer{ Name: "buildtag", diff --git a/go/analysis/passes/buildtag/buildtag_old.go b/go/analysis/passes/buildtag/buildtag_old.go index e9234925f9c..0001ba53639 100644 --- a/go/analysis/passes/buildtag/buildtag_old.go +++ b/go/analysis/passes/buildtag/buildtag_old.go @@ -22,7 +22,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" ) -const Doc = "check that +build tags are well-formed and correctly located" +const Doc = "check // +build directives" var Analyzer = &analysis.Analyzer{ Name: "buildtag", diff --git a/go/analysis/passes/directive/directive.go b/go/analysis/passes/directive/directive.go new file mode 100644 index 00000000000..76d852cd0fe --- /dev/null +++ b/go/analysis/passes/directive/directive.go @@ -0,0 +1,216 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package directive defines an Analyzer that checks known Go toolchain directives. +package directive + +import ( + "go/ast" + "go/parser" + "go/token" + "strings" + "unicode" + "unicode/utf8" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/internal/analysisutil" +) + +const Doc = `check Go toolchain directives such as //go:debug + +This analyzer checks for problems with known Go toolchain directives +in all Go source files in a package directory, even those excluded by +//go:build constraints, and all non-Go source files too. + +For //go:debug (see https://go.dev/doc/godebug), the analyzer checks +that the directives are placed only in Go source files, only above the +package comment, and only in package main or *_test.go files. + +Support for other known directives may be added in the future. + +This analyzer does not check //go:build, which is handled by the +buildtag analyzer. +` + +var Analyzer = &analysis.Analyzer{ + Name: "directive", + Doc: Doc, + Run: runDirective, +} + +func runDirective(pass *analysis.Pass) (interface{}, error) { + for _, f := range pass.Files { + checkGoFile(pass, f) + } + for _, name := range pass.OtherFiles { + if err := checkOtherFile(pass, name); err != nil { + return nil, err + } + } + for _, name := range pass.IgnoredFiles { + if strings.HasSuffix(name, ".go") { + f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments) + if err != nil { + // Not valid Go source code - not our job to diagnose, so ignore. + continue + } + checkGoFile(pass, f) + } else { + if err := checkOtherFile(pass, name); err != nil { + return nil, err + } + } + } + return nil, nil +} + +func checkGoFile(pass *analysis.Pass, f *ast.File) { + check := newChecker(pass, pass.Fset.File(f.Package).Name(), f) + + for _, group := range f.Comments { + // A +build comment is ignored after or adjoining the package declaration. + if group.End()+1 >= f.Package { + check.inHeader = false + } + // A //go:build comment is ignored after the package declaration + // (but adjoining it is OK, in contrast to +build comments). + if group.Pos() >= f.Package { + check.inHeader = false + } + + // Check each line of a //-comment. + for _, c := range group.List { + check.comment(c.Slash, c.Text) + } + } +} + +func checkOtherFile(pass *analysis.Pass, filename string) error { + // We cannot use the Go parser, since is not a Go source file. + // Read the raw bytes instead. + content, tf, err := analysisutil.ReadFile(pass.Fset, filename) + if err != nil { + return err + } + + check := newChecker(pass, filename, nil) + check.nonGoFile(token.Pos(tf.Base()), string(content)) + return nil +} + +type checker struct { + pass *analysis.Pass + filename string + file *ast.File // nil for non-Go file + inHeader bool // in file header (before package declaration) + inStar bool // currently in a /* */ comment +} + +func newChecker(pass *analysis.Pass, filename string, file *ast.File) *checker { + return &checker{ + pass: pass, + filename: filename, + file: file, + inHeader: true, + } +} + +func (check *checker) nonGoFile(pos token.Pos, fullText string) { + // Process each line. + text := fullText + inStar := false + for text != "" { + offset := len(fullText) - len(text) + var line string + line, text, _ = stringsCut(text, "\n") + + if !inStar && strings.HasPrefix(line, "//") { + check.comment(pos+token.Pos(offset), line) + continue + } + + // Skip over, cut out any /* */ comments, + // to avoid being confused by a commented-out // comment. + for { + line = strings.TrimSpace(line) + if inStar { + var ok bool + _, line, ok = stringsCut(line, "*/") + if !ok { + break + } + inStar = false + continue + } + line, inStar = stringsCutPrefix(line, "/*") + if !inStar { + break + } + } + if line != "" { + // Found non-comment non-blank line. + // Ends space for valid //go:build comments, + // but also ends the fraction of the file we can + // reliably parse. From this point on we might + // incorrectly flag "comments" inside multiline + // string constants or anything else (this might + // not even be a Go program). So stop. + break + } + } +} + +func (check *checker) comment(pos token.Pos, line string) { + if !strings.HasPrefix(line, "//go:") { + return + } + // testing hack: stop at // ERROR + if i := strings.Index(line, " // ERROR "); i >= 0 { + line = line[:i] + } + + verb := line + if i := strings.IndexFunc(verb, unicode.IsSpace); i >= 0 { + verb = verb[:i] + if line[i] != ' ' && line[i] != '\t' && line[i] != '\n' { + r, _ := utf8.DecodeRuneInString(line[i:]) + check.pass.Reportf(pos, "invalid space %#q in %s directive", r, verb) + } + } + + switch verb { + default: + // TODO: Use the go language version for the file. + // If that version is not newer than us, then we can + // report unknown directives. + + case "//go:build": + // Ignore. The buildtag analyzer reports misplaced comments. + + case "//go:debug": + if check.file == nil { + check.pass.Reportf(pos, "//go:debug directive only valid in Go source files") + } else if check.file.Name.Name != "main" && !strings.HasSuffix(check.filename, "_test.go") { + check.pass.Reportf(pos, "//go:debug directive only valid in package main or test") + } else if !check.inHeader { + check.pass.Reportf(pos, "//go:debug directive only valid before package declaration") + } + } +} + +// Go 1.18 strings.Cut. +func stringsCut(s, sep string) (before, after string, found bool) { + if i := strings.Index(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true + } + return s, "", false +} + +// Go 1.20 strings.CutPrefix. +func stringsCutPrefix(s, prefix string) (after string, found bool) { + if !strings.HasPrefix(s, prefix) { + return s, false + } + return s[len(prefix):], true +} diff --git a/go/analysis/passes/directive/directive_test.go b/go/analysis/passes/directive/directive_test.go new file mode 100644 index 00000000000..a526c0d740d --- /dev/null +++ b/go/analysis/passes/directive/directive_test.go @@ -0,0 +1,39 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package directive_test + +import ( + "runtime" + "strings" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/directive" +) + +func Test(t *testing.T) { + if strings.HasPrefix(runtime.Version(), "go1.") && runtime.Version() < "go1.16" { + t.Skipf("skipping on %v", runtime.Version()) + } + analyzer := *directive.Analyzer + analyzer.Run = func(pass *analysis.Pass) (interface{}, error) { + defer func() { + // The directive pass is unusual in that it checks the IgnoredFiles. + // After analysis, add IgnoredFiles to OtherFiles so that + // the test harness checks for expected diagnostics in those. + // (The test harness shouldn't do this by default because most + // passes can't do anything with the IgnoredFiles without type + // information, which is unavailable because they are ignored.) + var files []string + files = append(files, pass.OtherFiles...) + files = append(files, pass.IgnoredFiles...) + pass.OtherFiles = files + }() + + return directive.Analyzer.Run(pass) + } + analysistest.Run(t, analysistest.TestData(), &analyzer, "a") +} diff --git a/go/analysis/passes/directive/testdata/src/a/badspace.go b/go/analysis/passes/directive/testdata/src/a/badspace.go new file mode 100644 index 00000000000..11313996046 --- /dev/null +++ b/go/analysis/passes/directive/testdata/src/a/badspace.go @@ -0,0 +1,11 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +// want +1 `invalid space '\\u00a0' in //go:debug directive` +//go:debug 00a0 + +package main + diff --git a/go/analysis/passes/directive/testdata/src/a/misplaced.go b/go/analysis/passes/directive/testdata/src/a/misplaced.go new file mode 100644 index 00000000000..db30ceb476e --- /dev/null +++ b/go/analysis/passes/directive/testdata/src/a/misplaced.go @@ -0,0 +1,10 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +// want +1 `//go:debug directive only valid before package declaration` +//go:debug panicnil=1 diff --git a/go/analysis/passes/directive/testdata/src/a/misplaced.s b/go/analysis/passes/directive/testdata/src/a/misplaced.s new file mode 100644 index 00000000000..9e26dbc5241 --- /dev/null +++ b/go/analysis/passes/directive/testdata/src/a/misplaced.s @@ -0,0 +1,19 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// want +1 `//go:debug directive only valid in Go source files` +//go:debug panicnil=1 + +/* +can skip over comments +//go:debug doesn't matter here +*/ + +// want +1 `//go:debug directive only valid in Go source files` +//go:debug panicnil=1 + +package a + +// no error here because we can't parse this far +//go:debug panicnil=1 diff --git a/go/analysis/passes/directive/testdata/src/a/misplaced_test.go b/go/analysis/passes/directive/testdata/src/a/misplaced_test.go new file mode 100644 index 00000000000..6b4527a3589 --- /dev/null +++ b/go/analysis/passes/directive/testdata/src/a/misplaced_test.go @@ -0,0 +1,10 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:debug panicnil=1 + +package p_test + +// want +1 `//go:debug directive only valid before package declaration` +//go:debug panicnil=1 diff --git a/go/analysis/passes/directive/testdata/src/a/p.go b/go/analysis/passes/directive/testdata/src/a/p.go new file mode 100644 index 00000000000..e1e3e65520f --- /dev/null +++ b/go/analysis/passes/directive/testdata/src/a/p.go @@ -0,0 +1,11 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// want +1 `//go:debug directive only valid in package main or test` +//go:debug panicnil=1 + +package p + +// want +1 `//go:debug directive only valid in package main or test` +//go:debug panicnil=1 diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 28bf1deae83..a1134bee3b3 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -48,7 +48,7 @@ check for common mistakes involving boolean operators ## **buildtag** -check that +build tags are well-formed and correctly located +check //go:build and // +build directives **Enabled by default.** @@ -106,6 +106,26 @@ The deepequalerrors checker looks for calls of the form: where err1 and err2 are errors. Using reflect.DeepEqual to compare errors is discouraged. +**Enabled by default.** + +## **directive** + +check Go toolchain directives such as //go:debug + +This analyzer checks for problems with known Go toolchain directives +in all Go source files in a package directory, even those excluded by +//go:build constraints, and all non-Go source files too. + +For //go:debug (see https://go.dev/doc/godebug), the analyzer checks +that the directives are placed only in Go source files, only above the +package comment, and only in package main or *_test.go files. + +Support for other known directives may be added in the future. + +This analyzer does not check //go:build, which is handled by the +buildtag analyzer. + + **Enabled by default.** ## **embed** diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 0d24ececa1e..e97c1474a28 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -236,7 +236,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "\"buildtag\"", - Doc: "check that +build tags are well-formed and correctly located", + Doc: "check //go:build and // +build directives", Default: "true", }, { @@ -259,6 +259,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.", Default: "true", }, + { + Name: "\"directive\"", + Doc: "check Go toolchain directives such as //go:debug\n\nThis analyzer checks for problems with known Go toolchain directives\nin all Go source files in a package directory, even those excluded by\n//go:build constraints, and all non-Go source files too.\n\nFor //go:debug (see https://go.dev/doc/godebug), the analyzer checks\nthat the directives are placed only in Go source files, only above the\npackage comment, and only in package main or *_test.go files.\n\nSupport for other known directives may be added in the future.\n\nThis analyzer does not check //go:build, which is handled by the\nbuildtag analyzer.\n", + Default: "true", + }, { Name: "\"embed\"", Doc: "check for //go:embed directive import\n\nThis analyzer checks that the embed package is imported when source code contains //go:embed comment directives.\nThe embed package must be imported for //go:embed directives to function.import _ \"embed\".", @@ -875,7 +880,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "buildtag", - Doc: "check that +build tags are well-formed and correctly located", + Doc: "check //go:build and // +build directives", Default: true, }, { @@ -898,6 +903,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.", Default: true, }, + { + Name: "directive", + Doc: "check Go toolchain directives such as //go:debug\n\nThis analyzer checks for problems with known Go toolchain directives\nin all Go source files in a package directory, even those excluded by\n//go:build constraints, and all non-Go source files too.\n\nFor //go:debug (see https://go.dev/doc/godebug), the analyzer checks\nthat the directives are placed only in Go source files, only above the\npackage comment, and only in package main or *_test.go files.\n\nSupport for other known directives may be added in the future.\n\nThis analyzer does not check //go:build, which is handled by the\nbuildtag analyzer.\n", + Default: true, + }, { Name: "embed", Doc: "check for //go:embed directive import\n\nThis analyzer checks that the embed package is imported when source code contains //go:embed comment directives.\nThe embed package must be imported for //go:embed directives to function.import _ \"embed\".", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 01feb210d7a..88a3678e147 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -26,6 +26,7 @@ import ( "golang.org/x/tools/go/analysis/passes/composite" "golang.org/x/tools/go/analysis/passes/copylock" "golang.org/x/tools/go/analysis/passes/deepequalerrors" + "golang.org/x/tools/go/analysis/passes/directive" "golang.org/x/tools/go/analysis/passes/errorsas" "golang.org/x/tools/go/analysis/passes/fieldalignment" "golang.org/x/tools/go/analysis/passes/httpresponse" @@ -1422,6 +1423,7 @@ func defaultAnalyzers() map[string]*Analyzer { cgocall.Analyzer.Name: {Analyzer: cgocall.Analyzer, Enabled: true}, composite.Analyzer.Name: {Analyzer: composite.Analyzer, Enabled: true}, copylock.Analyzer.Name: {Analyzer: copylock.Analyzer, Enabled: true}, + directive.Analyzer.Name: {Analyzer: directive.Analyzer, Enabled: true}, errorsas.Analyzer.Name: {Analyzer: errorsas.Analyzer, Enabled: true}, httpresponse.Analyzer.Name: {Analyzer: httpresponse.Analyzer, Enabled: true}, ifaceassert.Analyzer.Name: {Analyzer: ifaceassert.Analyzer, Enabled: true}, diff --git a/internal/gcimporter/testdata/a/a.go b/internal/gcimporter/testdata/a/a.go new file mode 100644 index 00000000000..56e4292cda9 --- /dev/null +++ b/internal/gcimporter/testdata/a/a.go @@ -0,0 +1,14 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Input for TestIssue13566 + +package a + +import "encoding/json" + +type A struct { + a *A + json json.RawMessage +} diff --git a/internal/gcimporter/testdata/issue51836/a/a.go b/internal/gcimporter/testdata/issue51836/a/a.go new file mode 100644 index 00000000000..e9223c9aa82 --- /dev/null +++ b/internal/gcimporter/testdata/issue51836/a/a.go @@ -0,0 +1,8 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package a + +type T[K any] struct { +} From 46b69584a1dd507297e6e46e9017fe327f070d35 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 19 Jan 2023 15:54:50 -0500 Subject: [PATCH 657/723] gopls/internal/lsp/source: delete source_test The source test is one of three (partial) implementations of the tests.Tests interface, which defines the behavior of the marker tests. It makes direct calls to logic in the source package; the other two implementations make LSP RPCs (lsp_test) or fork+exec the gopls command (cmd_test). I have audited all the functions in source_test and satisfied myself that they provide no additional coverage beyond what is provided by lsp_test, and in some cases strictly less. A lot of logic was redundant. Ultimately, lsp_test is what matters, since the LSP is our main interface. Where there was any subtlety or discrepancy, I have remarked below. A few functions in source have been made unexported. This also removes 9s real, 18s CPU, from our CI builds. Details: - CompletionSnippet source_test had opts.Matcher = source.Fuzzy - DeepCompletion source_test had a FuzzyMatcher, but it never affected the outcome. - RankCompletion source_test failed to set these options used in the LSP test: opts.CompleteUnimported = false opts.LiteralCompletions = true - Import lsp_test invokes the broader textDocument/codeAction RPC where source_test called AllImportsFixes. - Definition source_test was calling FormatHover twice! - Rename the LSP implementation does strictly more (reports the file rename). Updates golang/go#54845 Change-Id: I1b0956d56540856dc0494f50ede0be7b7acc3e8e Reviewed-on: https://go-review.googlesource.com/c/tools/+/462816 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/lsp_test.go | 62 +- gopls/internal/lsp/source/hover.go | 8 +- gopls/internal/lsp/source/source_test.go | 781 ----------------------- gopls/internal/lsp/tests/tests.go | 5 +- gopls/internal/lsp/tests/util.go | 8 - 5 files changed, 32 insertions(+), 832 deletions(-) delete mode 100644 gopls/internal/lsp/source/source_test.go diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index efb10be5589..46ff0706835 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -224,8 +224,7 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost // Get the diagnostics for this view if we have not done it before. v := r.server.session.View(r.data.Config.Dir) r.collectDiagnostics(v) - got := append([]*source.Diagnostic(nil), r.diagnostics[uri]...) // copy - tests.CompareDiagnostics(t, uri, want, got) + tests.CompareDiagnostics(t, uri, want, r.diagnostics[uri]) } func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { @@ -238,41 +237,30 @@ func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { modified := original defer r.server.session.SetViewOptions(r.ctx, view, original) - // Test all folding ranges. - modified.LineFoldingOnly = false - view, err = r.server.session.SetViewOptions(r.ctx, view, modified) - if err != nil { - t.Error(err) - return - } - ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(uri), - }, - }) - if err != nil { - t.Error(err) - return - } - r.foldingRanges(t, "foldingRange", uri, ranges) - - // Test folding ranges with lineFoldingOnly = true. - modified.LineFoldingOnly = true - view, err = r.server.session.SetViewOptions(r.ctx, view, modified) - if err != nil { - t.Error(err) - return - } - ranges, err = r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(uri), - }, - }) - if err != nil { - t.Error(err) - return + for _, test := range []struct { + lineFoldingOnly bool + prefix string + }{ + {false, "foldingRange"}, + {true, "foldingRange-lineFolding"}, + } { + modified.LineFoldingOnly = test.lineFoldingOnly + view, err = r.server.session.SetViewOptions(r.ctx, view, modified) + if err != nil { + t.Error(err) + continue + } + ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(uri), + }, + }) + if err != nil { + t.Error(err) + continue + } + r.foldingRanges(t, test.prefix, uri, ranges) } - r.foldingRanges(t, "foldingRange-lineFolding", uri, ranges) } func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) { @@ -446,6 +434,8 @@ func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { } func (r *runner) Import(t *testing.T, spn span.Span) { + // Invokes textDocument/codeAction and applies all the "goimports" edits. + uri := spn.URI() filename := uri.Filename() actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index d023a360f01..9eee53ad3bd 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -79,11 +79,11 @@ func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position proto } return nil, nil } - h, err := HoverIdentifier(ctx, ident) + h, err := hoverIdentifier(ctx, ident) if err != nil { return nil, err } - hover, err := FormatHover(h, snapshot.View().Options()) + hover, err := formatHover(h, snapshot.View().Options()) if err != nil { return nil, err } @@ -239,7 +239,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr return r, rng, nil } -func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) { +func hoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) { ctx, done := event.Start(ctx, "source.Hover") defer done() @@ -782,7 +782,7 @@ func findFieldComment(pos token.Pos, fieldList *ast.FieldList) *ast.CommentGroup return nil } -func FormatHover(h *HoverJSON, options *Options) (string, error) { +func formatHover(h *HoverJSON, options *Options) (string, error) { signature := formatSignature(h, options) switch options.HoverKind { diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go deleted file mode 100644 index 2959a59cba8..00000000000 --- a/gopls/internal/lsp/source/source_test.go +++ /dev/null @@ -1,781 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package source_test - -import ( - "context" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "sort" - "strings" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/source/completion" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/fuzzy" - "golang.org/x/tools/internal/testenv" -) - -func TestMain(m *testing.M) { - bug.PanicOnBugs = true - testenv.ExitIfSmallMachine() - os.Exit(m.Run()) -} - -// TestSource runs the marker tests in files beneath testdata/ using -// implementations of each of the marker operations (e.g. @hover) that -// make direct calls to the server logic (such as source.Hover). -func TestSource(t *testing.T) { - tests.RunTests(t, "../testdata", true, testSource) -} - -// runner implements tests.Tests by making direct calls to the source package. -type runner struct { - session *cache.Session - view *cache.View - snapshot source.Snapshot - data *tests.Data - ctx context.Context - normalizers []tests.Normalizer -} - -func testSource(t *testing.T, datum *tests.Data) { - ctx := tests.Context(t) - - session := cache.NewSession(ctx, cache.New(nil, nil), nil) - options := source.DefaultOptions().Clone() - tests.DefaultOptions(options) - options.SetEnvSlice(datum.Config.Env) - view, _, release, err := session.NewView(ctx, "source_test", span.URIFromPath(datum.Config.Dir), options) - if err != nil { - t.Fatal(err) - } - release() - defer session.RemoveView(view) - - // Enable type error analyses for tests. - // TODO(golang/go#38212): Delete this once they are enabled by default. - tests.EnableAllAnalyzers(options) - view, err = session.SetViewOptions(ctx, view, options) - if err != nil { - t.Fatal(err) - } - - var modifications []source.FileModification - for filename, content := range datum.Config.Overlay { - if filepath.Ext(filename) != ".go" { - continue - } - modifications = append(modifications, source.FileModification{ - URI: span.URIFromPath(filename), - Action: source.Open, - Version: -1, - Text: content, - LanguageID: "go", - }) - } - if err := session.ModifyFiles(ctx, modifications); err != nil { - t.Fatal(err) - } - snapshot, release := view.Snapshot(ctx) - defer release() - r := &runner{ - session: session, - view: view, - snapshot: snapshot, - data: datum, - ctx: ctx, - normalizers: tests.CollectNormalizers(datum.Exported), - } - tests.Run(t, r, datum) -} - -func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) { - mapper, err := r.data.Mapper(spn.URI()) - if err != nil { - t.Fatal(err) - } - loc, err := mapper.SpanLocation(spn) - if err != nil { - t.Fatalf("failed for %v: %v", spn, err) - } - fh, err := r.snapshot.GetFile(r.ctx, spn.URI()) - if err != nil { - t.Fatal(err) - } - - items, err := source.PrepareCallHierarchy(r.ctx, r.snapshot, fh, loc.Range.Start) - if err != nil { - t.Fatal(err) - } - if len(items) == 0 { - t.Fatalf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range) - } - - callLocation := protocol.Location{ - URI: items[0].URI, - Range: items[0].Range, - } - if callLocation != loc { - t.Fatalf("expected source.PrepareCallHierarchy to return identifier at %v but got %v\n", loc, callLocation) - } - - incomingCalls, err := source.IncomingCalls(r.ctx, r.snapshot, fh, loc.Range.Start) - if err != nil { - t.Error(err) - } - var incomingCallItems []protocol.CallHierarchyItem - for _, item := range incomingCalls { - incomingCallItems = append(incomingCallItems, item.From) - } - msg := tests.DiffCallHierarchyItems(incomingCallItems, expectedCalls.IncomingCalls) - if msg != "" { - t.Error(fmt.Sprintf("incoming calls differ: %s", msg)) - } - - outgoingCalls, err := source.OutgoingCalls(r.ctx, r.snapshot, fh, loc.Range.Start) - if err != nil { - t.Error(err) - } - var outgoingCallItems []protocol.CallHierarchyItem - for _, item := range outgoingCalls { - outgoingCallItems = append(outgoingCallItems, item.To) - } - msg = tests.DiffCallHierarchyItems(outgoingCallItems, expectedCalls.OutgoingCalls) - if msg != "" { - t.Error(fmt.Sprintf("outgoing calls differ: %s", msg)) - } -} - -func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) { - fileID, got, err := source.FileDiagnostics(r.ctx, r.snapshot, uri) - if err != nil { - t.Fatal(err) - } - tests.CompareDiagnostics(t, fileID.URI, want, got) -} - -func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - var want []protocol.CompletionItem - for _, pos := range test.CompletionItems { - want = append(want, tests.ToProtocolCompletionItem(*items[pos])) - } - _, got := r.callCompletion(t, src, func(opts *source.Options) { - opts.Matcher = source.CaseInsensitive - opts.DeepCompletion = false - opts.CompleteUnimported = false - opts.InsertTextFormat = protocol.SnippetTextFormat - opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal") - opts.ExperimentalPostfixCompletions = strings.Contains(string(src.URI()), "postfix") - }) - got = tests.FilterBuiltins(src, got) - if diff := tests.DiffCompletionItems(want, got); diff != "" { - t.Errorf("%s: %s", src, diff) - } -} - -func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) { - _, list := r.callCompletion(t, src, func(opts *source.Options) { - opts.UsePlaceholders = placeholders - opts.DeepCompletion = true - opts.CompleteUnimported = false - }) - got := tests.FindItem(list, *items[expected.CompletionItem]) - want := expected.PlainSnippet - if placeholders { - want = expected.PlaceholderSnippet - } - if diff := tests.DiffSnippets(want, got); diff != "" { - t.Errorf("%s: %s", src, diff) - } -} - -func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - var want []protocol.CompletionItem - for _, pos := range test.CompletionItems { - want = append(want, tests.ToProtocolCompletionItem(*items[pos])) - } - _, got := r.callCompletion(t, src, func(opts *source.Options) {}) - got = tests.FilterBuiltins(src, got) - if diff := tests.CheckCompletionOrder(want, got, false); diff != "" { - t.Errorf("%s: %s", src, diff) - } -} - -func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - var want []protocol.CompletionItem - for _, pos := range test.CompletionItems { - want = append(want, tests.ToProtocolCompletionItem(*items[pos])) - } - prefix, list := r.callCompletion(t, src, func(opts *source.Options) { - opts.DeepCompletion = true - opts.Matcher = source.CaseInsensitive - opts.CompleteUnimported = false - }) - list = tests.FilterBuiltins(src, list) - fuzzyMatcher := fuzzy.NewMatcher(prefix) - var got []protocol.CompletionItem - for _, item := range list { - if fuzzyMatcher.Score(item.Label) <= 0 { - continue - } - got = append(got, item) - } - if msg := tests.DiffCompletionItems(want, got); msg != "" { - t.Errorf("%s: %s", src, msg) - } -} - -func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - var want []protocol.CompletionItem - for _, pos := range test.CompletionItems { - want = append(want, tests.ToProtocolCompletionItem(*items[pos])) - } - _, got := r.callCompletion(t, src, func(opts *source.Options) { - opts.DeepCompletion = true - opts.Matcher = source.Fuzzy - opts.CompleteUnimported = false - }) - got = tests.FilterBuiltins(src, got) - if msg := tests.DiffCompletionItems(want, got); msg != "" { - t.Errorf("%s: %s", src, msg) - } -} - -func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - var want []protocol.CompletionItem - for _, pos := range test.CompletionItems { - want = append(want, tests.ToProtocolCompletionItem(*items[pos])) - } - _, list := r.callCompletion(t, src, func(opts *source.Options) { - opts.Matcher = source.CaseSensitive - opts.CompleteUnimported = false - }) - list = tests.FilterBuiltins(src, list) - if diff := tests.DiffCompletionItems(want, list); diff != "" { - t.Errorf("%s: %s", src, diff) - } -} - -func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - var want []protocol.CompletionItem - for _, pos := range test.CompletionItems { - want = append(want, tests.ToProtocolCompletionItem(*items[pos])) - } - _, got := r.callCompletion(t, src, func(opts *source.Options) { - opts.DeepCompletion = true - opts.Matcher = source.Fuzzy - opts.ExperimentalPostfixCompletions = true - }) - if msg := tests.CheckCompletionOrder(want, got, true); msg != "" { - t.Errorf("%s: %s", src, msg) - } -} - -func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*source.Options)) (string, []protocol.CompletionItem) { - fh, err := r.snapshot.GetFile(r.ctx, src.URI()) - if err != nil { - t.Fatal(err) - } - original := r.view.Options() - modified := original.Clone() - options(modified) - view, err := r.session.SetViewOptions(r.ctx, r.view, modified) - if view != r.view { - t.Fatalf("options change unexpectedly created new view") - } - if err != nil { - t.Fatal(err) - } - defer r.session.SetViewOptions(r.ctx, view, original) - - list, surrounding, err := completion.Completion(r.ctx, r.snapshot, fh, protocol.Position{ - Line: uint32(src.Start().Line() - 1), - Character: uint32(src.Start().Column() - 1), - }, protocol.CompletionContext{}) - if err != nil && !errors.As(err, &completion.ErrIsDefinition{}) { - t.Fatalf("failed for %v: %v", src, err) - } - var prefix string - if surrounding != nil { - prefix = strings.ToLower(surrounding.Prefix()) - } - - var numDeepCompletionsSeen int - var items []completion.CompletionItem - // Apply deep completion filtering. - for _, item := range list { - if item.Depth > 0 { - if !modified.DeepCompletion { - continue - } - if numDeepCompletionsSeen >= completion.MaxDeepCompletions { - continue - } - numDeepCompletionsSeen++ - } - items = append(items, item) - } - return prefix, tests.ToProtocolCompletionItems(items) -} - -func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { - uri := spn.URI() - - fh, err := r.snapshot.GetFile(r.ctx, spn.URI()) - if err != nil { - t.Fatal(err) - } - data, err := fh.Read() - if err != nil { - t.Error(err) - return - } - - // Test all folding ranges. - ranges, err := source.FoldingRange(r.ctx, r.snapshot, fh, false) - if err != nil { - t.Error(err) - return - } - r.foldingRanges(t, "foldingRange", uri, string(data), ranges) - - // Test folding ranges with lineFoldingOnly - ranges, err = source.FoldingRange(r.ctx, r.snapshot, fh, true) - if err != nil { - t.Error(err) - return - } - r.foldingRanges(t, "foldingRange-lineFolding", uri, string(data), ranges) -} - -func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, data string, ranges []*source.FoldingRangeInfo) { - t.Helper() - // Fold all ranges. - nonOverlapping := nonOverlappingRanges(t, ranges) - for i, rngs := range nonOverlapping { - got, err := foldRanges(string(data), rngs) - if err != nil { - t.Error(err) - continue - } - tag := fmt.Sprintf("%s-%d", prefix, i) - want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { - return []byte(got), nil - })) - - if diff := compare.Text(want, got); diff != "" { - t.Errorf("%s: foldingRanges failed for %s, diff:\n%v", tag, uri.Filename(), diff) - } - } - - // Filter by kind. - kinds := []protocol.FoldingRangeKind{protocol.Imports, protocol.Comment} - for _, kind := range kinds { - var kindOnly []*source.FoldingRangeInfo - for _, fRng := range ranges { - if fRng.Kind == kind { - kindOnly = append(kindOnly, fRng) - } - } - - nonOverlapping := nonOverlappingRanges(t, kindOnly) - for i, rngs := range nonOverlapping { - got, err := foldRanges(string(data), rngs) - if err != nil { - t.Error(err) - continue - } - tag := fmt.Sprintf("%s-%s-%d", prefix, kind, i) - want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { - return []byte(got), nil - })) - - if diff := compare.Text(want, got); diff != "" { - t.Errorf("%s: failed for %s, diff:\n%v", tag, uri.Filename(), diff) - } - } - - } -} - -func nonOverlappingRanges(t *testing.T, ranges []*source.FoldingRangeInfo) (res [][]*source.FoldingRangeInfo) { - for _, fRng := range ranges { - setNum := len(res) - for i := 0; i < len(res); i++ { - canInsert := true - for _, rng := range res[i] { - if conflict(t, rng, fRng) { - canInsert = false - break - } - } - if canInsert { - setNum = i - break - } - } - if setNum == len(res) { - res = append(res, []*source.FoldingRangeInfo{}) - } - res[setNum] = append(res[setNum], fRng) - } - return res -} - -func conflict(t *testing.T, a, b *source.FoldingRangeInfo) bool { - arng := a.MappedRange.Range() - brng := b.MappedRange.Range() - // a start position is <= b start positions - return protocol.ComparePosition(arng.Start, brng.Start) <= 0 && protocol.ComparePosition(arng.End, brng.Start) > 0 -} - -func foldRanges(contents string, ranges []*source.FoldingRangeInfo) (string, error) { - foldedText := "<>" - res := contents - // Apply the folds from the end of the file forward - // to preserve the offsets. - for i := len(ranges) - 1; i >= 0; i-- { - fRange := ranges[i] - spn := fRange.MappedRange.Span() - start := spn.Start().Offset() - end := spn.End().Offset() - - tmp := res[0:start] + foldedText - res = tmp + res[end:] - } - return res, nil -} - -func (r *runner) Format(t *testing.T, spn span.Span) { - gofmted := string(r.data.Golden(t, "gofmt", spn.URI().Filename(), func() ([]byte, error) { - cmd := exec.Command("gofmt", spn.URI().Filename()) - out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files - return out, nil - })) - fh, err := r.snapshot.GetFile(r.ctx, spn.URI()) - if err != nil { - t.Fatal(err) - } - edits, err := source.Format(r.ctx, r.snapshot, fh) - if err != nil { - if gofmted != "" { - t.Error(err) - } - return - } - m, err := r.data.Mapper(spn.URI()) - if err != nil { - t.Fatal(err) - } - got, _, err := source.ApplyProtocolEdits(m, edits) - if err != nil { - t.Error(err) - } - if gofmted != got { - t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", spn.URI().Filename(), gofmted, got) - } -} - -func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { - t.Skip("nothing to test in source") -} - -func (r *runner) SelectionRanges(t *testing.T, spn span.Span) { - t.Skip("nothing to test in source") -} - -func (r *runner) Import(t *testing.T, spn span.Span) { - fh, err := r.snapshot.GetFile(r.ctx, spn.URI()) - if err != nil { - t.Fatal(err) - } - edits, _, err := source.AllImportsFixes(r.ctx, r.snapshot, fh) - if err != nil { - t.Error(err) - } - m, err := r.data.Mapper(fh.URI()) - if err != nil { - t.Fatal(err) - } - got, _, err := source.ApplyProtocolEdits(m, edits) - if err != nil { - t.Error(err) - } - want := string(r.data.Golden(t, "goimports", spn.URI().Filename(), func() ([]byte, error) { - return []byte(got), nil - })) - if d := compare.Text(got, want); d != "" { - t.Errorf("import failed for %s:\n%s", spn.URI().Filename(), d) - } -} - -func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { - _, srcRng, err := spanToRange(r.data, d.Src) - if err != nil { - t.Fatal(err) - } - fh, err := r.snapshot.GetFile(r.ctx, spn.URI()) - if err != nil { - t.Fatal(err) - } - ident, err := source.Identifier(r.ctx, r.snapshot, fh, srcRng.Start) - if err != nil { - t.Fatalf("failed for %v: %v", d.Src, err) - } - h, err := source.HoverIdentifier(r.ctx, ident) - if err != nil { - t.Fatalf("failed for %v: %v", d.Src, err) - } - hover, err := source.FormatHover(h, r.view.Options()) - if err != nil { - t.Fatal(err) - } - rng := ident.Declaration.MappedRange[0].Range() - if d.IsType { - rng = ident.Type.MappedRange.Range() - hover = "" - } - didSomething := false - if hover != "" { - didSomething = true - tag := fmt.Sprintf("%s-hoverdef", d.Name) - expectHover := string(r.data.Golden(t, tag, d.Src.URI().Filename(), func() ([]byte, error) { - return []byte(hover), nil - })) - hover = tests.StripSubscripts(hover) - expectHover = tests.StripSubscripts(expectHover) - if hover != expectHover { - tests.CheckSameMarkdown(t, hover, expectHover) - - } - } - if !d.OnlyHover { - didSomething = true - if _, defRng, err := spanToRange(r.data, d.Def); err != nil { - t.Fatal(err) - } else if rng != defRng { - t.Errorf("for %v got %v want %v", d.Src, rng, defRng) - } - } - if !didSomething { - t.Errorf("no tests ran for %s", d.Src.URI()) - } -} - -func (r *runner) Implementation(t *testing.T, spn span.Span, impls []span.Span) { - // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go -} - -func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) { - ctx := r.ctx - m, srcRng, err := spanToRange(r.data, src) - if err != nil { - t.Fatal(err) - } - fh, err := r.snapshot.GetFile(r.ctx, src.URI()) - if err != nil { - t.Fatal(err) - } - highlights, err := source.Highlight(ctx, r.snapshot, fh, srcRng.Start) - if err != nil { - t.Errorf("highlight failed for %s: %v", src.URI(), err) - } - if len(highlights) != len(locations) { - t.Fatalf("got %d highlights for highlight at %v:%v:%v, expected %d", len(highlights), src.URI().Filename(), src.Start().Line(), src.Start().Column(), len(locations)) - } - // Check to make sure highlights have a valid range. - var results []span.Span - for i := range highlights { - h, err := m.RangeSpan(highlights[i]) - if err != nil { - t.Fatalf("failed for %v: %v", highlights[i], err) - } - results = append(results, h) - } - // Sort results to make tests deterministic since DocumentHighlight uses a map. - span.SortSpans(results) - // Check to make sure all the expected highlights are found. - for i := range results { - if results[i] != locations[i] { - t.Errorf("want %v, got %v\n", locations[i], results[i]) - } - } -} - -func (r *runner) InlayHints(t *testing.T, src span.Span) { - // TODO(golang/go#53315): add source test -} - -func (r *runner) Hover(t *testing.T, src span.Span, text string) { - ctx := r.ctx - _, srcRng, err := spanToRange(r.data, src) - if err != nil { - t.Fatal(err) - } - fh, err := r.snapshot.GetFile(r.ctx, src.URI()) - if err != nil { - t.Fatal(err) - } - hover, err := source.Hover(ctx, r.snapshot, fh, srcRng.Start) - if err != nil { - t.Errorf("hover failed for %s: %v", src.URI(), err) - } - if text == "" { - if hover != nil { - t.Errorf("want nil, got %v\n", hover) - } - } else { - if hover == nil { - t.Fatalf("want hover result to not be nil") - } - if got := hover.Contents.Value; got != text { - t.Errorf("want %v, got %v\n", got, text) - } - if want, got := srcRng, hover.Range; want != got { - t.Errorf("want range %v, got %v instead", want, got) - } - } -} - -func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { - // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go -} - -func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { - tag := fmt.Sprintf("%s-rename", newText) - - _, srcRng, err := spanToRange(r.data, spn) - if err != nil { - t.Fatal(err) - } - fh, err := r.snapshot.GetFile(r.ctx, spn.URI()) - if err != nil { - t.Fatal(err) - } - changes, _, err := source.Rename(r.ctx, r.snapshot, fh, srcRng.Start, newText) - if err != nil { - renamed := string(r.data.Golden(t, tag, spn.URI().Filename(), func() ([]byte, error) { - return []byte(err.Error()), nil - })) - if err.Error() != renamed { - t.Errorf("rename failed for %s, expected:\n%v\ngot:\n%v\n", newText, renamed, err) - } - return - } - - var res []string - for editURI, edits := range changes { - fh, err := r.snapshot.GetFile(r.ctx, editURI) - if err != nil { - t.Fatal(err) - } - m, err := r.data.Mapper(fh.URI()) - if err != nil { - t.Fatal(err) - } - contents, _, err := source.ApplyProtocolEdits(m, edits) - if err != nil { - t.Fatal(err) - } - if len(changes) > 1 { - filename := filepath.Base(editURI.Filename()) - contents = fmt.Sprintf("%s:\n%s", filename, contents) - } - res = append(res, contents) - } - - // Sort on filename - sort.Strings(res) - - var got string - for i, val := range res { - if i != 0 { - got += "\n" - } - got += val - } - - renamed := string(r.data.Golden(t, tag, spn.URI().Filename(), func() ([]byte, error) { - return []byte(got), nil - })) - - if renamed != got { - t.Errorf("rename failed for %s, expected:\n%v\ngot:\n%v", newText, renamed, got) - } -} - -func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) { - // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go -} - -func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { - // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go -} - -func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go -} - -func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) { - _, rng, err := spanToRange(r.data, spn) - if err != nil { - t.Fatal(err) - } - fh, err := r.snapshot.GetFile(r.ctx, spn.URI()) - if err != nil { - t.Fatal(err) - } - gotSignature, gotActiveParameter, err := source.SignatureHelp(r.ctx, r.snapshot, fh, rng.Start) - if err != nil { - // Only fail if we got an error we did not expect. - if want != nil { - t.Fatalf("failed for %v: %v", spn, err) - } - return - } - if gotSignature == nil { - if want != nil { - t.Fatalf("got nil signature, but expected %v", want) - } - return - } - got := &protocol.SignatureHelp{ - Signatures: []protocol.SignatureInformation{*gotSignature}, - ActiveParameter: uint32(gotActiveParameter), - } - if diff := tests.DiffSignatures(spn, want, got); diff != "" { - t.Error(diff) - } -} - -// These are pure LSP features, no source level functionality to be tested. -func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {} -func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actions []tests.SuggestedFix, want int) {} -func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {} -func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) {} -func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {} -func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {} - -func spanToRange(data *tests.Data, spn span.Span) (*protocol.Mapper, protocol.Range, error) { - m, err := data.Mapper(spn.URI()) - if err != nil { - return nil, protocol.Range{}, err - } - srcRng, err := m.SpanRange(spn) - if err != nil { - return nil, protocol.Range{}, err - } - return m, srcRng, nil -} diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index d9a55c1594c..58bb6a02877 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -144,11 +144,10 @@ type Data struct { } // The Tests interface abstracts a set of implementations of marker -// test operators (such as @hover) appearing +// test operators (such as @hover) appearing in files beneath ../testdata/. // -// There are three implementations: +// There are two implementations: // - *runner in ../cmd/test/check.go, which runs the command-line tool (e.g. "gopls hover") -// - *runner in ../source/source_test.go, which makes direct calls (e.g. to source.Hover) // - *runner in ../lsp_test.go, which makes LSP requests (textDocument/hover) to a gopls server. // // Not all implementations implement all methods. diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index 454710b56a0..ec7f80c3ef3 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -275,14 +275,6 @@ func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls return "" } -func ToProtocolCompletionItems(items []completion.CompletionItem) []protocol.CompletionItem { - var result []protocol.CompletionItem - for _, item := range items { - result = append(result, ToProtocolCompletionItem(item)) - } - return result -} - func ToProtocolCompletionItem(item completion.CompletionItem) protocol.CompletionItem { pItem := protocol.CompletionItem{ Label: item.Label, From 7d4ba2fe8e53d15bdd02b2dc6e5936eb1d720ffc Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 19 Jan 2023 15:19:32 -0500 Subject: [PATCH 658/723] gopls/release: remove unused functionality from release script Narrow the scope of the release script to simply verifying that the release is ready. Remove: - checks for the release branch; the current branch doesn't matter for tagging - the -release and -remote flags, since we won't be using this script for performing the release Updates golang/go#57643 Change-Id: I80a5e367ad5b7df1d85f3af023dc10ccef242702 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462815 Reviewed-by: Dylan Le TryBot-Result: Gopher Robot Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/release/release.go | 67 ++-------------------------------------- 1 file changed, 3 insertions(+), 64 deletions(-) diff --git a/gopls/release/release.go b/gopls/release/release.go index e78a2674710..dab95822eb6 100644 --- a/gopls/release/release.go +++ b/gopls/release/release.go @@ -18,7 +18,6 @@ import ( "io/ioutil" "log" "os" - "os/user" "path/filepath" "strconv" "strings" @@ -30,11 +29,7 @@ import ( "golang.org/x/tools/go/packages" ) -var ( - versionFlag = flag.String("version", "", "version to tag") - remoteFlag = flag.String("remote", "", "remote to which to push the tag") - releaseFlag = flag.Bool("release", false, "release is true if you intend to tag and push a release") -) +var versionFlag = flag.String("version", "", "version to tag") func main() { flag.Parse() @@ -51,13 +46,6 @@ func main() { if semver.Build(*versionFlag) != "" { log.Fatalf("unexpected build suffix: %s", *versionFlag) } - if *releaseFlag && *remoteFlag == "" { - log.Fatalf("must provide -remote flag if releasing") - } - user, err := user.Current() - if err != nil { - log.Fatal(err) - } // Validate that the user is running the program from the gopls module. wd, err := os.Getwd() if err != nil { @@ -66,11 +54,6 @@ func main() { if filepath.Base(wd) != "gopls" { log.Fatalf("must run from the gopls module") } - // Confirm that they are running on a branch with a name following the - // format of "gopls-release-branch..". - if err := validateBranchName(*versionFlag); err != nil { - log.Fatal(err) - } // Confirm that they have updated the hardcoded version. if err := validateHardcodedVersion(*versionFlag); err != nil { log.Fatal(err) @@ -79,52 +62,8 @@ func main() { if err := validateGoModFile(wd); err != nil { log.Fatal(err) } - earlyExitMsg := "Validated that the release is ready. Exiting without tagging and publishing." - if !*releaseFlag { - fmt.Println(earlyExitMsg) - os.Exit(0) - } - fmt.Println(`Proceeding to tagging and publishing the release... -Please enter Y if you wish to proceed or anything else if you wish to exit.`) - // Accept and process user input. - var input string - fmt.Scanln(&input) - switch input { - case "Y": - fmt.Println("Proceeding to tagging and publishing the release.") - default: - fmt.Println(earlyExitMsg) - os.Exit(0) - } - // To tag the release: - // $ git -c user.email=username@google.com tag -a -m “” gopls/v..- - goplsVersion := fmt.Sprintf("gopls/%s", *versionFlag) - cmd := exec.Command("git", "-c", fmt.Sprintf("user.email=%s@google.com", user.Username), "tag", "-a", "-m", fmt.Sprintf("%q", goplsVersion), goplsVersion) - if err := cmd.Run(); err != nil { - log.Fatal(err) - } - // Push the tag to the remote: - // $ git push gopls/v..-pre.1 - cmd = exec.Command("git", "push", *remoteFlag, goplsVersion) - if err := cmd.Run(); err != nil { - log.Fatal(err) - } -} - -// validateBranchName reports whether the user's current branch name is of the -// form "gopls-release-branch..". It reports an error if not. -func validateBranchName(version string) error { - cmd := exec.Command("git", "branch", "--show-current") - stdout, err := cmd.Output() - if err != nil { - return err - } - branch := strings.TrimSpace(string(stdout)) - expectedBranch := fmt.Sprintf("gopls-release-branch.%s", strings.TrimPrefix(semver.MajorMinor(version), "v")) - if branch != expectedBranch { - return fmt.Errorf("expected release branch %s, got %s", expectedBranch, branch) - } - return nil + fmt.Println("Validated that the release is ready.") + os.Exit(0) } // validateHardcodedVersion reports whether the version hardcoded in the gopls From ff9bea528a4d82b263956174babe5af1ffb35a49 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 19 Jan 2023 16:55:09 -0500 Subject: [PATCH 659/723] gopls/internal/lsp/cmd/test: signpost future cleanups Also, remove dead code. Updates golang/go#54845 Change-Id: Ida96b91b878bfb0598dbe59e4d704771f83e0c0a Reviewed-on: https://go-review.googlesource.com/c/tools/+/462817 Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cmd/test/cmdtest.go | 117 ++++++++++++------------- gopls/internal/lsp/completion_test.go | 24 ++++- gopls/internal/lsp/tests/normalizer.go | 16 ---- gopls/internal/lsp/tests/util.go | 19 ---- 4 files changed, 79 insertions(+), 97 deletions(-) diff --git a/gopls/internal/lsp/cmd/test/cmdtest.go b/gopls/internal/lsp/cmd/test/cmdtest.go index a95875d8f90..cc0ea8623a8 100644 --- a/gopls/internal/lsp/cmd/test/cmdtest.go +++ b/gopls/internal/lsp/cmd/test/cmdtest.go @@ -30,7 +30,7 @@ import ( // TestCommandLine runs the marker tests in files beneath testdata/ using // implementations of each of the marker operations (e.g. @hover) that -// fork+exec the gopls command. +// call the main function of the gopls command within this process. func TestCommandLine(t *testing.T, testdata string, options func(*source.Options)) { // On Android, the testdata directory is not copied to the runner. if runtime.GOOS == "android" { @@ -39,7 +39,7 @@ func TestCommandLine(t *testing.T, testdata string, options func(*source.Options tests.RunTests(t, testdata, false, func(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) ts := newTestServer(ctx, options) - tests.Run(t, NewRunner(datum, ctx, ts.Addr, options), datum) + tests.Run(t, newRunner(datum, ctx, ts.Addr, options), datum) cmd.CloseTestConnections(ctx) }) } @@ -51,7 +51,29 @@ func newTestServer(ctx context.Context, options func(*source.Options)) *serverte return servertest.NewTCPServer(ctx, ss, nil) } -// runner implements tests.Tests by fork+execing the gopls command. +// runner implements tests.Tests by invoking the gopls command. +// +// TODO(golang/go#54845): We don't plan to implement all the methods +// of tests.Test. Indeed, we'd like to delete the methods that are +// implemented because the two problems they solve are best addressed +// in other ways: +// +// 1. They provide coverage of the behavior of the server, but this +// coverage is almost identical to the coverage provided by +// executing the same tests by making LSP RPCs directly (see +// lsp_test), and the latter is more efficient. When they do +// differ, it is a pain for maintainers. +// +// 2. They provide coverage of the client-side code of the +// command-line tool, which turns arguments into an LSP request and +// prints the results. But this coverage could be more directly and +// efficiently achieved by running a small number of tests tailored +// to exercise the client-side code, not the server behavior. +// +// Once that's done, tests.Tests would have only a single +// implementation (LSP), and we could refactor the marker tests +// so that they more closely resemble self-contained regtests, +// as described in #54845. type runner struct { data *tests.Data ctx context.Context @@ -60,7 +82,7 @@ type runner struct { remote string } -func NewRunner(data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner { +func newRunner(data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner { return &runner{ data: data, ctx: ctx, @@ -70,60 +92,15 @@ func NewRunner(data *tests.Data, ctx context.Context, remote string, options fun } } -func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) { - //TODO: add command line completions tests when it works -} - -func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - //TODO: add command line completions tests when it works -} - -func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) { - //TODO: add command line completions tests when it works -} - -func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - //TODO: add command line completions tests when it works -} - -func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - //TODO: add command line completions tests when it works -} - -func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - //TODO: add command line completions tests when it works -} - -func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - //TODO: add command line completions tests when it works -} - -func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - //TODO: add command line completions tests when it works -} - -func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) { - //TODO: function extraction not supported on command line -} - -func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) { - //TODO: function extraction not supported on command line -} - -func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) { - //TODO: import addition not supported on command line -} - -func (r *runner) Hover(t *testing.T, spn span.Span, info string) { - //TODO: hovering not supported on command line -} - -func (r *runner) InlayHints(t *testing.T, spn span.Span) { - // TODO: inlayHints not supported on command line -} - -func (r *runner) SelectionRanges(t *testing.T, spn span.Span) {} - +// runGoplsCmd returns the stdout and stderr of a gopls command. +// +// It does not fork+exec gopls, but in effect calls its main function, +// and thus the subcommand's Application.Run method, within this process. +// +// Stdout and stderr are temporarily redirected: not concurrency-safe! +// +// The "exit code" is printed to stderr but not returned. +// Invalid flags cause process exit. func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) { rStdout, wStdout, err := os.Pipe() if err != nil { @@ -175,6 +152,26 @@ func (r *runner) Normalize(s string) string { return tests.Normalize(s, r.normalizers) } -func (r *runner) NormalizePrefix(s string) string { - return tests.NormalizePrefix(s, r.normalizers) +// Unimplemented methods of tests.Tests (see comment at runner): + +func (*runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {} +func (*runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { +} +func (*runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) { +} +func (*runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { +} +func (*runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { +} +func (*runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { +} +func (*runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { +} +func (*runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { } +func (*runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {} +func (*runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) {} +func (*runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {} +func (*runner) Hover(t *testing.T, spn span.Span, info string) {} +func (*runner) InlayHints(t *testing.T, spn span.Span) {} +func (*runner) SelectionRanges(t *testing.T, spn span.Span) {} diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index 22578467dbf..8211cc51c62 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -5,11 +5,13 @@ package lsp import ( + "fmt" "strings" "testing" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/completion" "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/span" ) @@ -111,10 +113,28 @@ func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completi func expected(t *testing.T, test tests.Completion, items tests.CompletionItems) []protocol.CompletionItem { t.Helper() + toProtocolCompletionItem := func(item *completion.CompletionItem) protocol.CompletionItem { + pItem := protocol.CompletionItem{ + Label: item.Label, + Kind: item.Kind, + Detail: item.Detail, + Documentation: item.Documentation, + InsertText: item.InsertText, + TextEdit: &protocol.TextEdit{ + NewText: item.Snippet(), + }, + // Negate score so best score has lowest sort text like real API. + SortText: fmt.Sprint(-item.Score), + } + if pItem.InsertText == "" { + pItem.InsertText = pItem.Label + } + return pItem + } + var want []protocol.CompletionItem for _, pos := range test.CompletionItems { - item := items[pos] - want = append(want, tests.ToProtocolCompletionItem(*item)) + want = append(want, toProtocolCompletionItem(items[pos])) } return want } diff --git a/gopls/internal/lsp/tests/normalizer.go b/gopls/internal/lsp/tests/normalizer.go index 77d9e66a8ed..9c5d7b9c82f 100644 --- a/gopls/internal/lsp/tests/normalizer.go +++ b/gopls/internal/lsp/tests/normalizer.go @@ -41,22 +41,6 @@ func CollectNormalizers(exported *packagestest.Exported) []Normalizer { return normalizers } -// NormalizePrefix normalizes a single path at the front of the input string. -func NormalizePrefix(s string, normalizers []Normalizer) string { - for _, n := range normalizers { - if t := strings.TrimPrefix(s, n.path); t != s { - return n.fragment + t - } - if t := strings.TrimPrefix(s, n.slashed); t != s { - return n.fragment + t - } - if t := strings.TrimPrefix(s, n.escaped); t != s { - return n.fragment + t - } - } - return s -} - // Normalize replaces all paths present in s with just the fragment portion // this is used to make golden files not depend on the temporary paths of the files func Normalize(s string, normalizers []Normalizer) string { diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index ec7f80c3ef3..cdbd12e89e0 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -275,25 +275,6 @@ func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls return "" } -func ToProtocolCompletionItem(item completion.CompletionItem) protocol.CompletionItem { - pItem := protocol.CompletionItem{ - Label: item.Label, - Kind: item.Kind, - Detail: item.Detail, - Documentation: item.Documentation, - InsertText: item.InsertText, - TextEdit: &protocol.TextEdit{ - NewText: item.Snippet(), - }, - // Negate score so best score has lowest sort text like real API. - SortText: fmt.Sprint(-item.Score), - } - if pItem.InsertText == "" { - pItem.InsertText = pItem.Label - } - return pItem -} - func FilterBuiltins(src span.Span, items []protocol.CompletionItem) []protocol.CompletionItem { var ( got []protocol.CompletionItem From 271e621c9735573bbab95ad33f04333a75c97f95 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 19 Jan 2023 11:16:11 -0500 Subject: [PATCH 660/723] internal/lockedfile/internal/filelock: fix aix build tag aix uses the fnctl-based fallback implementation, not flock, since its flock is susceptible to the problem described at https://en.wikipedia.org/wiki/File_locking: All fcntl locks associated with a file for a given process are removed when any file descriptor for that file is closed by that process. Change-Id: I266bbf80c126765b302fbc713c47eb6000ec7be1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462657 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan Reviewed-by: Bryan Mills --- internal/lockedfile/internal/filelock/filelock_other.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/lockedfile/internal/filelock/filelock_other.go b/internal/lockedfile/internal/filelock/filelock_other.go index 1333edf7514..a108915bbb1 100644 --- a/internal/lockedfile/internal/filelock/filelock_other.go +++ b/internal/lockedfile/internal/filelock/filelock_other.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !(darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd) && !plan9 && !windows -// +build !darwin,!dragonfly,!freebsd,!illumos,!linux,!netbsd,!openbsd,!plan9,!windows +//go:build !(aix || darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd) && !plan9 && !windows +// +build !aix,!darwin,!dragonfly,!freebsd,!illumos,!linux,!netbsd,!openbsd,!plan9,!windows package filelock From a7f033afe52dbc28c02f411b8727c4c2e379b0e0 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 19 Jan 2023 18:12:18 -0500 Subject: [PATCH 661/723] gopls/internal/lsp: consolidate the FileHandle API Clean up several aspects of the FileHandle APIs. - Eliminate the VersionedFileHandle. There is no need for this specialization, when we already had the closedFile wrapper to make an on-disk file satisfy the Versioning interface. - Remove the VersionedFileIdentity concept. This was an artifact of an earlier time when we stored files across sessions, views, and snapshots. In all remaining uses, a snapshot is implied. - Clean up now-unnecessary APIs accordingly. - Rename cache.fileHandle and cache.overlay to cache.DiskFile and cache.Overlay. There was some convenience to exporting Overlay, and DiskFile was exported for symmetry. - Remove a bunch of unnecessary test boilerplate. - Remove the link from Overlay to Session, as it was only necessary for debug templates. Change-Id: I3cbf599c260d8e53c8ace913bbf92b2c6f054d3a Reviewed-on: https://go-review.googlesource.com/c/tools/+/462818 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/cache/cache.go | 53 ++++----- gopls/internal/lsp/cache/load.go | 4 +- gopls/internal/lsp/cache/maps.go | 10 +- gopls/internal/lsp/cache/mod.go | 2 +- gopls/internal/lsp/cache/mod_tidy.go | 2 +- gopls/internal/lsp/cache/mod_vuln.go | 2 +- gopls/internal/lsp/cache/session.go | 110 +++++------------- gopls/internal/lsp/cache/snapshot.go | 66 +++++------ gopls/internal/lsp/cache/view.go | 6 +- gopls/internal/lsp/cache/view_test.go | 2 +- gopls/internal/lsp/cache/workspace_test.go | 86 -------------- gopls/internal/lsp/code_action.go | 4 +- gopls/internal/lsp/command.go | 8 +- gopls/internal/lsp/debug/serve.go | 8 +- gopls/internal/lsp/diagnostics.go | 16 +-- gopls/internal/lsp/general.go | 4 +- gopls/internal/lsp/mod/diagnostics.go | 18 +-- gopls/internal/lsp/rename.go | 2 +- gopls/internal/lsp/server.go | 2 +- gopls/internal/lsp/source/add_import.go | 2 +- gopls/internal/lsp/source/diagnostics.go | 12 +- gopls/internal/lsp/source/fix.go | 8 +- gopls/internal/lsp/source/gc_annotations.go | 6 +- gopls/internal/lsp/source/known_packages.go | 2 +- gopls/internal/lsp/source/stub.go | 2 +- gopls/internal/lsp/source/view.go | 50 +++----- gopls/internal/lsp/template/completion.go | 2 +- .../internal/lsp/template/implementations.go | 4 +- gopls/internal/lsp/template/parse.go | 2 +- gopls/internal/lsp/work/completion.go | 2 +- gopls/internal/lsp/work/diagnostics.go | 12 +- gopls/test/debug/debug_test.go | 52 +-------- 32 files changed, 175 insertions(+), 386 deletions(-) delete mode 100644 gopls/internal/lsp/cache/workspace_test.go diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go index 88d1b435bdf..ef56b12efe5 100644 --- a/gopls/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -50,7 +50,7 @@ func New(fset *token.FileSet, store *memoize.Store) *Cache { id: strconv.FormatInt(index, 10), fset: fset, store: store, - fileContent: map[span.URI]*fileHandle{}, + fileContent: map[span.URI]*DiskFile{}, } return c } @@ -62,30 +62,40 @@ type Cache struct { store *memoize.Store fileMu sync.Mutex - fileContent map[span.URI]*fileHandle + fileContent map[span.URI]*DiskFile } -type fileHandle struct { - modTime time.Time +// A DiskFile is a file on the filesystem, or a failure to read one. +// It implements the source.FileHandle interface. +type DiskFile struct { uri span.URI + modTime time.Time content []byte hash source.Hash err error } -func (h *fileHandle) Saved() bool { - return true +func (h *DiskFile) URI() span.URI { return h.uri } + +func (h *DiskFile) FileIdentity() source.FileIdentity { + return source.FileIdentity{ + URI: h.uri, + Hash: h.hash, + } } -// GetFile stats and (maybe) reads the file, updates the cache, and returns it. -func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { - return c.getFile(ctx, uri) +func (h *DiskFile) Saved() bool { return true } +func (h *DiskFile) Version() int32 { return 0 } + +func (h *DiskFile) Read() ([]byte, error) { + return h.content, h.err } -func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error) { +// GetFile stats and (maybe) reads the file, updates the cache, and returns it. +func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { fi, statErr := os.Stat(uri.Filename()) if statErr != nil { - return &fileHandle{ + return &DiskFile{ err: statErr, uri: uri, }, nil @@ -127,7 +137,7 @@ func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error) // ioLimit limits the number of parallel file reads per process. var ioLimit = make(chan struct{}, 128) -func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, error) { +func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*DiskFile, error) { select { case ioLimit <- struct{}{}: case <-ctx.Done(): @@ -143,7 +153,7 @@ func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, e if err != nil { content = nil // just in case } - return &fileHandle{ + return &DiskFile{ modTime: fi.ModTime(), uri: uri, content: content, @@ -166,27 +176,12 @@ func NewSession(ctx context.Context, c *Cache, optionsOverrides func(*source.Opt cache: c, gocmdRunner: &gocommand.Runner{}, options: options, - overlays: make(map[span.URI]*overlay), + overlays: make(map[span.URI]*Overlay), } event.Log(ctx, "New session", KeyCreateSession.Of(s)) return s } -func (h *fileHandle) URI() span.URI { - return h.uri -} - -func (h *fileHandle) FileIdentity() source.FileIdentity { - return source.FileIdentity{ - URI: h.uri, - Hash: h.hash, - } -} - -func (h *fileHandle) Read() ([]byte, error) { - return h.content, h.err -} - var cacheIndex, sessionIndex, viewIndex int64 func (c *Cache) ID() string { return c.id } diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index c4e0296a3c6..1d69504a95e 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -335,7 +335,7 @@ https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` rootMod = uri.Filename() } rootDir := filepath.Dir(rootMod) - nestedModules := make(map[string][]source.VersionedFileHandle) + nestedModules := make(map[string][]source.FileHandle) for _, fh := range openFiles { mod, err := findRootPattern(ctx, filepath.Dir(fh.URI().Filename()), "go.mod", s) if err != nil { @@ -375,7 +375,7 @@ See https://github.com/golang/tools/blob/master/gopls/doc/workspace.md for more return nil, nil } -func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.VersionedFileHandle) []*source.Diagnostic { +func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.FileHandle) []*source.Diagnostic { var srcDiags []*source.Diagnostic for _, fh := range files { // Place the diagnostics on the package or module declarations. diff --git a/gopls/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go index 5cbcaf78a97..baa0debc174 100644 --- a/gopls/internal/lsp/cache/maps.go +++ b/gopls/internal/lsp/cache/maps.go @@ -39,21 +39,21 @@ func (m filesMap) Destroy() { m.impl.Destroy() } -func (m filesMap) Get(key span.URI) (source.VersionedFileHandle, bool) { +func (m filesMap) Get(key span.URI) (source.FileHandle, bool) { value, ok := m.impl.Get(key) if !ok { return nil, false } - return value.(source.VersionedFileHandle), true + return value.(source.FileHandle), true } -func (m filesMap) Range(do func(key span.URI, value source.VersionedFileHandle)) { +func (m filesMap) Range(do func(key span.URI, value source.FileHandle)) { m.impl.Range(func(key, value interface{}) { - do(key.(span.URI), value.(source.VersionedFileHandle)) + do(key.(span.URI), value.(source.FileHandle)) }) } -func (m filesMap) Set(key span.URI, value source.VersionedFileHandle) { +func (m filesMap) Set(key span.URI, value source.FileHandle) { m.impl.Set(key, value, nil) } diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index b198dffb392..50f557ddf5e 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -188,7 +188,7 @@ func (s *snapshot) goSum(ctx context.Context, modURI span.URI) []byte { var sumFH source.FileHandle = s.FindFile(sumURI) if sumFH == nil { var err error - sumFH, err = s.view.cache.getFile(ctx, sumURI) + sumFH, err = s.view.cache.GetFile(ctx, sumURI) if err != nil { return nil } diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go index 385b0143795..def10d55fb6 100644 --- a/gopls/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -51,7 +51,7 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc if err != nil { return nil, err } - if _, ok := fh.(*overlay); ok { + if _, ok := fh.(*Overlay); ok { if info, _ := os.Stat(uri.Filename()); info == nil { return nil, source.ErrNoModOnDisk } diff --git a/gopls/internal/lsp/cache/mod_vuln.go b/gopls/internal/lsp/cache/mod_vuln.go index b16c8c57ba7..88d1a1cb4d2 100644 --- a/gopls/internal/lsp/cache/mod_vuln.go +++ b/gopls/internal/lsp/cache/mod_vuln.go @@ -37,7 +37,7 @@ func (s *snapshot) ModVuln(ctx context.Context, modURI span.URI) (*govulncheck.R if err != nil { return nil, err } - if _, ok := fh.(*overlay); ok { + if _, ok := fh.(*Overlay); ok { if info, _ := os.Stat(modURI.Filename()); info == nil { return nil, source.ErrNoModOnDisk } diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 82938732581..ab42d291cc1 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -39,11 +39,12 @@ type Session struct { viewMap map[span.URI]*View // map of URI->best view overlayMu sync.Mutex - overlays map[span.URI]*overlay + overlays map[span.URI]*Overlay } -type overlay struct { - session *Session +// An Overlay is a file open in the editor. +// It implements the source.FileSource interface. +type Overlay struct { uri span.URI text []byte hash source.Hash @@ -55,69 +56,19 @@ type overlay struct { saved bool } -func (o *overlay) Read() ([]byte, error) { - return o.text, nil -} +func (o *Overlay) URI() span.URI { return o.uri } -func (o *overlay) FileIdentity() source.FileIdentity { +func (o *Overlay) FileIdentity() source.FileIdentity { return source.FileIdentity{ URI: o.uri, Hash: o.hash, } } -func (o *overlay) VersionedFileIdentity() source.VersionedFileIdentity { - return source.VersionedFileIdentity{ - URI: o.uri, - SessionID: o.session.id, - Version: o.version, - } -} - -func (o *overlay) Kind() source.FileKind { - return o.kind -} - -func (o *overlay) URI() span.URI { - return o.uri -} - -func (o *overlay) Version() int32 { - return o.version -} - -func (o *overlay) Session() string { - return o.session.id -} - -func (o *overlay) Saved() bool { - return o.saved -} - -// closedFile implements LSPFile for a file that the editor hasn't told us about. -type closedFile struct { - source.FileHandle -} - -func (c *closedFile) VersionedFileIdentity() source.VersionedFileIdentity { - return source.VersionedFileIdentity{ - URI: c.FileHandle.URI(), - SessionID: "", - Version: 0, - } -} - -func (c *closedFile) Saved() bool { - return true -} - -func (c *closedFile) Session() string { - return "" -} - -func (c *closedFile) Version() int32 { - return 0 -} +func (o *Overlay) Read() ([]byte, error) { return o.text, nil } +func (o *Overlay) Version() int32 { return o.version } +func (o *Overlay) Saved() bool { return o.saved } +func (o *Overlay) Kind() source.FileKind { return o.kind } // ID returns the unique identifier for this session on this server. func (s *Session) ID() string { return s.id } @@ -442,7 +393,7 @@ func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModifica type fileChange struct { content []byte exists bool - fileHandle source.VersionedFileHandle + fileHandle source.FileHandle // isUnchanged indicates whether the file action is one that does not // change the actual contents of the file. Opens and closes should not @@ -579,16 +530,19 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif isUnchanged: isUnchanged, } } else { - fsFile, err := s.cache.getFile(ctx, c.URI) + fsFile, err := s.cache.GetFile(ctx, c.URI) if err != nil { return nil, nil, err } content, err := fsFile.Read() - fh := &closedFile{fsFile} + if err != nil { + // Ignore the error: the file may be deleted. + content = nil + } views[view][c.URI] = &fileChange{ content: content, exists: err == nil, - fileHandle: fh, + fileHandle: fsFile, isUnchanged: isUnchanged, } } @@ -701,7 +655,7 @@ func knownFilesInDir(ctx context.Context, snapshots []*snapshot, dir span.URI) m } // Precondition: caller holds s.viewMu lock. -func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModification) (map[span.URI]*overlay, error) { +func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModification) (map[span.URI]*Overlay, error) { s.overlayMu.Lock() defer s.overlayMu.Unlock() @@ -768,15 +722,14 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif } sameContentOnDisk = true default: - fh, err := s.cache.getFile(ctx, c.URI) + fh, err := s.cache.GetFile(ctx, c.URI) if err != nil { return nil, err } _, readErr := fh.Read() sameContentOnDisk = (readErr == nil && fh.FileIdentity().Hash == hash) } - o = &overlay{ - session: s, + o = &Overlay{ uri: c.URI, version: version, text: text, @@ -801,7 +754,7 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif // Get the overlays for each change while the session's overlay map is // locked. - overlays := make(map[span.URI]*overlay) + overlays := make(map[span.URI]*Overlay) for _, c := range changes { if o, ok := s.overlays[c.URI]; ok { overlays[c.URI] = o @@ -812,29 +765,22 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif // GetFile returns a handle for the specified file. func (s *Session) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { - if overlay := s.readOverlay(uri); overlay != nil { + s.overlayMu.Lock() + overlay, ok := s.overlays[uri] + s.overlayMu.Unlock() + if ok { return overlay, nil } - // Fall back to the cache-level file system. - return s.cache.getFile(ctx, uri) -} - -func (s *Session) readOverlay(uri span.URI) *overlay { - s.overlayMu.Lock() - defer s.overlayMu.Unlock() - if overlay, ok := s.overlays[uri]; ok { - return overlay - } - return nil + return s.cache.GetFile(ctx, uri) } // Overlays returns a slice of file overlays for the session. -func (s *Session) Overlays() []source.Overlay { +func (s *Session) Overlays() []*Overlay { s.overlayMu.Lock() defer s.overlayMu.Unlock() - overlays := make([]source.Overlay, 0, len(s.overlays)) + overlays := make([]*Overlay, 0, len(s.overlays)) for _, overlay := range s.overlays { overlays = append(overlays, overlay) } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index b78962e15b1..0eef84efe95 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -268,12 +268,12 @@ func (s *snapshot) WorkFile() span.URI { return s.view.effectiveGOWORK() } -func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle { +func (s *snapshot) Templates() map[span.URI]source.FileHandle { s.mu.Lock() defer s.mu.Unlock() - tmpls := map[span.URI]source.VersionedFileHandle{} - s.files.Range(func(k span.URI, fh source.VersionedFileHandle) { + tmpls := map[span.URI]source.FileHandle{} + s.files.Range(func(k span.URI, fh source.FileHandle) { if s.view.FileKind(fh) == source.Tmpl { tmpls[k] = fh } @@ -602,8 +602,8 @@ func (s *snapshot) buildOverlay() map[string][]byte { defer s.mu.Unlock() overlays := make(map[string][]byte) - s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { - overlay, ok := fh.(*overlay) + s.files.Range(func(uri span.URI, fh source.FileHandle) { + overlay, ok := fh.(*Overlay) if !ok { return } @@ -879,7 +879,7 @@ func (s *snapshot) collectAllKnownSubdirs(ctx context.Context) { s.knownSubdirs.Destroy() s.knownSubdirs = newKnownDirsSet() s.knownSubdirsPatternCache = "" - s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { + s.files.Range(func(uri span.URI, fh source.FileHandle) { s.addKnownSubdirLocked(uri, dirs) }) } @@ -963,7 +963,7 @@ func (s *snapshot) knownFilesInDir(ctx context.Context, dir span.URI) []span.URI s.mu.Lock() defer s.mu.Unlock() - s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { + s.files.Range(func(uri span.URI, fh source.FileHandle) { if source.InDir(dir.Filename(), uri.Filename()) { files = append(files, uri) } @@ -995,9 +995,9 @@ func (s *snapshot) ActiveMetadata(ctx context.Context) ([]*source.Metadata, erro // Symbols extracts and returns the symbols for each file in all the snapshot's views. func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { // Read the set of Go files out of the snapshot. - var goFiles []source.VersionedFileHandle + var goFiles []source.FileHandle s.mu.Lock() - s.files.Range(func(uri span.URI, f source.VersionedFileHandle) { + s.files.Range(func(uri span.URI, f source.FileHandle) { if s.View().FileKind(f) == source.Go { goFiles = append(goFiles, f) } @@ -1153,7 +1153,7 @@ func (s *snapshot) isWorkspacePackage(id PackageID) bool { return ok } -func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle { +func (s *snapshot) FindFile(uri span.URI) source.FileHandle { uri, _ = s.view.canonicalURI(uri, true) s.mu.Lock() @@ -1163,12 +1163,12 @@ func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle { return result } -// GetVersionedFile returns a File for the given URI. If the file is unknown it -// is added to the managed set. +// GetFile returns a File for the given URI. If the file is unknown it is added +// to the managed set. // -// GetVersionedFile succeeds even if the file does not exist. A non-nil error return +// GetFile succeeds even if the file does not exist. A non-nil error return // indicates some type of internal error, for example if ctx is cancelled. -func (s *snapshot) GetVersionedFile(ctx context.Context, uri span.URI) (source.VersionedFileHandle, error) { +func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { uri, _ = s.view.canonicalURI(uri, true) s.mu.Lock() @@ -1178,18 +1178,12 @@ func (s *snapshot) GetVersionedFile(ctx context.Context, uri span.URI) (source.V return fh, nil } - fh, err := s.view.cache.getFile(ctx, uri) // read the file + fh, err := s.view.cache.GetFile(ctx, uri) // read the file if err != nil { return nil, err } - closed := &closedFile{fh} - s.files.Set(uri, closed) - return closed, nil -} - -// GetFile implements the fileSource interface by wrapping GetVersionedFile. -func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { - return s.GetVersionedFile(ctx, uri) + s.files.Set(uri, fh) + return fh, nil } func (s *snapshot) IsOpen(uri span.URI) bool { @@ -1199,12 +1193,12 @@ func (s *snapshot) IsOpen(uri span.URI) bool { } -func (s *snapshot) openFiles() []source.VersionedFileHandle { +func (s *snapshot) openFiles() []source.FileHandle { s.mu.Lock() defer s.mu.Unlock() - var open []source.VersionedFileHandle - s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { + var open []source.FileHandle + s.files.Range(func(uri span.URI, fh source.FileHandle) { if isFileOpen(fh) { open = append(open, fh) } @@ -1217,8 +1211,8 @@ func (s *snapshot) isOpenLocked(uri span.URI) bool { return isFileOpen(fh) } -func isFileOpen(fh source.VersionedFileHandle) bool { - _, open := fh.(*overlay) +func isFileOpen(fh source.FileHandle) bool { + _, open := fh.(*Overlay) return open } @@ -1479,14 +1473,14 @@ func (s *snapshot) reloadOrphanedOpenFiles(ctx context.Context) error { return nil } -func (s *snapshot) orphanedOpenFiles() []source.VersionedFileHandle { +func (s *snapshot) orphanedOpenFiles() []source.FileHandle { s.mu.Lock() defer s.mu.Unlock() - var files []source.VersionedFileHandle - s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { + var files []source.FileHandle + s.files.Range(func(uri span.URI, fh source.FileHandle) { // Only consider open files, which will be represented as overlays. - if _, isOverlay := fh.(*overlay); !isOverlay { + if _, isOverlay := fh.(*Overlay); !isOverlay { return } // Don't try to reload metadata for go.mod files. @@ -1716,8 +1710,8 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // The original FileHandle for this URI is cached on the snapshot. originalFH, _ := s.files.Get(uri) var originalOpen, newOpen bool - _, originalOpen = originalFH.(*overlay) - _, newOpen = change.fileHandle.(*overlay) + _, originalOpen = originalFH.(*Overlay) + _, newOpen = change.fileHandle.(*Overlay) anyFileOpenedOrClosed = anyFileOpenedOrClosed || (originalOpen != newOpen) anyFileAdded = anyFileAdded || (originalFH == nil && change.fileHandle != nil) @@ -1999,11 +1993,11 @@ func invalidatedPackageIDs(uri span.URI, known map[span.URI][]PackageID, package // are both overlays, and if the current FileHandle is saved while the original // FileHandle was not saved. func fileWasSaved(originalFH, currentFH source.FileHandle) bool { - c, ok := currentFH.(*overlay) + c, ok := currentFH.(*Overlay) if !ok || c == nil { return true } - o, ok := originalFH.(*overlay) + o, ok := originalFH.(*Overlay) if !ok || o == nil { return c.saved } diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index a4c87c3dddc..ea9016ecec2 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -331,9 +331,9 @@ func (v *View) FileKind(fh source.FileHandle) source.FileKind { // The kind of an unsaved buffer comes from the // TextDocumentItem.LanguageID field in the didChange event, // not from the file name. They may differ. - if o, ok := fh.(source.Overlay); ok { - if o.Kind() != source.UnknownKind { - return o.Kind() + if o, ok := fh.(*Overlay); ok { + if o.kind != source.UnknownKind { + return o.kind } } diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go index 8adfbfaf0f8..4b456810577 100644 --- a/gopls/internal/lsp/cache/view_test.go +++ b/gopls/internal/lsp/cache/view_test.go @@ -96,7 +96,7 @@ module fg rel := fake.RelativeTo(dir) folderURI := span.URIFromPath(rel.AbsPath(test.folder)) excludeNothing := func(string) bool { return false } - got, err := findWorkspaceModFile(ctx, folderURI, &osFileSource{}, excludeNothing) + got, err := findWorkspaceModFile(ctx, folderURI, New(nil, nil), excludeNothing) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/lsp/cache/workspace_test.go b/gopls/internal/lsp/cache/workspace_test.go deleted file mode 100644 index 5f1e13ef124..00000000000 --- a/gopls/internal/lsp/cache/workspace_test.go +++ /dev/null @@ -1,86 +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" - "os" - - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/span" -) - -// osFileSource is a fileSource that just reads from the operating system. -type osFileSource struct { - overlays map[span.URI]fakeOverlay -} - -type fakeOverlay struct { - source.VersionedFileHandle - uri span.URI - content string - err error - saved bool -} - -func (o fakeOverlay) Saved() bool { return o.saved } - -func (o fakeOverlay) Read() ([]byte, error) { - if o.err != nil { - return nil, o.err - } - return []byte(o.content), nil -} - -func (o fakeOverlay) URI() span.URI { - return o.uri -} - -// change updates the file source with the given file content. For convenience, -// empty content signals a deletion. If saved is true, these changes are -// persisted to disk. -func (s *osFileSource) change(ctx context.Context, uri span.URI, content string, saved bool) (*fileChange, error) { - if content == "" { - delete(s.overlays, uri) - if saved { - if err := os.Remove(uri.Filename()); err != nil { - return nil, err - } - } - fh, err := s.GetFile(ctx, uri) - if err != nil { - return nil, err - } - data, err := fh.Read() - return &fileChange{exists: err == nil, content: data, fileHandle: &closedFile{fh}}, nil - } - if s.overlays == nil { - s.overlays = map[span.URI]fakeOverlay{} - } - s.overlays[uri] = fakeOverlay{uri: uri, content: content, saved: saved} - return &fileChange{ - exists: content != "", - content: []byte(content), - fileHandle: s.overlays[uri], - }, nil -} - -func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { - if overlay, ok := s.overlays[uri]; ok { - return overlay, nil - } - fi, statErr := os.Stat(uri.Filename()) - if statErr != nil { - return &fileHandle{ - err: statErr, - uri: uri, - }, nil - } - fh, err := readFile(ctx, uri, fi) - if err != nil { - return nil, err - } - return fh, nil -} diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 8f39ea97ece..ec918b7c7bd 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -366,7 +366,7 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI return actions, nil } -func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.DocumentChanges { +func documentChanges(fh source.FileHandle, edits []protocol.TextEdit) []protocol.DocumentChanges { return []protocol.DocumentChanges{ { TextDocumentEdit: &protocol.TextDocumentEdit{ @@ -410,7 +410,7 @@ func codeActionsForDiagnostic(ctx context.Context, snapshot source.Snapshot, sd for _, fix := range sd.SuggestedFixes { var changes []protocol.DocumentChanges for uri, edits := range fix.Edits { - fh, err := snapshot.GetVersionedFile(ctx, uri) + fh, err := snapshot.GetFile(ctx, uri) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index b9e1fe6dd64..a1ff862cb76 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -70,9 +70,9 @@ type commandConfig struct { // be populated, depending on which configuration is set. See comments in-line // for details. type commandDeps struct { - snapshot source.Snapshot // present if cfg.forURI was set - fh source.VersionedFileHandle // present if cfg.forURI was set - work *progress.WorkDone // present cfg.progress was set + snapshot source.Snapshot // present if cfg.forURI was set + fh source.FileHandle // present if cfg.forURI was set + work *progress.WorkDone // present cfg.progress was set } type commandFunc func(context.Context, commandDeps) error @@ -606,7 +606,7 @@ func (s *Server) runGoModUpdateCommands(ctx context.Context, snapshot source.Sna } func applyFileEdits(ctx context.Context, snapshot source.Snapshot, uri span.URI, newContent []byte) ([]protocol.TextDocumentEdit, error) { - fh, err := snapshot.GetVersionedFile(ctx, uri) + fh, err := snapshot.GetFile(ctx, uri) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go index 6934adf490f..3e6a905197d 100644 --- a/gopls/internal/lsp/debug/serve.go +++ b/gopls/internal/lsp/debug/serve.go @@ -727,7 +727,6 @@ Unknown page {{define "serverlink"}}
    Server {{.}}{{end}} {{define "sessionlink"}}Session {{.}}{{end}} {{define "viewlink"}}View {{.}}{{end}} -{{define "filelink"}}{{.FileIdentity.URI}}{{end}} `)).Funcs(template.FuncMap{ "fuint64": fuint64, "fuint32": fuint32, @@ -876,7 +875,11 @@ From: {{template "cachelink" .Cache.ID}}

    Views

      {{range .Views}}
    • {{.Name}} is {{template "viewlink" .ID}} in {{.Folder}}
    • {{end}}

    Overlays

    -
      {{range .Overlays}}
    • {{template "filelink" .}}
    • {{end}}
    +{{$session := .}} +

    Options

    {{range options .}}

    {{.Name}} {{.Type}}

    @@ -900,7 +903,6 @@ var FileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` {{define "title"}}Overlay {{.FileIdentity.Hash}}{{end}} {{define "body"}} {{with .}} - From: {{template "sessionlink" .Session}}
    URI: {{.URI}}
    Identifier: {{.FileIdentity.Hash}}
    Version: {{.Version}}
    diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index e7b290d3cbb..ef30b6f776b 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -247,16 +247,16 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn }() // common code for dispatching diagnostics - store := func(dsource diagnosticSource, operation string, diagsByFileID map[source.VersionedFileIdentity][]*source.Diagnostic, err error, merge bool) { + store := func(dsource diagnosticSource, operation string, diagsByFile map[span.URI][]*source.Diagnostic, err error, merge bool) { if err != nil { event.Error(ctx, "warning: while "+operation, err, source.SnapshotLabels(snapshot)...) } - for id, diags := range diagsByFileID { - if id.URI == "" { + for uri, diags := range diagsByFile { + if uri == "" { event.Error(ctx, "missing URI while "+operation, fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) continue } - s.storeDiagnostics(snapshot, id.URI, dsource, diags, merge) + s.storeDiagnostics(snapshot, uri, dsource, diags, merge) } } @@ -425,14 +425,14 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, m *s // results. This ensures that the toggling of GC details and clearing of // diagnostics does not race with storing the results here. if enableGCDetails { - for id, diags := range gcReports { - fh := snapshot.FindFile(id.URI) + for uri, diags := range gcReports { + fh := snapshot.FindFile(uri) // Don't publish gc details for unsaved buffers, since the underlying // logic operates on the file on disk. if fh == nil || !fh.Saved() { continue } - s.storeDiagnostics(snapshot, id.URI, gcDetailsSource, diags, true) + s.storeDiagnostics(snapshot, uri, gcDetailsSource, diags, true) } } s.gcOptimizationDetailsMu.Unlock() @@ -543,7 +543,7 @@ func (s *Server) showCriticalErrorStatus(ctx context.Context, snapshot source.Sn // checkForOrphanedFile checks that the given URIs can be mapped to packages. // If they cannot and the workspace is not otherwise unloaded, it also surfaces // a warning, suggesting that the user check the file for build tags. -func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle) *source.Diagnostic { +func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) *source.Diagnostic { // TODO(rfindley): this function may fail to produce a diagnostic for a // variety of reasons, some of which should probably not be ignored. For // example, should this function be tolerant of the case where fh does not diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index 1d7135e57a0..87a86f481b7 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -549,7 +549,7 @@ func (s *Server) handleOptionResults(ctx context.Context, results source.OptionR // We don't want to return errors for benign conditions like wrong file type, // so callers should do if !ok { return err } rather than if err != nil. // The returned cleanup function is non-nil even in case of false/error result. -func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.VersionedFileHandle, bool, func(), error) { +func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.FileHandle, bool, func(), error) { uri := pURI.SpanURI() if !uri.IsFile() { // Not a file URI. Stop processing the request, but don't return an error. @@ -560,7 +560,7 @@ func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI return nil, nil, false, func() {}, err } snapshot, release := view.Snapshot(ctx) - fh, err := snapshot.GetVersionedFile(ctx, uri) + fh, err := snapshot.GetFile(ctx, uri) if err != nil { release() return nil, nil, false, func() {}, err diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 65b3786f10b..3cbe3529ead 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -26,7 +26,7 @@ import ( // Diagnostics returns diagnostics for the modules in the workspace. // // It waits for completion of type-checking of all active packages. -func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { +func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { ctx, done := event.Start(ctx, "mod.Diagnostics", source.SnapshotLabels(snapshot)...) defer done() @@ -35,7 +35,7 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.Vers // UpgradeDiagnostics returns upgrade diagnostics for the modules in the // workspace with known upgrades. -func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { +func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", source.SnapshotLabels(snapshot)...) defer done() @@ -44,31 +44,31 @@ func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[sour // VulnerabilityDiagnostics returns vulnerability diagnostics for the active modules in the // workspace with known vulnerabilites. -func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { +func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", source.SnapshotLabels(snapshot)...) defer done() return collectDiagnostics(ctx, snapshot, ModVulnerabilityDiagnostics) } -func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { - reports := make(map[source.VersionedFileIdentity][]*source.Diagnostic) +func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[span.URI][]*source.Diagnostic, error) { + reports := make(map[span.URI][]*source.Diagnostic) for _, uri := range snapshot.ModFiles() { - fh, err := snapshot.GetVersionedFile(ctx, uri) + fh, err := snapshot.GetFile(ctx, uri) if err != nil { return nil, err } - reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{} + reports[fh.URI()] = []*source.Diagnostic{} diagnostics, err := diagFn(ctx, snapshot, fh) if err != nil { return nil, err } for _, d := range diagnostics { - fh, err := snapshot.GetVersionedFile(ctx, d.URI) + fh, err := snapshot.GetFile(ctx, d.URI) if err != nil { return nil, err } - reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], d) + reports[fh.URI()] = append(reports[fh.URI()], d) } } return reports, nil diff --git a/gopls/internal/lsp/rename.go b/gopls/internal/lsp/rename.go index e9bb2d40033..359d9acd011 100644 --- a/gopls/internal/lsp/rename.go +++ b/gopls/internal/lsp/rename.go @@ -29,7 +29,7 @@ func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*pr var docChanges []protocol.DocumentChanges for uri, e := range edits { - fh, err := snapshot.GetVersionedFile(ctx, uri) + fh, err := snapshot.GetFile(ctx, uri) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go index 82d90dcf4f9..3d42df11948 100644 --- a/gopls/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -136,7 +136,7 @@ func (s *Server) nonstandardRequest(ctx context.Context, method string, params i if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ URI: protocol.URIFromSpanURI(fh.URI()), Diagnostics: toProtocolDiagnostics(diagnostics), - Version: fileID.Version, + Version: fileID.Version(), }); err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/add_import.go b/gopls/internal/lsp/source/add_import.go index c50c1554165..cd8ec7ab70b 100644 --- a/gopls/internal/lsp/source/add_import.go +++ b/gopls/internal/lsp/source/add_import.go @@ -12,7 +12,7 @@ import ( ) // AddImport adds a single import statement to the given file -func AddImport(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, importPath string) ([]protocol.TextEdit, error) { +func AddImport(ctx context.Context, snapshot Snapshot, fh FileHandle, importPath string) ([]protocol.TextEdit, error) { pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index b8fab6a4436..a7350b00ea6 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -73,22 +73,22 @@ func Analyze(ctx context.Context, snapshot Snapshot, pkgid PackageID, includeCon // "gopls/diagnoseFiles" nonstandard request handler. It would be more // efficient to compute the set of packages and TypeCheck and // Analyze them all at once. -func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (VersionedFileIdentity, []*Diagnostic, error) { - fh, err := snapshot.GetVersionedFile(ctx, uri) +func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (FileHandle, []*Diagnostic, error) { + fh, err := snapshot.GetFile(ctx, uri) if err != nil { - return VersionedFileIdentity{}, nil, err + return nil, nil, err } pkg, _, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, NarrowestPackage) if err != nil { - return VersionedFileIdentity{}, nil, err + return nil, nil, err } adiags, err := Analyze(ctx, snapshot, pkg.ID(), false) if err != nil { - return VersionedFileIdentity{}, nil, err + return nil, nil, err } var fileDiags []*Diagnostic // combine load/parse/type + analysis diagnostics CombineDiagnostics(pkg, fh.URI(), adiags, &fileDiags, &fileDiags) - return fh.VersionedFileIdentity(), fileDiags, nil + return fh, fileDiags, nil } // CombineDiagnostics combines and filters list/parse/type diagnostics diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 50f1cb04512..68e5f8ea38a 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -27,7 +27,7 @@ type ( // suggested fixes with their diagnostics, so we have to compute them // separately. Such analyzers should provide a function with a signature of // SuggestedFixFunc. - SuggestedFixFunc func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) + SuggestedFixFunc func(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) singleFileFixFunc func(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) ) @@ -52,7 +52,7 @@ var suggestedFixes = map[string]SuggestedFixFunc{ // singleFile calls analyzers that expect inputs for a single file func singleFile(sf singleFileFixFunc) SuggestedFixFunc { - return func(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { + return func(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { fset, rng, src, file, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng) if err != nil { return nil, nil, err @@ -72,7 +72,7 @@ func SuggestedFixFromCommand(cmd protocol.Command, kind protocol.CodeActionKind) // ApplyFix applies the command's suggested fix to the given file and // range, returning the resulting edits. -func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) { +func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) { handler, ok := suggestedFixes[fix] if !ok { return nil, fmt.Errorf("no suggested fix function for %s", fix) @@ -94,7 +94,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh VersionedFi if !end.IsValid() { end = edit.Pos } - fh, err := snapshot.GetVersionedFile(ctx, span.URIFromPath(tokFile.Name())) + fh, err := snapshot.GetFile(ctx, span.URIFromPath(tokFile.Name())) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/gc_annotations.go b/gopls/internal/lsp/source/gc_annotations.go index d5245c8cdd1..72159e6f46e 100644 --- a/gopls/internal/lsp/source/gc_annotations.go +++ b/gopls/internal/lsp/source/gc_annotations.go @@ -35,7 +35,7 @@ const ( Bounds Annotation = "bounds" ) -func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, m *Metadata) (map[VersionedFileIdentity][]*Diagnostic, error) { +func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, m *Metadata) (map[span.URI][]*Diagnostic, error) { if len(m.CompiledGoFiles) == 0 { return nil, nil } @@ -74,7 +74,7 @@ func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, m *Metadata) if err != nil { return nil, err } - reports := make(map[VersionedFileIdentity][]*Diagnostic) + reports := make(map[span.URI][]*Diagnostic) opts := snapshot.View().Options() var parseError error for _, fn := range files { @@ -93,7 +93,7 @@ func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, m *Metadata) // outside the package can never be taken back. continue } - reports[fh.VersionedFileIdentity()] = diagnostics + reports[fh.URI()] = diagnostics } return reports, parseError } diff --git a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go index d84febe5417..07b4c30a818 100644 --- a/gopls/internal/lsp/source/known_packages.go +++ b/gopls/internal/lsp/source/known_packages.go @@ -24,7 +24,7 @@ import ( // all dot-free paths (standard packages) appear before dotful ones. // // It is part of the gopls.list_known_packages command. -func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle) ([]PackagePath, error) { +func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]PackagePath, error) { // This algorithm is expressed in terms of Metadata, not Packages, // so it doesn't cause or wait for type checking. diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index c2edf08f939..cf360dd41e5 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -25,7 +25,7 @@ import ( "golang.org/x/tools/internal/typeparams" ) -func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { +func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) if err != nil { return nil, nil, fmt.Errorf("GetTypedFile: %w", err) diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 0a96eb376db..4b6fd13900b 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -78,11 +78,7 @@ type Snapshot interface { // FindFile returns the FileHandle for the given URI, if it is already // in the given snapshot. - FindFile(uri span.URI) VersionedFileHandle - - // GetVersionedFile returns the VersionedFileHandle for a given URI, - // initializing it if it is not already part of the snapshot. - GetVersionedFile(ctx context.Context, uri span.URI) (VersionedFileHandle, error) + FindFile(uri span.URI) FileHandle // GetFile returns the FileHandle for a given URI, initializing it if it is // not already part of the snapshot. @@ -99,7 +95,7 @@ type Snapshot interface { IgnoredFile(uri span.URI) bool // Templates returns the .tmpl files - Templates() map[span.URI]VersionedFileHandle + Templates() map[span.URI]FileHandle // ParseGo returns the parsed AST for the file. // If the file is not available, returns nil and an error. @@ -513,12 +509,6 @@ func RemoveIntermediateTestVariants(metas []*Metadata) []*Metadata { var ErrViewExists = errors.New("view already exists for session") -// Overlay is the type for a file held in memory on a session. -type Overlay interface { - Kind() FileKind - VersionedFileHandle -} - // FileModification represents a modification to a file. type FileModification struct { URI span.URI @@ -613,38 +603,26 @@ const ( TypecheckWorkspace ) -type VersionedFileHandle interface { - FileHandle - Version() int32 - Session() string - - // LSPIdentity returns the version identity of a file. - VersionedFileIdentity() VersionedFileIdentity -} - -type VersionedFileIdentity struct { - URI span.URI - - // SessionID is the ID of the LSP session. - SessionID string - - // Version is the version of the file, as specified by the client. It should - // only be set in combination with SessionID. - Version int32 -} - -// FileHandle represents a handle to a specific version of a single file. +// A FileHandle is an interface to files tracked by the LSP session, which may +// be either files read from disk, or open in the editor session (overlays). type FileHandle interface { + // URI is the URI for this file handle. + // TODO(rfindley): this is not actually well-defined. In some cases, there + // may be more than one URI that resolve to the same FileHandle. Which one is + // this? URI() span.URI - // FileIdentity returns a FileIdentity for the file, even if there was an // error reading it. FileIdentity() FileIdentity + // Saved reports whether the file has the same content on disk. + // For on-disk files, this is trivially true. + Saved() bool + // Version returns the file version, as defined by the LSP client. + // For on-disk file handles, Version returns 0. + Version() int32 // Read reads the contents of a file. // If the file is not available, returns a nil slice and an error. Read() ([]byte, error) - // Saved reports whether the file has the same content on disk. - Saved() bool } // A Hash is a cryptographic digest of the contents of a file. diff --git a/gopls/internal/lsp/template/completion.go b/gopls/internal/lsp/template/completion.go index 140c674747d..292563a88cd 100644 --- a/gopls/internal/lsp/template/completion.go +++ b/gopls/internal/lsp/template/completion.go @@ -25,7 +25,7 @@ type completer struct { syms map[string]symbol } -func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) { +func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) { all := New(snapshot.Templates()) var start int // the beginning of the Token (completed or not) syms := make(map[string]symbol) diff --git a/gopls/internal/lsp/template/implementations.go b/gopls/internal/lsp/template/implementations.go index 6c90b68cd5c..ed9b986a76b 100644 --- a/gopls/internal/lsp/template/implementations.go +++ b/gopls/internal/lsp/template/implementations.go @@ -22,7 +22,7 @@ var errRe = regexp.MustCompile(`template.*:(\d+): (.*)`) // Diagnose returns parse errors. There is only one. // The errors are not always helpful. For instance { {end}} // will likely point to the end of the file. -func Diagnose(f source.VersionedFileHandle) []*source.Diagnostic { +func Diagnose(f source.FileHandle) []*source.Diagnostic { // no need for skipTemplate check, as Diagnose is called on the // snapshot's template files buf, err := f.Read() @@ -73,7 +73,7 @@ func Diagnose(f source.VersionedFileHandle) []*source.Diagnostic { // does not understand scoping (if any) in templates. This code is // for definitions, type definitions, and implementations. // Results only for variables and templates. -func Definition(snapshot source.Snapshot, fh source.VersionedFileHandle, loc protocol.Position) ([]protocol.Location, error) { +func Definition(snapshot source.Snapshot, fh source.FileHandle, loc protocol.Position) ([]protocol.Location, error) { x, _, err := symAtPosition(fh, loc) if err != nil { return nil, err diff --git a/gopls/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go index eb644c0a007..a6befdcb928 100644 --- a/gopls/internal/lsp/template/parse.go +++ b/gopls/internal/lsp/template/parse.go @@ -70,7 +70,7 @@ type All struct { // New returns the Parses of the snapshot's tmpl files // (maybe cache these, but then avoiding import cycles needs code rearrangements) -func New(tmpls map[span.URI]source.VersionedFileHandle) *All { +func New(tmpls map[span.URI]source.FileHandle) *All { all := make(map[span.URI]*Parsed) for k, v := range tmpls { buf, err := v.Read() diff --git a/gopls/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go index 223f022a1a5..bcdc2d1f42e 100644 --- a/gopls/internal/lsp/work/completion.go +++ b/gopls/internal/lsp/work/completion.go @@ -18,7 +18,7 @@ import ( "golang.org/x/tools/internal/event" ) -func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, position protocol.Position) (*protocol.CompletionList, error) { +func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.CompletionList, error) { ctx, done := event.Start(ctx, "work.Completion") defer done() diff --git a/gopls/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go index c64027c54b2..cbcc8505512 100644 --- a/gopls/internal/lsp/work/diagnostics.go +++ b/gopls/internal/lsp/work/diagnostics.go @@ -17,30 +17,30 @@ import ( "golang.org/x/tools/internal/event" ) -func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { +func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { ctx, done := event.Start(ctx, "work.Diagnostics", source.SnapshotLabels(snapshot)...) defer done() - reports := map[source.VersionedFileIdentity][]*source.Diagnostic{} + reports := map[span.URI][]*source.Diagnostic{} uri := snapshot.WorkFile() if uri == "" { return nil, nil } - fh, err := snapshot.GetVersionedFile(ctx, uri) + fh, err := snapshot.GetFile(ctx, uri) if err != nil { return nil, err } - reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{} + reports[fh.URI()] = []*source.Diagnostic{} diagnostics, err := DiagnosticsForWork(ctx, snapshot, fh) if err != nil { return nil, err } for _, d := range diagnostics { - fh, err := snapshot.GetVersionedFile(ctx, d.URI) + fh, err := snapshot.GetFile(ctx, d.URI) if err != nil { return nil, err } - reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], d) + reports[fh.URI()] = append(reports[fh.URI()], d) } return reports, nil diff --git a/gopls/test/debug/debug_test.go b/gopls/test/debug/debug_test.go index 9d5d6f0f12b..9bd89288a54 100644 --- a/gopls/test/debug/debug_test.go +++ b/gopls/test/debug/debug_test.go @@ -13,7 +13,6 @@ package debug_test import ( "go/ast" "html/template" - "log" "runtime" "sort" "strings" @@ -23,16 +22,12 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/debug" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/span" ) -type tdata struct { +var templates = map[string]struct { tmpl *template.Template data interface{} // a value of the needed type -} - -var templates = map[string]tdata{ +}{ "MainTmpl": {debug.MainTmpl, &debug.Instance{}}, "DebugTmpl": {debug.DebugTmpl, nil}, "RPCTmpl": {debug.RPCTmpl, &debug.Rpcs{}}, @@ -42,45 +37,9 @@ var templates = map[string]tdata{ "ViewTmpl": {debug.ViewTmpl, &cache.View{}}, "ClientTmpl": {debug.ClientTmpl, &debug.Client{}}, "ServerTmpl": {debug.ServerTmpl, &debug.Server{}}, - //"FileTmpl": {FileTmpl, source.Overlay{}}, // need to construct a source.Overlay in init - "InfoTmpl": {debug.InfoTmpl, "something"}, - "MemoryTmpl": {debug.MemoryTmpl, runtime.MemStats{}}, -} - -// construct a source.Overlay for fileTmpl -type fakeOverlay struct{} - -func (fakeOverlay) Version() int32 { - return 0 -} -func (fakeOverlay) Session() string { - return "" -} -func (fakeOverlay) VersionedFileIdentity() source.VersionedFileIdentity { - return source.VersionedFileIdentity{} -} -func (fakeOverlay) FileIdentity() source.FileIdentity { - return source.FileIdentity{} -} -func (fakeOverlay) Kind() source.FileKind { - return 0 -} -func (fakeOverlay) Read() ([]byte, error) { - return nil, nil -} -func (fakeOverlay) Saved() bool { - return true -} -func (fakeOverlay) URI() span.URI { - return "" -} - -var _ source.Overlay = fakeOverlay{} - -func init() { - log.SetFlags(log.Lshortfile) - var v fakeOverlay - templates["FileTmpl"] = tdata{debug.FileTmpl, v} + "FileTmpl": {debug.FileTmpl, &cache.Overlay{}}, + "InfoTmpl": {debug.InfoTmpl, "something"}, + "MemoryTmpl": {debug.MemoryTmpl, runtime.MemStats{}}, } func TestTemplates(t *testing.T) { @@ -169,6 +128,7 @@ func callsOf(p *packages.Package, tree *ast.File, name string) []*ast.CallExpr { ast.Inspect(tree, f) return ans } + func treeOf(p *packages.Package, fname string) *ast.File { for _, tree := range p.Syntax { loc := tree.Package From 1faecd32c9853cee6c43961fe4785669efd318d4 Mon Sep 17 00:00:00 2001 From: Peter Weinbergr Date: Thu, 22 Dec 2022 13:48:53 -0500 Subject: [PATCH 662/723] tools/internal/diff: fix off-by-one error in computing diffs In the removed code if endCol := end - 1 - strings.LastIndex(src[:end], "\n"); endCol > 0 the endCol test is off by one. It should have been endCol >= 0, otherwise there are cases where extending the edit won't be considered. But endCol is always >= 0, as LastIndex(src[:end], "\n") is <= end-1. Another way of saying all this is the extending the edit doesn't need endCol at all. The effect of this was to fail to extend edits if the edit started the line. The tests were revised to check unified diffs by using the patch command. Fixes: golang/go#457256 Change-Id: Idb23f1a28d36f92a7b8712e9459df86a3d420d7e Reviewed-on: https://go-review.googlesource.com/c/tools/+/459236 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan gopls-CI: kokoro Run-TryBot: Peter Weinberger --- internal/diff/diff.go | 17 ++++++----- internal/diff/diff_test.go | 47 +++++++++++++++++++++++------- internal/diff/difftest/difftest.go | 29 ++++++++++++++++-- 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 7b08ad57c82..3b315054c9f 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -17,6 +17,10 @@ type Edit struct { New string // the replacement } +func (e Edit) String() string { + return fmt.Sprintf("{Start:%d,End:%d,New:%s}", e.Start, e.End, e.New) +} + // Apply applies a sequence of edits to the src buffer and returns the // result. Edits are applied in order of start offset; edits with the // same start offset are applied in they order they were provided. @@ -146,16 +150,13 @@ func expandEdit(edit Edit, src string) Edit { } // Expand end right to end of line. - // (endCol is the zero-based column number of end.) end := edit.End - if endCol := end - 1 - strings.LastIndex(src[:end], "\n"); endCol > 0 { - if nl := strings.IndexByte(src[end:], '\n'); nl < 0 { - edit.End = len(src) // extend to EOF - } else { - edit.End = end + nl + 1 // extend beyond \n - } - edit.New += src[end:edit.End] + if nl := strings.IndexByte(src[end:], '\n'); nl < 0 { + edit.End = len(src) // extend to EOF + } else { + edit.End = end + nl + 1 // extend beyond \n } + edit.New += src[end:edit.End] return edit } diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index be949372945..b6881c1f2f0 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -5,13 +5,19 @@ package diff_test import ( + "bytes" "math/rand" + "os" + "os/exec" + "path/filepath" "reflect" + "strings" "testing" "unicode/utf8" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/diff/difftest" + "golang.org/x/tools/internal/testenv" ) func TestApply(t *testing.T) { @@ -99,31 +105,50 @@ func TestLineEdits(t *testing.T) { t.Fatalf("LineEdits: %v", err) } if !reflect.DeepEqual(got, edits) { - t.Errorf("LineEdits got %q, want %q", got, edits) + t.Errorf("LineEdits got\n%q, want\n%q\n%#v", got, edits, tc) } }) } } func TestToUnified(t *testing.T) { + testenv.NeedsTool(t, "patch") for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { unified, err := diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.Edits) if err != nil { t.Fatal(err) } - if unified != tc.Unified { - t.Errorf("Unified(Edits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) + if unified == "" { + return } - if tc.LineEdits != nil { - unified, err := diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.LineEdits) - if err != nil { - t.Fatal(err) - } - if unified != tc.Unified { - t.Errorf("Unified(LineEdits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) - } + orig := filepath.Join(t.TempDir(), "original") + err = os.WriteFile(orig, []byte(tc.In), 0644) + if err != nil { + t.Fatal(err) } + temp := filepath.Join(t.TempDir(), "patched") + err = os.WriteFile(temp, []byte(tc.In), 0644) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command("patch", "-p0", "-u", "-s", "-o", temp, orig) + cmd.Stdin = strings.NewReader(unified) + cmd.Stdout = new(bytes.Buffer) + cmd.Stderr = new(bytes.Buffer) + if err = cmd.Run(); err != nil { + t.Fatalf("%v: %q (%q) (%q)", err, cmd.String(), + cmd.Stderr, cmd.Stdout) + } + got, err := os.ReadFile(temp) + if err != nil { + t.Fatal(err) + } + if string(got) != tc.Out { + t.Errorf("applying unified failed: got\n%q, wanted\n%q unified\n%q", + got, tc.Out, unified) + } + }) } } diff --git a/internal/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go index cd026bee490..4a251111b6f 100644 --- a/internal/diff/difftest/difftest.go +++ b/internal/diff/difftest/difftest.go @@ -7,6 +7,13 @@ // "golang.org/x/tools/internal/diff" package difftest +// There are two kinds of tests, semantic tests, and 'golden data' tests. +// The semantic tests check that the computed diffs transform the input to +// the output, and that 'patch' accepts the computed unified diffs. +// The other tests just check that Edits and LineEdits haven't changed +// unexpectedly. These fields may need to be changed when the diff algorithm +// changes. + import ( "testing" @@ -201,6 +208,12 @@ var TestCases = []struct { {Start: 10, End: 12, New: ""}, {Start: 14, End: 14, New: "C\n"}, }, + LineEdits: []diff.Edit{ + {Start: 0, End: 6, New: "C\n"}, + {Start: 6, End: 8, New: "B\nA\n"}, + {Start: 10, End: 14, New: "A\n"}, + {Start: 14, End: 14, New: "C\n"}, + }, }, { Name: "replace_last_line", In: "A\nB\n", @@ -239,6 +252,16 @@ var TestCases = []struct { }, NoDiff: true, // diff algorithm produces different delete/insert pattern }, + { + Name: "extra_newline", + In: "\nA\n", + Out: "A\n", + Edits: []diff.Edit{{Start: 0, End: 1, New: ""}}, + Unified: UnifiedPrefix + `@@ -1,2 +1 @@ +- + A +`, + }, } func DiffTest(t *testing.T, compute func(before, after string) []diff.Edit) { @@ -254,10 +277,12 @@ func DiffTest(t *testing.T, compute func(before, after string) []diff.Edit) { t.Fatalf("ToUnified: %v", err) } if got != test.Out { - t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out) + t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", + got, unified, test.Out) } if !test.NoDiff && unified != test.Unified { - t.Errorf("Unified: got diff:\n%v\nexpected:\n%v", unified, test.Unified) + t.Errorf("Unified: got diff:\n%q\nexpected:\n%q diffs:%v", + unified, test.Unified, edits) } }) } From c4c6aa6407abf38f9e3594192225a94c33fcb717 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 19 Jan 2023 20:24:55 -0500 Subject: [PATCH 663/723] internal/lsp/cache: don't panic in Snapshot on a shutdown view There are many places where we may use a view asynchronously while it is shutting down. In these cases, it should be fine to fail or skip the view when we fail to acquire its snapshot. Fixes golang/go#56466 Change-Id: Icc4edfc1b951c40c8aa42a18ba4ecbf85621523d Reviewed-on: https://go-review.googlesource.com/c/tools/+/462820 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/session.go | 10 ++++++++-- gopls/internal/lsp/cache/snapshot.go | 11 ++++++++++- gopls/internal/lsp/cache/view.go | 9 +++++---- gopls/internal/lsp/general.go | 5 ++++- gopls/internal/lsp/lsp_test.go | 5 ++++- gopls/internal/lsp/source/view.go | 5 ++++- gopls/internal/lsp/source/workspace_symbol.go | 5 ++++- gopls/internal/lsp/text_synchronization.go | 5 ++++- gopls/internal/lsp/workspace.go | 5 ++++- 9 files changed, 47 insertions(+), 13 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index ab42d291cc1..e418a0046fe 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -593,7 +593,10 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes var snapshots []*snapshot s.viewMu.Lock() for _, v := range s.views { - snapshot, release := v.getSnapshot() + snapshot, release, err := v.getSnapshot() + if err != nil { + continue // view is shut down; continue with others + } defer release() snapshots = append(snapshots, snapshot) } @@ -795,7 +798,10 @@ func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struc defer s.viewMu.Unlock() patterns := map[string]struct{}{} for _, view := range s.views { - snapshot, release := view.getSnapshot() + snapshot, release, err := view.getSnapshot() + if err != nil { + continue // view is shut down; continue with others + } for k, v := range snapshot.fileWatchingGlobPatterns(ctx) { patterns[k] = v } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 0eef84efe95..51d7a7964bb 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -43,7 +43,16 @@ import ( type snapshot struct { sequenceID uint64 globalID source.GlobalSnapshotID - view *View + + // TODO(rfindley): the snapshot holding a reference to the view poses + // lifecycle problems: a view may be shut down and waiting for work + // associated with this snapshot to complete. While most accesses of the view + // are benign (options or workspace information), this is not formalized and + // it is wrong for the snapshot to use a shutdown view. + // + // Fix this by passing options and workspace information to the snapshot, + // both of which should be immutable for the snapshot. + view *View cancel func() backgroundCtx context.Context diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index ea9016ecec2..fe1479d37e0 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -9,6 +9,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io/ioutil" "os" @@ -677,17 +678,17 @@ func checkIgnored(suffix string) bool { return false } -func (v *View) Snapshot(ctx context.Context) (source.Snapshot, func()) { +func (v *View) Snapshot() (source.Snapshot, func(), error) { return v.getSnapshot() } -func (v *View) getSnapshot() (*snapshot, func()) { +func (v *View) getSnapshot() (*snapshot, func(), error) { v.snapshotMu.Lock() defer v.snapshotMu.Unlock() if v.snapshot == nil { - panic("getSnapshot called after shutdown") + return nil, nil, errors.New("view is shutdown") } - return v.snapshot, v.snapshot.Acquire() + return v.snapshot, v.snapshot.Acquire(), nil } func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) { diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index 87a86f481b7..af9aff96bc9 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -559,7 +559,10 @@ func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI if err != nil { return nil, nil, false, func() {}, err } - snapshot, release := view.Snapshot(ctx) + snapshot, release, err := view.Snapshot() + if err != nil { + return nil, nil, false, func() {}, err + } fh, err := snapshot.GetFile(ctx, uri) if err != nil { release() diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 46ff0706835..6a6a8caad30 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -1397,7 +1397,10 @@ func (r *runner) collectDiagnostics(view *cache.View) { } r.diagnostics = make(map[span.URI][]*source.Diagnostic) - snapshot, release := view.Snapshot(r.ctx) + snapshot, release, err := view.Snapshot() + if err != nil { + panic(err) + } defer release() // Always run diagnostics with analysis. diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 4b6fd13900b..640a32e7110 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -309,7 +309,10 @@ type View interface { // Snapshot returns the current snapshot for the view, and a // release function that must be called when the Snapshot is // no longer needed. - Snapshot(ctx context.Context) (Snapshot, func()) + // + // If the view is shut down, the resulting error will be non-nil, and the + // release function need not be called. + Snapshot() (Snapshot, func(), error) // IsGoPrivatePath reports whether target is a private import path, as identified // by the GOPRIVATE environment variable. diff --git a/gopls/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go index dc5abd211f0..1b157c6a51e 100644 --- a/gopls/internal/lsp/source/workspace_symbol.go +++ b/gopls/internal/lsp/source/workspace_symbol.go @@ -305,7 +305,10 @@ func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher seen := make(map[span.URI]bool) // TODO(adonovan): opt: parallelize this loop? How often is len > 1? for _, v := range views { - snapshot, release := v.Snapshot(ctx) + snapshot, release, err := v.Snapshot() + if err != nil { + continue // view is shut down; continue with others + } defer release() // Use the root view URIs for determining (lexically) diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go index 77e081a8a08..3028dc96c81 100644 --- a/gopls/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -144,7 +144,10 @@ func (s *Server) warnAboutModifyingGeneratedFiles(ctx context.Context, uri span. if err != nil { return err } - snapshot, release := view.Snapshot(ctx) + snapshot, release, err := view.Snapshot() + if err != nil { + return err + } isGenerated := source.IsGenerated(ctx, snapshot, uri) release() diff --git a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go index e4326b1fbce..f2df9c1d8c3 100644 --- a/gopls/internal/lsp/workspace.go +++ b/gopls/internal/lsp/workspace.go @@ -63,7 +63,10 @@ func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChan return err } go func() { - snapshot, release := view.Snapshot(ctx) + snapshot, release, err := view.Snapshot() + if err != nil { + return // view is shut down; no need to diagnose + } defer release() s.diagnoseDetached(snapshot) }() From ce28f407b85d67101e4c6f783977f921c6f13788 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 19 Jan 2023 21:15:16 -0500 Subject: [PATCH 664/723] gopls/internal/regtest: add a test demonstrating confusion following an options change Add a test that demonstrates gopls gets confused about file content following an options change. For golang/go#57934 Change-Id: I536245877ef9069613d6e551354b00fc42b3bc5c Reviewed-on: https://go-review.googlesource.com/c/tools/+/462821 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/cache/view.go | 3 +- .../regtest/misc/configuration_test.go | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index fe1479d37e0..d8eb82e94ca 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -360,7 +360,8 @@ func (v *View) FileKind(fh source.FileHandle) source.FileKind { } func minorOptionsChange(a, b *source.Options) bool { - // Check if any of the settings that modify our understanding of files have been changed + // Check if any of the settings that modify our understanding of files have + // been changed. if !reflect.DeepEqual(a.Env, b.Env) { return false } diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index 5af1311f838..6e6d9d73388 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -51,6 +51,55 @@ var FooErr = errors.New("foo") }) } +// 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) { + t.Skip("broken due to golang/go#57934") + + testenv.NeedsGo1Point(t, 17) + + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- a/a.go -- +package a + +import "errors" + +var ErrFoo = errors.New("foo") +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + // Introduce a staticcheck diagnostic. It should be detected when we enable + // staticcheck later. + env.RegexpReplace("a/a.go", "ErrFoo", "FooErr") + env.AfterChange( + NoDiagnostics(ForFile("a/a.go")), + ) + cfg := env.Editor.Config() + // Any change to environment recreates the view, but this should not cause + // gopls to get confused about the content of a/a.go: we should get the + // staticcheck diagnostic below. + cfg.Env = map[string]string{ + "AN_ARBITRARY_VAR": "FOO", + } + cfg.Settings = map[string]interface{}{ + "staticcheck": true, + } + // TODO(rfindley): support waiting on diagnostics following a configuration + // change. + env.ChangeConfiguration(cfg) + env.Await( + Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")), + ) + }) +} + func TestStaticcheckWarning(t *testing.T) { // Note: keep this in sync with TestChangeConfiguration. testenv.SkipAfterGo1Point(t, 16) From 51abc5bd865f4a8a94772f911be1a286a32202c3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 20 Jan 2023 23:17:55 -0500 Subject: [PATCH 665/723] gopls/internal/lsp/source: stub: don't panic when encountering 'error' The built-in error interface is the only non-trivial named interface type with no package. This caused v0.11.0 of gopls to panic when the ApplyFix stub operation was applied to an interface that embedded 'error'. The crash was inadvertently fixed by recent changes, but still the operation failed with an error that mentioned the filename "". This change causes the operation to succeed with the proper result. It also adds a test. I think there's a simpler implementation of this operation following the approach described in the existing comment that would obviate the special-case logic added in this change. Fixes golang/vscode-go#2606 Change-Id: I544de5f828055e999344117e4f8b2579bde21cfb Reviewed-on: https://go-review.googlesource.com/c/tools/+/463035 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Alan Donovan --- gopls/internal/lsp/source/stub.go | 41 +++++++++++++------ .../lsp/testdata/stub/stub_issue2606.go | 7 ++++ .../testdata/stub/stub_issue2606.go.golden | 14 +++++++ .../internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- 5 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 gopls/internal/lsp/testdata/stub/stub_issue2606.go create mode 100644 gopls/internal/lsp/testdata/stub/stub_issue2606.go.golden diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index cf360dd41e5..7b6f4f45137 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -255,6 +255,20 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method if !ok { return nil, fmt.Errorf("expected %v to be an interface but got %T", iface, ifaceObj.Type().Underlying()) } + + // The built-in error interface is special. + if ifaceObj.Pkg() == nil && ifaceObj.Name() == "error" { + var missingInterfaces []*missingInterface + if concMS.Lookup(nil, "Error") == nil { + errorMethod, _, _ := types.LookupFieldOrMethod(iface, false, nil, "Error") + missingInterfaces = append(missingInterfaces, &missingInterface{ + iface: ifaceObj, + missing: []*types.Func{errorMethod.(*types.Func)}, + }) + } + return missingInterfaces, nil + } + // Parse the imports from the file that declares the interface. ifaceFilename := safetoken.StartPosition(snapshot.FileSet(), ifaceObj.Pos()).Filename ifaceFH, err := snapshot.GetFile(ctx, span.URIFromPath(ifaceFilename)) @@ -266,18 +280,15 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method return nil, fmt.Errorf("error parsing imports from interface file: %w", err) } - mi := &missingInterface{ - iface: ifaceObj, - imports: ifaceFile.File.Imports, - } + var missing []*types.Func - // Add all the interface methods not defined by the concrete type to mi.missing. + // Add all the interface methods not defined by the concrete type to missing. for i := 0; i < iface.NumExplicitMethods(); i++ { method := iface.ExplicitMethod(i) if sel := concMS.Lookup(concPkg, method.Name()); sel == nil { // Concrete type does not have the interface method. if _, ok := visited[method.Name()]; !ok { - mi.missing = append(mi.missing, method) + missing = append(missing, method) visited[method.Name()] = struct{}{} } } else { @@ -297,27 +308,31 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method // sets of the interface and concrete types. Once the set // difference (missing methods) is computed, the imports // from the declaring file(s) could be loaded as needed. - var missing []*missingInterface + var missingInterfaces []*missingInterface for i := 0; i < iface.NumEmbeddeds(); i++ { eiface := iface.Embedded(i).Obj() em, err := missingMethods(ctx, snapshot, concMS, concPkg, eiface, visited) if err != nil { return nil, err } - missing = append(missing, em...) + missingInterfaces = append(missingInterfaces, em...) } // The type checker is deterministic, but its choice of // ordering of embedded interfaces varies with Go version // (e.g. go1.17 was sorted, go1.18 was lexical order). // Sort to ensure test portability. - sort.Slice(missing, func(i, j int) bool { - return missing[i].iface.Id() < missing[j].iface.Id() + sort.Slice(missingInterfaces, func(i, j int) bool { + return missingInterfaces[i].iface.Id() < missingInterfaces[j].iface.Id() }) - if len(mi.missing) > 0 { - missing = append(missing, mi) + if len(missing) > 0 { + missingInterfaces = append(missingInterfaces, &missingInterface{ + iface: ifaceObj, + imports: ifaceFile.File.Imports, + missing: missing, + }) } - return missing, nil + return missingInterfaces, nil } // missingInterface represents an interface diff --git a/gopls/internal/lsp/testdata/stub/stub_issue2606.go b/gopls/internal/lsp/testdata/stub/stub_issue2606.go new file mode 100644 index 00000000000..66ef2b24b97 --- /dev/null +++ b/gopls/internal/lsp/testdata/stub/stub_issue2606.go @@ -0,0 +1,7 @@ +package stub + +type I interface{ error } + +type C int + +var _ I = C(0) //@suggestedfix("C", "refactor.rewrite", "") diff --git a/gopls/internal/lsp/testdata/stub/stub_issue2606.go.golden b/gopls/internal/lsp/testdata/stub/stub_issue2606.go.golden new file mode 100644 index 00000000000..4db266346e2 --- /dev/null +++ b/gopls/internal/lsp/testdata/stub/stub_issue2606.go.golden @@ -0,0 +1,14 @@ +-- suggestedfix_stub_issue2606_7_11 -- +package stub + +type I interface{ error } + +type C int + +// Error implements I +func (C) Error() string { + panic("unimplemented") +} + +var _ I = C(0) //@suggestedfix("C", "refactor.rewrite", "") + diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index e0982417c81..478b6a6ca92 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -13,7 +13,7 @@ FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 SemanticTokenCount = 3 -SuggestedFixCount = 64 +SuggestedFixCount = 65 FunctionExtractionCount = 27 MethodExtractionCount = 6 DefinitionsCount = 99 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index b402eef2e63..735259e6428 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -13,7 +13,7 @@ FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 SemanticTokenCount = 3 -SuggestedFixCount = 70 +SuggestedFixCount = 71 FunctionExtractionCount = 27 MethodExtractionCount = 6 DefinitionsCount = 110 From b5d65e025bccf79b02252db0c040e158f58fed40 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Sat, 14 Jan 2023 15:58:41 -0500 Subject: [PATCH 666/723] tools/gopls: register semantic tokens statically Not all clients support dynamic registration for semantic tokens, so register the semantic tokens provider statically. Gopls had used dynamic unregistration to disable semantic token processing when the user turned off the option, but this is not needed as the option is checked before processing semantic tokens. Fixes: golang/go#54531 Change-Id: Ic10e8b7252e008f30c3bfb9c4f1af78d5762857b Reviewed-on: https://go-review.googlesource.com/c/tools/+/462215 TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Peter Weinberger Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/general.go | 11 ++++++++--- gopls/internal/lsp/source/options.go | 3 ++- gopls/internal/lsp/workspace.go | 21 --------------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go index af9aff96bc9..d55e10384e2 100644 --- a/gopls/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -159,6 +159,14 @@ See https://github.com/golang/go/issues/45732 for more information.`, ReferencesProvider: true, RenameProvider: renameOpts, SelectionRangeProvider: protocol.SelectionRangeRegistrationOptions{}, + SemanticTokensProvider: protocol.SemanticTokensOptions{ + Range: true, + Full: true, + Legend: protocol.SemanticTokensLegend{ + TokenTypes: s.session.Options().SemanticTypes, + TokenModifiers: s.session.Options().SemanticMods, + }, + }, SignatureHelpProvider: protocol.SignatureHelpOptions{ TriggerCharacters: []string{"(", ","}, }, @@ -213,9 +221,6 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa Method: "workspace/didChangeConfiguration", }) } - if options.SemanticTokens && options.DynamicRegistrationSemanticTokensSupported { - registrations = append(registrations, semanticTokenRegistration(options.SemanticTypes, options.SemanticMods)) - } if len(registrations) > 0 { if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ Registrations: registrations, diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 88a3678e147..a32ed128aa8 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -753,7 +753,8 @@ func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) { o.LineFoldingOnly = fr.LineFoldingOnly // Check if the client supports hierarchical document symbols. o.HierarchicalDocumentSymbolSupport = caps.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport - // Check if the client supports semantic tokens + + // Client's semantic tokens o.SemanticTypes = caps.TextDocument.SemanticTokens.TokenTypes o.SemanticMods = caps.TextDocument.SemanticTokens.TokenModifiers // we don't need Requests, as we support full functionality diff --git a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go index f2df9c1d8c3..c50ae32bb1c 100644 --- a/gopls/internal/lsp/workspace.go +++ b/gopls/internal/lsp/workspace.go @@ -46,7 +46,6 @@ func (s *Server) addView(ctx context.Context, name string, uri span.URI) (source func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChangeConfigurationParams) error { // Apply any changes to the session-level settings. options := s.session.Options().Clone() - semanticTokensRegistered := options.SemanticTokens if err := s.fetchConfig(ctx, "", "", options); err != nil { return err } @@ -75,26 +74,6 @@ func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChan // An options change may have affected the detected Go version. s.checkViewGoVersions() - registration := semanticTokenRegistration(options.SemanticTypes, options.SemanticMods) - // Update any session-specific registrations or unregistrations. - if !semanticTokensRegistered && options.SemanticTokens { - if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ - Registrations: []protocol.Registration{registration}, - }); err != nil { - return err - } - } else if semanticTokensRegistered && !options.SemanticTokens { - if err := s.client.UnregisterCapability(ctx, &protocol.UnregistrationParams{ - Unregisterations: []protocol.Unregistration{ - { - ID: registration.ID, - Method: registration.Method, - }, - }, - }); err != nil { - return err - } - } return nil } From aae3642ea0abd2ff0ae94195510c84e32d6eb621 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 20 Jan 2023 12:09:19 -0500 Subject: [PATCH 667/723] gopls/internal/lsp/source: referencesV2: support unexported methods This change adds support for unexported methods to the incremental implementation of the 'references' operation, which previously had to fall back to a type-check-the-world strategy for methods. (Exported methods are still to do.) The implementation strategy is to record the receiver type R of the target method M, and search types.Info.Uses for all objects that are methods of the same name as M, whose receiver is mutually assignable with R. Change-Id: I4121f34df822662ccbaac5d53f3e2a5578be5431 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462915 TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/lsp_test.go | 2 + gopls/internal/lsp/source/references2.go | 78 +++++++++++++++---- .../internal/regtest/misc/references_test.go | 2 + 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 6a6a8caad30..eb5baf95ab0 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -867,6 +867,8 @@ func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { want := make(map[protocol.Location]bool) for i, pos := range itemList { // We don't want the first result if we aren't including the declaration. + // TODO(adonovan): don't assume a single declaration: + // there may be >1 if corresponding methods are considered. if i == 0 && !includeDeclaration { continue } diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index 9ebeace6563..1fc01dc2423 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -30,6 +30,7 @@ import ( "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source/methodsets" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" ) @@ -248,18 +249,7 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp break } if obj == nil { - return nil, ErrNoIdentFound - } - - // If the object is a method, we need to search for all - // matching implementations and/or interfaces, without - // type-checking everything. - // - // That will require an approach such as the one sketched in - // go.dev/cl/452060. Until then, we simply fall back to the - // old implementation for now. TODO(adonovan): fix. - if fn, ok := obj.(*types.Func); ok && fn.Type().(*types.Signature).Recv() != nil { - return nil, ErrFallback + return nil, ErrNoIdentFound // can't happen } // nil, error, iota, or other built-in? @@ -303,6 +293,18 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp var exportedObjectPath objectpath.Path if path, err := objectpath.For(obj); err == nil && obj.Exported() { exportedObjectPath = path + + // If the object is an exported method, we need to search for + // all matching implementations (using the incremental + // implementation of 'implementations') and then search for + // the set of corresponding methods (requiring the incremental + // implementation of 'references' to be generalized to a set + // of search objects). + // Until then, we simply fall back to the old implementation for now. + // TODO(adonovan): fix. + if fn, ok := obj.(*types.Func); ok && fn.Type().(*types.Signature).Recv() != nil { + return nil, ErrFallback + } } // If it is exported, how far need we search? @@ -368,7 +370,7 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp return refs, nil } -// localReferences reports (concurrently) each reference to the object +// localReferences reports each reference to the object // declared at the specified URI/offset within its enclosing package m. func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, declOffset int, m *Metadata, report func(loc protocol.Location, isDecl bool)) error { pkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, m.ID) @@ -393,15 +395,57 @@ func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, d } // Report the locations of the declaration(s). + // TODO(adonovan): what about for corresponding methods? Add tests. for _, node := range targets { report(mustLocation(pgf, node), true) } + // receiver returns the effective receiver type for method-set + // comparisons for obj, if it is a method, or nil otherwise. + receiver := func(obj types.Object) types.Type { + if fn, ok := obj.(*types.Func); ok { + if recv := fn.Type().(*types.Signature).Recv(); recv != nil { + return methodsets.EnsurePointer(recv.Type()) + } + } + return nil + } + + // If we're searching for references to a method, broaden the + // search to include references to corresponding methods of + // mutually assignable receiver types. + // (We use a slice, but objectsAt never returns >1 methods.) + var methodRecvs []types.Type + var methodName string // name of an arbitrary target, iff a method + for obj := range targets { + if t := receiver(obj); t != nil { + methodRecvs = append(methodRecvs, t) + methodName = obj.Name() + } + } + + // matches reports whether obj either is or corresponds to a target. + // (Correspondence is defined as usual for interface methods.) + matches := func(obj types.Object) bool { + if targets[obj] != nil { + return true + } else if methodRecvs != nil && obj.Name() == methodName { + if orecv := receiver(obj); orecv != nil { + for _, mrecv := range methodRecvs { + if concreteImplementsIntf(orecv, mrecv) { + return true + } + } + } + } + return false + } + // Scan through syntax looking for uses of one of the target objects. for _, pgf := range pkg.CompiledGoFiles() { ast.Inspect(pgf.File, func(n ast.Node) bool { if id, ok := n.(*ast.Ident); ok { - if used, ok := pkg.GetTypesInfo().Uses[id]; ok && targets[used] != nil { + if obj, ok := pkg.GetTypesInfo().Uses[id]; ok && matches(obj) { report(mustLocation(pgf, id), false) } } @@ -453,10 +497,14 @@ func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Objec } targets[obj] = leaf } + + if len(targets) == 0 { + return nil, fmt.Errorf("objectAt: internal error: no targets") // can't happen + } return targets, nil } -// globalReferences reports (concurrently) each cross-package +// globalReferences reports each cross-package // reference to the object identified by (pkgPath, objectPath). func globalReferences(ctx context.Context, snapshot Snapshot, m *Metadata, pkgPath PackagePath, objectPath objectpath.Path, report func(loc protocol.Location, isDecl bool)) error { // TODO(adonovan): opt: don't actually type-check here, diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index 19704ffbf87..f105f4c87c0 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -52,6 +52,8 @@ func main() { // This reproduces and tests golang/go#48400. func TestReferencesPanicOnError(t *testing.T) { + // Ideally this would actually return the correct answer, + // instead of merely failing gracefully. const files = ` -- go.mod -- module mod.com From fcd57ebb758fadd469cf0fc8deeb9d38b0762bf2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 23 Jan 2023 14:49:59 -0500 Subject: [PATCH 668/723] gopls: allow 'any' and 'comparable' in completion results Remove long-since unnecessary filters in lexical completion results excluding 'any' and 'comparable', and update tests accordingly. Also improve diff output for completion results, using cmp.Diff. Fixes golang/go#47669 Updates golang/go#54845 Change-Id: I5ea542d09b4dffb0dd701bc5f813c7bbdcd846f7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463139 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/completion_test.go | 14 ++-- .../lsp/source/completion/completion.go | 13 +--- .../lsp/testdata/builtins/builtin_go117.go | 8 ++ .../lsp/testdata/builtins/builtin_go118.go | 8 ++ .../lsp/testdata/builtins/builtins.go | 6 +- gopls/internal/lsp/tests/util.go | 73 ++++++++++++------- gopls/internal/lsp/tests/util_go118.go | 13 ++++ 7 files changed, 88 insertions(+), 47 deletions(-) create mode 100644 gopls/internal/lsp/testdata/builtins/builtin_go117.go create mode 100644 gopls/internal/lsp/testdata/builtins/builtin_go118.go create mode 100644 gopls/internal/lsp/tests/util_go118.go diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index 8211cc51c62..9a72f123e62 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -28,7 +28,7 @@ func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, got = tests.FilterBuiltins(src, got) want := expected(t, test, items) if diff := tests.DiffCompletionItems(want, got); diff != "" { - t.Errorf("%s", diff) + t.Errorf("mismatching completion items (-want +got):\n%s", diff) } } @@ -66,8 +66,8 @@ func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completi }) got = tests.FilterBuiltins(src, got) want := expected(t, test, items) - if msg := tests.DiffCompletionItems(want, got); msg != "" { - t.Errorf("%s", msg) + if diff := tests.DiffCompletionItems(want, got); diff != "" { + t.Errorf("mismatching completion items (-want +got):\n%s", diff) } } @@ -79,8 +79,8 @@ func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Complet }) got = tests.FilterBuiltins(src, got) want := expected(t, test, items) - if msg := tests.DiffCompletionItems(want, got); msg != "" { - t.Errorf("%s", msg) + if diff := tests.DiffCompletionItems(want, got); diff != "" { + t.Errorf("mismatching completion items (-want +got):\n%s", diff) } } @@ -91,8 +91,8 @@ func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests }) got = tests.FilterBuiltins(src, got) want := expected(t, test, items) - if msg := tests.DiffCompletionItems(want, got); msg != "" { - t.Errorf("%s", msg) + if diff := tests.DiffCompletionItems(want, got); diff != "" { + t.Errorf("mismatching completion items (-want +got):\n%s", diff) } } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 8aace97feb5..056b2893c49 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -1287,12 +1287,10 @@ func (c *completer) lexical(ctx context.Context) error { var ( builtinIota = types.Universe.Lookup("iota") builtinNil = types.Universe.Lookup("nil") - // comparable is an interface that exists on the dev.typeparams Go branch. - // Filter it out from completion results to stabilize tests. - // TODO(rFindley) update (or remove) our handling for comparable once the - // type parameter API has stabilized. - builtinAny = types.Universe.Lookup("any") - builtinComparable = types.Universe.Lookup("comparable") + + // TODO(rfindley): only allow "comparable" where it is valid (in constraint + // position or embedded in interface declarations). + // builtinComparable = types.Universe.Lookup("comparable") ) // Track seen variables to avoid showing completions for shadowed variables. @@ -1311,9 +1309,6 @@ func (c *completer) lexical(ctx context.Context) error { if declScope != scope { continue // Name was declared in some enclosing scope, or not at all. } - if obj == builtinComparable || obj == builtinAny { - continue - } // If obj's type is invalid, find the AST node that defines the lexical block // containing the declaration of obj. Don't resolve types for packages. diff --git a/gopls/internal/lsp/testdata/builtins/builtin_go117.go b/gopls/internal/lsp/testdata/builtins/builtin_go117.go new file mode 100644 index 00000000000..57abcde1517 --- /dev/null +++ b/gopls/internal/lsp/testdata/builtins/builtin_go117.go @@ -0,0 +1,8 @@ +//go:build !go1.18 +// +build !go1.18 + +package builtins + +func _() { + //@complete("", append, bool, byte, cap, close, complex, complex128, complex64, copy, delete, error, _false, float32, float64, imag, int, int16, int32, int64, int8, len, make, new, panic, print, println, real, recover, rune, string, _true, uint, uint16, uint32, uint64, uint8, uintptr, _nil) +} diff --git a/gopls/internal/lsp/testdata/builtins/builtin_go118.go b/gopls/internal/lsp/testdata/builtins/builtin_go118.go new file mode 100644 index 00000000000..1fa0b14a4a1 --- /dev/null +++ b/gopls/internal/lsp/testdata/builtins/builtin_go118.go @@ -0,0 +1,8 @@ +//go:build go1.18 +// +build go1.18 + +package builtins + +func _() { + //@complete("", any, append, bool, byte, cap, close, comparable, complex, complex128, complex64, copy, delete, error, _false, float32, float64, imag, int, int16, int32, int64, int8, len, make, new, panic, print, println, real, recover, rune, string, _true, uint, uint16, uint32, uint64, uint8, uintptr, _nil) +} diff --git a/gopls/internal/lsp/testdata/builtins/builtins.go b/gopls/internal/lsp/testdata/builtins/builtins.go index 25c29f21e6c..d30176cfbae 100644 --- a/gopls/internal/lsp/testdata/builtins/builtins.go +++ b/gopls/internal/lsp/testdata/builtins/builtins.go @@ -1,15 +1,15 @@ package builtins -func _() { - //@complete("", append, bool, byte, cap, close, complex, complex128, complex64, copy, delete, error, _false, float32, float64, imag, int, int16, int32, int64, int8, len, make, new, panic, print, println, real, recover, rune, string, _true, uint, uint16, uint32, uint64, uint8, uintptr, _nil) -} +// Definitions of builtin completion items. +/* any */ //@item(any, "any", "", "interface") /* Create markers for builtin types. Only for use by this test. /* append(slice []Type, elems ...Type) []Type */ //@item(append, "append", "func(slice []Type, elems ...Type) []Type", "func") /* bool */ //@item(bool, "bool", "", "type") /* byte */ //@item(byte, "byte", "", "type") /* cap(v Type) int */ //@item(cap, "cap", "func(v Type) int", "func") /* close(c chan<- Type) */ //@item(close, "close", "func(c chan<- Type)", "func") +/* comparable */ //@item(comparable, "comparable", "", "interface") /* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") /* complex128 */ //@item(complex128, "complex128", "", "type") /* complex64 */ //@item(complex64, "complex64", "", "type") diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index cdbd12e89e0..344a768fac9 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -17,6 +17,8 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/completion" @@ -24,6 +26,29 @@ import ( "golang.org/x/tools/gopls/internal/span" ) +var builtins = map[string]bool{ + "append": true, + "cap": true, + "close": true, + "complex": true, + "copy": true, + "delete": true, + "error": true, + "false": true, + "imag": true, + "iota": true, + "len": true, + "make": true, + "new": true, + "nil": true, + "panic": true, + "print": true, + "println": true, + "real": true, + "recover": true, + "true": true, +} + // DiffLinks takes the links we got and checks if they are located within the source or a Note. // If the link is within a Note, the link is removed. // Returns an diff comment if there are differences and empty string if no diffs. @@ -304,13 +329,7 @@ func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool { if i := strings.Index(trimmed, "("); i >= 0 { trimmed = trimmed[:i] } - switch trimmed { - case "append", "cap", "close", "complex", "copy", "delete", - "error", "false", "imag", "iota", "len", "make", "new", - "nil", "panic", "print", "println", "real", "recover", "true": - return true - } - return false + return builtins[trimmed] } func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string { @@ -390,28 +409,26 @@ func FindItem(list []protocol.CompletionItem, want completion.CompletionItem) *p // DiffCompletionItems prints the diff between expected and actual completion // test results. +// +// The diff will be formatted using '-' and '+' for want and got, respectively. func DiffCompletionItems(want, got []protocol.CompletionItem) string { - if len(got) != len(want) { - return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want)) - } - for i, w := range want { - g := got[i] - if w.Label != g.Label { - return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label) - } - if NormalizeAny(w.Detail) != NormalizeAny(g.Detail) { - return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail) - } - if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") { - if w.Documentation != g.Documentation { - return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation) - } - } - if w.Kind != g.Kind { - return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind) - } - } - return "" + // Many fields are not set in the "want" slice. + irrelevantFields := []string{ + "AdditionalTextEdits", + "Documentation", + "TextEdit", + "SortText", + "Preselect", + "FilterText", + "InsertText", + "InsertTextFormat", + } + ignore := cmpopts.IgnoreFields(protocol.CompletionItem{}, irrelevantFields...) + normalizeAny := cmpopts.AcyclicTransformer("NormalizeAny", func(item protocol.CompletionItem) protocol.CompletionItem { + item.Detail = NormalizeAny(item.Detail) + return item + }) + return cmp.Diff(want, got, ignore, normalizeAny) } func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string { diff --git a/gopls/internal/lsp/tests/util_go118.go b/gopls/internal/lsp/tests/util_go118.go new file mode 100644 index 00000000000..6115342df74 --- /dev/null +++ b/gopls/internal/lsp/tests/util_go118.go @@ -0,0 +1,13 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package tests + +func init() { + builtins["any"] = true + builtins["comparable"] = true +} From f269f537bcf2b5b9c218a3aa7e589a0d6215da05 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 18 Jan 2023 11:29:48 -0500 Subject: [PATCH 669/723] gopls/internal/lsp: remove Server.processModifications Now that we no longer support a process delay, there is no need for this function. Also, clean up the logic to await the completion of change processing. Updates golang/go#55332 Change-Id: I27936e112ee872eb1a46b350bf6f8f867a83bf19 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462595 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/text_synchronization.go | 34 ++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go index 3028dc96c81..b7be1e1ce11 100644 --- a/gopls/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "path/filepath" + "sync" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" @@ -208,32 +209,35 @@ func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocu } func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification, cause ModificationSource) error { - diagnoseDone := make(chan struct{}) + // wg guards two conditions: + // 1. didModifyFiles is complete + // 2. the goroutine diagnosing changes on behalf of didModifyFiles is + // complete, if it was started + // + // Both conditions must be satisfied for the purpose of testing: we don't + // want to observe the completion of change processing until we have received + // all diagnostics as well as all server->client notifications done on behalf + // of this function. + var wg sync.WaitGroup + wg.Add(1) + defer wg.Done() + if s.session.Options().VerboseWorkDoneProgress { work := s.progress.Start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil) - defer func() { - go func() { - <-diagnoseDone - work.End(ctx, "Done.") - }() + go func() { + wg.Wait() + work.End(ctx, "Done.") }() } onDisk := cause == FromDidChangeWatchedFiles - return s.processModifications(ctx, modifications, onDisk, diagnoseDone) -} -// processModifications update server state to reflect file changes, and -// triggers diagnostics to run asynchronously. The diagnoseDone channel will be -// closed once diagnostics complete. -func (s *Server) processModifications(ctx context.Context, modifications []source.FileModification, onDisk bool, diagnoseDone chan struct{}) error { s.stateMu.Lock() if s.state >= serverShutDown { // This state check does not prevent races below, and exists only to // produce a better error message. The actual race to the cache should be // guarded by Session.viewMu. s.stateMu.Unlock() - close(diagnoseDone) return errors.New("server is shut down") } s.stateMu.Unlock() @@ -251,7 +255,6 @@ func (s *Server) processModifications(ctx context.Context, modifications []sourc snapshots, release, err := s.session.DidModifyFiles(ctx, modifications) if err != nil { - close(diagnoseDone) return err } @@ -266,10 +269,11 @@ func (s *Server) processModifications(ctx context.Context, modifications []sourc } } + wg.Add(1) go func() { s.diagnoseSnapshots(snapshots, onDisk) release() - close(diagnoseDone) + wg.Done() }() // After any file modifications, we need to update our watched files, From deeb64bdf7340c8d2aaaf344b086a17381bf1320 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 20 Jan 2023 16:14:01 -0500 Subject: [PATCH 670/723] gopls/internal/lsp/source/xrefs: allow Lookup of a set This change generalizes xrefs.Lookup to allow lookup of a set of (package path, object path) targets in a single call, amortizing decoding. It will be used to support exported methods in implementsV2. Change-Id: If10c21db97df7a9e8088b3a98e81018d12c52fb2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463140 Reviewed-by: Robert Findley Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/pkg.go | 6 ++++-- gopls/internal/lsp/source/references2.go | 18 ++++++++++-------- gopls/internal/lsp/source/view.go | 6 ++++-- gopls/internal/lsp/source/xrefs/xrefs.go | 14 +++++++------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 0a986dec7c4..37aae50a087 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -178,13 +178,15 @@ func (p *pkg) DiagnosticsForFile(uri span.URI) []*source.Diagnostic { return res } -func (p *pkg) ReferencesTo(pkgPath PackagePath, objPath objectpath.Path) []protocol.Location { +// ReferencesTo returns the location of each reference within package p +// to one of the target objects denoted by the pair (package path, object path). +func (p *pkg) ReferencesTo(targets map[PackagePath]map[objectpath.Path]struct{}) []protocol.Location { // TODO(adonovan): In future, p.xrefs will be retrieved from a // section of the cache file produced by type checking. // (Other sections will include the package's export data, // "implements" relations, exported symbols, etc.) // For now we just hang it off the pkg. - return xrefs.Lookup(p.m, p.xrefs, pkgPath, objPath) + return xrefs.Lookup(p.m, p.xrefs, targets) } func (p *pkg) MethodSetsIndex() *methodsets.Index { diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index 1fc01dc2423..538639aed2f 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -290,9 +290,9 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp // Is the object exported? // (objectpath succeeds for lowercase names, arguably a bug.) - var exportedObjectPath objectpath.Path + var exportedObjectPaths map[objectpath.Path]unit if path, err := objectpath.For(obj); err == nil && obj.Exported() { - exportedObjectPath = path + exportedObjectPaths = map[objectpath.Path]unit{path: unit{}} // If the object is an exported method, we need to search for // all matching implementations (using the incremental @@ -342,10 +342,12 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp return localReferences(ctx, snapshot, declURI, declPosn.Offset, m, report) }) - if exportedObjectPath == "" { + if exportedObjectPaths == nil { continue // non-exported } + targets := map[PackagePath]map[objectpath.Path]struct{}{m.PkgPath: exportedObjectPaths} + // global group.Go(func() error { // Compute the global-scope query for every variant @@ -358,7 +360,7 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp for _, rdep := range rdeps { rdep := rdep group.Go(func() error { - return globalReferences(ctx, snapshot, rdep, m.PkgPath, exportedObjectPath, report) + return globalReferences(ctx, snapshot, rdep, targets, report) }) } return nil @@ -504,9 +506,9 @@ func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Objec return targets, nil } -// globalReferences reports each cross-package -// reference to the object identified by (pkgPath, objectPath). -func globalReferences(ctx context.Context, snapshot Snapshot, m *Metadata, pkgPath PackagePath, objectPath objectpath.Path, report func(loc protocol.Location, isDecl bool)) error { +// globalReferences reports each cross-package reference to one of the +// target objects denoted by (package path, object path). +func globalReferences(ctx context.Context, snapshot Snapshot, m *Metadata, targets map[PackagePath]map[objectpath.Path]unit, report func(loc protocol.Location, isDecl bool)) error { // TODO(adonovan): opt: don't actually type-check here, // since we quite intentionally don't look at type information. // Instead, access the reference index computed during @@ -515,7 +517,7 @@ func globalReferences(ctx context.Context, snapshot Snapshot, m *Metadata, pkgPa if err != nil { return err } - for _, loc := range pkgs[0].ReferencesTo(pkgPath, objectPath) { + for _, loc := range pkgs[0].ReferencesTo(targets) { report(loc, false) } return nil diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 640a32e7110..81a91ccd9d1 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -773,11 +773,13 @@ type Package interface { ResolveImportPath(path ImportPath) (Package, error) Imports() []Package // new slice of all direct dependencies, unordered HasTypeErrors() bool - DiagnosticsForFile(uri span.URI) []*Diagnostic // new array of list/parse/type errors - ReferencesTo(PackagePath, objectpath.Path) []protocol.Location // new sorted array of xrefs + DiagnosticsForFile(uri span.URI) []*Diagnostic // new array of list/parse/type errors + ReferencesTo(map[PackagePath]map[objectpath.Path]unit) []protocol.Location // new sorted array of xrefs MethodSetsIndex() *methodsets.Index } +type unit = struct{} + // A CriticalError is a workspace-wide error that generally prevents gopls from // functioning correctly. In the presence of critical errors, other diagnostics // in the workspace may not make sense. diff --git a/gopls/internal/lsp/source/xrefs/xrefs.go b/gopls/internal/lsp/source/xrefs/xrefs.go index db9bab12975..b0d2207508c 100644 --- a/gopls/internal/lsp/source/xrefs/xrefs.go +++ b/gopls/internal/lsp/source/xrefs/xrefs.go @@ -134,8 +134,9 @@ func Index(pkg source.Package) []byte { // Lookup searches a serialized index produced by an indexPackage // operation on m, and returns the locations of all references from m -// to the object denoted by (pkgPath, objectPath). -func Lookup(m *source.Metadata, data []byte, pkgPath source.PackagePath, objPath objectpath.Path) []protocol.Location { +// to any object in the target set. Each object is denoted by a pair +// of (package path, object path). +func Lookup(m *source.Metadata, data []byte, targets map[source.PackagePath]map[objectpath.Path]struct{}) (locs []protocol.Location) { // TODO(adonovan): opt: evaluate whether it would be faster to decode // in two passes, first with struct { PkgPath string; Objects BLOB } @@ -146,10 +147,9 @@ func Lookup(m *source.Metadata, data []byte, pkgPath source.PackagePath, objPath mustDecode(data, &packages) for _, gp := range packages { - if gp.PkgPath == pkgPath { - var locs []protocol.Location + if objectSet, ok := targets[gp.PkgPath]; ok { for _, gobObj := range gp.Objects { - if gobObj.Path == objPath { + if _, ok := objectSet[gobObj.Path]; ok { for _, ref := range gobObj.Refs { uri := m.CompiledGoFiles[ref.FileIndex] locs = append(locs, protocol.Location{ @@ -159,10 +159,10 @@ func Lookup(m *source.Metadata, data []byte, pkgPath source.PackagePath, objPath } } } - return locs } } - return nil // this package does not reference that one + + return locs } // -- serialized representation -- From c224aaef06a9db6cd0c48edec48303984f8c9750 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 25 Jan 2023 15:46:51 -0500 Subject: [PATCH 671/723] gopls/internal/lsp/cmd/test: new integration test for gopls command This change creates an entirely new set of integration tests for the client-side logic of the gopls command and its subcommands. Each test fork+execs the gopls command and makes assertions about its exit code and stdout/stderr. The tests run in parallel (~3s). This is not intended as a test of server-side behavior. By decoupling the client tests from the server tests, we'll be able to delete all the other files in this directory (in a follow-up), allowing us to reorganize the marker-based tests of the LSP protocol so that they can be used in regtests. Updates golang/go#54845 Change-Id: I5fe11849079f7cc5fe44fc50cfcfd6bbff384014 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463515 gopls-CI: kokoro Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/cmd/call_hierarchy.go | 7 +- gopls/internal/lsp/cmd/folding_range.go | 2 +- .../internal/lsp/cmd/test/integration_test.go | 898 ++++++++++++++++++ gopls/internal/lsp/signature_help.go | 6 +- .../internal/lsp/testdata/folding/a.go.golden | 70 +- .../lsp/testdata/folding/bad.go.golden | 16 +- 6 files changed, 948 insertions(+), 51 deletions(-) create mode 100644 gopls/internal/lsp/cmd/test/integration_test.go diff --git a/gopls/internal/lsp/cmd/call_hierarchy.go b/gopls/internal/lsp/cmd/call_hierarchy.go index 0220fd275e2..9892fdff966 100644 --- a/gopls/internal/lsp/cmd/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/call_hierarchy.go @@ -129,13 +129,12 @@ func callItemPrintString(ctx context.Context, conn *connection, item protocol.Ca } var callRanges []string for _, rng := range calls { - callSpan, err := callsFile.mapper.LocationSpan(protocol.Location{URI: item.URI, Range: rng}) + call, err := callsFile.mapper.RangeSpan(rng) if err != nil { return "", err } - - spn := fmt.Sprint(callSpan) - callRanges = append(callRanges, fmt.Sprint(spn[strings.Index(spn, ":")+1:])) + callRange := fmt.Sprintf("%d:%d-%d", call.Start().Line(), call.Start().Column(), call.End().Column()) + callRanges = append(callRanges, callRange) } printString := fmt.Sprintf("function %s in %v", item.Name, itemSpan) diff --git a/gopls/internal/lsp/cmd/folding_range.go b/gopls/internal/lsp/cmd/folding_range.go index 7a9cbf9e8fb..68d93a3fb7e 100644 --- a/gopls/internal/lsp/cmd/folding_range.go +++ b/gopls/internal/lsp/cmd/folding_range.go @@ -65,7 +65,7 @@ func (r *foldingRanges) Run(ctx context.Context, args ...string) error { r.StartLine+1, r.StartCharacter+1, r.EndLine+1, - r.EndCharacter, + r.EndCharacter+1, ) } diff --git a/gopls/internal/lsp/cmd/test/integration_test.go b/gopls/internal/lsp/cmd/test/integration_test.go new file mode 100644 index 00000000000..7b0f852fa42 --- /dev/null +++ b/gopls/internal/lsp/cmd/test/integration_test.go @@ -0,0 +1,898 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package cmdtest + +// This file defines integration tests of each gopls subcommand that +// fork+exec the command in a separate process. +// +// (Rather than execute 'go build gopls' during the test, we reproduce +// the main entrypoint in the test executable.) +// +// The purpose of this test is to exercise client-side logic such as +// argument parsing and formatting of LSP RPC responses, not server +// behavior; see lsp_test for that. +// +// All tests run in parallel. +// +// TODO(adonovan): +// - Use markers to represent positions in the input and in assertions. +// - Coverage of cross-cutting things like cwd, enviro, span parsing, etc. +// - Subcommands that accept -write and -diff flags should implement +// them consistently wrt the default behavior; factor their tests. +// - Add missing test for 'vulncheck' subcommand. +// - Add tests for client-only commands: serve, bug, help, api-json, licenses. + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + exec "golang.org/x/sys/execabs" + "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/gopls/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/tool" + "golang.org/x/tools/txtar" +) + +// TestVersion tests the 'version' subcommand (../info.go). +func TestVersion(t *testing.T) { + t.Parallel() + + tree := writeTree(t, "") + + // There's not much we can robustly assert about the actual version. + const want = debug.Version // e.g. "master" + + // basic + { + res := gopls(t, tree, "version") + res.checkExit(0) + res.checkStdout(want) + } + + // -json flag + { + res := gopls(t, tree, "version", "-json") + res.checkExit(0) + var v debug.ServerVersion + if res.toJSON(&v) { + if v.Version != want { + t.Errorf("expected Version %q, got %q (%v)", want, v.Version, res) + } + } + } +} + +// TestCheck tests the 'check' subcommand (../check.go). +func TestCheck(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +import "fmt" +var _ = fmt.Sprintf("%s", 123) + +-- b.go -- +package a +import "fmt" +var _ = fmt.Sprintf("%d", "123") +`) + + // no files + { + res := gopls(t, tree, "check") + res.checkExit(0) + if res.stdout != "" { + t.Errorf("unexpected output: %v", res) + } + } + + // one file + { + res := gopls(t, tree, "check", "./a.go") + res.checkExit(0) + res.checkStdout("fmt.Sprintf format %s has arg 123 of wrong type int") + } + + // two files + { + res := gopls(t, tree, "check", "./a.go", "./b.go") + res.checkExit(0) + res.checkStdout(`a.go:.* fmt.Sprintf format %s has arg 123 of wrong type int`) + res.checkStdout(`b.go:.* fmt.Sprintf format %d has arg "123" of wrong type string`) + } +} + +// TestCallHierarchy tests the 'call_hierarchy' subcommand (../call_hierarchy.go). +func TestCallHierarchy(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +func f() {} +func g() { + f() +} +func h() { + f() + f() +} +`) + // missing position + { + res := gopls(t, tree, "call_hierarchy") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // wrong place + { + res := gopls(t, tree, "call_hierarchy", "a.go:1") + res.checkExit(2) + res.checkStderr("identifier not found") + } + // f is called once from g and twice from h. + { + res := gopls(t, tree, "call_hierarchy", "a.go:2:6") + res.checkExit(0) + // We use regexp '.' as an OS-agnostic path separator. + res.checkStdout("ranges 7:2-3, 8:2-3 in ..a.go from/to function h in ..a.go:6:6-7") + res.checkStdout("ranges 4:2-3 in ..a.go from/to function g in ..a.go:3:6-7") + res.checkStdout("identifier: function f in ..a.go:2:6-7") + } +} + +// TestDefinition tests the 'definition' subcommand (../definition.go). +func TestDefinition(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +import "fmt" +func f() { + fmt.Println() +} +func g() { + f() +} +`) + // missing position + { + res := gopls(t, tree, "definition") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // intra-package + { + res := gopls(t, tree, "definition", "a.go:7:2") // "f()" + res.checkExit(0) + res.checkStdout("a.go:3:6-7: defined here as func f") + } + // cross-package + { + res := gopls(t, tree, "definition", "a.go:4:7") // "Println" + res.checkExit(0) + res.checkStdout("print.go.* defined here as func fmt.Println") + res.checkStdout("Println formats using the default formats for its operands") + } + // -json and -markdown + { + res := gopls(t, tree, "definition", "-json", "-markdown", "a.go:4:7") + res.checkExit(0) + var defn cmd.Definition + if res.toJSON(&defn) { + if !strings.HasPrefix(defn.Description, "```go\nfunc fmt.Println") { + t.Errorf("Description does not start with markdown code block. Got: %s", defn.Description) + } + } + } +} + +// TestFoldingRanges tests the 'folding_ranges' subcommand (../folding_range.go). +func TestFoldingRanges(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +func f(x int) { + // hello +} +`) + // missing filename + { + res := gopls(t, tree, "folding_ranges") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // success + { + res := gopls(t, tree, "folding_ranges", "a.go") + res.checkExit(0) + res.checkStdout("2:8-2:13") // params (x int) + res.checkStdout("2:16-4:1") // body { ... } + } +} + +// TestFormat tests the 'format' subcommand (../format.go). +func TestFormat(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- a.go -- +package a ; func f ( ) { } +`) + const want = `package a + +func f() {} +` + + // no files => nop + { + res := gopls(t, tree, "format") + res.checkExit(0) + } + // default => print formatted result + { + res := gopls(t, tree, "format", "a.go") + res.checkExit(0) + if res.stdout != want { + t.Errorf("format: got <<%s>>, want <<%s>>", res.stdout, want) + } + } + // start/end position not supported (unless equal to start/end of file) + { + res := gopls(t, tree, "format", "a.go:1-2") + res.checkExit(2) + res.checkStderr("only full file formatting supported") + } + // -list: show only file names + { + res := gopls(t, tree, "format", "-list", "a.go") + res.checkExit(0) + res.checkStdout("a.go") + } + // -diff prints a unified diff + { + res := gopls(t, tree, "format", "-diff", "a.go") + res.checkExit(0) + // We omit the filenames as they vary by OS. + want := ` +-package a ; func f ( ) { } ++package a ++ ++func f() {} +` + res.checkStdout(regexp.QuoteMeta(want)) + } + // -write updates the file + { + res := gopls(t, tree, "format", "-write", "a.go") + res.checkExit(0) + res.checkStdout("^$") // empty + checkContent(t, filepath.Join(tree, "a.go"), want) + } +} + +// TestHighlight tests the 'highlight' subcommand (../highlight.go). +func TestHighlight(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- a.go -- +package a +import "fmt" +func f() { + fmt.Println() + fmt.Println() +} +`) + + // no arguments + { + res := gopls(t, tree, "highlight") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // all occurrences of Println + { + res := gopls(t, tree, "highlight", "a.go:4:7") + res.checkExit(0) + res.checkStdout("a.go:4:6-13") + res.checkStdout("a.go:5:6-13") + } +} + +// TestImplementations tests the 'implementation' subcommand (../implementation.go). +func TestImplementations(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- a.go -- +package a +import "fmt" +type T int +func (T) String() string { return "" } +`) + + // no arguments + { + res := gopls(t, tree, "implementation") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // T.String + { + res := gopls(t, tree, "implementation", "a.go:4:10") + res.checkExit(0) + // TODO(adonovan): extract and check the content of the reported ranges? + // We use regexp '.' as an OS-agnostic path separator. + res.checkStdout("fmt.print.go:") // fmt.Stringer.String + res.checkStdout("runtime.error.go:") // runtime.stringer.String + } +} + +// TestImports tests the 'imports' subcommand (../imports.go). +func TestImports(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- a.go -- +package a +func _() { + fmt.Println() +} +`) + + want := ` +package a + +import "fmt" +func _() { + fmt.Println() +} +`[1:] + + // no arguments + { + res := gopls(t, tree, "imports") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // default: print with imports + { + res := gopls(t, tree, "imports", "a.go") + res.checkExit(0) + if res.stdout != want { + t.Errorf("format: got <<%s>>, want <<%s>>", res.stdout, want) + } + } + // -diff: show a unified diff + { + res := gopls(t, tree, "imports", "-diff", "a.go") + res.checkExit(0) + res.checkStdout(regexp.QuoteMeta(`+import "fmt"`)) + } + // -write: update file + { + res := gopls(t, tree, "imports", "-write", "a.go") + res.checkExit(0) + checkContent(t, filepath.Join(tree, "a.go"), want) + } +} + +// TestLinks tests the 'links' subcommand (../links.go). +func TestLinks(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- a.go -- +// Link in package doc: https://pkg.go.dev/ +package a + +// Link in internal comment: https://go.dev/cl + +// Doc comment link: https://blog.go.dev/ +func f() {} +`) + // no arguments + { + res := gopls(t, tree, "links") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // success + { + res := gopls(t, tree, "links", "a.go") + res.checkExit(0) + res.checkStdout("https://go.dev/cl") + res.checkStdout("https://pkg.go.dev") + res.checkStdout("https://blog.go.dev/") + } + // -json + { + res := gopls(t, tree, "links", "-json", "a.go") + res.checkExit(0) + res.checkStdout("https://pkg.go.dev") + res.checkStdout("https://go.dev/cl") + res.checkStdout("https://blog.go.dev/") // at 5:21-5:41 + var links []protocol.DocumentLink + if res.toJSON(&links) { + // Check just one of the three locations. + if got, want := fmt.Sprint(links[2].Range), "5:21-5:41"; got != want { + t.Errorf("wrong link location: got %v, want %v", got, want) + } + } + } +} + +// TestReferences tests the 'references' subcommand (../references.go). +func TestReferences(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +import "fmt" +func f() { + fmt.Println() +} + +-- b.go -- +package a +import "fmt" +func g() { + fmt.Println() +} +`) + // no arguments + { + res := gopls(t, tree, "references") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // fmt.Println + { + res := gopls(t, tree, "references", "a.go:4:10") + res.checkExit(0) + res.checkStdout("a.go:4:6-13") + res.checkStdout("b.go:4:6-13") + } +} + +// TestSignature tests the 'signature' subcommand (../signature.go). +func TestSignature(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +import "fmt" +func f() { + fmt.Println(123) +} +`) + // no arguments + { + res := gopls(t, tree, "signature") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // at 123 inside fmt.Println() call + { + res := gopls(t, tree, "signature", "a.go:4:15") + res.checkExit(0) + res.checkStdout("Println\\(a ...") + res.checkStdout("Println formats using the default formats...") + } +} + +// TestPrepareRename tests the 'prepare_rename' subcommand (../prepare_rename.go). +func TestPrepareRename(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +func oldname() {} +`) + // no arguments + { + res := gopls(t, tree, "prepare_rename") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // in 'package' keyword + { + res := gopls(t, tree, "prepare_rename", "a.go:1:3") + res.checkExit(2) + res.checkStderr("request is not valid at the given position") + } + // in 'package' identifier (not supported by client) + { + res := gopls(t, tree, "prepare_rename", "a.go:1:9") + res.checkExit(2) + res.checkStderr("can't rename package") + } + // in func oldname + { + res := gopls(t, tree, "prepare_rename", "a.go:2:9") + res.checkExit(0) + res.checkStdout("a.go:2:6-13") // all of "oldname" + } +} + +// TestRename tests the 'rename' subcommand (../rename.go). +func TestRename(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +func oldname() {} +`) + // no arguments + { + res := gopls(t, tree, "rename") + res.checkExit(2) + res.checkStderr("expects 2 arguments") + } + // missing newname + { + res := gopls(t, tree, "rename", "a.go:1:3") + res.checkExit(2) + res.checkStderr("expects 2 arguments") + } + // in 'package' keyword + { + res := gopls(t, tree, "rename", "a.go:1:3", "newname") + res.checkExit(2) + res.checkStderr("no object found") + } + // in 'package' identifier + { + res := gopls(t, tree, "rename", "a.go:1:9", "newname") + res.checkExit(2) + res.checkStderr(`cannot rename package: module path .* same as the package path, so .* no effect`) + } + // success, func oldname (and -diff) + { + res := gopls(t, tree, "rename", "-diff", "a.go:2:9", "newname") + res.checkExit(0) + res.checkStdout(regexp.QuoteMeta("-func oldname() {}")) + res.checkStdout(regexp.QuoteMeta("+func newname() {}")) + } +} + +// TestSymbols tests the 'symbols' subcommand (../symbols.go). +func TestSymbols(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +func f() +var v int +const c = 0 +`) + // no files + { + res := gopls(t, tree, "symbols") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // success + { + res := gopls(t, tree, "symbols", "a.go:123:456") // (line/col ignored) + res.checkExit(0) + res.checkStdout("f Function 2:6-2:7") + res.checkStdout("v Variable 3:5-3:6") + res.checkStdout("c Constant 4:7-4:8") + } +} + +// TestSemtok tests the 'semtok' subcommand (../semantictokens.go). +func TestSemtok(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +func f() +var v int +const c = 0 +`) + // no files + { + res := gopls(t, tree, "semtok") + res.checkExit(2) + res.checkStderr("expected one file name") + } + // success + { + res := gopls(t, tree, "semtok", "a.go") + res.checkExit(0) + got := res.stdout + want := ` +/*⇒7,keyword,[]*/package /*⇒1,namespace,[]*/a +/*⇒4,keyword,[]*/func /*⇒1,function,[definition]*/f() +/*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/v /*⇒3,type,[defaultLibrary]*/int +/*⇒5,keyword,[]*/const /*⇒1,variable,[definition readonly]*/c = /*⇒1,number,[]*/0 +`[1:] + if got != want { + t.Errorf("semtok: got <<%s>>, want <<%s>>", got, want) + } + } +} + +// TestFix tests the 'fix' subcommand (../suggested_fix.go). +func TestFix(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +var _ error = T(0) +type T int +func f() (int, string) { return } +`) + want := ` +package a +var _ error = T(0) +type T int +func f() (int, string) { return 0, "" } +`[1:] + + // no arguments + { + res := gopls(t, tree, "fix") + res.checkExit(2) + res.checkStderr("expects at least 1 argument") + } + // success (-a enables fillreturns) + { + res := gopls(t, tree, "fix", "-a", "a.go") + res.checkExit(0) + got := res.stdout + if got != want { + t.Errorf("fix: got <<%s>>, want <<%s>>", got, want) + } + } + // TODO(adonovan): more tests: + // - -write, -diff: factor with imports, format, rename. + // - without -all flag + // - args[2:] is an optional list of protocol.CodeActionKind enum values. + // - a span argument with a range causes filtering. +} + +// TestWorkspaceSymbol tests the 'workspace_symbol' subcommand (../workspace_symbol.go). +func TestWorkspaceSymbol(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- a.go -- +package a +func someFunctionName() +`) + // no files + { + res := gopls(t, tree, "workspace_symbol") + res.checkExit(2) + res.checkStderr("expects 1 argument") + } + // success + { + res := gopls(t, tree, "workspace_symbol", "meFun") + res.checkExit(0) + res.checkStdout("a.go:2:6-22 someFunctionName Function") + } +} + +// -- test framework -- + +func TestMain(m *testing.M) { + switch os.Getenv("ENTRYPOINT") { + case "goplsMain": + goplsMain() + default: + os.Exit(m.Run()) + } +} + +// This function is a stand-in for gopls.main in ../../../../main.go. +func goplsMain() { + bug.PanicOnBugs = true // (not in the production command) + tool.Main(context.Background(), cmd.New("gopls", "", nil, hooks.Options), os.Args[1:]) +} + +// writeTree extracts a txtar archive into a new directory and returns its path. +func writeTree(t *testing.T, archive string) string { + root := t.TempDir() + + // This unfortunate step is required because gopls output + // expands symbolic links it its input file names (arguably it + // should not), and on macOS the temp dir is in /var -> private/var. + root, err := filepath.EvalSymlinks(root) + if err != nil { + t.Fatal(err) + } + + for _, f := range txtar.Parse([]byte(archive)).Files { + filename := filepath.Join(root, f.Name) + if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filename, f.Data, 0666); err != nil { + t.Fatal(err) + } + } + return root +} + +// gopls executes gopls in a child process. +func gopls(t *testing.T, dir string, args ...string) *result { + testenv.NeedsTool(t, "go") + + // Catch inadvertent use of dir=".", which would make + // the ReplaceAll below unpredictable. + if !filepath.IsAbs(dir) { + t.Fatalf("dir is not absolute: %s", dir) + } + + cmd := exec.Command(os.Args[0], args...) + cmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain") + cmd.Dir = dir + cmd.Stdout = new(bytes.Buffer) + cmd.Stderr = new(bytes.Buffer) + + cmdErr := cmd.Run() + + stdout := strings.ReplaceAll(fmt.Sprint(cmd.Stdout), dir, ".") + stderr := strings.ReplaceAll(fmt.Sprint(cmd.Stderr), dir, ".") + exitcode := 0 + if cmdErr != nil { + if exitErr, ok := cmdErr.(*exec.ExitError); ok { + exitcode = exitErr.ExitCode() + } else { + stderr = cmdErr.Error() // (execve failure) + exitcode = -1 + } + } + res := &result{ + t: t, + command: "gopls " + strings.Join(args, " "), + exitcode: exitcode, + stdout: stdout, + stderr: stderr, + } + if false { + t.Log(res) + } + return res +} + +// A result holds the result of a gopls invocation, and provides assertion helpers. +type result struct { + t *testing.T + command string + exitcode int + stdout, stderr string +} + +func (res *result) String() string { + return fmt.Sprintf("%s: exit=%d stdout=<<%s>> stderr=<<%s>>", + res.command, res.exitcode, res.stdout, res.stderr) +} + +// checkExit asserts that gopls returned the expected exit code. +func (res *result) checkExit(code int) { + res.t.Helper() + if res.exitcode != code { + res.t.Errorf("%s: exited with code %d, want %d (%s)", + res.command, res.exitcode, code, res) + } +} + +// checkStdout asserts that the gopls standard output matches the pattern. +func (res *result) checkStdout(pattern string) { + res.t.Helper() + res.checkOutput(pattern, "stdout", res.stdout) +} + +// checkStderr asserts that the gopls standard error matches the pattern. +func (res *result) checkStderr(pattern string) { + res.t.Helper() + res.checkOutput(pattern, "stderr", res.stderr) +} + +func (res *result) checkOutput(pattern, name, content string) { + res.t.Helper() + if match, err := regexp.MatchString(pattern, content); err != nil { + res.t.Errorf("invalid regexp: %v", err) + } else if !match { + res.t.Errorf("%s: %s does not match [%s]; got <<%s>>", + res.command, name, pattern, content) + } +} + +// toJSON decodes res.stdout as JSON into to *ptr and reports its success. +func (res *result) toJSON(ptr interface{}) bool { + if err := json.Unmarshal([]byte(res.stdout), ptr); err != nil { + res.t.Errorf("invalid JSON %v", err) + return false + } + return true +} + +// checkContent checks that the contents of the file are as expected. +func checkContent(t *testing.T, filename, want string) { + data, err := os.ReadFile(filename) + if err != nil { + t.Error(err) + return + } + if got := string(data); got != want { + t.Errorf("content of %s is <<%s>>, want <<%s>>", filename, got, want) + } +} diff --git a/gopls/internal/lsp/signature_help.go b/gopls/internal/lsp/signature_help.go index 8a343fbec81..b623f78ea74 100644 --- a/gopls/internal/lsp/signature_help.go +++ b/gopls/internal/lsp/signature_help.go @@ -7,10 +7,10 @@ package lsp import ( "context" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" ) func (s *Server) signatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) { @@ -22,7 +22,7 @@ func (s *Server) signatureHelp(ctx context.Context, params *protocol.SignatureHe info, activeParameter, err := source.SignatureHelp(ctx, snapshot, fh, params.Position) if err != nil { event.Error(ctx, "no signature help", err, tag.Position.Of(params.Position)) - return nil, nil + return nil, nil // sic? There could be many reasons for failure. } return &protocol.SignatureHelp{ Signatures: []protocol.SignatureInformation{*info}, diff --git a/gopls/internal/lsp/testdata/folding/a.go.golden b/gopls/internal/lsp/testdata/folding/a.go.golden index ce691023361..23befa73be6 100644 --- a/gopls/internal/lsp/testdata/folding/a.go.golden +++ b/gopls/internal/lsp/testdata/folding/a.go.golden @@ -247,41 +247,41 @@ is not indented` } -- foldingRange-cmd -- -3:9-6:0 -10:22-11:32 -12:10-12:9 -12:20-75:0 -14:10-25:1 -15:12-20:3 -16:12-18:2 -17:16-17:21 -18:11-20:2 -19:16-19:22 -21:13-22:22 -22:15-22:21 -23:10-24:24 -24:15-24:23 -26:24-28:11 -30:24-33:32 -34:12-38:1 -39:16-41:1 -42:21-46:1 -47:17-51:1 -52:8-56:1 -57:15-57:23 -57:32-57:40 -58:10-69:1 -59:18-64:3 -60:11-62:2 -61:16-61:28 -62:11-64:2 -63:16-63:29 -65:11-66:18 -66:15-66:17 -67:10-68:24 -68:15-68:23 -70:32-71:30 -72:9-74:16 +3:9-6:1 +10:22-11:33 +12:10-12:10 +12:20-75:1 +14:10-25:2 +15:12-20:4 +16:12-18:3 +17:16-17:22 +18:11-20:3 +19:16-19:23 +21:13-22:23 +22:15-22:22 +23:10-24:25 +24:15-24:24 +26:24-28:12 +30:24-33:33 +34:12-38:2 +39:16-41:2 +42:21-46:2 +47:17-51:2 +52:8-56:2 +57:15-57:24 +57:32-57:41 +58:10-69:2 +59:18-64:4 +60:11-62:3 +61:16-61:29 +62:11-64:3 +63:16-63:30 +65:11-66:19 +66:15-66:18 +67:10-68:25 +68:15-68:24 +70:32-71:31 +72:9-74:17 -- foldingRange-comment-0 -- package folding //@fold("package") diff --git a/gopls/internal/lsp/testdata/folding/bad.go.golden b/gopls/internal/lsp/testdata/folding/bad.go.golden index d1bdfec60cd..6db6982eb4c 100644 --- a/gopls/internal/lsp/testdata/folding/bad.go.golden +++ b/gopls/internal/lsp/testdata/folding/bad.go.golden @@ -45,14 +45,14 @@ func badBar() string { x := true } -- foldingRange-cmd -- -3:9-5:0 -7:9-8:8 -11:13-11:12 -11:23-18:0 -12:8-15:1 -14:15-14:20 -15:10-16:23 -16:15-16:21 +3:9-5:1 +7:9-8:9 +11:13-11:13 +11:23-18:1 +12:8-15:2 +14:15-14:21 +15:10-16:24 +16:15-16:22 -- foldingRange-imports-0 -- package folding //@fold("package") From f3c36a20991b11a5f6b0dd2049e40bcd21bc3530 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 25 Jan 2023 16:43:37 -0500 Subject: [PATCH 672/723] gopls/internal/lsp/cmd/test: delete marker-based tests of gopls cmd These tests were an implementation of the tests.Test marker-test machinery, based on invoking the logic of the gopls command, within the same process. The marker tests exercise server logic, which is best done by making LSP requests, as the lsp_test does. Client logic in the gopls command and its subcommands is best exercised by fork+execing the command, and exercising features on the client, such as flags, argument parsing, and output printing. That's what the new integration_test does. With this change we are down to one implementation of tests.Tests, which means we can start to change it to make it usable from regtests too. Updates golang/go#54845 Change-Id: Ia00897f0268ed4c293e716e1d34d9c84cfdf3109 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463555 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/cmd/test/call_hierarchy.go | 85 --------- gopls/internal/lsp/cmd/test/check.go | 63 ------- gopls/internal/lsp/cmd/test/cmdtest.go | 173 +----------------- gopls/internal/lsp/cmd/test/definition.go | 55 ------ gopls/internal/lsp/cmd/test/folding_range.go | 25 --- gopls/internal/lsp/cmd/test/format.go | 87 --------- gopls/internal/lsp/cmd/test/highlight.go | 29 --- gopls/internal/lsp/cmd/test/implementation.go | 37 ---- gopls/internal/lsp/cmd/test/imports.go | 25 --- gopls/internal/lsp/cmd/test/links.go | 30 --- gopls/internal/lsp/cmd/test/prepare_rename.go | 45 ----- gopls/internal/lsp/cmd/test/references.go | 52 ------ gopls/internal/lsp/cmd/test/rename.go | 30 --- gopls/internal/lsp/cmd/test/semanticdriver.go | 36 ---- gopls/internal/lsp/cmd/test/signature.go | 34 ---- gopls/internal/lsp/cmd/test/suggested_fix.go | 38 ---- gopls/internal/lsp/cmd/test/symbols.go | 24 --- .../internal/lsp/cmd/test/workspace_symbol.go | 54 ------ .../internal/lsp/testdata/folding/a.go.golden | 37 ---- .../lsp/testdata/folding/bad.go.golden | 10 - gopls/internal/lsp/tests/tests.go | 10 +- gopls/test/gopls_test.go | 42 ----- 22 files changed, 4 insertions(+), 1017 deletions(-) delete mode 100644 gopls/internal/lsp/cmd/test/call_hierarchy.go delete mode 100644 gopls/internal/lsp/cmd/test/check.go delete mode 100644 gopls/internal/lsp/cmd/test/definition.go delete mode 100644 gopls/internal/lsp/cmd/test/folding_range.go delete mode 100644 gopls/internal/lsp/cmd/test/format.go delete mode 100644 gopls/internal/lsp/cmd/test/highlight.go delete mode 100644 gopls/internal/lsp/cmd/test/implementation.go delete mode 100644 gopls/internal/lsp/cmd/test/imports.go delete mode 100644 gopls/internal/lsp/cmd/test/links.go delete mode 100644 gopls/internal/lsp/cmd/test/prepare_rename.go delete mode 100644 gopls/internal/lsp/cmd/test/references.go delete mode 100644 gopls/internal/lsp/cmd/test/rename.go delete mode 100644 gopls/internal/lsp/cmd/test/semanticdriver.go delete mode 100644 gopls/internal/lsp/cmd/test/signature.go delete mode 100644 gopls/internal/lsp/cmd/test/suggested_fix.go delete mode 100644 gopls/internal/lsp/cmd/test/symbols.go delete mode 100644 gopls/internal/lsp/cmd/test/workspace_symbol.go delete mode 100644 gopls/test/gopls_test.go diff --git a/gopls/internal/lsp/cmd/test/call_hierarchy.go b/gopls/internal/lsp/cmd/test/call_hierarchy.go deleted file mode 100644 index 5517a51d5eb..00000000000 --- a/gopls/internal/lsp/cmd/test/call_hierarchy.go +++ /dev/null @@ -1,85 +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 cmdtest - -import ( - "fmt" - "sort" - "strings" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) { - collectCallSpansString := func(callItems []protocol.CallHierarchyItem) string { - var callSpans []string - for _, call := range callItems { - mapper, err := r.data.Mapper(call.URI.SpanURI()) - if err != nil { - t.Fatal(err) - } - callSpan, err := mapper.LocationSpan(protocol.Location{URI: call.URI, Range: call.Range}) - if err != nil { - t.Fatal(err) - } - callSpans = append(callSpans, fmt.Sprint(callSpan)) - } - // to make tests deterministic - sort.Strings(callSpans) - return r.Normalize(strings.Join(callSpans, "\n")) - } - - expectIn, expectOut := collectCallSpansString(expectedCalls.IncomingCalls), collectCallSpansString(expectedCalls.OutgoingCalls) - expectIdent := r.Normalize(fmt.Sprint(spn)) - - uri := spn.URI() - filename := uri.Filename() - target := filename + fmt.Sprintf(":%v:%v", spn.Start().Line(), spn.Start().Column()) - - got, stderr := r.NormalizeGoplsCmd(t, "call_hierarchy", target) - if stderr != "" { - t.Fatalf("call_hierarchy failed for %s: %s", target, stderr) - } - - gotIn, gotIdent, gotOut := cleanCallHierarchyCmdResult(got) - if expectIn != gotIn { - t.Errorf("incoming calls call_hierarchy failed for %s expected:\n%s\ngot:\n%s", target, expectIn, gotIn) - } - if expectIdent != gotIdent { - t.Errorf("call_hierarchy failed for %s expected:\n%s\ngot:\n%s", target, expectIdent, gotIdent) - } - if expectOut != gotOut { - t.Errorf("outgoing calls call_hierarchy failed for %s expected:\n%s\ngot:\n%s", target, expectOut, gotOut) - } - -} - -// parses function URI and Range from call hierarchy cmd output to -// incoming, identifier and outgoing calls (returned in that order) -// ex: "identifier: function d at .../callhierarchy/callhierarchy.go:19:6-7" -> ".../callhierarchy/callhierarchy.go:19:6-7" -func cleanCallHierarchyCmdResult(output string) (incoming, ident, outgoing string) { - var incomingCalls, outgoingCalls []string - for _, out := range strings.Split(output, "\n") { - if out == "" { - continue - } - - callLocation := out[strings.LastIndex(out, " ")+1:] - if strings.HasPrefix(out, "caller") { - incomingCalls = append(incomingCalls, callLocation) - } else if strings.HasPrefix(out, "callee") { - outgoingCalls = append(outgoingCalls, callLocation) - } else { - ident = callLocation - } - } - sort.Strings(incomingCalls) - sort.Strings(outgoingCalls) - incoming, outgoing = strings.Join(incomingCalls, "\n"), strings.Join(outgoingCalls, "\n") - return -} diff --git a/gopls/internal/lsp/cmd/test/check.go b/gopls/internal/lsp/cmd/test/check.go deleted file mode 100644 index dcbb2e0f2c1..00000000000 --- a/gopls/internal/lsp/cmd/test/check.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "io/ioutil" - "strings" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/span" -) - -// Diagnostics runs the "gopls check" command on a single file, parses -// its diagnostics, and compares against the expectations defined by -// markers in the source file. -func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) { - out, _ := r.runGoplsCmd(t, "check", uri.Filename()) - - content, err := ioutil.ReadFile(uri.Filename()) - if err != nil { - t.Fatal(err) - } - mapper := protocol.NewMapper(uri, content) - - // Parse command output into a set of diagnostics. - var got []*source.Diagnostic - for _, line := range strings.Split(out, "\n") { - if line == "" { - continue // skip blank - } - parts := strings.SplitN(line, ": ", 2) // "span: message" - if len(parts) != 2 { - t.Fatalf("output line not of form 'span: message': %q", line) - } - spn, message := span.Parse(parts[0]), parts[1] - rng, err := mapper.SpanRange(spn) - if err != nil { - t.Fatal(err) - } - // Set only the fields needed by DiffDiagnostics. - got = append(got, &source.Diagnostic{ - URI: uri, - Range: rng, - Message: message, - }) - } - - // Don't expect fields that we can't populate from the command output. - for _, diag := range want { - if diag.Source == "no_diagnostics" { - continue // see DiffDiagnostics - } - diag.Source = "" - diag.Severity = 0 - } - - tests.CompareDiagnostics(t, uri, want, got) -} diff --git a/gopls/internal/lsp/cmd/test/cmdtest.go b/gopls/internal/lsp/cmd/test/cmdtest.go index cc0ea8623a8..7f8a13b762d 100644 --- a/gopls/internal/lsp/cmd/test/cmdtest.go +++ b/gopls/internal/lsp/cmd/test/cmdtest.go @@ -1,177 +1,6 @@ -// Copyright 2019 The Go Authors. All rights reserved. +// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package cmdtest contains the test suite for the command line behavior of gopls. package cmdtest - -import ( - "bytes" - "context" - "flag" - "fmt" - "io" - "os" - "runtime" - "sync" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/cmd" - "golang.org/x/tools/gopls/internal/lsp/debug" - "golang.org/x/tools/gopls/internal/lsp/lsprpc" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/tool" -) - -// TestCommandLine runs the marker tests in files beneath testdata/ using -// implementations of each of the marker operations (e.g. @hover) that -// call the main function of the gopls command within this process. -func TestCommandLine(t *testing.T, testdata string, options func(*source.Options)) { - // On Android, the testdata directory is not copied to the runner. - if runtime.GOOS == "android" { - t.Skip("testdata directory not present on android") - } - tests.RunTests(t, testdata, false, func(t *testing.T, datum *tests.Data) { - ctx := tests.Context(t) - ts := newTestServer(ctx, options) - tests.Run(t, newRunner(datum, ctx, ts.Addr, options), datum) - cmd.CloseTestConnections(ctx) - }) -} - -func newTestServer(ctx context.Context, options func(*source.Options)) *servertest.TCPServer { - ctx = debug.WithInstance(ctx, "", "") - cache := cache.New(nil, nil) - ss := lsprpc.NewStreamServer(cache, false, options) - return servertest.NewTCPServer(ctx, ss, nil) -} - -// runner implements tests.Tests by invoking the gopls command. -// -// TODO(golang/go#54845): We don't plan to implement all the methods -// of tests.Test. Indeed, we'd like to delete the methods that are -// implemented because the two problems they solve are best addressed -// in other ways: -// -// 1. They provide coverage of the behavior of the server, but this -// coverage is almost identical to the coverage provided by -// executing the same tests by making LSP RPCs directly (see -// lsp_test), and the latter is more efficient. When they do -// differ, it is a pain for maintainers. -// -// 2. They provide coverage of the client-side code of the -// command-line tool, which turns arguments into an LSP request and -// prints the results. But this coverage could be more directly and -// efficiently achieved by running a small number of tests tailored -// to exercise the client-side code, not the server behavior. -// -// Once that's done, tests.Tests would have only a single -// implementation (LSP), and we could refactor the marker tests -// so that they more closely resemble self-contained regtests, -// as described in #54845. -type runner struct { - data *tests.Data - ctx context.Context - options func(*source.Options) - normalizers []tests.Normalizer - remote string -} - -func newRunner(data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner { - return &runner{ - data: data, - ctx: ctx, - options: options, - normalizers: tests.CollectNormalizers(data.Exported), - remote: remote, - } -} - -// runGoplsCmd returns the stdout and stderr of a gopls command. -// -// It does not fork+exec gopls, but in effect calls its main function, -// and thus the subcommand's Application.Run method, within this process. -// -// Stdout and stderr are temporarily redirected: not concurrency-safe! -// -// The "exit code" is printed to stderr but not returned. -// Invalid flags cause process exit. -func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) { - rStdout, wStdout, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - oldStdout := os.Stdout - rStderr, wStderr, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - oldStderr := os.Stderr - stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} - var wg sync.WaitGroup - wg.Add(2) - go func() { - io.Copy(stdout, rStdout) - wg.Done() - }() - go func() { - io.Copy(stderr, rStderr) - wg.Done() - }() - os.Stdout, os.Stderr = wStdout, wStderr - app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options) - remote := r.remote - s := flag.NewFlagSet(app.Name(), flag.ExitOnError) - err = tool.Run(tests.Context(t), s, - app, - append([]string{fmt.Sprintf("-remote=internal@%s", remote)}, args...)) - if err != nil { - fmt.Fprint(os.Stderr, err) - } - wStdout.Close() - wStderr.Close() - wg.Wait() - os.Stdout, os.Stderr = oldStdout, oldStderr - rStdout.Close() - rStderr.Close() - return stdout.String(), stderr.String() -} - -// NormalizeGoplsCmd runs the gopls command and normalizes its output. -func (r *runner) NormalizeGoplsCmd(t testing.TB, args ...string) (string, string) { - stdout, stderr := r.runGoplsCmd(t, args...) - return r.Normalize(stdout), r.Normalize(stderr) -} - -func (r *runner) Normalize(s string) string { - return tests.Normalize(s, r.normalizers) -} - -// Unimplemented methods of tests.Tests (see comment at runner): - -func (*runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {} -func (*runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { -} -func (*runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) { -} -func (*runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { -} -func (*runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { -} -func (*runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { -} -func (*runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { -} -func (*runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { -} -func (*runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {} -func (*runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) {} -func (*runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {} -func (*runner) Hover(t *testing.T, spn span.Span, info string) {} -func (*runner) InlayHints(t *testing.T, spn span.Span) {} -func (*runner) SelectionRanges(t *testing.T, spn span.Span) {} diff --git a/gopls/internal/lsp/cmd/test/definition.go b/gopls/internal/lsp/cmd/test/definition.go deleted file mode 100644 index ca84e80ebe2..00000000000 --- a/gopls/internal/lsp/cmd/test/definition.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "fmt" - "runtime" - "strings" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/span" -) - -type godefMode int - -const ( - plainGodef = godefMode(1 << iota) - jsonGoDef -) - -var godefModes = []godefMode{ - plainGodef, - jsonGoDef, -} - -func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { - if d.IsType || d.OnlyHover { - // TODO: support type definition, hover queries - return - } - d.Src = span.New(d.Src.URI(), span.NewPoint(0, 0, d.Src.Start().Offset()), span.Point{}) - for _, mode := range godefModes { - args := []string{"definition", "-markdown"} - tag := d.Name + "-definition" - if mode&jsonGoDef != 0 { - tag += "-json" - args = append(args, "-json") - } - uri := d.Src.URI() - args = append(args, fmt.Sprint(d.Src)) - got, _ := r.NormalizeGoplsCmd(t, args...) - if mode&jsonGoDef != 0 && runtime.GOOS == "windows" { - got = strings.Replace(got, "file:///", "file://", -1) - } - expect := strings.TrimSpace(string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { - return []byte(got), nil - }))) - if expect != "" && !strings.HasPrefix(got, expect) { - tests.CheckSameMarkdown(t, got, expect) - } - } -} diff --git a/gopls/internal/lsp/cmd/test/folding_range.go b/gopls/internal/lsp/cmd/test/folding_range.go deleted file mode 100644 index 184c01a05bb..00000000000 --- a/gopls/internal/lsp/cmd/test/folding_range.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "testing" - - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { - goldenTag := "foldingRange-cmd" - uri := spn.URI() - filename := uri.Filename() - got, _ := r.NormalizeGoplsCmd(t, "folding_ranges", filename) - expect := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { - return []byte(got), nil - })) - - if expect != got { - t.Errorf("folding_ranges failed failed for %s expected:\n%s\ngot:\n%s", filename, expect, got) - } -} diff --git a/gopls/internal/lsp/cmd/test/format.go b/gopls/internal/lsp/cmd/test/format.go deleted file mode 100644 index 368d535b20a..00000000000 --- a/gopls/internal/lsp/cmd/test/format.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "bytes" - "io/ioutil" - "os" - "regexp" - "strings" - "testing" - - exec "golang.org/x/sys/execabs" - - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/testenv" -) - -func (r *runner) Format(t *testing.T, spn span.Span) { - tag := "gofmt" - uri := spn.URI() - filename := uri.Filename() - expect := string(r.data.Golden(t, tag, filename, func() ([]byte, error) { - cmd := exec.Command("gofmt", filename) - contents, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files - contents = []byte(r.Normalize(fixFileHeader(string(contents)))) - return contents, nil - })) - if expect == "" { - //TODO: our error handling differs, for now just skip unformattable files - t.Skip("Unformattable file") - } - got, _ := r.NormalizeGoplsCmd(t, "format", filename) - if expect != got { - t.Errorf("format failed for %s expected:\n%s\ngot:\n%s", filename, expect, got) - } - // now check we can build a valid unified diff - unified, _ := r.NormalizeGoplsCmd(t, "format", "-d", filename) - checkUnified(t, filename, expect, unified) -} - -var unifiedHeader = regexp.MustCompile(`^diff -u.*\n(---\s+\S+\.go\.orig)\s+[\d-:. ]+(\n\+\+\+\s+\S+\.go)\s+[\d-:. ]+(\n@@)`) - -func fixFileHeader(s string) string { - match := unifiedHeader.FindStringSubmatch(s) - if match == nil { - return s - } - return strings.Join(append(match[1:], s[len(match[0]):]), "") -} - -func checkUnified(t *testing.T, filename string, expect string, patch string) { - testenv.NeedsTool(t, "patch") - if strings.Count(patch, "\n+++ ") > 1 { - // TODO(golang/go/#34580) - t.Skip("multi-file patch tests not supported yet") - } - applied := "" - if patch == "" { - applied = expect - } else { - temp, err := ioutil.TempFile("", "applied") - if err != nil { - t.Fatal(err) - } - temp.Close() - defer os.Remove(temp.Name()) - cmd := exec.Command("patch", "-u", "-p0", "-o", temp.Name(), filename) - cmd.Stdin = bytes.NewBuffer([]byte(patch)) - msg, err := cmd.CombinedOutput() - if err != nil { - t.Errorf("failed applying patch to %s: %v\ngot:\n%s\npatch:\n%s", filename, err, msg, patch) - return - } - out, err := ioutil.ReadFile(temp.Name()) - if err != nil { - t.Errorf("failed reading patched output for %s: %v\n", filename, err) - return - } - applied = string(out) - } - if expect != applied { - t.Errorf("apply unified gave wrong result for %s expected:\n%s\ngot:\n%s\npatch:\n%s", filename, expect, applied, patch) - } -} diff --git a/gopls/internal/lsp/cmd/test/highlight.go b/gopls/internal/lsp/cmd/test/highlight.go deleted file mode 100644 index cd51b093c68..00000000000 --- a/gopls/internal/lsp/cmd/test/highlight.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "testing" - - "fmt" - - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) Highlight(t *testing.T, spn span.Span, spans []span.Span) { - var expect string - for _, l := range spans { - expect += fmt.Sprintln(l) - } - expect = r.Normalize(expect) - - uri := spn.URI() - filename := uri.Filename() - target := filename + ":" + fmt.Sprint(spn.Start().Line()) + ":" + fmt.Sprint(spn.Start().Column()) - got, _ := r.NormalizeGoplsCmd(t, "highlight", target) - if expect != got { - t.Errorf("highlight failed for %s expected:\n%s\ngot:\n%s", target, expect, got) - } -} diff --git a/gopls/internal/lsp/cmd/test/implementation.go b/gopls/internal/lsp/cmd/test/implementation.go deleted file mode 100644 index e24584da99d..00000000000 --- a/gopls/internal/lsp/cmd/test/implementation.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "fmt" - "sort" - "testing" - - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) Implementation(t *testing.T, spn span.Span, imps []span.Span) { - var itemStrings []string - for _, i := range imps { - itemStrings = append(itemStrings, fmt.Sprint(i)) - } - sort.Strings(itemStrings) - var expect string - for _, i := range itemStrings { - expect += i + "\n" - } - expect = r.Normalize(expect) - - uri := spn.URI() - filename := uri.Filename() - target := filename + fmt.Sprintf(":%v:%v", spn.Start().Line(), spn.Start().Column()) - - got, stderr := r.NormalizeGoplsCmd(t, "implementation", target) - if stderr != "" { - t.Errorf("implementation failed for %s: %s", target, stderr) - } else if expect != got { - t.Errorf("implementation failed for %s expected:\n%s\ngot:\n%s", target, expect, got) - } -} diff --git a/gopls/internal/lsp/cmd/test/imports.go b/gopls/internal/lsp/cmd/test/imports.go deleted file mode 100644 index d26c88664e2..00000000000 --- a/gopls/internal/lsp/cmd/test/imports.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "testing" - - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/diff" -) - -func (r *runner) Import(t *testing.T, spn span.Span) { - uri := spn.URI() - filename := uri.Filename() - got, _ := r.NormalizeGoplsCmd(t, "imports", filename) - want := string(r.data.Golden(t, "goimports", filename, func() ([]byte, error) { - return []byte(got), nil - })) - if want != got { - unified := diff.Unified("want", "got", want, got) - t.Errorf("imports failed for %s, expected:\n%s", filename, unified) - } -} diff --git a/gopls/internal/lsp/cmd/test/links.go b/gopls/internal/lsp/cmd/test/links.go deleted file mode 100644 index a9616ee48a9..00000000000 --- a/gopls/internal/lsp/cmd/test/links.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "encoding/json" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { - m, err := r.data.Mapper(uri) - if err != nil { - t.Fatal(err) - } - out, _ := r.NormalizeGoplsCmd(t, "links", "-json", uri.Filename()) - var got []protocol.DocumentLink - err = json.Unmarshal([]byte(out), &got) - if err != nil { - t.Fatal(err) - } - if diff := tests.DiffLinks(m, wantLinks, got); diff != "" { - t.Error(diff) - } -} diff --git a/gopls/internal/lsp/cmd/test/prepare_rename.go b/gopls/internal/lsp/cmd/test/prepare_rename.go deleted file mode 100644 index b24738881e5..00000000000 --- a/gopls/internal/lsp/cmd/test/prepare_rename.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "fmt" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/cmd" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) { - m, err := r.data.Mapper(src.URI()) - if err != nil { - t.Errorf("prepare_rename failed: %v", err) - } - - var ( - target = fmt.Sprintf("%v", src) - args = []string{"prepare_rename", target} - stdOut, stdErr = r.NormalizeGoplsCmd(t, args...) - expect string - ) - - if want.Text == "" { - if stdErr != "" && stdErr != cmd.ErrInvalidRenamePosition.Error() { - t.Errorf("prepare_rename failed for %s,\nexpected:\n`%v`\ngot:\n`%v`", target, expect, stdErr) - } - return - } - - ws, err := m.RangeSpan(want.Range) - if err != nil { - t.Errorf("prepare_rename failed: %v", err) - } - - expect = r.Normalize(fmt.Sprintln(ws)) - if expect != stdOut { - t.Errorf("prepare_rename failed for %s expected:\n`%s`\ngot:\n`%s`\n", target, expect, stdOut) - } -} diff --git a/gopls/internal/lsp/cmd/test/references.go b/gopls/internal/lsp/cmd/test/references.go deleted file mode 100644 index 4b26f982a1a..00000000000 --- a/gopls/internal/lsp/cmd/test/references.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "fmt" - "sort" - "testing" - - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) References(t *testing.T, spn span.Span, itemList []span.Span) { - for _, includeDeclaration := range []bool{true, false} { - t.Run(fmt.Sprintf("refs-declaration-%v", includeDeclaration), func(t *testing.T) { - var itemStrings []string - for i, s := range itemList { - // We don't want the first result if we aren't including the declaration. - if i == 0 && !includeDeclaration { - continue - } - itemStrings = append(itemStrings, fmt.Sprint(s)) - } - sort.Strings(itemStrings) - var expect string - for _, s := range itemStrings { - expect += s + "\n" - } - expect = r.Normalize(expect) - - uri := spn.URI() - filename := uri.Filename() - target := filename + fmt.Sprintf(":%v:%v", spn.Start().Line(), spn.Start().Column()) - args := []string{"references"} - if includeDeclaration { - args = append(args, "-d") - } - args = append(args, target) - got, stderr := r.NormalizeGoplsCmd(t, args...) - if stderr != "" { - t.Errorf("references failed for %s: %s", target, stderr) - } else if expect != got { - // TODO(adonovan): print the query, the expectations, and - // the actual results clearly. Factor in common with the - // other two implementations of runner.References. - t.Errorf("references failed for %s expected:\n%s\ngot:\n%s", target, expect, got) - } - }) - } -} diff --git a/gopls/internal/lsp/cmd/test/rename.go b/gopls/internal/lsp/cmd/test/rename.go deleted file mode 100644 index a9eb31e3877..00000000000 --- a/gopls/internal/lsp/cmd/test/rename.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "fmt" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { - filename := spn.URI().Filename() - goldenTag := newText + "-rename" - loc := fmt.Sprintf("%v", spn) - got, err := r.NormalizeGoplsCmd(t, "rename", loc, newText) - got += err - want := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { - return []byte(got), nil - })) - if diff := compare.Text(want, got); diff != "" { - t.Errorf("rename failed with %v %v (-want +got):\n%s", loc, newText, diff) - } - // now check we can build a valid unified diff - unified, _ := r.NormalizeGoplsCmd(t, "rename", "-d", loc, newText) - checkUnified(t, filename, want, unified) -} diff --git a/gopls/internal/lsp/cmd/test/semanticdriver.go b/gopls/internal/lsp/cmd/test/semanticdriver.go deleted file mode 100644 index 069dd64f6e6..00000000000 --- a/gopls/internal/lsp/cmd/test/semanticdriver.go +++ /dev/null @@ -1,36 +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 cmdtest - -import ( - "strings" - "testing" - - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { - uri := spn.URI() - filename := uri.Filename() - got, stderr := r.NormalizeGoplsCmd(t, "semtok", filename) - if stderr != "" { - t.Fatalf("%s: %q", filename, stderr) - } - want := string(r.data.Golden(t, "semantic", filename, func() ([]byte, error) { - return []byte(got), nil - })) - if want != got { - lwant := strings.Split(want, "\n") - lgot := strings.Split(got, "\n") - t.Errorf("want(%d-%d) != got(%d-%d) for %s", len(want), len(lwant), len(got), len(lgot), r.Normalize(filename)) - for i := 0; i < len(lwant) && i < len(lgot); i++ { - if lwant[i] != lgot[i] { - // This is the line number in the golden file. - // It is one larger than the line number in the source file. - t.Errorf("line %d:\nwant%q\ngot %q\n", i+2, lwant[i], lgot[i]) - } - } - } -} diff --git a/gopls/internal/lsp/cmd/test/signature.go b/gopls/internal/lsp/cmd/test/signature.go deleted file mode 100644 index 40669e8d223..00000000000 --- a/gopls/internal/lsp/cmd/test/signature.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "fmt" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) { - uri := spn.URI() - filename := uri.Filename() - target := filename + fmt.Sprintf(":%v:%v", spn.Start().Line(), spn.Start().Column()) - got, _ := r.NormalizeGoplsCmd(t, "signature", target) - if want == nil { - if got != "" { - t.Fatalf("want nil, but got %s", got) - } - return - } - goldenTag := want.Signatures[0].Label + "-signature" - expect := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { - return []byte(got), nil - })) - if tests.NormalizeAny(expect) != tests.NormalizeAny(got) { - t.Errorf("signature failed for %s expected:\n%q\ngot:\n%q'", filename, expect, got) - } -} diff --git a/gopls/internal/lsp/cmd/test/suggested_fix.go b/gopls/internal/lsp/cmd/test/suggested_fix.go deleted file mode 100644 index 1e61fe9bcd5..00000000000 --- a/gopls/internal/lsp/cmd/test/suggested_fix.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "fmt" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) SuggestedFix(t *testing.T, spn span.Span, suggestedFixes []tests.SuggestedFix, expectedActions int) { - uri := spn.URI() - filename := uri.Filename() - args := []string{"fix", "-a", fmt.Sprintf("%s", spn)} - var actionKinds []string - for _, sf := range suggestedFixes { - if sf.ActionKind == "refactor.rewrite" { - t.Skip("refactor.rewrite is not yet supported on the command line") - } - actionKinds = append(actionKinds, sf.ActionKind) - } - args = append(args, actionKinds...) - got, stderr := r.NormalizeGoplsCmd(t, args...) - if stderr == "ExecuteCommand is not yet supported on the command line" { - return // don't skip to keep the summary counts correct - } - want := string(r.data.Golden(t, "suggestedfix_"+tests.SpanName(spn), filename, func() ([]byte, error) { - return []byte(got), nil - })) - if want != got { - t.Errorf("suggested fixes failed for %s:\n%s", filename, compare.Text(want, got)) - } -} diff --git a/gopls/internal/lsp/cmd/test/symbols.go b/gopls/internal/lsp/cmd/test/symbols.go deleted file mode 100644 index aaf3725d9c0..00000000000 --- a/gopls/internal/lsp/cmd/test/symbols.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "testing" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { - filename := uri.Filename() - got, _ := r.NormalizeGoplsCmd(t, "symbols", filename) - expect := string(r.data.Golden(t, "symbols", filename, func() ([]byte, error) { - return []byte(got), nil - })) - if diff := compare.Text(expect, got); diff != "" { - t.Errorf("symbols differ from expected:\n%s", diff) - } -} diff --git a/gopls/internal/lsp/cmd/test/workspace_symbol.go b/gopls/internal/lsp/cmd/test/workspace_symbol.go deleted file mode 100644 index 40c2c65d019..00000000000 --- a/gopls/internal/lsp/cmd/test/workspace_symbol.go +++ /dev/null @@ -1,54 +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 cmdtest - -import ( - "fmt" - "path/filepath" - "sort" - "strings" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - var matcher string - switch typ { - case tests.WorkspaceSymbolsFuzzy: - matcher = "fuzzy" - case tests.WorkspaceSymbolsCaseSensitive: - matcher = "caseSensitive" - case tests.WorkspaceSymbolsDefault: - matcher = "caseInsensitive" - } - r.runWorkspaceSymbols(t, uri, matcher, query) -} - -func (r *runner) runWorkspaceSymbols(t *testing.T, uri span.URI, matcher, query string) { - t.Helper() - - out, _ := r.runGoplsCmd(t, "workspace_symbol", "-matcher", matcher, query) - var filtered []string - dir := filepath.Dir(uri.Filename()) - for _, line := range strings.Split(out, "\n") { - if source.InDir(dir, line) { - filtered = append(filtered, filepath.ToSlash(line)) - } - } - sort.Strings(filtered) - got := r.Normalize(strings.Join(filtered, "\n") + "\n") - - expect := string(r.data.Golden(t, fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { - return []byte(got), nil - })) - - if expect != got { - t.Errorf("workspace_symbol failed for %s:\n%s", query, compare.Text(expect, got)) - } -} diff --git a/gopls/internal/lsp/testdata/folding/a.go.golden b/gopls/internal/lsp/testdata/folding/a.go.golden index 23befa73be6..b04ca4dab3f 100644 --- a/gopls/internal/lsp/testdata/folding/a.go.golden +++ b/gopls/internal/lsp/testdata/folding/a.go.golden @@ -246,43 +246,6 @@ this string is not indented` } --- foldingRange-cmd -- -3:9-6:1 -10:22-11:33 -12:10-12:10 -12:20-75:1 -14:10-25:2 -15:12-20:4 -16:12-18:3 -17:16-17:22 -18:11-20:3 -19:16-19:23 -21:13-22:23 -22:15-22:22 -23:10-24:25 -24:15-24:24 -26:24-28:12 -30:24-33:33 -34:12-38:2 -39:16-41:2 -42:21-46:2 -47:17-51:2 -52:8-56:2 -57:15-57:24 -57:32-57:41 -58:10-69:2 -59:18-64:4 -60:11-62:3 -61:16-61:29 -62:11-64:3 -63:16-63:30 -65:11-66:19 -66:15-66:18 -67:10-68:25 -68:15-68:24 -70:32-71:31 -72:9-74:17 - -- foldingRange-comment-0 -- package folding //@fold("package") diff --git a/gopls/internal/lsp/testdata/folding/bad.go.golden b/gopls/internal/lsp/testdata/folding/bad.go.golden index 6db6982eb4c..ab274f75ac6 100644 --- a/gopls/internal/lsp/testdata/folding/bad.go.golden +++ b/gopls/internal/lsp/testdata/folding/bad.go.golden @@ -44,16 +44,6 @@ func badBar() string { x := true return } --- foldingRange-cmd -- -3:9-5:1 -7:9-8:9 -11:13-11:13 -11:23-18:1 -12:8-15:2 -14:15-14:21 -15:10-16:24 -16:15-16:22 - -- foldingRange-imports-0 -- package folding //@fold("package") diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 58bb6a02877..eaa2bb9b078 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -143,16 +143,12 @@ type Data struct { mappers map[span.URI]*protocol.Mapper } -// The Tests interface abstracts a set of implementations of marker +// The Tests interface abstracts the LSP-based implementation of the marker // test operators (such as @hover) appearing in files beneath ../testdata/. // -// There are two implementations: -// - *runner in ../cmd/test/check.go, which runs the command-line tool (e.g. "gopls hover") -// - *runner in ../lsp_test.go, which makes LSP requests (textDocument/hover) to a gopls server. -// -// Not all implementations implement all methods. -// // TODO(adonovan): reduce duplication; see https://github.com/golang/go/issues/54845. +// There is only one implementation (*runner in ../lsp_test.go), so +// we can abolish the interface now. type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) CodeLens(*testing.T, span.URI, []protocol.CodeLens) diff --git a/gopls/test/gopls_test.go b/gopls/test/gopls_test.go deleted file mode 100644 index efa998cd1de..00000000000 --- a/gopls/test/gopls_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gopls_test - -import ( - "os" - "testing" - - "golang.org/x/tools/gopls/internal/hooks" - cmdtest "golang.org/x/tools/gopls/internal/lsp/cmd/test" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/testenv" -) - -func TestMain(m *testing.M) { - bug.PanicOnBugs = true - testenv.ExitIfSmallMachine() - - // Set the global exporter to nil so that we don't log to stderr. This avoids - // a lot of misleading noise in test output. - // - // See also ../internal/lsp/lsp_test.go. - event.SetExporter(nil) - - os.Exit(m.Run()) -} - -func TestCommandLine(t *testing.T) { - cmdtest.TestCommandLine(t, "../internal/lsp/testdata", commandLineOptions) -} - -func commandLineOptions(options *source.Options) { - options.Staticcheck = true - options.GoDiff = false // workaround for golang/go#57290 TODO(pjw): fix - tests.DefaultOptions(options) - hooks.Options(options) -} From 37c69d87c2a9de833b7fefbd3e99260a0ba68f81 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 20 Jan 2023 17:57:21 -0500 Subject: [PATCH 673/723] gopls/internal/lsp/source: references: support exported methods This change extends referencesV2 to support exported methods, avoiding the need to fall back to the old type-check-the-world implementation. (The latter is not quite dead code yet.) A references query on a method (C).F will compute all methods I.F of interfaces that correspond to it, by doing a lookup in the method set index used by the global "implementations" operation. The found methods are then used to augment the set of targets in the global search. The methodsets index has been extended to save the (PkgPath, objectpath) pair for each method, since these are the needed inputs to the global references query. (The method's package is not necessarily the same as that of the enclosing type's package due to embedding.) The current encoding increases the index size by about 10%. I suspect we could do better by exploiting redundancy between the objectpath and fingerprint, but there's no urgency. Also: - typeDeclPosition no longer returns methodID, since it can be derived later. - ensure that pkg.xrefs is always set by doTypeCheck. Change-Id: I4037dcfc1a2a710f0cb7a2f5268527e77ebfcd79 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463141 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/cache/check.go | 8 +- gopls/internal/lsp/source/implementation2.go | 35 ++- .../lsp/source/methodsets/methodsets.go | 147 +++++++---- gopls/internal/lsp/source/references.go | 4 +- gopls/internal/lsp/source/references2.go | 235 +++++++++++------- gopls/internal/lsp/source/xrefs/xrefs.go | 1 + .../internal/regtest/misc/references_test.go | 21 +- 7 files changed, 280 insertions(+), 171 deletions(-) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index d7b06851a2a..722691d41d3 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -432,9 +432,6 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF } pkg.diagnostics = append(pkg.diagnostics, depsErrors...) - // Build index of outbound cross-references. - pkg.xrefs = xrefs.Index(pkg) - return pkg, nil } @@ -488,8 +485,10 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil if m.PkgPath == "unsafe" { // Don't type check Unsafe: it's unnecessary, and doing so exposes a data // race to Unsafe.completed. + // TODO(adonovan): factor (tail-merge) with the normal control path. pkg.types = types.Unsafe pkg.methodsets = methodsets.NewIndex(pkg.fset, pkg.types) + pkg.xrefs = xrefs.Index(pkg) return pkg, nil } @@ -574,6 +573,9 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil // Build global index of method sets for 'implementations' queries. pkg.methodsets = methodsets.NewIndex(pkg.fset, pkg.types) + // Build global index of outbound cross-references. + pkg.xrefs = xrefs.Index(pkg) + // If the context was cancelled, we may have returned a ton of transient // errors to the type checker. Swallow them. if ctx.Err() != nil { diff --git a/gopls/internal/lsp/source/implementation2.go b/gopls/internal/lsp/source/implementation2.go index 7ca2e087e96..09659ccd622 100644 --- a/gopls/internal/lsp/source/implementation2.go +++ b/gopls/internal/lsp/source/implementation2.go @@ -72,8 +72,8 @@ func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp pro func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.Location, error) { // Type-check the query package, find the query identifier, - // and locate the type/method declaration it refers to. - declPosn, methodID, err := typeDeclPosition(ctx, snapshot, fh.URI(), pp) + // and locate the type or method declaration it refers to. + declPosn, err := typeDeclPosition(ctx, snapshot, fh.URI(), pp) if err != nil { return nil, err } @@ -127,6 +127,7 @@ func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp // Is the selected identifier a type name or method? // (For methods, report the corresponding method names.) var queryType types.Type + var queryMethodID string switch obj := obj.(type) { case *types.TypeName: queryType = obj.Type() @@ -134,6 +135,7 @@ func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp // For methods, use the receiver type, which may be anonymous. if recv := obj.Type().(*types.Signature).Recv(); recv != nil { queryType = recv.Type() + queryMethodID = obj.Id() } default: return nil, fmt.Errorf("%s is not a type or method", id.Name) @@ -184,7 +186,7 @@ func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp for _, localPkg := range localPkgs { localPkg := localPkg group.Go(func() error { - localLocs, err := localImplementations(ctx, snapshot, localPkg, queryType, methodID) + localLocs, err := localImplementations(ctx, snapshot, localPkg, queryType, queryMethodID) if err != nil { return err } @@ -198,8 +200,8 @@ func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp for _, globalPkg := range globalPkgs { globalPkg := globalPkg group.Go(func() error { - for _, loc := range globalPkg.MethodSetsIndex().Search(key, methodID) { - loc := loc + for _, res := range globalPkg.MethodSetsIndex().Search(key, queryMethodID) { + loc := res.Location // Map offsets to protocol.Locations in parallel (may involve I/O). group.Go(func() error { ploc, err := offsetToLocation(ctx, snapshot, loc.Filename, loc.Start, loc.End) @@ -239,18 +241,17 @@ func offsetToLocation(ctx context.Context, snapshot Snapshot, filename string, s } // typeDeclPosition returns the position of the declaration of the -// type referred to at (uri, ppos). If it refers to a method, the -// function returns the method's receiver type and ID. -func typeDeclPosition(ctx context.Context, snapshot Snapshot, uri span.URI, ppos protocol.Position) (token.Position, string, error) { +// type (or one of its methods) referred to at (uri, ppos). +func typeDeclPosition(ctx context.Context, snapshot Snapshot, uri span.URI, ppos protocol.Position) (token.Position, error) { var noPosn token.Position pkg, pgf, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, WidestPackage) if err != nil { - return noPosn, "", err + return noPosn, err } pos, err := pgf.PositionPos(ppos) if err != nil { - return noPosn, "", err + return noPosn, err } // This function inherits the limitation of its predecessor in @@ -265,15 +266,14 @@ func typeDeclPosition(ctx context.Context, snapshot Snapshot, uri span.URI, ppos // TODO(adonovan): simplify: use objectsAt? path := pathEnclosingObjNode(pgf.File, pos) if path == nil { - return noPosn, "", ErrNoIdentFound + return noPosn, ErrNoIdentFound } id, ok := path[0].(*ast.Ident) if !ok { - return noPosn, "", ErrNoIdentFound + return noPosn, ErrNoIdentFound } // Is the object a type or method? Reject other kinds. - var methodID string obj := pkg.GetTypesInfo().Uses[id] if obj == nil { // Check uses first (unlike ObjectOf) so that T in @@ -286,19 +286,18 @@ func typeDeclPosition(ctx context.Context, snapshot Snapshot, uri span.URI, ppos // ok case *types.Func: if obj.Type().(*types.Signature).Recv() == nil { - return noPosn, "", fmt.Errorf("%s is a function, not a method", id.Name) + return noPosn, fmt.Errorf("%s is a function, not a method", id.Name) } - methodID = obj.Id() case nil: - return noPosn, "", fmt.Errorf("%s denotes unknown object", id.Name) + return noPosn, fmt.Errorf("%s denotes unknown object", id.Name) default: // e.g. *types.Var -> "var". kind := strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types.")) - return noPosn, "", fmt.Errorf("%s is a %s, not a type", id.Name, kind) + return noPosn, fmt.Errorf("%s is a %s, not a type", id.Name, kind) } declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) - return declPosn, methodID, nil + return declPosn, nil } // localImplementations searches within pkg for declarations of all diff --git a/gopls/internal/lsp/source/methodsets/methodsets.go b/gopls/internal/lsp/source/methodsets/methodsets.go index 81d5d0070da..2c025557821 100644 --- a/gopls/internal/lsp/source/methodsets/methodsets.go +++ b/gopls/internal/lsp/source/methodsets/methodsets.go @@ -51,6 +51,7 @@ import ( "strconv" "strings" + "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/internal/typeparams" ) @@ -72,13 +73,6 @@ func NewIndex(fset *token.FileSet, pkg *types.Package) *Index { // // Conversion to protocol (UTF-16) form is done by the caller after a // search, not during index construction. -// TODO(adonovan): opt: reconsider this choice, if FileHandles, not -// ParsedGoFiles were to provide ColumnMapper-like functionality. -// (Column mapping is currently associated with parsing, -// but non-parsed and even non-Go files need it too.) -// Since type checking requires reading (but not parsing) all -// dependencies' Go files, we could do the conversion at type-checking -// time at little extra cost in that case. type Location struct { Filename string Start, End int // byte offsets @@ -93,19 +87,30 @@ type Key struct { // KeyOf returns the search key for the method sets of a given type. // It returns false if the type has no methods. func KeyOf(t types.Type) (Key, bool) { - mset := methodSetInfo(func(types.Object) (_ gobPosition) { return }, t, gobPosition{}) + mset := methodSetInfo(t, nil) if mset.Mask == 0 { return Key{}, false // no methods } return Key{mset}, true } +// A Result reports a matching type or method in a method-set search. +type Result struct { + Location Location // location of the type or method + + // methods only: + PkgPath string // path of declaring package (may differ due to embedding) + ObjectPath objectpath.Path // path of method within declaring package +} + // Search reports each type that implements (or is implemented by) the // type that produced the search key. If methodID is nonempty, only // that method of each type is reported. -// The result is the location of each type or method. -func (index *Index) Search(key Key, methodID string) []Location { - var locs []Location +// +// The result does not include the error.Error method. +// TODO(adonovan): give this special case a more systematic treatment. +func (index *Index) Search(key Key, methodID string) []Result { + var results []Result for _, candidate := range index.pkg.MethodSets { // Traditionally this feature doesn't report // interface/interface elements of the relation. @@ -125,19 +130,34 @@ func (index *Index) Search(key Key, methodID string) []Location { } if methodID == "" { - locs = append(locs, index.location(candidate.Posn)) + results = append(results, Result{Location: index.location(candidate.Posn)}) } else { for _, m := range candidate.Methods { // Here we exploit knowledge of the shape of the fingerprint string. if strings.HasPrefix(m.Fingerprint, methodID) && m.Fingerprint[len(methodID)] == '(' { - locs = append(locs, index.location(m.Posn)) + + // Don't report error.Error among the results: + // it has no true source location, no package, + // and is excluded from the xrefs index. + if m.PkgPath == 0 || m.ObjectPath == 0 { + if methodID != "Error" { + panic("missing info for" + methodID) + } + continue + } + + results = append(results, Result{ + Location: index.location(m.Posn), + PkgPath: index.pkg.Strings[m.PkgPath], + ObjectPath: objectpath.Path(index.pkg.Strings[m.ObjectPath]), + }) break } } } } - return locs + return results } // satisfies does a fast check for whether x satisfies y. @@ -161,7 +181,7 @@ outer: func (index *Index) location(posn gobPosition) Location { return Location{ - Filename: index.pkg.Filenames[posn.File], + Filename: index.pkg.Strings[posn.File], Start: posn.Offset, End: posn.Offset + posn.Len, } @@ -170,54 +190,73 @@ func (index *Index) location(posn gobPosition) Location { // An indexBuilder builds an index for a single package. type indexBuilder struct { gobPackage - filenameIndex map[string]int + stringIndex map[string]int } // build adds to the index all package-level named types of the specified package. func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index { + _ = b.string("") // 0 => "" + + objectPos := func(obj types.Object) gobPosition { + posn := safetoken.StartPosition(fset, obj.Pos()) + return gobPosition{b.string(posn.Filename), posn.Offset, len(obj.Name())} + } + + // setindexInfo sets the (Posn, PkgPath, ObjectPath) fields for each method declaration. + setIndexInfo := func(m *gobMethod, method *types.Func) { + // error.Error has empty Position, PkgPath, and ObjectPath. + if method.Pkg() == nil { + return + } + + m.Posn = objectPos(method) + m.PkgPath = b.string(method.Pkg().Path()) + + // Instantiations of generic methods don't have an + // object path, so we use the generic. + if p, err := objectpath.For(typeparams.OriginMethod(method)); err != nil { + panic(err) // can't happen for a method of a package-level type + } else { + m.ObjectPath = b.string(string(p)) + } + } + // We ignore aliases, though in principle they could define a // struct{...} or interface{...} type, or an instantiation of // a generic, that has a novel method set. scope := pkg.Scope() for _, name := range scope.Names() { if tname, ok := scope.Lookup(name).(*types.TypeName); ok && !tname.IsAlias() { - b.add(fset, tname) + if mset := methodSetInfo(tname.Type(), setIndexInfo); mset.Mask != 0 { + mset.Posn = objectPos(tname) + // Only record types with non-trivial method sets. + b.MethodSets = append(b.MethodSets, mset) + } } } return &Index{pkg: b.gobPackage} } -func (b *indexBuilder) add(fset *token.FileSet, tname *types.TypeName) { - objectPos := func(obj types.Object) gobPosition { - posn := safetoken.StartPosition(fset, obj.Pos()) - return gobPosition{b.fileIndex(posn.Filename), posn.Offset, len(obj.Name())} - } - if mset := methodSetInfo(objectPos, tname.Type(), objectPos(tname)); mset.Mask != 0 { - // Only record types with non-trivial method sets. - b.MethodSets = append(b.MethodSets, mset) - } -} - -// fileIndex returns a small integer that encodes the file name. -func (b *indexBuilder) fileIndex(filename string) int { - i, ok := b.filenameIndex[filename] +// string returns a small integer that encodes the string. +func (b *indexBuilder) string(s string) int { + i, ok := b.stringIndex[s] if !ok { - i = len(b.Filenames) - if b.filenameIndex == nil { - b.filenameIndex = make(map[string]int) + i = len(b.Strings) + if b.stringIndex == nil { + b.stringIndex = make(map[string]int) } - b.filenameIndex[filename] = i - b.Filenames = append(b.Filenames, filename) + b.stringIndex[s] = i + b.Strings = append(b.Strings, s) } return i } -// methodSetInfo returns the method-set fingerprint -// of a type and records its position (typePosn) -// and the position of each of its methods m, -// as provided by objectPos(m). -func methodSetInfo(objectPos func(types.Object) gobPosition, t types.Type, typePosn gobPosition) gobMethodSet { +// methodSetInfo returns the method-set fingerprint of a type. +// It calls the optional setIndexInfo function for each gobMethod. +// This is used during index construction, but not search (KeyOf), +// to store extra information. +func methodSetInfo(t types.Type, setIndexInfo func(*gobMethod, *types.Func)) gobMethodSet { // For non-interface types, use *T // (if T is not already a pointer) // since it may have more methods. @@ -234,10 +273,18 @@ func methodSetInfo(objectPos func(types.Object) gobPosition, t types.Type, typeP tricky = true } sum := crc32.ChecksumIEEE([]byte(fp)) - methods[i] = gobMethod{fp, sum, objectPos(m)} + methods[i] = gobMethod{Fingerprint: fp, Sum: sum} + if setIndexInfo != nil { + setIndexInfo(&methods[i], m) // set Position, PkgPath, ObjectPath + } mask |= 1 << uint64(((sum>>24)^(sum>>16)^(sum>>8)^sum)&0x3f) } - return gobMethodSet{typePosn, types.IsInterface(t), tricky, mask, methods} + return gobMethodSet{ + IsInterface: types.IsInterface(t), + Tricky: tricky, + Mask: mask, + Methods: methods, + } } // EnsurePointer wraps T in a types.Pointer if T is a named, non-interface type. @@ -398,7 +445,7 @@ func fingerprint(method *types.Func) (string, bool) { // A gobPackage records the method set of each package-level type for a single package. type gobPackage struct { - Filenames []string // see gobPosition.File + Strings []string // index of strings used by gobPosition.File, gobMethod.{Pkg,Object}Path MethodSets []gobMethodSet } @@ -413,13 +460,17 @@ type gobMethodSet struct { // A gobMethod records the name, type, and position of a single method. type gobMethod struct { - Fingerprint string // string of form "methodID(params...)(results)" - Sum uint32 // checksum of fingerprint - Posn gobPosition // location of method declaration + Fingerprint string // string of form "methodID(params...)(results)" + Sum uint32 // checksum of fingerprint + + // index records only (zero in KeyOf; also for index of error.Error). + Posn gobPosition // location of method declaration + PkgPath int // path of package containing method declaration + ObjectPath int // object path of method relative to PkgPath } // A gobPosition records the file, offset, and length of an identifier. type gobPosition struct { - File int // index into Index.filenames + File int // index into gopPackage.Strings Offset, Len int // in bytes } diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 25d4a99cafe..afe03f1d9b9 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -34,8 +34,8 @@ type ReferenceInfo struct { // referencesV1 returns a list of references for a given identifier within the packages // containing pp. Declarations appear first in the result. // -// Currently used by Server.{incomingCalls,rename}. -// TODO(adonovan): switch each over to referencesV2 in turn. +// Currently called only from Server.incomingCalls. +// TODO(adonovan): switch over to referencesV2. func referencesV1(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) { ctx, done := event.Start(ctx, "source.References") defer done() diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index 538639aed2f..2a40065aaec 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -17,7 +17,6 @@ package source import ( "context" - "errors" "fmt" "go/ast" "go/token" @@ -32,6 +31,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source/methodsets" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" ) @@ -43,27 +43,12 @@ type ReferenceInfoV2 struct { Location protocol.Location } -var ErrFallback = errors.New("fallback") - // References returns a list of all references (sorted with // 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 Snapshot, fh FileHandle, pp protocol.Position, includeDeclaration bool) ([]protocol.Location, error) { references, err := referencesV2(ctx, snapshot, fh, pp, includeDeclaration) if err != nil { - if err == ErrFallback { - // Fall back to old implementation. - // TODO(adonovan): support methods in V2 and eliminate referencesV1. - references, err := referencesV1(ctx, snapshot, fh, pp, includeDeclaration) - if err != nil { - return nil, err - } - var locations []protocol.Location - for _, ref := range references { - locations = append(locations, ref.MappedRange.Location()) - } - return locations, nil - } return nil, err } // TODO(adonovan): eliminate references[i].Name field? @@ -78,9 +63,6 @@ func References(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protoc // referencesV2 returns a list of all references (sorted with // definitions before uses) to the object denoted by the identifier at // the given file/position, searching the entire workspace. -// -// Returns ErrFallback if it can't yet handle the case, indicating -// that we should call the old implementation. func referencesV2(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfoV2, error) { ctx, done := event.Start(ctx, "source.References2") defer done() @@ -252,7 +234,7 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp return nil, ErrNoIdentFound // can't happen } - // nil, error, iota, or other built-in? + // nil, error, error.Error, iota, or other built-in? if obj.Pkg() == nil { // For some reason, existing tests require that iota has no references, // nor an error. TODO(adonovan): do something more principled. @@ -267,10 +249,58 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp // This may include the query pkg, and possibly other variants. declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) declURI := span.URIFromPath(declPosn.Filename) - metas, err := snapshot.MetadataForFile(ctx, declURI) + variants, err := snapshot.MetadataForFile(ctx, declURI) if err != nil { return nil, err } + if len(variants) == 0 { + return nil, fmt.Errorf("no packages for file %q", declURI) // can't happen + } + + // Is object exported? + // If so, compute scope and targets of the global search. + var ( + globalScope = make(map[PackageID]*Metadata) + globalTargets map[PackagePath]map[objectpath.Path]unit + ) + // TODO(adonovan): what about generic functions. Need to consider both + // uninstantiated and instantiated. The latter have no objectpath. Use Origin? + if path, err := objectpath.For(obj); err == nil && obj.Exported() { + pkgPath := variants[0].PkgPath // (all variants have same package path) + globalTargets = map[PackagePath]map[objectpath.Path]unit{ + pkgPath: {path: {}}, // primary target + } + + // How far need we search? + // For package-level objects, we need only search the direct importers. + // For fields and methods, we must search transitively. + transitive := obj.Pkg().Scope().Lookup(obj.Name()) != obj + + // The scope is the union of rdeps of each variant. + // (Each set is disjoint so there's no benefit to + // to combining the metadata graph traversals.) + for _, m := range variants { + rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, transitive) + if err != nil { + return nil, err + } + for id, rdep := range rdeps { + globalScope[id] = rdep + } + } + + // Is object a method? + // + // If so, expand the search so that the targets include + // all methods that correspond to it through interface + // satisfaction, and the scope includes the rdeps of + // the package that declares each corresponding type. + if recv := effectiveReceiver(obj); recv != nil { + if err := expandMethodSearch(ctx, snapshot, obj.(*types.Func), recv, globalScope, globalTargets); err != nil { + return nil, err + } + } + } // The search functions will call report(loc) for each hit. var ( @@ -288,30 +318,6 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp refsMu.Unlock() } - // Is the object exported? - // (objectpath succeeds for lowercase names, arguably a bug.) - var exportedObjectPaths map[objectpath.Path]unit - if path, err := objectpath.For(obj); err == nil && obj.Exported() { - exportedObjectPaths = map[objectpath.Path]unit{path: unit{}} - - // If the object is an exported method, we need to search for - // all matching implementations (using the incremental - // implementation of 'implementations') and then search for - // the set of corresponding methods (requiring the incremental - // implementation of 'references' to be generalized to a set - // of search objects). - // Until then, we simply fall back to the old implementation for now. - // TODO(adonovan): fix. - if fn, ok := obj.(*types.Func); ok && fn.Type().(*types.Signature).Recv() != nil { - return nil, ErrFallback - } - } - - // If it is exported, how far need we search? - // For package-level objects, we need only search the direct importers. - // For fields and methods, we must search transitively. - transitive := obj.Pkg().Scope().Lookup(obj.Name()) != obj - // Loop over the variants of the declaring package, // and perform both the local (in-package) and global // (cross-package) searches, in parallel. @@ -322,54 +328,97 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp // // Careful: this goroutine must not return before group.Wait. var group errgroup.Group - for _, m := range metas { // for each variant - m := m - // local + // Compute local references for each variant. + for _, m := range variants { + // We want the ordinary importable package, + // plus any test-augmented variants, since + // declarations in _test.go files may change + // the reference of a selection, or even a + // field into a method or vice versa. + // + // But we don't need intermediate test variants, + // as their local references will be covered + // already by other variants. + if m.IsIntermediateTestVariant() { + continue + } + m := m group.Go(func() error { - // We want the ordinary importable package, - // plus any test-augmented variants, since - // declarations in _test.go files may change - // the reference of a selection, or even a - // field into a method or vice versa. - // - // But we don't need intermediate test variants, - // as their local references will be covered - // already by other variants. - if m.IsIntermediateTestVariant() { - return nil - } return localReferences(ctx, snapshot, declURI, declPosn.Offset, m, report) }) + } - if exportedObjectPaths == nil { - continue // non-exported - } + // Compute global references for selected reverse dependencies. + for _, m := range globalScope { + m := m + group.Go(func() error { + return globalReferences(ctx, snapshot, m, globalTargets, report) + }) + } - targets := map[PackagePath]map[objectpath.Path]struct{}{m.PkgPath: exportedObjectPaths} + if err := group.Wait(); err != nil { + return nil, err + } + return refs, nil +} - // global +// expandMethodSearch expands the scope and targets of a global search +// for an exported method to include all methods that correspond to +// it through interface satisfaction. +// +// recv is the method's effective receiver type, for method-set computations. +func expandMethodSearch(ctx context.Context, snapshot Snapshot, method *types.Func, recv types.Type, scope map[PackageID]*Metadata, targets map[PackagePath]map[objectpath.Path]unit) error { + // Compute the method-set fingerprint used as a key to the global search. + key, hasMethods := methodsets.KeyOf(recv) + if !hasMethods { + return bug.Errorf("KeyOf(%s)={} yet %s is a method", recv, method) + } + metas, err := snapshot.AllMetadata(ctx) + if err != nil { + return err + } + allIDs := make([]PackageID, 0, len(metas)) + for _, m := range metas { + allIDs = append(allIDs, m.ID) + } + // Search the methodset index of each package in the workspace. + allPkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, allIDs...) + if err != nil { + return err + } + var group errgroup.Group + for _, pkg := range allPkgs { + pkg := pkg group.Go(func() error { - // Compute the global-scope query for every variant - // of the declaring package in parallel, - // as the rdeps of each variant are disjoint. - rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, transitive) - if err != nil { - return err + // Consult index for matching methods. + results := pkg.MethodSetsIndex().Search(key, method.Name()) + + // Expand global search scope to include rdeps of this pkg. + if len(results) > 0 { + rdeps, err := snapshot.ReverseDependencies(ctx, pkg.ID(), true) + if err != nil { + return err + } + for _, rdep := range rdeps { + scope[rdep.ID] = rdep + } } - for _, rdep := range rdeps { - rdep := rdep - group.Go(func() error { - return globalReferences(ctx, snapshot, rdep, targets, report) - }) + + // Add each corresponding method the to set of global search targets. + for _, res := range results { + methodPkg := PackagePath(res.PkgPath) + opaths, ok := targets[methodPkg] + if !ok { + opaths = make(map[objectpath.Path]unit) + targets[methodPkg] = opaths + } + opaths[res.ObjectPath] = unit{} } return nil }) } - if err := group.Wait(); err != nil { - return nil, err - } - return refs, nil + return group.Wait() } // localReferences reports each reference to the object @@ -402,17 +451,6 @@ func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, d report(mustLocation(pgf, node), true) } - // receiver returns the effective receiver type for method-set - // comparisons for obj, if it is a method, or nil otherwise. - receiver := func(obj types.Object) types.Type { - if fn, ok := obj.(*types.Func); ok { - if recv := fn.Type().(*types.Signature).Recv(); recv != nil { - return methodsets.EnsurePointer(recv.Type()) - } - } - return nil - } - // If we're searching for references to a method, broaden the // search to include references to corresponding methods of // mutually assignable receiver types. @@ -420,7 +458,7 @@ func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, d var methodRecvs []types.Type var methodName string // name of an arbitrary target, iff a method for obj := range targets { - if t := receiver(obj); t != nil { + if t := effectiveReceiver(obj); t != nil { methodRecvs = append(methodRecvs, t) methodName = obj.Name() } @@ -432,7 +470,7 @@ func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, d if targets[obj] != nil { return true } else if methodRecvs != nil && obj.Name() == methodName { - if orecv := receiver(obj); orecv != nil { + if orecv := effectiveReceiver(obj); orecv != nil { for _, mrecv := range methodRecvs { if concreteImplementsIntf(orecv, mrecv) { return true @@ -457,6 +495,17 @@ func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, d return nil } +// effectiveReceiver returns the effective receiver type for method-set +// comparisons for obj, if it is a method, or nil otherwise. +func effectiveReceiver(obj types.Object) types.Type { + if fn, ok := obj.(*types.Func); ok { + if recv := fn.Type().(*types.Signature).Recv(); recv != nil { + return methodsets.EnsurePointer(recv.Type()) + } + } + return nil +} + // objectsAt returns the non-empty set of objects denoted (def or use) // by the specified position within a file syntax tree, or an error if // none were found. diff --git a/gopls/internal/lsp/source/xrefs/xrefs.go b/gopls/internal/lsp/source/xrefs/xrefs.go index b0d2207508c..5f781f78814 100644 --- a/gopls/internal/lsp/source/xrefs/xrefs.go +++ b/gopls/internal/lsp/source/xrefs/xrefs.go @@ -176,6 +176,7 @@ func Lookup(m *source.Metadata, data []byte, targets map[source.PackagePath]map[ // TODO(adonovan): opt: choose a more compact encoding. Gzip reduces // the gob output to about one third its size, so clearly there's room // to improve. The gobRef.Range field is the obvious place to begin. +// Even a zero-length slice gob-encodes to ~285 bytes. // A gobPackage records the set of outgoing references from the index // package to symbols defined in a dependency package. diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index f105f4c87c0..8e99a1c30e3 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -50,8 +50,8 @@ func main() { }) } -// This reproduces and tests golang/go#48400. -func TestReferencesPanicOnError(t *testing.T) { +// This is a regression test for golang/go#48400 (a panic). +func TestReferencesOnErrorMethod(t *testing.T) { // Ideally this would actually return the correct answer, // instead of merely failing gracefully. const files = ` @@ -81,12 +81,19 @@ func _() { env.OpenFile("main.go") file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `Error`)) refs, err := env.Editor.References(env.Ctx, file, pos) - if err == nil { - t.Fatalf("expected error for references, instead got %v", refs) + if err != nil { + t.Fatalf("references on (*s).Error failed: %v", err) + } + // TODO(adonovan): this test is crying out for marker support in regtests. + var buf strings.Builder + for _, ref := range refs { + fmt.Fprintf(&buf, "%s %s\n", env.Sandbox.Workdir.URIToPath(ref.URI), ref.Range) } - wantErr := "no position for func (error).Error() string" - if err.Error() != wantErr { - t.Fatalf("expected error with message %s, instead got %s", wantErr, err.Error()) + got := buf.String() + want := "main.go 8:10-8:15\n" + // (*s).Error decl + "main.go 14:7-14:12\n" // s.Error() call + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected references on (*s).Error (-want +got):\n%s", diff) } }) } From 827075753a9d1db28951d9fa52deee63cdf5f857 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 27 Dec 2022 11:03:55 -0500 Subject: [PATCH 674/723] gopls/internal/lsp/source: switch call hierarchy to references v2 This change removes the last use of the old referencesV1 function. (Its 'references' helper function still has one remaining use, from Server.rename.) The IncomingCalls operation re-parses the files referenced by the 'references' operation, rather than requesting a type-checked package. Also, inline toProtocolIncomingCalls into sole caller. Change-Id: I33fbb210d42b7ca1a70cfebc4061275a153c3537 Reviewed-on: https://go-review.googlesource.com/c/tools/+/459515 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/source/call_hierarchy.go | 54 +++++----- gopls/internal/lsp/source/references.go | 114 +------------------- gopls/internal/lsp/source/references2.go | 32 +++--- 3 files changed, 46 insertions(+), 154 deletions(-) diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index efe5f9a8281..cefc5395514 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" ) @@ -65,13 +64,7 @@ func IncomingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr ctx, done := event.Start(ctx, "source.IncomingCalls") defer done() - // TODO(adonovan): switch to referencesV2 here once it supports methods. - // This will require that we parse files containing - // references instead of accessing refs[i].pkg. - // (We could use pre-parser trimming, either a scanner-based - // implementation such as https://go.dev/play/p/KUrObH1YkX8 - // (~31% speedup), or a byte-oriented implementation (2x speedup). - refs, err := referencesV1(ctx, snapshot, fh, pos, false) + refs, err := referencesV2(ctx, snapshot, fh, pos, false) if err != nil { if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) { return nil, nil @@ -79,23 +72,14 @@ func IncomingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr return nil, err } - return toProtocolIncomingCalls(ctx, snapshot, refs) -} - -// toProtocolIncomingCalls returns an array of protocol.CallHierarchyIncomingCall for ReferenceInfo's. -// References inside same enclosure are assigned to the same enclosing function. -func toProtocolIncomingCalls(ctx context.Context, snapshot Snapshot, refs []*ReferenceInfo) ([]protocol.CallHierarchyIncomingCall, error) { - // an enclosing node could have multiple calls to a reference, we only show the enclosure - // once in the result but highlight all calls using FromRanges (ranges at which the calls occur) - var incomingCalls = map[protocol.Location]*protocol.CallHierarchyIncomingCall{} + // Group references by their enclosing function declaration. + incomingCalls := make(map[protocol.Location]*protocol.CallHierarchyIncomingCall) for _, ref := range refs { - refRange := ref.MappedRange.Range() - callItem, err := enclosingNodeCallItem(snapshot, ref.pkg, ref.MappedRange.URI(), ref.ident.NamePos) + callItem, err := enclosingNodeCallItem(ctx, snapshot, ref.PkgPath, ref.Location) if err != nil { event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name)) continue } - loc := protocol.Location{ URI: callItem.URI, Range: callItem.Range, @@ -105,9 +89,10 @@ func toProtocolIncomingCalls(ctx context.Context, snapshot Snapshot, refs []*Ref call = &protocol.CallHierarchyIncomingCall{From: callItem} incomingCalls[loc] = call } - call.FromRanges = append(call.FromRanges, refRange) + call.FromRanges = append(call.FromRanges, ref.Location.Range) } + // Flatten the map of pointers into a slice of values. incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls)) for _, callItem := range incomingCalls { incomingCallItems = append(incomingCallItems, *callItem) @@ -115,18 +100,31 @@ func toProtocolIncomingCalls(ctx context.Context, snapshot Snapshot, refs []*Ref return incomingCallItems, nil } -// enclosingNodeCallItem creates a CallHierarchyItem representing the function call at pos -func enclosingNodeCallItem(snapshot Snapshot, pkg Package, uri span.URI, pos token.Pos) (protocol.CallHierarchyItem, error) { - pgf, err := pkg.File(uri) +// enclosingNodeCallItem creates a CallHierarchyItem representing the function call at loc. +func enclosingNodeCallItem(ctx context.Context, snapshot Snapshot, pkgPath PackagePath, loc protocol.Location) (protocol.CallHierarchyItem, error) { + // Parse the file containing the reference. + fh, err := snapshot.GetFile(ctx, loc.URI.SpanURI()) + if err != nil { + return protocol.CallHierarchyItem{}, err + } + // TODO(adonovan): opt: before parsing, trim the bodies of functions + // that don't contain the reference, using either a scanner-based + // implementation such as https://go.dev/play/p/KUrObH1YkX8 + // (~31% speedup), or a byte-oriented implementation (2x speedup). + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + if err != nil { + return protocol.CallHierarchyItem{}, err + } + srcRng, err := pgf.RangeToTokenRange(loc.Range) if err != nil { return protocol.CallHierarchyItem{}, err } + // Find the enclosing function, if any, and the number of func literals in between. var funcDecl *ast.FuncDecl var funcLit *ast.FuncLit // innermost function literal var litCount int - // Find the enclosing function, if any, and the number of func literals in between. - path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) + path, _ := astutil.PathEnclosingInterval(pgf.File, srcRng.Start, srcRng.End) outer: for _, node := range path { switch n := node.(type) { @@ -168,8 +166,8 @@ outer: Name: name, Kind: kind, Tags: []protocol.SymbolTag{}, - Detail: fmt.Sprintf("%s • %s", pkg.PkgPath(), filepath.Base(uri.Filename())), - URI: protocol.DocumentURI(uri), + Detail: fmt.Sprintf("%s • %s", pkgPath, filepath.Base(fh.URI().Filename())), + URI: loc.URI, Range: rng, SelectionRange: rng, }, nil diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index afe03f1d9b9..f14d1d601f6 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -11,14 +11,11 @@ import ( "go/ast" "go/token" "go/types" - "sort" - "strings" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/event" ) // ReferenceInfo holds information about reference to an identifier in Go source. @@ -31,114 +28,6 @@ type ReferenceInfo struct { isDeclaration bool } -// referencesV1 returns a list of references for a given identifier within the packages -// containing pp. Declarations appear first in the result. -// -// Currently called only from Server.incomingCalls. -// TODO(adonovan): switch over to referencesV2. -func referencesV1(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) { - ctx, done := event.Start(ctx, "source.References") - defer done() - - // Is the cursor within the package name declaration? - pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) - if err != nil { - return nil, err - } - if inPackageName { - // TODO(rfindley): this is redundant with package renaming. Refactor to share logic. - metas, err := snapshot.MetadataForFile(ctx, f.URI()) - if err != nil { - return nil, err - } - if len(metas) == 0 { - return nil, fmt.Errorf("found no package containing %s", f.URI()) - } - targetPkg := metas[len(metas)-1] // widest package - - // Find external direct references to the package (imports). - rdeps, err := snapshot.ReverseDependencies(ctx, targetPkg.ID, false) - if err != nil { - return nil, err - } - - var refs []*ReferenceInfo - for _, rdep := range rdeps { - for _, uri := range rdep.CompiledGoFiles { - fh, err := snapshot.GetFile(ctx, uri) - if err != nil { - return nil, err - } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) - if err != nil { - return nil, err - } - for _, imp := range f.File.Imports { - if rdep.DepsByImpPath[UnquoteImportPath(imp)] == targetPkg.ID { - rng, err := f.PosMappedRange(imp.Pos(), imp.End()) - if err != nil { - return nil, err - } - refs = append(refs, &ReferenceInfo{ - Name: pgf.File.Name.Name, - MappedRange: rng, - }) - } - } - } - } - - // Find the package declaration of each file in the target package itself. - for _, uri := range targetPkg.CompiledGoFiles { - fh, err := snapshot.GetFile(ctx, uri) - if err != nil { - return nil, err - } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) - if err != nil { - return nil, err - } - rng, err := f.PosMappedRange(f.File.Name.Pos(), f.File.Name.End()) - if err != nil { - return nil, err - } - refs = append(refs, &ReferenceInfo{ - Name: pgf.File.Name.Name, - MappedRange: rng, - }) - } - - return refs, nil - } - - qualifiedObjs, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp) - // Don't return references for builtin types. - if errors.Is(err, errBuiltin) { - return nil, nil - } - if err != nil { - return nil, err - } - - refs, err := references(ctx, snapshot, qualifiedObjs, includeDeclaration, true, false) - if err != nil { - return nil, err - } - - toSort := refs - if includeDeclaration { - toSort = refs[1:] - } - sort.Slice(toSort, func(i, j int) bool { - x, y := toSort[i], toSort[j] - if cmp := strings.Compare(string(x.MappedRange.URI()), string(y.MappedRange.URI())); cmp != 0 { - return cmp < 0 - } - return x.ident.Pos() < y.ident.Pos() - }) - return refs, nil -} - // parsePackageNameDecl is a convenience function that parses and // returns the package name declaration of file fh, and reports // whether the position ppos lies within it. @@ -159,8 +48,7 @@ func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, // Only the definition-related fields of qualifiedObject are used. // (Arguably it should accept a smaller data type.) // -// This implementation serves referencesV1 (the soon-to-be obsolete -// portion of Server.references) and Server.rename. +// This implementation serves Server.rename. TODO(adonovan): obviate it. func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, includeDeclaration, includeInterfaceRefs, includeEmbeddedRefs bool) ([]*ReferenceInfo, error) { var ( references []*ReferenceInfo diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index 2a40065aaec..4b6ba4cd757 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -39,8 +39,13 @@ import ( // object as the subject of a References query. type ReferenceInfoV2 struct { IsDeclaration bool - Name string // TODO(adonovan): same for all elements; factor out of the slice? Location protocol.Location + + // TODO(adonovan): these are the same for all elements; factor out of the slice. + // TODO(adonovan): Name is currently unused. If it's still unused when we + // eliminate 'references' (v1), delete it. Or replace both fields by a *Metadata. + PkgPath PackagePath + Name string } // References returns a list of all references (sorted with @@ -51,11 +56,9 @@ func References(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protoc if err != nil { return nil, err } - // TODO(adonovan): eliminate references[i].Name field? - // But it may be needed when replacing referencesV1. - var locations []protocol.Location - for _, ref := range references { - locations = append(locations, ref.Location) + locations := make([]protocol.Location, len(references)) + for i, ref := range references { + locations[i] = ref.Location } return locations, nil } @@ -68,14 +71,14 @@ func referencesV2(ctx context.Context, snapshot Snapshot, f FileHandle, pp proto defer done() // Is the cursor within the package name declaration? - pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) + _, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) if err != nil { return nil, err } var refs []*ReferenceInfoV2 if inPackageName { - refs, err = packageReferences(ctx, snapshot, f.URI(), pgf.File.Name.Name) + refs, err = packageReferences(ctx, snapshot, f.URI()) } else { refs, err = ordinaryReferences(ctx, snapshot, f.URI(), pp) } @@ -110,7 +113,7 @@ func referencesV2(ctx context.Context, snapshot Snapshot, f FileHandle, pp proto // declaration of the specified name and uri by searching among the // import declarations of all packages that directly import the target // package. -func packageReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pkgname string) ([]*ReferenceInfoV2, error) { +func packageReferences(ctx context.Context, snapshot Snapshot, uri span.URI) ([]*ReferenceInfoV2, error) { metas, err := snapshot.MetadataForFile(ctx, uri) if err != nil { return nil, err @@ -135,7 +138,7 @@ func packageReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pkg if narrowest.ForTest != "" && strings.HasSuffix(string(uri), "_test.go") { for _, f := range narrowest.CompiledGoFiles { if !strings.HasSuffix(string(f), "_test.go") { - return packageReferences(ctx, snapshot, f, pkgname) + return packageReferences(ctx, snapshot, f) } } // This package has no non-test files. @@ -160,8 +163,9 @@ func packageReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pkg if rdep.DepsByImpPath[UnquoteImportPath(imp)] == narrowest.ID { refs = append(refs, &ReferenceInfoV2{ IsDeclaration: false, - Name: pkgname, Location: mustLocation(f, imp), + PkgPath: narrowest.PkgPath, + Name: string(narrowest.Name), }) } } @@ -187,8 +191,9 @@ func packageReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pkg } refs = append(refs, &ReferenceInfoV2{ IsDeclaration: true, // (one of many) - Name: pkgname, Location: mustLocation(f, f.File.Name), + PkgPath: widest.PkgPath, + Name: string(widest.Name), }) } @@ -310,8 +315,9 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp report := func(loc protocol.Location, isDecl bool) { ref := &ReferenceInfoV2{ IsDeclaration: isDecl, - Name: obj.Name(), Location: loc, + PkgPath: pkg.PkgPath(), + Name: obj.Name(), } refsMu.Lock() refs = append(refs, ref) From f10e7d56a323823a26e612712857b47fab7ec65a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 12 Jan 2023 13:51:49 -0500 Subject: [PATCH 675/723] gopls/internal/lsp/cache: remove package dependence on packages.Config The cache.pkg type was a mix of metadata-related information and type checking information, resulting in unnecessary relationships between type-checking results (which are shared) and loading results (which are not shared). As a result, the experimentalPackageCacheKey was more or less a hope that these relationships were valid. Avoid this relationship altogether by separating the shared type-checking result from other derived calculations. This makes the experimentalPackageCacheKey obsolete and lays the groundwork for type-checking from export data. Additionally: - revisit the package cache key to ensure it covers all inputs into type-checking, and make it more similar to the analysis key - remove methods from the source.Package API that return source.Package: we can't have edges between packages if they are going to be standalone - remove the experimentalPackageCacheKey setting - add a test for go list errors - use the proper types.Sizes when type-checking - address a comment from an earlier CL in completion_test.go Fixes golang/go#57853 Change-Id: I238913c7c8305cb534db77ebec5f062e96ed2503 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461944 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/doc/settings.md | 14 - gopls/internal/lsp/cache/analysis.go | 11 +- gopls/internal/lsp/cache/cache.go | 2 +- gopls/internal/lsp/cache/check.go | 293 +++++++++--------- gopls/internal/lsp/cache/errors.go | 74 ++--- .../cache/{error_test.go => errors_test.go} | 0 gopls/internal/lsp/cache/load.go | 14 +- gopls/internal/lsp/cache/pkg.go | 205 +++++++----- gopls/internal/lsp/cache/snapshot.go | 88 ++++-- gopls/internal/lsp/code_action.go | 8 +- gopls/internal/lsp/diagnostics.go | 9 +- gopls/internal/lsp/mod/diagnostics.go | 6 +- gopls/internal/lsp/semantic.go | 50 +-- gopls/internal/lsp/source/api_json.go | 8 - .../lsp/source/completion/completion.go | 30 +- .../internal/lsp/source/completion/format.go | 4 +- .../internal/lsp/source/completion/literal.go | 10 +- .../lsp/source/completion/statements.go | 31 +- gopls/internal/lsp/source/diagnostics.go | 19 +- gopls/internal/lsp/source/highlight.go | 2 +- gopls/internal/lsp/source/hover.go | 22 +- gopls/internal/lsp/source/identifier.go | 18 +- gopls/internal/lsp/source/implementation.go | 26 +- gopls/internal/lsp/source/implementation2.go | 2 +- gopls/internal/lsp/source/options.go | 22 +- gopls/internal/lsp/source/references.go | 6 +- gopls/internal/lsp/source/references2.go | 4 +- gopls/internal/lsp/source/rename.go | 12 +- gopls/internal/lsp/source/rename_check.go | 13 +- gopls/internal/lsp/source/signature_help.go | 2 +- gopls/internal/lsp/source/types_format.go | 8 +- gopls/internal/lsp/source/util.go | 87 ++++-- gopls/internal/lsp/source/view.go | 24 +- gopls/internal/lsp/source/xrefs/xrefs.go | 12 +- .../internal/regtest/bench/completion_test.go | 13 +- .../regtest/diagnostics/diagnostics_test.go | 1 + .../regtest/diagnostics/golist_test.go | 68 ++++ gopls/internal/regtest/misc/hover_test.go | 4 + 38 files changed, 711 insertions(+), 511 deletions(-) rename gopls/internal/lsp/cache/{error_test.go => errors_test.go} (100%) create mode 100644 gopls/internal/regtest/diagnostics/golist_test.go diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 1b8f6d779a0..52a75391048 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -116,20 +116,6 @@ a go.mod file, narrowing the scope to that directory if it exists. Default: `true`. -#### **experimentalPackageCacheKey** *bool* - -**This setting is experimental and may be deleted.** - -experimentalPackageCacheKey controls whether to use a coarser cache key -for package type information to increase cache hits. This setting removes -the user's environment, build flags, and working directory from the cache -key, which should be a safe change as all relevant inputs into the type -checking pass are already hashed into the key. This is temporarily guarded -by an experiment because caching behavior is subtle and difficult to -comprehensively test. - -Default: `true`. - #### **allowModfileModifications** *bool* **This setting is experimental and may be deleted.** diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index e9f1c7f7ce9..0005b0e44f1 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -520,10 +520,8 @@ func analysisCacheKey(analyzers []*analysis.Analyzer, m *source.Metadata, compil sz := m.TypesSizes.(*types.StdSizes) fmt.Fprintf(hasher, "sizes: %d %d\n", sz.WordSize, sz.MaxAlign) - // metadata errors - for _, err := range m.Errors { - fmt.Fprintf(hasher, "error: %q", err) - } + // metadata errors: used for 'compiles' field + fmt.Fprintf(hasher, "errors: %d", len(m.Errors)) // module Go version if m.Module != nil && m.Module.GoVersion != "" { @@ -542,8 +540,8 @@ func analysisCacheKey(analyzers []*analysis.Analyzer, m *source.Metadata, compil depIDs = append(depIDs, string(depID)) } sort.Strings(depIDs) - for _, id := range depIDs { - vdep := vdeps[PackageID(id)] + for _, depID := range depIDs { + vdep := vdeps[PackageID(depID)] fmt.Fprintf(hasher, "dep: %s\n", vdep.PkgPath) fmt.Fprintf(hasher, "export: %s\n", vdep.DeepExportHash) @@ -766,6 +764,7 @@ func typeCheckForAnalysis(fset *token.FileSet, parsed []*source.ParsedGoFile, m } cfg := &types.Config{ + Sizes: m.TypesSizes, Error: func(e error) { pkg.compiles = false // type error pkg.typeErrors = append(pkg.typeErrors, e.(types.Error)) diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go index ef56b12efe5..9da185ccc57 100644 --- a/gopls/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -209,7 +209,7 @@ func (c *Cache) PackageStats(withNames bool) template.HTML { typsCost := typesCost(v.pkg.types.Scope()) typInfoCost := typesInfoCost(v.pkg.typesInfo) stat := packageStat{ - id: v.pkg.m.ID, + id: v.pkg.id, mode: v.pkg.mode, types: typsCost, typesInfo: typInfoCost, diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 722691d41d3..03691e50e8a 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -5,21 +5,21 @@ package cache import ( - "bytes" "context" + "crypto/sha256" "errors" "fmt" "go/ast" "go/types" "path/filepath" "regexp" + "sort" "strings" "sync" "golang.org/x/mod/module" "golang.org/x/sync/errgroup" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/methodsets" @@ -42,12 +42,14 @@ type packageKey struct { type packageHandleKey source.Hash -// A packageHandle is a handle to the future result of type-checking a package. -// The resulting package is obtained from the await() method. +// A packageHandle holds package information, some of which may not be fully +// evaluated. +// +// The only methods on packageHandle that are safe to call before calling await +// are Metadata and await itself. type packageHandle struct { - promise *memoize.Promise // [typeCheckResult] - - // m is the metadata associated with the package. + // TODO(rfindley): remove metadata from packageHandle. It is only used for + // bug detection. m *source.Metadata // key is the hashed key for the package. @@ -58,12 +60,29 @@ type packageHandle struct { // enough. (The key for analysis actions could similarly // hash only Facts of direct dependencies.) key packageHandleKey + + // The shared type-checking promise. + promise *memoize.Promise // [typeCheckResult] +} + +// typeCheckInputs contains the inputs of a call to typeCheckImpl, which +// type-checks a package. +type typeCheckInputs struct { + id PackageID + pkgPath PackagePath + name PackageName + mode source.ParseMode + goFiles, compiledGoFiles []source.FileHandle + sizes types.Sizes + deps map[PackageID]*packageHandle + depsByImpPath map[ImportPath]PackageID + goVersion string // packages.Module.GoVersion, e.g. "1.18" } // typeCheckResult contains the result of a call to // typeCheckImpl, which type-checks a package. type typeCheckResult struct { - pkg *pkg + pkg *syntaxPackage err error } @@ -101,7 +120,6 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // for each package is computed by at most one thread, then do // the recursive key building of dependencies in parallel. deps := make(map[PackageID]*packageHandle) - var depKey source.Hash // XOR of all unique deps for _, depID := range m.DepsByPkgPath { depHandle, err := s.buildPackageHandle(ctx, depID, s.workspaceParseMode(depID)) // Don't use invalid metadata for dependencies if the top-level @@ -121,9 +139,6 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // checking the entire package. Leave depKeys[i] unset. continue } - - depKey.XORWith(source.Hash(depHandle.key)) - deps[depID] = depHandle } @@ -139,12 +154,29 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so return nil, err } + goVersion := "" + if m.Module != nil && m.Module.GoVersion != "" { + goVersion = m.Module.GoVersion + } + + inputs := typeCheckInputs{ + id: m.ID, + pkgPath: m.PkgPath, + name: m.Name, + mode: mode, + goFiles: goFiles, + compiledGoFiles: compiledGoFiles, + sizes: m.TypesSizes, + deps: deps, + depsByImpPath: m.DepsByImpPath, + goVersion: goVersion, + } + // All the file reading has now been done. // Create a handle for the result of type checking. - experimentalKey := s.View().Options().ExperimentalPackageCacheKey - phKey := computePackageKey(m.ID, compiledGoFiles, m, depKey, mode, experimentalKey) + phKey := computePackageKey(s, inputs) promise, release := s.store.Promise(phKey, func(ctx context.Context, arg interface{}) interface{} { - pkg, err := typeCheckImpl(ctx, arg.(*snapshot), goFiles, compiledGoFiles, m, mode, deps) + pkg, err := typeCheckImpl(ctx, arg.(*snapshot), inputs) return typeCheckResult{pkg, err} }) @@ -165,10 +197,11 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so // handles for type-checking its immediate deps, at which // point there will be no need to even access s.meta.) if s.meta.metadata[ph.m.ID] != ph.m { + // TODO(rfindley): this should be bug.Errorf. return nil, fmt.Errorf("stale metadata for %s", ph.m.ID) } - // Check cache again in case another thread got there first. + // Check cache again in case another goroutine got there first. if prev, ok := s.packages.Get(packageKey); ok { prevPH := prev.(*packageHandle) release() @@ -223,61 +256,64 @@ func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { // computePackageKey returns a key representing the act of type checking // a package named id containing the specified files, metadata, and // combined dependency hash. -func computePackageKey(id PackageID, files []source.FileHandle, m *source.Metadata, depsKey source.Hash, mode source.ParseMode, experimentalKey bool) packageHandleKey { - // TODO(adonovan): opt: no need to materalize the bytes; hash them directly. - // Also, use field separators to avoid spurious collisions. - b := bytes.NewBuffer(nil) - b.WriteString(string(id)) - if m.Module != nil { - b.WriteString(m.Module.GoVersion) // go version affects type check errors. - } - if !experimentalKey { - // cfg was used to produce the other hashed inputs (package ID, parsed Go - // files, and deps). It should not otherwise affect the inputs to the type - // checker, so this experiment omits it. This should increase cache hits on - // the daemon as cfg contains the environment and working directory. - hc := hashConfig(m.Config) - b.Write(hc[:]) - } - b.WriteByte(byte(mode)) - b.Write(depsKey[:]) - for _, file := range files { - b.WriteString(file.FileIdentity().String()) - } - // Metadata errors are interpreted and memoized on the computed package, so - // we must hash them into the key here. - // - // TODO(rfindley): handle metadata diagnostics independently from - // type-checking diagnostics. - for _, err := range m.Errors { - b.WriteString(err.Msg) - b.WriteString(err.Pos) - b.WriteRune(rune(err.Kind)) - } - return packageHandleKey(source.HashOf(b.Bytes())) -} +func computePackageKey(s *snapshot, inputs typeCheckInputs) packageHandleKey { + hasher := sha256.New() + + // In principle, a key must be the hash of an + // unambiguous encoding of all the relevant data. + // If it's ambiguous, we risk collisons. + + // package identifiers + fmt.Fprintf(hasher, "package: %s %s %s\n", inputs.id, inputs.name, inputs.pkgPath) + + // module Go version + fmt.Fprintf(hasher, "go %s\n", inputs.goVersion) + + // parse mode + fmt.Fprintf(hasher, "mode %d\n", inputs.mode) -// hashConfig returns the hash for the *packages.Config. -func hashConfig(config *packages.Config) source.Hash { - // TODO(adonovan): opt: don't materialize the bytes; hash them directly. - // Also, use sound field separators to avoid collisions. - var b bytes.Buffer + // import map + importPaths := make([]string, 0, len(inputs.depsByImpPath)) + for impPath := range inputs.depsByImpPath { + importPaths = append(importPaths, string(impPath)) + } + sort.Strings(importPaths) + for _, impPath := range importPaths { + fmt.Fprintf(hasher, "import %s %s", impPath, string(inputs.depsByImpPath[ImportPath(impPath)])) + } - // Dir, Mode, Env, BuildFlags are the parts of the config that can change. - b.WriteString(config.Dir) - b.WriteRune(rune(config.Mode)) + // deps, in PackageID order + depIDs := make([]string, 0, len(inputs.deps)) + for depID := range inputs.deps { + depIDs = append(depIDs, string(depID)) + } + sort.Strings(depIDs) + for _, depID := range depIDs { + dep := inputs.deps[PackageID(depID)] + fmt.Fprintf(hasher, "dep: %s key:%s\n", dep.m.PkgPath, dep.key) + } - for _, e := range config.Env { - b.WriteString(e) + // file names and contents + fmt.Fprintf(hasher, "compiledGoFiles: %d\n", len(inputs.compiledGoFiles)) + for _, fh := range inputs.compiledGoFiles { + fmt.Fprintln(hasher, fh.FileIdentity()) } - for _, f := range config.BuildFlags { - b.WriteString(f) + fmt.Fprintf(hasher, "goFiles: %d\n", len(inputs.goFiles)) + for _, fh := range inputs.goFiles { + fmt.Fprintln(hasher, fh.FileIdentity()) } - return source.HashOf(b.Bytes()) + + // types sizes + sz := inputs.sizes.(*types.StdSizes) + fmt.Fprintf(hasher, "sizes: %d %d\n", sz.WordSize, sz.MaxAlign) + + var hash [sha256.Size]byte + hasher.Sum(hash[:0]) + return packageHandleKey(hash) } // await waits for typeCheckImpl to complete and returns its result. -func (ph *packageHandle) await(ctx context.Context, s *snapshot) (*pkg, error) { +func (ph *packageHandle) await(ctx context.Context, s *snapshot) (*syntaxPackage, error) { v, err := s.awaitPromise(ctx, ph.promise) if err != nil { return nil, err @@ -286,15 +322,7 @@ func (ph *packageHandle) await(ctx context.Context, s *snapshot) (*pkg, error) { return data.pkg, data.err } -func (ph *packageHandle) CompiledGoFiles() []span.URI { - return ph.m.CompiledGoFiles -} - -func (ph *packageHandle) ID() string { - return string(ph.m.ID) -} - -func (ph *packageHandle) cached() (*pkg, error) { +func (ph *packageHandle) cached() (*syntaxPackage, error) { v := ph.promise.Cached() if v == nil { return nil, fmt.Errorf("no cached type information for %s", ph.m.PkgPath) @@ -306,13 +334,13 @@ func (ph *packageHandle) cached() (*pkg, error) { // typeCheckImpl type checks the parsed source files in compiledGoFiles. // (The resulting pkg also holds the parsed but not type-checked goFiles.) // deps holds the future results of type-checking the direct dependencies. -func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *source.Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle) (*pkg, error) { +func typeCheckImpl(ctx context.Context, snapshot *snapshot, inputs typeCheckInputs) (*syntaxPackage, error) { // Start type checking of direct dependencies, // in parallel and asynchronously. // As the type checker imports each of these // packages, it will wait for its completion. var wg sync.WaitGroup - for _, dep := range deps { + for _, dep := range inputs.deps { wg.Add(1) go func(dep *packageHandle) { dep.await(ctx, snapshot) // ignore result @@ -328,56 +356,39 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF defer wg.Wait() var filter *unexportedFilter - if mode == source.ParseExported { + if inputs.mode == source.ParseExported { filter = &unexportedFilter{uses: map[string]bool{}} } - pkg, err := doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, filter) + pkg, err := doTypeCheck(ctx, snapshot, inputs, filter) if err != nil { return nil, err } - if mode == source.ParseExported { + if inputs.mode == source.ParseExported { // The AST filtering is a little buggy and may remove things it // shouldn't. If we only got undeclared name errors, try one more // time keeping those names. missing, unexpected := filter.ProcessErrors(pkg.typeErrors) if len(unexpected) == 0 && len(missing) != 0 { - pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, filter) + pkg, err = doTypeCheck(ctx, snapshot, inputs, filter) if err != nil { return nil, err } missing, unexpected = filter.ProcessErrors(pkg.typeErrors) } if len(unexpected) != 0 || len(missing) != 0 { - pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, nil) + pkg, err = doTypeCheck(ctx, snapshot, inputs, nil) if err != nil { return nil, err } } } - // If this is a replaced module in the workspace, the version is - // meaningless, and we don't want clients to access it. - if m.Module != nil { - pkg.version = &module.Version{ - Path: m.Module.Path, - Version: m.Module.Version, - } - } // We don't care about a package's errors unless we have parsed it in full. - if mode != source.ParseFull { + if inputs.mode != source.ParseFull { return pkg, nil } - for _, e := range m.Errors { - diags, err := goPackagesErrorDiagnostics(snapshot, pkg, e) - if err != nil { - event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(string(pkg.ID()))) - continue - } - pkg.diagnostics = append(pkg.diagnostics, diags...) - } - // Our heuristic for whether to show type checking errors is: // + If any file was 'fixed', don't show type checking errors as we // can't guarantee that they reference accurate locations in the source. @@ -394,7 +405,7 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF for _, e := range pkg.parseErrors { diags, err := parseErrorDiagnostics(snapshot, pkg, e) if err != nil { - event.Error(ctx, "unable to compute positions for parse errors", err, tag.Package.Of(string(pkg.ID()))) + event.Error(ctx, "unable to compute positions for parse errors", err, tag.Package.Of(string(inputs.id))) continue } for _, diag := range diags { @@ -412,7 +423,7 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF for _, e := range expandErrors(unexpanded, snapshot.View().Options().RelatedInformationSupported) { diags, err := typeErrorDiagnostics(snapshot, pkg, e) if err != nil { - event.Error(ctx, "unable to compute positions for type errors", err, tag.Package.Of(string(pkg.ID()))) + event.Error(ctx, "unable to compute positions for type errors", err, tag.Package.Of(string(inputs.id))) continue } pkg.typeErrors = append(pkg.typeErrors, e.primary) @@ -426,27 +437,20 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF } } - depsErrors, err := snapshot.depsErrors(ctx, pkg) - if err != nil { - return nil, err - } - pkg.diagnostics = append(pkg.diagnostics, depsErrors...) - return pkg, nil } var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) -func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *source.Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { - ctx, done := event.Start(ctx, "cache.typeCheck", tag.Package.Of(string(m.ID))) +func doTypeCheck(ctx context.Context, snapshot *snapshot, inputs typeCheckInputs, astFilter *unexportedFilter) (*syntaxPackage, error) { + ctx, done := event.Start(ctx, "cache.typeCheck", tag.Package.Of(string(inputs.id))) defer done() - pkg := &pkg{ - m: m, - mode: mode, + pkg := &syntaxPackage{ + id: inputs.id, + mode: inputs.mode, fset: snapshot.FileSet(), // must match parse call below (snapshot.ParseGo for now) - deps: make(map[PackageID]*pkg), - types: types.NewPackage(string(m.PkgPath), string(m.Name)), + types: types.NewPackage(string(inputs.pkgPath), string(inputs.name)), typesInfo: &types.Info{ Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object), @@ -461,9 +465,9 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil // Parse the non-compiled GoFiles. (These aren't presented to // the type checker but are part of the returned pkg.) // TODO(adonovan): opt: parallelize parsing. - for _, fh := range goFiles { - goMode := mode - if mode == source.ParseExported { + for _, fh := range inputs.goFiles { + goMode := inputs.mode + if inputs.mode == source.ParseExported { // This package is being loaded only for type information, // to which non-compiled Go files are irrelevant, // so parse only the header. @@ -477,40 +481,32 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil } // Parse the CompiledGoFiles: those seen by the compiler/typechecker. - if err := parseCompiledGoFiles(ctx, compiledGoFiles, snapshot, mode, pkg, astFilter); err != nil { + if err := parseCompiledGoFiles(ctx, inputs.compiledGoFiles, snapshot, inputs.mode, pkg, astFilter); err != nil { return nil, err } // Use the default type information for the unsafe package. - if m.PkgPath == "unsafe" { + if inputs.pkgPath == "unsafe" { // Don't type check Unsafe: it's unnecessary, and doing so exposes a data // race to Unsafe.completed. // TODO(adonovan): factor (tail-merge) with the normal control path. pkg.types = types.Unsafe pkg.methodsets = methodsets.NewIndex(pkg.fset, pkg.types) - pkg.xrefs = xrefs.Index(pkg) + pkg.xrefs = xrefs.Index(pkg.compiledGoFiles, pkg.types, pkg.typesInfo) return pkg, nil } - if len(m.CompiledGoFiles) == 0 { - // No files most likely means go/packages failed. Try to attach error - // messages to the file as much as possible. - var found bool - for _, e := range m.Errors { - srcDiags, err := goPackagesErrorDiagnostics(snapshot, pkg, e) - if err != nil { - continue - } - found = true - pkg.diagnostics = append(pkg.diagnostics, srcDiags...) - } - if found { - return pkg, nil - } - return nil, fmt.Errorf("no parsed files for package %s, expected: %v, errors: %v", pkg.m.PkgPath, pkg.compiledGoFiles, m.Errors) + if len(pkg.compiledGoFiles) == 0 { + // No files most likely means go/packages failed. + // + // TODO(rfindley): in the past, we would capture go list errors in this + // case, to present go list errors to the user. However we had no tests for + // this behavior. It is unclear if anything better can be done here. + return nil, fmt.Errorf("no parsed files for package %s", inputs.pkgPath) } cfg := &types.Config{ + Sizes: inputs.sizes, Error: func(e error) { pkg.typeErrors = append(pkg.typeErrors, e.(types.Error)) }, @@ -519,30 +515,30 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil // based on the metadata before we start type checking, // reporting them via types.Importer places the errors // at the correct source location. - id, ok := pkg.m.DepsByImpPath[ImportPath(path)] + id, ok := inputs.depsByImpPath[ImportPath(path)] if !ok { // If the import declaration is broken, // go list may fail to report metadata about it. // See TestFixImportDecl for an example. return nil, fmt.Errorf("missing metadata for import of %q", path) } - dep, ok := deps[id] // id may be "" + dep, ok := inputs.deps[id] // id may be "" if !ok { return nil, snapshot.missingPkgError(path) } - if !source.IsValidImport(m.PkgPath, dep.m.PkgPath) { + if !source.IsValidImport(inputs.pkgPath, dep.m.PkgPath) { return nil, fmt.Errorf("invalid use of internal package %s", path) } depPkg, err := dep.await(ctx, snapshot) if err != nil { return nil, err } - pkg.deps[depPkg.m.ID] = depPkg return depPkg.types, nil }), } - if pkg.m.Module != nil && pkg.m.Module.GoVersion != "" { - goVersion := "go" + pkg.m.Module.GoVersion + + if inputs.goVersion != "" { + goVersion := "go" + inputs.goVersion // types.NewChecker panics if GoVersion is invalid. An unparsable mod // file should probably stop us before we get here, but double check // just in case. @@ -551,7 +547,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil } } - if mode != source.ParseFull { + if inputs.mode != source.ParseFull { cfg.DisableUnusedImportCheck = true cfg.IgnoreFuncBodies = true } @@ -574,7 +570,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil pkg.methodsets = methodsets.NewIndex(pkg.fset, pkg.types) // Build global index of outbound cross-references. - pkg.xrefs = xrefs.Index(pkg) + pkg.xrefs = xrefs.Index(pkg.compiledGoFiles, pkg.types, pkg.typesInfo) // If the context was cancelled, we may have returned a ton of transient // errors to the type checker. Swallow them. @@ -584,7 +580,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil return pkg, nil } -func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHandle, snapshot *snapshot, mode source.ParseMode, pkg *pkg, astFilter *unexportedFilter) error { +func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHandle, snapshot *snapshot, mode source.ParseMode, pkg *syntaxPackage, astFilter *unexportedFilter) error { // TODO(adonovan): opt: parallelize this loop, which takes 1-25ms. for _, fh := range compiledGoFiles { var pgf *source.ParsedGoFile @@ -633,11 +629,16 @@ func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHand return nil } -func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnostic, error) { +// depsErrors creates diagnostics for each metadata error (e.g. import cycle). +// These may be attached to import declarations in the transitive source files +// of pkg, or to 'requires' declarations in the package's go.mod file. +// +// TODO(rfindley): move this to errors.go +func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsErrors []*packagesinternal.PackageError) ([]*source.Diagnostic, error) { // Select packages that can't be found, and were imported in non-workspace packages. // Workspace packages already show their own errors. var relevantErrors []*packagesinternal.PackageError - for _, depsError := range pkg.m.DepsErrors { + for _, depsError := range depsErrors { // Up to Go 1.15, the missing package was included in the stack, which // was presumably a bug. We want the next one up. directImporterIdx := len(depsError.ImportStack) - 1 @@ -665,7 +666,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost allImports := map[string][]fileImport{} for _, cgf := range pkg.compiledGoFiles { // TODO(adonovan): modify Imports() to accept a single token.File (cgf.Tok). - for _, group := range astutil.Imports(s.FileSet(), cgf.File) { + for _, group := range astutil.Imports(pkg.fset, cgf.File) { for _, imp := range group { if imp.Path == nil { continue diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index 8af1ac355c0..b0babc7b07f 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -28,30 +28,20 @@ import ( "golang.org/x/tools/internal/typesinternal" ) -func goPackagesErrorDiagnostics(snapshot *snapshot, pkg *pkg, e packages.Error) ([]*source.Diagnostic, error) { - if msg, spn, ok := parseGoListImportCycleError(snapshot, e, pkg); ok { - rng, err := spanToRange(pkg, spn) - if err != nil { - return nil, err - } - return []*source.Diagnostic{{ - URI: spn.URI(), - Range: rng, - Severity: protocol.SeverityError, - Source: source.ListError, - Message: msg, - }}, nil +func goPackagesErrorDiagnostics(e packages.Error, pkg *syntaxPackage, fromDir string) (diags []*source.Diagnostic, rerr error) { + if diag, ok := parseGoListImportCycleError(e, pkg); ok { + return []*source.Diagnostic{diag}, nil } var spn span.Span if e.Pos == "" { - spn = parseGoListError(e.Msg, pkg.m.Config.Dir) + spn = parseGoListError(e.Msg, fromDir) // We may not have been able to parse a valid span. Apply the errors to all files. if _, err := spanToRange(pkg, spn); err != nil { var diags []*source.Diagnostic - for _, cgf := range pkg.compiledGoFiles { + for _, pgf := range pkg.compiledGoFiles { diags = append(diags, &source.Diagnostic{ - URI: cgf.URI, + URI: pgf.URI, Severity: protocol.SeverityError, Source: source.ListError, Message: e.Msg, @@ -60,9 +50,20 @@ func goPackagesErrorDiagnostics(snapshot *snapshot, pkg *pkg, e packages.Error) return diags, nil } } else { - spn = span.ParseInDir(e.Pos, pkg.m.Config.Dir) + spn = span.ParseInDir(e.Pos, fromDir) } + // TODO(rfindley): in some cases the go command outputs invalid spans, for + // example (from TestGoListErrors): + // + // package a + // import + // + // In this case, the go command will complain about a.go:2:8, which is after + // the trailing newline but still considered to be on the second line, most + // likely because *token.File lacks information about newline termination. + // + // We could do better here by handling that case. rng, err := spanToRange(pkg, spn) if err != nil { return nil, err @@ -76,7 +77,7 @@ func goPackagesErrorDiagnostics(snapshot *snapshot, pkg *pkg, e packages.Error) }}, nil } -func parseErrorDiagnostics(snapshot *snapshot, pkg *pkg, errList scanner.ErrorList) ([]*source.Diagnostic, error) { +func parseErrorDiagnostics(snapshot *snapshot, pkg *syntaxPackage, errList scanner.ErrorList) ([]*source.Diagnostic, error) { // The first parser error is likely the root cause of the problem. if errList.Len() <= 0 { return nil, fmt.Errorf("no errors in %v", errList) @@ -102,7 +103,7 @@ func parseErrorDiagnostics(snapshot *snapshot, pkg *pkg, errList scanner.ErrorLi var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`) var unsupportedFeatureRe = regexp.MustCompile(`.*require.* go(\d+\.\d+) or later`) -func typeErrorDiagnostics(snapshot *snapshot, pkg *pkg, e extendedError) ([]*source.Diagnostic, error) { +func typeErrorDiagnostics(snapshot *snapshot, pkg *syntaxPackage, e extendedError) ([]*source.Diagnostic, error) { code, loc, err := typeErrorData(pkg, e.primary) if err != nil { return nil, err @@ -286,7 +287,7 @@ func relatedInformation(diag *gobDiagnostic) []source.RelatedInformation { return out } -func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, protocol.Location, error) { +func typeErrorData(pkg *syntaxPackage, terr types.Error) (typesinternal.ErrorCode, protocol.Location, error) { ecode, start, end, ok := typesinternal.ReadGo116ErrorData(terr) if !ok { start, end = terr.Pos, terr.Pos @@ -299,7 +300,7 @@ func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, protoco } // go/types errors retain their FileSet. // Sanity-check that we're using the right one. - fset := pkg.FileSet() + fset := pkg.fset if fset != terr.Fset { return 0, protocol.Location{}, bug.Errorf("wrong FileSet for type error") } @@ -320,7 +321,7 @@ func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, protoco // spanToRange converts a span.Span to a protocol.Range, // assuming that the span belongs to the package whose diagnostics are being computed. -func spanToRange(pkg *pkg, spn span.Span) (protocol.Range, error) { +func spanToRange(pkg *syntaxPackage, spn span.Span) (protocol.Range, error) { pgf, err := pkg.File(spn.URI()) if err != nil { return protocol.Range{}, err @@ -344,36 +345,39 @@ func parseGoListError(input, wd string) span.Span { return span.ParseInDir(input[:msgIndex], wd) } -func parseGoListImportCycleError(snapshot *snapshot, e packages.Error, pkg *pkg) (string, span.Span, bool) { +func parseGoListImportCycleError(e packages.Error, pkg *syntaxPackage) (*source.Diagnostic, bool) { re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`) matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg)) if len(matches) < 3 { - return e.Msg, span.Span{}, false + return nil, false } msg := matches[1] importList := strings.Split(matches[2], " ") // Since the error is relative to the current package. The import that is causing // the import cycle error is the second one in the list. if len(importList) < 2 { - return msg, span.Span{}, false + return nil, false } // Imports have quotation marks around them. circImp := strconv.Quote(importList[1]) - for _, cgf := range pkg.compiledGoFiles { + for _, pgf := range pkg.compiledGoFiles { // Search file imports for the import that is causing the import cycle. - for _, imp := range cgf.File.Imports { + for _, imp := range pgf.File.Imports { if imp.Path.Value == circImp { - start, end, err := safetoken.Offsets(cgf.Tok, imp.Pos(), imp.End()) - if err != nil { - return msg, span.Span{}, false - } - spn, err := cgf.Mapper.OffsetSpan(start, end) + rng, err := pgf.PosMappedRange(imp.Pos(), imp.End()) if err != nil { - return msg, span.Span{}, false + return nil, false } - return msg, spn, true + + return &source.Diagnostic{ + URI: pgf.URI, + Range: rng.Range(), + Severity: protocol.SeverityError, + Source: source.ListError, + Message: msg, + }, true } } } - return msg, span.Span{}, false + return nil, false } diff --git a/gopls/internal/lsp/cache/error_test.go b/gopls/internal/lsp/cache/errors_test.go similarity index 100% rename from gopls/internal/lsp/cache/error_test.go rename to gopls/internal/lsp/cache/errors_test.go diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 1d69504a95e..9b321cec574 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -447,21 +447,13 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con Name: PackageName(pkg.Name), ForTest: PackagePath(packagesinternal.GetForTest(pkg)), TypesSizes: pkg.TypesSizes, - Config: cfg, + LoadDir: cfg.Dir, Module: pkg.Module, + Errors: pkg.Errors, DepsErrors: packagesinternal.GetDepsErrors(pkg), } - updates[id] = m - for _, err := range pkg.Errors { - // Filter out parse errors from go list. We'll get them when we - // actually parse, and buggy overlay support may generate spurious - // errors. (See TestNewModule_Issue38207.) - if strings.Contains(err.Msg, "expected '") { - continue - } - m.Errors = append(m.Errors, err) - } + updates[id] = m for _, filename := range pkg.CompiledGoFiles { uri := span.URIFromPath(filename) diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go index 37aae50a087..bb4823c0326 100644 --- a/gopls/internal/lsp/cache/pkg.go +++ b/gopls/internal/lsp/cache/pkg.go @@ -5,19 +5,22 @@ package cache import ( + "context" "fmt" "go/ast" "go/scanner" "go/token" "go/types" + "strings" - "golang.org/x/mod/module" "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/methodsets" "golang.org/x/tools/gopls/internal/lsp/source/xrefs" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/memoize" ) @@ -29,30 +32,93 @@ type ( ImportPath = source.ImportPath ) -// pkg contains parse trees and type information for a package. -type pkg struct { +// A Package is the union of snapshot-local information (Metadata) and shared +// type-checking information (a syntaxPackage). +// +// TODO(rfindley): for now, we do not persist the post-processing of +// loadDiagnostics, because the value of the snapshot.packages map is just the +// package handle. Fix this. +type Package struct { m *source.Metadata - mode source.ParseMode + pkg *syntaxPackage + loadDiagnostics *memoize.Promise // post-processed errors from loading +} + +func newPackage(m *source.Metadata, pkg *syntaxPackage) *Package { + p := &Package{ + m: m, + pkg: pkg, + } + if len(m.Errors) > 0 || len(m.DepsErrors) > 0 { + p.loadDiagnostics = memoize.NewPromise(fmt.Sprintf("loadDiagnostics(%s)", m.ID), func(ctx context.Context, arg interface{}) interface{} { + s := arg.(*snapshot) + var diags []*source.Diagnostic + for _, packagesErr := range p.m.Errors { + // Filter out parse errors from go list. We'll get them when we + // actually parse, and buggy overlay support may generate spurious + // errors. (See TestNewModule_Issue38207.) + if strings.Contains(packagesErr.Msg, "expected '") { + continue + } + pkgDiags, err := goPackagesErrorDiagnostics(packagesErr, p.pkg, p.m.LoadDir) + if err != nil { + // There are certain cases where the go command returns invalid + // positions, so we cannot panic or even bug.Reportf here. + event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(string(p.m.ID))) + continue + } + diags = append(diags, pkgDiags...) + } + + // TODO(rfindley): this is buggy: an insignificant change to a modfile + // (or an unsaved modfile) could affect the position of deps errors, + // without invalidating the package. + depsDiags, err := s.depsErrors(ctx, p.pkg, p.m.DepsErrors) + if err != nil { + if ctx.Err() == nil { + // TODO(rfindley): consider making this a bug.Reportf. depsErrors should + // not normally fail. + event.Error(ctx, "unable to compute deps errors", err, tag.Package.Of(string(p.m.ID))) + } + return nil + } + diags = append(diags, depsDiags...) + return diags + }) + } + return p +} + +// syntaxPackage contains parse trees and type information for a package. +type syntaxPackage struct { + // -- identifiers -- + id PackageID + mode source.ParseMode + + // -- outputs -- fset *token.FileSet // for now, same as the snapshot's FileSet goFiles []*source.ParsedGoFile compiledGoFiles []*source.ParsedGoFile diagnostics []*source.Diagnostic - deps map[PackageID]*pkg // use m.DepsBy{Pkg,Imp}Path to look up ID - version *module.Version // may be nil; may differ from m.Module.Version parseErrors []scanner.ErrorList typeErrors []types.Error types *types.Package typesInfo *types.Info - hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors - xrefs []byte // serializable index of outbound cross-references - - analyses memoize.Store // maps analyzer.Name to Promise[actionResult] - methodsets *methodsets.Index // index of method sets of package-level types + hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors + xrefs []byte // serializable index of outbound cross-references + analyses memoize.Store // maps analyzer.Name to Promise[actionResult] + methodsets *methodsets.Index // index of method sets of package-level types } -func (p *pkg) String() string { return string(p.ID()) } +func (p *Package) String() string { return string(p.m.ID) } + +func (p *Package) Metadata() *source.Metadata { + return p.m +} // A loadScope defines a package loading scope for use with go/packages. +// +// TODO(rfindley): move this to load.go. type loadScope interface { aScope() } @@ -70,127 +136,96 @@ func (packageLoadScope) aScope() {} func (moduleLoadScope) aScope() {} func (viewLoadScope) aScope() {} -func (p *pkg) ID() PackageID { return p.m.ID } -func (p *pkg) Name() PackageName { return p.m.Name } -func (p *pkg) PkgPath() PackagePath { return p.m.PkgPath } +func (p *Package) ParseMode() source.ParseMode { + return p.pkg.mode +} -func (p *pkg) ParseMode() source.ParseMode { - return p.mode +func (p *Package) CompiledGoFiles() []*source.ParsedGoFile { + return p.pkg.compiledGoFiles } -func (p *pkg) CompiledGoFiles() []*source.ParsedGoFile { - return p.compiledGoFiles +func (p *Package) File(uri span.URI) (*source.ParsedGoFile, error) { + return p.pkg.File(uri) } -func (p *pkg) File(uri span.URI) (*source.ParsedGoFile, error) { - for _, cgf := range p.compiledGoFiles { +func (pkg *syntaxPackage) File(uri span.URI) (*source.ParsedGoFile, error) { + for _, cgf := range pkg.compiledGoFiles { if cgf.URI == uri { return cgf, nil } } - for _, gf := range p.goFiles { + for _, gf := range pkg.goFiles { if gf.URI == uri { return gf, nil } } - return nil, fmt.Errorf("no parsed file for %s in %v", uri, p.m.ID) + return nil, fmt.Errorf("no parsed file for %s in %v", uri, pkg.id) } -func (p *pkg) GetSyntax() []*ast.File { +func (p *Package) GetSyntax() []*ast.File { var syntax []*ast.File - for _, pgf := range p.compiledGoFiles { + for _, pgf := range p.pkg.compiledGoFiles { syntax = append(syntax, pgf.File) } return syntax } -func (p *pkg) FileSet() *token.FileSet { - return p.fset +func (p *Package) FileSet() *token.FileSet { + return p.pkg.fset } -func (p *pkg) GetTypes() *types.Package { - return p.types +func (p *Package) GetTypes() *types.Package { + return p.pkg.types } -func (p *pkg) GetTypesInfo() *types.Info { - return p.typesInfo +func (p *Package) GetTypesInfo() *types.Info { + return p.pkg.typesInfo } -func (p *pkg) GetTypesSizes() types.Sizes { - return p.m.TypesSizes +func (p *Package) HasParseErrors() bool { + return len(p.pkg.parseErrors) != 0 } -func (p *pkg) ForTest() string { - return string(p.m.ForTest) +func (p *Package) HasTypeErrors() bool { + return len(p.pkg.typeErrors) != 0 } -// DirectDep returns the directly imported dependency of this package, -// given its PackagePath. (If you have an ImportPath, e.g. a string -// from an import declaration, use ResolveImportPath instead. -// They may differ in case of vendoring.) -func (p *pkg) DirectDep(pkgPath PackagePath) (source.Package, error) { - if id, ok := p.m.DepsByPkgPath[pkgPath]; ok { - if imp := p.deps[id]; imp != nil { - return imp, nil +func (p *Package) DiagnosticsForFile(ctx context.Context, s source.Snapshot, uri span.URI) ([]*source.Diagnostic, error) { + var diags []*source.Diagnostic + for _, diag := range p.pkg.diagnostics { + if diag.URI == uri { + diags = append(diags, diag) } } - return nil, fmt.Errorf("package does not import package with path %s", pkgPath) -} -// ResolveImportPath returns the directly imported dependency of this package, -// given its ImportPath. See also DirectDep. -func (p *pkg) ResolveImportPath(importPath ImportPath) (source.Package, error) { - if id, ok := p.m.DepsByImpPath[importPath]; ok && id != "" { - if imp := p.deps[id]; imp != nil { - return imp, nil + if p.loadDiagnostics != nil { + res, err := p.loadDiagnostics.Get(ctx, s) + if err != nil { + return nil, err } - } - return nil, fmt.Errorf("package does not import %s", importPath) -} - -func (p *pkg) Imports() []source.Package { - var result []source.Package // unordered - for _, dep := range p.deps { - result = append(result, dep) - } - return result -} - -func (p *pkg) Version() *module.Version { - return p.version -} - -func (p *pkg) HasListOrParseErrors() bool { - return len(p.m.Errors) != 0 || len(p.parseErrors) != 0 -} - -func (p *pkg) HasTypeErrors() bool { - return len(p.typeErrors) != 0 -} - -func (p *pkg) DiagnosticsForFile(uri span.URI) []*source.Diagnostic { - var res []*source.Diagnostic - for _, diag := range p.diagnostics { - if diag.URI == uri { - res = append(res, diag) + for _, diag := range res.([]*source.Diagnostic) { + if diag.URI == uri { + diags = append(diags, diag) + } } } - return res + + return diags, nil } // ReferencesTo returns the location of each reference within package p // to one of the target objects denoted by the pair (package path, object path). -func (p *pkg) ReferencesTo(targets map[PackagePath]map[objectpath.Path]struct{}) []protocol.Location { +func (p *Package) ReferencesTo(targets map[PackagePath]map[objectpath.Path]struct{}) []protocol.Location { // TODO(adonovan): In future, p.xrefs will be retrieved from a // section of the cache file produced by type checking. // (Other sections will include the package's export data, // "implements" relations, exported symbols, etc.) // For now we just hang it off the pkg. - return xrefs.Lookup(p.m, p.xrefs, targets) + return xrefs.Lookup(p.m, p.pkg.xrefs, targets) } -func (p *pkg) MethodSetsIndex() *methodsets.Index { +func (p *Package) MethodSetsIndex() *methodsets.Index { // TODO(adonovan): In future, p.methodsets will be retrieved from a // section of the cache file produced by type checking. - return p.methodsets + return p.pkg.methodsets } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 51d7a7964bb..995fba34ca6 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -118,7 +118,7 @@ type snapshot struct { // analyses maps an analysisKey (which identifies a package // and a set of analyzers) to the handle for the future result // of loading the package and analyzing it. - analyses *persistent.Map // from analysisKey to analysisHandle + analyses *persistent.Map // from analysisKey to analysisPromise // workspacePackages contains the workspace's packages, which are loaded // when the view is created. @@ -626,10 +626,20 @@ func (s *snapshot) buildOverlay() map[string][]byte { } // TypeCheck type-checks the specified packages in the given mode. +// +// The resulting packages slice always contains len(ids) entries, though some +// of them may be nil if (and only if) the resulting error is non-nil. +// +// An error is returned if any of the packages fail to type-check. This is +// different from having type-checking errors: a failure to type-check +// indicates context cancellation or otherwise significant failure to perform +// the type-checking operation. func (s *snapshot) TypeCheck(ctx context.Context, mode source.TypecheckMode, ids ...PackageID) ([]source.Package, error) { // Build all the handles... - var phs []*packageHandle - for _, id := range ids { + phs := make([]*packageHandle, len(ids)) + pkgs := make([]source.Package, len(ids)) + var firstErr error + for i, id := range ids { parseMode := source.ParseFull if mode == source.TypecheckWorkspace { parseMode = s.workspaceParseMode(id) @@ -637,21 +647,36 @@ func (s *snapshot) TypeCheck(ctx context.Context, mode source.TypecheckMode, ids ph, err := s.buildPackageHandle(ctx, id, parseMode) if err != nil { - return nil, err + if firstErr == nil { + firstErr = err + } + if ctx.Err() != nil { + return pkgs, firstErr + } + continue } - phs = append(phs, ph) + phs[i] = ph } // ...then await them all. - var pkgs []source.Package - for _, ph := range phs { - pkg, err := ph.await(ctx, s) + for i, ph := range phs { + if ph == nil { + continue + } + p, err := ph.await(ctx, s) if err != nil { - return nil, err + if firstErr == nil { + firstErr = err + } + if ctx.Err() != nil { + return pkgs, firstErr + } + continue } - pkgs = append(pkgs, pkg) + pkgs[i] = newPackage(ph.m, p) } - return pkgs, nil + + return pkgs, firstErr } func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) { @@ -1058,7 +1083,7 @@ func (s *snapshot) AllMetadata(ctx context.Context) ([]*source.Metadata, error) return meta, nil } -func (s *snapshot) CachedImportPaths(ctx context.Context) (map[PackagePath]source.Package, error) { +func (s *snapshot) CachedImportPaths(ctx context.Context) (map[PackagePath]*types.Package, error) { // Don't reload workspace package metadata. // This function is meant to only return currently cached information. s.AwaitInitialized(ctx) @@ -1066,24 +1091,41 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[PackagePath]sourc s.mu.Lock() defer s.mu.Unlock() - results := map[PackagePath]source.Package{} + pkgs := make(map[PackagePath]*syntaxPackage) + + // Find all cached packages that are imported a nonzero amount of time. + // + // TODO(rfindley): this is pre-existing behavior, and a test fails if we + // don't do the importCount filter, but why do we care if a package is + // imported a nonzero amount of times? + imported := make(map[PackagePath]bool) s.packages.Range(func(_, v interface{}) { - cachedPkg, err := v.(*packageHandle).cached() + ph := v.(*packageHandle) + for dep := range ph.m.DepsByPkgPath { + imported[dep] = true + } + if ph.m.Name == "main" { + return + } + pkg, err := ph.cached() if err != nil { return } - for _, newPkg := range cachedPkg.deps { - pkgPath := newPkg.PkgPath() - if oldPkg, ok := results[pkgPath]; ok { - // Using the same trick as NarrowestPackage, prefer non-variants. - if len(newPkg.compiledGoFiles) < len(oldPkg.(*pkg).compiledGoFiles) { - results[pkgPath] = newPkg - } - } else { - results[pkgPath] = newPkg + if old, ok := pkgs[ph.m.PkgPath]; ok { + if len(pkg.compiledGoFiles) < len(old.compiledGoFiles) { + pkgs[ph.m.PkgPath] = pkg } + } else { + pkgs[ph.m.PkgPath] = pkg } }) + results := make(map[PackagePath]*types.Package) + for pkgPath, pkg := range pkgs { + if imported[pkgPath] { + results[pkgPath] = pkg.types + } + } + return results, nil } diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index ec918b7c7bd..c91def60054 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -162,12 +162,16 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara if err != nil { return nil, err } - analysisDiags, err := source.Analyze(ctx, snapshot, pkg.ID(), true) + pkgDiags, err := pkg.DiagnosticsForFile(ctx, snapshot, uri) + if err != nil { + return nil, err + } + analysisDiags, err := source.Analyze(ctx, snapshot, pkg.Metadata().ID, true) if err != nil { return nil, err } var fileDiags []*source.Diagnostic - source.CombineDiagnostics(pkg, fh.URI(), analysisDiags, &fileDiags, &fileDiags) + source.CombineDiagnostics(pkgDiags, analysisDiags[uri], &fileDiags, &fileDiags) // Split diagnostics into fixes, which must match incoming diagnostics, // and non-fixes, which must match the requested range. Build actions diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index ef30b6f776b..a58f40497da 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -401,8 +401,15 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, m *s if snapshot.IsBuiltin(ctx, cgf.URI) { continue } + + pkgDiags, err := pkg.DiagnosticsForFile(ctx, snapshot, cgf.URI) + if err != nil { + event.Error(ctx, "warning: getting package diagnostics", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...) + return + } + var tdiags, adiags []*source.Diagnostic - source.CombineDiagnostics(pkg, cgf.URI, analysisDiags, &tdiags, &adiags) + source.CombineDiagnostics(pkgDiags, analysisDiags[cgf.URI], &tdiags, &adiags) s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, tdiags, true) s.storeDiagnostics(snapshot, cgf.URI, analysisSource, adiags, true) } diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 3cbe3529ead..25860ead36b 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -107,7 +107,11 @@ func ModDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.Fil return nil, err } for _, pkg := range pkgs { - diagnostics = append(diagnostics, pkg.DiagnosticsForFile(fh.URI())...) + pkgDiags, err := pkg.DiagnosticsForFile(ctx, snapshot, fh.URI()) + if err != nil { + return nil, err + } + diagnostics = append(diagnostics, pkgDiags...) } } diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 728f61de7ab..93328773b99 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -76,10 +76,11 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu // this is a little cumbersome to avoid both exporting 'encoded' and its methods // and to avoid import cycles e := &encoded{ - ctx: ctx, - rng: rng, - tokTypes: s.session.Options().SemanticTypes, - tokMods: s.session.Options().SemanticMods, + ctx: ctx, + metadataSource: snapshot, + rng: rng, + tokTypes: s.session.Options().SemanticTypes, + tokMods: s.session.Options().SemanticMods, } add := func(line, start uint32, len uint32) { e.add(line, start, len, tokMacro, nil) @@ -103,16 +104,17 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu return nil, err } e := &encoded{ - ctx: ctx, - pgf: pgf, - rng: rng, - ti: pkg.GetTypesInfo(), - pkg: pkg, - fset: pkg.FileSet(), - tokTypes: s.session.Options().SemanticTypes, - tokMods: s.session.Options().SemanticMods, - noStrings: vv.Options().NoSemanticString, - noNumbers: vv.Options().NoSemanticNumber, + ctx: ctx, + metadataSource: snapshot, + pgf: pgf, + rng: rng, + ti: pkg.GetTypesInfo(), + pkg: pkg, + fset: pkg.FileSet(), + tokTypes: s.session.Options().SemanticTypes, + tokMods: s.session.Options().SemanticMods, + noStrings: vv.Options().NoSemanticString, + noNumbers: vv.Options().NoSemanticNumber, } if err := e.init(); err != nil { // e.init should never return an error, unless there's some @@ -216,7 +218,11 @@ type encoded struct { noStrings bool noNumbers bool - ctx context.Context + ctx context.Context + // metadataSource is used to resolve imports + metadataSource interface { + Metadata(source.PackageID) *source.Metadata + } tokTypes, tokMods []string pgf *source.ParsedGoFile rng *protocol.Range @@ -908,20 +914,24 @@ func (e *encoded) importSpec(d *ast.ImportSpec) { return } // Import strings are implementation defined. Try to match with parse information. - imported, err := e.pkg.ResolveImportPath(importPath) - if err != nil { + 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(imported.Name())) + j := strings.LastIndex(d.Path.Value, string(depMD.Name)) if j == -1 { - // name doesn't show up, for whatever reason, so nothing to report + // 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(imported.Name()), tokNamespace, nil) + e.token(start, len(depMD.Name), tokNamespace, nil) } // log unexpected state diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index e97c1474a28..3fa7ca2227d 100755 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -56,14 +56,6 @@ var GeneratedAPIJSON = &APIJSON{ Status: "experimental", Hierarchy: "build", }, - { - Name: "experimentalPackageCacheKey", - Type: "bool", - Doc: "experimentalPackageCacheKey controls whether to use a coarser cache key\nfor package type information to increase cache hits. This setting removes\nthe user's environment, build flags, and working directory from the cache\nkey, which should be a safe change as all relevant inputs into the type\nchecking pass are already hashed into the key. This is temporarily guarded\nby an experiment because caching behavior is subtle and difficult to\ncomprehensively test.\n", - Default: "true", - Status: "experimental", - Hierarchy: "build", - }, { Name: "allowModfileModifications", Type: "bool", diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 056b2893c49..3343a1a5962 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -1088,10 +1088,12 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { // Is sel a qualified identifier? if id, ok := sel.X.(*ast.Ident); ok { if pkgName, ok := c.pkg.GetTypesInfo().Uses[id].(*types.PkgName); ok { - pkg, _ := c.pkg.DirectDep(source.PackagePath(pkgName.Imported().Path())) - // If the package is not imported, try searching for unimported - // completions. - if pkg == nil && c.opts.unimported { + // If this package path is not a known dep, it means that it resolves to + // an import path that couldn't be resolved by go/packages. + // + // Try to complete from the package cache. + pkgPath := source.PackagePath(pkgName.Imported().Path()) + if _, ok := c.pkg.Metadata().DepsByPkgPath[pkgPath]; !ok && c.opts.unimported { if err := c.unimportedMembers(ctx, id); err != nil { return err } @@ -1133,7 +1135,7 @@ func (c *completer) unimportedMembers(ctx context.Context, id *ast.Ident) error var paths []string for path, pkg := range known { - if pkg.GetTypes().Name() != id.Name { + if pkg.Name() != id.Name { continue } paths = append(paths, string(path)) @@ -1155,16 +1157,16 @@ func (c *completer) unimportedMembers(ctx context.Context, id *ast.Ident) error for _, path := range paths { pkg := known[source.PackagePath(path)] - if pkg.GetTypes().Name() != id.Name { + if pkg.Name() != id.Name { continue } imp := &importInfo{ importPath: path, } - if imports.ImportPathToAssumedName(path) != pkg.GetTypes().Name() { - imp.name = pkg.GetTypes().Name() + if imports.ImportPathToAssumedName(path) != pkg.Name() { + imp.name = pkg.Name() } - c.packageMembers(pkg.GetTypes(), unimportedScore(relevances[path]), imp, func(cand candidate) { + c.packageMembers(pkg, unimportedScore(relevances[path]), imp, func(cand candidate) { c.deepState.enqueue(cand) }) if len(c.items) >= unimportedMemberTarget { @@ -1479,7 +1481,7 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru } var paths []string // actually PackagePaths for path, pkg := range known { - if !strings.HasPrefix(pkg.GetTypes().Name(), prefix) { + if !strings.HasPrefix(pkg.Name(), prefix) { continue } paths = append(paths, string(path)) @@ -1508,21 +1510,21 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru for _, path := range paths { pkg := known[source.PackagePath(path)] - if _, ok := seen[pkg.GetTypes().Name()]; ok { + if _, ok := seen[pkg.Name()]; ok { continue } imp := &importInfo{ importPath: path, } - if imports.ImportPathToAssumedName(path) != pkg.GetTypes().Name() { - imp.name = pkg.GetTypes().Name() + if imports.ImportPathToAssumedName(path) != pkg.Name() { + imp.name = pkg.Name() } if count >= maxUnimportedPackageNames { return nil } c.deepState.enqueue(candidate{ // Pass an empty *types.Package to disable deep completions. - obj: types.NewPkgName(0, nil, pkg.GetTypes().Name(), types.NewPackage(path, string(pkg.Name()))), + obj: types.NewPkgName(0, nil, pkg.Name(), types.NewPackage(path, string(pkg.Name()))), score: unimportedScore(relevances[path]), imp: imp, }) diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go index bb3ccd72d69..16da642fe57 100644 --- a/gopls/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -81,7 +81,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e if _, ok := obj.Type().(*types.Struct); ok { detail = "struct{...}" // for anonymous structs } else if obj.IsField() { - detail = source.FormatVarType(c.pkg, obj, c.qf) + detail = source.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qf) } if obj.IsField() { kind = protocol.FieldCompletion @@ -238,7 +238,7 @@ Suffixes: uri := span.URIFromPath(pos.Filename) // Find the source file of the candidate. - pkg, err := source.FindPackageFromPos(c.pkg, obj.Pos()) + pkg, err := source.FindPackageFromPos(ctx, c.snapshot, c.pkg, obj.Pos()) if err != nil { return item, nil } diff --git a/gopls/internal/lsp/source/completion/literal.go b/gopls/internal/lsp/source/completion/literal.go index 870b97ab27b..9d4b0e6fba9 100644 --- a/gopls/internal/lsp/source/completion/literal.go +++ b/gopls/internal/lsp/source/completion/literal.go @@ -162,7 +162,7 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im if score := c.matcher.Score("func"); !cand.hasMod(reference) && score > 0 && (expType == nil || !types.IsInterface(expType)) { switch t := literalType.Underlying().(type) { case *types.Signature: - c.functionLiteral(t, float64(score)) + c.functionLiteral(ctx, t, float64(score)) } } } @@ -175,7 +175,7 @@ const literalCandidateScore = highScore / 2 // functionLiteral adds a function literal completion item for the // given signature. -func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) { +func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, matchScore float64) { snip := &snippet.Builder{} snip.WriteText("func(") @@ -202,7 +202,7 @@ func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) { // If the param has no name in the signature, guess a name based // on the type. Use an empty qualifier to ignore the package. // For example, we want to name "http.Request" "r", not "hr". - name = source.FormatVarType(c.pkg, p, func(p *types.Package) string { + name = source.FormatVarType(ctx, c.snapshot, c.pkg, p, func(p *types.Package) string { return "" }) name = abbreviateTypeName(name) @@ -264,7 +264,7 @@ func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) { // of "i int, j int". if i == sig.Params().Len()-1 || !types.Identical(p.Type(), sig.Params().At(i+1).Type()) { snip.WriteText(" ") - typeStr := source.FormatVarType(c.pkg, p, c.qf) + typeStr := source.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qf) if sig.Variadic() && i == sig.Params().Len()-1 { typeStr = strings.Replace(typeStr, "[]", "...", 1) } @@ -314,7 +314,7 @@ func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) { snip.WriteText(name + " ") } - text := source.FormatVarType(c.pkg, r, c.qf) + text := source.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qf) if tp, _ := r.Type().(*typeparams.TypeParam); tp != nil && !c.typeParamInScope(tp) { snip.WritePlaceholder(func(snip *snippet.Builder) { snip.WriteText(text) diff --git a/gopls/internal/lsp/source/completion/statements.go b/gopls/internal/lsp/source/completion/statements.go index dcce76ff76e..809cb808a60 100644 --- a/gopls/internal/lsp/source/completion/statements.go +++ b/gopls/internal/lsp/source/completion/statements.go @@ -330,24 +330,31 @@ func getTestVar(enclosingFunc *funcInfo, pkg source.Package) string { return "" } + var testingPkg *types.Package + for _, p := range pkg.GetTypes().Imports() { + if p.Path() == "testing" { + testingPkg = p + break + } + } + if testingPkg == nil { + return "" + } + tbObj := testingPkg.Scope().Lookup("TB") + if tbObj == nil { + return "" + } + iface, ok := tbObj.Type().Underlying().(*types.Interface) + if !ok { + return "" + } + sig := enclosingFunc.sig for i := 0; i < sig.Params().Len(); i++ { param := sig.Params().At(i) if param.Name() == "_" { continue } - testingPkg, err := pkg.DirectDep("testing") - if err != nil { - continue - } - tbObj := testingPkg.GetTypes().Scope().Lookup("TB") - if tbObj == nil { - continue - } - iface, ok := tbObj.Type().Underlying().(*types.Interface) - if !ok { - continue - } if !types.Implements(param.Type(), iface) { continue } diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index a7350b00ea6..042db178504 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -82,18 +82,22 @@ func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (File if err != nil { return nil, nil, err } - adiags, err := Analyze(ctx, snapshot, pkg.ID(), false) + pkgDiags, err := pkg.DiagnosticsForFile(ctx, snapshot, uri) + if err != nil { + return nil, nil, err + } + adiags, err := Analyze(ctx, snapshot, pkg.Metadata().ID, false) if err != nil { return nil, nil, err } var fileDiags []*Diagnostic // combine load/parse/type + analysis diagnostics - CombineDiagnostics(pkg, fh.URI(), adiags, &fileDiags, &fileDiags) + CombineDiagnostics(pkgDiags, adiags[uri], &fileDiags, &fileDiags) return fh, fileDiags, nil } -// CombineDiagnostics combines and filters list/parse/type diagnostics -// from pkg.DiagnosticsForFile(uri) with analysisDiagnostics[uri], and -// appends the two lists to *outT and *outA, respectively. +// CombineDiagnostics combines and filters list/parse/type diagnostics from +// tdiags with adiags, and appends the two lists to *outT and *outA, +// respectively. // // Type-error analyzers produce diagnostics that are redundant // with type checker diagnostics, but more detailed (e.g. fixes). @@ -111,7 +115,7 @@ func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (File // easily choose whether to keep the results separate or combined. // // The arguments are not modified. -func CombineDiagnostics(pkg Package, uri span.URI, analysisDiagnostics map[span.URI][]*Diagnostic, outT, outA *[]*Diagnostic) { +func CombineDiagnostics(tdiags []*Diagnostic, adiags []*Diagnostic, outT, outA *[]*Diagnostic) { // Build index of (list+parse+)type errors. type key struct { @@ -119,14 +123,13 @@ func CombineDiagnostics(pkg Package, uri span.URI, analysisDiagnostics map[span. message string } index := make(map[key]int) // maps (Range,Message) to index in tdiags slice - tdiags := pkg.DiagnosticsForFile(uri) for i, diag := range tdiags { index[key{diag.Range, diag.Message}] = i } // Filter out analysis diagnostics that match type errors, // retaining their suggested fix (etc) fields. - for _, diag := range analysisDiagnostics[uri] { + for _, diag := range adiags { if i, ok := index[key{diag.Range, diag.Message}]; ok { copy := *tdiags[i] copy.SuggestedFixes = diag.SuggestedFixes diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index 4fe3ae8b3bb..d0f77e62a8b 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -54,7 +54,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p } var ranges []protocol.Range for rng := range result { - mRng, err := posToMappedRange(pkg, rng.start, rng.end) + mRng, err := posToMappedRange(ctx, snapshot, pkg, rng.start, rng.end) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 9eee53ad3bd..c27065662d2 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -327,7 +327,7 @@ func hoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) // Check if the identifier is test-only (and is therefore not part of a // package's API). This is true if the request originated in a test package, // and if the declaration is also found in the same test package. - if i.pkg != nil && obj.Pkg() != nil && i.pkg.ForTest() != "" { + if i.pkg != nil && obj.Pkg() != nil && i.pkg.Metadata().ForTest != "" { if _, err := i.pkg.File(i.Declaration.MappedRange[0].URI()); err == nil { return h, nil } @@ -443,18 +443,22 @@ func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) { if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" { return "", "", false } - impPkg, err := i.pkg.DirectDep(PackagePath(path)) - if err != nil { + impID, ok := i.pkg.Metadata().DepsByPkgPath[PackagePath(path)] + if !ok { + return "", "", false + } + impMeta := i.Snapshot.Metadata(impID) + if impMeta == nil { return "", "", false } - if impPkg.Version() == nil { + module := impMeta.Module + if module == nil { return "", "", false } - version, modpath := impPkg.Version().Version, impPkg.Version().Path - if modpath == "" || version == "" { + if module.Path == "" || module.Version == "" { return "", "", false } - return modpath, version, true + return module.Path, module.Version, true } // objectString is a wrapper around the types.ObjectString function. @@ -534,7 +538,9 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob if err != nil { return nil, err } - imp, err := pkg.ResolveImportPath(ImportPath(importPath)) + // TODO(rfindley): avoid type-checking here, by re-parsing the package with + // ParseHeader. + imp, err := ResolveImportPath(ctx, s, pkg.Metadata().ID, ImportPath(importPath)) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 39743b2a507..0be26b56540 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -98,7 +98,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa file := pgf.File // Handle import specs separately, as there is no formal position for a // package declaration. - if result, err := importSpec(snapshot, pkg, pgf, pos); result != nil || err != nil { + if result, err := importSpec(ctx, snapshot, pkg, pgf, pos); result != nil || err != nil { return result, err } path := pathEnclosingObjNode(file, pos) @@ -155,7 +155,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa result.Name = result.ident.Name var err error - if result.MappedRange, err = posToMappedRange(pkg, result.ident.Pos(), result.ident.End()); err != nil { + if result.MappedRange, err = posToMappedRange(ctx, snapshot, pkg, result.ident.Pos(), result.ident.End()); err != nil { return nil, err } @@ -266,7 +266,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // findFileInDeps, which is also called below. Refactor // objToMappedRange to separate the find-file from the // lookup-position steps to avoid the redundancy. - rng, err := objToMappedRange(pkg, result.Declaration.obj) + rng, err := objToMappedRange(ctx, snapshot, pkg, result.Declaration.obj) if err != nil { return nil, err } @@ -274,7 +274,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa declPos := result.Declaration.obj.Pos() objURI := span.URIFromPath(pkg.FileSet().File(declPos).Name()) - declFile, declPkg, err := findFileInDeps(pkg, objURI) + declFile, declPkg, err := findFileInDeps(ctx, snapshot, pkg, objURI) if err != nil { return nil, err } @@ -301,7 +301,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if hasErrorType(result.Type.Object) { return result, nil } - if result.Type.MappedRange, err = objToMappedRange(pkg, result.Type.Object); err != nil { + if result.Type.MappedRange, err = objToMappedRange(ctx, snapshot, pkg, result.Type.Object); err != nil { return nil, err } } @@ -449,7 +449,7 @@ func hasErrorType(obj types.Object) bool { } // importSpec handles positions inside of an *ast.ImportSpec. -func importSpec(snapshot Snapshot, pkg Package, pgf *ParsedGoFile, pos token.Pos) (*IdentifierInfo, error) { +func importSpec(ctx context.Context, snapshot Snapshot, pkg Package, pgf *ParsedGoFile, pos token.Pos) (*IdentifierInfo, error) { var imp *ast.ImportSpec for _, spec := range pgf.File.Imports { if spec.Path.Pos() <= pos && pos < spec.Path.End() { @@ -463,7 +463,7 @@ func importSpec(snapshot Snapshot, pkg Package, pgf *ParsedGoFile, pos token.Pos if err != nil { return nil, fmt.Errorf("import path not quoted: %s (%v)", imp.Path.Value, err) } - imported, err := pkg.ResolveImportPath(ImportPath(importPath)) + imported, err := ResolveImportPath(ctx, snapshot, pkg.Metadata().ID, ImportPath(importPath)) if err != nil { return nil, err } @@ -472,13 +472,13 @@ func importSpec(snapshot Snapshot, pkg Package, pgf *ParsedGoFile, pos token.Pos Name: importPath, // should this perhaps be imported.PkgPath()? pkg: pkg, } - if result.MappedRange, err = posToMappedRange(pkg, imp.Path.Pos(), imp.Path.End()); err != nil { + if result.MappedRange, err = posToMappedRange(ctx, snapshot, pkg, imp.Path.Pos(), imp.Path.End()); err != nil { return nil, err } // Consider the "declaration" of an import spec to be the imported package. // Return all of the files in the package as the definition of the import spec. for _, dst := range imported.GetSyntax() { - rng, err := posToMappedRange(pkg, dst.Pos(), dst.End()) + rng, err := posToMappedRange(ctx, snapshot, pkg, dst.Pos(), dst.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index be29973ae8c..50520267e8e 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -324,22 +324,28 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s } // Get all of the transitive dependencies of the search package. - pkgs := make(map[*types.Package]Package) - var addPkg func(pkg Package) - addPkg = func(pkg Package) { - pkgs[pkg.GetTypes()] = pkg - for _, imp := range pkg.Imports() { - if _, ok := pkgs[imp.GetTypes()]; !ok { - addPkg(imp) - } + pkgSet := map[*types.Package]Package{ + searchpkg.GetTypes(): searchpkg, + } + deps := recursiveDeps(s, searchpkg.Metadata())[1:] + // Ignore the error from type checking, but check if the context was + // canceled (which would have caused TypeCheck to exit early). + depPkgs, _ := s.TypeCheck(ctx, TypecheckWorkspace, deps...) + if ctx.Err() != nil { + return nil, ctx.Err() + } + for _, dep := range depPkgs { + // Since we ignored the error from type checking, pkg may be nil. + if dep != nil { + pkgSet[dep.GetTypes()] = dep } } - addPkg(searchpkg) + for _, obj := range objs { if obj.Parent() == types.Universe { return nil, fmt.Errorf("%q: %w", obj.Name(), errBuiltin) } - pkg, ok := pkgs[obj.Pkg()] + pkg, ok := pkgSet[obj.Pkg()] if !ok { event.Error(ctx, fmt.Sprintf("no package for obj %s: %v", obj, obj.Pkg()), err) continue diff --git a/gopls/internal/lsp/source/implementation2.go b/gopls/internal/lsp/source/implementation2.go index 09659ccd622..a776c038951 100644 --- a/gopls/internal/lsp/source/implementation2.go +++ b/gopls/internal/lsp/source/implementation2.go @@ -166,7 +166,7 @@ func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp } globalIDs := make([]PackageID, 0, len(globalMetas)) for _, m := range globalMetas { - if m.PkgPath == declPkg.PkgPath() { + if m.PkgPath == declPkg.Metadata().PkgPath { continue // declaring package is handled by local implementation } globalIDs = append(globalIDs, m.ID) diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index a32ed128aa8..bb0b5ecb6d3 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -116,12 +116,11 @@ func DefaultOptions() *Options { }, UserOptions: UserOptions{ BuildOptions: BuildOptions{ - ExpandWorkspaceToModule: true, - ExperimentalPackageCacheKey: true, - MemoryMode: ModeNormal, - DirectoryFilters: []string{"-**/node_modules"}, - TemplateExtensions: []string{}, - StandaloneTags: []string{"ignore"}, + ExpandWorkspaceToModule: true, + MemoryMode: ModeNormal, + DirectoryFilters: []string{"-**/node_modules"}, + TemplateExtensions: []string{}, + StandaloneTags: []string{"ignore"}, }, UIOptions: UIOptions{ DiagnosticOptions: DiagnosticOptions{ @@ -269,15 +268,6 @@ type BuildOptions struct { // a go.mod file, narrowing the scope to that directory if it exists. ExpandWorkspaceToModule bool `status:"experimental"` - // ExperimentalPackageCacheKey controls whether to use a coarser cache key - // for package type information to increase cache hits. This setting removes - // the user's environment, build flags, and working directory from the cache - // key, which should be a safe change as all relevant inputs into the type - // checking pass are already hashed into the key. This is temporarily guarded - // by an experiment because caching behavior is subtle and difficult to - // comprehensively test. - ExperimentalPackageCacheKey bool `status:"experimental"` - // AllowModfileModifications disables -mod=readonly, allowing imports from // out-of-scope modules. This option will eventually be removed. AllowModfileModifications bool `status:"experimental"` @@ -1113,7 +1103,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.deprecated("") case "experimentalPackageCacheKey": - result.setBool(&o.ExperimentalPackageCacheKey) + result.deprecated("") case "allowModfileModifications": result.setBool(&o.AllowModfileModifications) diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index f14d1d601f6..1fc7fde78b9 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -93,7 +93,7 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i // transitively for (e.g.) capitalized local variables. // We could do better by checking for an objectpath. transitive := qo.obj.Pkg().Scope().Lookup(qo.obj.Name()) != qo.obj - rdeps, err := snapshot.ReverseDependencies(ctx, qo.pkg.ID(), transitive) + rdeps, err := snapshot.ReverseDependencies(ctx, qo.pkg.Metadata().ID, transitive) if err != nil { return nil, err } @@ -134,14 +134,14 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i } key, found := packagePositionKey(pkg, ident.Pos()) if !found { - bug.Reportf("ident %v (pos: %v) not found in package %v", ident.Name, ident.Pos(), pkg.Name()) + bug.Reportf("ident %v (pos: %v) not found in package %v", ident.Name, ident.Pos(), pkg.Metadata().ID) continue } if seen[key] { continue } seen[key] = true - rng, err := posToMappedRange(pkg, ident.Pos(), ident.End()) + rng, err := posToMappedRange(ctx, snapshot, pkg, ident.Pos(), ident.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index 4b6ba4cd757..1a01eb98a85 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -316,7 +316,7 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp ref := &ReferenceInfoV2{ IsDeclaration: isDecl, Location: loc, - PkgPath: pkg.PkgPath(), + PkgPath: pkg.Metadata().PkgPath, Name: obj.Name(), } refsMu.Lock() @@ -402,7 +402,7 @@ func expandMethodSearch(ctx context.Context, snapshot Snapshot, method *types.Fu // Expand global search scope to include rdeps of this pkg. if len(results) > 0 { - rdeps, err := snapshot.ReverseDependencies(ctx, pkg.ID(), true) + rdeps, err := snapshot.ReverseDependencies(ctx, pkg.Metadata().ID, true) if err != nil { return err } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 1d2a03b0f13..385dff4a78b 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -30,6 +30,7 @@ import ( type renamer struct { ctx context.Context + snapshot Snapshot refs []*ReferenceInfo objsToUpdate map[types.Object]bool hadConflicts bool @@ -125,15 +126,15 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot if err := checkRenamable(obj); err != nil { return nil, nil, err } - result, err := computePrepareRenameResp(snapshot, pkg, node, obj.Name()) + result, err := computePrepareRenameResp(ctx, snapshot, pkg, node, obj.Name()) if err != nil { return nil, nil, err } return result, nil, nil } -func computePrepareRenameResp(snapshot Snapshot, pkg Package, node ast.Node, text string) (*PrepareItem, error) { - mr, err := posToMappedRange(pkg, node.Pos(), node.End()) +func computePrepareRenameResp(ctx context.Context, snapshot Snapshot, pkg Package, node ast.Node, text string) (*PrepareItem, error) { + mr, err := posToMappedRange(ctx, snapshot, pkg, node.Pos(), node.End()) if err != nil { return nil, err } @@ -592,6 +593,7 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO } r := renamer{ ctx: ctx, + snapshot: s, refs: refs, objsToUpdate: make(map[types.Object]bool), from: obj.Name(), @@ -735,7 +737,7 @@ func (r *renamer) update() (map[span.URI][]diff.Edit, error) { // docComment returns the doc for an identifier. func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { - _, tokFile, nodes, _ := pathEnclosingInterval(pkg, id.Pos(), id.End()) + _, tokFile, nodes, _ := pathEnclosingInterval(r.ctx, r.snapshot, pkg, id.Pos(), id.End()) for _, node := range nodes { switch decl := node.(type) { case *ast.FuncDecl: @@ -788,7 +790,7 @@ func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.Edit, error) { // Modify ImportSpec syntax to add or remove the Name as needed. pkg := r.packages[pkgName.Pkg()] - _, tokFile, path, _ := pathEnclosingInterval(pkg, pkgName.Pos(), pkgName.Pos()) + _, tokFile, path, _ := pathEnclosingInterval(r.ctx, r.snapshot, pkg, pkgName.Pos(), pkgName.Pos()) if len(path) < 2 { return nil, fmt.Errorf("no path enclosing interval for %s", pkgName.Name()) } diff --git a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go index 0285fc0c9de..e442dae8d58 100644 --- a/gopls/internal/lsp/source/rename_check.go +++ b/gopls/internal/lsp/source/rename_check.go @@ -7,6 +7,7 @@ package source import ( + "context" "fmt" "go/ast" "go/token" @@ -371,7 +372,7 @@ func (r *renamer) checkStructField(from *types.Var) { if !ok { return } - pkg, _, path, _ := pathEnclosingInterval(fromPkg, from.Pos(), from.Pos()) + pkg, _, path, _ := pathEnclosingInterval(r.ctx, r.snapshot, fromPkg, from.Pos(), from.Pos()) if pkg == nil || path == nil { return } @@ -790,10 +791,10 @@ func (r *renamer) satisfy() map[satisfy.Constraint]bool { // type-checker. // // Only proceed if all packages have no errors. - if pkg.HasListOrParseErrors() || pkg.HasTypeErrors() { + if pkg.HasParseErrors() || pkg.HasTypeErrors() { r.errorf(token.NoPos, // we don't have a position for this error. "renaming %q to %q not possible because %q has errors", - r.from, r.to, pkg.PkgPath()) + r.from, r.to, pkg.Metadata().PkgPath) return nil } f.Find(pkg.GetTypesInfo(), pkg.GetSyntax()) @@ -826,7 +827,9 @@ func someUse(info *types.Info, obj types.Object) *ast.Ident { // exact is defined as for astutil.PathEnclosingInterval. // // The zero value is returned if not found. -func pathEnclosingInterval(pkg Package, start, end token.Pos) (resPkg Package, tokFile *token.File, path []ast.Node, exact bool) { +// +// TODO(rfindley): this has some redundancy with FindPackageFromPos, etc. Refactor. +func pathEnclosingInterval(ctx context.Context, s Snapshot, pkg Package, start, end token.Pos) (resPkg Package, tokFile *token.File, path []ast.Node, exact bool) { pkgs := []Package{pkg} for _, f := range pkg.GetSyntax() { for _, imp := range f.Imports { @@ -837,7 +840,7 @@ func pathEnclosingInterval(pkg Package, start, end token.Pos) (resPkg Package, t if importPath == "" { continue } - imported, err := pkg.ResolveImportPath(importPath) + imported, err := ResolveImportPath(ctx, s, pkg.Metadata().ID, importPath) if err != nil { return nil, nil, nil, false } diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index 3f81c27b053..e6554814924 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -94,7 +94,7 @@ FindCall: comment *ast.CommentGroup ) if obj != nil { - declPkg, err := FindPackageFromPos(pkg, obj.Pos()) + declPkg, err := FindPackageFromPos(ctx, snapshot, pkg, obj.Pos()) if err != nil { return nil, 0, err } diff --git a/gopls/internal/lsp/source/types_format.go b/gopls/internal/lsp/source/types_format.go index e3a884d8542..7a2605c72bd 100644 --- a/gopls/internal/lsp/source/types_format.go +++ b/gopls/internal/lsp/source/types_format.go @@ -206,7 +206,7 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa params := make([]string, 0, sig.Params().Len()) for i := 0; i < sig.Params().Len(); i++ { el := sig.Params().At(i) - typ := FormatVarType(pkg, el, qf) + typ := FormatVarType(ctx, s, pkg, el, qf) p := typ if el.Name() != "" { p = el.Name() + " " + typ @@ -221,7 +221,7 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa needResultParens = true } el := sig.Results().At(i) - typ := FormatVarType(pkg, el, qf) + typ := FormatVarType(ctx, s, pkg, el, qf) if el.Name() == "" { results = append(results, typ) } else { @@ -254,8 +254,8 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa // FormatVarType formats a *types.Var, accounting for type aliases. // To do this, it looks in the AST of the file in which the object is declared. // On any errors, it always falls back to types.TypeString. -func FormatVarType(srcpkg Package, obj *types.Var, qf types.Qualifier) string { - pkg, err := FindPackageFromPos(srcpkg, obj.Pos()) +func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj *types.Var, qf types.Qualifier) string { + pkg, err := FindPackageFromPos(ctx, snapshot, srcpkg, obj.Pos()) if err != nil { return types.TypeString(obj.Type(), qf) } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index ec4c6bdbea3..eacd9ce0be2 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -51,7 +51,7 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { return false } -func objToMappedRange(pkg Package, obj types.Object) (protocol.MappedRange, error) { +func objToMappedRange(ctx context.Context, snapshot Snapshot, pkg Package, obj types.Object) (protocol.MappedRange, error) { nameLen := len(obj.Name()) if pkgName, ok := obj.(*types.PkgName); ok { // An imported Go package has a package-local, unqualified name. @@ -68,7 +68,7 @@ func objToMappedRange(pkg Package, obj types.Object) (protocol.MappedRange, erro nameLen = len(pkgName.Imported().Path()) + len(`""`) } } - return posToMappedRange(pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen)) + return posToMappedRange(ctx, snapshot, pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen)) } // posToMappedRange returns the MappedRange for the given [start, end) span, @@ -77,7 +77,7 @@ func objToMappedRange(pkg Package, obj types.Object) (protocol.MappedRange, erro // TODO(adonovan): many of the callers need only the ParsedGoFile so // that they can call pgf.PosRange(pos, end) to get a Range; they // don't actually need a MappedRange. -func posToMappedRange(pkg Package, pos, end token.Pos) (protocol.MappedRange, error) { +func posToMappedRange(ctx context.Context, snapshot Snapshot, pkg Package, pos, end token.Pos) (protocol.MappedRange, error) { if !pos.IsValid() { return protocol.MappedRange{}, fmt.Errorf("invalid start position") } @@ -86,7 +86,7 @@ func posToMappedRange(pkg Package, pos, end token.Pos) (protocol.MappedRange, er } logicalFilename := pkg.FileSet().File(pos).Name() // ignore line directives - pgf, _, err := findFileInDeps(pkg, span.URIFromPath(logicalFilename)) + pgf, _, err := findFileInDeps(ctx, snapshot, pkg, span.URIFromPath(logicalFilename)) if err != nil { return protocol.MappedRange{}, err } @@ -99,13 +99,13 @@ func posToMappedRange(pkg Package, pos, end token.Pos) (protocol.MappedRange, er // TODO(rfindley): is this the best factoring of this API? This function is // really a trivial wrapper around findFileInDeps, which may be a more useful // function to expose. -func FindPackageFromPos(pkg Package, pos token.Pos) (Package, error) { +func FindPackageFromPos(ctx context.Context, snapshot Snapshot, pkg Package, pos token.Pos) (Package, error) { if !pos.IsValid() { return nil, fmt.Errorf("invalid position") } fileName := pkg.FileSet().File(pos).Name() uri := span.URIFromPath(fileName) - _, pkg, err := findFileInDeps(pkg, uri) + _, pkg, err := findFileInDeps(ctx, snapshot, pkg, uri) return pkg, err } @@ -223,25 +223,52 @@ func CompareDiagnostic(a, b *Diagnostic) int { } // findFileInDeps finds uri in pkg or its dependencies. -func findFileInDeps(pkg Package, uri span.URI) (*ParsedGoFile, Package, error) { - queue := []Package{pkg} - seen := make(map[PackageID]bool) - - for len(queue) > 0 { - pkg := queue[0] - queue = queue[1:] - seen[pkg.ID()] = true - +func findFileInDeps(ctx context.Context, snapshot Snapshot, pkg Package, uri span.URI) (*ParsedGoFile, Package, error) { + pkgs := []Package{pkg} + deps := recursiveDeps(snapshot, pkg.Metadata())[1:] + // Ignore the error from type checking, but check if the context was + // canceled (which would have caused TypeCheck to exit early). + depPkgs, _ := snapshot.TypeCheck(ctx, TypecheckWorkspace, deps...) + if ctx.Err() != nil { + return nil, nil, ctx.Err() + } + for _, dep := range depPkgs { + // Since we ignored the error from type checking, pkg may be nil. + if dep != nil { + pkgs = append(pkgs, dep) + } + } + for _, pkg := range pkgs { if pgf, err := pkg.File(uri); err == nil { return pgf, pkg, nil } - for _, dep := range pkg.Imports() { - if !seen[dep.ID()] { - queue = append(queue, dep) - } + } + return nil, nil, fmt.Errorf("no file for %s in deps of package %s", uri, pkg.Metadata().ID) +} + +// recursiveDeps finds unique transitive dependencies of m, including m itself. +// +// Invariant: for the resulting slice res, res[0] == m.ID. +// +// TODO(rfindley): consider replacing this with a snapshot.ForwardDependencies +// method, or exposing the metadata graph itself. +func recursiveDeps(s interface{ Metadata(PackageID) *Metadata }, m *Metadata) []PackageID { + seen := make(map[PackageID]bool) + var ids []PackageID + var add func(*Metadata) + add = func(m *Metadata) { + if seen[m.ID] { + return + } + seen[m.ID] = true + ids = append(ids, m.ID) + for _, dep := range m.DepsByPkgPath { + m := s.Metadata(dep) + add(m) } } - return nil, nil, fmt.Errorf("no file for %s in package %s", uri, pkg.ID()) + add(m) + return ids } // UnquoteImportPath returns the unquoted import path of s, @@ -463,3 +490,23 @@ func embeddedIdent(x ast.Expr) *ast.Ident { } return nil } + +// ResolveImportPath returns the directly imported dependency of the package with id fromID, +// given its ImportPath, type-checked in its workspace parse mode. +// +// TODO(rfindley): eliminate this function, in favor of inlining where it is used. +func ResolveImportPath(ctx context.Context, snapshot Snapshot, fromID PackageID, importPath ImportPath) (Package, error) { + meta := snapshot.Metadata(fromID) + if meta == nil { + return nil, fmt.Errorf("unknown package %s", fromID) + } + depID, ok := meta.DepsByImpPath[importPath] + if !ok { + return nil, fmt.Errorf("package does not import %s", importPath) + } + pkgs, err := snapshot.TypeCheck(ctx, TypecheckWorkspace, depID) + if err != nil { + return nil, fmt.Errorf("type checking dep: %v", err) + } + return pkgs[0], nil +} diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 81a91ccd9d1..3744e7c9fd1 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -17,7 +17,6 @@ import ( "io" "golang.org/x/mod/modfile" - "golang.org/x/mod/module" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/objectpath" @@ -171,7 +170,7 @@ type Snapshot interface { // // To reduce latency, it does not wait for type-checking to complete. // It is intended for use only in completions. - CachedImportPaths(ctx context.Context) (map[PackagePath]Package, error) + CachedImportPaths(ctx context.Context) (map[PackagePath]*types.Package, error) // ActiveMetadata returns a new, unordered slice containing // metadata for all packages considered 'active' in the workspace. @@ -460,9 +459,7 @@ type Metadata struct { DepsByPkgPath map[PackagePath]PackageID // values are unique and non-empty Module *packages.Module DepsErrors []*packagesinternal.PackageError - - // Config is the *packages.Config associated with the loaded package. - Config *packages.Config + LoadDir string // directory from which go/packages was run } // IsIntermediateTestVariant reports whether the given package is an @@ -750,13 +747,7 @@ type ( // Package represents a Go package that has been parsed and type-checked. // It maintains only the relevant fields of a *go/packages.Package. type Package interface { - // Metadata: - ID() PackageID - Name() PackageName - PkgPath() PackagePath - GetTypesSizes() types.Sizes - ForTest() string - Version() *module.Version + Metadata() *Metadata // Results of parsing: FileSet() *token.FileSet @@ -764,17 +755,14 @@ type Package interface { CompiledGoFiles() []*ParsedGoFile // (borrowed) File(uri span.URI) (*ParsedGoFile, error) GetSyntax() []*ast.File // (borrowed) - HasListOrParseErrors() bool + HasParseErrors() bool // Results of type checking: GetTypes() *types.Package GetTypesInfo() *types.Info - DirectDep(path PackagePath) (Package, error) - ResolveImportPath(path ImportPath) (Package, error) - Imports() []Package // new slice of all direct dependencies, unordered HasTypeErrors() bool - DiagnosticsForFile(uri span.URI) []*Diagnostic // new array of list/parse/type errors - ReferencesTo(map[PackagePath]map[objectpath.Path]unit) []protocol.Location // new sorted array of xrefs + DiagnosticsForFile(ctx context.Context, s Snapshot, uri span.URI) ([]*Diagnostic, error) + ReferencesTo(map[PackagePath]map[objectpath.Path]unit) []protocol.Location MethodSetsIndex() *methodsets.Index } diff --git a/gopls/internal/lsp/source/xrefs/xrefs.go b/gopls/internal/lsp/source/xrefs/xrefs.go index 5f781f78814..6a8b391e911 100644 --- a/gopls/internal/lsp/source/xrefs/xrefs.go +++ b/gopls/internal/lsp/source/xrefs/xrefs.go @@ -23,7 +23,7 @@ import ( // Index constructs a serializable index of outbound cross-references // for the specified type-checked package. -func Index(pkg source.Package) []byte { +func Index(files []*source.ParsedGoFile, pkg *types.Package, info *types.Info) []byte { // pkgObjects maps each referenced package Q to a mapping: // from each referenced symbol in Q to the ordered list // of references to that symbol from this package. @@ -41,7 +41,7 @@ func Index(pkg source.Package) []byte { return objects } - for fileIndex, pgf := range pkg.CompiledGoFiles() { + for fileIndex, pgf := range files { nodeRange := func(n ast.Node) protocol.Range { rng, err := pgf.PosRange(n.Pos(), n.End()) @@ -58,9 +58,9 @@ func Index(pkg source.Package) []byte { // uses a symbol exported from another package. // (The built-in error.Error method has no package.) if n.IsExported() { - if obj, ok := pkg.GetTypesInfo().Uses[n]; ok && + if obj, ok := info.Uses[n]; ok && obj.Pkg() != nil && - obj.Pkg() != pkg.GetTypes() { + obj.Pkg() != pkg { objects := getObjects(obj.Pkg()) gobObj, ok := objects[obj] @@ -87,9 +87,9 @@ func Index(pkg source.Package) []byte { // string to the imported package. var obj types.Object if n.Name != nil { - obj = pkg.GetTypesInfo().Defs[n.Name] + obj = info.Defs[n.Name] } else { - obj = pkg.GetTypesInfo().Implicits[n] + obj = info.Implicits[n] } if obj == nil { return true // missing import diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go index aafc970f1c2..1a464ca8a7e 100644 --- a/gopls/internal/regtest/bench/completion_test.go +++ b/gopls/internal/regtest/bench/completion_test.go @@ -7,7 +7,6 @@ package bench import ( "context" "fmt" - "strings" "testing" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -87,14 +86,12 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) { // the given file. func endRangeInBuffer(env *Env, name string) protocol.Range { buffer := env.BufferText(name) - lines := strings.Split(buffer, "\n") - numLines := len(lines) - - end := protocol.Position{ - Line: uint32(numLines - 1), - Character: uint32(len([]rune(lines[numLines-1]))), + m := protocol.NewMapper("", []byte(buffer)) + rng, err := m.OffsetRange(len(buffer), len(buffer)) + if err != nil { + env.T.Fatal(err) } - return protocol.Range{Start: end, End: end} + return rng } // Benchmark struct completion in tools codebase. diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 800274b5c29..f96a0aa04fa 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -862,6 +862,7 @@ package foo_ ` Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("foo/bar_test.go") + env.AfterChange() env.RegexpReplace("foo/bar_test.go", "package foo_", "package foo_test") env.AfterChange( NoDiagnostics(ForFile("foo/bar_test.go")), diff --git a/gopls/internal/regtest/diagnostics/golist_test.go b/gopls/internal/regtest/diagnostics/golist_test.go new file mode 100644 index 00000000000..ec54a92cef0 --- /dev/null +++ b/gopls/internal/regtest/diagnostics/golist_test.go @@ -0,0 +1,68 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diagnostics + +import ( + "testing" + + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/source" +) + +func TestGoListErrors(t *testing.T) { + const src = ` +-- go.mod -- +module a.com + +go 1.18 +-- a/a.go -- +package a + +import +-- c/c.go -- +package c + +/* +int fortythree() { return 42; } +*/ +import "C" + +func Foo() { + print(C.fortytwo()) +} +-- p/p.go -- +package p + +import "a.com/q" + +const P = q.Q + 1 +-- q/q.go -- +package q + +import "a.com/p" + +const Q = p.P + 1 +` + + Run(t, src, func(t *testing.T, env *Env) { + env.OnceMet( + InitialWorkspaceLoad, + Diagnostics( + env.AtRegexp("a/a.go", "import\n()"), + FromSource(string(source.ParseError)), + ), + Diagnostics( + AtPosition("c/c.go", 0, 0), + FromSource(string(source.ListError)), + WithMessage("may indicate failure to perform cgo processing"), + ), + Diagnostics( + env.AtRegexp("p/p.go", `"a.com/q"`), + FromSource(string(source.ListError)), + WithMessage("import cycle not allowed"), + ), + ) + }) +} diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go index dafcc62037b..013c4d160c4 100644 --- a/gopls/internal/regtest/misc/hover_test.go +++ b/gopls/internal/regtest/misc/hover_test.go @@ -204,6 +204,10 @@ func main() { env.OpenFile("main.go") for _, test := range tests { got, _ := env.Hover("main.go", env.RegexpSearch("main.go", test.hoverPackage)) + if got == nil { + t.Error("nil hover for", test.hoverPackage) + continue + } if !strings.Contains(got.Value, test.want) { t.Errorf("Hover: got:\n%q\nwant:\n%q", got.Value, test.want) } From f2cd9ef6a300737097ad965d76057455ff292d9a Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 24 Jan 2023 19:35:36 -0500 Subject: [PATCH 676/723] gopls/internal/lsp/source: reduce usage of TypecheckWorkspace Fix calls to TypeCheck using TypecheckWorkspace where we really want TypecheckFull. Also, use NarrowestPackage where it suffices. In approximate order of appearance: - Code actions, semantic tokens, code lens, and document highlights are all scoped to a file; the narrowest package for that file should suffice. - When completing at a position, we need the full package to find enclosing context. Furthermore, that file is open, and so will be fully type-checked by other operations. - Ditto for suggested fixes, inlay hints, and signature help. The current behavior leads to incorrect or missing functionality when outside the workspace. I did not add comprehensive tests demonstrating this in all cases, but added one for signature help. For golang/go#57987 Change-Id: I8270d0f0a0787e36bd4103378176d150426d37f2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463375 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley --- gopls/internal/lsp/code_action.go | 2 +- gopls/internal/lsp/regtest/wrappers.go | 3 + gopls/internal/lsp/semantic.go | 2 +- gopls/internal/lsp/source/code_lens.go | 2 +- .../lsp/source/completion/completion.go | 2 +- gopls/internal/lsp/source/fix.go | 24 ++----- gopls/internal/lsp/source/highlight.go | 2 +- gopls/internal/lsp/source/inlay_hint.go | 2 +- gopls/internal/lsp/source/signature_help.go | 4 +- gopls/internal/lsp/source/stub.go | 2 +- .../regtest/misc/signature_help_test.go | 69 +++++++++++++++++++ 11 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 gopls/internal/regtest/misc/signature_help_test.go diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index c91def60054..58bb59e16fc 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -158,7 +158,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara // Type-check the package and also run analysis, // then combine their diagnostics. - pkg, _, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.WidestPackage) + pkg, _, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.NarrowestPackage) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index 03f3551ee5f..c2062967ae7 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -131,6 +131,9 @@ func (e *Env) RegexpRange(name, re string) protocol.Range { // RegexpSearch returns the starting position of the first match for re in the // buffer specified by name, calling t.Fatal on any error. It first searches // for the position in open buffers, then in workspace files. +// +// TODO(rfindley): RegexpSearch should return a protocol.Location (but that is +// a large change). func (e *Env) RegexpSearch(name, re string) protocol.Position { e.T.Helper() pos, err := e.Editor.RegexpSearch(name, re) diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 93328773b99..0406311598c 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -93,7 +93,7 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu if kind != source.Go { return nil, nil } - pkg, pgf, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.WidestPackage) + pkg, pgf, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.NarrowestPackage) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go index a864981091d..dd66fe5f382 100644 --- a/gopls/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -100,7 +100,7 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) ( if !strings.HasSuffix(fh.URI().Filename(), "_test.go") { return out, nil } - pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, WidestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return out, err } diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 3343a1a5962..f27190c8915 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -430,7 +430,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan startTime := time.Now() - pkg, pgf, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckWorkspace, source.NarrowestPackage) + pkg, pgf, err := source.PackageForFile(ctx, snapshot, fh.URI(), source.TypecheckFull, source.NarrowestPackage) 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 diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 68e5f8ea38a..49bce04b6e0 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -53,12 +53,16 @@ var suggestedFixes = map[string]SuggestedFixFunc{ // singleFile calls analyzers that expect inputs for a single file func singleFile(sf singleFileFixFunc) SuggestedFixFunc { return func(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { - fset, rng, src, file, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return nil, nil, err } - fix, err := sf(fset, rng, src, file, pkg, info) - return fset, fix, err + rng, err := pgf.RangeToTokenRange(pRng) + if err != nil { + return nil, nil, err + } + fix, err := sf(pkg.FileSet(), rng, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()) + return pkg.FileSet(), fix, err } } @@ -130,17 +134,3 @@ func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh FileHandle, } return edits, nil } - -// getAllSuggestedFixInputs is a helper function to collect all possible needed -// inputs for an AppliesFunc or SuggestedFixFunc. -func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, safetoken.Range, []byte, *ast.File, *types.Package, *types.Info, error) { - pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) - if err != nil { - return nil, safetoken.Range{}, nil, nil, nil, nil, fmt.Errorf("getting file for Identifier: %w", err) - } - rng, err := pgf.RangeToTokenRange(pRng) - if err != nil { - return nil, safetoken.Range{}, nil, nil, nil, nil, err - } - return pkg.FileSet(), rng, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo(), nil -} diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index d0f77e62a8b..78718e57ea3 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -23,7 +23,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p // We always want fully parsed files for highlight, regardless // of whether the file belongs to a workspace package. - pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, WidestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return nil, fmt.Errorf("getting package for Highlight: %w", err) } diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go index 08fefa24158..da2e31a27c8 100644 --- a/gopls/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -82,7 +82,7 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng proto ctx, done := event.Start(ctx, "source.InlayHint") defer done() - pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return nil, fmt.Errorf("getting file for InlayHint: %w", err) } diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index e6554814924..1d43dc5407d 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -20,7 +20,9 @@ func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, positi ctx, done := event.Start(ctx, "source.SignatureHelp") defer done() - pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) + // We need full type-checking here, as we must type-check function bodies in + // order to provide signature help at the requested position. + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return nil, 0, fmt.Errorf("getting file for SignatureHelp: %w", err) } diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index 7b6f4f45137..c3f2661765d 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -26,7 +26,7 @@ import ( ) func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { - pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckWorkspace, NarrowestPackage) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return nil, nil, fmt.Errorf("GetTypedFile: %w", err) } diff --git a/gopls/internal/regtest/misc/signature_help_test.go b/gopls/internal/regtest/misc/signature_help_test.go new file mode 100644 index 00000000000..05f58300355 --- /dev/null +++ b/gopls/internal/regtest/misc/signature_help_test.go @@ -0,0 +1,69 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package misc + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" +) + +func TestSignatureHelpInNonWorkspacePackage(t *testing.T) { + const files = ` +-- a/go.mod -- +module a.com + +go 1.18 +-- a/a/a.go -- +package a + +func DoSomething(int) {} + +func _() { + DoSomething() +} +-- b/go.mod -- +module b.com +go 1.18 + +require a.com v1.0.0 + +replace a.com => ../a +-- b/b/b.go -- +package b + +import "a.com/a" + +func _() { + a.DoSomething() +} +` + + WithOptions( + WorkspaceFolders("a"), + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("a/a/a.go") + env.OpenFile("b/b/b.go") + signatureHelp := func(filename string) *protocol.SignatureHelp { + pos := env.RegexpSearch(filename, `DoSomething\(()\)`) + var params protocol.SignatureHelpParams + params.Position = pos + params.TextDocument.URI = env.Sandbox.Workdir.URI(filename) + help, err := env.Editor.Server.SignatureHelp(env.Ctx, ¶ms) + if err != nil { + t.Fatal(err) + } + return help + } + ahelp := signatureHelp("a/a/a.go") + bhelp := signatureHelp("b/b/b.go") + + if diff := cmp.Diff(ahelp, bhelp); diff != "" { + t.Fatal(diff) + } + }) +} From 031e6e6d060e1fb379ffe226224624c80ad34a7c Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 24 Jan 2023 20:05:07 -0500 Subject: [PATCH 677/723] gopls/internal/lsp/source: eliminate ResolveImportPath Following up on CL 461944, eliminate uses of ResolveImportPath. At two of the three callsites, we avoid type-checking. The one that remains is in renaming. For golang/go#57987 Change-Id: Ia974d39f2db72a1fe1373cff5faeb07ecb54effb Reviewed-on: https://go-review.googlesource.com/c/tools/+/463376 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/source/hover.go | 43 ++++++++++++++--------- gopls/internal/lsp/source/identifier.go | 34 +++++++++++++----- gopls/internal/lsp/source/rename_check.go | 8 +++-- gopls/internal/lsp/source/util.go | 22 ++---------- 4 files changed, 61 insertions(+), 46 deletions(-) diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index c27065662d2..e67e4d897f5 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -534,27 +534,38 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob } case *ast.ImportSpec: // Try to find the package documentation for an imported package. - importPath, err := strconv.Unquote(node.Path.Value) - if err != nil { - return nil, err + importPath := UnquoteImportPath(node) + impID := pkg.Metadata().DepsByImpPath[importPath] + if impID == "" { + return nil, fmt.Errorf("failed to resolve import %q", importPath) } - // TODO(rfindley): avoid type-checking here, by re-parsing the package with - // ParseHeader. - imp, err := ResolveImportPath(ctx, s, pkg.Metadata().ID, ImportPath(importPath)) - if err != nil { - return nil, err + impMetadata := s.Metadata(impID) + if impMetadata == nil { + return nil, fmt.Errorf("failed to resolve import ID %q", impID) } - // Assume that only one file will contain package documentation, - // so pick the first file that has a doc comment. - for _, file := range imp.GetSyntax() { - if file.Doc != nil { - info = &HoverContext{Comment: file.Doc} - if file.Name != nil { - info.signatureSource = "package " + file.Name.Name + for _, f := range impMetadata.CompiledGoFiles { + fh, err := s.GetFile(ctx, f) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() } - break + continue + } + pgf, err := s.ParseGo(ctx, fh, ParseHeader) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + continue + } + if pgf.File.Doc != nil { + return &HoverContext{ + Comment: pgf.File.Doc, + signatureSource: "package " + impMetadata.Name, + }, nil } } + case *ast.GenDecl: switch obj := obj.(type) { case *types.TypeName, *types.Var, *types.Const, *types.Func: diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 0be26b56540..d34fdf102fe 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -463,10 +463,7 @@ func importSpec(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Parsed if err != nil { return nil, fmt.Errorf("import path not quoted: %s (%v)", imp.Path.Value, err) } - imported, err := ResolveImportPath(ctx, snapshot, pkg.Metadata().ID, ImportPath(importPath)) - if err != nil { - return nil, err - } + result := &IdentifierInfo{ Snapshot: snapshot, Name: importPath, // should this perhaps be imported.PkgPath()? @@ -475,10 +472,31 @@ func importSpec(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Parsed if result.MappedRange, err = posToMappedRange(ctx, snapshot, pkg, imp.Path.Pos(), imp.Path.End()); err != nil { return nil, err } - // Consider the "declaration" of an import spec to be the imported package. - // Return all of the files in the package as the definition of the import spec. - for _, dst := range imported.GetSyntax() { - rng, err := posToMappedRange(ctx, snapshot, pkg, dst.Pos(), dst.End()) + + impID := pkg.Metadata().DepsByImpPath[ImportPath(importPath)] + if impID == "" { + return nil, fmt.Errorf("failed to resolve import %q", importPath) + } + impMetadata := snapshot.Metadata(impID) + if impMetadata == nil { + return nil, fmt.Errorf("failed to resolve import ID %q", impID) + } + for _, f := range impMetadata.CompiledGoFiles { + fh, err := snapshot.GetFile(ctx, f) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + continue + } + pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + continue + } + rng, err := pgf.PosMappedRange(pgf.File.Pos(), pgf.File.End()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go index e442dae8d58..5495e6a2259 100644 --- a/gopls/internal/lsp/source/rename_check.go +++ b/gopls/internal/lsp/source/rename_check.go @@ -840,11 +840,15 @@ func pathEnclosingInterval(ctx context.Context, s Snapshot, pkg Package, start, if importPath == "" { continue } - imported, err := ResolveImportPath(ctx, s, pkg.Metadata().ID, importPath) + depID, ok := pkg.Metadata().DepsByImpPath[importPath] + if !ok { + return nil, nil, nil, false + } + depPkgs, err := s.TypeCheck(ctx, TypecheckWorkspace, depID) if err != nil { return nil, nil, nil, false } - pkgs = append(pkgs, imported) + pkgs = append(pkgs, depPkgs[0]) } } for _, p := range pkgs { diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index eacd9ce0be2..7dab47b0f57 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -223,6 +223,8 @@ func CompareDiagnostic(a, b *Diagnostic) int { } // findFileInDeps finds uri in pkg or its dependencies. +// +// TODO(rfindley): eliminate this function. func findFileInDeps(ctx context.Context, snapshot Snapshot, pkg Package, uri span.URI) (*ParsedGoFile, Package, error) { pkgs := []Package{pkg} deps := recursiveDeps(snapshot, pkg.Metadata())[1:] @@ -490,23 +492,3 @@ func embeddedIdent(x ast.Expr) *ast.Ident { } return nil } - -// ResolveImportPath returns the directly imported dependency of the package with id fromID, -// given its ImportPath, type-checked in its workspace parse mode. -// -// TODO(rfindley): eliminate this function, in favor of inlining where it is used. -func ResolveImportPath(ctx context.Context, snapshot Snapshot, fromID PackageID, importPath ImportPath) (Package, error) { - meta := snapshot.Metadata(fromID) - if meta == nil { - return nil, fmt.Errorf("unknown package %s", fromID) - } - depID, ok := meta.DepsByImpPath[importPath] - if !ok { - return nil, fmt.Errorf("package does not import %s", importPath) - } - pkgs, err := snapshot.TypeCheck(ctx, TypecheckWorkspace, depID) - if err != nil { - return nil, fmt.Errorf("type checking dep: %v", err) - } - return pkgs[0], nil -} From 60782e9bdba7278c4c8cb796c5d2845d07d79f25 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 25 Jan 2023 14:35:38 -0500 Subject: [PATCH 678/723] gopls/internal/lsp/source: eliminate a couple uses of posToMappedRange Eliminate a couple uses of posToMappedRange, which potentially type-checks, where it is clearly unnecessary. Also improve test output for highlight. Updates golang/go#57987 Updates golang/go#54845 Change-Id: I5580bf6431def0a6ee635e394932934ec7fe1afb Reviewed-on: https://go-review.googlesource.com/c/tools/+/463556 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan Run-TryBot: Robert Findley --- gopls/internal/lsp/lsp_test.go | 53 +++++++++++++++---------- gopls/internal/lsp/source/highlight.go | 42 ++++++++++---------- gopls/internal/lsp/source/references.go | 7 +++- 3 files changed, 58 insertions(+), 44 deletions(-) diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index eb5baf95ab0..212ba0f3958 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -771,45 +771,56 @@ func (r *runner) Implementation(t *testing.T, spn span.Span, wantSpans []span.Sp } } -func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) { +func (r *runner) Highlight(t *testing.T, src span.Span, spans []span.Span) { m, err := r.data.Mapper(src.URI()) if err != nil { t.Fatal(err) } loc, err := m.SpanLocation(src) if err != nil { - t.Fatalf("failed for %v: %v", locations[0], err) + t.Fatal(err) } tdpp := protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, + TextDocument: protocol.TextDocumentIdentifier{ + URI: loc.URI, + }, + Position: loc.Range.Start, + } + if err != nil { + t.Fatalf("Mapper.SpanDocumentPosition(%v) failed: %v", src, err) } params := &protocol.DocumentHighlightParams{ TextDocumentPositionParams: tdpp, } highlights, err := r.server.DocumentHighlight(r.ctx, params) if err != nil { - t.Fatal(err) + t.Fatalf("DocumentHighlight(%v) failed: %v", params, err) } - if len(highlights) != len(locations) { - t.Fatalf("got %d highlights for highlight at %v:%v:%v, expected %d", len(highlights), src.URI().Filename(), src.Start().Line(), src.Start().Column(), len(locations)) + var got []protocol.Range + for _, h := range highlights { + got = append(got, h.Range) } - // Check to make sure highlights have a valid range. - var results []span.Span - for i := range highlights { - h, err := m.RangeSpan(highlights[i].Range) + + var want []protocol.Range + for _, s := range spans { + rng, err := m.SpanRange(s) if err != nil { - t.Fatalf("failed for %v: %v", highlights[i], err) - } - results = append(results, h) - } - // Sort results to make tests deterministic since DocumentHighlight uses a map. - span.SortSpans(results) - // Check to make sure all the expected highlights are found. - for i := range results { - if results[i] != locations[i] { - t.Errorf("want %v, got %v\n", locations[i], results[i]) + t.Fatalf("Mapper.SpanRange(%v) failed: %v", s, err) } + want = append(want, rng) + } + + sortRanges := func(s []protocol.Range) { + sort.Slice(s, func(i, j int) bool { + return protocol.CompareRange(s[i], s[j]) < 0 + }) + } + + sortRanges(got) + sortRanges(want) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("DocumentHighlight(%v) mismatch (-want +got):\n%s", src, diff) } } diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go index 78718e57ea3..e2f6c84e8cb 100644 --- a/gopls/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -48,28 +48,28 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p } } } - result, err := highlightPath(pkg, path) + result, err := highlightPath(path, pgf.File, pkg.GetTypesInfo()) if err != nil { return nil, err } var ranges []protocol.Range for rng := range result { - mRng, err := posToMappedRange(ctx, snapshot, pkg, rng.start, rng.end) + rng, err := pgf.PosRange(rng.start, rng.end) if err != nil { return nil, err } - ranges = append(ranges, mRng.Range()) + ranges = append(ranges, rng) } return ranges, nil } -func highlightPath(pkg Package, path []ast.Node) (map[posRange]struct{}, error) { +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) { case *ast.BasicLit: if len(path) > 1 { if _, ok := path[1].(*ast.ImportSpec); ok { - err := highlightImportUses(pkg, path, result) + err := highlightImportUses(path, info, result) return result, err } } @@ -77,7 +77,9 @@ func highlightPath(pkg Package, path []ast.Node) (map[posRange]struct{}, error) case *ast.ReturnStmt, *ast.FuncDecl, *ast.FuncType: highlightFuncControlFlow(path, result) case *ast.Ident: - highlightIdentifiers(pkg, path, result) + // Check if ident is inside return or func decl. + highlightFuncControlFlow(path, result) + highlightIdentifier(node, file, info, result) case *ast.ForStmt, *ast.RangeStmt: highlightLoopControlFlow(path, result) case *ast.SwitchStmt: @@ -426,7 +428,7 @@ func labelDecl(n *ast.Ident) *ast.Ident { return stmt.Label } -func highlightImportUses(pkg Package, path []ast.Node, result map[posRange]struct{}) error { +func highlightImportUses(path []ast.Node, info *types.Info, result map[posRange]struct{}) error { basicLit, ok := path[0].(*ast.BasicLit) if !ok { return fmt.Errorf("highlightImportUses called with an ast.Node of type %T", basicLit) @@ -440,7 +442,7 @@ func highlightImportUses(pkg Package, path []ast.Node, result map[posRange]struc if !ok { return true } - obj, ok := pkg.GetTypesInfo().ObjectOf(n).(*types.PkgName) + obj, ok := info.ObjectOf(n).(*types.PkgName) if !ok { return true } @@ -453,19 +455,16 @@ func highlightImportUses(pkg Package, path []ast.Node, result map[posRange]struc return nil } -func highlightIdentifiers(pkg Package, path []ast.Node, result map[posRange]struct{}) error { - id, ok := path[0].(*ast.Ident) - if !ok { - return fmt.Errorf("highlightIdentifiers called with an ast.Node of type %T", id) - } - // Check if ident is inside return or func decl. - highlightFuncControlFlow(path, result) - - // TODO: maybe check if ident is a reserved word, if true then don't continue and return results. - - idObj := pkg.GetTypesInfo().ObjectOf(id) +func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result map[posRange]struct{}) { + // TODO(rfindley): idObj may be nil. Note that returning early in this case + // causes tests to fail (because the nObj == idObj check below was succeeded + // for nil == nil!) + // + // Revisit this. If ObjectOf is nil, there are type errors, and it seems + // reasonable for identifier highlighting not to work. + idObj := info.ObjectOf(id) pkgObj, isImported := idObj.(*types.PkgName) - ast.Inspect(path[len(path)-1], func(node ast.Node) bool { + ast.Inspect(file, func(node ast.Node) bool { if imp, ok := node.(*ast.ImportSpec); ok && isImported { highlightImport(pkgObj, imp, result) } @@ -476,12 +475,11 @@ func highlightIdentifiers(pkg Package, path []ast.Node, result map[posRange]stru if n.Name != id.Name { return false } - if nObj := pkg.GetTypesInfo().ObjectOf(n); nObj == idObj { + if nObj := info.ObjectOf(n); nObj == idObj { result[posRange{start: n.Pos(), end: n.End()}] = struct{}{} } return false }) - return nil } func highlightImport(obj *types.PkgName, imp *ast.ImportSpec, result map[posRange]struct{}) { diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 1fc7fde78b9..5f3fdbb3d34 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -141,7 +141,12 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i continue } seen[key] = true - rng, err := posToMappedRange(ctx, snapshot, pkg, ident.Pos(), ident.End()) + filename := pkg.FileSet().File(ident.Pos()).Name() + pgf, err := pkg.File(span.URIFromPath(filename)) + if err != nil { + return nil, err + } + rng, err := pgf.PosMappedRange(ident.Pos(), ident.End()) if err != nil { return nil, err } From bcb677e49c1f32930d563a19f74099af15c268fd Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 26 Jan 2023 09:16:46 -0500 Subject: [PATCH 679/723] gopls/internal/regtest: make RegexpSearch return a Location ...along with various other Editor methods. Do we need a more convenient way to say env.Sandbox.Workdir.URIToPath(loc.URI) ? Change-Id: I452028db4b99843e07861909ad8cef87cf9fb118 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463655 Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/fake/editor.go | 179 +++++++++--------- gopls/internal/lsp/fake/workdir.go | 16 +- gopls/internal/lsp/regtest/expectation.go | 4 +- gopls/internal/lsp/regtest/wrappers.go | 71 +++---- .../internal/regtest/bench/completion_test.go | 6 +- .../regtest/bench/editor_features_test.go | 24 +-- .../regtest/completion/completion18_test.go | 12 +- .../regtest/completion/completion_test.go | 60 +++--- .../completion/postfix_snippet_test.go | 6 +- .../regtest/diagnostics/builtin_test.go | 6 +- .../regtest/misc/call_hierarchy_test.go | 6 +- .../internal/regtest/misc/definition_test.go | 60 +++--- gopls/internal/regtest/misc/extract_test.go | 10 +- gopls/internal/regtest/misc/failures_test.go | 2 +- gopls/internal/regtest/misc/fix_test.go | 6 +- gopls/internal/regtest/misc/highlight_test.go | 31 +-- gopls/internal/regtest/misc/hover_test.go | 31 +-- gopls/internal/regtest/misc/imports_test.go | 3 +- gopls/internal/regtest/misc/link_test.go | 8 +- .../regtest/misc/multiple_adhoc_test.go | 6 +- .../internal/regtest/misc/references_test.go | 32 ++-- gopls/internal/regtest/misc/rename_test.go | 59 +++--- .../regtest/misc/signature_help_test.go | 6 +- gopls/internal/regtest/misc/vuln_test.go | 12 +- .../internal/regtest/modfile/modfile_test.go | 10 +- .../regtest/template/template_test.go | 10 +- .../internal/regtest/workspace/broken_test.go | 2 +- .../regtest/workspace/metadata_test.go | 3 +- .../regtest/workspace/standalone_test.go | 24 ++- .../regtest/workspace/workspace_test.go | 47 +++-- 30 files changed, 361 insertions(+), 391 deletions(-) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index c9214a8aa6d..9a1f0dd7de3 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -39,6 +39,7 @@ type Editor struct { sandbox *Sandbox defaultEnv map[string]string + // TODO(adonovan): buffers should be keyed by protocol.DocumentURI. mu sync.Mutex config EditorConfig // editor configuration buffers map[string]buffer // open buffers (relative path -> buffer content) @@ -597,17 +598,17 @@ var ( ErrUnknownBuffer = errors.New("unknown buffer") ) -// regexpRange returns the start and end of the first occurrence of either re +// regexpLocation returns the location of the first occurrence of either re // or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match. -func regexpRange(mapper *protocol.Mapper, re string) (protocol.Range, error) { +func regexpLocation(mapper *protocol.Mapper, re string) (protocol.Location, error) { var start, end int rec, err := regexp.Compile(re) if err != nil { - return protocol.Range{}, err + return protocol.Location{}, err } indexes := rec.FindSubmatchIndex(mapper.Content) if indexes == nil { - return protocol.Range{}, ErrNoMatch + return protocol.Location{}, ErrNoMatch } switch len(indexes) { case 2: @@ -617,33 +618,26 @@ func regexpRange(mapper *protocol.Mapper, re string) (protocol.Range, error) { // one subgroup: return its range start, end = indexes[2], indexes[3] default: - return protocol.Range{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1) + return protocol.Location{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1) } - return mapper.OffsetRange(start, end) + return mapper.OffsetLocation(start, end) } -// RegexpRange returns the first range in the buffer bufName matching re. See -// RegexpSearch for more information on matching. -func (e *Editor) RegexpRange(bufName, re string) (protocol.Range, error) { - e.mu.Lock() - defer e.mu.Unlock() - buf, ok := e.buffers[bufName] - if !ok { - return protocol.Range{}, ErrUnknownBuffer - } - return regexpRange(buf.mapper, re) -} - -// RegexpSearch returns the position of the first match for re in the buffer +// RegexpSearch returns the Location of the first match for re in the buffer // bufName. For convenience, RegexpSearch supports the following two modes: // 1. If re has no subgroups, return the position of the match for re itself. // 2. If re has one subgroup, return the position of the first subgroup. // // It returns an error re is invalid, has more than one subgroup, or doesn't // match the buffer. -func (e *Editor) RegexpSearch(bufName, re string) (protocol.Position, error) { - rng, err := e.RegexpRange(bufName, re) - return rng.Start, err +func (e *Editor) RegexpSearch(bufName, re string) (protocol.Location, error) { + e.mu.Lock() + buf, ok := e.buffers[bufName] + e.mu.Unlock() + if !ok { + return protocol.Location{}, ErrUnknownBuffer + } + return regexpLocation(buf.mapper, re) } // RegexpReplace edits the buffer corresponding to path by replacing the first @@ -658,12 +652,12 @@ func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) er if !ok { return ErrUnknownBuffer } - rng, err := regexpRange(buf.mapper, re) + loc, err := regexpLocation(buf.mapper, re) if err != nil { return err } edits := []protocol.TextEdit{{ - Range: rng, + Range: loc.Range, NewText: replace, }} patched, err := applyEdits(buf.mapper, edits, e.config.WindowsLineEndings) @@ -765,53 +759,53 @@ func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty } // GoToDefinition jumps to the definition of the symbol at the given position -// in an open buffer. It returns the path and position of the resulting jump. -func (e *Editor) GoToDefinition(ctx context.Context, path string, pos protocol.Position) (string, protocol.Position, error) { - if err := e.checkBufferPosition(path, pos); err != nil { - return "", protocol.Position{}, err +// in an open buffer. It returns the location of the resulting jump. +func (e *Editor) GoToDefinition(ctx context.Context, loc protocol.Location) (protocol.Location, error) { + if err := e.checkBufferLocation(loc); err != nil { + return protocol.Location{}, err } params := &protocol.DefinitionParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos + params.TextDocument.URI = loc.URI + params.Position = loc.Range.Start resp, err := e.Server.Definition(ctx, params) if err != nil { - return "", protocol.Position{}, fmt.Errorf("definition: %w", err) + return protocol.Location{}, fmt.Errorf("definition: %w", err) } - return e.extractFirstPathAndPos(ctx, resp) + return e.extractFirstLocation(ctx, resp) } -// GoToTypeDefinition jumps to the type definition of the symbol at the given position +// GoToTypeDefinition jumps to the type definition of the symbol at the given location // in an open buffer. -func (e *Editor) GoToTypeDefinition(ctx context.Context, path string, pos protocol.Position) (string, protocol.Position, error) { - if err := e.checkBufferPosition(path, pos); err != nil { - return "", protocol.Position{}, err +func (e *Editor) GoToTypeDefinition(ctx context.Context, loc protocol.Location) (protocol.Location, error) { + if err := e.checkBufferLocation(loc); err != nil { + return protocol.Location{}, err } params := &protocol.TypeDefinitionParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos + params.TextDocument.URI = loc.URI + params.Position = loc.Range.Start resp, err := e.Server.TypeDefinition(ctx, params) if err != nil { - return "", protocol.Position{}, fmt.Errorf("type definition: %w", err) + return protocol.Location{}, fmt.Errorf("type definition: %w", err) } - return e.extractFirstPathAndPos(ctx, resp) + return e.extractFirstLocation(ctx, resp) } -// extractFirstPathAndPos returns the path and the position of the first location. +// extractFirstPathAndPos returns the first location. // It opens the file if needed. -func (e *Editor) extractFirstPathAndPos(ctx context.Context, locs []protocol.Location) (string, protocol.Position, error) { +func (e *Editor) extractFirstLocation(ctx context.Context, locs []protocol.Location) (protocol.Location, error) { if len(locs) == 0 { - return "", protocol.Position{}, nil + return protocol.Location{}, nil } newPath := e.sandbox.Workdir.URIToPath(locs[0].URI) if !e.HasBuffer(newPath) { if err := e.OpenFile(ctx, newPath); err != nil { - return "", protocol.Position{}, fmt.Errorf("OpenFile: %w", err) + return protocol.Location{}, fmt.Errorf("OpenFile: %w", err) } } - return newPath, locs[0].Range.Start, nil + return locs[0], nil } // Symbol performs a workspace symbol search using query @@ -822,13 +816,14 @@ func (e *Editor) Symbol(ctx context.Context, query string) ([]protocol.SymbolInf // OrganizeImports requests and performs the source.organizeImports codeAction. func (e *Editor) OrganizeImports(ctx context.Context, path string) error { - _, err := e.applyCodeActions(ctx, path, nil, nil, protocol.SourceOrganizeImports) + loc := protocol.Location{URI: e.sandbox.Workdir.URI(path)} // zero Range => whole file + _, err := e.applyCodeActions(ctx, loc, nil, protocol.SourceOrganizeImports) return err } // RefactorRewrite requests and performs the source.refactorRewrite codeAction. -func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error { - applied, err := e.applyCodeActions(ctx, path, rng, nil, protocol.RefactorRewrite) +func (e *Editor) RefactorRewrite(ctx context.Context, loc protocol.Location) error { + applied, err := e.applyCodeActions(ctx, loc, nil, protocol.RefactorRewrite) if err != nil { return err } @@ -839,8 +834,8 @@ func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol } // ApplyQuickFixes requests and performs the quickfix codeAction. -func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) error { - applied, err := e.applyCodeActions(ctx, path, rng, diagnostics, protocol.SourceFixAll, protocol.QuickFix) +func (e *Editor) ApplyQuickFixes(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) error { + applied, err := e.applyCodeActions(ctx, loc, diagnostics, protocol.SourceFixAll, protocol.QuickFix) if applied == 0 { return fmt.Errorf("no quick fixes were applied") } @@ -876,12 +871,12 @@ 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, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { - return e.getCodeActions(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll) +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) } -func (e *Editor) applyCodeActions(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) (int, error) { - actions, err := e.getCodeActions(ctx, path, rng, diagnostics, only...) +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...) if err != nil { return 0, err } @@ -908,19 +903,17 @@ func (e *Editor) applyCodeActions(ctx context.Context, path string, rng *protoco return applied, nil } -func (e *Editor) getCodeActions(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) { +func (e *Editor) getCodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) { if e.Server == nil { return nil, nil } params := &protocol.CodeActionParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) + params.TextDocument.URI = loc.URI params.Context.Only = only + params.Range = loc.Range // may be zero => whole file if diagnostics != nil { params.Context.Diagnostics = diagnostics } - if rng != nil { - params.Range = *rng - } return e.Server.CodeAction(ctx, params) } @@ -976,15 +969,16 @@ func (e *Editor) FormatBuffer(ctx context.Context, path string) error { return e.editBufferLocked(ctx, path, edits) } -func (e *Editor) checkBufferPosition(path string, pos protocol.Position) error { +func (e *Editor) checkBufferLocation(loc protocol.Location) error { e.mu.Lock() defer e.mu.Unlock() + path := e.sandbox.Workdir.URIToPath(loc.URI) buf, ok := e.buffers[path] if !ok { return fmt.Errorf("buffer %q is not open", path) } - _, err := buf.mapper.PositionOffset(pos) + _, _, err := buf.mapper.RangeOffsets(loc.Range) return err } @@ -1041,10 +1035,11 @@ func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens } // Completion executes a completion request on the server. -func (e *Editor) Completion(ctx context.Context, path string, pos protocol.Position) (*protocol.CompletionList, error) { +func (e *Editor) Completion(ctx context.Context, loc protocol.Location) (*protocol.CompletionList, error) { if e.Server == nil { return nil, nil } + path := e.sandbox.Workdir.URIToPath(loc.URI) e.mu.Lock() _, ok := e.buffers[path] e.mu.Unlock() @@ -1054,7 +1049,7 @@ func (e *Editor) Completion(ctx context.Context, path string, pos protocol.Posit params := &protocol.CompletionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocument: e.TextDocumentIdentifier(path), - Position: pos, + Position: loc.Range.Start, }, } completions, err := e.Server.Completion(ctx, params) @@ -1066,12 +1061,13 @@ func (e *Editor) Completion(ctx context.Context, path string, pos protocol.Posit // AcceptCompletion accepts a completion for the given item at the given // position. -func (e *Editor) AcceptCompletion(ctx context.Context, path string, pos protocol.Position, item protocol.CompletionItem) error { +func (e *Editor) AcceptCompletion(ctx context.Context, loc protocol.Location, item protocol.CompletionItem) error { if e.Server == nil { return nil } e.mu.Lock() defer e.mu.Unlock() + path := e.sandbox.Workdir.URIToPath(loc.URI) _, ok := e.buffers[path] if !ok { return fmt.Errorf("buffer %q is not open", path) @@ -1112,12 +1108,13 @@ func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHi return hints, nil } -// References returns references to the object at (path, pos), as returned by +// References returns references to the object at loc, as returned by // the connected LSP server. If no server is connected, it returns (nil, nil). -func (e *Editor) References(ctx context.Context, path string, pos protocol.Position) ([]protocol.Location, error) { +func (e *Editor) References(ctx context.Context, loc protocol.Location) ([]protocol.Location, error) { if e.Server == nil { return nil, nil } + path := e.sandbox.Workdir.URIToPath(loc.URI) e.mu.Lock() _, ok := e.buffers[path] e.mu.Unlock() @@ -1127,7 +1124,7 @@ func (e *Editor) References(ctx context.Context, path string, pos protocol.Posit params := &protocol.ReferenceParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocument: e.TextDocumentIdentifier(path), - Position: pos, + Position: loc.Range.Start, }, Context: protocol.ReferenceContext{ IncludeDeclaration: true, @@ -1140,24 +1137,25 @@ func (e *Editor) References(ctx context.Context, path string, pos protocol.Posit return locations, nil } -// Rename performs a rename of the object at (path, pos) to newName, using the +// Rename performs a rename of the object at loc to newName, using the // connected LSP server. If no server is connected, it returns nil. -func (e *Editor) Rename(ctx context.Context, path string, pos protocol.Position, newName string) error { +func (e *Editor) Rename(ctx context.Context, loc protocol.Location, newName string) error { if e.Server == nil { return nil } + path := e.sandbox.Workdir.URIToPath(loc.URI) // Verify that PrepareRename succeeds. prepareParams := &protocol.PrepareRenameParams{} prepareParams.TextDocument = e.TextDocumentIdentifier(path) - prepareParams.Position = pos + prepareParams.Position = loc.Range.Start if _, err := e.Server.PrepareRename(ctx, prepareParams); err != nil { return fmt.Errorf("preparing rename: %v", err) } params := &protocol.RenameParams{ TextDocument: e.TextDocumentIdentifier(path), - Position: pos, + Position: loc.Range.Start, NewName: newName, } wsEdits, err := e.Server.Rename(ctx, params) @@ -1172,13 +1170,14 @@ func (e *Editor) Rename(ctx context.Context, path string, pos protocol.Position, return nil } -// Implementations returns implementations for the object at (path, pos), as +// Implementations returns implementations for the object at loc, as // returned by the connected LSP server. If no server is connected, it returns // (nil, nil). -func (e *Editor) Implementations(ctx context.Context, path string, pos protocol.Position) ([]protocol.Location, error) { +func (e *Editor) Implementations(ctx context.Context, loc protocol.Location) ([]protocol.Location, error) { if e.Server == nil { return nil, nil } + path := e.sandbox.Workdir.URIToPath(loc.URI) e.mu.Lock() _, ok := e.buffers[path] e.mu.Unlock() @@ -1188,7 +1187,7 @@ func (e *Editor) Implementations(ctx context.Context, path string, pos protocol. params := &protocol.ImplementationParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocument: e.TextDocumentIdentifier(path), - Position: pos, + Position: loc.Range.Start, }, } return e.Server.Implementation(ctx, params) @@ -1368,10 +1367,12 @@ func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) e } // CodeAction executes a codeAction request on the server. -func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { +// If loc.Range is zero, the whole file is implied. +func (e *Editor) CodeAction(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { if e.Server == nil { return nil, nil } + path := e.sandbox.Workdir.URIToPath(loc.URI) e.mu.Lock() _, ok := e.buffers[path] e.mu.Unlock() @@ -1383,9 +1384,7 @@ func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Rang Context: protocol.CodeActionContext{ Diagnostics: diagnostics, }, - } - if rng != nil { - params.Range = *rng + Range: loc.Range, // may be zero } lens, err := e.Server.CodeAction(ctx, params) if err != nil { @@ -1395,22 +1394,22 @@ func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Rang } // Hover triggers a hover at the given position in an open buffer. -func (e *Editor) Hover(ctx context.Context, path string, pos protocol.Position) (*protocol.MarkupContent, protocol.Position, error) { - if err := e.checkBufferPosition(path, pos); err != nil { - return nil, protocol.Position{}, err +func (e *Editor) Hover(ctx context.Context, loc protocol.Location) (*protocol.MarkupContent, protocol.Location, error) { + if err := e.checkBufferLocation(loc); err != nil { + return nil, protocol.Location{}, err } params := &protocol.HoverParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos + params.TextDocument.URI = loc.URI + params.Position = loc.Range.Start resp, err := e.Server.Hover(ctx, params) if err != nil { - return nil, protocol.Position{}, fmt.Errorf("hover: %w", err) + return nil, protocol.Location{}, fmt.Errorf("hover: %w", err) } if resp == nil { - return nil, protocol.Position{}, nil + return nil, protocol.Location{}, nil } - return &resp.Contents, resp.Range.Start, nil + return &resp.Contents, protocol.Location{URI: loc.URI, Range: resp.Range}, nil } func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) { @@ -1422,16 +1421,16 @@ func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.Docu return e.Server.DocumentLink(ctx, params) } -func (e *Editor) DocumentHighlight(ctx context.Context, path string, pos protocol.Position) ([]protocol.DocumentHighlight, error) { +func (e *Editor) DocumentHighlight(ctx context.Context, loc protocol.Location) ([]protocol.DocumentHighlight, error) { if e.Server == nil { return nil, nil } - if err := e.checkBufferPosition(path, pos); err != nil { + if err := e.checkBufferLocation(loc); err != nil { return nil, err } params := &protocol.DocumentHighlightParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos + params.TextDocument.URI = loc.URI + params.Position = loc.Range.Start return e.Server.DocumentHighlight(ctx, params) } diff --git a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go index 2470e74e734..97d70b9cafb 100644 --- a/gopls/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -196,25 +196,15 @@ func (w *Workdir) ReadFile(path string) ([]byte, error) { } } -func (w *Workdir) RegexpRange(path, re string) (protocol.Range, error) { - content, err := w.ReadFile(path) - if err != nil { - return protocol.Range{}, err - } - mapper := protocol.NewMapper(w.URI(path).SpanURI(), content) - return regexpRange(mapper, re) -} - // RegexpSearch searches the file corresponding to path for the first position // matching re. -func (w *Workdir) RegexpSearch(path string, re string) (protocol.Position, error) { +func (w *Workdir) RegexpSearch(path string, re string) (protocol.Location, error) { content, err := w.ReadFile(path) if err != nil { - return protocol.Position{}, err + return protocol.Location{}, err } mapper := protocol.NewMapper(w.URI(path).SpanURI(), content) - rng, err := regexpRange(mapper, re) - return rng.Start, err + return regexpLocation(mapper, re) } // RemoveFile removes a workdir-relative file path and notifies watchers of the diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 6d3f3340a31..cba4c60639e 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -712,11 +712,11 @@ func FromSource(source string) DiagnosticFilter { // TODO(rfindley): pass in the editor to expectations, so that they may depend // on editor state and AtRegexp can be a function rather than a method. func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { - pos := e.RegexpSearch(name, pattern) + loc := e.RegexpSearch(name, pattern) return DiagnosticFilter{ desc: fmt.Sprintf("at the first position matching %#q in %q", pattern, name), check: func(diagName string, d protocol.Diagnostic) bool { - return diagName == name && d.Range.Start == pos + return diagName == name && d.Range.Start == loc.Range.Start }, } } diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index c2062967ae7..1207a1425af 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -113,37 +113,19 @@ func (e *Env) SetBufferContent(name string, content string) { } } -// RegexpRange returns the range of the first match for re in the buffer -// specified by name, calling t.Fatal on any error. It first searches for the -// position in open buffers, then in workspace files. -func (e *Env) RegexpRange(name, re string) protocol.Range { - e.T.Helper() - rng, err := e.Editor.RegexpRange(name, re) - if err == fake.ErrUnknownBuffer { - rng, err = e.Sandbox.Workdir.RegexpRange(name, re) - } - if err != nil { - e.T.Fatalf("RegexpRange: %v, %v", name, err) - } - return rng -} - // RegexpSearch returns the starting position of the first match for re in the // buffer specified by name, calling t.Fatal on any error. It first searches // for the position in open buffers, then in workspace files. -// -// TODO(rfindley): RegexpSearch should return a protocol.Location (but that is -// a large change). -func (e *Env) RegexpSearch(name, re string) protocol.Position { +func (e *Env) RegexpSearch(name, re string) protocol.Location { e.T.Helper() - pos, err := e.Editor.RegexpSearch(name, re) + loc, err := e.Editor.RegexpSearch(name, re) if err == fake.ErrUnknownBuffer { - pos, err = e.Sandbox.Workdir.RegexpSearch(name, re) + loc, err = e.Sandbox.Workdir.RegexpSearch(name, re) } if err != nil { e.T.Fatalf("RegexpSearch: %v, %v for %q", name, err, re) } - return pos + return loc } // RegexpReplace replaces the first group in the first match of regexpStr with @@ -172,13 +154,13 @@ func (e *Env) SaveBufferWithoutActions(name string) { // GoToDefinition goes to definition in the editor, calling t.Fatal on any // error. It returns the path and position of the resulting jump. -func (e *Env) GoToDefinition(name string, pos protocol.Position) (string, protocol.Position) { +func (e *Env) GoToDefinition(loc protocol.Location) protocol.Location { e.T.Helper() - n, p, err := e.Editor.GoToDefinition(e.Ctx, name, pos) + loc, err := e.Editor.GoToDefinition(e.Ctx, loc) if err != nil { e.T.Fatal(err) } - return n, p + return loc } // FormatBuffer formats the editor buffer, calling t.Fatal on any error. @@ -201,7 +183,8 @@ func (e *Env) OrganizeImports(name string) { // ApplyQuickFixes processes the quickfix codeAction, calling t.Fatal on any error. func (e *Env) ApplyQuickFixes(path string, diagnostics []protocol.Diagnostic) { e.T.Helper() - if err := e.Editor.ApplyQuickFixes(e.Ctx, path, nil, diagnostics); err != nil { + loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // zero Range => whole file + if err := e.Editor.ApplyQuickFixes(e.Ctx, loc, diagnostics); err != nil { e.T.Fatal(err) } } @@ -217,7 +200,8 @@ func (e *Env) ApplyCodeAction(action protocol.CodeAction) { // GetQuickFixes returns the available quick fix code actions. func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { e.T.Helper() - actions, err := e.Editor.GetQuickFixes(e.Ctx, path, nil, diagnostics) + loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // zero Range => whole file + actions, err := e.Editor.GetQuickFixes(e.Ctx, loc, diagnostics) if err != nil { e.T.Fatal(err) } @@ -225,13 +209,13 @@ func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) []pr } // Hover in the editor, calling t.Fatal on any error. -func (e *Env) Hover(name string, pos protocol.Position) (*protocol.MarkupContent, protocol.Position) { +func (e *Env) Hover(loc protocol.Location) (*protocol.MarkupContent, protocol.Location) { e.T.Helper() - c, p, err := e.Editor.Hover(e.Ctx, name, pos) + c, loc, err := e.Editor.Hover(e.Ctx, loc) if err != nil { e.T.Fatal(err) } - return c, p + return c, loc } func (e *Env) DocumentLink(name string) []protocol.DocumentLink { @@ -243,9 +227,9 @@ func (e *Env) DocumentLink(name string) []protocol.DocumentLink { return links } -func (e *Env) DocumentHighlight(name string, pos protocol.Position) []protocol.DocumentHighlight { +func (e *Env) DocumentHighlight(loc protocol.Location) []protocol.DocumentHighlight { e.T.Helper() - highlights, err := e.Editor.DocumentHighlight(e.Ctx, name, pos) + highlights, err := e.Editor.DocumentHighlight(e.Ctx, loc) if err != nil { e.T.Fatal(err) } @@ -398,9 +382,9 @@ func (e *Env) Symbol(query string) []protocol.SymbolInformation { } // References wraps Editor.References, calling t.Fatal on any error. -func (e *Env) References(path string, pos protocol.Position) []protocol.Location { +func (e *Env) References(loc protocol.Location) []protocol.Location { e.T.Helper() - locations, err := e.Editor.References(e.Ctx, path, pos) + locations, err := e.Editor.References(e.Ctx, loc) if err != nil { e.T.Fatal(err) } @@ -408,17 +392,17 @@ func (e *Env) References(path string, pos protocol.Position) []protocol.Location } // Rename wraps Editor.Rename, calling t.Fatal on any error. -func (e *Env) Rename(path string, pos protocol.Position, newName string) { +func (e *Env) Rename(loc protocol.Location, newName string) { e.T.Helper() - if err := e.Editor.Rename(e.Ctx, path, pos, newName); err != nil { + if err := e.Editor.Rename(e.Ctx, loc, newName); err != nil { e.T.Fatal(err) } } // Implementations wraps Editor.Implementations, calling t.Fatal on any error. -func (e *Env) Implementations(path string, pos protocol.Position) []protocol.Location { +func (e *Env) Implementations(loc protocol.Location) []protocol.Location { e.T.Helper() - locations, err := e.Editor.Implementations(e.Ctx, path, pos) + locations, err := e.Editor.Implementations(e.Ctx, loc) if err != nil { e.T.Fatal(err) } @@ -434,9 +418,9 @@ func (e *Env) RenameFile(oldPath, newPath string) { } // Completion executes a completion request on the server. -func (e *Env) Completion(path string, pos protocol.Position) *protocol.CompletionList { +func (e *Env) Completion(loc protocol.Location) *protocol.CompletionList { e.T.Helper() - completions, err := e.Editor.Completion(e.Ctx, path, pos) + completions, err := e.Editor.Completion(e.Ctx, loc) if err != nil { e.T.Fatal(err) } @@ -445,9 +429,9 @@ func (e *Env) Completion(path string, pos protocol.Position) *protocol.Completio // AcceptCompletion accepts a completion for the given item at the given // position. -func (e *Env) AcceptCompletion(path string, pos protocol.Position, item protocol.CompletionItem) { +func (e *Env) AcceptCompletion(loc protocol.Location, item protocol.CompletionItem) { e.T.Helper() - if err := e.Editor.AcceptCompletion(e.Ctx, path, pos, item); err != nil { + if err := e.Editor.AcceptCompletion(e.Ctx, loc, item); err != nil { e.T.Fatal(err) } } @@ -456,7 +440,8 @@ func (e *Env) AcceptCompletion(path string, pos protocol.Position, item protocol // t.Fatal if there are errors. func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { e.T.Helper() - actions, err := e.Editor.CodeAction(e.Ctx, path, nil, diagnostics) + loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // no Range => whole file + actions, err := e.Editor.CodeAction(e.Ctx, loc, diagnostics) if err != nil { e.T.Fatal(err) } diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go index 1a464ca8a7e..ffccf34b363 100644 --- a/gopls/internal/regtest/bench/completion_test.go +++ b/gopls/internal/regtest/bench/completion_test.go @@ -57,8 +57,8 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) { } // Run a completion to make sure the system is warm. - pos := env.RegexpSearch(options.file, options.locationRegexp) - completions := env.Completion(options.file, pos) + loc := env.RegexpSearch(options.file, options.locationRegexp) + completions := env.Completion(loc) if testing.Verbose() { fmt.Println("Results:") @@ -77,7 +77,7 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) { if options.beforeCompletion != nil { options.beforeCompletion(env) } - env.Completion(options.file, pos) + env.Completion(loc) } }) } diff --git a/gopls/internal/regtest/bench/editor_features_test.go b/gopls/internal/regtest/bench/editor_features_test.go index bba6baae1d7..ea6727b5c31 100644 --- a/gopls/internal/regtest/bench/editor_features_test.go +++ b/gopls/internal/regtest/bench/editor_features_test.go @@ -13,14 +13,14 @@ func BenchmarkGoToDefinition(b *testing.B) { env := benchmarkEnv(b) env.OpenFile("internal/imports/mod.go") - pos := env.RegexpSearch("internal/imports/mod.go", "ModuleJSON") - env.GoToDefinition("internal/imports/mod.go", pos) + loc := env.RegexpSearch("internal/imports/mod.go", "ModuleJSON") + env.GoToDefinition(loc) env.Await(env.DoneWithOpen()) b.ResetTimer() for i := 0; i < b.N; i++ { - env.GoToDefinition("internal/imports/mod.go", pos) + env.GoToDefinition(loc) } } @@ -28,14 +28,14 @@ func BenchmarkFindAllReferences(b *testing.B) { env := benchmarkEnv(b) env.OpenFile("internal/imports/mod.go") - pos := env.RegexpSearch("internal/imports/mod.go", "gopathwalk") - env.References("internal/imports/mod.go", pos) + loc := env.RegexpSearch("internal/imports/mod.go", "gopathwalk") + env.References(loc) env.Await(env.DoneWithOpen()) b.ResetTimer() for i := 0; i < b.N; i++ { - env.References("internal/imports/mod.go", pos) + env.References(loc) } } @@ -48,9 +48,9 @@ func BenchmarkRename(b *testing.B) { b.ResetTimer() for i := 1; i < b.N; i++ { - pos := env.RegexpSearch("internal/imports/mod.go", "gopathwalk") + loc := env.RegexpSearch("internal/imports/mod.go", "gopathwalk") newName := fmt.Sprintf("%s%d", "gopathwalk", i) - env.Rename("internal/imports/mod.go", pos, newName) + env.Rename(loc, newName) } } @@ -58,13 +58,13 @@ func BenchmarkFindAllImplementations(b *testing.B) { env := benchmarkEnv(b) env.OpenFile("internal/imports/mod.go") - pos := env.RegexpSearch("internal/imports/mod.go", "initAllMods") + loc := env.RegexpSearch("internal/imports/mod.go", "initAllMods") env.Await(env.DoneWithOpen()) b.ResetTimer() for i := 0; i < b.N; i++ { - env.Implementations("internal/imports/mod.go", pos) + env.Implementations(loc) } } @@ -72,12 +72,12 @@ func BenchmarkHover(b *testing.B) { env := benchmarkEnv(b) env.OpenFile("internal/imports/mod.go") - pos := env.RegexpSearch("internal/imports/mod.go", "bytes") + loc := env.RegexpSearch("internal/imports/mod.go", "bytes") env.Await(env.DoneWithOpen()) b.ResetTimer() for i := 0; i < b.N; i++ { - env.Hover("internal/imports/mod.go", pos) + env.Hover(loc) } } diff --git a/gopls/internal/regtest/completion/completion18_test.go b/gopls/internal/regtest/completion/completion18_test.go index b9edf06d5b1..18e81bc4b34 100644 --- a/gopls/internal/regtest/completion/completion18_test.go +++ b/gopls/internal/regtest/completion/completion18_test.go @@ -42,9 +42,9 @@ func (s SyncMap[XX,string]) g(v UU) {} env.OpenFile("main.go") env.Await(env.DoneWithOpen()) for _, tst := range tests { - pos := env.RegexpSearch("main.go", tst.pat) - pos.Character += uint32(protocol.UTF16Len([]byte(tst.pat))) - completions := env.Completion("main.go", pos) + loc := env.RegexpSearch("main.go", tst.pat) + loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte(tst.pat))) + completions := env.Completion(loc) result := compareCompletionLabels(tst.want, completions.Items) if result != "" { t.Errorf("%s: wanted %v", result, tst.want) @@ -109,9 +109,9 @@ func FuzzHex(f *testing.F) { for _, test := range tests { env.OpenFile(test.file) env.Await(env.DoneWithOpen()) - pos := env.RegexpSearch(test.file, test.pat) - pos.Character += test.offset // character user just typed? will type? - completions := env.Completion(test.file, pos) + loc := env.RegexpSearch(test.file, test.pat) + loc.Range.Start.Character += test.offset // character user just typed? will type? + completions := env.Completion(loc) result := compareCompletionLabels(test.want, completions.Items) if result != "" { t.Errorf("pat %q %q", test.pat, result) diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 6d0bfc56844..7d9214fa52d 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -176,7 +176,7 @@ package env.Await(env.DoneWithChangeWatchedFiles()) } env.OpenFile(tc.filename) - completions := env.Completion(tc.filename, env.RegexpSearch(tc.filename, tc.triggerRegexp)) + completions := env.Completion(env.RegexpSearch(tc.filename, tc.triggerRegexp)) // Check that the completion item suggestions are in the range // of the file. {Start,End}.Line are zero-based. @@ -191,12 +191,12 @@ package } if tc.want != nil { - expectedRng := env.RegexpRange(tc.filename, tc.editRegexp) + expectedLoc := env.RegexpSearch(tc.filename, tc.editRegexp) for _, item := range completions.Items { gotRng := item.TextEdit.Range - if expectedRng != gotRng { + if expectedLoc.Range != gotRng { t.Errorf("unexpected completion range for completion item %s: got %v, want %v", - item.Label, gotRng, expectedRng) + item.Label, gotRng, expectedLoc.Range) } } } @@ -223,7 +223,7 @@ package ma want := []string{"ma", "ma_test", "main", "math", "math_test"} Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("math/add.go") - completions := env.Completion("math/add.go", env.RegexpSearch("math/add.go", "package ma()")) + completions := env.Completion(env.RegexpSearch("math/add.go", "package ma()")) diff := compareCompletionLabels(want, completions.Items) if diff != "" { @@ -293,19 +293,19 @@ func _() { // Trigger unimported completions for the example.com/blah package. env.OpenFile("main.go") env.Await(env.DoneWithOpen()) - pos := env.RegexpSearch("main.go", "ah") - completions := env.Completion("main.go", pos) + loc := env.RegexpSearch("main.go", "ah") + completions := env.Completion(loc) if len(completions.Items) == 0 { t.Fatalf("no completion items") } - env.AcceptCompletion("main.go", pos, completions.Items[0]) + env.AcceptCompletion(loc, completions.Items[0]) env.Await(env.DoneWithChange()) // Trigger completions once again for the blah.<> selector. env.RegexpReplace("main.go", "_ = blah", "_ = blah.") env.Await(env.DoneWithChange()) - pos = env.RegexpSearch("main.go", "\n}") - completions = env.Completion("main.go", pos) + loc = env.RegexpSearch("main.go", "\n}") + completions = env.Completion(loc) if len(completions.Items) != 1 { t.Fatalf("expected 1 completion item, got %v", len(completions.Items)) } @@ -313,7 +313,7 @@ func _() { if item.Label != "Name" { t.Fatalf("expected completion item blah.Name, got %v", item.Label) } - env.AcceptCompletion("main.go", pos, item) + env.AcceptCompletion(loc, item) // Await the diagnostics to add example.com/blah to the go.mod file. env.AfterChange( @@ -381,7 +381,7 @@ type S struct { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo.go") - completions := env.Completion("foo.go", env.RegexpSearch("foo.go", `if s\.()`)) + completions := env.Completion(env.RegexpSearch("foo.go", `if s\.()`)) diff := compareCompletionLabels([]string{"i"}, completions.Items) if diff != "" { t.Fatal(diff) @@ -441,7 +441,7 @@ func _() { {`var _ e = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}}, } for _, tt := range tests { - completions := env.Completion("main.go", env.RegexpSearch("main.go", tt.re)) + completions := env.Completion(env.RegexpSearch("main.go", tt.re)) diff := compareCompletionLabels(tt.want, completions.Items) if diff != "" { t.Errorf("%s: %s", tt.re, diff) @@ -474,9 +474,9 @@ func doit() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("prog.go") - pos := env.RegexpSearch("prog.go", "if fooF") - pos.Character += uint32(protocol.UTF16Len([]byte("if fooF"))) - completions := env.Completion("prog.go", pos) + loc := env.RegexpSearch("prog.go", "if fooF") + loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte("if fooF"))) + completions := env.Completion(loc) diff := compareCompletionLabels([]string{"fooFunc"}, completions.Items) if diff != "" { t.Error(diff) @@ -484,9 +484,9 @@ func doit() { if completions.Items[0].Tags == nil { t.Errorf("expected Tags to show deprecation %#v", diff[0]) } - pos = env.RegexpSearch("prog.go", "= badP") - pos.Character += uint32(protocol.UTF16Len([]byte("= badP"))) - completions = env.Completion("prog.go", pos) + loc = env.RegexpSearch("prog.go", "= badP") + loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte("= badP"))) + completions = env.Completion(loc) diff = compareCompletionLabels([]string{"badPi"}, completions.Items) if diff != "" { t.Error(diff) @@ -520,12 +520,12 @@ func main() { // Trigger unimported completions for the example.com/blah package. env.OpenFile("main.go") env.Await(env.DoneWithOpen()) - pos := env.RegexpSearch("main.go", "Sqr()") - completions := env.Completion("main.go", pos) + loc := env.RegexpSearch("main.go", "Sqr()") + completions := env.Completion(loc) if len(completions.Items) == 0 { t.Fatalf("no completion items") } - env.AcceptCompletion("main.go", pos, completions.Items[0]) + env.AcceptCompletion(loc, completions.Items[0]) env.Await(env.DoneWithChange()) got := env.BufferText("main.go") want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:})\r\n}\r\n" @@ -574,9 +574,9 @@ package foo env.Await(env.DoneWithOpen()) for _, tst := range tests { env.SetBufferContent(fname, "package foo\n"+tst.line) - pos := env.RegexpSearch(fname, tst.pat) - pos.Character += uint32(protocol.UTF16Len([]byte(tst.pat))) - completions := env.Completion(fname, pos) + loc := env.RegexpSearch(fname, tst.pat) + loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte(tst.pat))) + completions := env.Completion(loc) result := compareCompletionLabels(tst.want, completions.Items) if result != "" { t.Errorf("\npat:%q line:%q failed: %s:%q", tst.pat, tst.line, result, tst.want) @@ -656,14 +656,14 @@ func Benchmark${1:Xxx}(b *testing.B) { tst.after = strings.Trim(tst.after, "\n") env.SetBufferContent("foo_test.go", tst.before) - pos := env.RegexpSearch("foo_test.go", tst.name) - pos.Character = uint32(protocol.UTF16Len([]byte(tst.name))) - completions := env.Completion("foo_test.go", pos) + loc := env.RegexpSearch("foo_test.go", tst.name) + loc.Range.Start.Character = uint32(protocol.UTF16Len([]byte(tst.name))) + completions := env.Completion(loc) if len(completions.Items) == 0 { t.Fatalf("no completion items") } - env.AcceptCompletion("foo_test.go", pos, completions.Items[0]) + env.AcceptCompletion(loc, completions.Items[0]) env.Await(env.DoneWithChange()) if buf := env.BufferText("foo_test.go"); buf != tst.after { t.Errorf("%s:incorrect completion: got %q, want %q", tst.name, buf, tst.after) @@ -708,7 +708,7 @@ use ./dir/foobar/ {`use ./dir/foobar/()`, []string{}}, } for _, tt := range tests { - completions := env.Completion("go.work", env.RegexpSearch("go.work", tt.re)) + completions := env.Completion(env.RegexpSearch("go.work", tt.re)) diff := compareCompletionLabels(tt.want, completions.Items) if diff != "" { t.Errorf("%s: %s", tt.re, diff) diff --git a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go index 6d2be0594d6..df69703ee26 100644 --- a/gopls/internal/regtest/completion/postfix_snippet_test.go +++ b/gopls/internal/regtest/completion/postfix_snippet_test.go @@ -447,13 +447,13 @@ func foo() string { env.SetBufferContent("foo.go", c.before) - pos := env.RegexpSearch("foo.go", "\n}") - completions := env.Completion("foo.go", pos) + loc := env.RegexpSearch("foo.go", "\n}") + completions := env.Completion(loc) if len(completions.Items) != 1 { t.Fatalf("expected one completion, got %v", completions.Items) } - env.AcceptCompletion("foo.go", pos, completions.Items[0]) + env.AcceptCompletion(loc, completions.Items[0]) if buf := env.BufferText("foo.go"); buf != c.after { t.Errorf("\nGOT:\n%s\nEXPECTED:\n%s", buf, c.after) diff --git a/gopls/internal/regtest/diagnostics/builtin_test.go b/gopls/internal/regtest/diagnostics/builtin_test.go index 193bbe0a9e3..935a7f9b831 100644 --- a/gopls/internal/regtest/diagnostics/builtin_test.go +++ b/gopls/internal/regtest/diagnostics/builtin_test.go @@ -26,9 +26,9 @@ const ( ` Run(t, src, func(t *testing.T, env *Env) { env.OpenFile("a.go") - name, _ := env.GoToDefinition("a.go", env.RegexpSearch("a.go", "iota")) - if !strings.HasSuffix(name, "builtin.go") { - t.Fatalf("jumped to %q, want builtin.go", name) + loc := env.GoToDefinition(env.RegexpSearch("a.go", "iota")) + if !strings.HasSuffix(string(loc.URI), "builtin.go") { + t.Fatalf("jumped to %q, want builtin.go", loc.URI) } env.AfterChange(NoDiagnostics(ForFile("builtin.go"))) }) diff --git a/gopls/internal/regtest/misc/call_hierarchy_test.go b/gopls/internal/regtest/misc/call_hierarchy_test.go index b807ee991d7..f0f5d4a4117 100644 --- a/gopls/internal/regtest/misc/call_hierarchy_test.go +++ b/gopls/internal/regtest/misc/call_hierarchy_test.go @@ -23,11 +23,11 @@ package pkg // TODO(rfindley): this could probably just be a marker test. Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("p.go") - pos := env.RegexpSearch("p.go", "pkg") + loc := env.RegexpSearch("p.go", "pkg") var params protocol.CallHierarchyPrepareParams - params.TextDocument.URI = env.Sandbox.Workdir.URI("p.go") - params.Position = pos + params.TextDocument.URI = loc.URI + params.Position = loc.Range.Start // Check that this doesn't panic. env.Editor.Server.PrepareCallHierarchy(env.Ctx, ¶ms) diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index 0186d50b364..cad69c142c4 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -38,12 +38,13 @@ const message = "Hello World." func TestGoToInternalDefinition(t *testing.T) { Run(t, internalDefinition, func(t *testing.T, env *Env) { env.OpenFile("main.go") - name, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", "message")) + loc := env.GoToDefinition(env.RegexpSearch("main.go", "message")) + name := env.Sandbox.Workdir.URIToPath(loc.URI) if want := "const.go"; name != want { t.Errorf("GoToDefinition: got file %q, want %q", name, want) } - if want := env.RegexpSearch("const.go", "message"); pos != want { - t.Errorf("GoToDefinition: got position %v, want %v", pos, want) + if want := env.RegexpSearch("const.go", "message"); loc != want { + t.Errorf("GoToDefinition: got location %v, want %v", loc, want) } }) } @@ -65,19 +66,21 @@ func main() { func TestGoToStdlibDefinition_Issue37045(t *testing.T) { Run(t, stdlibDefinition, func(t *testing.T, env *Env) { env.OpenFile("main.go") - name, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Printf)`)) + loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Printf)`)) + name := env.Sandbox.Workdir.URIToPath(loc.URI) if got, want := path.Base(name), "print.go"; got != want { t.Errorf("GoToDefinition: got file %q, want %q", name, want) } // Test that we can jump to definition from outside our workspace. // See golang.org/issues/37045. - newName, newPos := env.GoToDefinition(name, pos) + newLoc := env.GoToDefinition(loc) + newName := env.Sandbox.Workdir.URIToPath(newLoc.URI) if newName != name { t.Errorf("GoToDefinition is not idempotent: got %q, want %q", newName, name) } - if newPos != pos { - t.Errorf("GoToDefinition is not idempotent: got %v, want %v", newPos, pos) + if newLoc != loc { + t.Errorf("GoToDefinition is not idempotent: got %v, want %v", newLoc, loc) } }) } @@ -85,23 +88,24 @@ func TestGoToStdlibDefinition_Issue37045(t *testing.T) { func TestUnexportedStdlib_Issue40809(t *testing.T) { Run(t, stdlibDefinition, func(t *testing.T, env *Env) { env.OpenFile("main.go") - name, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Printf)`)) + loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Printf)`)) + name := env.Sandbox.Workdir.URIToPath(loc.URI) - pos := env.RegexpSearch(name, `:=\s*(newPrinter)\(\)`) + loc = env.RegexpSearch(name, `:=\s*(newPrinter)\(\)`) // Check that we can find references on a reference - refs := env.References(name, pos) + refs := env.References(loc) if len(refs) < 5 { t.Errorf("expected 5+ references to newPrinter, found: %#v", refs) } - name, pos = env.GoToDefinition(name, pos) - content, _ := env.Hover(name, pos) + loc = env.GoToDefinition(loc) + content, _ := env.Hover(loc) if !strings.Contains(content.Value, "newPrinter") { t.Fatal("definition of newPrinter went to the incorrect place") } // And on the definition too. - refs = env.References(name, pos) + refs = env.References(loc) if len(refs) < 5 { t.Errorf("expected 5+ references to newPrinter, found: %#v", refs) } @@ -125,7 +129,7 @@ func main() { }` Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") - content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error")) + content, _ := env.Hover(env.RegexpSearch("main.go", "Error")) if content == nil { t.Fatalf("nil hover content for Error") } @@ -163,10 +167,10 @@ func main() {} Settings{"importShortcut": tt.importShortcut}, ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") - file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"fmt"`)) - if !tt.wantDef && (file != "" || pos != (protocol.Position{})) { - t.Fatalf("expected no definition, got one: %s:%v", file, pos) - } else if tt.wantDef && file == "" && pos == (protocol.Position{}) { + loc := env.GoToDefinition(env.RegexpSearch("main.go", `"fmt"`)) + if !tt.wantDef && (loc != (protocol.Location{})) { + t.Fatalf("expected no definition, got one: %v", loc) + } else if tt.wantDef && loc == (protocol.Location{}) { t.Fatalf("expected definition, got none") } links := env.DocumentLink("main.go") @@ -213,7 +217,7 @@ func main() {} Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") - _, pos, err := env.Editor.GoToTypeDefinition(env.Ctx, "main.go", env.RegexpSearch("main.go", tt.re)) + loc, err := env.Editor.GoToTypeDefinition(env.Ctx, env.RegexpSearch("main.go", tt.re)) if tt.wantError { if err == nil { t.Fatal("expected error, got nil") @@ -224,9 +228,9 @@ func main() {} t.Fatalf("expected nil error, got %s", err) } - typePos := env.RegexpSearch("main.go", tt.wantTypeRe) - if pos != typePos { - t.Errorf("invalid pos: want %+v, got %+v", typePos, pos) + typeLoc := env.RegexpSearch("main.go", tt.wantTypeRe) + if loc != typeLoc { + t.Errorf("invalid pos: want %+v, got %+v", typeLoc, loc) } }) }) @@ -269,7 +273,7 @@ package client ` Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("client/client_role_test.go") - env.GoToDefinition("client/client_role_test.go", env.RegexpSearch("client/client_role_test.go", "RoleSetup")) + env.GoToDefinition(env.RegexpSearch("client/client_role_test.go", "RoleSetup")) }) } @@ -327,10 +331,11 @@ const _ = b.K } env.OpenFile("a.go") - refPos := env.RegexpSearch("a.go", "K") // find "b.K" reference + refLoc := env.RegexpSearch("a.go", "K") // find "b.K" reference // Initially, b.K is defined in the module cache. - gotFile, _ := env.GoToDefinition("a.go", refPos) + gotLoc := env.GoToDefinition(refLoc) + gotFile := env.Sandbox.Workdir.URIToPath(gotLoc.URI) wantCache := filepath.ToSlash(env.Sandbox.GOPATH()) + "/pkg/mod/other.com/b@v1.0.0/b.go" if gotFile != wantCache { t.Errorf("GoToDefinition, before: got file %q, want %q", gotFile, wantCache) @@ -345,7 +350,7 @@ const _ = b.K env.Await(env.DoneWithChangeWatchedFiles()) // Now, b.K is defined in the vendor tree. - gotFile, _ = env.GoToDefinition("a.go", refPos) + gotLoc = env.GoToDefinition(refLoc) wantVendor := "vendor/other.com/b/b.go" if gotFile != wantVendor { t.Errorf("GoToDefinition, after go mod vendor: got file %q, want %q", gotFile, wantVendor) @@ -364,7 +369,8 @@ const _ = b.K env.Await(env.DoneWithChangeWatchedFiles()) // b.K is once again defined in the module cache. - gotFile, _ = env.GoToDefinition("a.go", refPos) + gotLoc = env.GoToDefinition(gotLoc) + gotFile = env.Sandbox.Workdir.URIToPath(gotLoc.URI) if gotFile != wantCache { t.Errorf("GoToDefinition, after rm -rf vendor: got file %q, want %q", gotFile, wantCache) } diff --git a/gopls/internal/regtest/misc/extract_test.go b/gopls/internal/regtest/misc/extract_test.go index f159e5e9f4d..23efffbb70e 100644 --- a/gopls/internal/regtest/misc/extract_test.go +++ b/gopls/internal/regtest/misc/extract_test.go @@ -28,11 +28,8 @@ func Foo() int { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - - start := env.RegexpSearch("main.go", "a := 5") - end := env.RegexpSearch("main.go", "return a") - - actions, err := env.Editor.CodeAction(env.Ctx, "main.go", &protocol.Range{Start: start, End: end}, nil) + loc := env.RegexpSearch("main.go", `a := 5\n.*return a`) + actions, err := env.Editor.CodeAction(env.Ctx, loc, nil) if err != nil { t.Fatal(err) } @@ -53,8 +50,7 @@ func Foo() int { want := `package main func Foo() int { - a := newFunction() - return a + return newFunction() } func newFunction() int { diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index 9ab456d64c4..42aa3721a34 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -34,7 +34,7 @@ func main() { }` Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") - content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error")) + content, _ := env.Hover(env.RegexpSearch("main.go", "Error")) if content == nil { t.Fatalf("Hover('Error') returned nil") } diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index 42096a8b360..7a5e530e307 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -34,11 +34,7 @@ func Foo() { ` Run(t, basic, func(t *testing.T, env *Env) { env.OpenFile("main.go") - pos := env.RegexpSearch("main.go", "Info{}") - if err := env.Editor.RefactorRewrite(env.Ctx, "main.go", &protocol.Range{ - Start: pos, - End: pos, - }); err != nil { + if err := env.Editor.RefactorRewrite(env.Ctx, env.RegexpSearch("main.go", "Info{}")); err != nil { t.Fatal(err) } want := `package main diff --git a/gopls/internal/regtest/misc/highlight_test.go b/gopls/internal/regtest/misc/highlight_test.go index 587e7d6f2ab..8835d608ecf 100644 --- a/gopls/internal/regtest/misc/highlight_test.go +++ b/gopls/internal/regtest/misc/highlight_test.go @@ -30,9 +30,9 @@ func main() { Run(t, mod, func(t *testing.T, env *Env) { const file = "main.go" env.OpenFile(file) - _, pos := env.GoToDefinition(file, env.RegexpSearch(file, `var (A) string`)) + loc := env.GoToDefinition(env.RegexpSearch(file, `var (A) string`)) - checkHighlights(env, file, pos, 3) + checkHighlights(env, loc, 3) }) } @@ -53,10 +53,11 @@ func main() { Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") - file, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt\.(Printf)`)) - pos := env.RegexpSearch(file, `func Printf\((format) string`) + defLoc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt\.(Printf)`)) + file := env.Sandbox.Workdir.URIToPath(defLoc.URI) + loc := env.RegexpSearch(file, `func Printf\((format) string`) - checkHighlights(env, file, pos, 2) + checkHighlights(env, loc, 2) }) } @@ -112,26 +113,28 @@ func main() {}` ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") - file, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"example.com/global"`)) - pos := env.RegexpSearch(file, `const (A)`) - checkHighlights(env, file, pos, 4) + defLoc := env.GoToDefinition(env.RegexpSearch("main.go", `"example.com/global"`)) + file := env.Sandbox.Workdir.URIToPath(defLoc.URI) + loc := env.RegexpSearch(file, `const (A)`) + checkHighlights(env, loc, 4) - file, _ = env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"example.com/local"`)) - pos = env.RegexpSearch(file, `const (b)`) - checkHighlights(env, file, pos, 5) + defLoc = env.GoToDefinition(env.RegexpSearch("main.go", `"example.com/local"`)) + file = env.Sandbox.Workdir.URIToPath(defLoc.URI) + loc = env.RegexpSearch(file, `const (b)`) + checkHighlights(env, loc, 5) }) } -func checkHighlights(env *Env, file string, pos protocol.Position, highlightCount int) { +func checkHighlights(env *Env, loc protocol.Location, highlightCount int) { t := env.T t.Helper() - highlights := env.DocumentHighlight(file, pos) + highlights := env.DocumentHighlight(loc) if len(highlights) != highlightCount { t.Fatalf("expected %v highlight(s), got %v", highlightCount, len(highlights)) } - references := env.References(file, pos) + references := env.References(loc) if len(highlights) != len(references) { t.Fatalf("number of highlights and references is expected to be equal: %v != %v", len(highlights), len(references)) } diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go index 013c4d160c4..6b1d2438698 100644 --- a/gopls/internal/regtest/misc/hover_test.go +++ b/gopls/internal/regtest/misc/hover_test.go @@ -59,21 +59,22 @@ func main() { ProxyFiles(proxy), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") - mixedPos := env.RegexpSearch("main.go", "Mixed") - got, _ := env.Hover("main.go", mixedPos) + mixedLoc := env.RegexpSearch("main.go", "Mixed") + got, _ := env.Hover(mixedLoc) if !strings.Contains(got.Value, "unexported") { t.Errorf("Workspace hover: missing expected field 'unexported'. Got:\n%q", got.Value) } - cacheFile, _ := env.GoToDefinition("main.go", mixedPos) - argPos := env.RegexpSearch(cacheFile, "printMixed.*(Mixed)") - got, _ = env.Hover(cacheFile, argPos) + cacheLoc := env.GoToDefinition(mixedLoc) + cacheFile := env.Sandbox.Workdir.URIToPath(cacheLoc.URI) + argLoc := env.RegexpSearch(cacheFile, "printMixed.*(Mixed)") + got, _ = env.Hover(argLoc) if !strings.Contains(got.Value, "unexported") { t.Errorf("Non-workspace hover: missing expected field 'unexported'. Got:\n%q", got.Value) } - exportedFieldPos := env.RegexpSearch("main.go", "Exported") - got, _ = env.Hover("main.go", exportedFieldPos) + exportedFieldLoc := env.RegexpSearch("main.go", "Exported") + got, _ = env.Hover(exportedFieldLoc) if !strings.Contains(got.Value, "comment") { t.Errorf("Workspace hover: missing comment for field 'Exported'. Got:\n%q", got.Value) } @@ -97,13 +98,13 @@ func main() { Run(t, source, func(t *testing.T, env *Env) { env.OpenFile("main.go") hexExpected := "58190" - got, _ := env.Hover("main.go", env.RegexpSearch("main.go", "hex")) + got, _ := env.Hover(env.RegexpSearch("main.go", "hex")) if got != nil && !strings.Contains(got.Value, hexExpected) { t.Errorf("Hover: missing expected field '%s'. Got:\n%q", hexExpected, got.Value) } binExpected := "73" - got, _ = env.Hover("main.go", env.RegexpSearch("main.go", "bigBin")) + got, _ = env.Hover(env.RegexpSearch("main.go", "bigBin")) if got != nil && !strings.Contains(got.Value, binExpected) { t.Errorf("Hover: missing expected field '%s'. Got:\n%q", binExpected, got.Value) } @@ -119,7 +120,7 @@ package main type Example struct` Run(t, source, func(t *testing.T, env *Env) { env.OpenFile("main.go") - env.Editor.Hover(env.Ctx, "main.go", env.RegexpSearch("main.go", "Example")) + env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "Example")) }) } @@ -135,7 +136,7 @@ package main Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.EditBuffer("main.go", fake.NewEdit(0, 0, 1, 0, "package main\nfunc main() {\nconst x = `\nfoo\n`\n}")) - env.Editor.Hover(env.Ctx, "main.go", env.RegexpSearch("main.go", "foo")) + env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "foo")) }) } @@ -203,7 +204,7 @@ func main() { Run(t, source, func(t *testing.T, env *Env) { env.OpenFile("main.go") for _, test := range tests { - got, _ := env.Hover("main.go", env.RegexpSearch("main.go", test.hoverPackage)) + got, _ := env.Hover(env.RegexpSearch("main.go", test.hoverPackage)) if got == nil { t.Error("nil hover for", test.hoverPackage) continue @@ -213,7 +214,7 @@ func main() { } } - got, _ := env.Hover("main.go", env.RegexpSearch("main.go", "mod.com/lib4")) + got, _ := env.Hover(env.RegexpSearch("main.go", "mod.com/lib4")) if got != nil { t.Errorf("Hover: got:\n%q\nwant:\n%v", got.Value, nil) } @@ -250,7 +251,7 @@ package lib env.OpenFile("a.go") z := env.RegexpSearch("a.go", "lib") t.Logf("%#v", z) - got, _ := env.Hover("a.go", env.RegexpSearch("a.go", "lib")) + got, _ := env.Hover(env.RegexpSearch("a.go", "lib")) if strings.Contains(got.Value, "{#hdr-") { t.Errorf("Hover: got {#hdr- tag:\n%q", got) } @@ -266,6 +267,6 @@ go 1.16 ` Run(t, source, func(t *testing.T, env *Env) { env.OpenFile("go.mod") - env.Hover("go.mod", env.RegexpSearch("go.mod", "go")) // no panic + env.Hover(env.RegexpSearch("go.mod", "go")) // no panic }) } diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index 513171a1e69..bea955220a4 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -159,7 +159,8 @@ var _, _ = x.X, y.Y env.AfterChange(Diagnostics(env.AtRegexp("main.go", `y.Y`))) env.SaveBuffer("main.go") env.AfterChange(NoDiagnostics(ForFile("main.go"))) - path, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `y.(Y)`)) + loc := env.GoToDefinition(env.RegexpSearch("main.go", `y.(Y)`)) + path := env.Sandbox.Workdir.URIToPath(loc.URI) if !strings.HasPrefix(path, filepath.ToSlash(modcache)) { t.Errorf("found module dependency outside of GOMODCACHE: got %v, wanted subdir of %v", path, filepath.ToSlash(modcache)) } diff --git a/gopls/internal/regtest/misc/link_test.go b/gopls/internal/regtest/misc/link_test.go index b782fff4693..8a64c54e225 100644 --- a/gopls/internal/regtest/misc/link_test.go +++ b/gopls/internal/regtest/misc/link_test.go @@ -53,11 +53,11 @@ const Hello = "Hello" pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg" // First, check that we get the expected links via hover and documentLink. - content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello")) + content, _ := env.Hover(env.RegexpSearch("main.go", "pkg.Hello")) if content == nil || !strings.Contains(content.Value, pkgLink) { t.Errorf("hover: got %v in main.go, want contains %q", content, pkgLink) } - content, _ = env.Hover("go.mod", env.RegexpSearch("go.mod", "import.test")) + content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test")) if content == nil || !strings.Contains(content.Value, pkgLink) { t.Errorf("hover: got %v in go.mod, want contains %q", content, pkgLink) } @@ -76,11 +76,11 @@ const Hello = "Hello" env.ChangeConfiguration(cfg) // Finally, verify that the links are gone. - content, _ = env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello")) + content, _ = env.Hover(env.RegexpSearch("main.go", "pkg.Hello")) if content == nil || strings.Contains(content.Value, pkgLink) { t.Errorf("hover: got %v in main.go, want non-empty hover without %q", content, pkgLink) } - content, _ = env.Hover("go.mod", env.RegexpSearch("go.mod", "import.test")) + content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test")) if content == nil || strings.Contains(content.Value, modLink) { t.Errorf("hover: got %v in go.mod, want contains %q", content, modLink) } diff --git a/gopls/internal/regtest/misc/multiple_adhoc_test.go b/gopls/internal/regtest/misc/multiple_adhoc_test.go index 400e7843483..981b74efca0 100644 --- a/gopls/internal/regtest/misc/multiple_adhoc_test.go +++ b/gopls/internal/regtest/misc/multiple_adhoc_test.go @@ -30,14 +30,14 @@ func main() () { } `, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") - if list := env.Completion("a/a.go", env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 { + if list := env.Completion(env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 { t.Fatal("expected completions, got none") } env.OpenFile("a/b.go") - if list := env.Completion("a/b.go", env.RegexpSearch("a/b.go", "Println")); list == nil || len(list.Items) == 0 { + if list := env.Completion(env.RegexpSearch("a/b.go", "Println")); list == nil || len(list.Items) == 0 { t.Fatal("expected completions, got none") } - if list := env.Completion("a/a.go", env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 { + if list := env.Completion(env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 { t.Fatal("expected completions, got none") } }) diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index 8e99a1c30e3..e1f5d8e0502 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -34,8 +34,8 @@ func main() { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Print)`)) - refs, err := env.Editor.References(env.Ctx, file, pos) + loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Print)`)) + refs, err := env.Editor.References(env.Ctx, loc) if err != nil { t.Fatal(err) } @@ -79,8 +79,8 @@ func _() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `Error`)) - refs, err := env.Editor.References(env.Ctx, file, pos) + loc := env.GoToDefinition(env.RegexpSearch("main.go", `Error`)) + refs, err := env.Editor.References(env.Ctx, loc) if err != nil { t.Fatalf("references on (*s).Error failed: %v", err) } @@ -157,10 +157,10 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { for _, test := range tests { - f := fmt.Sprintf("%s/a.go", test.packageName) - env.OpenFile(f) - pos := env.RegexpSearch(f, test.packageName) - refs := env.References(fmt.Sprintf("%s/a.go", test.packageName), pos) + file := fmt.Sprintf("%s/a.go", test.packageName) + env.OpenFile(file) + loc := env.RegexpSearch(file, test.packageName) + refs := env.References(loc) if len(refs) != test.wantRefCount { // TODO(adonovan): make this assertion less maintainer-hostile. t.Fatalf("got %v reference(s), want %d", len(refs), test.wantRefCount) @@ -277,8 +277,8 @@ func _() { } for _, test := range refTests { - pos := env.RegexpSearch("foo/foo.go", test.re) - refs := env.References("foo/foo.go", pos) + loc := env.RegexpSearch("foo/foo.go", test.re) + refs := env.References(loc) got := fileLocations(refs) if diff := cmp.Diff(test.wantRefs, got); diff != "" { @@ -301,8 +301,8 @@ func _() { } for _, test := range implTests { - pos := env.RegexpSearch("foo/foo.go", test.re) - impls := env.Implementations("foo/foo.go", pos) + loc := env.RegexpSearch("foo/foo.go", test.re) + impls := env.Implementations(loc) got := fileLocations(impls) if diff := cmp.Diff(test.wantImpls, got); diff != "" { @@ -364,11 +364,11 @@ var _ b.B } env.OpenFile("a.go") - refPos := env.RegexpSearch("a.go", "I") // find "I" reference + refLoc := env.RegexpSearch("a.go", "I") // find "I" reference // Initially, a.I has one implementation b.B in // the module cache, not the vendor tree. - checkVendor(env.Implementations("a.go", refPos), false) + checkVendor(env.Implementations(refLoc), false) // Run 'go mod vendor' outside the editor. if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, true); err != nil { @@ -379,7 +379,7 @@ var _ b.B env.Await(env.DoneWithChangeWatchedFiles()) // Now, b.B is found in the vendor tree. - checkVendor(env.Implementations("a.go", refPos), true) + checkVendor(env.Implementations(refLoc), true) // Delete the vendor tree. if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil { @@ -394,6 +394,6 @@ var _ b.B env.Await(env.DoneWithChangeWatchedFiles()) // b.B is once again defined in the module cache. - checkVendor(env.Implementations("a.go", refPos), false) + checkVendor(env.Implementations(refLoc), false) }) } diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 86fd567cde7..833d3db0c08 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -34,10 +34,11 @@ func main() { const wantErr = "can't rename package \"main\"" Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - pos := env.RegexpSearch("main.go", `main`) + loc := env.RegexpSearch("main.go", `main`) + // TODO(adonovan): define a helper from Location to TextDocumentPositionParams. tdpp := protocol.TextDocumentPositionParams{ TextDocument: env.Editor.TextDocumentIdentifier("main.go"), - Position: pos, + Position: loc.Range.Start, } params := &protocol.PrepareRenameParams{ TextDocumentPositionParams: tdpp, @@ -79,7 +80,7 @@ func _() { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("p.go") - env.Rename("p.go", env.RegexpSearch("p.go", "M"), "N") // must not panic + env.Rename(env.RegexpSearch("p.go", "M"), "N") // must not panic }) } @@ -107,9 +108,7 @@ func main() { const wantErr = "no object found" Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "fmt") - - err := env.Editor.Rename(env.Ctx, "lib/a.go", pos, "fmt1") + err := env.Editor.Rename(env.Ctx, env.RegexpSearch("lib/a.go", "fmt"), "fmt1") if err == nil { t.Errorf("missing no object found from Rename") } @@ -142,10 +141,10 @@ func main() { ` const wantErr = "can't rename package: missing module information for package" Run(t, files, func(t *testing.T, env *Env) { - pos := env.RegexpSearch("lib/a.go", "lib") + loc := env.RegexpSearch("lib/a.go", "lib") tdpp := protocol.TextDocumentPositionParams{ TextDocument: env.Editor.TextDocumentIdentifier("lib/a.go"), - Position: pos, + Position: loc.Range.Start, } params := &protocol.PrepareRenameParams{ TextDocumentPositionParams: tdpp, @@ -194,8 +193,7 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "lib") - env.Rename("lib/a.go", pos, "nested") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") // Check if the new package name exists. env.RegexpSearch("nested/a.go", "package nested") @@ -236,8 +234,7 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "lib") - env.Rename("lib/a.go", pos, "nested") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") // Check if the new package name exists. env.RegexpSearch("nested/a.go", "package nested") @@ -277,8 +274,7 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "lib") - env.Rename("lib/a.go", pos, "nested") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") // Check if the new package name exists. env.RegexpSearch("nested/a.go", "package nested") @@ -323,8 +319,7 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "lib") - env.Rename("lib/a.go", pos, "lib1") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1") // Check if the new package name exists. env.RegexpSearch("lib1/a.go", "package lib1") @@ -371,8 +366,7 @@ func main() { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") - pos := env.RegexpSearch("main.go", `stringutil\.(Identity)`) - env.Rename("main.go", pos, "Identityx") + env.Rename(env.RegexpSearch("main.go", `stringutil\.(Identity)`), "Identityx") text := env.BufferText("stringutil/stringutil_test.go") if !strings.Contains(text, "Identityx") { t.Errorf("stringutil/stringutil_test.go: missing expected token `Identityx` after rename:\n%s", text) @@ -495,8 +489,7 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "lib") - env.Rename("lib/a.go", pos, "lib1") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1") // Check if the new package name exists. env.RegexpSearch("lib1/a.go", "package lib1") @@ -577,8 +570,7 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/foo.go") - pos := env.RegexpSearch("foo/foo.go", "foo") - env.Rename("foo/foo.go", pos, "foox") + env.Rename(env.RegexpSearch("foo/foo.go", "foo"), "foox") env.RegexpSearch("foox/foo.go", "package foox") env.OpenFile("foox/bar/bar.go") @@ -625,8 +617,7 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "lib") - env.Rename("lib/a.go", pos, "nested") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") // Check if the new package name exists. env.RegexpSearch("nested/a.go", "package nested") @@ -668,8 +659,7 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "lib") - env.Rename("lib/a.go", pos, "nested") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") // Check if the new package name exists. env.RegexpSearch("nested/a.go", "package nested") @@ -716,7 +706,7 @@ const Baz = foox.Foo ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/foo.go") - env.Rename("foo/foo.go", env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") + env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") checkTestdata(t, env) }) @@ -796,7 +786,7 @@ const _ = bar.Bar + baz.Baz + foox.Foo Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/foo.go") - env.Rename("foo/foo.go", env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") + env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") checkTestdata(t, env) }) @@ -846,8 +836,7 @@ const C = libx.A + nested.B ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "package (lib)") - env.Rename("lib/a.go", pos, "libx") + env.Rename(env.RegexpSearch("lib/a.go", "package (lib)"), "libx") checkTestdata(t, env) }) @@ -870,10 +859,10 @@ const A = 1 + nested.B Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/a.go") - pos := env.RegexpSearch("lib/a.go", "package (lib)") + loc := env.RegexpSearch("lib/a.go", "package (lib)") for _, badName := range []string{"$$$", "lib_test"} { - if err := env.Editor.Rename(env.Ctx, "lib/a.go", pos, badName); err == nil { + if err := env.Editor.Rename(env.Ctx, loc, badName); err == nil { t.Errorf("Rename(lib, libx) succeeded, want non-nil error") } } @@ -917,8 +906,7 @@ func main() { ` Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("lib/internal/x/a.go") - pos := env.RegexpSearch("lib/internal/x/a.go", "x") - env.Rename("lib/internal/x/a.go", pos, "utils") + env.Rename(env.RegexpSearch("lib/internal/x/a.go", "x"), "utils") // Check if the new package name exists. env.RegexpSearch("lib/a.go", "mod.com/lib/internal/utils") @@ -928,8 +916,7 @@ func main() { env.RegexpSearch("lib/internal/utils/a.go", "package utils") env.OpenFile("lib/a.go") - pos = env.RegexpSearch("lib/a.go", "lib") - env.Rename("lib/a.go", pos, "lib1") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1") // Check if the new package name exists. env.RegexpSearch("lib1/a.go", "package lib1") diff --git a/gopls/internal/regtest/misc/signature_help_test.go b/gopls/internal/regtest/misc/signature_help_test.go index 05f58300355..fd9f4f07adb 100644 --- a/gopls/internal/regtest/misc/signature_help_test.go +++ b/gopls/internal/regtest/misc/signature_help_test.go @@ -49,10 +49,10 @@ func _() { env.OpenFile("a/a/a.go") env.OpenFile("b/b/b.go") signatureHelp := func(filename string) *protocol.SignatureHelp { - pos := env.RegexpSearch(filename, `DoSomething\(()\)`) + loc := env.RegexpSearch(filename, `DoSomething\(()\)`) var params protocol.SignatureHelpParams - params.Position = pos - params.TextDocument.URI = env.Sandbox.Workdir.URI(filename) + params.TextDocument.URI = loc.URI + params.Position = loc.Range.Start help, err := env.Editor.Server.SignatureHelp(env.Ctx, ¶ms) if err != nil { t.Fatal(err) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 9d732727ea9..bbf99b3b5dd 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -644,9 +644,9 @@ func TestRunVulncheckWarning(t *testing.T) { "go.mod": {IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, Mode: govulncheck.ModeGovulncheck}, }) env.OpenFile("x/x.go") - lineX := env.RegexpSearch("x/x.go", `c\.C1\(\)\.Vuln1\(\)`) + lineX := env.RegexpSearch("x/x.go", `c\.C1\(\)\.Vuln1\(\)`).Range.Start env.OpenFile("y/y.go") - lineY := env.RegexpSearch("y/y.go", `c\.C2\(\)\(\)`) + lineY := env.RegexpSearch("y/y.go", `c\.C2\(\)\(\)`).Range.Start wantDiagnostics := map[string]vulnDiagExpectation{ "golang.org/amod": { applyAction: "Upgrade to v1.0.6", @@ -862,14 +862,14 @@ func TestGovulncheckInfo(t *testing.T) { // and runs checks if diagnostics and code actions associated with the line match expectation. func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagExpectation, got *protocol.PublishDiagnosticsParams) []protocol.Diagnostic { t.Helper() - pos := env.RegexpSearch("go.mod", pattern) + loc := env.RegexpSearch("go.mod", pattern) var modPathDiagnostics []protocol.Diagnostic for _, w := range want.diagnostics { - // Find the diagnostics at pos. + // Find the diagnostics at loc.start. var diag *protocol.Diagnostic for _, g := range got.Diagnostics { g := g - if g.Range.Start == pos && w.msg == g.Message { + if g.Range.Start == loc.Range.Start && w.msg == g.Message { modPathDiagnostics = append(modPathDiagnostics, g) diag = &g break @@ -895,7 +895,7 @@ func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagEx } // Check that useful info is supplemented as hover. if len(want.hover) > 0 { - hover, _ := env.Hover("go.mod", pos) + hover, _ := env.Hover(loc) for _, part := range want.hover { if !strings.Contains(hover.Value, part) { t.Errorf("hover contents for %q do not match, want %v, got %v\n", pattern, strings.Join(want.hover, ","), hover.Value) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index f44eefa70f9..483118dd3d4 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -913,13 +913,13 @@ func hello() {} env.RegexpReplace("go.mod", "module", "modul") // Confirm that we still have metadata with only on-disk edits. env.OpenFile("main.go") - file, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", "hello")) - if filepath.Base(file) != "hello.go" { - t.Fatalf("expected definition in hello.go, got %s", file) + loc := env.GoToDefinition(env.RegexpSearch("main.go", "hello")) + if filepath.Base(string(loc.URI)) != "hello.go" { + t.Fatalf("expected definition in hello.go, got %s", loc.URI) } // Confirm that we no longer have metadata when the file is saved. env.SaveBufferWithoutActions("go.mod") - _, _, err := env.Editor.GoToDefinition(env.Ctx, "main.go", env.RegexpSearch("main.go", "hello")) + _, err := env.Editor.GoToDefinition(env.Ctx, env.RegexpSearch("main.go", "hello")) if err == nil { t.Fatalf("expected error, got none") } @@ -1017,7 +1017,7 @@ func main() {} ).Run(t, mod, func(t *testing.T, env *Env) { d := &protocol.PublishDiagnosticsParams{} env.OpenFile("go.mod") - pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3") + pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3").Range.Start env.AfterChange( Diagnostics(AtPosition("go.mod", pos.Line, pos.Character)), ReadDiagnostics("go.mod", d), diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index c81bf1a1184..48635643c2d 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -191,8 +191,8 @@ go 1.12 ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a.tmpl") x := env.RegexpSearch("a.tmpl", `A`) - file, pos := env.GoToDefinition("a.tmpl", x) - refs := env.References(file, pos) + loc := env.GoToDefinition(x) + refs := env.References(loc) if len(refs) != 2 { t.Fatalf("got %v reference(s), want 2", len(refs)) } @@ -205,9 +205,9 @@ go 1.12 } } - content, npos := env.Hover(file, pos) - if pos != npos { - t.Errorf("pos? got %v, wanted %v", npos, pos) + content, nloc := env.Hover(loc) + if loc != nloc { + t.Errorf("loc? got %v, wanted %v", nloc, loc) } if content.Value != "template A defined" { t.Errorf("got %s, wanted 'template A defined", content.Value) diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go index 5c5a68ae1dd..005a7e94638 100644 --- a/gopls/internal/regtest/workspace/broken_test.go +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -113,7 +113,7 @@ const CompleteMe = 222 env.Await(NoOutstandingWork()) // Check that definitions in package1 go to the copy vendored in package2. - location, _ := env.GoToDefinition("package1/main.go", env.RegexpSearch("package1/main.go", "CompleteMe")) + location := env.GoToDefinition(env.RegexpSearch("package1/main.go", "CompleteMe")).URI.SpanURI().Filename() const wantLocation = "package2/vendor/example.com/foo/foo.go" if !strings.HasSuffix(location, wantLocation) { t.Errorf("got definition of CompleteMe at %q, want %q", location, wantLocation) diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index 11dfe670c42..ac64b0758e7 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -172,7 +172,8 @@ func Hello() int { // Now, to satisfy a definition request, gopls will try to reload moda. But // without access to the proxy (because this is no longer a // reinitialization), this loading will fail. - got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) + loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) + got := env.Sandbox.Workdir.URIToPath(loc.URI) if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { t.Errorf("expected %s, got %v", want, got) } diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go index c9618eb3aeb..e1021dfbcd1 100644 --- a/gopls/internal/regtest/workspace/standalone_test.go +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -83,10 +83,10 @@ func main() { env.RegexpReplace("lib/lib.go", "D", "C") env.AfterChange(NoDiagnostics()) - refs := env.References("lib/lib.go", env.RegexpSearch("lib/lib.go", "C")) + refs := env.References(env.RegexpSearch("lib/lib.go", "C")) checkLocations("References", refs, "lib/lib.go") - impls := env.Implementations("lib/lib.go", env.RegexpSearch("lib/lib.go", "I")) + impls := env.Implementations(env.RegexpSearch("lib/lib.go", "I")) checkLocations("Implementations", impls) // no implementations // Opening the standalone file should not result in any diagnostics. @@ -113,19 +113,17 @@ func main() { } // We should resolve workspace definitions in the standalone file. - file, _ := env.GoToDefinition("lib/ignore.go", env.RegexpSearch("lib/ignore.go", "lib.(C)")) + fileLoc := env.GoToDefinition(env.RegexpSearch("lib/ignore.go", "lib.(C)")) + file := env.Sandbox.Workdir.URIToPath(fileLoc.URI) if got, want := file, "lib/lib.go"; got != want { t.Errorf("GoToDefinition(lib.C) = %v, want %v", got, want) } // ...as well as intra-file definitions - file, pos := env.GoToDefinition("lib/ignore.go", env.RegexpSearch("lib/ignore.go", "\\+ (C)")) - if got, want := file, "lib/ignore.go"; got != want { - t.Errorf("GoToDefinition(C) = %v, want %v", got, want) - } - wantPos := env.RegexpSearch("lib/ignore.go", "const (C)") - if pos != wantPos { - t.Errorf("GoToDefinition(C) = %v, want %v", pos, wantPos) + loc := env.GoToDefinition(env.RegexpSearch("lib/ignore.go", "\\+ (C)")) + wantLoc := env.RegexpSearch("lib/ignore.go", "const (C)") + if loc != wantLoc { + t.Errorf("GoToDefinition(C) = %v, want %v", loc, wantLoc) } // Renaming "lib.C" to "lib.D" should cause a diagnostic in the standalone @@ -139,14 +137,14 @@ func main() { // Now that our workspace has no errors, we should be able to find // references and rename. - refs = env.References("lib/lib.go", env.RegexpSearch("lib/lib.go", "C")) + refs = env.References(env.RegexpSearch("lib/lib.go", "C")) checkLocations("References", refs, "lib/lib.go", "lib/ignore.go") - impls = env.Implementations("lib/lib.go", env.RegexpSearch("lib/lib.go", "I")) + impls = env.Implementations(env.RegexpSearch("lib/lib.go", "I")) checkLocations("Implementations", impls, "lib/ignore.go") // Renaming should rename in the standalone package. - env.Rename("lib/lib.go", env.RegexpSearch("lib/lib.go", "C"), "D") + env.Rename(env.RegexpSearch("lib/lib.go", "C"), "D") env.RegexpSearch("lib/ignore.go", "lib.D") }) } diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 3699fa823aa..0aff4713be4 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -131,7 +131,7 @@ func TestReferences(t *testing.T) { WithOptions(opts...).Run(t, workspaceModule, func(t *testing.T, env *Env) { f := "pkg/inner/inner.go" env.OpenFile(f) - locations := env.References(f, env.RegexpSearch(f, `SaySomething`)) + locations := env.References(env.RegexpSearch(f, `SaySomething`)) want := 3 if got := len(locations); got != want { t.Fatalf("expected %v locations, got %v", want, got) @@ -378,7 +378,8 @@ func Hello() int { env.OpenFile("moda/a/a.go") env.Await(env.DoneWithOpen()) - original, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) + originalLoc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) + original := env.Sandbox.Workdir.URIToPath(originalLoc.URI) if want := "modb/b/b.go"; !strings.HasSuffix(original, want) { t.Errorf("expected %s, got %v", want, original) } @@ -390,7 +391,8 @@ func Hello() int { env.WriteWorkspaceFile("go.work", "go 1.18\nuse moda/a") env.AfterChange() - got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) + gotLoc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) + got := env.Sandbox.Workdir.URIToPath(gotLoc.URI) if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { t.Errorf("expected %s, got %v", want, got) } @@ -431,7 +433,8 @@ func main() { ProxyFiles(workspaceModuleProxy), ).Run(t, multiModule, func(t *testing.T, env *Env) { env.OpenFile("moda/a/a.go") - original, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) + loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) + original := env.Sandbox.Workdir.URIToPath(loc.URI) if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(original, want) { t.Errorf("expected %s, got %v", want, original) } @@ -453,7 +456,8 @@ func Hello() int { `, }) env.AfterChange(Diagnostics(env.AtRegexp("modb/b/b.go", "x"))) - got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) + gotLoc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) + got := env.Sandbox.Workdir.URIToPath(gotLoc.URI) if want := "modb/b/b.go"; !strings.HasSuffix(got, want) { t.Errorf("expected %s, got %v", want, original) } @@ -581,9 +585,10 @@ use ( // To verify which modules are loaded, we'll jump to the definition of // b.Hello. checkHelloLocation := func(want string) error { - location, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) - if !strings.HasSuffix(location, want) { - return fmt.Errorf("expected %s, got %v", want, location) + loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) + file := env.Sandbox.Workdir.URIToPath(loc.URI) + if !strings.HasSuffix(file, want) { + return fmt.Errorf("expected %s, got %v", want, file) } return nil } @@ -749,8 +754,7 @@ module example.com/bar/baz } for hoverRE, want := range tcs { - pos := env.RegexpSearch("go.work", hoverRE) - got, _ := env.Hover("go.work", pos) + got, _ := env.Hover(env.RegexpSearch("go.work", hoverRE)) if got.Value != want { t.Errorf(`hover on %q: got %q, want %q`, hoverRE, got, want) } @@ -799,21 +803,22 @@ use ( ).Run(t, workspace, func(t *testing.T, env *Env) { env.OpenFile("moda/a/a.go") env.Await(env.DoneWithOpen()) - location, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) + loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) + file := env.Sandbox.Workdir.URIToPath(loc.URI) want := "modb/b/b.go" - if !strings.HasSuffix(location, want) { - t.Errorf("expected %s, got %v", want, location) + if !strings.HasSuffix(file, want) { + t.Errorf("expected %s, got %v", want, file) } }) } func TestNonWorkspaceFileCreation(t *testing.T) { const files = ` --- go.mod -- +-- work/go.mod -- module mod.com go 1.12 --- x.go -- +-- work/x.go -- package x ` @@ -822,10 +827,12 @@ package foo import "fmt" var _ = fmt.Printf ` - Run(t, files, func(t *testing.T, env *Env) { - env.CreateBuffer("/tmp/foo.go", "") - env.EditBuffer("/tmp/foo.go", fake.NewEdit(0, 0, 0, 0, code)) - env.GoToDefinition("/tmp/foo.go", env.RegexpSearch("/tmp/foo.go", `Printf`)) + WithOptions( + WorkspaceFolders("work"), // so that outside/... is outside the workspace + ).Run(t, files, func(t *testing.T, env *Env) { + env.CreateBuffer("outside/foo.go", "") + env.EditBuffer("outside/foo.go", fake.NewEdit(0, 0, 0, 0, code)) + env.GoToDefinition(env.RegexpSearch("outside/foo.go", `Printf`)) }) } @@ -1124,7 +1131,7 @@ func (Server) Foo() {} ) // This will cause a test failure if other_test.go is not in any package. - _, _ = env.GoToDefinition("other_test.go", env.RegexpSearch("other_test.go", "Server")) + _ = env.GoToDefinition(env.RegexpSearch("other_test.go", "Server")) }) } From d093a1395bfec672a3bba36b56333dfe3ff2c132 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 26 Jan 2023 16:14:35 -0500 Subject: [PATCH 680/723] gopls/internal/lsp/protocol: LocationTextDocumentPositionParams A helper function for populating LSP requests. Change-Id: Id9cbca56f8e32321680ba8ee4d7073d097789784 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463683 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan --- gopls/internal/lsp/cmd/call_hierarchy.go | 5 +-- gopls/internal/lsp/cmd/definition.go | 8 +--- gopls/internal/lsp/cmd/highlight.go | 5 +-- gopls/internal/lsp/cmd/implementation.go | 6 +-- gopls/internal/lsp/cmd/prepare_rename.go | 5 +-- gopls/internal/lsp/cmd/references.go | 5 +-- gopls/internal/lsp/cmd/signature.go | 8 +--- gopls/internal/lsp/fake/editor.go | 15 ++----- gopls/internal/lsp/lsp_test.go | 51 ++++------------------ gopls/internal/lsp/protocol/mapper.go | 8 ++++ gopls/internal/regtest/misc/rename_test.go | 13 +----- 11 files changed, 29 insertions(+), 100 deletions(-) diff --git a/gopls/internal/lsp/cmd/call_hierarchy.go b/gopls/internal/lsp/cmd/call_hierarchy.go index 9892fdff966..eb5d29de808 100644 --- a/gopls/internal/lsp/cmd/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/call_hierarchy.go @@ -58,10 +58,7 @@ func (c *callHierarchy) Run(ctx context.Context, args ...string) error { } p := protocol.CallHierarchyPrepareParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } callItems, err := conn.PrepareCallHierarchy(ctx, &p) diff --git a/gopls/internal/lsp/cmd/definition.go b/gopls/internal/lsp/cmd/definition.go index 269172db952..952f43b5132 100644 --- a/gopls/internal/lsp/cmd/definition.go +++ b/gopls/internal/lsp/cmd/definition.go @@ -88,12 +88,8 @@ func (d *definition) Run(ctx context.Context, args ...string) error { if err != nil { return err } - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - } p := protocol.DefinitionParams{ - TextDocumentPositionParams: tdpp, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } locs, err := conn.Definition(ctx, &p) if err != nil { @@ -104,7 +100,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { return fmt.Errorf("%v: not an identifier", from) } q := protocol.HoverParams{ - TextDocumentPositionParams: tdpp, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } hover, err := conn.Hover(ctx, &q) if err != nil { diff --git a/gopls/internal/lsp/cmd/highlight.go b/gopls/internal/lsp/cmd/highlight.go index bcb7149a43d..60c04b2d46c 100644 --- a/gopls/internal/lsp/cmd/highlight.go +++ b/gopls/internal/lsp/cmd/highlight.go @@ -57,10 +57,7 @@ func (r *highlight) Run(ctx context.Context, args ...string) error { } p := protocol.DocumentHighlightParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } highlights, err := conn.DocumentHighlight(ctx, &p) if err != nil { diff --git a/gopls/internal/lsp/cmd/implementation.go b/gopls/internal/lsp/cmd/implementation.go index eed41ab4402..bb5b1c24edb 100644 --- a/gopls/internal/lsp/cmd/implementation.go +++ b/gopls/internal/lsp/cmd/implementation.go @@ -58,12 +58,8 @@ func (i *implementation) Run(ctx context.Context, args ...string) error { } p := protocol.ImplementationParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } - implementations, err := conn.Implementation(ctx, &p) if err != nil { return err diff --git a/gopls/internal/lsp/cmd/prepare_rename.go b/gopls/internal/lsp/cmd/prepare_rename.go index 774433df605..5e9d732fbf2 100644 --- a/gopls/internal/lsp/cmd/prepare_rename.go +++ b/gopls/internal/lsp/cmd/prepare_rename.go @@ -60,10 +60,7 @@ func (r *prepareRename) Run(ctx context.Context, args ...string) error { return err } p := protocol.PrepareRenameParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } result, err := conn.PrepareRename(ctx, &p) if err != nil { diff --git a/gopls/internal/lsp/cmd/references.go b/gopls/internal/lsp/cmd/references.go index 533fcc19b81..6db5ce34e75 100644 --- a/gopls/internal/lsp/cmd/references.go +++ b/gopls/internal/lsp/cmd/references.go @@ -63,10 +63,7 @@ func (r *references) Run(ctx context.Context, args ...string) error { Context: protocol.ReferenceContext{ IncludeDeclaration: r.IncludeDeclaration, }, - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } locations, err := conn.References(ctx, &p) if err != nil { diff --git a/gopls/internal/lsp/cmd/signature.go b/gopls/internal/lsp/cmd/signature.go index 2f34b9bef75..64c892e5ee5 100644 --- a/gopls/internal/lsp/cmd/signature.go +++ b/gopls/internal/lsp/cmd/signature.go @@ -56,14 +56,8 @@ func (r *signature) Run(ctx context.Context, args ...string) error { return err } - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(from.URI()), - }, - Position: loc.Range.Start, - } p := protocol.SignatureHelpParams{ - TextDocumentPositionParams: tdpp, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } s, err := conn.SignatureHelp(ctx, &p) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 9a1f0dd7de3..70d5e5e547e 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -1047,10 +1047,7 @@ func (e *Editor) Completion(ctx context.Context, loc protocol.Location) (*protoc return nil, fmt.Errorf("buffer %q is not open", path) } params := &protocol.CompletionParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: e.TextDocumentIdentifier(path), - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } completions, err := e.Server.Completion(ctx, params) if err != nil { @@ -1122,10 +1119,7 @@ func (e *Editor) References(ctx context.Context, loc protocol.Location) ([]proto return nil, fmt.Errorf("buffer %q is not open", path) } params := &protocol.ReferenceParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: e.TextDocumentIdentifier(path), - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), Context: protocol.ReferenceContext{ IncludeDeclaration: true, }, @@ -1185,10 +1179,7 @@ func (e *Editor) Implementations(ctx context.Context, loc protocol.Location) ([] return nil, fmt.Errorf("buffer %q is not open", path) } params := &protocol.ImplementationParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: e.TextDocumentIdentifier(path), - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } return e.Server.Implementation(ctx, params) } diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 212ba0f3958..5290e618cee 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -154,10 +154,7 @@ func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests } params := &protocol.CallHierarchyPrepareParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } items, err := r.server.PrepareCallHierarchy(r.ctx, params) @@ -678,10 +675,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if err != nil { t.Fatalf("failed for %v: %v", d.Src, err) } - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - } + tdpp := protocol.LocationTextDocumentPositionParams(loc) var locs []protocol.Location var hover *protocol.Hover if d.IsType { @@ -749,10 +743,7 @@ func (r *runner) Implementation(t *testing.T, spn span.Span, wantSpans []span.Sp t.Fatal(err) } gotImpls, err := r.server.Implementation(r.ctx, &protocol.ImplementationParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), }) if err != nil { t.Fatalf("Server.Implementation(%s): %v", spn, err) @@ -780,17 +771,8 @@ func (r *runner) Highlight(t *testing.T, src span.Span, spans []span.Span) { if err != nil { t.Fatal(err) } - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: loc.URI, - }, - Position: loc.Range.Start, - } - if err != nil { - t.Fatalf("Mapper.SpanDocumentPosition(%v) failed: %v", src, err) - } params := &protocol.DocumentHighlightParams{ - TextDocumentPositionParams: tdpp, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } highlights, err := r.server.DocumentHighlight(r.ctx, params) if err != nil { @@ -833,12 +815,8 @@ func (r *runner) Hover(t *testing.T, src span.Span, text string) { if err != nil { t.Fatalf("failed for %v", err) } - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - } params := &protocol.HoverParams{ - TextDocumentPositionParams: tdpp, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } hover, err := r.server.Hover(r.ctx, params) if err != nil { @@ -894,10 +872,7 @@ func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { want[loc] = true } params := &protocol.ReferenceParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - }, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), Context: protocol.ReferenceContext{ IncludeDeclaration: includeDeclaration, }, @@ -1065,12 +1040,8 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare if err != nil { t.Fatalf("failed for %v: %v", src, err) } - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, - Position: loc.Range.Start, - } params := &protocol.PrepareRenameParams{ - TextDocumentPositionParams: tdpp, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } got, err := r.server.PrepareRename(context.Background(), params) if err != nil { @@ -1206,14 +1177,8 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa if err != nil { t.Fatalf("failed for %v: %v", loc, err) } - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(spn.URI()), - }, - Position: loc.Range.Start, - } params := &protocol.SignatureHelpParams{ - TextDocumentPositionParams: tdpp, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } got, err := r.server.SignatureHelp(r.ctx, params) if err != nil { diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go index e1e2578bf34..63f28d1f6f6 100644 --- a/gopls/internal/lsp/protocol/mapper.go +++ b/gopls/internal/lsp/protocol/mapper.go @@ -502,3 +502,11 @@ func (mr MappedRange) Span() span.Span { func (mr MappedRange) String() string { return fmt.Sprint(mr.Span()) } + +// LocationTextDocumentPositionParams converts its argument to its result. +func LocationTextDocumentPositionParams(loc Location) TextDocumentPositionParams { + return TextDocumentPositionParams{ + TextDocument: TextDocumentIdentifier{URI: loc.URI}, + Position: loc.Range.Start, + } +} diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 833d3db0c08..ba5cf7ae8e0 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -35,13 +35,8 @@ func main() { Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") loc := env.RegexpSearch("main.go", `main`) - // TODO(adonovan): define a helper from Location to TextDocumentPositionParams. - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: env.Editor.TextDocumentIdentifier("main.go"), - Position: loc.Range.Start, - } params := &protocol.PrepareRenameParams{ - TextDocumentPositionParams: tdpp, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } _, err := env.Editor.Server.PrepareRename(env.Ctx, params) if err == nil { @@ -142,12 +137,8 @@ func main() { const wantErr = "can't rename package: missing module information for package" Run(t, files, func(t *testing.T, env *Env) { loc := env.RegexpSearch("lib/a.go", "lib") - tdpp := protocol.TextDocumentPositionParams{ - TextDocument: env.Editor.TextDocumentIdentifier("lib/a.go"), - Position: loc.Range.Start, - } params := &protocol.PrepareRenameParams{ - TextDocumentPositionParams: tdpp, + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), } _, err := env.Editor.Server.PrepareRename(env.Ctx, params) if err == nil || !strings.Contains(err.Error(), wantErr) { From f0521587991483386e52549ced4e4970f1be28f2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 26 Jan 2023 17:15:32 -0500 Subject: [PATCH 681/723] gopls/internal/lsp/protocol: move TestBytesOffset Change-Id: Ied6bbcc7a4da1a513e500f1e650ca9b1b51c6254 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463685 Run-TryBot: Alan Donovan Auto-Submit: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot --- gopls/internal/lsp/lsp_test.go | 37 ---------------------- gopls/internal/lsp/protocol/mapper_test.go | 37 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 5290e618cee..47f6b0ad148 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -1332,43 +1332,6 @@ func (r *runner) SelectionRanges(t *testing.T, spn span.Span) { } } -func TestBytesOffset(t *testing.T) { - tests := []struct { - text string - pos protocol.Position - want int - }{ - // U+10400 encodes as [F0 90 90 80] in UTF-8 and [D801 DC00] in UTF-16. - {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0}, - {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1}, - {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1}, - {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5}, - {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6}, - {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1}, - {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3}, - {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: -1}, - {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4}, - {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7}, - {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: -1}, - {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, - {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1}, - {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, - } - - for i, test := range tests { - fname := fmt.Sprintf("test %d", i) - uri := span.URIFromPath(fname) - mapper := protocol.NewMapper(uri, []byte(test.text)) - got, err := mapper.PositionPoint(test.pos) - if err != nil && test.want != -1 { - t.Errorf("%d: unexpected error: %v", i, err) - } - if err == nil && got.Offset() != test.want { - t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got.Offset()) - } - } -} - func (r *runner) collectDiagnostics(view *cache.View) { if r.diagnostics != nil { return diff --git a/gopls/internal/lsp/protocol/mapper_test.go b/gopls/internal/lsp/protocol/mapper_test.go index 5e854812793..0780491de69 100644 --- a/gopls/internal/lsp/protocol/mapper_test.go +++ b/gopls/internal/lsp/protocol/mapper_test.go @@ -401,4 +401,41 @@ func TestRange(t *testing.T) { } } +func TestBytesOffset(t *testing.T) { + tests := []struct { + text string + pos protocol.Position + want int + }{ + // U+10400 encodes as [F0 90 90 80] in UTF-8 and [D801 DC00] in UTF-16. + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0}, + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1}, + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1}, + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5}, + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6}, + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: -1}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: -1}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1}, + {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, + } + + for i, test := range tests { + fname := fmt.Sprintf("test %d", i) + uri := span.URIFromPath(fname) + mapper := protocol.NewMapper(uri, []byte(test.text)) + got, err := mapper.PositionPoint(test.pos) + if err != nil && test.want != -1 { + t.Errorf("%d: unexpected error: %v", i, err) + } + if err == nil && got.Offset() != test.want { + t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got.Offset()) + } + } +} + // -- end -- From 1edcfe78f91c8c05aa9325a7bcd6c13e4e5190b6 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 26 Jan 2023 17:19:04 -0500 Subject: [PATCH 682/723] gopls/internal/regtest/diagnostics: require cgo for TestGoListErrors Fixes golang/go#58103 Change-Id: I378496e5ed791e33932c58d88cfb1f37ff125936 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463686 Reviewed-by: Bryan Mills Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/regtest/diagnostics/golist_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gopls/internal/regtest/diagnostics/golist_test.go b/gopls/internal/regtest/diagnostics/golist_test.go index ec54a92cef0..85b35be024f 100644 --- a/gopls/internal/regtest/diagnostics/golist_test.go +++ b/gopls/internal/regtest/diagnostics/golist_test.go @@ -9,9 +9,12 @@ import ( . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/testenv" ) func TestGoListErrors(t *testing.T) { + testenv.NeedsTool(t, "cgo") + const src = ` -- go.mod -- module a.com From e0659d1f36ace71fc67398f8dcee3ff4811ca633 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 26 Jan 2023 17:55:24 -0500 Subject: [PATCH 683/723] gopls/internal/lsp/source: simplify legacy 'references' func The function used to accept three booleans, the middle of which was always false. By simplifying under that invariant, the other two booleans fall away, along with the 'implementations' query that it used to make. Small steps... Change-Id: Ib520661b822afd85d6f0311253779a6202afd33d Reviewed-on: https://go-review.googlesource.com/c/tools/+/463687 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley --- gopls/internal/lsp/source/implementation.go | 133 -------------------- gopls/internal/lsp/source/references.go | 68 ++-------- gopls/internal/lsp/source/rename.go | 13 +- 3 files changed, 20 insertions(+), 194 deletions(-) diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 50520267e8e..70bb0bd2859 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -14,145 +14,12 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" - "golang.org/x/tools/gopls/internal/lsp/source/methodsets" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" ) var ErrNotAType = errors.New("not a type name or method") -// implementations returns the concrete implementations of the specified -// interface, or the interfaces implemented by the specified concrete type. -// It populates only the definition-related fields of qualifiedObject. -// (Arguably it should return a smaller data type.) -// -// This is the legacy implementation using the type-check-the-world -// strategy. It is still needed by the 'references' command for now, -// but we are moving one step at a time. See implementation2.go for -// the incremental algorithm used by the 'implementations' command. -// TODO(adonovan): eliminate this implementation. -func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) ([]qualifiedObject, error) { - // Find all named types, even local types - // (which can have methods due to promotion). - var ( - allNamed []*types.Named - pkgs = make(map[*types.Package]Package) - ) - metas, err := s.AllMetadata(ctx) - if err != nil { - return nil, err - } - ids := make([]PackageID, len(metas)) - for i, m := range metas { - ids[i] = m.ID - } - packages, err := s.TypeCheck(ctx, TypecheckFull, ids...) - if err != nil { - return nil, err - } - for _, pkg := range packages { - pkgs[pkg.GetTypes()] = pkg - for _, obj := range pkg.GetTypesInfo().Defs { - obj, ok := obj.(*types.TypeName) - // We ignore aliases 'type M = N' to avoid duplicate reporting - // of the Named type N. - if !ok || obj.IsAlias() { - continue - } - if named, ok := obj.Type().(*types.Named); ok { - allNamed = append(allNamed, named) - } - } - } - - qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) - if err != nil { - return nil, err - } - var ( - impls []qualifiedObject - seen = make(map[token.Position]bool) - ) - for _, qo := range qos { - // Ascertain the query identifier (type or method). - var ( - queryType types.Type - queryMethod *types.Func - ) - switch obj := qo.obj.(type) { - case *types.Func: - queryMethod = obj - if recv := obj.Type().(*types.Signature).Recv(); recv != nil { - queryType = methodsets.EnsurePointer(recv.Type()) - } - case *types.TypeName: - queryType = methodsets.EnsurePointer(obj.Type()) - } - - if queryType == nil { - return nil, ErrNotAType - } - - if types.NewMethodSet(queryType).Len() == 0 { - return nil, nil - } - - // Find all the named types that match our query. - for _, named := range allNamed { - var ( - candObj types.Object = named.Obj() - candType = methodsets.EnsurePointer(named) - ) - - if !concreteImplementsIntf(candType, queryType) { - continue - } - - ms := types.NewMethodSet(candType) - if ms.Len() == 0 { - // Skip empty interfaces. - continue - } - - // If client queried a method, look up corresponding candType method. - if queryMethod != nil { - sel := ms.Lookup(queryMethod.Pkg(), queryMethod.Name()) - if sel == nil { - continue - } - candObj = sel.Obj() - } - - pos := safetoken.StartPosition(s.FileSet(), candObj.Pos()) - if candObj == queryMethod || seen[pos] { - continue - } - - pkg := pkgs[candObj.Pkg()] // may be nil (e.g. error) - - // TODO(adonovan): the logic below assumes there is only one - // predeclared (pkg=nil) object of interest, the error type. - // That could change in a future version of Go. - - var posn token.Position - if pkg != nil { - posn = safetoken.StartPosition(pkg.FileSet(), candObj.Pos()) - } - if seen[posn] { - continue - } - seen[posn] = true - - impls = append(impls, qualifiedObject{ - obj: candObj, - pkg: pkg, - }) - } - } - - return impls, nil -} - // concreteImplementsIntf returns true if a is an interface type implemented by // concrete type b, or vice versa. func concreteImplementsIntf(a, b types.Type) bool { diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 5f3fdbb3d34..36f7c3a3983 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -6,7 +6,6 @@ package source import ( "context" - "errors" "fmt" "go/ast" "go/token" @@ -20,7 +19,6 @@ import ( // ReferenceInfo holds information about reference to an identifier in Go source. type ReferenceInfo struct { - Name string MappedRange protocol.MappedRange ident *ast.Ident obj types.Object @@ -49,7 +47,7 @@ func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, // (Arguably it should accept a smaller data type.) // // This implementation serves Server.rename. TODO(adonovan): obviate it. -func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, includeDeclaration, includeInterfaceRefs, includeEmbeddedRefs bool) ([]*ReferenceInfo, error) { +func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject) ([]*ReferenceInfo, error) { var ( references []*ReferenceInfo seen = make(map[positionKey]bool) @@ -71,16 +69,13 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i return nil, err } // Make sure declaration is the first item in the response. - if includeDeclaration { - references = append(references, &ReferenceInfo{ - MappedRange: declIdent.MappedRange, - Name: qos[0].obj.Name(), - ident: declIdent.ident, - obj: qos[0].obj, - pkg: declIdent.pkg, - isDeclaration: true, - }) - } + references = append(references, &ReferenceInfo{ + MappedRange: declIdent.MappedRange, + ident: declIdent.ident, + obj: qos[0].obj, + pkg: declIdent.pkg, + isDeclaration: true, + }) for _, qo := range qos { var searchPkgs []Package @@ -120,9 +115,6 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i // If ident is not a use of qo.obj, skip it, with one exception: // uses of an embedded field can be considered references of the // embedded type name - if !includeEmbeddedRefs { - continue - } v, ok := obj.(*types.Var) if !ok || !v.Embedded() { continue @@ -151,7 +143,6 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i return nil, err } references = append(references, &ReferenceInfo{ - Name: ident.Name, ident: ident, pkg: pkg, obj: obj, @@ -161,25 +152,6 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i } } - // When searching on type name, don't include interface references -- they - // would be things like all references to Stringer for any type that - // happened to have a String method. - _, isType := declIdent.Declaration.obj.(*types.TypeName) - if includeInterfaceRefs && !isType { - // TODO(adonovan): opt: don't go back into the position domain: - // we have complete type information already. - declRange := declIdent.MappedRange.Range() - fh, err := snapshot.GetFile(ctx, declIdent.MappedRange.URI()) - if err != nil { - return nil, err - } - interfaceRefs, err := interfaceReferences(ctx, snapshot, fh, declRange.Start) - if err != nil { - return nil, err - } - references = append(references, interfaceRefs...) - } - return references, nil } @@ -189,27 +161,3 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i func equalOrigin(obj1, obj2 types.Object) bool { return obj1.Pkg() == obj2.Pkg() && obj1.Pos() == obj2.Pos() && obj1.Name() == obj2.Name() } - -// interfaceReferences returns the references to the interfaces implemented by -// the type or method at the given position. -func interfaceReferences(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) ([]*ReferenceInfo, error) { - implementations, err := implementations(ctx, s, f, pp) - if err != nil { - if errors.Is(err, ErrNotAType) { - return nil, nil - } - return nil, err - } - - // Make a separate call to references() for each element - // since it treats the first qualifiedObject as a definition. - var refs []*ReferenceInfo - for _, impl := range implementations { - implRefs, err := references(ctx, s, []qualifiedObject{impl}, false, false, false) - if err != nil { - return nil, err - } - refs = append(refs, implRefs...) - } - return refs, nil -} diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 385dff4a78b..ca3c6943b0c 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -548,6 +548,17 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath try++ localName = fmt.Sprintf("%s%d", newName, try) } + + // renameObj detects various conflicts, including: + // - new name conflicts with a package-level decl in this file; + // - new name hides a package-level decl in another file that + // is actually referenced in this file; + // - new name hides a built-in that is actually referenced + // in this file; + // - a reference in this file to the old package name would + // become shadowed by an intervening declaration that + // uses the new name. + // It returns the edits if no conflict was detected. changes, err := renameObj(ctx, snapshot, localName, qos) if err != nil { return err @@ -587,7 +598,7 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO if !isValidIdentifier(newName) { return nil, fmt.Errorf("invalid identifier to rename: %q", newName) } - refs, err := references(ctx, s, qos, true, false, true) + refs, err := references(ctx, s, qos) if err != nil { return nil, err } From 5ed33dffa5c1096599a4bdeaa78b81aa447cadb4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 26 Jan 2023 22:08:04 -0500 Subject: [PATCH 684/723] gopls/internal/lsp/source: rename: prep for incrementality This change moves into rename.go various declarations that, thanks to recent work, are only used for renaming. (The moved functions have not changed.) It also extracts the lengthy "package name" special case into a separate function, and adds a few comments. The checkRenaming call has been pulled out of renameObj into the callers. In one of these, it was not needed. Change-Id: I4e2d354c098296980b6bcc2fe8ddc2e212e10aa8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463895 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley Auto-Submit: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/protocol/mapper.go | 4 + gopls/internal/lsp/source/implementation.go | 237 +------ gopls/internal/lsp/source/implementation2.go | 4 +- gopls/internal/lsp/source/references.go | 158 ----- gopls/internal/lsp/source/rename.go | 641 +++++++++++++++---- 5 files changed, 521 insertions(+), 523 deletions(-) diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go index 63f28d1f6f6..ee2622326a4 100644 --- a/gopls/internal/lsp/protocol/mapper.go +++ b/gopls/internal/lsp/protocol/mapper.go @@ -460,6 +460,10 @@ func (m *Mapper) OffsetMappedRange(start, end int) (MappedRange, error) { // Construct one by calling Mapper.OffsetMappedRange with start/end offsets. // From the go/token domain, call safetoken.Offsets first, // or use a helper such as ParsedGoFile.MappedPosRange. +// +// Two MappedRanges produced the same Mapper are equal if and only if they +// denote the same range. Two MappedRanges produced by different Mappers +// are unequal even when they represent the same range of the same file. type MappedRange struct { Mapper *Mapper start, end int // valid byte offsets: 0 <= start <= end <= len(Mapper.Content) diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go index 70bb0bd2859..6d165df3f94 100644 --- a/gopls/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -5,20 +5,13 @@ package source import ( - "context" "errors" - "fmt" "go/ast" "go/token" "go/types" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/safetoken" - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/event" ) -var ErrNotAType = errors.New("not a type name or method") +// TODO(adonovan): move these declarations elsewhere. // concreteImplementsIntf returns true if a is an interface type implemented by // concrete type b, or vice versa. @@ -44,234 +37,14 @@ func concreteImplementsIntf(a, b types.Type) bool { return types.AssignableTo(a, b) } -// A qualifiedObject is the result of resolving a reference from an -// identifier to an object. -type qualifiedObject struct { - // definition - obj types.Object // the referenced object - pkg Package // the Package that defines the object (nil => universe) - - // reference (optional) - node ast.Node // the reference (*ast.Ident or *ast.ImportSpec) to the object - sourcePkg Package // the Package containing node -} - var ( - errBuiltin = errors.New("builtin object") + // TODO(adonovan): why do various RPC handlers related to + // IncomingCalls return (nil, nil) on the protocol in response + // to this error? That seems like a violation of the protocol. + // Is it perhaps a workaround for VSCode behavior? errNoObjectFound = errors.New("no object found") ) -// qualifiedObjsAtProtocolPos returns info for all the types.Objects referenced -// at the given position, for the following selection of packages: -// -// 1. all packages (including all test variants), in their workspace parse mode -// 2. if not included above, at least one package containing uri in full parse mode -// -// Finding objects in (1) ensures that we locate references within all -// workspace packages, including in x_test packages. Including (2) ensures that -// we find local references in the current package, for non-workspace packages -// that may be open. -func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, pp protocol.Position) ([]qualifiedObject, error) { - fh, err := s.GetFile(ctx, uri) - if err != nil { - return nil, err - } - content, err := fh.Read() - if err != nil { - return nil, err - } - m := protocol.NewMapper(uri, content) - offset, err := m.PositionOffset(pp) - if err != nil { - return nil, err - } - return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{}) -} - -// A positionKey identifies a byte offset within a file (URI). -// -// When a file has been parsed multiple times in the same FileSet, -// there may be multiple token.Pos values denoting the same logical -// position. In such situations, a positionKey may be used for -// de-duplication. -type positionKey struct { - uri span.URI - offset int -} - -// qualifiedObjsAtLocation finds all objects referenced at offset in uri, -// across all packages in the snapshot. -func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, seen map[positionKey]bool) ([]qualifiedObject, error) { - if seen[key] { - return nil, nil - } - seen[key] = true - - // We search for referenced objects starting with all packages containing the - // current location, and then repeating the search for every distinct object - // location discovered. - // - // In the common case, there should be at most one additional location to - // consider: the definition of the object referenced by the location. But we - // try to be comprehensive in case we ever support variations on build - // constraints. - metas, err := s.MetadataForFile(ctx, key.uri) - if err != nil { - return nil, err - } - ids := make([]PackageID, len(metas)) - for i, m := range metas { - ids[i] = m.ID - } - pkgs, err := s.TypeCheck(ctx, TypecheckWorkspace, ids...) - if err != nil { - return nil, err - } - - // In order to allow basic references/rename/implementations to function when - // non-workspace packages are open, ensure that we have at least one fully - // parsed package for the current file. This allows us to find references - // inside the open package. Use WidestPackage to capture references in test - // files. - hasFullPackage := false - for _, pkg := range pkgs { - if pkg.ParseMode() == ParseFull { - hasFullPackage = true - break - } - } - if !hasFullPackage { - pkg, _, err := PackageForFile(ctx, s, key.uri, TypecheckFull, WidestPackage) - if err != nil { - return nil, err - } - pkgs = append(pkgs, pkg) - } - - // report objects in the order we encounter them. This ensures that the first - // result is at the cursor... - var qualifiedObjs []qualifiedObject - // ...but avoid duplicates. - seenObjs := map[types.Object]bool{} - - for _, searchpkg := range pkgs { - pgf, err := searchpkg.File(key.uri) - if err != nil { - return nil, err - } - pos := pgf.Tok.Pos(key.offset) - - // TODO(adonovan): replace this section with a call to objectsAt(). - path := pathEnclosingObjNode(pgf.File, pos) - if path == nil { - continue - } - var objs []types.Object - switch leaf := path[0].(type) { - case *ast.Ident: - // If leaf represents an implicit type switch object or the type - // switch "assign" variable, expand to all of the type switch's - // implicit objects. - if implicits, _ := typeSwitchImplicits(searchpkg.GetTypesInfo(), path); len(implicits) > 0 { - objs = append(objs, implicits...) - } else { - obj := searchpkg.GetTypesInfo().ObjectOf(leaf) - if obj == nil { - return nil, fmt.Errorf("%w for %q", errNoObjectFound, leaf.Name) - } - objs = append(objs, obj) - } - case *ast.ImportSpec: - // Look up the implicit *types.PkgName. - obj := searchpkg.GetTypesInfo().Implicits[leaf] - if obj == nil { - return nil, fmt.Errorf("%w for import %s", errNoObjectFound, UnquoteImportPath(leaf)) - } - objs = append(objs, obj) - } - - // Get all of the transitive dependencies of the search package. - pkgSet := map[*types.Package]Package{ - searchpkg.GetTypes(): searchpkg, - } - deps := recursiveDeps(s, searchpkg.Metadata())[1:] - // Ignore the error from type checking, but check if the context was - // canceled (which would have caused TypeCheck to exit early). - depPkgs, _ := s.TypeCheck(ctx, TypecheckWorkspace, deps...) - if ctx.Err() != nil { - return nil, ctx.Err() - } - for _, dep := range depPkgs { - // Since we ignored the error from type checking, pkg may be nil. - if dep != nil { - pkgSet[dep.GetTypes()] = dep - } - } - - for _, obj := range objs { - if obj.Parent() == types.Universe { - return nil, fmt.Errorf("%q: %w", obj.Name(), errBuiltin) - } - pkg, ok := pkgSet[obj.Pkg()] - if !ok { - event.Error(ctx, fmt.Sprintf("no package for obj %s: %v", obj, obj.Pkg()), err) - continue - } - qualifiedObjs = append(qualifiedObjs, qualifiedObject{ - obj: obj, - pkg: pkg, - sourcePkg: searchpkg, - node: path[0], - }) - seenObjs[obj] = true - - // If the qualified object is in another file (or more likely, another - // package), it's possible that there is another copy of it in a package - // that we haven't searched, e.g. a test variant. See golang/go#47564. - // - // In order to be sure we've considered all packages, call - // qualifiedObjsAtLocation recursively for all locations we encounter. We - // could probably be more precise here, only continuing the search if obj - // is in another package, but this should be good enough to find all - // uses. - - if key, found := packagePositionKey(pkg, obj.Pos()); found { - otherObjs, err := qualifiedObjsAtLocation(ctx, s, key, seen) - if err != nil { - return nil, err - } - for _, other := range otherObjs { - if !seenObjs[other.obj] { - qualifiedObjs = append(qualifiedObjs, other) - seenObjs[other.obj] = true - } - } - } else { - return nil, fmt.Errorf("missing file for position of %q in %q", obj.Name(), obj.Pkg().Name()) - } - } - } - // Return an error if no objects were found since callers will assume that - // the slice has at least 1 element. - if len(qualifiedObjs) == 0 { - return nil, errNoObjectFound - } - return qualifiedObjs, nil -} - -// packagePositionKey finds the positionKey for the given pos. -// -// The second result reports whether the position was found. -func packagePositionKey(pkg Package, pos token.Pos) (positionKey, bool) { - for _, pgf := range pkg.CompiledGoFiles() { - offset, err := safetoken.Offset(pgf.Tok, pos) - if err == nil { - return positionKey{pgf.URI, offset}, true - } - } - return positionKey{}, false -} - // pathEnclosingObjNode returns the AST path to the object-defining // node associated with pos. "Object-defining" means either an // *ast.Ident mapped directly to a types.Object or an ast.Node mapped diff --git a/gopls/internal/lsp/source/implementation2.go b/gopls/internal/lsp/source/implementation2.go index a776c038951..c7ff5bfb6cf 100644 --- a/gopls/internal/lsp/source/implementation2.go +++ b/gopls/internal/lsp/source/implementation2.go @@ -137,11 +137,9 @@ func implementations2(ctx context.Context, snapshot Snapshot, fh FileHandle, pp queryType = recv.Type() queryMethodID = obj.Id() } - default: - return nil, fmt.Errorf("%s is not a type or method", id.Name) } if queryType == nil { - return nil, ErrNotAType + return nil, fmt.Errorf("%s is not a type or method", id.Name) } // Compute the method-set fingerprint used as a key to the global search. diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go index 36f7c3a3983..fcbbc336d90 100644 --- a/gopls/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -3,161 +3,3 @@ // license that can be found in the LICENSE file. package source - -import ( - "context" - "fmt" - "go/ast" - "go/token" - "go/types" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/safetoken" - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/bug" -) - -// ReferenceInfo holds information about reference to an identifier in Go source. -type ReferenceInfo struct { - MappedRange protocol.MappedRange - ident *ast.Ident - obj types.Object - pkg Package - isDeclaration bool -} - -// parsePackageNameDecl is a convenience function that parses and -// returns the package name declaration of file fh, and reports -// whether the position ppos lies within it. -func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, ppos protocol.Position) (*ParsedGoFile, bool, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) - if err != nil { - return nil, false, err - } - // Careful: because we used ParseHeader, - // pgf.Pos(ppos) may be beyond EOF => (0, err). - pos, _ := pgf.PositionPos(ppos) - return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil -} - -// references is a helper function to avoid recomputing qualifiedObjsAtProtocolPos. -// The first element of qos is considered to be the declaration; -// if isDeclaration, the first result is an extra item for it. -// Only the definition-related fields of qualifiedObject are used. -// (Arguably it should accept a smaller data type.) -// -// This implementation serves Server.rename. TODO(adonovan): obviate it. -func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject) ([]*ReferenceInfo, error) { - var ( - references []*ReferenceInfo - seen = make(map[positionKey]bool) - ) - - pos := qos[0].obj.Pos() - if pos == token.NoPos { - return nil, fmt.Errorf("no position for %s", qos[0].obj) // e.g. error.Error - } - // Inv: qos[0].pkg != nil, since Pos is valid. - // Inv: qos[*].pkg != nil, since all qos are logically the same declaration. - filename := safetoken.StartPosition(qos[0].pkg.FileSet(), pos).Filename - pgf, err := qos[0].pkg.File(span.URIFromPath(filename)) - if err != nil { - return nil, err - } - declIdent, err := findIdentifier(ctx, snapshot, qos[0].pkg, pgf, qos[0].obj.Pos()) - if err != nil { - return nil, err - } - // Make sure declaration is the first item in the response. - references = append(references, &ReferenceInfo{ - MappedRange: declIdent.MappedRange, - ident: declIdent.ident, - obj: qos[0].obj, - pkg: declIdent.pkg, - isDeclaration: true, - }) - - for _, qo := range qos { - var searchPkgs []Package - - // Only search dependents if the object is exported. - if qo.obj.Exported() { - // If obj is a package-level object, we need only search - // among direct reverse dependencies. - // TODO(adonovan): opt: this will still spuriously search - // transitively for (e.g.) capitalized local variables. - // We could do better by checking for an objectpath. - transitive := qo.obj.Pkg().Scope().Lookup(qo.obj.Name()) != qo.obj - rdeps, err := snapshot.ReverseDependencies(ctx, qo.pkg.Metadata().ID, transitive) - if err != nil { - return nil, err - } - ids := make([]PackageID, 0, len(rdeps)) - for _, rdep := range rdeps { - ids = append(ids, rdep.ID) - } - // TODO(adonovan): opt: build a search index - // that doesn't require type checking. - reverseDeps, err := snapshot.TypeCheck(ctx, TypecheckFull, ids...) - if err != nil { - return nil, err - } - searchPkgs = append(searchPkgs, reverseDeps...) - } - // Add the package in which the identifier is declared. - searchPkgs = append(searchPkgs, qo.pkg) - for _, pkg := range searchPkgs { - for ident, obj := range pkg.GetTypesInfo().Uses { - // For instantiated objects (as in methods or fields on instantiated - // types), we may not have pointer-identical objects but still want to - // consider them references. - if !equalOrigin(obj, qo.obj) { - // If ident is not a use of qo.obj, skip it, with one exception: - // uses of an embedded field can be considered references of the - // embedded type name - v, ok := obj.(*types.Var) - if !ok || !v.Embedded() { - continue - } - named, ok := v.Type().(*types.Named) - if !ok || named.Obj() != qo.obj { - continue - } - } - key, found := packagePositionKey(pkg, ident.Pos()) - if !found { - bug.Reportf("ident %v (pos: %v) not found in package %v", ident.Name, ident.Pos(), pkg.Metadata().ID) - continue - } - if seen[key] { - continue - } - seen[key] = true - filename := pkg.FileSet().File(ident.Pos()).Name() - pgf, err := pkg.File(span.URIFromPath(filename)) - if err != nil { - return nil, err - } - rng, err := pgf.PosMappedRange(ident.Pos(), ident.End()) - if err != nil { - return nil, err - } - references = append(references, &ReferenceInfo{ - ident: ident, - pkg: pkg, - obj: obj, - MappedRange: rng, - }) - } - } - } - - return references, nil -} - -// equalOrigin reports whether obj1 and obj2 have equivalent origin object. -// This may be the case even if obj1 != obj2, if one or both of them is -// instantiated. -func equalOrigin(obj1, obj2 types.Object) bool { - return obj1.Pkg() == obj2.Pkg() && obj1.Pos() == obj2.Pos() && obj1.Name() == obj2.Name() -} diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index ca3c6943b0c..03823a949e1 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -23,6 +23,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/refactor/satisfy" @@ -63,6 +64,7 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot return nil, err, err } if inPackageName { + // Does the client support file renaming? fileRenameSupported := false for _, op := range snapshot.View().Options().SupportedResourceOperations { if op == protocol.Rename { @@ -70,11 +72,12 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot break } } - if !fileRenameSupported { err := errors.New("can't rename package: LSP client does not support file renaming") return nil, err, err } + + // Check validity of the metadata for the file's containing package. fileMeta, err := snapshot.MetadataForFile(ctx, f.URI()) if err != nil { return nil, err, err @@ -84,24 +87,19 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot err := fmt.Errorf("no packages found for file %q", f.URI()) return nil, err, err } - meta := fileMeta[0] - if meta.Name == "main" { err := errors.New("can't rename package \"main\"") return nil, err, err } - if strings.HasSuffix(string(meta.Name), "_test") { err := errors.New("can't rename x_test packages") return nil, err, err } - if meta.Module == nil { err := fmt.Errorf("can't rename package: missing module information for package %q", meta.PkgPath) return nil, err, err } - if meta.Module.Path == string(meta.PkgPath) { err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path) return nil, err, err @@ -118,17 +116,20 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot }, nil, nil } + // Prepare for ordinary (non-package) renaming. + qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp) if err != nil { - return nil, nil, err + return nil, nil, err // => suppress error to user } node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg if err := checkRenamable(obj); err != nil { - return nil, nil, err + return nil, nil, err // => suppress error to user } + result, err := computePrepareRenameResp(ctx, snapshot, pkg, node, obj.Name()) if err != nil { - return nil, nil, err + return nil, nil, err // -> suppress error to user } return result, nil, nil } @@ -149,7 +150,6 @@ func computePrepareRenameResp(ctx context.Context, snapshot Snapshot, pkg Packag }, nil } -// checkRenamable verifies if an obj may be renamed. func checkRenamable(obj types.Object) error { if v, ok := obj.(*types.Var); ok && v.Embedded() { return errors.New("can't rename embedded fields: rename the type directly or name the field") @@ -167,152 +167,164 @@ func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, ctx, done := event.Start(ctx, "source.Rename") defer done() - // Is the cursor within the package name declaration? - _, inPackageName, err := parsePackageNameDecl(ctx, s, f, pp) + // Cursor within package name declaration? + if _, inPackageName, err := parsePackageNameDecl(ctx, s, f, pp); err != nil { + return nil, false, err + } else if inPackageName { + return renamePackageName(ctx, s, f, pp, newName) + } + + // ordinary (non-package) rename + qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) + if err != nil { + return nil, false, err + } + if err := checkRenamable(qos[0].obj); err != nil { + return nil, false, err + } + if qos[0].obj.Name() == newName { + return nil, false, fmt.Errorf("old and new names are the same: %s", newName) + } + if !isValidIdentifier(newName) { + return nil, false, fmt.Errorf("invalid identifier to rename: %q", newName) + } + result, err := renameObj(ctx, s, newName, qos) if err != nil { return nil, false, err } - if inPackageName { - if !isValidIdentifier(newName) { - return nil, true, fmt.Errorf("%q is not a valid identifier", newName) - } - fileMeta, err := s.MetadataForFile(ctx, f.URI()) - if err != nil { - return nil, true, err - } + return result, false, nil +} - if len(fileMeta) == 0 { - return nil, true, fmt.Errorf("no packages found for file %q", f.URI()) - } +func renamePackageName(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, bool, error) { + if !isValidIdentifier(newName) { + return nil, true, fmt.Errorf("%q is not a valid identifier", newName) + } - // We need metadata for the relevant package and module paths. These should - // be the same for all packages containing the file. - // - // TODO(rfindley): we mix package path and import path here haphazardly. - // Fix this. - meta := fileMeta[0] - oldPath := meta.PkgPath - var modulePath PackagePath - if mi := meta.Module; mi == nil { - return nil, true, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PkgPath) - } else { - modulePath = PackagePath(mi.Path) - } + fileMeta, err := s.MetadataForFile(ctx, f.URI()) + if err != nil { + return nil, true, err + } - if strings.HasSuffix(newName, "_test") { - return nil, true, fmt.Errorf("cannot rename to _test package") - } + if len(fileMeta) == 0 { + return nil, true, fmt.Errorf("no packages found for file %q", f.URI()) + } + + // We need metadata for the relevant package and module paths. These should + // be the same for all packages containing the file. + // + // TODO(rfindley): we mix package path and import path here haphazardly. + // Fix this. + meta := fileMeta[0] + oldPath := meta.PkgPath + var modulePath PackagePath + if mi := meta.Module; mi == nil { + return nil, true, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PkgPath) + } else { + modulePath = PackagePath(mi.Path) + } - metadata, err := s.AllMetadata(ctx) + if strings.HasSuffix(newName, "_test") { + return nil, true, fmt.Errorf("cannot rename to _test package") + } + + metadata, err := s.AllMetadata(ctx) + if err != nil { + return nil, true, err + } + + renamingEdits, err := renamePackage(ctx, s, modulePath, oldPath, PackageName(newName), metadata) + if err != nil { + return nil, true, err + } + + oldBase := filepath.Dir(span.URI.Filename(f.URI())) + newPkgDir := filepath.Join(filepath.Dir(oldBase), newName) + + // TODO: should this operate on all go.mod files, irrespective of whether they are included in the workspace? + // Get all active mod files in the workspace + modFiles := s.ModFiles() + for _, m := range modFiles { + fh, err := s.GetFile(ctx, m) if err != nil { return nil, true, err } - - renamingEdits, err := renamePackage(ctx, s, modulePath, oldPath, PackageName(newName), metadata) + pm, err := s.ParseMod(ctx, fh) if err != nil { return nil, true, err } - oldBase := filepath.Dir(span.URI.Filename(f.URI())) - newPkgDir := filepath.Join(filepath.Dir(oldBase), newName) + modFileDir := filepath.Dir(pm.URI.Filename()) + affectedReplaces := []*modfile.Replace{} - // TODO: should this operate on all go.mod files, irrespective of whether they are included in the workspace? - // Get all active mod files in the workspace - modFiles := s.ModFiles() - for _, m := range modFiles { - fh, err := s.GetFile(ctx, m) - if err != nil { - return nil, true, err - } - pm, err := s.ParseMod(ctx, fh) - if err != nil { - return nil, true, err + // Check if any replace directives need to be fixed + for _, r := range pm.File.Replace { + if !strings.HasPrefix(r.New.Path, "/") && !strings.HasPrefix(r.New.Path, "./") && !strings.HasPrefix(r.New.Path, "../") { + continue } - modFileDir := filepath.Dir(pm.URI.Filename()) - affectedReplaces := []*modfile.Replace{} + replacedPath := r.New.Path + if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { + replacedPath = filepath.Join(modFileDir, r.New.Path) + } - // Check if any replace directives need to be fixed - for _, r := range pm.File.Replace { - if !strings.HasPrefix(r.New.Path, "/") && !strings.HasPrefix(r.New.Path, "./") && !strings.HasPrefix(r.New.Path, "../") { - continue - } + // TODO: Is there a risk of converting a '\' delimited replacement to a '/' delimited replacement? + if !strings.HasPrefix(filepath.ToSlash(replacedPath)+"/", filepath.ToSlash(oldBase)+"/") { + continue // not affected by the package renaming + } - replacedPath := r.New.Path - if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { - replacedPath = filepath.Join(modFileDir, r.New.Path) - } + affectedReplaces = append(affectedReplaces, r) + } - // TODO: Is there a risk of converting a '\' delimited replacement to a '/' delimited replacement? - if !strings.HasPrefix(filepath.ToSlash(replacedPath)+"/", filepath.ToSlash(oldBase)+"/") { - continue // not affected by the package renaming - } + if len(affectedReplaces) == 0 { + continue + } + copied, err := modfile.Parse("", pm.Mapper.Content, nil) + if err != nil { + return nil, true, err + } - affectedReplaces = append(affectedReplaces, r) + for _, r := range affectedReplaces { + replacedPath := r.New.Path + if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { + replacedPath = filepath.Join(modFileDir, r.New.Path) } - if len(affectedReplaces) == 0 { - continue - } - copied, err := modfile.Parse("", pm.Mapper.Content, nil) + suffix := strings.TrimPrefix(replacedPath, string(oldBase)) + + newReplacedPath, err := filepath.Rel(modFileDir, newPkgDir+suffix) if err != nil { return nil, true, err } - for _, r := range affectedReplaces { - replacedPath := r.New.Path - if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { - replacedPath = filepath.Join(modFileDir, r.New.Path) - } - - suffix := strings.TrimPrefix(replacedPath, string(oldBase)) - - newReplacedPath, err := filepath.Rel(modFileDir, newPkgDir+suffix) - if err != nil { - return nil, true, err - } - - newReplacedPath = filepath.ToSlash(newReplacedPath) - - if !strings.HasPrefix(newReplacedPath, "/") && !strings.HasPrefix(newReplacedPath, "../") { - newReplacedPath = "./" + newReplacedPath - } + newReplacedPath = filepath.ToSlash(newReplacedPath) - if err := copied.AddReplace(r.Old.Path, "", newReplacedPath, ""); err != nil { - return nil, true, err - } + if !strings.HasPrefix(newReplacedPath, "/") && !strings.HasPrefix(newReplacedPath, "../") { + newReplacedPath = "./" + newReplacedPath } - copied.Cleanup() - newContent, err := copied.Format() - if err != nil { - return nil, true, err - } - - // Calculate the edits to be made due to the change. - diff := s.View().Options().ComputeEdits(string(pm.Mapper.Content), string(newContent)) - modFileEdits, err := ToProtocolEdits(pm.Mapper, diff) - if err != nil { + if err := copied.AddReplace(r.Old.Path, "", newReplacedPath, ""); err != nil { return nil, true, err } + } - renamingEdits[pm.URI] = append(renamingEdits[pm.URI], modFileEdits...) + copied.Cleanup() + newContent, err := copied.Format() + if err != nil { + return nil, true, err } - return renamingEdits, true, nil - } + // Calculate the edits to be made due to the change. + diff := s.View().Options().ComputeEdits(string(pm.Mapper.Content), string(newContent)) + modFileEdits, err := ToProtocolEdits(pm.Mapper, diff) + if err != nil { + return nil, true, err + } - qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) - if err != nil { - return nil, false, err - } - result, err := renameObj(ctx, s, newName, qos) - if err != nil { - return nil, false, err + renamingEdits[pm.URI] = append(renamingEdits[pm.URI], modFileEdits...) } - return result, false, nil + return renamingEdits, true, nil } // renamePackage computes all workspace edits required to rename the package @@ -486,6 +498,11 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath // If the import does not explicitly specify // a local name, then we need to invoke the // type checker to locate references to update. + // + // TODO(adonovan): is this actually true? + // Renaming an import with a local name can still + // cause conflicts: shadowing of built-ins, or of + // package-level decls in the same or another file. if imp.Name == nil { needsTypeCheck[rdep.ID] = append(needsTypeCheck[rdep.ID], uri) } @@ -559,6 +576,11 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath // become shadowed by an intervening declaration that // uses the new name. // It returns the edits if no conflict was detected. + // + // TODO(adonovan): reduce the strength of this operation + // since, for imports specifically, it should require only + // the current file and the current package, which we + // already have. Finding references is trivial (Info.Uses). changes, err := renameObj(ctx, snapshot, localName, qos) if err != nil { return err @@ -587,17 +609,6 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath // renameObj returns a map of TextEdits for renaming an identifier within a file // and boolean value of true if there is no renaming conflicts and false otherwise. func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedObject) (map[span.URI][]protocol.TextEdit, error) { - obj := qos[0].obj - - if err := checkRenamable(obj); err != nil { - return nil, err - } - if obj.Name() == newName { - return nil, fmt.Errorf("old and new names are the same: %s", newName) - } - if !isValidIdentifier(newName) { - return nil, fmt.Errorf("invalid identifier to rename: %q", newName) - } refs, err := references(ctx, s, qos) if err != nil { return nil, err @@ -607,7 +618,7 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO snapshot: s, refs: refs, objsToUpdate: make(map[types.Object]bool), - from: obj.Name(), + from: qos[0].obj.Name(), to: newName, packages: make(map[*types.Package]Package), } @@ -636,7 +647,7 @@ func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedO } } if r.hadConflicts { - return nil, fmt.Errorf(r.errors) + return nil, fmt.Errorf("%s", r.errors) } changes, err := r.update() @@ -829,3 +840,373 @@ func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.Edit, error) { New: newText, }, nil } + +// qualifiedObjsAtProtocolPos returns info for all the types.Objects referenced +// at the given position, for the following selection of packages: +// +// 1. all packages (including all test variants), in their workspace parse mode +// 2. if not included above, at least one package containing uri in full parse mode +// +// Finding objects in (1) ensures that we locate references within all +// workspace packages, including in x_test packages. Including (2) ensures that +// we find local references in the current package, for non-workspace packages +// that may be open. +func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, pp protocol.Position) ([]qualifiedObject, error) { + fh, err := s.GetFile(ctx, uri) + if err != nil { + return nil, err + } + content, err := fh.Read() + if err != nil { + return nil, err + } + m := protocol.NewMapper(uri, content) + offset, err := m.PositionOffset(pp) + if err != nil { + return nil, err + } + return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{}) +} + +// A qualifiedObject is the result of resolving a reference from an +// identifier to an object. +type qualifiedObject struct { + // definition + obj types.Object // the referenced object + pkg Package // the Package that defines the object (nil => universe) + + // reference (optional) + node ast.Node // the reference (*ast.Ident or *ast.ImportSpec) to the object + sourcePkg Package // the Package containing node +} + +// A positionKey identifies a byte offset within a file (URI). +// +// When a file has been parsed multiple times in the same FileSet, +// there may be multiple token.Pos values denoting the same logical +// position. In such situations, a positionKey may be used for +// de-duplication. +type positionKey struct { + uri span.URI + offset int +} + +// qualifiedObjsAtLocation finds all objects referenced at offset in uri, +// across all packages in the snapshot. +func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, seen map[positionKey]bool) ([]qualifiedObject, error) { + if seen[key] { + return nil, nil + } + seen[key] = true + + // We search for referenced objects starting with all packages containing the + // current location, and then repeating the search for every distinct object + // location discovered. + // + // In the common case, there should be at most one additional location to + // consider: the definition of the object referenced by the location. But we + // try to be comprehensive in case we ever support variations on build + // constraints. + metas, err := s.MetadataForFile(ctx, key.uri) + if err != nil { + return nil, err + } + ids := make([]PackageID, len(metas)) + for i, m := range metas { + ids[i] = m.ID + } + pkgs, err := s.TypeCheck(ctx, TypecheckWorkspace, ids...) + if err != nil { + return nil, err + } + + // In order to allow basic references/rename/implementations to function when + // non-workspace packages are open, ensure that we have at least one fully + // parsed package for the current file. This allows us to find references + // inside the open package. Use WidestPackage to capture references in test + // files. + hasFullPackage := false + for _, pkg := range pkgs { + if pkg.ParseMode() == ParseFull { + hasFullPackage = true + break + } + } + if !hasFullPackage { + pkg, _, err := PackageForFile(ctx, s, key.uri, TypecheckFull, WidestPackage) + if err != nil { + return nil, err + } + pkgs = append(pkgs, pkg) + } + + // report objects in the order we encounter them. This ensures that the first + // result is at the cursor... + var qualifiedObjs []qualifiedObject + // ...but avoid duplicates. + seenObjs := map[types.Object]bool{} + + for _, searchpkg := range pkgs { + pgf, err := searchpkg.File(key.uri) + if err != nil { + return nil, err + } + pos := pgf.Tok.Pos(key.offset) + + // TODO(adonovan): replace this section with a call to objectsAt(). + path := pathEnclosingObjNode(pgf.File, pos) + if path == nil { + continue + } + var objs []types.Object + switch leaf := path[0].(type) { + case *ast.Ident: + // If leaf represents an implicit type switch object or the type + // switch "assign" variable, expand to all of the type switch's + // implicit objects. + if implicits, _ := typeSwitchImplicits(searchpkg.GetTypesInfo(), path); len(implicits) > 0 { + objs = append(objs, implicits...) + } else { + obj := searchpkg.GetTypesInfo().ObjectOf(leaf) + if obj == nil { + return nil, fmt.Errorf("no object found for %q", leaf.Name) + } + objs = append(objs, obj) + } + case *ast.ImportSpec: + // Look up the implicit *types.PkgName. + obj := searchpkg.GetTypesInfo().Implicits[leaf] + if obj == nil { + return nil, fmt.Errorf("no object found for import %s", UnquoteImportPath(leaf)) + } + objs = append(objs, obj) + } + + // Get all of the transitive dependencies of the search package. + pkgSet := map[*types.Package]Package{ + searchpkg.GetTypes(): searchpkg, + } + deps := recursiveDeps(s, searchpkg.Metadata())[1:] + // Ignore the error from type checking, but check if the context was + // canceled (which would have caused TypeCheck to exit early). + depPkgs, _ := s.TypeCheck(ctx, TypecheckWorkspace, deps...) + if ctx.Err() != nil { + return nil, ctx.Err() + } + for _, dep := range depPkgs { + // Since we ignored the error from type checking, pkg may be nil. + if dep != nil { + pkgSet[dep.GetTypes()] = dep + } + } + + for _, obj := range objs { + if obj.Parent() == types.Universe { + return nil, fmt.Errorf("%q: builtin object", obj.Name()) + } + pkg, ok := pkgSet[obj.Pkg()] + if !ok { + event.Error(ctx, fmt.Sprintf("no package for obj %s: %v", obj, obj.Pkg()), err) + continue + } + qualifiedObjs = append(qualifiedObjs, qualifiedObject{ + obj: obj, + pkg: pkg, + sourcePkg: searchpkg, + node: path[0], + }) + seenObjs[obj] = true + + // If the qualified object is in another file (or more likely, another + // package), it's possible that there is another copy of it in a package + // that we haven't searched, e.g. a test variant. See golang/go#47564. + // + // In order to be sure we've considered all packages, call + // qualifiedObjsAtLocation recursively for all locations we encounter. We + // could probably be more precise here, only continuing the search if obj + // is in another package, but this should be good enough to find all + // uses. + + if key, found := packagePositionKey(pkg, obj.Pos()); found { + otherObjs, err := qualifiedObjsAtLocation(ctx, s, key, seen) + if err != nil { + return nil, err + } + for _, other := range otherObjs { + if !seenObjs[other.obj] { + qualifiedObjs = append(qualifiedObjs, other) + seenObjs[other.obj] = true + } + } + } else { + return nil, fmt.Errorf("missing file for position of %q in %q", obj.Name(), obj.Pkg().Name()) + } + } + } + // Return an error if no objects were found since callers will assume that + // the slice has at least 1 element. + if len(qualifiedObjs) == 0 { + return nil, errNoObjectFound + } + return qualifiedObjs, nil +} + +// packagePositionKey finds the positionKey for the given pos. +// +// The second result reports whether the position was found. +func packagePositionKey(pkg Package, pos token.Pos) (positionKey, bool) { + for _, pgf := range pkg.CompiledGoFiles() { + offset, err := safetoken.Offset(pgf.Tok, pos) + if err == nil { + return positionKey{pgf.URI, offset}, true + } + } + return positionKey{}, false +} + +// ReferenceInfo holds information about reference to an identifier in Go source. +type ReferenceInfo struct { + MappedRange protocol.MappedRange + ident *ast.Ident + obj types.Object + pkg Package + isDeclaration bool +} + +// references is a helper function to avoid recomputing qualifiedObjsAtProtocolPos. +// The first element of qos is considered to be the declaration; +// if isDeclaration, the first result is an extra item for it. +// Only the definition-related fields of qualifiedObject are used. +// (Arguably it should accept a smaller data type.) +// +// This implementation serves Server.rename. TODO(adonovan): obviate it. +func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject) ([]*ReferenceInfo, error) { + var ( + references []*ReferenceInfo + seen = make(map[positionKey]bool) + ) + + pos := qos[0].obj.Pos() + if pos == token.NoPos { + return nil, fmt.Errorf("no position for %s", qos[0].obj) // e.g. error.Error + } + // Inv: qos[0].pkg != nil, since Pos is valid. + // Inv: qos[*].pkg != nil, since all qos are logically the same declaration. + filename := safetoken.StartPosition(qos[0].pkg.FileSet(), pos).Filename + pgf, err := qos[0].pkg.File(span.URIFromPath(filename)) + if err != nil { + return nil, err + } + declIdent, err := findIdentifier(ctx, snapshot, qos[0].pkg, pgf, qos[0].obj.Pos()) + if err != nil { + return nil, err + } + // Make sure declaration is the first item in the response. + references = append(references, &ReferenceInfo{ + MappedRange: declIdent.MappedRange, + ident: declIdent.ident, + obj: qos[0].obj, + pkg: declIdent.pkg, + isDeclaration: true, + }) + + for _, qo := range qos { + var searchPkgs []Package + + // Only search dependents if the object is exported. + if qo.obj.Exported() { + // If obj is a package-level object, we need only search + // among direct reverse dependencies. + // TODO(adonovan): opt: this will still spuriously search + // transitively for (e.g.) capitalized local variables. + // We could do better by checking for an objectpath. + transitive := qo.obj.Pkg().Scope().Lookup(qo.obj.Name()) != qo.obj + rdeps, err := snapshot.ReverseDependencies(ctx, qo.pkg.Metadata().ID, transitive) + if err != nil { + return nil, err + } + ids := make([]PackageID, 0, len(rdeps)) + for _, rdep := range rdeps { + ids = append(ids, rdep.ID) + } + // TODO(adonovan): opt: build a search index + // that doesn't require type checking. + reverseDeps, err := snapshot.TypeCheck(ctx, TypecheckFull, ids...) + if err != nil { + return nil, err + } + searchPkgs = append(searchPkgs, reverseDeps...) + } + // Add the package in which the identifier is declared. + searchPkgs = append(searchPkgs, qo.pkg) + for _, pkg := range searchPkgs { + for ident, obj := range pkg.GetTypesInfo().Uses { + // For instantiated objects (as in methods or fields on instantiated + // types), we may not have pointer-identical objects but still want to + // consider them references. + if !equalOrigin(obj, qo.obj) { + // If ident is not a use of qo.obj, skip it, with one exception: + // uses of an embedded field can be considered references of the + // embedded type name + v, ok := obj.(*types.Var) + if !ok || !v.Embedded() { + continue + } + named, ok := v.Type().(*types.Named) + if !ok || named.Obj() != qo.obj { + continue + } + } + key, found := packagePositionKey(pkg, ident.Pos()) + if !found { + bug.Reportf("ident %v (pos: %v) not found in package %v", ident.Name, ident.Pos(), pkg.Metadata().ID) + continue + } + if seen[key] { + continue + } + seen[key] = true + filename := pkg.FileSet().File(ident.Pos()).Name() + pgf, err := pkg.File(span.URIFromPath(filename)) + if err != nil { + return nil, err + } + rng, err := pgf.PosMappedRange(ident.Pos(), ident.End()) + if err != nil { + return nil, err + } + references = append(references, &ReferenceInfo{ + ident: ident, + pkg: pkg, + obj: obj, + MappedRange: rng, + }) + } + } + } + + return references, nil +} + +// equalOrigin reports whether obj1 and obj2 have equivalent origin object. +// This may be the case even if obj1 != obj2, if one or both of them is +// instantiated. +func equalOrigin(obj1, obj2 types.Object) bool { + return obj1.Pkg() == obj2.Pkg() && obj1.Pos() == obj2.Pos() && obj1.Name() == obj2.Name() +} + +// parsePackageNameDecl is a convenience function that parses and +// returns the package name declaration of file fh, and reports +// whether the position ppos lies within it. +// +// Note: also used by references2. +func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, ppos protocol.Position) (*ParsedGoFile, bool, error) { + pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) + if err != nil { + return nil, false, err + } + // Careful: because we used ParseHeader, + // pgf.Pos(ppos) may be beyond EOF => (0, err). + pos, _ := pgf.PositionPos(ppos) + return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil +} From 1aa7e72e7264761bb7d50bea1d6bdb885e711f9b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 27 Jan 2023 11:02:04 -0500 Subject: [PATCH 685/723] gopls/internal/lsp/source: avoid qualifiedObjectsAtProtocolPos This change to PrepareRename eliminates one of the last two calls to qualifiedObjectsAtProtocolPos. All it needs is the current file and the object name, so it was overkill. The last use (from Rename) will be removed in a follow-up; it is much more involved. Change-Id: I64a06d95f7d2a88ace0f3c168bad7f0a8c0a7a04 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463896 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Auto-Submit: Alan Donovan --- gopls/internal/lsp/source/references2.go | 19 +-- gopls/internal/lsp/source/rename.go | 156 +++++++++++------------ 2 files changed, 85 insertions(+), 90 deletions(-) diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index 1a01eb98a85..d34e31246c8 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -223,7 +223,7 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp if err != nil { return nil, err } - candidates, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) + candidates, _, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) if err != nil { return nil, err } @@ -446,7 +446,7 @@ func localReferences(ctx context.Context, snapshot Snapshot, declURI span.URI, d if err != nil { return err } - targets, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) + targets, _, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) if err != nil { return err // unreachable? (probably caught earlier) } @@ -521,11 +521,12 @@ func effectiveReceiver(obj types.Object) types.Type { // position. // // Each object is mapped to the syntax node that was treated as an -// identifier, which is not always an ast.Ident. -func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Object]ast.Node, error) { +// identifier, which is not always an ast.Ident. The second component +// of the result is the innermost node enclosing pos. +func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Object]ast.Node, ast.Node, error) { path := pathEnclosingObjNode(file, pos) if path == nil { - return nil, ErrNoIdentFound + return nil, nil, ErrNoIdentFound } targets := make(map[types.Object]ast.Node) @@ -542,7 +543,7 @@ func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Objec } else { obj := info.ObjectOf(leaf) if obj == nil { - return nil, fmt.Errorf("%w for %q", errNoObjectFound, leaf.Name) + return nil, nil, fmt.Errorf("%w for %q", errNoObjectFound, leaf.Name) } targets[obj] = leaf } @@ -550,15 +551,15 @@ func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Objec // Look up the implicit *types.PkgName. obj := info.Implicits[leaf] if obj == nil { - return nil, fmt.Errorf("%w for import %s", errNoObjectFound, UnquoteImportPath(leaf)) + return nil, nil, fmt.Errorf("%w for import %s", errNoObjectFound, UnquoteImportPath(leaf)) } targets[obj] = leaf } if len(targets) == 0 { - return nil, fmt.Errorf("objectAt: internal error: no targets") // can't happen + return nil, nil, fmt.Errorf("objectAt: internal error: no targets") // can't happen } - return targets, nil + return targets, path[0], nil } // globalReferences reports each cross-package reference to one of the diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index 03823a949e1..e38ec5b75f3 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -59,94 +59,98 @@ func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp prot defer done() // Is the cursor within the package name declaration? - pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) - if err != nil { + if pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp); err != nil { return nil, err, err + } else if inPackageName { + item, err := prepareRenamePackageName(ctx, snapshot, pgf) + return item, err, err } - if inPackageName { - // Does the client support file renaming? - fileRenameSupported := false - for _, op := range snapshot.View().Options().SupportedResourceOperations { - if op == protocol.Rename { - fileRenameSupported = true - break - } - } - if !fileRenameSupported { - err := errors.New("can't rename package: LSP client does not support file renaming") - return nil, err, err - } - - // Check validity of the metadata for the file's containing package. - fileMeta, err := snapshot.MetadataForFile(ctx, f.URI()) - if err != nil { - return nil, err, err - } - - if len(fileMeta) == 0 { - err := fmt.Errorf("no packages found for file %q", f.URI()) - return nil, err, err - } - meta := fileMeta[0] - if meta.Name == "main" { - err := errors.New("can't rename package \"main\"") - return nil, err, err - } - if strings.HasSuffix(string(meta.Name), "_test") { - err := errors.New("can't rename x_test packages") - return nil, err, err - } - if meta.Module == nil { - err := fmt.Errorf("can't rename package: missing module information for package %q", meta.PkgPath) - return nil, err, err - } - if meta.Module.Path == string(meta.PkgPath) { - err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path) - return nil, err, err - } - // Return the location of the package declaration. - rng, err := pgf.NodeRange(pgf.File.Name) - if err != nil { - return nil, err, err - } - return &PrepareItem{ - Range: rng, - Text: string(meta.Name), - }, nil, nil + // Ordinary (non-package) renaming. + // + // Type-check the current package, locate the reference at the position, + // validate the object, and report its name and range. + // + // TODO(adonovan): in all cases below, we return usererr=nil, + // which means we return (nil, nil) at the protocol + // layer. This seems like a bug, or at best an exploitation of + // knowledge of VSCode-specific behavior. Can we avoid that? + pkg, pgf, err := PackageForFile(ctx, snapshot, f.URI(), TypecheckFull, NarrowestPackage) + if err != nil { + return nil, nil, err } - - // Prepare for ordinary (non-package) renaming. - - qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp) + pos, err := pgf.PositionPos(pp) + if err != nil { + return nil, nil, err + } + targets, node, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) if err != nil { - return nil, nil, err // => suppress error to user + return nil, nil, err + } + var obj types.Object + for obj = range targets { + break // pick one arbitrarily } - node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg if err := checkRenamable(obj); err != nil { - return nil, nil, err // => suppress error to user + return nil, nil, err } - - result, err := computePrepareRenameResp(ctx, snapshot, pkg, node, obj.Name()) + rng, err := pgf.NodeRange(node) if err != nil { - return nil, nil, err // -> suppress error to user + return nil, nil, err } - return result, nil, nil + if _, isImport := node.(*ast.ImportSpec); isImport { + // We're not really renaming the import path. + rng.End = rng.Start + } + return &PrepareItem{ + Range: rng, + Text: obj.Name(), + }, nil, nil } -func computePrepareRenameResp(ctx context.Context, snapshot Snapshot, pkg Package, node ast.Node, text string) (*PrepareItem, error) { - mr, err := posToMappedRange(ctx, snapshot, pkg, node.Pos(), node.End()) +func prepareRenamePackageName(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile) (*PrepareItem, error) { + // Does the client support file renaming? + fileRenameSupported := false + for _, op := range snapshot.View().Options().SupportedResourceOperations { + if op == protocol.Rename { + fileRenameSupported = true + break + } + } + if !fileRenameSupported { + return nil, errors.New("can't rename package: LSP client does not support file renaming") + } + + // Check validity of the metadata for the file's containing package. + fileMeta, err := snapshot.MetadataForFile(ctx, pgf.URI) if err != nil { return nil, err } - rng := mr.Range() - if _, isImport := node.(*ast.ImportSpec); isImport { - // We're not really renaming the import path. - rng.End = rng.Start + if len(fileMeta) == 0 { + return nil, fmt.Errorf("no packages found for file %q", pgf.URI) + } + meta := fileMeta[0] + if meta.Name == "main" { + return nil, fmt.Errorf("can't rename package \"main\"") + } + if strings.HasSuffix(string(meta.Name), "_test") { + return nil, fmt.Errorf("can't rename x_test packages") + } + if meta.Module == nil { + return nil, fmt.Errorf("can't rename package: missing module information for package %q", meta.PkgPath) + } + if meta.Module.Path == string(meta.PkgPath) { + return nil, fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path) + } + + // Return the location of the package declaration. + rng, err := pgf.NodeRange(pgf.File.Name) + if err != nil { + return nil, err } return &PrepareItem{ Range: rng, - Text: text, + Text: string(meta.Name), }, nil } @@ -871,13 +875,8 @@ func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, p // A qualifiedObject is the result of resolving a reference from an // identifier to an object. type qualifiedObject struct { - // definition obj types.Object // the referenced object pkg Package // the Package that defines the object (nil => universe) - - // reference (optional) - node ast.Node // the reference (*ast.Ident or *ast.ImportSpec) to the object - sourcePkg Package // the Package containing node } // A positionKey identifies a byte offset within a file (URI). @@ -1009,12 +1008,7 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, s event.Error(ctx, fmt.Sprintf("no package for obj %s: %v", obj, obj.Pkg()), err) continue } - qualifiedObjs = append(qualifiedObjs, qualifiedObject{ - obj: obj, - pkg: pkg, - sourcePkg: searchpkg, - node: path[0], - }) + qualifiedObjs = append(qualifiedObjs, qualifiedObject{obj: obj, pkg: pkg}) seenObjs[obj] = true // If the qualified object is in another file (or more likely, another From b62cbb6b59d04ddf6f425152fac49b8f89904037 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Fri, 27 Jan 2023 17:10:51 -0500 Subject: [PATCH 686/723] internal/lockedfile: fix build constraints on solaris The version of this file in the main repo uses a "!unix" build constraint, which had to be ported to a longer list for inclusion in gopls (which still supports Go versions that predate the "unix" tag). Solaris is a Unix derivative, and its implementation is provided in the "fcntl" implementation. Updates golang/go#57747. Change-Id: Ibde8ce55dadc03ad3cb797b5320f5b75580f639f Reviewed-on: https://go-review.googlesource.com/c/tools/+/463776 TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills Reviewed-by: Robert Findley gopls-CI: kokoro Auto-Submit: Bryan Mills --- internal/lockedfile/internal/filelock/filelock_other.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/lockedfile/internal/filelock/filelock_other.go b/internal/lockedfile/internal/filelock/filelock_other.go index a108915bbb1..cde868f49b0 100644 --- a/internal/lockedfile/internal/filelock/filelock_other.go +++ b/internal/lockedfile/internal/filelock/filelock_other.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 !(aix || darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd) && !plan9 && !windows -// +build !aix,!darwin,!dragonfly,!freebsd,!illumos,!linux,!netbsd,!openbsd,!plan9,!windows +//go:build !(aix || darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd || solaris) && !plan9 && !windows +// +build !aix,!darwin,!dragonfly,!freebsd,!illumos,!linux,!netbsd,!openbsd,!solaris,!plan9,!windows package filelock From e2603688849b2dc073041d49a795be98543fcf91 Mon Sep 17 00:00:00 2001 From: Peter Weinbergr Date: Wed, 25 Jan 2023 18:07:26 -0500 Subject: [PATCH 687/723] gopls/semantic: report type parameters in the type of a receiver The code was misclassifying K, V in the reciever, as in func (s *Foo[K, V]) Get(k K) (V, bool)... Fixes: golang/go#57619 Change-Id: I77eae7929c4b9434c8c25bbc337151dcf90f8452 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463316 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Peter Weinberger --- gopls/internal/lsp/semantic.go | 4 ++ .../regtest/misc/semantictokens_test.go | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 0406311598c..5b7655a3426 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -743,7 +743,11 @@ func (e *encoded) definitionFor(x *ast.Ident, def types.Object) (tokenType, []st 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 diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index e083faa3912..a0e22456e43 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -113,6 +113,53 @@ func Add[T int](target T, l []T) []T { } +// fix inconsistency in TypeParameters +// https://github.com/golang/go/issues/57619 +func TestSemantic_57619(t *testing.T) { + if !typeparams.Enabled { + t.Skip("type parameters are needed for this test") + } + src := ` +-- go.mod -- +module example.com + +go 1.19 +-- main.go -- +package foo +type Smap[K int, V any] struct { + Store map[K]V +} +func (s *Smap[K, V]) Get(k K) (V, bool) { + v, ok := s.Store[k] + return v, ok +} +func New[K int, V any]() Smap[K, V] { + return Smap[K, V]{Store: make(map[K]V)} +} +` + WithOptions( + Modes(Default), + Settings{"semanticTokens": true}, + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + p := &protocol.SemanticTokensParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: env.Sandbox.Workdir.URI("main.go"), + }, + } + v, err := env.Editor.Server.SemanticTokensFull(env.Ctx, p) + if err != nil { + t.Fatal(err) + } + seen := interpret(v.Data, env.BufferText("main.go")) + for i, s := range seen { + if (s.Token == "K" || s.Token == "V") && s.TokenType != "typeParameter" { + t.Errorf("%d: expected K and V to be type parameters, but got %v", i, s) + } + } + }) +} + type result struct { Token string TokenType string From 6f652132c901be9fbd5b220a5758d8d8601fc5e0 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 30 Jan 2023 12:45:45 -0500 Subject: [PATCH 688/723] gopls/internal/lsp/protocol: Mapper.NodeMappedRange This change adds a few missing convenience wrappers in the {PGF,Mapper}.{Pos,Node}MappedRange() cross product. Change-Id: I0187eae590757e32cd5cb402b3054bcc662e62d3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/464096 Auto-Submit: Alan Donovan Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/cache/errors.go | 2 +- gopls/internal/lsp/protocol/mapper.go | 32 +++++++++++++++++------- gopls/internal/lsp/source/identifier.go | 6 ++--- gopls/internal/lsp/source/references2.go | 2 +- gopls/internal/lsp/source/rename.go | 2 +- gopls/internal/lsp/source/view.go | 21 ++++++++++------ 6 files changed, 43 insertions(+), 22 deletions(-) diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go index b0babc7b07f..92e2330a974 100644 --- a/gopls/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -364,7 +364,7 @@ func parseGoListImportCycleError(e packages.Error, pkg *syntaxPackage) (*source. // Search file imports for the import that is causing the import cycle. for _, imp := range pgf.File.Imports { if imp.Path.Value == circImp { - rng, err := pgf.PosMappedRange(imp.Pos(), imp.End()) + rng, err := pgf.NodeMappedRange(imp) if err != nil { return nil, false } diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go index ee2622326a4..6a93c39273f 100644 --- a/gopls/internal/lsp/protocol/mapper.go +++ b/gopls/internal/lsp/protocol/mapper.go @@ -311,6 +311,15 @@ func (m *Mapper) OffsetPoint(offset int) (span.Point, error) { return span.NewPoint(line+1, col8+1, offset), nil } +// OffsetMappedRange returns a MappedRange for the given byte offsets. +// A MappedRange can be converted to any other form. +func (m *Mapper) OffsetMappedRange(start, end int) (MappedRange, error) { + if !(0 <= start && start <= end && end <= len(m.Content)) { + return MappedRange{}, fmt.Errorf("invalid offsets (%d, %d) (file %s has size %d)", start, end, m.URI, len(m.Content)) + } + return MappedRange{m, start, end}, nil +} + // -- conversions from protocol (UTF-16) domain -- // LocationSpan converts a protocol (UTF-16) Location to a (UTF-8) span. @@ -432,7 +441,7 @@ func (m *Mapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { return m.OffsetRange(startOffset, endOffset) } -// PosPosition converts a syntax node range to a protocol (UTF-16) range. +// NodeRange converts a syntax node range to a protocol (UTF-16) range. func (m *Mapper) NodeRange(tf *token.File, node ast.Node) (Range, error) { return m.PosRange(tf, node.Pos(), node.End()) } @@ -442,17 +451,22 @@ func (m *Mapper) RangeLocation(rng Range) Location { return Location{URI: URIFromSpanURI(m.URI), Range: rng} } -// -- MappedRange -- - -// OffsetMappedRange returns a MappedRange for the given byte offsets. -// A MappedRange can be converted to any other form. -func (m *Mapper) OffsetMappedRange(start, end int) (MappedRange, error) { - if !(0 <= start && start <= end && end <= len(m.Content)) { - return MappedRange{}, fmt.Errorf("invalid offsets (%d, %d) (file %s has size %d)", start, end, m.URI, len(m.Content)) +// PosMappedRange returns a MappedRange for the given token.Pos range. +func (m *Mapper) PosMappedRange(tf *token.File, start, end token.Pos) (MappedRange, error) { + startOffset, endOffset, err := safetoken.Offsets(tf, start, end) + if err != nil { + return MappedRange{}, nil } - return MappedRange{m, start, end}, nil + return m.OffsetMappedRange(startOffset, endOffset) +} + +// NodeMappedRange returns a MappedRange for the given node range. +func (m *Mapper) NodeMappedRange(tf *token.File, node ast.Node) (MappedRange, error) { + return m.PosMappedRange(tf, node.Pos(), node.End()) } +// -- MappedRange -- + // A MappedRange represents a valid byte-offset range of a file. // Through its Mapper it can be converted into other forms such // as protocol.Range or span.Span. diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index d34fdf102fe..0ed56e472cd 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -122,11 +122,11 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa decl = pgf } } - pkgRng, err := pgf.PosMappedRange(file.Name.Pos(), file.Name.End()) + pkgRng, err := pgf.NodeMappedRange(file.Name) if err != nil { return nil, err } - declRng, err := decl.PosMappedRange(decl.File.Name.Pos(), decl.File.Name.End()) + declRng, err := decl.NodeMappedRange(decl.File.Name) if err != nil { return nil, err } @@ -496,7 +496,7 @@ func importSpec(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Parsed } continue } - rng, err := pgf.PosMappedRange(pgf.File.Pos(), pgf.File.End()) + rng, err := pgf.NodeMappedRange(pgf.File) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/references2.go b/gopls/internal/lsp/source/references2.go index d34e31246c8..e579ab056a6 100644 --- a/gopls/internal/lsp/source/references2.go +++ b/gopls/internal/lsp/source/references2.go @@ -584,7 +584,7 @@ func globalReferences(ctx context.Context, snapshot Snapshot, m *Metadata, targe // // Safe for use only by references2 and implementations2. func mustLocation(pgf *ParsedGoFile, n ast.Node) protocol.Location { - loc, err := pgf.PosLocation(n.Pos(), n.End()) + loc, err := pgf.NodeLocation(n) if err != nil { panic(err) // can't happen in references2 or implementations2 } diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index e38ec5b75f3..da8aa272870 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -1165,7 +1165,7 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject) ( if err != nil { return nil, err } - rng, err := pgf.PosMappedRange(ident.Pos(), ident.End()) + rng, err := pgf.NodeMappedRange(ident) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 3744e7c9fd1..cbcc9b4e6f0 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -392,6 +392,12 @@ func (pgf *ParsedGoFile) PosRange(start, end token.Pos) (protocol.Range, error) return pgf.Mapper.PosRange(pgf.Tok, start, end) } +// PosMappedRange returns a MappedRange for the token.Pos interval in this file. +// A MappedRange can be converted to any other form. +func (pgf *ParsedGoFile) PosMappedRange(start, end token.Pos) (protocol.MappedRange, error) { + return pgf.Mapper.PosMappedRange(pgf.Tok, start, end) +} + // PosLocation returns a protocol Location for the token.Pos interval in this file. func (pgf *ParsedGoFile) PosLocation(start, end token.Pos) (protocol.Location, error) { return pgf.Mapper.PosLocation(pgf.Tok, start, end) @@ -402,14 +408,15 @@ func (pgf *ParsedGoFile) NodeRange(node ast.Node) (protocol.Range, error) { return pgf.Mapper.NodeRange(pgf.Tok, node) } -// PosMappedRange returns a MappedRange for the token.Pos interval in this file. +// NodeMappedRange returns a MappedRange for the ast.Node interval in this file. // A MappedRange can be converted to any other form. -func (pgf *ParsedGoFile) PosMappedRange(startPos, endPos token.Pos) (protocol.MappedRange, error) { - start, end, err := safetoken.Offsets(pgf.Tok, startPos, endPos) - if err != nil { - return protocol.MappedRange{}, nil - } - return pgf.Mapper.OffsetMappedRange(start, end) +func (pgf *ParsedGoFile) NodeMappedRange(node ast.Node) (protocol.MappedRange, error) { + return pgf.Mapper.NodeMappedRange(pgf.Tok, node) +} + +// NodeLocation returns a protocol Location for the ast.Node interval in this file. +func (pgf *ParsedGoFile) NodeLocation(node ast.Node) (protocol.Location, error) { + return pgf.Mapper.PosLocation(pgf.Tok, node.Pos(), node.End()) } // RangeToSpanRange parses a protocol Range back into the go/token domain. From ae242ec327b95176488336f365b0dc9c08d4e11b Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Thu, 19 Jan 2023 16:41:08 -0500 Subject: [PATCH 689/723] gopls: fix windows file corruption Fix the bug that gopls finds the wrong content when formatting an open URI whose spelling does not match the spelling on disk (i.e. because of case insensitivity). Remove the whole View.filesByBase mechanism: it is problematic as we can't generally know whether or not we want to associate two different spellings of the same file: for the purposes of finding packages we may want to treat Foo.go as foo.go, but we don't want to treat a symlink of foo.go in another directory the same. Instead, use robustio.FileID to de-duplicate content in the cache, and otherwise treat URIs as we receive them. This fixes the formatting corruption, but means that we don't find packages for the corresponding file (because go/packages.Load("file=foo.go") fails). A failing test is added for the latter bug. Also: use a seenFiles map in the view to satisfy the concern of tracking relevant files, with a TODO to delete this problematic map. Along the way, refactor somewhat to separate and normalize the implementations of source.FileSource. For golang/go#57081 Change-Id: I02971a1702f057b644fa18a873790e8f0d98a323 Reviewed-on: https://go-review.googlesource.com/c/tools/+/462819 gopls-CI: kokoro Run-TryBot: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/cache.go | 122 ++------------ gopls/internal/lsp/cache/fs_memoized.go | 149 +++++++++++++++++ gopls/internal/lsp/cache/fs_overlay.go | 76 +++++++++ gopls/internal/lsp/cache/mod.go | 5 +- gopls/internal/lsp/cache/session.go | 155 +++++------------- gopls/internal/lsp/cache/snapshot.go | 10 +- gopls/internal/lsp/cache/view.go | 96 +++-------- .../regtest/workspace/misspelling_test.go | 80 +++++++++ internal/robustio/robustio.go | 10 +- internal/robustio/robustio_posix.go | 7 +- internal/robustio/robustio_test.go | 29 +++- internal/robustio/robustio_windows.go | 15 +- 12 files changed, 429 insertions(+), 325 deletions(-) create mode 100644 gopls/internal/lsp/cache/fs_memoized.go create mode 100644 gopls/internal/lsp/cache/fs_overlay.go create mode 100644 gopls/internal/regtest/workspace/misspelling_test.go diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go index 9da185ccc57..edf1d0eeae6 100644 --- a/gopls/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -11,20 +11,16 @@ import ( "go/token" "go/types" "html/template" - "os" "reflect" "sort" "strconv" - "sync" "sync/atomic" - "time" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/memoize" + "golang.org/x/tools/internal/robustio" ) // New Creates a new cache for gopls operation results, using the given file @@ -47,124 +43,32 @@ func New(fset *token.FileSet, store *memoize.Store) *Cache { } c := &Cache{ - id: strconv.FormatInt(index, 10), - fset: fset, - store: store, - fileContent: map[span.URI]*DiskFile{}, + id: strconv.FormatInt(index, 10), + fset: fset, + store: store, + memoizedFS: &memoizedFS{filesByID: map[robustio.FileID][]*DiskFile{}}, } return c } +// A Cache holds caching stores that are bundled together for consistency. +// +// TODO(rfindley): once fset and store need not be bundled together, the Cache +// type can be eliminated. type Cache struct { id string fset *token.FileSet store *memoize.Store - fileMu sync.Mutex - fileContent map[span.URI]*DiskFile -} - -// A DiskFile is a file on the filesystem, or a failure to read one. -// It implements the source.FileHandle interface. -type DiskFile struct { - uri span.URI - modTime time.Time - content []byte - hash source.Hash - err error -} - -func (h *DiskFile) URI() span.URI { return h.uri } - -func (h *DiskFile) FileIdentity() source.FileIdentity { - return source.FileIdentity{ - URI: h.uri, - Hash: h.hash, - } -} - -func (h *DiskFile) Saved() bool { return true } -func (h *DiskFile) Version() int32 { return 0 } - -func (h *DiskFile) Read() ([]byte, error) { - return h.content, h.err -} - -// GetFile stats and (maybe) reads the file, updates the cache, and returns it. -func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { - fi, statErr := os.Stat(uri.Filename()) - if statErr != nil { - return &DiskFile{ - err: statErr, - uri: uri, - }, nil - } - - // We check if the file has changed by comparing modification times. Notably, - // this is an imperfect heuristic as various systems have low resolution - // mtimes (as much as 1s on WSL or s390x builders), so we only cache - // filehandles if mtime is old enough to be reliable, meaning that we don't - // expect a subsequent write to have the same mtime. - // - // The coarsest mtime precision we've seen in practice is 1s, so consider - // mtime to be unreliable if it is less than 2s old. Capture this before - // doing anything else. - recentlyModified := time.Since(fi.ModTime()) < 2*time.Second - - c.fileMu.Lock() - fh, ok := c.fileContent[uri] - c.fileMu.Unlock() - - if ok && fh.modTime.Equal(fi.ModTime()) { - return fh, nil - } - - fh, err := readFile(ctx, uri, fi) // ~25us - if err != nil { - return nil, err - } - c.fileMu.Lock() - if !recentlyModified { - c.fileContent[uri] = fh - } else { - delete(c.fileContent, uri) - } - c.fileMu.Unlock() - return fh, nil -} - -// ioLimit limits the number of parallel file reads per process. -var ioLimit = make(chan struct{}, 128) - -func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*DiskFile, error) { - select { - case ioLimit <- struct{}{}: - case <-ctx.Done(): - return nil, ctx.Err() - } - defer func() { <-ioLimit }() - - ctx, done := event.Start(ctx, "cache.readFile", tag.File.Of(uri.Filename())) - _ = ctx - defer done() - - content, err := os.ReadFile(uri.Filename()) // ~20us - if err != nil { - content = nil // just in case - } - return &DiskFile{ - modTime: fi.ModTime(), - uri: uri, - content: content, - hash: source.HashOf(content), - err: err, - }, nil + *memoizedFS // implements source.FileSource } // NewSession creates a new gopls session with the given cache and options overrides. // // The provided optionsOverrides may be nil. +// +// TODO(rfindley): move this to session.go. func NewSession(ctx context.Context, c *Cache, optionsOverrides func(*source.Options)) *Session { index := atomic.AddInt64(&sessionIndex, 1) options := source.DefaultOptions().Clone() @@ -176,7 +80,7 @@ func NewSession(ctx context.Context, c *Cache, optionsOverrides func(*source.Opt cache: c, gocmdRunner: &gocommand.Runner{}, options: options, - overlays: make(map[span.URI]*Overlay), + overlayFS: newOverlayFS(c), } event.Log(ctx, "New session", KeyCreateSession.Of(s)) return s diff --git a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/fs_memoized.go new file mode 100644 index 00000000000..9acd872762f --- /dev/null +++ b/gopls/internal/lsp/cache/fs_memoized.go @@ -0,0 +1,149 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "context" + "os" + "sync" + "time" + + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/robustio" +) + +// A memoizedFS is a file source that memoizes reads, to reduce IO. +type memoizedFS struct { + mu sync.Mutex + + // filesByID maps existing file inodes to the result of a read. + // (The read may have failed, e.g. due to EACCES or a delete between stat+read.) + // Each slice is a non-empty list of aliases: different URIs. + filesByID map[robustio.FileID][]*DiskFile +} + +func newMemoizedFS() *memoizedFS { + return &memoizedFS{filesByID: make(map[robustio.FileID][]*DiskFile)} +} + +// A DiskFile is a file on the filesystem, or a failure to read one. +// It implements the source.FileHandle interface. +type DiskFile struct { + uri span.URI + modTime time.Time + content []byte + hash source.Hash + err error +} + +func (h *DiskFile) URI() span.URI { return h.uri } + +func (h *DiskFile) FileIdentity() source.FileIdentity { + return source.FileIdentity{ + URI: h.uri, + Hash: h.hash, + } +} + +func (h *DiskFile) Saved() bool { return true } +func (h *DiskFile) Version() int32 { return 0 } +func (h *DiskFile) Read() ([]byte, error) { return h.content, h.err } + +// GetFile stats and (maybe) reads the file, updates the cache, and returns it. +func (fs *memoizedFS) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { + id, mtime, err := robustio.GetFileID(uri.Filename()) + if err != nil { + // file does not exist + return &DiskFile{ + err: err, + uri: uri, + }, nil + } + + // We check if the file has changed by comparing modification times. Notably, + // this is an imperfect heuristic as various systems have low resolution + // mtimes (as much as 1s on WSL or s390x builders), so we only cache + // filehandles if mtime is old enough to be reliable, meaning that we don't + // expect a subsequent write to have the same mtime. + // + // The coarsest mtime precision we've seen in practice is 1s, so consider + // mtime to be unreliable if it is less than 2s old. Capture this before + // doing anything else. + recentlyModified := time.Since(mtime) < 2*time.Second + + fs.mu.Lock() + fhs, ok := fs.filesByID[id] + if ok && fhs[0].modTime.Equal(mtime) { + var fh *DiskFile + // We have already seen this file and it has not changed. + for _, h := range fhs { + if h.uri == uri { + fh = h + break + } + } + // No file handle for this exact URI. Create an alias, but share content. + if fh == nil { + newFH := *fhs[0] + newFH.uri = uri + fh = &newFH + fhs = append(fhs, fh) + fs.filesByID[id] = fhs + } + fs.mu.Unlock() + return fh, nil + } + fs.mu.Unlock() + + // Unknown file, or file has changed. Read (or re-read) it. + fh, err := readFile(ctx, uri, mtime) // ~25us + if err != nil { + return nil, err // e.g. cancelled (not: read failed) + } + + fs.mu.Lock() + if !recentlyModified { + fs.filesByID[id] = []*DiskFile{fh} + } else { + delete(fs.filesByID, id) + } + fs.mu.Unlock() + return fh, nil +} + +// ioLimit limits the number of parallel file reads per process. +var ioLimit = make(chan struct{}, 128) + +func readFile(ctx context.Context, uri span.URI, mtime time.Time) (*DiskFile, error) { + select { + case ioLimit <- struct{}{}: + case <-ctx.Done(): + return nil, ctx.Err() + } + defer func() { <-ioLimit }() + + ctx, done := event.Start(ctx, "cache.readFile", tag.File.Of(uri.Filename())) + _ = ctx + defer done() + + // It is possible that a race causes us to read a file with different file + // ID, or whose mtime differs from the given mtime. However, in these cases + // we expect the client to notify of a subsequent file change, and the file + // content should be eventually consistent. + content, err := os.ReadFile(uri.Filename()) // ~20us + if err != nil { + content = nil // just in case + } + return &DiskFile{ + modTime: mtime, + uri: uri, + content: content, + hash: source.HashOf(content), + err: err, + }, nil +} diff --git a/gopls/internal/lsp/cache/fs_overlay.go b/gopls/internal/lsp/cache/fs_overlay.go new file mode 100644 index 00000000000..d277bc6aa18 --- /dev/null +++ b/gopls/internal/lsp/cache/fs_overlay.go @@ -0,0 +1,76 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "context" + "sync" + + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" +) + +// An overlayFS is a source.FileSource that keeps track of overlays on top of a +// delegate FileSource. +type overlayFS struct { + delegate source.FileSource + + mu sync.Mutex + overlays map[span.URI]*Overlay +} + +func newOverlayFS(delegate source.FileSource) *overlayFS { + return &overlayFS{ + delegate: delegate, + overlays: make(map[span.URI]*Overlay), + } +} + +// Overlays returns a new unordered array of overlays. +func (fs *overlayFS) Overlays() []*Overlay { + overlays := make([]*Overlay, 0, len(fs.overlays)) + for _, overlay := range fs.overlays { + overlays = append(overlays, overlay) + } + return overlays +} + +func (fs *overlayFS) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { + fs.mu.Lock() + overlay, ok := fs.overlays[uri] + fs.mu.Unlock() + if ok { + return overlay, nil + } + return fs.delegate.GetFile(ctx, uri) +} + +// An Overlay is a file open in the editor. It may have unsaved edits. +// It implements the source.FileHandle interface. +type Overlay struct { + uri span.URI + content []byte + hash source.Hash + version int32 + kind source.FileKind + + // saved is true if a file matches the state on disk, + // and therefore does not need to be part of the overlay sent to go/packages. + saved bool +} + +func (o *Overlay) URI() span.URI { return o.uri } + +func (o *Overlay) FileIdentity() source.FileIdentity { + return source.FileIdentity{ + URI: o.uri, + Hash: o.hash, + } +} + +func (o *Overlay) Read() ([]byte, error) { return o.content, nil } +func (o *Overlay) Version() int32 { return o.version } +func (o *Overlay) Saved() bool { return o.saved } +func (o *Overlay) Kind() source.FileKind { return o.kind } diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index 50f557ddf5e..9244f698757 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -184,11 +184,14 @@ func (s *snapshot) goSum(ctx context.Context, modURI span.URI) []byte { // Get the go.sum file, either from the snapshot or directly from the // cache. Avoid (*snapshot).GetFile here, as we don't want to add // nonexistent file handles to the snapshot if the file does not exist. + // + // TODO(rfindley): but that's not right. Changes to sum files should + // invalidate content, even if it's nonexistent content. sumURI := span.URIFromPath(sumFilename(modURI)) var sumFH source.FileHandle = s.FindFile(sumURI) if sumFH == nil { var err error - sumFH, err = s.view.cache.GetFile(ctx, sumURI) + sumFH, err = s.view.fs.GetFile(ctx, sumURI) if err != nil { return nil } diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index e418a0046fe..5df540e4f61 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -38,38 +38,9 @@ type Session struct { views []*View viewMap map[span.URI]*View // map of URI->best view - overlayMu sync.Mutex - overlays map[span.URI]*Overlay + *overlayFS } -// An Overlay is a file open in the editor. -// It implements the source.FileSource interface. -type Overlay struct { - uri span.URI - text []byte - hash source.Hash - version int32 - kind source.FileKind - - // saved is true if a file matches the state on disk, - // and therefore does not need to be part of the overlay sent to go/packages. - saved bool -} - -func (o *Overlay) URI() span.URI { return o.uri } - -func (o *Overlay) FileIdentity() source.FileIdentity { - return source.FileIdentity{ - URI: o.uri, - Hash: o.hash, - } -} - -func (o *Overlay) Read() ([]byte, error) { return o.text, nil } -func (o *Overlay) Version() int32 { return o.version } -func (o *Overlay) Saved() bool { return o.saved } -func (o *Overlay) Kind() source.FileKind { return o.kind } - // ID returns the unique identifier for this session on this server. func (s *Session) ID() string { return s.id } func (s *Session) String() string { return s.id } @@ -150,7 +121,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, v := &View{ id: strconv.FormatInt(index, 10), - cache: s.cache, + fset: s.cache.fset, gocmdRunner: s.gocmdRunner, initialWorkspaceLoad: make(chan struct{}), initializationSema: make(chan struct{}, 1), @@ -160,8 +131,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, folder: folder, moduleUpgrades: map[span.URI]map[string]string{}, vulns: map[span.URI]*govulncheck.Result{}, - filesByURI: make(map[span.URI]span.URI), - filesByBase: make(map[string][]canonicalURI), + fs: s.overlayFS, workspaceInformation: info, } v.importsState = &importsState{ @@ -422,8 +392,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // spurious diagnostics). However, any such view would immediately be // invalidated here, so it is possible that we could update overlays before // acquiring viewMu. - overlays, err := s.updateOverlays(ctx, changes) - if err != nil { + if err := s.updateOverlays(ctx, changes); err != nil { return nil, nil, err } @@ -517,34 +486,25 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // Apply the changes to all affected views. for _, view := range changedViews { - // Make sure that the file is added to the view's knownFiles set. - view.canonicalURI(c.URI, true) // ignore result + // Make sure that the file is added to the view's seenFiles set. + view.markKnown(c.URI) if _, ok := views[view]; !ok { views[view] = make(map[span.URI]*fileChange) } - if fh, ok := overlays[c.URI]; ok { - views[view][c.URI] = &fileChange{ - content: fh.text, - exists: true, - fileHandle: fh, - isUnchanged: isUnchanged, - } - } else { - fsFile, err := s.cache.GetFile(ctx, c.URI) - if err != nil { - return nil, nil, err - } - content, err := fsFile.Read() - if err != nil { - // Ignore the error: the file may be deleted. - content = nil - } - views[view][c.URI] = &fileChange{ - content: content, - exists: err == nil, - fileHandle: fsFile, - isUnchanged: isUnchanged, - } + fh, err := s.GetFile(ctx, c.URI) + if err != nil { + return nil, nil, err + } + content, err := fh.Read() + if err != nil { + // Ignore the error: the file may be deleted. + content = nil + } + views[view][c.URI] = &fileChange{ + content: content, + exists: err == nil, + fileHandle: fh, + isUnchanged: isUnchanged, } } } @@ -658,9 +618,10 @@ func knownFilesInDir(ctx context.Context, snapshots []*snapshot, dir span.URI) m } // Precondition: caller holds s.viewMu lock. -func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModification) (map[span.URI]*Overlay, error) { - s.overlayMu.Lock() - defer s.overlayMu.Unlock() +// TODO(rfindley): move this to fs_overlay.go. +func (fs *overlayFS) updateOverlays(ctx context.Context, changes []source.FileModification) error { + fs.mu.Lock() + defer fs.mu.Unlock() for _, c := range changes { // Don't update overlays for metadata invalidations. @@ -668,7 +629,7 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif continue } - o, ok := s.overlays[c.URI] + o, ok := fs.overlays[c.URI] // If the file is not opened in an overlay and the change is on disk, // there's no need to update an overlay. If there is an overlay, we @@ -684,14 +645,14 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif kind = source.FileKindForLang(c.LanguageID) default: if !ok { - return nil, fmt.Errorf("updateOverlays: modifying unopened overlay %v", c.URI) + return fmt.Errorf("updateOverlays: modifying unopened overlay %v", c.URI) } kind = o.kind } // Closing a file just deletes its overlay. if c.Action == source.Close { - delete(s.overlays, c.URI) + delete(fs.overlays, c.URI) continue } @@ -701,9 +662,9 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif text := c.Text if text == nil && (c.Action == source.Save || c.OnDisk) { if !ok { - return nil, fmt.Errorf("no known content for overlay for %s", c.Action) + return fmt.Errorf("no known content for overlay for %s", c.Action) } - text = o.text + text = o.content } // On-disk changes don't come with versions. version := c.Version @@ -718,16 +679,16 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif case source.Save: // Make sure the version and content (if present) is the same. if false && o.version != version { // Client no longer sends the version - return nil, fmt.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version) + return fmt.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version) } if c.Text != nil && o.hash != hash { - return nil, fmt.Errorf("updateOverlays: overlay %s changed on save", c.URI) + return fmt.Errorf("updateOverlays: overlay %s changed on save", c.URI) } sameContentOnDisk = true default: - fh, err := s.cache.GetFile(ctx, c.URI) + fh, err := fs.delegate.GetFile(ctx, c.URI) if err != nil { - return nil, err + return err } _, readErr := fh.Read() sameContentOnDisk = (readErr == nil && fh.FileIdentity().Hash == hash) @@ -735,59 +696,19 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif o = &Overlay{ uri: c.URI, version: version, - text: text, + content: text, kind: kind, hash: hash, saved: sameContentOnDisk, } - // When opening files, ensure that we actually have a well-defined view and file kind. - if c.Action == source.Open { - view, err := s.viewOfLocked(o.uri) - if err != nil { - return nil, fmt.Errorf("updateOverlays: finding view for %s: %v", o.uri, err) - } - if kind := view.FileKind(o); kind == source.UnknownKind { - return nil, fmt.Errorf("updateOverlays: unknown file kind for %s", o.uri) - } - } + // NOTE: previous versions of this code checked here that the overlay had a + // view and file kind (but we don't know why). - s.overlays[c.URI] = o + fs.overlays[c.URI] = o } - // Get the overlays for each change while the session's overlay map is - // locked. - overlays := make(map[span.URI]*Overlay) - for _, c := range changes { - if o, ok := s.overlays[c.URI]; ok { - overlays[c.URI] = o - } - } - return overlays, nil -} - -// GetFile returns a handle for the specified file. -func (s *Session) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { - s.overlayMu.Lock() - overlay, ok := s.overlays[uri] - s.overlayMu.Unlock() - if ok { - return overlay, nil - } - - return s.cache.GetFile(ctx, uri) -} - -// Overlays returns a slice of file overlays for the session. -func (s *Session) Overlays() []*Overlay { - s.overlayMu.Lock() - defer s.overlayMu.Unlock() - - overlays := make([]*Overlay, 0, len(s.overlays)) - for _, overlay := range s.overlays { - overlays = append(overlays, overlay) - } - return overlays + return nil } // FileWatchingGlobPatterns returns glob patterns to watch every directory diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 995fba34ca6..398240aa83c 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -262,7 +262,7 @@ func (s *snapshot) BackgroundContext() context.Context { } func (s *snapshot) FileSet() *token.FileSet { - return s.view.cache.fset + return s.view.fset } func (s *snapshot) ModFiles() []span.URI { @@ -620,7 +620,7 @@ func (s *snapshot) buildOverlay() map[string][]byte { return } // TODO(rstambler): Make sure not to send overlays outside of the current view. - overlays[uri.Filename()] = overlay.text + overlays[uri.Filename()] = overlay.content }) return overlays } @@ -1205,7 +1205,7 @@ func (s *snapshot) isWorkspacePackage(id PackageID) bool { } func (s *snapshot) FindFile(uri span.URI) source.FileHandle { - uri, _ = s.view.canonicalURI(uri, true) + s.view.markKnown(uri) s.mu.Lock() defer s.mu.Unlock() @@ -1220,7 +1220,7 @@ func (s *snapshot) FindFile(uri span.URI) source.FileHandle { // GetFile succeeds even if the file does not exist. A non-nil error return // indicates some type of internal error, for example if ctx is cancelled. func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { - uri, _ = s.view.canonicalURI(uri, true) + s.view.markKnown(uri) s.mu.Lock() defer s.mu.Unlock() @@ -1229,7 +1229,7 @@ func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle return fh, nil } - fh, err := s.view.cache.GetFile(ctx, uri) // read the file + fh, err := s.view.fs.GetFile(ctx, uri) // read the file if err != nil { return nil, err } diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index d8eb82e94ca..51660b3ac87 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -11,6 +11,7 @@ import ( "encoding/json" "errors" "fmt" + "go/token" "io/ioutil" "os" "path" @@ -38,7 +39,7 @@ import ( type View struct { id string - cache *Cache // shared cache + fset *token.FileSet // shared FileSet gocmdRunner *gocommand.Runner // limits go command concurrency // baseCtx is the context handed to NewView. This is the parent of all @@ -68,14 +69,14 @@ type View struct { vulnsMu sync.Mutex vulns map[span.URI]*govulncheck.Result - // filesByURI maps URIs to the canonical URI for the file it denotes. - // We also keep a set of candidates for a given basename - // to reduce the set of pairs that need to be tested for sameness. - // - // TODO(rfindley): move this file tracking to the session. - filesByMu sync.Mutex - filesByURI map[span.URI]span.URI // key is noncanonical URI (alias) - filesByBase map[string][]canonicalURI // key is basename + // fs is the file source used to populate this view. + fs source.FileSource + + // seenFiles tracks files that the view has accessed. + // TODO(golang/go#57558): this notion is fundamentally problematic, and + // should be removed. + knownFilesMu sync.Mutex + knownFiles map[span.URI]bool // initCancelFirstAttempt can be used to terminate the view's first // attempt at initialization. @@ -554,73 +555,20 @@ func (v *View) relevantChange(c source.FileModification) bool { return v.contains(c.URI) } -// knownFile reports whether the specified valid URI (or an alias) is known to the view. -func (v *View) knownFile(uri span.URI) bool { - _, known := v.canonicalURI(uri, false) - return known -} - -// TODO(adonovan): opt: eliminate 'filename' optimization. I doubt the -// cost of allocation is significant relative to the -// stat/open/fstat/close operations that follow on Windows. -type canonicalURI struct { - uri span.URI - filename string // = uri.Filename(), an optimization (on Windows) -} - -// canonicalURI returns the canonical URI that denotes the same file -// as uri, which may differ due to case insensitivity, unclean paths, -// soft or hard links, and so on. If no previous alias was found, or -// the file is missing, insert determines whether to make uri the -// canonical representative of the file or to return false. -// -// The cache grows indefinitely without invalidation: file system -// operations may cause two URIs that used to denote the same file to -// no longer to do so. Also, the basename cache grows without bound. -// TODO(adonovan): fix both bugs. -func (v *View) canonicalURI(uri span.URI, insert bool) (span.URI, bool) { - v.filesByMu.Lock() - defer v.filesByMu.Unlock() - - // Have we seen this exact URI before? - if canonical, ok := v.filesByURI[uri]; ok { - return canonical, true - } - - // Inspect all candidates with the same lowercase basename. - // This heuristic is easily defeated by symbolic links to files. - // Files with some basenames (e.g. doc.go) are very numerous. - // - // The set of candidates grows without bound, and incurs a - // linear sequence of SameFile queries to the file system. - // - // It is tempting to fetch the device/inode pair that - // uniquely identifies a file once, and then compare those - // pairs, but that would cause us to cache stale file system - // state (in addition to the filesByURI staleness). - filename := uri.Filename() - basename := strings.ToLower(filepath.Base(filename)) - if candidates := v.filesByBase[basename]; candidates != nil { - if pathStat, _ := os.Stat(filename); pathStat != nil { - for _, c := range candidates { - if cStat, _ := os.Stat(c.filename); cStat != nil { - // On Windows, SameFile is more expensive as it must - // open the file and use the equivalent of fstat(2). - if os.SameFile(pathStat, cStat) { - v.filesByURI[uri] = c.uri - return c.uri, true - } - } - } - } +func (v *View) markKnown(uri span.URI) { + v.knownFilesMu.Lock() + defer v.knownFilesMu.Unlock() + if v.knownFiles == nil { + v.knownFiles = make(map[span.URI]bool) } + v.knownFiles[uri] = true +} - // No candidates, stat failed, or no candidate matched. - if insert { - v.filesByURI[uri] = uri - v.filesByBase[basename] = append(v.filesByBase[basename], canonicalURI{uri, filename}) - } - return uri, insert +// knownFile reports whether the specified valid URI (or an alias) is known to the view. +func (v *View) knownFile(uri span.URI) bool { + v.knownFilesMu.Lock() + defer v.knownFilesMu.Unlock() + return v.knownFiles[uri] } // shutdown releases resources associated with the view, and waits for ongoing diff --git a/gopls/internal/regtest/workspace/misspelling_test.go b/gopls/internal/regtest/workspace/misspelling_test.go new file mode 100644 index 00000000000..0419a116344 --- /dev/null +++ b/gopls/internal/regtest/workspace/misspelling_test.go @@ -0,0 +1,80 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package workspace + +import ( + "runtime" + "testing" + + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" +) + +// Test for golang/go#57081. +func TestFormattingMisspelledURI(t *testing.T) { + if runtime.GOOS != "windows" && runtime.GOOS != "darwin" { + t.Skip("golang/go#57081 only reproduces on case-insensitive filesystems.") + } + const files = ` +-- go.mod -- +module mod.test + +go 1.19 +-- foo.go -- +package foo + +const C = 2 // extra space is intentional +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("Foo.go") + env.FormatBuffer("Foo.go") + want := env.BufferText("Foo.go") + + if want == "" { + t.Fatalf("Foo.go is empty") + } + + // In golang/go#57081, we observed that if overlay cases don't match, gopls + // will find (and format) the on-disk contents rather than the overlay, + // resulting in invalid edits. + // + // Verify that this doesn't happen, by confirming that formatting is + // idempotent. + env.FormatBuffer("Foo.go") + got := env.BufferText("Foo.go") + if diff := compare.Text(want, got); diff != "" { + t.Errorf("invalid content after second formatting:\n%s", diff) + } + }) +} + +// Test that we can find packages for open files with different spelling on +// case-insensitive file systems. +func TestPackageForMisspelledURI(t *testing.T) { + t.Skip("golang/go#57081: this test fails because the Go command does not load Foo.go correctly") + if runtime.GOOS != "windows" && runtime.GOOS != "darwin" { + t.Skip("golang/go#57081 only reproduces on case-insensitive filesystems.") + } + const files = ` +-- go.mod -- +module mod.test + +go 1.19 +-- foo.go -- +package foo + +const C = D +-- bar.go -- +package foo + +const D = 2 +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("Foo.go") + env.AfterChange(NoDiagnostics()) + }) +} diff --git a/internal/robustio/robustio.go b/internal/robustio/robustio.go index 3241f1d3a7d..0a559fc9b80 100644 --- a/internal/robustio/robustio.go +++ b/internal/robustio/robustio.go @@ -14,6 +14,8 @@ // but substantially reduce their rate of occurrence in practice. package robustio +import "time" + // Rename is like os.Rename, but on Windows retries errors that may occur if the // file is concurrently read or overwritten. // @@ -54,12 +56,14 @@ func IsEphemeralError(err error) bool { // A FileID uniquely identifies a file in the file system. // -// If GetFileID(name1) == GetFileID(name2), the two file names denote the same file. +// If GetFileID(name1) returns the same ID as GetFileID(name2), the two file +// names denote the same file. // A FileID is comparable, and thus suitable for use as a map key. type FileID struct { device, inode uint64 } -// GetFileID returns the file system's identifier for the file. +// GetFileID returns the file system's identifier for the file, and its +// modification time. // Like os.Stat, it reads through symbolic links. -func GetFileID(filename string) (FileID, error) { return getFileID(filename) } +func GetFileID(filename string) (FileID, time.Time, error) { return getFileID(filename) } diff --git a/internal/robustio/robustio_posix.go b/internal/robustio/robustio_posix.go index 175a6b49ba0..8aa13d02786 100644 --- a/internal/robustio/robustio_posix.go +++ b/internal/robustio/robustio_posix.go @@ -12,16 +12,17 @@ package robustio import ( "os" "syscall" + "time" ) -func getFileID(filename string) (FileID, error) { +func getFileID(filename string) (FileID, time.Time, error) { fi, err := os.Stat(filename) if err != nil { - return FileID{}, err + return FileID{}, time.Time{}, err } stat := fi.Sys().(*syscall.Stat_t) return FileID{ device: uint64(stat.Dev), // (int32 on darwin, uint64 on linux) inode: stat.Ino, - }, nil + }, fi.ModTime(), nil } diff --git a/internal/robustio/robustio_test.go b/internal/robustio/robustio_test.go index af53282200a..10244e21d69 100644 --- a/internal/robustio/robustio_test.go +++ b/internal/robustio/robustio_test.go @@ -9,14 +9,15 @@ import ( "path/filepath" "runtime" "testing" + "time" "golang.org/x/tools/internal/robustio" ) -func TestFileID(t *testing.T) { +func TestFileInfo(t *testing.T) { // A nonexistent file has no ID. nonexistent := filepath.Join(t.TempDir(), "nonexistent") - if _, err := robustio.GetFileID(nonexistent); err == nil { + if _, _, err := robustio.GetFileID(nonexistent); err == nil { t.Fatalf("GetFileID(nonexistent) succeeded unexpectedly") } @@ -25,22 +26,28 @@ func TestFileID(t *testing.T) { if err := os.WriteFile(real, nil, 0644); err != nil { t.Fatalf("can't create regular file: %v", err) } - realID, err := robustio.GetFileID(real) + realID, realMtime, err := robustio.GetFileID(real) if err != nil { t.Fatalf("can't get ID of regular file: %v", err) } + // Sleep so that we get a new mtime for subsequent writes. + time.Sleep(2 * time.Second) + // A second regular file has a different ID. real2 := filepath.Join(t.TempDir(), "real2") if err := os.WriteFile(real2, nil, 0644); err != nil { t.Fatalf("can't create second regular file: %v", err) } - real2ID, err := robustio.GetFileID(real2) + real2ID, real2Mtime, err := robustio.GetFileID(real2) if err != nil { t.Fatalf("can't get ID of second regular file: %v", err) } if realID == real2ID { - t.Errorf("realID %+v != real2ID %+v", realID, real2ID) + t.Errorf("realID %+v == real2ID %+v", realID, real2ID) + } + if realMtime.Equal(real2Mtime) { + t.Errorf("realMtime %v == real2Mtime %v", realMtime, real2Mtime) } // A symbolic link has the same ID as its target. @@ -49,13 +56,16 @@ func TestFileID(t *testing.T) { if err := os.Symlink(real, symlink); err != nil { t.Fatalf("can't create symbolic link: %v", err) } - symlinkID, err := robustio.GetFileID(symlink) + symlinkID, symlinkMtime, err := robustio.GetFileID(symlink) if err != nil { t.Fatalf("can't get ID of symbolic link: %v", err) } if realID != symlinkID { t.Errorf("realID %+v != symlinkID %+v", realID, symlinkID) } + if !realMtime.Equal(symlinkMtime) { + t.Errorf("realMtime %v != symlinkMtime %v", realMtime, symlinkMtime) + } } // Two hard-linked files have the same ID. @@ -64,12 +74,15 @@ func TestFileID(t *testing.T) { if err := os.Link(real, hardlink); err != nil { t.Fatal(err) } - hardlinkID, err := robustio.GetFileID(hardlink) + hardlinkID, hardlinkMtime, err := robustio.GetFileID(hardlink) if err != nil { t.Fatalf("can't get ID of hard link: %v", err) } - if hardlinkID != realID { + if realID != hardlinkID { t.Errorf("realID %+v != hardlinkID %+v", realID, hardlinkID) } + if !realMtime.Equal(hardlinkMtime) { + t.Errorf("realMtime %v != hardlinkMtime %v", realMtime, hardlinkMtime) + } } } diff --git a/internal/robustio/robustio_windows.go b/internal/robustio/robustio_windows.go index 98189560bae..616c32883d6 100644 --- a/internal/robustio/robustio_windows.go +++ b/internal/robustio/robustio_windows.go @@ -7,6 +7,7 @@ package robustio import ( "errors" "syscall" + "time" ) const errFileNotFound = syscall.ERROR_FILE_NOT_FOUND @@ -25,22 +26,26 @@ func isEphemeralError(err error) bool { return false } -func getFileID(filename string) (FileID, error) { +// Note: it may be convenient to have this helper return fs.FileInfo, but +// implementing this is actually quite involved on Windows. Since we only +// currently use mtime, keep it simple. +func getFileID(filename string) (FileID, time.Time, error) { filename16, err := syscall.UTF16PtrFromString(filename) if err != nil { - return FileID{}, err + return FileID{}, time.Time{}, err } h, err := syscall.CreateFile(filename16, 0, 0, nil, syscall.OPEN_EXISTING, uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS), 0) if err != nil { - return FileID{}, err + return FileID{}, time.Time{}, err } defer syscall.CloseHandle(h) var i syscall.ByHandleFileInformation if err := syscall.GetFileInformationByHandle(h, &i); err != nil { - return FileID{}, err + return FileID{}, time.Time{}, err } + mtime := time.Unix(0, i.LastWriteTime.Nanoseconds()) return FileID{ device: uint64(i.VolumeSerialNumber), inode: uint64(i.FileIndexHigh)<<32 | uint64(i.FileIndexLow), - }, nil + }, mtime, nil } From 87d00e630f8e636e931862dcc993ca2eb9c88c1c Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 27 Jan 2023 16:13:41 -0500 Subject: [PATCH 690/723] gopls/internal/lsp: separate some requests from source.Identifier Start to unwind source.Identifier by unrolling definition, type definition, and call hierarchy handlers. Along the way, introduce a couple primitive helper functions, which may be made obsolete in the future but allowed preserving source.Identifier behavior: - referencedObject returns the object referenced by the cursor position, as defined by source.Identifier. - mapPosition is a helper to map token.Pos to MappedRange in the narrow context of a package fileset. After this change, the only remaining use of source.Identifier is for Hover, but that is a sizeable refactoring and therefore left to a subsequent CL. Updates golang/go#57987 Change-Id: Iba4b0a574e6a6d3d54253f3b4bff8fe6e13a1b15 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463955 gopls-CI: kokoro Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- gopls/internal/lsp/definition.go | 34 ++- gopls/internal/lsp/lsp_test.go | 31 ++- gopls/internal/lsp/source/call_hierarchy.go | 176 +++++++------- gopls/internal/lsp/source/definition.go | 220 ++++++++++++++++++ gopls/internal/lsp/source/identifier.go | 21 +- gopls/internal/lsp/source/options.go | 14 +- gopls/internal/lsp/source/type_definition.go | 52 +++++ gopls/internal/lsp/source/util.go | 26 +-- gopls/internal/lsp/source/view.go | 17 ++ .../internal/regtest/misc/definition_test.go | 11 +- 10 files changed, 447 insertions(+), 155 deletions(-) create mode 100644 gopls/internal/lsp/source/definition.go create mode 100644 gopls/internal/lsp/source/type_definition.go diff --git a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go index cd91a0447db..cea57c3f630 100644 --- a/gopls/internal/lsp/definition.go +++ b/gopls/internal/lsp/definition.go @@ -14,41 +14,33 @@ import ( ) func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) { + // TODO(rfindley): definition requests should be multiplexed across all views. snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) defer release() if !ok { return nil, err } - if snapshot.View().FileKind(fh) == source.Tmpl { + switch kind := snapshot.View().FileKind(fh); kind { + case source.Tmpl: return template.Definition(snapshot, fh, params.Position) + case source.Go: + return source.Definition(ctx, snapshot, fh, params.Position) + default: + return nil, fmt.Errorf("can't find definitions for file type %s", kind) } - ident, err := source.Identifier(ctx, snapshot, fh, params.Position) - if err != nil { - return nil, err - } - if ident.IsImport() && !snapshot.View().Options().ImportShortcut.ShowDefinition() { - return nil, nil - } - var locations []protocol.Location - for _, ref := range ident.Declaration.MappedRange { - locations = append(locations, ref.Location()) - } - - return locations, nil } func (s *Server) typeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) { + // TODO(rfindley): type definition requests should be multiplexed across all views. snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) defer release() if !ok { return nil, err } - ident, err := source.Identifier(ctx, snapshot, fh, params.Position) - if err != nil { - return nil, err - } - if ident.Type.Object == nil { - return nil, fmt.Errorf("no type definition for %s", ident.Name) + switch kind := snapshot.View().FileKind(fh); kind { + case source.Go: + return source.TypeDefinition(ctx, snapshot, fh, params.Position) + default: + return nil, fmt.Errorf("can't find type definitions for file type %s", kind) } - return []protocol.Location{ident.Type.MappedRange.Location()}, nil } diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 47f6b0ad148..e6488284b81 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -666,7 +666,9 @@ func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) } } -func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { +// TODO(rfindley): This handler needs more work. The output is still a bit hard +// to read (range diffs do not format nicely), and it is too entangled with hover. +func (r *runner) Definition(t *testing.T, _ span.Span, d tests.Definition) { sm, err := r.data.Mapper(d.Src.URI()) if err != nil { t.Fatal(err) @@ -676,18 +678,18 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { t.Fatalf("failed for %v: %v", d.Src, err) } tdpp := protocol.LocationTextDocumentPositionParams(loc) - var locs []protocol.Location + var got []protocol.Location var hover *protocol.Hover if d.IsType { params := &protocol.TypeDefinitionParams{ TextDocumentPositionParams: tdpp, } - locs, err = r.server.TypeDefinition(r.ctx, params) + got, err = r.server.TypeDefinition(r.ctx, params) } else { params := &protocol.DefinitionParams{ TextDocumentPositionParams: tdpp, } - locs, err = r.server.Definition(r.ctx, params) + got, err = r.server.Definition(r.ctx, params) if err != nil { t.Fatalf("failed for %v: %+v", d.Src, err) } @@ -699,8 +701,19 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if err != nil { t.Fatalf("failed for %v: %v", d.Src, err) } - if len(locs) != 1 { - t.Errorf("got %d locations for definition, expected 1", len(locs)) + dm, err := r.data.Mapper(d.Def.URI()) + if err != nil { + t.Fatal(err) + } + def, err := dm.SpanLocation(d.Def) + if err != nil { + t.Fatal(err) + } + if !d.OnlyHover { + want := []protocol.Location{def} + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("Definition(%s) mismatch (-want +got):\n%s", d.Src, diff) + } } didSomething := false if hover != nil { @@ -717,13 +730,13 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { } if !d.OnlyHover { didSomething = true - locURI := locs[0].URI.SpanURI() + locURI := got[0].URI.SpanURI() lm, err := r.data.Mapper(locURI) if err != nil { t.Fatal(err) } - if def, err := lm.LocationSpan(locs[0]); err != nil { - t.Fatalf("failed for %v: %v", locs[0], err) + if def, err := lm.LocationSpan(got[0]); err != nil { + t.Fatalf("failed for %v: %v", got[0], err) } else if def != d.Def { t.Errorf("for %v got %v want %v", d.Src, def, d.Def) } diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index cefc5395514..31fdf491f3b 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -15,44 +15,48 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" ) // PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file. -func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyItem, error) { +func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.CallHierarchyItem, error) { ctx, done := event.Start(ctx, "source.PrepareCallHierarchy") defer done() - identifier, err := Identifier(ctx, snapshot, fh, pos) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) + if err != nil { + return nil, err + } + pos, err := pgf.PositionPos(pp) if err != nil { - if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) { - return nil, nil - } return nil, err } - // The identifier can be nil if it is an import spec. - if identifier == nil || identifier.Declaration.obj == nil { + obj := referencedObject(pkg, pgf, pos) + if obj == nil { return nil, nil } - if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok { + if _, ok := obj.Type().Underlying().(*types.Signature); !ok { return nil, nil } - if len(identifier.Declaration.MappedRange) == 0 { - return nil, nil + declLoc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj)) + if err != nil { + return nil, err } - declMappedRange := identifier.Declaration.MappedRange[0] - rng := declMappedRange.Range() + rng := declLoc.Range callHierarchyItem := protocol.CallHierarchyItem{ - Name: identifier.Name, + Name: obj.Name(), Kind: protocol.Function, Tags: []protocol.SymbolTag{}, - Detail: fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())), - URI: protocol.DocumentURI(declMappedRange.URI()), + Detail: fmt.Sprintf("%s • %s", obj.Pkg().Path(), filepath.Base(declLoc.URI.SpanURI().Filename())), + URI: declLoc.URI, Range: rng, SelectionRange: rng, } @@ -174,41 +178,71 @@ outer: } // OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file. -func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) { +func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) { ctx, done := event.Start(ctx, "source.OutgoingCalls") defer done() - identifier, err := Identifier(ctx, snapshot, fh, pos) + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) + if err != nil { + return nil, err + } + pos, err := pgf.PositionPos(pp) if err != nil { - if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) { - return nil, nil - } return nil, err } - if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok { + obj := referencedObject(pkg, pgf, pos) + if obj == nil { return nil, nil } - node := identifier.Declaration.node - if node == nil { + + if _, ok := obj.Type().Underlying().(*types.Signature); !ok { return nil, nil } - callExprs, err := collectCallExpressions(identifier.Declaration.nodeFile, node) + + // Skip builtins. + if obj.Pkg() == nil { + return nil, nil + } + + if !obj.Pos().IsValid() { + return nil, bug.Errorf("internal error: object %s.%s missing position", obj.Pkg().Path(), obj.Name()) + } + + declFile := pkg.FileSet().File(obj.Pos()) + if declFile == nil { + return nil, bug.Errorf("file not found for %d", obj.Pos()) + } + + uri := span.URIFromPath(declFile.Name()) + offset, err := safetoken.Offset(declFile, obj.Pos()) if err != nil { return nil, err } - return toProtocolOutgoingCalls(ctx, snapshot, fh, callExprs) -} + // Use TypecheckFull as we want to inspect the body of the function declaration. + declPkg, declPGF, err := PackageForFile(ctx, snapshot, uri, TypecheckFull, NarrowestPackage) + if err != nil { + return nil, err + } -// collectCallExpressions collects call expression ranges inside a function. -func collectCallExpressions(pgf *ParsedGoFile, node ast.Node) ([]protocol.Range, error) { - type callPos struct { - start, end token.Pos + declPos, err := safetoken.Pos(declPGF.Tok, offset) + if err != nil { + return nil, err } - callPositions := []callPos{} - ast.Inspect(node, func(n ast.Node) bool { + declNode, _ := FindDeclAndField([]*ast.File{declPGF.File}, declPos) + if declNode == nil { + // TODO(rfindley): why don't we return an error here, or even bug.Errorf? + return nil, nil + // return nil, bug.Errorf("failed to find declaration for object %s.%s", obj.Pkg().Path(), obj.Name()) + } + + type callRange struct { + start, end token.Pos + } + callRanges := []callRange{} + ast.Inspect(declNode, func(n ast.Node) bool { if call, ok := n.(*ast.CallExpr); ok { var start, end token.Pos switch n := call.Fun.(type) { @@ -225,70 +259,48 @@ func collectCallExpressions(pgf *ParsedGoFile, node ast.Node) ([]protocol.Range, // for ex: direct function literal calls since that's not an 'outgoing' call return false } - callPositions = append(callPositions, callPos{start: start, end: end}) + callRanges = append(callRanges, callRange{start: start, end: end}) } return true }) - callRanges := []protocol.Range{} - for _, call := range callPositions { - callRange, err := pgf.PosRange(call.start, call.end) - if err != nil { - return nil, err - } - callRanges = append(callRanges, callRange) - } - return callRanges, nil -} - -// toProtocolOutgoingCalls returns an array of protocol.CallHierarchyOutgoingCall for ast call expressions. -// Calls to the same function are assigned to the same declaration. -func toProtocolOutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, callRanges []protocol.Range) ([]protocol.CallHierarchyOutgoingCall, error) { - // Multiple calls could be made to the same function, defined by "same declaration - // AST node & same identifier name" to provide a unique identifier key even when - // the func is declared in a struct or interface. - type key struct { - decl ast.Node - name string - } - outgoingCalls := map[key]*protocol.CallHierarchyOutgoingCall{} + outgoingCalls := map[token.Pos]*protocol.CallHierarchyOutgoingCall{} for _, callRange := range callRanges { - identifier, err := Identifier(ctx, snapshot, fh, callRange.Start) - if err != nil { - if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) { - continue - } - return nil, err + obj := referencedObject(declPkg, declPGF, callRange.start) + if obj == nil { + continue } // ignore calls to builtin functions - if identifier.Declaration.obj.Pkg() == nil { + if obj.Pkg() == nil { continue } - if outgoingCall, ok := outgoingCalls[key{identifier.Declaration.node, identifier.Name}]; ok { - outgoingCall.FromRanges = append(outgoingCall.FromRanges, callRange) - continue + outgoingCall, ok := outgoingCalls[obj.Pos()] + if !ok { + loc, err := mapPosition(ctx, declPkg.FileSet(), snapshot, obj.Pos(), obj.Pos()+token.Pos(len(obj.Name()))) + if err != nil { + return nil, err + } + outgoingCall = &protocol.CallHierarchyOutgoingCall{ + To: protocol.CallHierarchyItem{ + Name: obj.Name(), + Kind: protocol.Function, + Tags: []protocol.SymbolTag{}, + Detail: fmt.Sprintf("%s • %s", obj.Pkg().Path(), filepath.Base(loc.URI.SpanURI().Filename())), + URI: loc.URI, + Range: loc.Range, + SelectionRange: loc.Range, + }, + } + outgoingCalls[obj.Pos()] = outgoingCall } - if len(identifier.Declaration.MappedRange) == 0 { - continue - } - declMappedRange := identifier.Declaration.MappedRange[0] - rng := declMappedRange.Range() - - outgoingCalls[key{identifier.Declaration.node, identifier.Name}] = &protocol.CallHierarchyOutgoingCall{ - To: protocol.CallHierarchyItem{ - Name: identifier.Name, - Kind: protocol.Function, - Tags: []protocol.SymbolTag{}, - Detail: fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())), - URI: protocol.DocumentURI(declMappedRange.URI()), - Range: rng, - SelectionRange: rng, - }, - FromRanges: []protocol.Range{callRange}, + rng, err := declPGF.PosRange(callRange.start, callRange.end) + if err != nil { + return nil, err } + outgoingCall.FromRanges = append(outgoingCall.FromRanges, rng) } outgoingCallItems := make([]protocol.CallHierarchyOutgoingCall, 0, len(outgoingCalls)) diff --git a/gopls/internal/lsp/source/definition.go b/gopls/internal/lsp/source/definition.go new file mode 100644 index 00000000000..d9dd446b451 --- /dev/null +++ b/gopls/internal/lsp/source/definition.go @@ -0,0 +1,220 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "context" + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" +) + +// Definition handles the textDocument/definition request for Go files. +func Definition(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Location, error) { + ctx, done := event.Start(ctx, "source.Definition") + defer done() + + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) + if err != nil { + return nil, err + } + pos, err := pgf.PositionPos(position) + if err != nil { + return nil, err + } + + // Handle the case where the cursor is in an import. + importLocations, err := importDefinition(ctx, snapshot, pkg, pgf, pos) + if err != nil { + return nil, err + } + if len(importLocations) > 0 { + return importLocations, nil + } + + // Handle the case where the cursor is in the package name. + // We use "<= End" to accept a query immediately after the package name. + if pgf.File != nil && pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End() { + // If there's no package documentation, just use current file. + declFile := pgf + for _, pgf := range pkg.CompiledGoFiles() { + if pgf.File.Name != nil && pgf.File.Doc != nil { + declFile = pgf + break + } + } + loc, err := declFile.NodeLocation(declFile.File.Name) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil + } + + // The general case: the cursor is on an identifier. + obj := referencedObject(pkg, pgf, pos) + if obj == nil { + return nil, nil + } + + // Handle built-in identifiers. + if obj.Parent() == types.Universe { + builtin, err := snapshot.BuiltinFile(ctx) + if err != nil { + return nil, err + } + // Note that builtinObj is an ast.Object, not types.Object :) + builtinObj := builtin.File.Scope.Lookup(obj.Name()) + if builtinObj == nil { + // Every builtin should have documentation. + return nil, bug.Errorf("internal error: no builtin object for %s", obj.Name()) + } + decl, ok := builtinObj.Decl.(ast.Node) + if !ok { + return nil, bug.Errorf("internal error: no declaration for %s", obj.Name()) + } + // The builtin package isn't in the dependency graph, so the usual + // utilities won't work here. + loc, err := builtin.PosLocation(decl.Pos(), decl.Pos()+token.Pos(len(obj.Name()))) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil + } + + // Finally, map the object position. + var locs []protocol.Location + if !obj.Pos().IsValid() { + return nil, bug.Errorf("internal error: no position for %v", obj.Name()) + } + loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj)) + if err != nil { + return nil, err + } + locs = append(locs, loc) + return locs, nil +} + +// referencedObject returns the object referenced at the specified position, +// which must be within the file pgf, for the purposes of definition/hover/call +// hierarchy operations. It may return nil if no object was found at the given +// position. +// +// It differs from types.Info.ObjectOf in several ways: +// - It adjusts positions to do a better job of finding associated +// identifiers. For example it finds 'foo' from the cursor position _*foo +// - It handles type switch implicits, choosing the first one. +// - For embedded fields, it returns the type name object rather 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 +// functionality with objectsAt, after choosing suitable primitives. +func referencedObject(pkg Package, pgf *ParsedGoFile, pos token.Pos) types.Object { + path := pathEnclosingObjNode(pgf.File, pos) + if len(path) == 0 { + return nil + } + var obj types.Object + info := pkg.GetTypesInfo() + switch n := path[0].(type) { + case *ast.Ident: + // If leaf represents an implicit type switch object or the type + // switch "assign" variable, expand to all of the type switch's + // implicit objects. + if implicits, _ := typeSwitchImplicits(info, path); len(implicits) > 0 { + obj = implicits[0] + } else { + obj = info.ObjectOf(n) + } + // If the original position was an embedded field, we want to jump + // to the field's type definition, not the field's definition. + if v, ok := obj.(*types.Var); ok && v.Embedded() { + // types.Info.Uses contains the embedded field's *types.TypeName. + if typeName := info.Uses[n]; typeName != nil { + obj = typeName + } + } + } + return obj +} + +// importDefinition returns locations defining a package referenced by the +// import spec containing pos. +// +// If pos is not inside an import spec, it returns nil, nil. +func importDefinition(ctx context.Context, s Snapshot, pkg Package, pgf *ParsedGoFile, pos token.Pos) ([]protocol.Location, error) { + var imp *ast.ImportSpec + for _, spec := range pgf.File.Imports { + // We use "<= End" to accept a query immediately after an ImportSpec. + if spec.Path.Pos() <= pos && pos <= spec.Path.End() { + imp = spec + } + } + if imp == nil { + return nil, nil + } + + importPath := UnquoteImportPath(imp) + impID := pkg.Metadata().DepsByImpPath[importPath] + if impID == "" { + return nil, fmt.Errorf("failed to resolve import %q", importPath) + } + impMetadata := s.Metadata(impID) + if impMetadata == nil { + return nil, fmt.Errorf("missing information for package %q", impID) + } + + var locs []protocol.Location + for _, f := range impMetadata.CompiledGoFiles { + fh, err := s.GetFile(ctx, f) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + continue + } + pgf, err := s.ParseGo(ctx, fh, ParseHeader) + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + continue + } + loc, err := pgf.NodeLocation(pgf.File) + if err != nil { + return nil, err + } + locs = append(locs, loc) + } + + if len(locs) == 0 { + return nil, fmt.Errorf("package %q has no readable files", impID) // incl. unsafe + } + + return locs, nil +} + +// TODO(rfindley): avoid the duplicate column mapping here, by associating a +// column mapper with each file handle. +func mapPosition(ctx context.Context, fset *token.FileSet, s FileSource, start, end token.Pos) (protocol.Location, error) { + file := fset.File(start) + uri := span.URIFromPath(file.Name()) + fh, err := s.GetFile(ctx, uri) + if err != nil { + return protocol.Location{}, err + } + content, err := fh.Read() + if err != nil { + return protocol.Location{}, err + } + m := protocol.NewMapper(fh.URI(), content) + return m.PosLocation(file, start, end) +} diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 0ed56e472cd..9497c657377 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -30,7 +30,7 @@ type IdentifierInfo struct { Type struct { MappedRange protocol.MappedRange - Object types.Object + Object *types.TypeName } Inferred *types.Signature @@ -48,11 +48,6 @@ type IdentifierInfo struct { qf types.Qualifier } -func (i *IdentifierInfo) IsImport() bool { - _, ok := i.Declaration.node.(*ast.ImportSpec) - return ok -} - type Declaration struct { MappedRange []protocol.MappedRange @@ -266,7 +261,8 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // findFileInDeps, which is also called below. Refactor // objToMappedRange to separate the find-file from the // lookup-position steps to avoid the redundancy. - rng, err := objToMappedRange(ctx, snapshot, pkg, result.Declaration.obj) + obj := result.Declaration.obj + rng, err := posToMappedRange(ctx, snapshot, pkg, obj.Pos(), adjustedObjEnd(obj)) if err != nil { return nil, err } @@ -301,7 +297,9 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if hasErrorType(result.Type.Object) { return result, nil } - if result.Type.MappedRange, err = objToMappedRange(ctx, snapshot, pkg, result.Type.Object); err != nil { + obj := result.Type.Object + // TODO(rfindley): no need to use an adjusted end here. + if result.Type.MappedRange, err = posToMappedRange(ctx, snapshot, pkg, obj.Pos(), adjustedObjEnd(obj)); err != nil { return nil, err } } @@ -407,7 +405,10 @@ func searchForEnclosing(info *types.Info, path []ast.Node) *types.TypeName { return nil } -func typeToObject(typ types.Type) types.Object { +// typeToObject returns the relevant type name for the given type, after +// unwrapping pointers, arrays, slices, channels, and function signatures with +// a single non-error result. +func typeToObject(typ types.Type) *types.TypeName { switch typ := typ.(type) { case *types.Named: return typ.Obj() @@ -422,7 +423,7 @@ func typeToObject(typ types.Type) types.Object { case *types.Signature: // Try to find a return value of a named type. If there's only one // such value, jump to its type definition. - var res types.Object + var res *types.TypeName results := typ.Results() for i := 0; i < results.Len(); i++ { diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index bb0b5ecb6d3..aaacc59b8e2 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -140,7 +140,7 @@ func DefaultOptions() *Options { LinksInHover: true, }, NavigationOptions: NavigationOptions{ - ImportShortcut: Both, + ImportShortcut: BothShortcuts, SymbolMatcher: SymbolFastFuzzy, SymbolStyle: DynamicSymbols, }, @@ -596,17 +596,17 @@ type InternalOptions struct { type ImportShortcut string const ( - Both ImportShortcut = "Both" - Link ImportShortcut = "Link" - Definition ImportShortcut = "Definition" + BothShortcuts ImportShortcut = "Both" + LinkShortcut ImportShortcut = "Link" + DefinitionShortcut ImportShortcut = "Definition" ) func (s ImportShortcut) ShowLinks() bool { - return s == Both || s == Link + return s == BothShortcuts || s == LinkShortcut } func (s ImportShortcut) ShowDefinition() bool { - return s == Both || s == Definition + return s == BothShortcuts || s == DefinitionShortcut } type Matcher string @@ -985,7 +985,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.LinksInHover) case "importShortcut": - if s, ok := result.asOneOf(string(Both), string(Link), string(Definition)); ok { + if s, ok := result.asOneOf(string(BothShortcuts), string(LinkShortcut), string(DefinitionShortcut)); ok { o.ImportShortcut = ImportShortcut(s) } diff --git a/gopls/internal/lsp/source/type_definition.go b/gopls/internal/lsp/source/type_definition.go new file mode 100644 index 00000000000..2a54fdfdbcb --- /dev/null +++ b/gopls/internal/lsp/source/type_definition.go @@ -0,0 +1,52 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "context" + "fmt" + "go/token" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/event" +) + +// TypeDefinition handles the textDocument/typeDefinition request for Go files. +func TypeDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Location, error) { + ctx, done := event.Start(ctx, "source.TypeDefinition") + defer done() + + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), TypecheckFull, NarrowestPackage) + if err != nil { + return nil, err + } + pos, err := pgf.PositionPos(position) + if err != nil { + return nil, err + } + + obj := referencedObject(pkg, pgf, pos) + if obj == nil { + return nil, nil + } + + typObj := typeToObject(obj.Type()) + if typObj == nil { + return nil, fmt.Errorf("no type definition for %s", obj.Name()) + } + + // Identifiers with the type "error" are a special case with no position. + if hasErrorType(typObj) { + // TODO(rfindley): we can do better here, returning a link to the builtin + // file. + return nil, nil + } + + loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, typObj.Pos(), typObj.Pos()+token.Pos(len(typObj.Name()))) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil +} diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 7dab47b0f57..c9a510224e3 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -51,7 +51,12 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { return false } -func objToMappedRange(ctx context.Context, snapshot Snapshot, pkg Package, obj types.Object) (protocol.MappedRange, error) { +// adjustedObjEnd returns the end position of obj, possibly modified for +// package names. +// +// TODO(rfindley): eliminate this function, by inlining it at callsites where +// it makes sense. +func adjustedObjEnd(obj types.Object) token.Pos { nameLen := len(obj.Name()) if pkgName, ok := obj.(*types.PkgName); ok { // An imported Go package has a package-local, unqualified name. @@ -68,7 +73,7 @@ func objToMappedRange(ctx context.Context, snapshot Snapshot, pkg Package, obj t nameLen = len(pkgName.Imported().Path()) + len(`""`) } } - return posToMappedRange(ctx, snapshot, pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen)) + return obj.Pos() + token.Pos(nameLen) } // posToMappedRange returns the MappedRange for the given [start, end) span, @@ -133,23 +138,6 @@ func FileKindForLang(langID string) FileKind { } } -func (k FileKind) String() string { - switch k { - case Go: - return "go" - case Mod: - return "go.mod" - case Sum: - return "go.sum" - case Tmpl: - return "tmpl" - case Work: - return "go.work" - default: - return fmt.Sprintf("unk%d", k) - } -} - // nodeAtPos returns the index and the node whose position is contained inside // the node list. func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index cbcc9b4e6f0..5f4e3f567d9 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -697,6 +697,23 @@ const ( Work ) +func (k FileKind) String() string { + switch k { + case Go: + return "go" + case Mod: + return "go.mod" + case Sum: + return "go.sum" + case Tmpl: + return "tmpl" + case Work: + return "go.work" + default: + return fmt.Sprintf("internal error: unknown file kind %d", k) + } +} + // Analyzer represents a go/analysis analyzer with some boolean properties // that let the user know how to use the analyzer. type Analyzer struct { diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index cad69c142c4..70a3336e72b 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -155,12 +155,11 @@ func main() {} ` for _, tt := range []struct { wantLinks int - wantDef bool importShortcut string }{ - {1, false, "Link"}, - {0, true, "Definition"}, - {1, true, "Both"}, + {1, "Link"}, + {0, "Definition"}, + {1, "Both"}, } { t.Run(tt.importShortcut, func(t *testing.T) { WithOptions( @@ -168,9 +167,7 @@ func main() {} ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") loc := env.GoToDefinition(env.RegexpSearch("main.go", `"fmt"`)) - if !tt.wantDef && (loc != (protocol.Location{})) { - t.Fatalf("expected no definition, got one: %v", loc) - } else if tt.wantDef && loc == (protocol.Location{}) { + if loc == (protocol.Location{}) { t.Fatalf("expected definition, got none") } links := env.DocumentLink("main.go") From d1e92d6abad47f7f97f43ef66290fae701527842 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 10 Jan 2023 21:49:15 -0500 Subject: [PATCH 691/723] gopls/internal/lsp/mod: reorder vulncheck quick fixes Nudge developers towards recommended actions by listing them first. More specifically, 0) Other diagnostics if any (e.g. errors in go.mod) 1) Run govulncheck 2) Upgrade to a specific version 3) Upgrade to latest 4) Reset govulncheck result Fixes golang/vscode-go#2576 Change-Id: Ib83f8dab5a1c5bea600fe1ec61701218c7a65ac1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461555 Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Suzy Mueller TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/mod/diagnostics.go | 37 +++++++++++++----------- gopls/internal/regtest/misc/vuln_test.go | 14 ++++----- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 25860ead36b..fdab833bcf8 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -527,34 +527,37 @@ func SelectUpgradeCodeActions(actions []protocol.CodeAction) []protocol.CodeActi if len(actions) <= 1 { return actions // return early if no sorting necessary } - var others []protocol.CodeAction + var versionedUpgrade, latestUpgrade, resetAction protocol.CodeAction + var chosenVersionedUpgrade string + var selected []protocol.CodeAction seen := make(map[string]bool) - set := make(map[string]protocol.CodeAction) for _, action := range actions { if strings.HasPrefix(action.Title, upgradeCodeActionPrefix) { - set[action.Command.Title] = action + if v := getUpgradeVersion(action); v == "latest" && latestUpgrade.Title == "" { + latestUpgrade = action + } else if versionedUpgrade.Title == "" || semver.Compare(v, chosenVersionedUpgrade) > 0 { + chosenVersionedUpgrade = v + versionedUpgrade = action + } + } else if strings.HasPrefix(action.Title, "Reset govulncheck") { + resetAction = action } else if !seen[action.Command.Title] { seen[action.Command.Title] = true - others = append(others, action) + selected = append(selected, action) } } - var upgrades []protocol.CodeAction - for _, action := range set { - upgrades = append(upgrades, action) + if versionedUpgrade.Title != "" { + selected = append(selected, versionedUpgrade) } - // Sort results by version number, latest first. - // There should be no duplicates at this point. - sort.Slice(upgrades, func(i, j int) bool { - vi, vj := getUpgradeVersion(upgrades[i]), getUpgradeVersion(upgrades[j]) - return vi == "latest" || (vj != "latest" && semver.Compare(vi, vj) > 0) - }) - // Choose at most one specific version and the latest. - if getUpgradeVersion(upgrades[0]) == "latest" { - return append(upgrades[:2], others...) + if latestUpgrade.Title != "" { + selected = append(selected, latestUpgrade) + } + if resetAction.Title != "" { + selected = append(selected, resetAction) } - return append(upgrades[:1], others...) + return selected } func getUpgradeVersion(p protocol.CodeAction) string { diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index bbf99b3b5dd..e311356e41d 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -503,16 +503,16 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { msg: "golang.org/amod has known vulnerabilities GO-2022-01, GO-2022-03.", severity: protocol.SeverityInformation, codeActions: []string{ - "Upgrade to latest", - "Upgrade to v1.0.6", "Run govulncheck to verify", + "Upgrade to v1.0.6", + "Upgrade to latest", }, }, }, codeActions: []string{ - "Upgrade to latest", - "Upgrade to v1.0.6", "Run govulncheck to verify", + "Upgrade to v1.0.6", + "Upgrade to latest", }, hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, }, @@ -655,8 +655,8 @@ func TestRunVulncheckWarning(t *testing.T) { msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.", severity: protocol.SeverityWarning, codeActions: []string{ - "Upgrade to latest", "Upgrade to v1.0.4", + "Upgrade to latest", "Reset govulncheck result", }, relatedInfo: []vulnRelatedInfo{ @@ -668,8 +668,8 @@ func TestRunVulncheckWarning(t *testing.T) { msg: "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.", severity: protocol.SeverityInformation, codeActions: []string{ - "Upgrade to latest", "Upgrade to v1.0.6", + "Upgrade to latest", "Reset govulncheck result", }, relatedInfo: []vulnRelatedInfo{ @@ -679,8 +679,8 @@ func TestRunVulncheckWarning(t *testing.T) { }, }, codeActions: []string{ - "Upgrade to latest", "Upgrade to v1.0.6", + "Upgrade to latest", "Reset govulncheck result", }, hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, From 26831282ac96b0fabd7cfaa922747f80c48f381e Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Tue, 10 Jan 2023 22:47:28 -0500 Subject: [PATCH 692/723] gopls/internal/lsp: differentiate govulncheck/vulncheck imports diags Use different diagnostic source names for diagnostics from govulncheck run ("govulncheck") and diagnostics from gopls's imports analysis ("vulncheck imports"). Fixes golang/vscode-go#2575 Change-Id: Ic79418146e4793bfc35fbf68d7539c7f0df1a636 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461556 Auto-Submit: Hyang-Ah Hana Kim TryBot-Result: Gopher Robot Reviewed-by: Suzy Mueller Reviewed-by: Robert Findley Run-TryBot: Hyang-Ah Hana Kim --- gopls/internal/lsp/command.go | 2 +- gopls/internal/lsp/diagnostics.go | 2 +- gopls/internal/lsp/mod/diagnostics.go | 24 ++++++++++++------------ gopls/internal/lsp/source/view.go | 3 ++- gopls/internal/regtest/misc/vuln_test.go | 13 +++++++++++-- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index a1ff862cb76..cb3065ff385 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -223,7 +223,7 @@ func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, args command c.s.clearDiagnosticSource(modCheckUpgradesSource) } - if args.DiagnosticSource == "" || args.DiagnosticSource == string(source.Vulncheck) { + if args.DiagnosticSource == "" || args.DiagnosticSource == string(source.Govulncheck) { deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), nil) c.s.clearDiagnosticSource(modVulncheckSource) } diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index a58f40497da..e473bcaa227 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -39,7 +39,7 @@ const ( orphanedSource workSource modCheckUpgradesSource - modVulncheckSource + modVulncheckSource // source.Govulncheck + source.Vulncheck ) // A diagnosticReport holds results for a single diagnostic source. diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index fdab833bcf8..7b598cf7273 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -191,20 +191,20 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, return nil, err } - fromGovulncheck := true + diagSource := source.Govulncheck vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()] if vs == nil && snapshot.View().Options().Vulncheck == source.ModeVulncheckImports { vs, err = snapshot.ModVuln(ctx, fh.URI()) if err != nil { return nil, err } - fromGovulncheck = false + diagSource = source.Vulncheck } if vs == nil || len(vs.Vulns) == 0 { return nil, nil } - suggestRunOrResetGovulncheck, err := suggestGovulncheckAction(fromGovulncheck, fh.URI()) + suggestRunOrResetGovulncheck, err := suggestGovulncheckAction(diagSource == source.Govulncheck, fh.URI()) if err != nil { // must not happen return nil, err // TODO: bug report @@ -310,8 +310,8 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, URI: fh.URI(), Range: rng, Severity: protocol.SeverityWarning, - Source: source.Vulncheck, - Message: getVulnMessage(req.Mod.Path, warning, true, fromGovulncheck), + Source: diagSource, + Message: getVulnMessage(req.Mod.Path, warning, true, diagSource == source.Govulncheck), SuggestedFixes: warningFixes, Related: relatedInfo, }) @@ -322,8 +322,8 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, URI: fh.URI(), Range: rng, Severity: protocol.SeverityInformation, - Source: source.Vulncheck, - Message: getVulnMessage(req.Mod.Path, info, false, fromGovulncheck), + Source: diagSource, + Message: getVulnMessage(req.Mod.Path, info, false, diagSource == source.Govulncheck), SuggestedFixes: infoFixes, Related: relatedInfo, }) @@ -365,8 +365,8 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, URI: fh.URI(), Range: rng, Severity: protocol.SeverityWarning, - Source: source.Vulncheck, - Message: getVulnMessage(stdlib, warning, true, fromGovulncheck), + Source: diagSource, + Message: getVulnMessage(stdlib, warning, true, diagSource == source.Govulncheck), SuggestedFixes: fixes, Related: relatedInfo, }) @@ -377,8 +377,8 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, URI: fh.URI(), Range: rng, Severity: protocol.SeverityInformation, - Source: source.Vulncheck, - Message: getVulnMessage(stdlib, info, false, fromGovulncheck), + Source: diagSource, + Message: getVulnMessage(stdlib, info, false, diagSource == source.Govulncheck), SuggestedFixes: fixes, Related: relatedInfo, }) @@ -396,7 +396,7 @@ func suggestGovulncheckAction(fromGovulncheck bool, uri span.URI) (source.Sugges if fromGovulncheck { resetVulncheck, err := command.NewResetGoModDiagnosticsCommand("Reset govulncheck result", command.ResetGoModDiagnosticsArgs{ URIArg: command.URIArg{URI: protocol.DocumentURI(uri)}, - DiagnosticSource: string(source.Vulncheck), + DiagnosticSource: string(source.Govulncheck), }) if err != nil { return source.SuggestedFix{}, err diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 5f4e3f567d9..c581286edca 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -840,7 +840,8 @@ const ( ModTidyError DiagnosticSource = "go mod tidy" OptimizationDetailsError DiagnosticSource = "optimizer details" UpgradeNotification DiagnosticSource = "upgrade available" - Vulncheck DiagnosticSource = "govulncheck" + Vulncheck DiagnosticSource = "vulncheck imports" + Govulncheck DiagnosticSource = "govulncheck" TemplateError DiagnosticSource = "template" WorkFileError DiagnosticSource = "go.work file" ) diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index e311356e41d..8badc879e1b 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -20,6 +20,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/gopls/internal/vulncheck/vulntest" @@ -502,6 +503,7 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { { msg: "golang.org/amod has known vulnerabilities GO-2022-01, GO-2022-03.", severity: protocol.SeverityInformation, + source: string(source.Vulncheck), codeActions: []string{ "Run govulncheck to verify", "Upgrade to v1.0.6", @@ -521,6 +523,7 @@ func TestRunVulncheckPackageDiagnostics(t *testing.T) { { msg: "golang.org/bmod has a vulnerability GO-2022-02.", severity: protocol.SeverityInformation, + source: string(source.Vulncheck), codeActions: []string{ "Run govulncheck to verify", }, @@ -654,6 +657,7 @@ func TestRunVulncheckWarning(t *testing.T) { { msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.", severity: protocol.SeverityWarning, + source: string(source.Govulncheck), codeActions: []string{ "Upgrade to v1.0.4", "Upgrade to latest", @@ -667,6 +671,7 @@ func TestRunVulncheckWarning(t *testing.T) { { msg: "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.", severity: protocol.SeverityInformation, + source: string(source.Govulncheck), codeActions: []string{ "Upgrade to v1.0.6", "Upgrade to latest", @@ -690,6 +695,7 @@ func TestRunVulncheckWarning(t *testing.T) { { msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.", severity: protocol.SeverityWarning, + source: string(source.Govulncheck), codeActions: []string{ "Reset govulncheck result", // no fix, but we should give an option to reset. }, @@ -817,6 +823,7 @@ func TestGovulncheckInfo(t *testing.T) { { msg: "golang.org/bmod has a vulnerability GO-2022-02 that is not used in the code.", severity: protocol.SeverityInformation, + source: string(source.Govulncheck), codeActions: []string{ "Reset govulncheck result", }, @@ -879,8 +886,8 @@ func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagEx t.Errorf("no diagnostic at %q matching %q found\n", pattern, w.msg) continue } - if diag.Severity != w.severity { - t.Errorf("incorrect severity for %q, want %s got %s\n", w.msg, w.severity, diag.Severity) + if diag.Severity != w.severity || diag.Source != w.source { + t.Errorf("incorrect (severity, source) for %q, want (%s, %s) got (%s, %s)\n", w.msg, w.severity, w.source, diag.Severity, diag.Source) } sort.Slice(w.relatedInfo, func(i, j int) bool { return w.relatedInfo[i].less(w.relatedInfo[j]) }) if got, want := summarizeRelatedInfo(diag.RelatedInformation), w.relatedInfo; !cmp.Equal(got, want) { @@ -937,6 +944,8 @@ type vulnDiag struct { // relatedInfo is related info message prefixed by the file base. // See summarizeRelatedInfo. relatedInfo []vulnRelatedInfo + // diagnostic source. + source string } func (i vulnRelatedInfo) less(j vulnRelatedInfo) bool { From bd5e595bef0e162ff934201506b5660ff2dda738 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 30 Jan 2023 18:35:50 -0500 Subject: [PATCH 693/723] gopls/internal/lsp/cache: add missing mutex Change-Id: Ie12588961a3f2ceba3a0c9d33eccbc83cf1f4871 Reviewed-on: https://go-review.googlesource.com/c/tools/+/464057 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Alan Donovan Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/fs_overlay.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gopls/internal/lsp/cache/fs_overlay.go b/gopls/internal/lsp/cache/fs_overlay.go index d277bc6aa18..36a7194cebb 100644 --- a/gopls/internal/lsp/cache/fs_overlay.go +++ b/gopls/internal/lsp/cache/fs_overlay.go @@ -30,6 +30,8 @@ func newOverlayFS(delegate source.FileSource) *overlayFS { // Overlays returns a new unordered array of overlays. func (fs *overlayFS) Overlays() []*Overlay { + fs.mu.Lock() + defer fs.mu.Unlock() overlays := make([]*Overlay, 0, len(fs.overlays)) for _, overlay := range fs.overlays { overlays = append(overlays, overlay) From 684a1c0d5edd116b8b5ea7592e823036c7730344 Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 10 Jan 2023 15:00:29 -0800 Subject: [PATCH 694/723] go/analysis/internal/analysisflags: use os.Executable for program path For -V=full, use the os.Executable for the program path instead of os.Args[0]. Fixes golang/go#57716 Change-Id: Ia571f922aef037a16ffc78f8c2c32ae4c70918c1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/461496 Run-TryBot: Tim King TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Alan Donovan Reviewed-by: Bryan Mills --- go/analysis/internal/analysisflags/flags.go | 7 +++++-- go/analysis/internal/analysisflags/flags_test.go | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go index 2ea630608c4..e127a42b97a 100644 --- a/go/analysis/internal/analysisflags/flags.go +++ b/go/analysis/internal/analysisflags/flags.go @@ -206,7 +206,7 @@ func (versionFlag) Get() interface{} { return nil } func (versionFlag) String() string { return "" } func (versionFlag) Set(s string) error { if s != "full" { - log.Fatalf("unsupported flag value: -V=%s", s) + log.Fatalf("unsupported flag value: -V=%s (use -V=full)", s) } // This replicates the minimal subset of @@ -218,7 +218,10 @@ func (versionFlag) Set(s string) error { // Formats: // $progname version devel ... buildID=... // $progname version go1.9.1 - progname := os.Args[0] + progname, err := os.Executable() + if err != nil { + return err + } f, err := os.Open(progname) if err != nil { log.Fatal(err) diff --git a/go/analysis/internal/analysisflags/flags_test.go b/go/analysis/internal/analysisflags/flags_test.go index 1f055dde72d..b5cfb3d4430 100644 --- a/go/analysis/internal/analysisflags/flags_test.go +++ b/go/analysis/internal/analysisflags/flags_test.go @@ -42,7 +42,7 @@ func TestExec(t *testing.T) { for _, test := range []struct { flags string - want string + want string // output should contain want }{ {"", "[a1 a2 a3]"}, {"-a1=0", "[a2 a3]"}, @@ -50,6 +50,7 @@ func TestExec(t *testing.T) { {"-a1", "[a1]"}, {"-a1=1 -a3=1", "[a1 a3]"}, {"-a1=1 -a3=0", "[a1]"}, + {"-V=full", "analysisflags.test version devel"}, } { cmd := exec.Command(progname, "-test.run=TestExec") cmd.Env = append(os.Environ(), "ANALYSISFLAGS_CHILD=1", "FLAGS="+test.flags) @@ -60,8 +61,8 @@ func TestExec(t *testing.T) { } got := strings.TrimSpace(string(output)) - if got != test.want { - t.Errorf("got %s, want %s", got, test.want) + if !strings.Contains(got, test.want) { + t.Errorf("got %q, does not contain %q", got, test.want) } } } From aa633e7edc60a448f987675a4b58124866aacbd3 Mon Sep 17 00:00:00 2001 From: Peter Weinbergr Date: Wed, 25 Jan 2023 09:23:21 -0500 Subject: [PATCH 695/723] tools/gopls: provide markdown for completion and signature help If the client prefers markdown, provide markdown. There is a change to the LSP stubs, with Documentation fields becoming Or-types (string|MarkupContent). If the client prefers, the returned Documentation is markdown. Fixes: golang/go#57300 Change-Id: I57300146333552da3849c1b6bfb97793042faee2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463377 Reviewed-by: Robert Findley Run-TryBot: Peter Weinberger TryBot-Result: Gopher Robot --- gopls/internal/lsp/cmd/signature.go | 11 +++- gopls/internal/lsp/completion.go | 11 +++- gopls/internal/lsp/completion_test.go | 12 ++-- gopls/internal/lsp/fake/editor.go | 17 ++++++ .../internal/lsp/protocol/generate/tables.go | 10 ++- gopls/internal/lsp/protocol/tsclient.go | 2 +- gopls/internal/lsp/protocol/tsjson.go | 2 +- gopls/internal/lsp/protocol/tsprotocol.go | 10 +-- gopls/internal/lsp/protocol/tsserver.go | 2 +- gopls/internal/lsp/regtest/wrappers.go | 10 +++ gopls/internal/lsp/source/signature_help.go | 23 ++++++- gopls/internal/regtest/misc/hover_test.go | 61 +++++++++++++++++++ 12 files changed, 147 insertions(+), 24 deletions(-) diff --git a/gopls/internal/lsp/cmd/signature.go b/gopls/internal/lsp/cmd/signature.go index 64c892e5ee5..4d47cd2d4bc 100644 --- a/gopls/internal/lsp/cmd/signature.go +++ b/gopls/internal/lsp/cmd/signature.go @@ -73,8 +73,15 @@ func (r *signature) Run(ctx context.Context, args ...string) error { // see toProtocolSignatureHelp in lsp/signature_help.go signature := s.Signatures[0] fmt.Printf("%s\n", signature.Label) - if signature.Documentation != "" { - fmt.Printf("\n%s\n", signature.Documentation) + switch x := signature.Documentation.Value.(type) { + case string: + if x != "" { + fmt.Printf("\n%s\n", x) + } + case protocol.MarkupContent: + if x.Value != "" { + fmt.Printf("\n%s\n", x.Value) + } } return nil diff --git a/gopls/internal/lsp/completion.go b/gopls/internal/lsp/completion.go index d63e3a3832c..b2e50cc8074 100644 --- a/gopls/internal/lsp/completion.go +++ b/gopls/internal/lsp/completion.go @@ -101,6 +101,15 @@ func toProtocolCompletionItems(candidates []completion.CompletionItem, rng proto continue } + doc := &protocol.Or_CompletionItem_documentation{ + Value: protocol.MarkupContent{ + Kind: protocol.Markdown, + Value: source.CommentToMarkdown(candidate.Documentation), + }, + } + if options.PreferredContentFormat != protocol.Markdown { + doc.Value = candidate.Documentation + } item := protocol.CompletionItem{ Label: candidate.Label, Detail: candidate.Detail, @@ -121,7 +130,7 @@ func toProtocolCompletionItems(candidates []completion.CompletionItem, rng proto FilterText: strings.TrimLeft(candidate.InsertText, "&*"), Preselect: i == 0, - Documentation: candidate.Documentation, + Documentation: doc, Tags: candidate.Tags, Deprecated: candidate.Deprecated, } diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index 9a72f123e62..cd3bcec992c 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -115,11 +115,13 @@ func expected(t *testing.T, test tests.Completion, items tests.CompletionItems) toProtocolCompletionItem := func(item *completion.CompletionItem) protocol.CompletionItem { pItem := protocol.CompletionItem{ - Label: item.Label, - Kind: item.Kind, - Detail: item.Detail, - Documentation: item.Documentation, - InsertText: item.InsertText, + Label: item.Label, + Kind: item.Kind, + Detail: item.Detail, + Documentation: &protocol.Or_CompletionItem_documentation{ + Value: item.Documentation, + }, + InsertText: item.InsertText, TextEdit: &protocol.TextEdit{ NewText: item.Snippet(), }, diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 70d5e5e547e..eaeca85f8c6 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -1184,6 +1184,23 @@ func (e *Editor) Implementations(ctx context.Context, loc protocol.Location) ([] return e.Server.Implementation(ctx, params) } +func (e *Editor) SignatureHelp(ctx context.Context, loc protocol.Location) (*protocol.SignatureHelp, error) { + if e.Server == nil { + return nil, nil + } + path := e.sandbox.Workdir.URIToPath(loc.URI) + e.mu.Lock() + _, ok := e.buffers[path] + e.mu.Unlock() + if !ok { + return nil, fmt.Errorf("buffer %q is not open", path) + } + params := &protocol.SignatureHelpParams{ + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), + } + return e.Server.SignatureHelp(ctx, params) +} + func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error { closed, opened, err := e.renameBuffers(ctx, oldPath, newPath) if err != nil { diff --git a/gopls/internal/lsp/protocol/generate/tables.go b/gopls/internal/lsp/protocol/generate/tables.go index 93a04367370..838990c4137 100644 --- a/gopls/internal/lsp/protocol/generate/tables.go +++ b/gopls/internal/lsp/protocol/generate/tables.go @@ -128,11 +128,10 @@ var usedGoplsStar = make(map[prop]bool) // For gopls compatibility, use a different, typically more restrictive, type for some fields. var renameProp = map[prop]string{ - {"CancelParams", "id"}: "interface{}", - {"Command", "arguments"}: "[]json.RawMessage", - {"CompletionItem", "documentation"}: "string", - {"CompletionItem", "textEdit"}: "TextEdit", - {"Diagnostic", "code"}: "interface{}", + {"CancelParams", "id"}: "interface{}", + {"Command", "arguments"}: "[]json.RawMessage", + {"CompletionItem", "textEdit"}: "TextEdit", + {"Diagnostic", "code"}: "interface{}", {"DocumentDiagnosticReportPartialResult", "relatedDocuments"}: "map[DocumentURI]interface{}", @@ -181,7 +180,6 @@ var renameProp = map[prop]string{ {"ServerCapabilities", "typeDefinitionProvider"}: "interface{}", {"ServerCapabilities", "typeHierarchyProvider"}: "interface{}", {"ServerCapabilities", "workspaceSymbolProvider"}: "bool", - {"SignatureInformation", "documentation"}: "string", {"TextDocumentEdit", "edits"}: "[]TextEdit", {"TextDocumentSyncOptions", "save"}: "SaveOptions", {"WorkspaceEdit", "documentChanges"}: "[]DocumentChanges", diff --git a/gopls/internal/lsp/protocol/tsclient.go b/gopls/internal/lsp/protocol/tsclient.go index 116a81b4d5c..b51b1f6fa38 100644 --- a/gopls/internal/lsp/protocol/tsclient.go +++ b/gopls/internal/lsp/protocol/tsclient.go @@ -7,7 +7,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14) +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23) import ( "context" diff --git a/gopls/internal/lsp/protocol/tsjson.go b/gopls/internal/lsp/protocol/tsjson.go index a0f11c61238..0c8bf10ecd6 100644 --- a/gopls/internal/lsp/protocol/tsjson.go +++ b/gopls/internal/lsp/protocol/tsjson.go @@ -7,7 +7,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14) +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23) import "encoding/json" diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go index becb255b1fb..54a878c3d27 100644 --- a/gopls/internal/lsp/protocol/tsprotocol.go +++ b/gopls/internal/lsp/protocol/tsprotocol.go @@ -7,7 +7,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14) +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23) import "encoding/json" @@ -667,7 +667,7 @@ type CompletionItem struct { // line 4550 */ Detail string `json:"detail,omitempty"` // A human-readable string that represents a doc-comment. - Documentation string `json:"documentation,omitempty"` + Documentation *Or_CompletionItem_documentation `json:"documentation,omitempty"` /* * Indicates if this item is deprecated. * @deprecated Use `tags` instead. @@ -2970,8 +2970,8 @@ type Msg_TextDocumentContentChangeEvent struct { // line 14034 Text string `json:"text"` } -// created for Literal (Lit_TextDocumentFilter_Item1) -type Msg_TextDocumentFilter struct { // line 14193 +// created for Literal (Lit_TextDocumentFilter_Item0) +type Msg_TextDocumentFilter struct { // line 14160 // A language id, like `typescript`. Language string `json:"language"` // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. @@ -4885,7 +4885,7 @@ type SignatureInformation struct { // line 8848 * The human-readable doc-comment of this signature. Will be shown * in the UI but can be omitted. */ - Documentation string `json:"documentation,omitempty"` + Documentation *Or_SignatureInformation_documentation `json:"documentation,omitempty"` // The parameters of this signature. Parameters []ParameterInformation `json:"parameters,omitempty"` /* diff --git a/gopls/internal/lsp/protocol/tsserver.go b/gopls/internal/lsp/protocol/tsserver.go index 6446a014564..d96801080aa 100644 --- a/gopls/internal/lsp/protocol/tsserver.go +++ b/gopls/internal/lsp/protocol/tsserver.go @@ -7,7 +7,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-14) +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23) import ( "context" diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index 1207a1425af..cdd39e5c7c9 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -417,6 +417,16 @@ func (e *Env) RenameFile(oldPath, newPath string) { } } +// SignatureHelp wraps Editor.SignatureHelp, calling t.Fatal on error +func (e *Env) SignatureHelp(loc protocol.Location) *protocol.SignatureHelp { + e.T.Helper() + sighelp, err := e.Editor.SignatureHelp(e.Ctx, loc) + if err != nil { + e.T.Fatal(err) + } + return sighelp +} + // Completion executes a completion request on the server. func (e *Env) Completion(loc protocol.Location) *protocol.CompletionList { e.T.Helper() diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index 1d43dc5407d..4902496466a 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -10,6 +10,7 @@ import ( "go/ast" "go/token" "go/types" + "strings" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -117,7 +118,7 @@ FindCall: } return &protocol.SignatureInformation{ Label: name + s.Format(), - Documentation: s.doc, + Documentation: stringToSigInfoDocumentation(s.doc, snapshot.View().Options()), Parameters: paramInfo, }, activeParam, nil } @@ -134,7 +135,7 @@ func builtinSignature(ctx context.Context, snapshot Snapshot, callExpr *ast.Call activeParam := activeParameter(callExpr, len(sig.params), sig.variadic, pos) return &protocol.SignatureInformation{ Label: sig.name + sig.Format(), - Documentation: sig.doc, + Documentation: stringToSigInfoDocumentation(sig.doc, snapshot.View().Options()), Parameters: paramInfo, }, activeParam, nil @@ -165,3 +166,21 @@ func activeParameter(callExpr *ast.CallExpr, numParams int, variadic bool, pos t } return activeParam } + +func stringToSigInfoDocumentation(s string, options *Options) *protocol.Or_SignatureInformation_documentation { + v := s + k := protocol.PlainText + if options.PreferredContentFormat == protocol.Markdown { + v = CommentToMarkdown(s) + // whether or not content is newline terminated may not matter for LSP clients, + // but our tests expect trailing newlines to be stripped. + v = strings.TrimSuffix(v, "\n") // TODO(pjw): change the golden files + k = protocol.Markdown + } + return &protocol.Or_SignatureInformation_documentation{ + Value: protocol.MarkupContent{ + Kind: k, + Value: v, + }, + } +} diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go index 6b1d2438698..7054993079c 100644 --- a/gopls/internal/regtest/misc/hover_test.go +++ b/gopls/internal/regtest/misc/hover_test.go @@ -10,7 +10,9 @@ import ( "testing" "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" ) func TestHoverUnexported(t *testing.T) { @@ -270,3 +272,62 @@ go 1.16 env.Hover(env.RegexpSearch("go.mod", "go")) // no panic }) } + +func TestHoverCompletionMarkdown(t *testing.T) { + testenv.NeedsGo1Point(t, 19) + const source = ` +-- go.mod -- +module mod.com +go 1.19 +-- main.go -- +package main +// Just says [hello]. +// +// [hello]: https://en.wikipedia.org/wiki/Hello +func Hello() string { + Hello() //Here + return "hello" +} +` + Run(t, source, func(t *testing.T, env *Env) { + // Hover, Completion, and SignatureHelp should all produce markdown + // check that the markdown for SignatureHelp and Completion are + // the same, and contained in that for Hover (up to trailing \n) + env.OpenFile("main.go") + loc := env.RegexpSearch("main.go", "func (Hello)") + hover, _ := env.Hover(loc) + hoverContent := hover.Value + + loc = env.RegexpSearch("main.go", "//Here") + loc.Range.Start.Character -= 3 // Hello(_) //Here + completions := env.Completion(loc) + signatures := env.SignatureHelp(loc) + + if len(completions.Items) != 1 { + t.Errorf("got %d completions, expected 1", len(completions.Items)) + } + if len(signatures.Signatures) != 1 { + t.Errorf("got %d signatures, expected 1", len(signatures.Signatures)) + } + item := completions.Items[0].Documentation.Value + var itemContent string + if x, ok := item.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown { + t.Fatalf("%#v is not markdown", item) + } else { + itemContent = strings.Trim(x.Value, "\n") + } + sig := signatures.Signatures[0].Documentation.Value + var sigContent string + if x, ok := sig.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown { + t.Fatalf("%#v is not markdown", item) + } else { + sigContent = x.Value + } + if itemContent != sigContent { + t.Errorf("item:%q not sig:%q", itemContent, sigContent) + } + if !strings.Contains(hoverContent, itemContent) { + t.Errorf("hover:%q does not containt sig;%q", hoverContent, sigContent) + } + }) +} From 072fca58f7d2adbd978b9a7b11e946d404dac985 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Mon, 30 Jan 2023 12:44:53 -0500 Subject: [PATCH 696/723] gopls/protocol: use the current definition of the lsp There are no changes to main line gopls code. The type of ParamInitialize.Trace changes from string to a string type TraceValues. This only affects fake/editor.go Four messages were changed to go from the server to the client, necessitating changes to the various test clients. Some copied comments in tsprotocol.go have changed. Finally, the parse completeness test found a new field, Deprecated, in metaModel.json. It duplicates information already in the Documentation field. Change-Id: I14bbc63e8b1a1aebb7cd2d55ce282fe42568ecc2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/464095 Reviewed-by: Alan Donovan Run-TryBot: Peter Weinberger gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/cmd/cmd.go | 16 + gopls/internal/lsp/fake/client.go | 8 + gopls/internal/lsp/fake/editor.go | 3 +- gopls/internal/lsp/protocol/generate/types.go | 2 + gopls/internal/lsp/protocol/tsclient.go | 30 +- gopls/internal/lsp/protocol/tsjson.go | 122 +- gopls/internal/lsp/protocol/tsprotocol.go | 1542 ++++++++--------- gopls/internal/lsp/protocol/tsserver.go | 30 +- gopls/internal/lsp/server_gen.go | 16 - 9 files changed, 886 insertions(+), 883 deletions(-) diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index afe6dda7336..96d43d34662 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -528,6 +528,22 @@ func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDonePr return nil } +func (c *cmdClient) DiagnosticRefresh(context.Context) error { + return nil +} + +func (c *cmdClient) InlayHintRefresh(context.Context) error { + return nil +} + +func (c *cmdClient) SemanticTokensRefresh(context.Context) error { + return nil +} + +func (c *cmdClient) InlineValueRefresh(context.Context) error { + return nil +} + func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile { file, found := c.files[uri] if !found || file.err != nil { diff --git a/gopls/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go index 34b95dbd1c1..d6c886f179d 100644 --- a/gopls/internal/lsp/fake/client.go +++ b/gopls/internal/lsp/fake/client.go @@ -34,6 +34,14 @@ type Client struct { func (c *Client) CodeLensRefresh(context.Context) error { return nil } +func (c *Client) InlayHintRefresh(context.Context) error { return nil } + +func (c *Client) DiagnosticRefresh(context.Context) error { return nil } + +func (c *Client) InlineValueRefresh(context.Context) error { return nil } + +func (c *Client) SemanticTokensRefresh(context.Context) error { return nil } + func (c *Client) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } func (c *Client) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error { diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index eaeca85f8c6..08074400e55 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -279,7 +279,8 @@ func (e *Editor) initialize(ctx context.Context) error { }, } - params.Trace = "messages" + trace := protocol.TraceValues("messages") + params.Trace = &trace // TODO: support workspace folders. if e.Server != nil { resp, err := e.Server.Initialize(ctx, params) diff --git a/gopls/internal/lsp/protocol/generate/types.go b/gopls/internal/lsp/protocol/generate/types.go index c509031fabf..a8d5af1e649 100644 --- a/gopls/internal/lsp/protocol/generate/types.go +++ b/gopls/internal/lsp/protocol/generate/types.go @@ -85,6 +85,7 @@ type Enumeration struct { // A TypeAlias is the parsed version of an LSP type alias from the spec type TypeAlias struct { Documentation string `json:"documentation"` + Deprecated string `json:"deprecated"` Name string `json:"name"` Proposed bool `json:"proposed"` Since string `json:"since"` @@ -125,6 +126,7 @@ type NameType struct { Type *Type `json:"type"` Optional bool `json:"optional"` Documentation string `json:"documentation"` + Deprecated string `json:"deprecated"` Since string `json:"since"` Proposed bool `json:"proposed"` Line int `json:"line"` diff --git a/gopls/internal/lsp/protocol/tsclient.go b/gopls/internal/lsp/protocol/tsclient.go index b51b1f6fa38..96ec28474dd 100644 --- a/gopls/internal/lsp/protocol/tsclient.go +++ b/gopls/internal/lsp/protocol/tsclient.go @@ -7,7 +7,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23) +// git hash 9b742021fb04ad081aa3676a9eecf4fa612084b4 (as of 2023-01-30) import ( "context" @@ -31,6 +31,10 @@ type Client interface { ApplyEdit(context.Context, *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) // workspace/applyEdit CodeLensRefresh(context.Context) error // workspace/codeLens/refresh Configuration(context.Context, *ParamConfiguration) ([]LSPAny, error) // workspace/configuration + DiagnosticRefresh(context.Context) error // workspace/diagnostic/refresh + InlayHintRefresh(context.Context) error // workspace/inlayHint/refresh + InlineValueRefresh(context.Context) error // workspace/inlineValue/refresh + SemanticTokensRefresh(context.Context) error // workspace/semanticTokens/refresh WorkspaceFolders(context.Context) ([]WorkspaceFolder, error) // workspace/workspaceFolders } @@ -142,6 +146,18 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, return true, reply(ctx, nil, err) } return true, reply(ctx, resp, nil) + case "workspace/diagnostic/refresh": + err := client.DiagnosticRefresh(ctx) + return true, reply(ctx, nil, err) + case "workspace/inlayHint/refresh": + err := client.InlayHintRefresh(ctx) + return true, reply(ctx, nil, err) + case "workspace/inlineValue/refresh": + err := client.InlineValueRefresh(ctx) + return true, reply(ctx, nil, err) + case "workspace/semanticTokens/refresh": + err := client.SemanticTokensRefresh(ctx) + return true, reply(ctx, nil, err) case "workspace/workspaceFolders": resp, err := client.WorkspaceFolders(ctx) if err != nil { @@ -211,6 +227,18 @@ func (s *clientDispatcher) Configuration(ctx context.Context, params *ParamConfi } return result, nil } +func (s *clientDispatcher) DiagnosticRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/diagnostic/refresh", nil, nil) +} +func (s *clientDispatcher) InlayHintRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/inlayHint/refresh", nil, nil) +} +func (s *clientDispatcher) InlineValueRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/inlineValue/refresh", nil, nil) +} +func (s *clientDispatcher) SemanticTokensRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/semanticTokens/refresh", nil, nil) +} func (s *clientDispatcher) WorkspaceFolders(ctx context.Context) ([]WorkspaceFolder, error) { var result []WorkspaceFolder if err := s.sender.Call(ctx, "workspace/workspaceFolders", nil, &result); err != nil { diff --git a/gopls/internal/lsp/protocol/tsjson.go b/gopls/internal/lsp/protocol/tsjson.go index 0c8bf10ecd6..a2904aa2dda 100644 --- a/gopls/internal/lsp/protocol/tsjson.go +++ b/gopls/internal/lsp/protocol/tsjson.go @@ -7,14 +7,14 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23) +// git hash 9b742021fb04ad081aa3676a9eecf4fa612084b4 (as of 2023-01-30) import "encoding/json" import "errors" import "fmt" -// from line 4790 +// from line 4768 func (t OrFEditRangePItemDefaults) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case FEditRangePItemDefaults: @@ -45,7 +45,7 @@ func (t *OrFEditRangePItemDefaults) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [FEditRangePItemDefaults Range]") } -// from line 9837 +// from line 9810 func (t OrFNotebookPNotebookSelector) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case NotebookDocumentFilter: @@ -76,7 +76,7 @@ func (t *OrFNotebookPNotebookSelector) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [NotebookDocumentFilter string]") } -// from line 5539 +// from line 5519 func (t OrPLocation_workspace_symbol) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case Location: @@ -107,7 +107,7 @@ func (t *OrPLocation_workspace_symbol) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [Location PLocationMsg_workspace_symbol]") } -// from line 4185 +// from line 4162 func (t OrPSection_workspace_didChangeConfiguration) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case []string: @@ -138,7 +138,7 @@ func (t *OrPSection_workspace_didChangeConfiguration) UnmarshalJSON(x []byte) er return errors.New("unmarshal failed to match one of [[]string string]") } -// from line 7080 +// from line 7074 func (t OrPTooltipPLabel) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case MarkupContent: @@ -169,7 +169,7 @@ func (t *OrPTooltipPLabel) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [MarkupContent string]") } -// from line 3721 +// from line 3698 func (t OrPTooltip_textDocument_inlayHint) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case MarkupContent: @@ -200,7 +200,7 @@ func (t *OrPTooltip_textDocument_inlayHint) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [MarkupContent string]") } -// from line 6203 +// from line 6183 func (t Or_CancelParams_id) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case int32: @@ -231,7 +231,7 @@ func (t *Or_CancelParams_id) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [int32 string]") } -// from line 4604 +// from line 4581 func (t Or_CompletionItem_documentation) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case MarkupContent: @@ -262,7 +262,7 @@ func (t *Or_CompletionItem_documentation) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [MarkupContent string]") } -// from line 4686 +// from line 4664 func (t Or_CompletionItem_textEdit) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case InsertReplaceEdit: @@ -293,7 +293,7 @@ func (t *Or_CompletionItem_textEdit) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [InsertReplaceEdit TextEdit]") } -// from line 13779 +// from line 13752 func (t Or_Definition) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case Location: @@ -324,7 +324,7 @@ func (t *Or_Definition) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [Location []Location]") } -// from line 8567 +// from line 8546 func (t Or_Diagnostic_code) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case int32: @@ -355,7 +355,7 @@ func (t *Or_Diagnostic_code) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [int32 string]") } -// from line 13911 +// from line 13884 func (t Or_DocumentDiagnosticReport) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case RelatedFullDocumentDiagnosticReport: @@ -386,7 +386,7 @@ func (t *Or_DocumentDiagnosticReport) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]") } -// from line 3844 +// from line 3821 func (t Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case FullDocumentDiagnosticReport: @@ -417,7 +417,7 @@ func (t *Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value) Unmars return errors.New("unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]") } -// from line 14120 +// from line 14094 func (t Or_DocumentFilter) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case NotebookCellTextDocumentFilter: @@ -448,7 +448,7 @@ func (t *Or_DocumentFilter) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [NotebookCellTextDocumentFilter TextDocumentFilter]") } -// from line 4912 +// from line 4890 func (t Or_Hover_contents) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case MarkedString: @@ -486,7 +486,7 @@ func (t *Or_Hover_contents) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [MarkedString MarkupContent []MarkedString]") } -// from line 3680 +// from line 3657 func (t Or_InlayHint_label) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case []InlayHintLabelPart: @@ -517,7 +517,7 @@ func (t *Or_InlayHint_label) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [[]InlayHintLabelPart string]") } -// from line 13889 +// from line 13862 func (t Or_InlineValue) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case InlineValueEvaluatableExpression: @@ -555,7 +555,7 @@ func (t *Or_InlineValue) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]") } -// from line 14086 +// from line 14059 func (t Or_MarkedString) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case Msg_MarkedString: @@ -586,7 +586,7 @@ func (t *Or_MarkedString) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [Msg_MarkedString string]") } -// from line 10144 +// from line 10117 func (t Or_NotebookCellTextDocumentFilter_notebook) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case NotebookDocumentFilter: @@ -617,7 +617,7 @@ func (t *Or_NotebookCellTextDocumentFilter_notebook) UnmarshalJSON(x []byte) err return errors.New("unmarshal failed to match one of [NotebookDocumentFilter string]") } -// from line 9883 +// from line 9856 func (t Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case NotebookDocumentFilter: @@ -648,7 +648,7 @@ func (t *Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook) Un return errors.New("unmarshal failed to match one of [NotebookDocumentFilter string]") } -// from line 7173 +// from line 7167 func (t Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case FullDocumentDiagnosticReport: @@ -679,7 +679,7 @@ func (t *Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value) Unmarsha return errors.New("unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]") } -// from line 7212 +// from line 7206 func (t Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case FullDocumentDiagnosticReport: @@ -710,7 +710,7 @@ func (t *Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value) Unm return errors.New("unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]") } -// from line 10767 +// from line 10740 func (t Or_RelativePattern_baseUri) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case URI: @@ -741,7 +741,7 @@ func (t *Or_RelativePattern_baseUri) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [URI WorkspaceFolder]") } -// from line 1379 +// from line 1370 func (t Or_Result_textDocument_codeAction_Item0_Elem) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case CodeAction: @@ -772,7 +772,7 @@ func (t *Or_Result_textDocument_codeAction_Item0_Elem) UnmarshalJSON(x []byte) e return errors.New("unmarshal failed to match one of [CodeAction Command]") } -// from line 12223 +// from line 12196 func (t Or_SemanticTokensClientCapabilities_requests_full) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case FFullPRequests: @@ -803,7 +803,7 @@ func (t *Or_SemanticTokensClientCapabilities_requests_full) UnmarshalJSON(x []by return errors.New("unmarshal failed to match one of [FFullPRequests bool]") } -// from line 12203 +// from line 12176 func (t Or_SemanticTokensClientCapabilities_requests_range) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case FRangePRequests: @@ -834,7 +834,7 @@ func (t *Or_SemanticTokensClientCapabilities_requests_range) UnmarshalJSON(x []b return errors.New("unmarshal failed to match one of [FRangePRequests bool]") } -// from line 6584 +// from line 6578 func (t Or_SemanticTokensOptions_full) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case PFullESemanticTokensOptions: @@ -865,7 +865,7 @@ func (t *Or_SemanticTokensOptions_full) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [PFullESemanticTokensOptions bool]") } -// from line 6564 +// from line 6558 func (t Or_SemanticTokensOptions_range) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case PRangeESemanticTokensOptions: @@ -896,7 +896,7 @@ func (t *Or_SemanticTokensOptions_range) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [PRangeESemanticTokensOptions bool]") } -// from line 8247 +// from line 8226 func (t Or_ServerCapabilities_callHierarchyProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case CallHierarchyOptions: @@ -934,7 +934,7 @@ func (t *Or_ServerCapabilities_callHierarchyProvider) UnmarshalJSON(x []byte) er return errors.New("unmarshal failed to match one of [CallHierarchyOptions CallHierarchyRegistrationOptions bool]") } -// from line 8055 +// from line 8034 func (t Or_ServerCapabilities_codeActionProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case CodeActionOptions: @@ -965,7 +965,7 @@ func (t *Or_ServerCapabilities_codeActionProvider) UnmarshalJSON(x []byte) error return errors.New("unmarshal failed to match one of [CodeActionOptions bool]") } -// from line 8091 +// from line 8070 func (t Or_ServerCapabilities_colorProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case DocumentColorOptions: @@ -1003,7 +1003,7 @@ func (t *Or_ServerCapabilities_colorProvider) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [DocumentColorOptions DocumentColorRegistrationOptions bool]") } -// from line 7917 +// from line 7896 func (t Or_ServerCapabilities_declarationProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case DeclarationOptions: @@ -1041,7 +1041,7 @@ func (t *Or_ServerCapabilities_declarationProvider) UnmarshalJSON(x []byte) erro return errors.New("unmarshal failed to match one of [DeclarationOptions DeclarationRegistrationOptions bool]") } -// from line 7939 +// from line 7918 func (t Or_ServerCapabilities_definitionProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case DefinitionOptions: @@ -1072,7 +1072,7 @@ func (t *Or_ServerCapabilities_definitionProvider) UnmarshalJSON(x []byte) error return errors.New("unmarshal failed to match one of [DefinitionOptions bool]") } -// from line 8404 +// from line 8383 func (t Or_ServerCapabilities_diagnosticProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case DiagnosticOptions: @@ -1103,7 +1103,7 @@ func (t *Or_ServerCapabilities_diagnosticProvider) UnmarshalJSON(x []byte) error return errors.New("unmarshal failed to match one of [DiagnosticOptions DiagnosticRegistrationOptions]") } -// from line 8131 +// from line 8110 func (t Or_ServerCapabilities_documentFormattingProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case DocumentFormattingOptions: @@ -1134,7 +1134,7 @@ func (t *Or_ServerCapabilities_documentFormattingProvider) UnmarshalJSON(x []byt return errors.New("unmarshal failed to match one of [DocumentFormattingOptions bool]") } -// from line 8019 +// from line 7998 func (t Or_ServerCapabilities_documentHighlightProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case DocumentHighlightOptions: @@ -1165,7 +1165,7 @@ func (t *Or_ServerCapabilities_documentHighlightProvider) UnmarshalJSON(x []byte return errors.New("unmarshal failed to match one of [DocumentHighlightOptions bool]") } -// from line 8149 +// from line 8128 func (t Or_ServerCapabilities_documentRangeFormattingProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case DocumentRangeFormattingOptions: @@ -1196,7 +1196,7 @@ func (t *Or_ServerCapabilities_documentRangeFormattingProvider) UnmarshalJSON(x return errors.New("unmarshal failed to match one of [DocumentRangeFormattingOptions bool]") } -// from line 8037 +// from line 8016 func (t Or_ServerCapabilities_documentSymbolProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case DocumentSymbolOptions: @@ -1227,7 +1227,7 @@ func (t *Or_ServerCapabilities_documentSymbolProvider) UnmarshalJSON(x []byte) e return errors.New("unmarshal failed to match one of [DocumentSymbolOptions bool]") } -// from line 8194 +// from line 8173 func (t Or_ServerCapabilities_foldingRangeProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case FoldingRangeOptions: @@ -1265,7 +1265,7 @@ func (t *Or_ServerCapabilities_foldingRangeProvider) UnmarshalJSON(x []byte) err return errors.New("unmarshal failed to match one of [FoldingRangeOptions FoldingRangeRegistrationOptions bool]") } -// from line 7890 +// from line 7869 func (t Or_ServerCapabilities_hoverProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case HoverOptions: @@ -1296,7 +1296,7 @@ func (t *Or_ServerCapabilities_hoverProvider) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [HoverOptions bool]") } -// from line 7979 +// from line 7958 func (t Or_ServerCapabilities_implementationProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case ImplementationOptions: @@ -1334,7 +1334,7 @@ func (t *Or_ServerCapabilities_implementationProvider) UnmarshalJSON(x []byte) e return errors.New("unmarshal failed to match one of [ImplementationOptions ImplementationRegistrationOptions bool]") } -// from line 8381 +// from line 8360 func (t Or_ServerCapabilities_inlayHintProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case InlayHintOptions: @@ -1372,7 +1372,7 @@ func (t *Or_ServerCapabilities_inlayHintProvider) UnmarshalJSON(x []byte) error return errors.New("unmarshal failed to match one of [InlayHintOptions InlayHintRegistrationOptions bool]") } -// from line 8358 +// from line 8337 func (t Or_ServerCapabilities_inlineValueProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case InlineValueOptions: @@ -1410,7 +1410,7 @@ func (t *Or_ServerCapabilities_inlineValueProvider) UnmarshalJSON(x []byte) erro return errors.New("unmarshal failed to match one of [InlineValueOptions InlineValueRegistrationOptions bool]") } -// from line 8270 +// from line 8249 func (t Or_ServerCapabilities_linkedEditingRangeProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case LinkedEditingRangeOptions: @@ -1448,7 +1448,7 @@ func (t *Or_ServerCapabilities_linkedEditingRangeProvider) UnmarshalJSON(x []byt return errors.New("unmarshal failed to match one of [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool]") } -// from line 8312 +// from line 8291 func (t Or_ServerCapabilities_monikerProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case MonikerOptions: @@ -1486,7 +1486,7 @@ func (t *Or_ServerCapabilities_monikerProvider) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [MonikerOptions MonikerRegistrationOptions bool]") } -// from line 7862 +// from line 7841 func (t Or_ServerCapabilities_notebookDocumentSync) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case NotebookDocumentSyncOptions: @@ -1517,7 +1517,7 @@ func (t *Or_ServerCapabilities_notebookDocumentSync) UnmarshalJSON(x []byte) err return errors.New("unmarshal failed to match one of [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions]") } -// from line 8001 +// from line 7980 func (t Or_ServerCapabilities_referencesProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case ReferenceOptions: @@ -1548,7 +1548,7 @@ func (t *Or_ServerCapabilities_referencesProvider) UnmarshalJSON(x []byte) error return errors.New("unmarshal failed to match one of [ReferenceOptions bool]") } -// from line 8176 +// from line 8155 func (t Or_ServerCapabilities_renameProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case RenameOptions: @@ -1579,7 +1579,7 @@ func (t *Or_ServerCapabilities_renameProvider) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [RenameOptions bool]") } -// from line 8216 +// from line 8195 func (t Or_ServerCapabilities_selectionRangeProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case SelectionRangeOptions: @@ -1617,7 +1617,7 @@ func (t *Or_ServerCapabilities_selectionRangeProvider) UnmarshalJSON(x []byte) e return errors.New("unmarshal failed to match one of [SelectionRangeOptions SelectionRangeRegistrationOptions bool]") } -// from line 8293 +// from line 8272 func (t Or_ServerCapabilities_semanticTokensProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case SemanticTokensOptions: @@ -1648,7 +1648,7 @@ func (t *Or_ServerCapabilities_semanticTokensProvider) UnmarshalJSON(x []byte) e return errors.New("unmarshal failed to match one of [SemanticTokensOptions SemanticTokensRegistrationOptions]") } -// from line 7844 +// from line 7823 func (t Or_ServerCapabilities_textDocumentSync) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case TextDocumentSyncKind: @@ -1679,7 +1679,7 @@ func (t *Or_ServerCapabilities_textDocumentSync) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [TextDocumentSyncKind TextDocumentSyncOptions]") } -// from line 7957 +// from line 7936 func (t Or_ServerCapabilities_typeDefinitionProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case TypeDefinitionOptions: @@ -1717,7 +1717,7 @@ func (t *Or_ServerCapabilities_typeDefinitionProvider) UnmarshalJSON(x []byte) e return errors.New("unmarshal failed to match one of [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool]") } -// from line 8335 +// from line 8314 func (t Or_ServerCapabilities_typeHierarchyProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case TypeHierarchyOptions: @@ -1755,7 +1755,7 @@ func (t *Or_ServerCapabilities_typeHierarchyProvider) UnmarshalJSON(x []byte) er return errors.New("unmarshal failed to match one of [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool]") } -// from line 8113 +// from line 8092 func (t Or_ServerCapabilities_workspaceSymbolProvider) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case WorkspaceSymbolOptions: @@ -1786,7 +1786,7 @@ func (t *Or_ServerCapabilities_workspaceSymbolProvider) UnmarshalJSON(x []byte) return errors.New("unmarshal failed to match one of [WorkspaceSymbolOptions bool]") } -// from line 8861 +// from line 8840 func (t Or_SignatureInformation_documentation) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case MarkupContent: @@ -1817,7 +1817,7 @@ func (t *Or_SignatureInformation_documentation) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [MarkupContent string]") } -// from line 6697 +// from line 6691 func (t Or_TextDocumentEdit_edits_Elem) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case AnnotatedTextEdit: @@ -1848,7 +1848,7 @@ func (t *Or_TextDocumentEdit_edits_Elem) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [AnnotatedTextEdit TextEdit]") } -// from line 9803 +// from line 9776 func (t Or_TextDocumentSyncOptions_save) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case SaveOptions: @@ -1879,7 +1879,7 @@ func (t *Or_TextDocumentSyncOptions_save) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [SaveOptions bool]") } -// from line 14012 +// from line 13985 func (t Or_WorkspaceDocumentDiagnosticReport) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case WorkspaceFullDocumentDiagnosticReport: @@ -1910,7 +1910,7 @@ func (t *Or_WorkspaceDocumentDiagnosticReport) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]") } -// from line 3241 +// from line 3218 func (t Or_WorkspaceEdit_documentChanges_Elem) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case CreateFile: @@ -1955,7 +1955,7 @@ func (t *Or_WorkspaceEdit_documentChanges_Elem) UnmarshalJSON(x []byte) error { return errors.New("unmarshal failed to match one of [CreateFile DeleteFile RenameFile TextDocumentEdit]") } -// from line 256 +// from line 247 func (t Or_textDocument_declaration) MarshalJSON() ([]byte, error) { switch x := t.Value.(type) { case Declaration: diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go index 54a878c3d27..e1170fe940c 100644 --- a/gopls/internal/lsp/protocol/tsprotocol.go +++ b/gopls/internal/lsp/protocol/tsprotocol.go @@ -7,29 +7,23 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23) +// git hash 9b742021fb04ad081aa3676a9eecf4fa612084b4 (as of 2023-01-30) import "encoding/json" -// created for And -type And_Param_workspace_configuration struct { // line 141 - ParamConfiguration - PartialResultParams -} - /* * A special text edit with an additional change annotation. * * @since 3.16.0. */ -type AnnotatedTextEdit struct { // line 9392 +type AnnotatedTextEdit struct { // line 9371 // The actual identifier of the change annotation AnnotationID ChangeAnnotationIdentifier `json:"annotationId"` TextEdit } // The parameters passed via a apply workspace edit request. -type ApplyWorkspaceEditParams struct { // line 6003 +type ApplyWorkspaceEditParams struct { // line 5983 /* * An optional label of the workspace edit. This label is * presented in the user interface for example on an undo @@ -45,7 +39,7 @@ type ApplyWorkspaceEditParams struct { // line 6003 * * @since 3.17 renamed from ApplyWorkspaceEditResponse */ -type ApplyWorkspaceEditResult struct { // line 6026 +type ApplyWorkspaceEditResult struct { // line 6006 // Indicates whether the edit was applied or not. Applied bool `json:"applied"` /* @@ -63,7 +57,7 @@ type ApplyWorkspaceEditResult struct { // line 6026 } // A base for all symbol information. -type BaseSymbolInformation struct { // line 8986 +type BaseSymbolInformation struct { // line 8965 // The name of this symbol. Name string `json:"name"` // The kind of this symbol. @@ -84,7 +78,7 @@ type BaseSymbolInformation struct { // line 8986 } // @since 3.16.0 -type CallHierarchyClientCapabilities struct { // line 12167 +type CallHierarchyClientCapabilities struct { // line 12140 /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` @@ -98,12 +92,12 @@ type CallHierarchyClientCapabilities struct { // line 12167 * * @since 3.16.0 */ -type CallHierarchyIncomingCall struct { // line 2801 +type CallHierarchyIncomingCall struct { // line 2778 // The item that makes the call. From CallHierarchyItem `json:"from"` /* * The ranges at which the calls appear. This is relative to the caller - * denoted by [`this.from`](#CallHierarchyIncomingCall.from). + * denoted by {@link CallHierarchyIncomingCall.from `this.from`}. */ FromRanges []Range `json:"fromRanges"` } @@ -113,7 +107,7 @@ type CallHierarchyIncomingCall struct { // line 2801 * * @since 3.16.0 */ -type CallHierarchyIncomingCallsParams struct { // line 2777 +type CallHierarchyIncomingCallsParams struct { // line 2754 Item CallHierarchyItem `json:"item"` WorkDoneProgressParams PartialResultParams @@ -125,7 +119,7 @@ type CallHierarchyIncomingCallsParams struct { // line 2777 * * @since 3.16.0 */ -type CallHierarchyItem struct { // line 2678 +type CallHierarchyItem struct { // line 2655 // The name of this item. Name string `json:"name"` // The kind of this item. @@ -140,7 +134,7 @@ type CallHierarchyItem struct { // line 2678 Range Range `json:"range"` /* * The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. - * Must be contained by the [`range`](#CallHierarchyItem.range). + * Must be contained by the {@link CallHierarchyItem.range `range`}. */ SelectionRange Range `json:"selectionRange"` /* @@ -155,7 +149,7 @@ type CallHierarchyItem struct { // line 2678 * * @since 3.16.0 */ -type CallHierarchyOptions struct { // line 6539 +type CallHierarchyOptions struct { // line 6533 WorkDoneProgressOptions } @@ -164,13 +158,13 @@ type CallHierarchyOptions struct { // line 6539 * * @since 3.16.0 */ -type CallHierarchyOutgoingCall struct { // line 2851 +type CallHierarchyOutgoingCall struct { // line 2828 // The item that is called. To CallHierarchyItem `json:"to"` /* * The range at which this item is called. This is the range relative to the caller, e.g the item - * passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls) - * and not [`this.to`](#CallHierarchyOutgoingCall.to). + * passed to {@link CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls `provideCallHierarchyOutgoingCalls`} + * and not {@link CallHierarchyOutgoingCall.to `this.to`}. */ FromRanges []Range `json:"fromRanges"` } @@ -180,7 +174,7 @@ type CallHierarchyOutgoingCall struct { // line 2851 * * @since 3.16.0 */ -type CallHierarchyOutgoingCallsParams struct { // line 2827 +type CallHierarchyOutgoingCallsParams struct { // line 2804 Item CallHierarchyItem `json:"item"` WorkDoneProgressParams PartialResultParams @@ -191,7 +185,7 @@ type CallHierarchyOutgoingCallsParams struct { // line 2827 * * @since 3.16.0 */ -type CallHierarchyPrepareParams struct { // line 2660 +type CallHierarchyPrepareParams struct { // line 2637 TextDocumentPositionParams WorkDoneProgressParams } @@ -201,12 +195,12 @@ type CallHierarchyPrepareParams struct { // line 2660 * * @since 3.16.0 */ -type CallHierarchyRegistrationOptions struct { // line 2755 +type CallHierarchyRegistrationOptions struct { // line 2732 TextDocumentRegistrationOptions CallHierarchyOptions StaticRegistrationOptions } -type CancelParams struct { // line 6198 +type CancelParams struct { // line 6178 // The request id to cancel. ID interface{} `json:"id"` } @@ -216,7 +210,7 @@ type CancelParams struct { // line 6198 * * @since 3.16.0 */ -type ChangeAnnotation struct { // line 6836 +type ChangeAnnotation struct { // line 6830 /* * A human-readable string describing the actual change. The string * is rendered prominent in the user interface. @@ -235,9 +229,9 @@ type ChangeAnnotation struct { // line 6836 } // An identifier to refer to a change annotation stored with a workspace edit. -type ChangeAnnotationIdentifier = string // (alias) line 14002 +type ChangeAnnotationIdentifier = string // (alias) line 13975 // Defines the capabilities provided by the client. -type ClientCapabilities struct { // line 9700 +type ClientCapabilities struct { // line 9673 // Workspace specific client capabilities. Workspace WorkspaceClientCapabilities `json:"workspace,omitempty"` // Text document specific client capabilities. @@ -266,7 +260,7 @@ type ClientCapabilities struct { // line 9700 * * A CodeAction must set either `edit` and/or a `command`. If both are supplied, the `edit` is applied first, then the `command` is executed. */ -type CodeAction struct { // line 5401 +type CodeAction struct { // line 5381 // A short, human-readable, title for this code action. Title string `json:"title"` /* @@ -322,8 +316,8 @@ type CodeAction struct { // line 5401 Data interface{} `json:"data,omitempty"` } -// The Client Capabilities of a [CodeActionRequest](#CodeActionRequest). -type CodeActionClientCapabilities struct { // line 11747 +// The Client Capabilities of a {@link CodeActionRequest}. +type CodeActionClientCapabilities struct { // line 11720 // Whether code action supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` /* @@ -375,9 +369,9 @@ type CodeActionClientCapabilities struct { // line 11747 /* * Contains additional diagnostic information about the context in which - * a [code action](#CodeActionProvider.provideCodeActions) is run. + * a {@link CodeActionProvider.provideCodeActions code action} is run. */ -type CodeActionContext struct { // line 9052 +type CodeActionContext struct { // line 9031 /* * An array of diagnostics known on the client side overlapping the range provided to the * `textDocument/codeAction` request. They are provided so that the server knows which @@ -402,9 +396,9 @@ type CodeActionContext struct { // line 9052 } // A set of predefined code action kinds -type CodeActionKind string // line 13352 -// Provider options for a [CodeActionRequest](#CodeActionRequest). -type CodeActionOptions struct { // line 9091 +type CodeActionKind string // line 13325 +// Provider options for a {@link CodeActionRequest}. +type CodeActionOptions struct { // line 9070 /* * CodeActionKinds that this server may return. * @@ -422,8 +416,8 @@ type CodeActionOptions struct { // line 9091 WorkDoneProgressOptions } -// The parameters of a [CodeActionRequest](#CodeActionRequest). -type CodeActionParams struct { // line 5327 +// The parameters of a {@link CodeActionRequest}. +type CodeActionParams struct { // line 5307 // The document in which the command was invoked. TextDocument TextDocumentIdentifier `json:"textDocument"` // The range for which the command was invoked. @@ -434,8 +428,8 @@ type CodeActionParams struct { // line 5327 PartialResultParams } -// Registration options for a [CodeActionRequest](#CodeActionRequest). -type CodeActionRegistrationOptions struct { // line 5495 +// Registration options for a {@link CodeActionRequest}. +type CodeActionRegistrationOptions struct { // line 5475 TextDocumentRegistrationOptions CodeActionOptions } @@ -445,66 +439,66 @@ type CodeActionRegistrationOptions struct { // line 5495 * * @since 3.17.0 */ -type CodeActionTriggerKind uint32 // line 13632 +type CodeActionTriggerKind uint32 // line 13605 /* * Structure to capture a description for an error code. * * @since 3.16.0 */ -type CodeDescription struct { // line 10052 +type CodeDescription struct { // line 10025 // An URI to open with more information about the diagnostic error. Href URI `json:"href"` } /* - * A code lens represents a [command](#Command) that should be shown along with + * A code lens represents a {@link Command command} that should be shown along with * source text, like the number of references, a way to run tests, etc. * * A code lens is _unresolved_ when no command is associated to it. For performance * reasons the creation of a code lens and resolving should be done in two stages. */ -type CodeLens struct { // line 5618 +type CodeLens struct { // line 5598 // The range in which this code lens is valid. Should only span a single line. Range Range `json:"range"` // The command this code lens represents. Command Command `json:"command,omitempty"` /* * A data entry field that is preserved on a code lens item between - * a [CodeLensRequest](#CodeLensRequest) and a [CodeLensResolveRequest] + * a {@link CodeLensRequest} and a [CodeLensResolveRequest] * (#CodeLensResolveRequest) */ Data interface{} `json:"data,omitempty"` } -// The client capabilities of a [CodeLensRequest](#CodeLensRequest). -type CodeLensClientCapabilities struct { // line 11861 +// The client capabilities of a {@link CodeLensRequest}. +type CodeLensClientCapabilities struct { // line 11834 // Whether code lens supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -// Code Lens provider options of a [CodeLensRequest](#CodeLensRequest). -type CodeLensOptions struct { // line 9147 +// Code Lens provider options of a {@link CodeLensRequest}. +type CodeLensOptions struct { // line 9126 // Code lens has a resolve provider as well. ResolveProvider bool `json:"resolveProvider,omitempty"` WorkDoneProgressOptions } -// The parameters of a [CodeLensRequest](#CodeLensRequest). -type CodeLensParams struct { // line 5594 +// The parameters of a {@link CodeLensRequest}. +type CodeLensParams struct { // line 5574 // The document to request code lens for. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } -// Registration options for a [CodeLensRequest](#CodeLensRequest). -type CodeLensRegistrationOptions struct { // line 5650 +// Registration options for a {@link CodeLensRequest}. +type CodeLensRegistrationOptions struct { // line 5630 TextDocumentRegistrationOptions CodeLensOptions } // @since 3.16.0 -type CodeLensWorkspaceClientCapabilities struct { // line 11019 +type CodeLensWorkspaceClientCapabilities struct { // line 10992 /* * Whether the client implementation supports a refresh request sent from the * server to the client. @@ -518,7 +512,7 @@ type CodeLensWorkspaceClientCapabilities struct { // line 11019 } // Represents a color in RGBA space. -type Color struct { // line 6438 +type Color struct { // line 6432 // The red component of this color in the range [0-1]. Red float64 `json:"red"` // The green component of this color in the range [0-1]. @@ -530,13 +524,13 @@ type Color struct { // line 6438 } // Represents a color range from a document. -type ColorInformation struct { // line 2261 +type ColorInformation struct { // line 2238 // The range in the document where this color appears. Range Range `json:"range"` // The actual color value for this color range. Color Color `json:"color"` } -type ColorPresentation struct { // line 2343 +type ColorPresentation struct { // line 2320 /* * The label of this color presentation. It will be shown on the color * picker header. By default this is also the text that is inserted when selecting @@ -544,20 +538,20 @@ type ColorPresentation struct { // line 2343 */ Label string `json:"label"` /* - * An [edit](#TextEdit) which is applied to a document when selecting - * this presentation for the color. When `falsy` the [label](#ColorPresentation.label) + * An {@link TextEdit edit} which is applied to a document when selecting + * this presentation for the color. When `falsy` the {@link ColorPresentation.label label} * is used. */ TextEdit *TextEdit `json:"textEdit,omitempty"` /* - * An optional array of additional [text edits](#TextEdit) that are applied when - * selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves. + * An optional array of additional {@link TextEdit text edits} that are applied when + * selecting this color presentation. Edits must not overlap with the main {@link ColorPresentation.textEdit edit} nor with themselves. */ AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` } -// Parameters for a [ColorPresentationRequest](#ColorPresentationRequest). -type ColorPresentationParams struct { // line 2303 +// Parameters for a {@link ColorPresentationRequest}. +type ColorPresentationParams struct { // line 2280 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` // The color to request presentations for. @@ -574,7 +568,7 @@ type ColorPresentationParams struct { // line 2303 * an array of arguments which will be passed to the command handler * function when invoked. */ -type Command struct { // line 5367 +type Command struct { // line 5347 // Title of the command, like `save`. Title string `json:"title"` // The identifier of the actual command handler. @@ -587,7 +581,7 @@ type Command struct { // line 5367 } // Completion client capabilities -type CompletionClientCapabilities struct { // line 11194 +type CompletionClientCapabilities struct { // line 11167 // Whether completion supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` /* @@ -619,7 +613,7 @@ type CompletionClientCapabilities struct { // line 11194 } // Contains additional information about the context in which a completion request is triggered. -type CompletionContext struct { // line 8648 +type CompletionContext struct { // line 8627 // How the completion was triggered. TriggerKind CompletionTriggerKind `json:"triggerKind"` /* @@ -633,7 +627,7 @@ type CompletionContext struct { // line 8648 * A completion item represents a text snippet that is * proposed to complete text that is being typed. */ -type CompletionItem struct { // line 4550 +type CompletionItem struct { // line 4527 /* * The label of this completion item. * @@ -683,19 +677,19 @@ type CompletionItem struct { // line 4550 Preselect bool `json:"preselect,omitempty"` /* * A string that should be used when comparing this item - * with other items. When `falsy` the [label](#CompletionItem.label) + * with other items. When `falsy` the {@link CompletionItem.label label} * is used. */ SortText string `json:"sortText,omitempty"` /* * A string that should be used when filtering a set of - * completion items. When `falsy` the [label](#CompletionItem.label) + * completion items. When `falsy` the {@link CompletionItem.label label} * is used. */ FilterText string `json:"filterText,omitempty"` /* * A string that should be inserted into a document when selecting - * this completion. When `falsy` the [label](#CompletionItem.label) + * this completion. When `falsy` the {@link CompletionItem.label label} * is used. * * The `insertText` is subject to interpretation by the client side. @@ -725,9 +719,9 @@ type CompletionItem struct { // line 4550 */ InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` /* - * An [edit](#TextEdit) which is applied to a document when selecting + * An {@link TextEdit edit} which is applied to a document when selecting * this completion. When an edit is provided the value of - * [insertText](#CompletionItem.insertText) is ignored. + * {@link CompletionItem.insertText insertText} is ignored. * * Most editors support two different operations when accepting a completion * item. One is to insert a completion text and the other is to replace an @@ -761,9 +755,9 @@ type CompletionItem struct { // line 4550 */ TextEditText string `json:"textEditText,omitempty"` /* - * An optional array of additional [text edits](#TextEdit) that are applied when + * An optional array of additional {@link TextEdit text edits} that are applied when * selecting this completion. Edits must not overlap (including the same insert position) - * with the main [edit](#CompletionItem.textEdit) nor with themselves. + * with the main {@link CompletionItem.textEdit edit} nor with themselves. * * Additional text edits should be used to change text unrelated to the current cursor position * (for example adding an import statement at the top of the file if the completion item will @@ -777,26 +771,26 @@ type CompletionItem struct { // line 4550 */ CommitCharacters []string `json:"commitCharacters,omitempty"` /* - * An optional [command](#Command) that is executed *after* inserting this completion. *Note* that + * An optional {@link Command command} that is executed *after* inserting this completion. *Note* that * additional modifications to the current document should be described with the - * [additionalTextEdits](#CompletionItem.additionalTextEdits)-property. + * {@link CompletionItem.additionalTextEdits additionalTextEdits}-property. */ Command *Command `json:"command,omitempty"` /* * A data entry field that is preserved on a completion item between a - * [CompletionRequest](#CompletionRequest) and a [CompletionResolveRequest](#CompletionResolveRequest). + * {@link CompletionRequest} and a {@link CompletionResolveRequest}. */ Data interface{} `json:"data,omitempty"` } // The kind of a completion entry. -type CompletionItemKind uint32 // line 13160 +type CompletionItemKind uint32 // line 13133 /* * Additional details for a completion item label. * * @since 3.17.0 */ -type CompletionItemLabelDetails struct { // line 8671 +type CompletionItemLabelDetails struct { // line 8650 /* * An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, * without any spacing. Should be used for function signatures and type annotations. @@ -815,12 +809,12 @@ type CompletionItemLabelDetails struct { // line 8671 * * @since 3.15.0 */ -type CompletionItemTag uint32 // line 13270 +type CompletionItemTag uint32 // line 13243 /* - * Represents a collection of [completion items](#CompletionItem) to be presented + * Represents a collection of {@link CompletionItem completion items} to be presented * in the editor. */ -type CompletionList struct { // line 4758 +type CompletionList struct { // line 4736 /* * This list it not complete. Further typing results in recomputing this list. * @@ -849,7 +843,7 @@ type CompletionList struct { // line 4758 } // Completion options. -type CompletionOptions struct { // line 8727 +type CompletionOptions struct { // line 8706 /* * Most tools trigger completion request automatically without explicitly requesting * it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user @@ -888,7 +882,7 @@ type CompletionOptions struct { // line 8727 } // Completion parameters -type CompletionParams struct { // line 4519 +type CompletionParams struct { // line 4496 /* * The completion context. This is only available it the client specifies * to send this using the client capability `textDocument.completion.contextSupport === true` @@ -899,15 +893,15 @@ type CompletionParams struct { // line 4519 PartialResultParams } -// Registration options for a [CompletionRequest](#CompletionRequest). -type CompletionRegistrationOptions struct { // line 4875 +// Registration options for a {@link CompletionRequest}. +type CompletionRegistrationOptions struct { // line 4853 TextDocumentRegistrationOptions CompletionOptions } // How a completion was triggered -type CompletionTriggerKind uint32 // line 13581 -type ConfigurationItem struct { // line 6401 +type CompletionTriggerKind uint32 // line 13554 +type ConfigurationItem struct { // line 6395 // The scope to get the configuration section for. ScopeURI string `json:"scopeUri,omitempty"` // The configuration section asked for. @@ -915,12 +909,12 @@ type ConfigurationItem struct { // line 6401 } // The parameters of a configuration request. -type ConfigurationParams struct { // line 2207 +type ConfigurationParams struct { // line 2198 Items []ConfigurationItem `json:"items"` } // Create file operation. -type CreateFile struct { // line 6717 +type CreateFile struct { // line 6711 // A create Kind string `json:"kind"` // The resource to create. @@ -931,7 +925,7 @@ type CreateFile struct { // line 6717 } // Options to create a file. -type CreateFileOptions struct { // line 9437 +type CreateFileOptions struct { // line 9416 // Overwrite existing file. Overwrite wins over `ignoreIfExists` Overwrite bool `json:"overwrite,omitempty"` // Ignore if exists. @@ -944,15 +938,15 @@ type CreateFileOptions struct { // line 9437 * * @since 3.16.0 */ -type CreateFilesParams struct { // line 3197 +type CreateFilesParams struct { // line 3174 // An array of all files/folders created in this operation. Files []FileCreate `json:"files"` } -// The declaration of a symbol representation as one or many [locations](#Location). -type Declaration = []Location // (alias) line 13859 +// The declaration of a symbol representation as one or many {@link Location locations}. +type Declaration = []Location // (alias) line 13832 // @since 3.14.0 -type DeclarationClientCapabilities struct { // line 11535 +type DeclarationClientCapabilities struct { // line 11508 /* * Whether declaration supports dynamic registration. If this is set to `true` * the client supports the new `DeclarationRegistrationOptions` return value @@ -966,38 +960,38 @@ type DeclarationClientCapabilities struct { // line 11535 /* * Information about where a symbol is declared. * - * Provides additional metadata over normal [location](#Location) declarations, including the range of + * Provides additional metadata over normal {@link Location location} declarations, including the range of * the declaring symbol. * * Servers should prefer returning `DeclarationLink` over `Declaration` if supported * by the client. */ -type DeclarationLink = LocationLink // (alias) line 13879 -type DeclarationOptions struct { // line 6496 +type DeclarationLink = LocationLink // (alias) line 13852 +type DeclarationOptions struct { // line 6490 WorkDoneProgressOptions } -type DeclarationParams struct { // line 2516 +type DeclarationParams struct { // line 2493 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -type DeclarationRegistrationOptions struct { // line 2536 +type DeclarationRegistrationOptions struct { // line 2513 DeclarationOptions TextDocumentRegistrationOptions StaticRegistrationOptions } /* - * The definition of a symbol represented as one or many [locations](#Location). + * The definition of a symbol represented as one or many {@link Location locations}. * For most programming languages there is only one location at which a symbol is * defined. * * Servers should prefer returning `DefinitionLink` over `Definition` if supported * by the client. */ -type Definition = Or_Definition // (alias) line 13777 -// Client Capabilities for a [DefinitionRequest](#DefinitionRequest). -type DefinitionClientCapabilities struct { // line 11560 +type Definition = Or_Definition // (alias) line 13750 +// Client Capabilities for a {@link DefinitionRequest}. +type DefinitionClientCapabilities struct { // line 11533 // Whether definition supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` /* @@ -1011,30 +1005,30 @@ type DefinitionClientCapabilities struct { // line 11560 /* * Information about where a symbol is defined. * - * Provides additional metadata over normal [location](#Location) definitions, including the range of + * Provides additional metadata over normal {@link Location location} definitions, including the range of * the defining symbol */ -type DefinitionLink = LocationLink // (alias) line 13797 -// Server Capabilities for a [DefinitionRequest](#DefinitionRequest). -type DefinitionOptions struct { // line 8939 +type DefinitionLink = LocationLink // (alias) line 13770 +// Server Capabilities for a {@link DefinitionRequest}. +type DefinitionOptions struct { // line 8918 WorkDoneProgressOptions } -// Parameters for a [DefinitionRequest](#DefinitionRequest). -type DefinitionParams struct { // line 5039 +// Parameters for a {@link DefinitionRequest}. +type DefinitionParams struct { // line 5017 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -// Registration options for a [DefinitionRequest](#DefinitionRequest). -type DefinitionRegistrationOptions struct { // line 5060 +// Registration options for a {@link DefinitionRequest}. +type DefinitionRegistrationOptions struct { // line 5038 TextDocumentRegistrationOptions DefinitionOptions } // Delete file operation -type DeleteFile struct { // line 6799 +type DeleteFile struct { // line 6793 // A delete Kind string `json:"kind"` // The file to delete. @@ -1045,7 +1039,7 @@ type DeleteFile struct { // line 6799 } // Delete file options -type DeleteFileOptions struct { // line 9485 +type DeleteFileOptions struct { // line 9464 // Delete the content recursively if a folder is denoted. Recursive bool `json:"recursive,omitempty"` // Ignore the operation if the file doesn't exist. @@ -1058,7 +1052,7 @@ type DeleteFileOptions struct { // line 9485 * * @since 3.16.0 */ -type DeleteFilesParams struct { // line 3322 +type DeleteFilesParams struct { // line 3299 // An array of all files/folders deleted in this operation. Files []FileDelete `json:"files"` } @@ -1067,7 +1061,7 @@ type DeleteFilesParams struct { // line 3322 * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects * are only valid in the scope of a resource. */ -type Diagnostic struct { // line 8545 +type Diagnostic struct { // line 8524 // The range at which the message applies Range Range `json:"range"` /* @@ -1117,7 +1111,7 @@ type Diagnostic struct { // line 8545 * * @since 3.17.0 */ -type DiagnosticClientCapabilities struct { // line 12434 +type DiagnosticClientCapabilities struct { // line 12407 /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` @@ -1133,7 +1127,7 @@ type DiagnosticClientCapabilities struct { // line 12434 * * @since 3.17.0 */ -type DiagnosticOptions struct { // line 7298 +type DiagnosticOptions struct { // line 7292 /* * An optional identifier under which the diagnostics are * managed by the client. @@ -1156,7 +1150,7 @@ type DiagnosticOptions struct { // line 7298 * * @since 3.17.0 */ -type DiagnosticRegistrationOptions struct { // line 3877 +type DiagnosticRegistrationOptions struct { // line 3854 TextDocumentRegistrationOptions DiagnosticOptions StaticRegistrationOptions @@ -1167,7 +1161,7 @@ type DiagnosticRegistrationOptions struct { // line 3877 * used to point to code locations that cause or related to a diagnostics, e.g when duplicating * a symbol in a scope. */ -type DiagnosticRelatedInformation struct { // line 10067 +type DiagnosticRelatedInformation struct { // line 10040 // The location of this related diagnostic information. Location Location `json:"location"` // The message of this related diagnostic information. @@ -1179,24 +1173,24 @@ type DiagnosticRelatedInformation struct { // line 10067 * * @since 3.17.0 */ -type DiagnosticServerCancellationData struct { // line 3863 +type DiagnosticServerCancellationData struct { // line 3840 RetriggerRequest bool `json:"retriggerRequest"` } // The diagnostic's severity. -type DiagnosticSeverity uint32 // line 13530 +type DiagnosticSeverity uint32 // line 13503 /* * The diagnostic tags. * * @since 3.15.0 */ -type DiagnosticTag uint32 // line 13560 +type DiagnosticTag uint32 // line 13533 /* * Workspace client capabilities specific to diagnostic pull requests. * * @since 3.17.0 */ -type DiagnosticWorkspaceClientCapabilities struct { // line 11137 +type DiagnosticWorkspaceClientCapabilities struct { // line 11110 /* * Whether the client implementation supports a refresh request sent from * the server to the client. @@ -1208,17 +1202,17 @@ type DiagnosticWorkspaceClientCapabilities struct { // line 11137 */ RefreshSupport bool `json:"refreshSupport,omitempty"` } -type DidChangeConfigurationClientCapabilities struct { // line 10863 +type DidChangeConfigurationClientCapabilities struct { // line 10836 // Did change configuration notification supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } // The parameters of a change configuration notification. -type DidChangeConfigurationParams struct { // line 4166 +type DidChangeConfigurationParams struct { // line 4143 // The actual changed settings Settings interface{} `json:"settings"` } -type DidChangeConfigurationRegistrationOptions struct { // line 4180 +type DidChangeConfigurationRegistrationOptions struct { // line 4157 Section *OrPSection_workspace_didChangeConfiguration `json:"section,omitempty"` } @@ -1227,7 +1221,7 @@ type DidChangeConfigurationRegistrationOptions struct { // line 4180 * * @since 3.17.0 */ -type DidChangeNotebookDocumentParams struct { // line 3996 +type DidChangeNotebookDocumentParams struct { // line 3973 /* * The notebook document that did change. The version number points * to the version after all provided changes have been applied. If @@ -1254,7 +1248,7 @@ type DidChangeNotebookDocumentParams struct { // line 3996 } // The change text document notification's parameters. -type DidChangeTextDocumentParams struct { // line 4309 +type DidChangeTextDocumentParams struct { // line 4286 /* * The document that did change. The version number points * to the version after all provided content changes have @@ -1276,7 +1270,7 @@ type DidChangeTextDocumentParams struct { // line 4309 */ ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` } -type DidChangeWatchedFilesClientCapabilities struct { // line 10877 +type DidChangeWatchedFilesClientCapabilities struct { // line 10850 /* * Did change watched files notification supports dynamic registration. Please note * that the current protocol doesn't support static configuration for file changes @@ -1293,19 +1287,19 @@ type DidChangeWatchedFilesClientCapabilities struct { // line 10877 } // The watched files change notification's parameters. -type DidChangeWatchedFilesParams struct { // line 4450 +type DidChangeWatchedFilesParams struct { // line 4427 // The actual file events. Changes []FileEvent `json:"changes"` } // Describe options to be used when registered for text document change events. -type DidChangeWatchedFilesRegistrationOptions struct { // line 4467 +type DidChangeWatchedFilesRegistrationOptions struct { // line 4444 // The watchers to register. Watchers []FileSystemWatcher `json:"watchers"` } // The parameters of a `workspace/didChangeWorkspaceFolders` notification. -type DidChangeWorkspaceFoldersParams struct { // line 2193 +type DidChangeWorkspaceFoldersParams struct { // line 2184 // The actual workspace folder change event. Event WorkspaceFoldersChangeEvent `json:"event"` } @@ -1315,7 +1309,7 @@ type DidChangeWorkspaceFoldersParams struct { // line 2193 * * @since 3.17.0 */ -type DidCloseNotebookDocumentParams struct { // line 4034 +type DidCloseNotebookDocumentParams struct { // line 4011 // The notebook document that got closed. NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` /* @@ -1326,7 +1320,7 @@ type DidCloseNotebookDocumentParams struct { // line 4034 } // The parameters sent in a close text document notification -type DidCloseTextDocumentParams struct { // line 4354 +type DidCloseTextDocumentParams struct { // line 4331 // The document that was closed. TextDocument TextDocumentIdentifier `json:"textDocument"` } @@ -1336,7 +1330,7 @@ type DidCloseTextDocumentParams struct { // line 4354 * * @since 3.17.0 */ -type DidOpenNotebookDocumentParams struct { // line 3970 +type DidOpenNotebookDocumentParams struct { // line 3947 // The notebook document that got opened. NotebookDocument NotebookDocument `json:"notebookDocument"` /* @@ -1347,7 +1341,7 @@ type DidOpenNotebookDocumentParams struct { // line 3970 } // The parameters sent in an open text document notification -type DidOpenTextDocumentParams struct { // line 4295 +type DidOpenTextDocumentParams struct { // line 4272 // The document that was opened. TextDocument TextDocumentItem `json:"textDocument"` } @@ -1357,13 +1351,13 @@ type DidOpenTextDocumentParams struct { // line 4295 * * @since 3.17.0 */ -type DidSaveNotebookDocumentParams struct { // line 4019 +type DidSaveNotebookDocumentParams struct { // line 3996 // The notebook document that got saved. NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` } // The parameters sent in a save text document notification -type DidSaveTextDocumentParams struct { // line 4368 +type DidSaveTextDocumentParams struct { // line 4345 // The document that was saved. TextDocument TextDocumentIdentifier `json:"textDocument"` /* @@ -1372,7 +1366,7 @@ type DidSaveTextDocumentParams struct { // line 4368 */ Text *string `json:"text,omitempty"` } -type DocumentColorClientCapabilities struct { // line 11901 +type DocumentColorClientCapabilities struct { // line 11874 /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `DocumentColorRegistrationOptions` return value @@ -1380,18 +1374,18 @@ type DocumentColorClientCapabilities struct { // line 11901 */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -type DocumentColorOptions struct { // line 6476 +type DocumentColorOptions struct { // line 6470 WorkDoneProgressOptions } -// Parameters for a [DocumentColorRequest](#DocumentColorRequest). -type DocumentColorParams struct { // line 2237 +// Parameters for a {@link DocumentColorRequest}. +type DocumentColorParams struct { // line 2214 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } -type DocumentColorRegistrationOptions struct { // line 2283 +type DocumentColorRegistrationOptions struct { // line 2260 TextDocumentRegistrationOptions DocumentColorOptions StaticRegistrationOptions @@ -1402,7 +1396,7 @@ type DocumentColorRegistrationOptions struct { // line 2283 * * @since 3.17.0 */ -type DocumentDiagnosticParams struct { // line 3790 +type DocumentDiagnosticParams struct { // line 3767 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` // The additional identifier provided during registration. @@ -1418,13 +1412,13 @@ type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) line 1390 * * @since 3.17.0 */ -type DocumentDiagnosticReportKind string // line 12748 +type DocumentDiagnosticReportKind string // line 12721 /* * A partial result for a document diagnostic report. * * @since 3.17.0 */ -type DocumentDiagnosticReportPartialResult struct { // line 3833 +type DocumentDiagnosticReportPartialResult struct { // line 3810 RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments"` } @@ -1434,20 +1428,20 @@ type DocumentDiagnosticReportPartialResult struct { // line 3833 * * @since 3.17.0 - proposed support for NotebookCellTextDocumentFilter. */ -type DocumentFilter = Or_DocumentFilter // (alias) line 14118 -// Client capabilities of a [DocumentFormattingRequest](#DocumentFormattingRequest). -type DocumentFormattingClientCapabilities struct { // line 11915 +type DocumentFilter = Or_DocumentFilter // (alias) line 14092 +// Client capabilities of a {@link DocumentFormattingRequest}. +type DocumentFormattingClientCapabilities struct { // line 11888 // Whether formatting supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -// Provider options for a [DocumentFormattingRequest](#DocumentFormattingRequest). -type DocumentFormattingOptions struct { // line 9241 +// Provider options for a {@link DocumentFormattingRequest}. +type DocumentFormattingOptions struct { // line 9220 WorkDoneProgressOptions } -// The parameters of a [DocumentFormattingRequest](#DocumentFormattingRequest). -type DocumentFormattingParams struct { // line 5746 +// The parameters of a {@link DocumentFormattingRequest}. +type DocumentFormattingParams struct { // line 5726 // The document to format. TextDocument TextDocumentIdentifier `json:"textDocument"` // The format options. @@ -1455,8 +1449,8 @@ type DocumentFormattingParams struct { // line 5746 WorkDoneProgressParams } -// Registration options for a [DocumentFormattingRequest](#DocumentFormattingRequest). -type DocumentFormattingRegistrationOptions struct { // line 5774 +// Registration options for a {@link DocumentFormattingRequest}. +type DocumentFormattingRegistrationOptions struct { // line 5754 TextDocumentRegistrationOptions DocumentFormattingOptions } @@ -1466,35 +1460,35 @@ type DocumentFormattingRegistrationOptions struct { // line 5774 * special attention. Usually a document highlight is visualized by changing * the background color of its range. */ -type DocumentHighlight struct { // line 5140 +type DocumentHighlight struct { // line 5118 // The range this highlight applies to. Range Range `json:"range"` - // The highlight kind, default is [text](#DocumentHighlightKind.Text). + // The highlight kind, default is {@link DocumentHighlightKind.Text text}. Kind DocumentHighlightKind `json:"kind,omitempty"` } -// Client Capabilities for a [DocumentHighlightRequest](#DocumentHighlightRequest). -type DocumentHighlightClientCapabilities struct { // line 11650 +// Client Capabilities for a {@link DocumentHighlightRequest}. +type DocumentHighlightClientCapabilities struct { // line 11623 // Whether document highlight supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } // A document highlight kind. -type DocumentHighlightKind uint32 // line 13327 -// Provider options for a [DocumentHighlightRequest](#DocumentHighlightRequest). -type DocumentHighlightOptions struct { // line 8975 +type DocumentHighlightKind uint32 // line 13300 +// Provider options for a {@link DocumentHighlightRequest}. +type DocumentHighlightOptions struct { // line 8954 WorkDoneProgressOptions } -// Parameters for a [DocumentHighlightRequest](#DocumentHighlightRequest). -type DocumentHighlightParams struct { // line 5119 +// Parameters for a {@link DocumentHighlightRequest}. +type DocumentHighlightParams struct { // line 5097 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -// Registration options for a [DocumentHighlightRequest](#DocumentHighlightRequest). -type DocumentHighlightRegistrationOptions struct { // line 5163 +// Registration options for a {@link DocumentHighlightRequest}. +type DocumentHighlightRegistrationOptions struct { // line 5141 TextDocumentRegistrationOptions DocumentHighlightOptions } @@ -1503,7 +1497,7 @@ type DocumentHighlightRegistrationOptions struct { // line 5163 * A document link is a range in a text document that links to an internal or external resource, like another * text document or a web site. */ -type DocumentLink struct { // line 5689 +type DocumentLink struct { // line 5669 // The range this link applies to. Range Range `json:"range"` // The uri this link points to. If missing a resolve request is sent later. @@ -1525,8 +1519,8 @@ type DocumentLink struct { // line 5689 Data interface{} `json:"data,omitempty"` } -// The client capabilities of a [DocumentLinkRequest](#DocumentLinkRequest). -type DocumentLinkClientCapabilities struct { // line 11876 +// The client capabilities of a {@link DocumentLinkRequest}. +type DocumentLinkClientCapabilities struct { // line 11849 // Whether document link supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` /* @@ -1537,43 +1531,43 @@ type DocumentLinkClientCapabilities struct { // line 11876 TooltipSupport bool `json:"tooltipSupport,omitempty"` } -// Provider options for a [DocumentLinkRequest](#DocumentLinkRequest). -type DocumentLinkOptions struct { // line 9168 +// Provider options for a {@link DocumentLinkRequest}. +type DocumentLinkOptions struct { // line 9147 // Document links have a resolve provider as well. ResolveProvider bool `json:"resolveProvider,omitempty"` WorkDoneProgressOptions } -// The parameters of a [DocumentLinkRequest](#DocumentLinkRequest). -type DocumentLinkParams struct { // line 5665 +// The parameters of a {@link DocumentLinkRequest}. +type DocumentLinkParams struct { // line 5645 // The document to provide document links for. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } -// Registration options for a [DocumentLinkRequest](#DocumentLinkRequest). -type DocumentLinkRegistrationOptions struct { // line 5731 +// Registration options for a {@link DocumentLinkRequest}. +type DocumentLinkRegistrationOptions struct { // line 5711 TextDocumentRegistrationOptions DocumentLinkOptions } -// Client capabilities of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). -type DocumentOnTypeFormattingClientCapabilities struct { // line 11945 +// Client capabilities of a {@link DocumentOnTypeFormattingRequest}. +type DocumentOnTypeFormattingClientCapabilities struct { // line 11918 // Whether on type formatting supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -// Provider options for a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). -type DocumentOnTypeFormattingOptions struct { // line 9263 +// Provider options for a {@link DocumentOnTypeFormattingRequest}. +type DocumentOnTypeFormattingOptions struct { // line 9242 // A character on which formatting should be triggered, like `{`. FirstTriggerCharacter string `json:"firstTriggerCharacter"` // More trigger characters. MoreTriggerCharacter []string `json:"moreTriggerCharacter,omitempty"` } -// The parameters of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). -type DocumentOnTypeFormattingParams struct { // line 5840 +// The parameters of a {@link DocumentOnTypeFormattingRequest}. +type DocumentOnTypeFormattingParams struct { // line 5820 // The document to format. TextDocument TextDocumentIdentifier `json:"textDocument"` /* @@ -1593,25 +1587,25 @@ type DocumentOnTypeFormattingParams struct { // line 5840 Options FormattingOptions `json:"options"` } -// Registration options for a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). -type DocumentOnTypeFormattingRegistrationOptions struct { // line 5878 +// Registration options for a {@link DocumentOnTypeFormattingRequest}. +type DocumentOnTypeFormattingRegistrationOptions struct { // line 5858 TextDocumentRegistrationOptions DocumentOnTypeFormattingOptions } -// Client capabilities of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). -type DocumentRangeFormattingClientCapabilities struct { // line 11930 +// Client capabilities of a {@link DocumentRangeFormattingRequest}. +type DocumentRangeFormattingClientCapabilities struct { // line 11903 // Whether range formatting supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -// Provider options for a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). -type DocumentRangeFormattingOptions struct { // line 9252 +// Provider options for a {@link DocumentRangeFormattingRequest}. +type DocumentRangeFormattingOptions struct { // line 9231 WorkDoneProgressOptions } -// The parameters of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). -type DocumentRangeFormattingParams struct { // line 5789 +// The parameters of a {@link DocumentRangeFormattingRequest}. +type DocumentRangeFormattingParams struct { // line 5769 // The document to format. TextDocument TextDocumentIdentifier `json:"textDocument"` // The range to format @@ -1621,8 +1615,8 @@ type DocumentRangeFormattingParams struct { // line 5789 WorkDoneProgressParams } -// Registration options for a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). -type DocumentRangeFormattingRegistrationOptions struct { // line 5825 +// Registration options for a {@link DocumentRangeFormattingRequest}. +type DocumentRangeFormattingRegistrationOptions struct { // line 5805 TextDocumentRegistrationOptions DocumentRangeFormattingOptions } @@ -1634,14 +1628,14 @@ type DocumentRangeFormattingRegistrationOptions struct { // line 5825 * * The use of a string as a document filter is deprecated @since 3.16.0. */ -type DocumentSelector = []DocumentFilter // (alias) line 13990 +type DocumentSelector = []DocumentFilter // (alias) line 13947 /* * Represents programming constructs like variables, classes, interfaces etc. * that appear in a document. Document symbols can be hierarchical and they * have two ranges: one that encloses its definition and one that points to * its most interesting range, e.g. the range of an identifier. */ -type DocumentSymbol struct { // line 5231 +type DocumentSymbol struct { // line 5210 /* * The name of this symbol. Will be displayed in the user interface and therefore must not be * an empty string or a string only consisting of white spaces. @@ -1678,8 +1672,8 @@ type DocumentSymbol struct { // line 5231 Children []DocumentSymbol `json:"children,omitempty"` } -// Client Capabilities for a [DocumentSymbolRequest](#DocumentSymbolRequest). -type DocumentSymbolClientCapabilities struct { // line 11665 +// Client Capabilities for a {@link DocumentSymbolRequest}. +type DocumentSymbolClientCapabilities struct { // line 11638 // Whether document symbol supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` /* @@ -1706,8 +1700,8 @@ type DocumentSymbolClientCapabilities struct { // line 11665 LabelSupport bool `json:"labelSupport,omitempty"` } -// Provider options for a [DocumentSymbolRequest](#DocumentSymbolRequest). -type DocumentSymbolOptions struct { // line 9030 +// Provider options for a {@link DocumentSymbolRequest}. +type DocumentSymbolOptions struct { // line 9009 /* * A human-readable string that is shown when multiple outlines trees * are shown for the same document. @@ -1718,38 +1712,38 @@ type DocumentSymbolOptions struct { // line 9030 WorkDoneProgressOptions } -// Parameters for a [DocumentSymbolRequest](#DocumentSymbolRequest). -type DocumentSymbolParams struct { // line 5178 +// Parameters for a {@link DocumentSymbolRequest}. +type DocumentSymbolParams struct { // line 5156 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } -// Registration options for a [DocumentSymbolRequest](#DocumentSymbolRequest). -type DocumentSymbolRegistrationOptions struct { // line 5312 +// Registration options for a {@link DocumentSymbolRequest}. +type DocumentSymbolRegistrationOptions struct { // line 5292 TextDocumentRegistrationOptions DocumentSymbolOptions } type DocumentURI string // Predefined error codes. -type ErrorCodes int32 // line 12769 -// The client capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). -type ExecuteCommandClientCapabilities struct { // line 10988 +type ErrorCodes int32 // line 12742 +// The client capabilities of a {@link ExecuteCommandRequest}. +type ExecuteCommandClientCapabilities struct { // line 10961 // Execute command supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -// The server capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). -type ExecuteCommandOptions struct { // line 9311 +// The server capabilities of a {@link ExecuteCommandRequest}. +type ExecuteCommandOptions struct { // line 9290 // The commands to be executed on the server Commands []string `json:"commands"` WorkDoneProgressOptions } -// The parameters of a [ExecuteCommandRequest](#ExecuteCommandRequest). -type ExecuteCommandParams struct { // line 5960 +// The parameters of a {@link ExecuteCommandRequest}. +type ExecuteCommandParams struct { // line 5940 // The identifier of the actual command handler. Command string `json:"command"` // Arguments that the command should be invoked with. @@ -1757,11 +1751,11 @@ type ExecuteCommandParams struct { // line 5960 WorkDoneProgressParams } -// Registration options for a [ExecuteCommandRequest](#ExecuteCommandRequest). -type ExecuteCommandRegistrationOptions struct { // line 5992 +// Registration options for a {@link ExecuteCommandRequest}. +type ExecuteCommandRegistrationOptions struct { // line 5972 ExecuteCommandOptions } -type ExecutionSummary struct { // line 10188 +type ExecutionSummary struct { // line 10161 /* * A strict monotonically increasing value * indicating the execution order of a cell @@ -1776,12 +1770,12 @@ type ExecutionSummary struct { // line 10188 } // created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0_cells_Elem) -type FCellsPNotebookSelector struct { // line 9857 +type FCellsPNotebookSelector struct { // line 9830 Language string `json:"language"` } // created for Literal (Lit_CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind) -type FCodeActionKindPCodeActionLiteralSupport struct { // line 11768 +type FCodeActionKindPCodeActionLiteralSupport struct { // line 11741 /* * The code action kind values the client supports. When this * property exists the client also guarantees that it will @@ -1792,13 +1786,13 @@ type FCodeActionKindPCodeActionLiteralSupport struct { // line 11768 } // created for Literal (Lit_CompletionList_itemDefaults_editRange_Item1) -type FEditRangePItemDefaults struct { // line 4798 +type FEditRangePItemDefaults struct { // line 4776 Insert Range `json:"insert"` Replace Range `json:"replace"` } // created for Literal (Lit_SemanticTokensClientCapabilities_requests_full_Item1) -type FFullPRequests struct { // line 12231 +type FFullPRequests struct { // line 12204 /* * The client will send the `textDocument/semanticTokens/full/delta` request if * the server provides a corresponding handler. @@ -1807,12 +1801,12 @@ type FFullPRequests struct { // line 12231 } // created for Literal (Lit_CompletionClientCapabilities_completionItem_insertTextModeSupport) -type FInsertTextModeSupportPCompletionItem struct { // line 11321 +type FInsertTextModeSupportPCompletionItem struct { // line 11294 ValueSet []InsertTextMode `json:"valueSet"` } // created for Literal (Lit_SignatureHelpClientCapabilities_signatureInformation_parameterInformation) -type FParameterInformationPSignatureInformation struct { // line 11487 +type FParameterInformationPSignatureInformation struct { // line 11460 /* * The client supports processing label offsets instead of a * simple label string. @@ -1823,17 +1817,17 @@ type FParameterInformationPSignatureInformation struct { // line 11487 } // created for Literal (Lit_SemanticTokensClientCapabilities_requests_range_Item1) -type FRangePRequests struct { // line 12211 +type FRangePRequests struct { // line 12184 } // created for Literal (Lit_CompletionClientCapabilities_completionItem_resolveSupport) -type FResolveSupportPCompletionItem struct { // line 11297 +type FResolveSupportPCompletionItem struct { // line 11270 // The properties that a client can resolve lazily. Properties []string `json:"properties"` } // created for Literal (Lit_NotebookDocumentChangeEvent_cells_structure) -type FStructurePCells struct { // line 7492 +type FStructurePCells struct { // line 7486 // The change to the cell array. Array NotebookCellArrayChange `json:"array"` // Additional opened cell text documents. @@ -1843,25 +1837,25 @@ type FStructurePCells struct { // line 7492 } // created for Literal (Lit_CompletionClientCapabilities_completionItem_tagSupport) -type FTagSupportPCompletionItem struct { // line 11263 +type FTagSupportPCompletionItem struct { // line 11236 // The tags supported by the client. ValueSet []CompletionItemTag `json:"valueSet"` } // created for Literal (Lit_NotebookDocumentChangeEvent_cells_textContent_Elem) -type FTextContentPCells struct { // line 7550 +type FTextContentPCells struct { // line 7544 Document VersionedTextDocumentIdentifier `json:"document"` Changes []TextDocumentContentChangeEvent `json:"changes"` } -type FailureHandlingKind string // line 13719 +type FailureHandlingKind string // line 13692 // The file event type -type FileChangeType uint32 // line 13480 +type FileChangeType uint32 // line 13453 /* * Represents information on a file/folder create. * * @since 3.16.0 */ -type FileCreate struct { // line 6667 +type FileCreate struct { // line 6661 // A file:// URI for the location of the file/folder being created. URI string `json:"uri"` } @@ -1871,13 +1865,13 @@ type FileCreate struct { // line 6667 * * @since 3.16.0 */ -type FileDelete struct { // line 6916 +type FileDelete struct { // line 6910 // A file:// URI for the location of the file/folder being deleted. URI string `json:"uri"` } // An event describing a file change. -type FileEvent struct { // line 8500 +type FileEvent struct { // line 8479 // The file's uri. URI DocumentURI `json:"uri"` // The change type. @@ -1892,7 +1886,7 @@ type FileEvent struct { // line 8500 * * @since 3.16.0 */ -type FileOperationClientCapabilities struct { // line 11035 +type FileOperationClientCapabilities struct { // line 11008 // Whether the client supports dynamic registration for file requests/notifications. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` // The client has support for sending didCreateFiles notifications. @@ -1915,7 +1909,7 @@ type FileOperationClientCapabilities struct { // line 11035 * * @since 3.16.0 */ -type FileOperationFilter struct { // line 6869 +type FileOperationFilter struct { // line 6863 // A Uri scheme like `file` or `untitled`. Scheme string `json:"scheme,omitempty"` // The actual file operation pattern. @@ -1927,7 +1921,7 @@ type FileOperationFilter struct { // line 6869 * * @since 3.16.0 */ -type FileOperationOptions struct { // line 9991 +type FileOperationOptions struct { // line 9964 // The server is interested in receiving didCreateFiles notifications. DidCreate *FileOperationRegistrationOptions `json:"didCreate,omitempty"` // The server is interested in receiving willCreateFiles requests. @@ -1948,7 +1942,7 @@ type FileOperationOptions struct { // line 9991 * * @since 3.16.0 */ -type FileOperationPattern struct { // line 9509 +type FileOperationPattern struct { // line 9488 /* * The glob pattern to match. Glob patterns can have the following syntax: * - `*` to match one or more characters in a path segment @@ -1975,13 +1969,13 @@ type FileOperationPattern struct { // line 9509 * * @since 3.16.0 */ -type FileOperationPatternKind string // line 13653 +type FileOperationPatternKind string // line 13626 /* * Matching options for the file operation pattern. * * @since 3.16.0 */ -type FileOperationPatternOptions struct { // line 10172 +type FileOperationPatternOptions struct { // line 10145 // The pattern should be matched ignoring casing. IgnoreCase bool `json:"ignoreCase,omitempty"` } @@ -1991,7 +1985,7 @@ type FileOperationPatternOptions struct { // line 10172 * * @since 3.16.0 */ -type FileOperationRegistrationOptions struct { // line 3286 +type FileOperationRegistrationOptions struct { // line 3263 // The actual filters. Filters []FileOperationFilter `json:"filters"` } @@ -2001,13 +1995,13 @@ type FileOperationRegistrationOptions struct { // line 3286 * * @since 3.16.0 */ -type FileRename struct { // line 6893 +type FileRename struct { // line 6887 // A file:// URI for the original location of the file/folder being renamed. OldURI string `json:"oldUri"` // A file:// URI for the new location of the file/folder being renamed. NewURI string `json:"newUri"` } -type FileSystemWatcher struct { // line 8522 +type FileSystemWatcher struct { // line 8501 /* * The glob pattern to watch. See {@link GlobPattern glob pattern} for more detail. * @@ -2026,7 +2020,7 @@ type FileSystemWatcher struct { // line 8522 * Represents a folding range. To be valid, start and end line must be bigger than zero and smaller * than the number of lines in the document. Clients are free to ignore invalid ranges. */ -type FoldingRange struct { // line 2437 +type FoldingRange struct { // line 2414 /* * The zero-based start line of the range to fold. The folded area starts after the line's last character. * To be valid, the end must be zero or larger and smaller than the number of lines in the document. @@ -2044,7 +2038,7 @@ type FoldingRange struct { // line 2437 /* * Describes the kind of the folding range such as `comment' or 'region'. The kind * is used to categorize folding ranges and used by commands like 'Fold all comments'. - * See [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + * See {@link FoldingRangeKind} for an enumeration of standardized kinds. */ Kind string `json:"kind,omitempty"` /* @@ -2056,7 +2050,7 @@ type FoldingRange struct { // line 2437 */ CollapsedText string `json:"collapsedText,omitempty"` } -type FoldingRangeClientCapabilities struct { // line 12004 +type FoldingRangeClientCapabilities struct { // line 11977 /* * Whether implementation supports dynamic registration for folding range * providers. If this is set to `true` the client supports the new @@ -2091,26 +2085,26 @@ type FoldingRangeClientCapabilities struct { // line 12004 } // A set of predefined range kinds. -type FoldingRangeKind string // line 12841 -type FoldingRangeOptions struct { // line 6486 +type FoldingRangeKind string // line 12814 +type FoldingRangeOptions struct { // line 6480 WorkDoneProgressOptions } -// Parameters for a [FoldingRangeRequest](#FoldingRangeRequest). -type FoldingRangeParams struct { // line 2413 +// Parameters for a {@link FoldingRangeRequest}. +type FoldingRangeParams struct { // line 2390 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams PartialResultParams } -type FoldingRangeRegistrationOptions struct { // line 2496 +type FoldingRangeRegistrationOptions struct { // line 2473 TextDocumentRegistrationOptions FoldingRangeOptions StaticRegistrationOptions } // Value-object describing what options formatting should use. -type FormattingOptions struct { // line 9189 +type FormattingOptions struct { // line 9168 // Size of a tab in spaces. TabSize uint32 `json:"tabSize"` // Prefer spaces over tabs. @@ -2140,7 +2134,7 @@ type FormattingOptions struct { // line 9189 * * @since 3.17.0 */ -type FullDocumentDiagnosticReport struct { // line 7240 +type FullDocumentDiagnosticReport struct { // line 7234 // A full document diagnostic report. Kind string `json:"kind"` /* @@ -2158,7 +2152,7 @@ type FullDocumentDiagnosticReport struct { // line 7240 * * @since 3.16.0 */ -type GeneralClientCapabilities struct { // line 10690 +type GeneralClientCapabilities struct { // line 10663 /* * Client capability that signals how the client * handles stale requests (e.g. a request @@ -2208,9 +2202,9 @@ type GeneralClientCapabilities struct { // line 10690 * * @since 3.17.0 */ -type GlobPattern = string // (alias) line 14136 +type GlobPattern = string // (alias) line 14126 // The result of a hover request. -type Hover struct { // line 4907 +type Hover struct { // line 4885 // The hover's content Contents MarkupContent `json:"contents"` /* @@ -2219,7 +2213,7 @@ type Hover struct { // line 4907 */ Range Range `json:"range,omitempty"` } -type HoverClientCapabilities struct { // line 11428 +type HoverClientCapabilities struct { // line 11401 // Whether hover supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` /* @@ -2230,24 +2224,24 @@ type HoverClientCapabilities struct { // line 11428 } // Hover options. -type HoverOptions struct { // line 8796 +type HoverOptions struct { // line 8775 WorkDoneProgressOptions } -// Parameters for a [HoverRequest](#HoverRequest). -type HoverParams struct { // line 4890 +// Parameters for a {@link HoverRequest}. +type HoverParams struct { // line 4868 TextDocumentPositionParams WorkDoneProgressParams } -// Registration options for a [HoverRequest](#HoverRequest). -type HoverRegistrationOptions struct { // line 4946 +// Registration options for a {@link HoverRequest}. +type HoverRegistrationOptions struct { // line 4924 TextDocumentRegistrationOptions HoverOptions } // @since 3.6.0 -type ImplementationClientCapabilities struct { // line 11609 +type ImplementationClientCapabilities struct { // line 11582 /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `ImplementationRegistrationOptions` return value @@ -2261,15 +2255,15 @@ type ImplementationClientCapabilities struct { // line 11609 */ LinkSupport bool `json:"linkSupport,omitempty"` } -type ImplementationOptions struct { // line 6338 +type ImplementationOptions struct { // line 6332 WorkDoneProgressOptions } -type ImplementationParams struct { // line 2071 +type ImplementationParams struct { // line 2062 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -type ImplementationRegistrationOptions struct { // line 2111 +type ImplementationRegistrationOptions struct { // line 2102 TextDocumentRegistrationOptions ImplementationOptions StaticRegistrationOptions @@ -2279,7 +2273,7 @@ type ImplementationRegistrationOptions struct { // line 2111 * The data type of the ResponseError if the * initialize request fails. */ -type InitializeError struct { // line 4148 +type InitializeError struct { // line 4125 /* * Indicates whether the client execute the following retry logic: * (1) show the message provided by the ResponseError to the user @@ -2288,13 +2282,13 @@ type InitializeError struct { // line 4148 */ Retry bool `json:"retry"` } -type InitializeParams struct { // line 4090 +type InitializeParams struct { // line 4067 XInitializeParams WorkspaceFoldersInitializeParams } // The result returned from an initialize request. -type InitializeResult struct { // line 4104 +type InitializeResult struct { // line 4081 // The capabilities the language server provides. Capabilities ServerCapabilities `json:"capabilities"` /* @@ -2304,7 +2298,7 @@ type InitializeResult struct { // line 4104 */ ServerInfo PServerInfoMsg_initialize `json:"serverInfo,omitempty"` } -type InitializedParams struct { // line 4162 +type InitializedParams struct { // line 4139 } /* @@ -2312,7 +2306,7 @@ type InitializedParams struct { // line 4162 * * @since 3.17.0 */ -type InlayHint struct { // line 3667 +type InlayHint struct { // line 3644 // The position of this hint. Position *Position `json:"position"` /* @@ -2365,7 +2359,7 @@ type InlayHint struct { // line 3667 * * @since 3.17.0 */ -type InlayHintClientCapabilities struct { // line 12395 +type InlayHintClientCapabilities struct { // line 12368 // Whether inlay hints support dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` /* @@ -2380,14 +2374,14 @@ type InlayHintClientCapabilities struct { // line 12395 * * @since 3.17.0 */ -type InlayHintKind uint32 // line 13059 +type InlayHintKind uint32 // line 13032 /* * An inlay hint label part allows for interactive and composite labels * of inlay hints. * * @since 3.17.0 */ -type InlayHintLabelPart struct { // line 7067 +type InlayHintLabelPart struct { // line 7061 // The value of this label part. Value string `json:"value"` /* @@ -2424,7 +2418,7 @@ type InlayHintLabelPart struct { // line 7067 * * @since 3.17.0 */ -type InlayHintOptions struct { // line 7140 +type InlayHintOptions struct { // line 7134 /* * The server provides support to resolve additional * information for an inlay hint item. @@ -2438,7 +2432,7 @@ type InlayHintOptions struct { // line 7140 * * @since 3.17.0 */ -type InlayHintParams struct { // line 3638 +type InlayHintParams struct { // line 3615 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` // The document range for which inlay hints should be computed. @@ -2451,7 +2445,7 @@ type InlayHintParams struct { // line 3638 * * @since 3.17.0 */ -type InlayHintRegistrationOptions struct { // line 3768 +type InlayHintRegistrationOptions struct { // line 3745 InlayHintOptions TextDocumentRegistrationOptions StaticRegistrationOptions @@ -2462,7 +2456,7 @@ type InlayHintRegistrationOptions struct { // line 3768 * * @since 3.17.0 */ -type InlayHintWorkspaceClientCapabilities struct { // line 11121 +type InlayHintWorkspaceClientCapabilities struct { // line 11094 /* * Whether the client implementation supports a refresh request sent from * the server to the client. @@ -2484,19 +2478,19 @@ type InlayHintWorkspaceClientCapabilities struct { // line 11121 * * @since 3.17.0 */ -type InlineValue = Or_InlineValue // (alias) line 13887 +type InlineValue = Or_InlineValue // (alias) line 13860 /* * Client capabilities specific to inline values. * * @since 3.17.0 */ -type InlineValueClientCapabilities struct { // line 12379 +type InlineValueClientCapabilities struct { // line 12352 // Whether implementation supports dynamic registration for inline value providers. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } // @since 3.17.0 -type InlineValueContext struct { // line 6953 +type InlineValueContext struct { // line 6947 // The stack frame (as a DAP Id) where the execution has stopped. FrameID int32 `json:"frameId"` /* @@ -2513,7 +2507,7 @@ type InlineValueContext struct { // line 6953 * * @since 3.17.0 */ -type InlineValueEvaluatableExpression struct { // line 7031 +type InlineValueEvaluatableExpression struct { // line 7025 /* * The document range for which the inline value applies. * The range is used to extract the evaluatable expression from the underlying document. @@ -2528,7 +2522,7 @@ type InlineValueEvaluatableExpression struct { // line 7031 * * @since 3.17.0 */ -type InlineValueOptions struct { // line 7055 +type InlineValueOptions struct { // line 7049 WorkDoneProgressOptions } @@ -2537,7 +2531,7 @@ type InlineValueOptions struct { // line 7055 * * @since 3.17.0 */ -type InlineValueParams struct { // line 3579 +type InlineValueParams struct { // line 3556 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` // The document range for which inline values should be computed. @@ -2555,7 +2549,7 @@ type InlineValueParams struct { // line 3579 * * @since 3.17.0 */ -type InlineValueRegistrationOptions struct { // line 3616 +type InlineValueRegistrationOptions struct { // line 3593 InlineValueOptions TextDocumentRegistrationOptions StaticRegistrationOptions @@ -2566,7 +2560,7 @@ type InlineValueRegistrationOptions struct { // line 3616 * * @since 3.17.0 */ -type InlineValueText struct { // line 6976 +type InlineValueText struct { // line 6970 // The document range for which the inline value applies. Range Range `json:"range"` // The text of the inline value. @@ -2580,7 +2574,7 @@ type InlineValueText struct { // line 6976 * * @since 3.17.0 */ -type InlineValueVariableLookup struct { // line 6999 +type InlineValueVariableLookup struct { // line 6993 /* * The document range for which the inline value applies. * The range is used to extract the variable name from the underlying document. @@ -2597,7 +2591,7 @@ type InlineValueVariableLookup struct { // line 6999 * * @since 3.17.0 */ -type InlineValueWorkspaceClientCapabilities struct { // line 11105 +type InlineValueWorkspaceClientCapabilities struct { // line 11078 /* * Whether the client implementation supports a refresh request sent from the * server to the client. @@ -2615,7 +2609,7 @@ type InlineValueWorkspaceClientCapabilities struct { // line 11105 * * @since 3.16.0 */ -type InsertReplaceEdit struct { // line 8696 +type InsertReplaceEdit struct { // line 8675 // The string to be inserted. NewText string `json:"newText"` // The range if the insert is requested @@ -2628,35 +2622,33 @@ type InsertReplaceEdit struct { // line 8696 * Defines whether the insert text in a completion item should be interpreted as * plain text or a snippet. */ -type InsertTextFormat uint32 // line 13286 +type InsertTextFormat uint32 // line 13259 /* * How whitespace and indentation is handled during completion * item insertion. * * @since 3.16.0 */ -type InsertTextMode uint32 // line 13306 +type InsertTextMode uint32 // line 13279 type LSPAny = interface{} /* * LSP arrays. * @since 3.17.0 */ -type LSPArray = []interface{} // (alias) line 13805 -type LSPErrorCodes int32 // line 12809 +type LSPArray = []interface{} // (alias) line 13778 +type LSPErrorCodes int32 // line 12782 /* * LSP object definition. * @since 3.17.0 */ -type LSPObject struct { // line 9618 -} - +type LSPObject = map[string]LSPAny // (alias) line 14110 /* * Client capabilities for the linked editing range request. * * @since 3.16.0 */ -type LinkedEditingRangeClientCapabilities struct { // line 12331 +type LinkedEditingRangeClientCapabilities struct { // line 12304 /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` @@ -2664,14 +2656,14 @@ type LinkedEditingRangeClientCapabilities struct { // line 12331 */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -type LinkedEditingRangeOptions struct { // line 6657 +type LinkedEditingRangeOptions struct { // line 6651 WorkDoneProgressOptions } -type LinkedEditingRangeParams struct { // line 3134 +type LinkedEditingRangeParams struct { // line 3111 TextDocumentPositionParams WorkDoneProgressParams } -type LinkedEditingRangeRegistrationOptions struct { // line 3177 +type LinkedEditingRangeRegistrationOptions struct { // line 3154 TextDocumentRegistrationOptions LinkedEditingRangeOptions StaticRegistrationOptions @@ -2682,7 +2674,7 @@ type LinkedEditingRangeRegistrationOptions struct { // line 3177 * * @since 3.16.0 */ -type LinkedEditingRanges struct { // line 3150 +type LinkedEditingRanges struct { // line 3127 /* * A list of ranges that can be edited together. The ranges must have * identical length and contain identical text content. The ranges cannot overlap. @@ -2697,27 +2689,27 @@ type LinkedEditingRanges struct { // line 3150 } // created for Literal (Lit_NotebookDocumentFilter_Item1) -type Lit_NotebookDocumentFilter_Item1 struct { // line 14302 +type Lit_NotebookDocumentFilter_Item1 struct { // line 14292 // The type of the enclosing notebook. NotebookType string `json:"notebookType,omitempty"` - // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. Scheme string `json:"scheme"` // A glob pattern. Pattern string `json:"pattern,omitempty"` } // created for Literal (Lit_NotebookDocumentFilter_Item2) -type Lit_NotebookDocumentFilter_Item2 struct { // line 14335 +type Lit_NotebookDocumentFilter_Item2 struct { // line 14325 // The type of the enclosing notebook. NotebookType string `json:"notebookType,omitempty"` - // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. Scheme string `json:"scheme,omitempty"` // A glob pattern. Pattern string `json:"pattern"` } // created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1) -type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1 struct { // line 9878 +type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1 struct { // line 9851 /* * The notebook to be synced If a string * value is provided it matches against the @@ -2729,26 +2721,26 @@ type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1 struct { // lin } // created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_cells_Elem) -type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_cells_Elem struct { // line 9904 +type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_cells_Elem struct { // line 9877 Language string `json:"language"` } // created for Literal (Lit_PrepareRenameResult_Item2) -type Lit_PrepareRenameResult_Item2 struct { // line 13958 +type Lit_PrepareRenameResult_Item2 struct { // line 13931 DefaultBehavior bool `json:"defaultBehavior"` } // created for Literal (Lit_TextDocumentContentChangeEvent_Item1) -type Lit_TextDocumentContentChangeEvent_Item1 struct { // line 14066 +type Lit_TextDocumentContentChangeEvent_Item1 struct { // line 14039 // The new text of the whole document. Text string `json:"text"` } // created for Literal (Lit_TextDocumentFilter_Item2) -type Lit_TextDocumentFilter_Item2 struct { // line 14226 +type Lit_TextDocumentFilter_Item2 struct { // line 14216 // A language id, like `typescript`. Language string `json:"language,omitempty"` - // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. Scheme string `json:"scheme,omitempty"` // A glob pattern, like `*.{ts,js}`. Pattern string `json:"pattern"` @@ -2758,16 +2750,16 @@ type Lit_TextDocumentFilter_Item2 struct { // line 14226 * Represents a location inside a resource, such as a line * inside a text file. */ -type Location struct { // line 2091 +type Location struct { // line 2082 URI DocumentURI `json:"uri"` Range Range `json:"range"` } /* - * Represents the connection of two locations. Provides additional metadata over normal [locations](#Location), + * Represents the connection of two locations. Provides additional metadata over normal {@link Location locations}, * including an origin range. */ -type LocationLink struct { // line 6277 +type LocationLink struct { // line 6271 /* * Span of the origin of this link. * @@ -2791,13 +2783,13 @@ type LocationLink struct { // line 6277 } // The log message parameters. -type LogMessageParams struct { // line 4273 +type LogMessageParams struct { // line 4250 // The message type. See {@link MessageType} Type MessageType `json:"type"` // The actual message. Message string `json:"message"` } -type LogTraceParams struct { // line 6178 +type LogTraceParams struct { // line 6158 Message string `json:"message"` Verbose string `json:"verbose,omitempty"` } @@ -2807,7 +2799,7 @@ type LogTraceParams struct { // line 6178 * * @since 3.16.0 */ -type MarkdownClientCapabilities struct { // line 12550 +type MarkdownClientCapabilities struct { // line 12523 // The name of the parser. Parser string `json:"parser"` // The version of the parser. @@ -2835,7 +2827,7 @@ type MarkdownClientCapabilities struct { // line 12550 * Note that markdown strings will be sanitized - that means html will be escaped. * @deprecated use MarkupContent instead. */ -type MarkedString = Or_MarkedString // (alias) line 14084 +type MarkedString = Or_MarkedString // (alias) line 14057 /* * A `MarkupContent` literal represents a string value which content is interpreted base on its * kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. @@ -2860,7 +2852,7 @@ type MarkedString = Or_MarkedString // (alias) line 14084 * *Please Note* that clients might sanitize the return markdown. A client could decide to * remove HTML from the markdown to avoid script execution. */ -type MarkupContent struct { // line 7118 +type MarkupContent struct { // line 7112 // The type of the Markup Kind MarkupKind `json:"kind"` // The content itself @@ -2874,20 +2866,20 @@ type MarkupContent struct { // line 7118 * Please note that `MarkupKinds` must not start with a `$`. This kinds * are reserved for internal usage. */ -type MarkupKind string // line 13433 -type MessageActionItem struct { // line 4260 +type MarkupKind string // line 13406 +type MessageActionItem struct { // line 4237 // A short title like 'Retry', 'Open Log' etc. Title string `json:"title"` } // The message type -type MessageType uint32 // line 13080 +type MessageType uint32 // line 13053 /* * Moniker definition to match LSIF 0.5 moniker definition. * * @since 3.16.0 */ -type Moniker struct { // line 3360 +type Moniker struct { // line 3337 // The scheme of the moniker. For example tsc or .Net Scheme string `json:"scheme"` /* @@ -2906,7 +2898,7 @@ type Moniker struct { // line 3360 * * @since 3.16.0 */ -type MonikerClientCapabilities struct { // line 12347 +type MonikerClientCapabilities struct { // line 12320 /* * Whether moniker supports dynamic registration. If this is set to `true` * the client supports the new `MonikerRegistrationOptions` return value @@ -2920,44 +2912,44 @@ type MonikerClientCapabilities struct { // line 12347 * * @since 3.16.0 */ -type MonikerKind string // line 13033 -type MonikerOptions struct { // line 6931 +type MonikerKind string // line 13006 +type MonikerOptions struct { // line 6925 WorkDoneProgressOptions } -type MonikerParams struct { // line 3340 +type MonikerParams struct { // line 3317 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -type MonikerRegistrationOptions struct { // line 3400 +type MonikerRegistrationOptions struct { // line 3377 TextDocumentRegistrationOptions MonikerOptions } // created for Literal (Lit_MarkedString_Item1) -type Msg_MarkedString struct { // line 14094 +type Msg_MarkedString struct { // line 14067 Language string `json:"language"` Value string `json:"value"` } // created for Literal (Lit_NotebookDocumentFilter_Item0) -type Msg_NotebookDocumentFilter struct { // line 14269 +type Msg_NotebookDocumentFilter struct { // line 14259 // The type of the enclosing notebook. NotebookType string `json:"notebookType"` - // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. Scheme string `json:"scheme"` // A glob pattern. Pattern string `json:"pattern"` } // created for Literal (Lit_PrepareRenameResult_Item1) -type Msg_PrepareRename2Gn struct { // line 13937 +type Msg_PrepareRename2Gn struct { // line 13910 Range Range `json:"range"` Placeholder string `json:"placeholder"` } // created for Literal (Lit_TextDocumentContentChangeEvent_Item0) -type Msg_TextDocumentContentChangeEvent struct { // line 14034 +type Msg_TextDocumentContentChangeEvent struct { // line 14007 // The range of the document that changed. Range *Range `json:"range"` /* @@ -2971,17 +2963,17 @@ type Msg_TextDocumentContentChangeEvent struct { // line 14034 } // created for Literal (Lit_TextDocumentFilter_Item0) -type Msg_TextDocumentFilter struct { // line 14160 +type Msg_TextDocumentFilter struct { // line 14150 // A language id, like `typescript`. Language string `json:"language"` - // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. Scheme string `json:"scheme"` // A glob pattern, like `*.{ts,js}`. Pattern string `json:"pattern"` } // created for Literal (Lit__InitializeParams_clientInfo) -type Msg_XInitializeParams_clientInfo struct { // line 7678 +type Msg_XInitializeParams_clientInfo struct { // line 7672 // The name of the client as defined by the client. Name string `json:"name"` // The client's version as defined by the client. @@ -2997,7 +2989,7 @@ type Msg_XInitializeParams_clientInfo struct { // line 7678 * * @since 3.17.0 */ -type NotebookCell struct { // line 9624 +type NotebookCell struct { // line 9597 // The cell's kind Kind NotebookCellKind `json:"kind"` /* @@ -3024,7 +3016,7 @@ type NotebookCell struct { // line 9624 * * @since 3.17.0 */ -type NotebookCellArrayChange struct { // line 9665 +type NotebookCellArrayChange struct { // line 9638 // The start oftest of the cell that changed. Start uint32 `json:"start"` // The deleted cells @@ -3038,14 +3030,14 @@ type NotebookCellArrayChange struct { // line 9665 * * @since 3.17.0 */ -type NotebookCellKind uint32 // line 13674 +type NotebookCellKind uint32 // line 13647 /* * A notebook cell text document filter denotes a cell text * document by different properties. * * @since 3.17.0 */ -type NotebookCellTextDocumentFilter struct { // line 10139 +type NotebookCellTextDocumentFilter struct { // line 10112 /* * A filter that matches against the notebook * containing the notebook cell. If a string @@ -3067,7 +3059,7 @@ type NotebookCellTextDocumentFilter struct { // line 10139 * * @since 3.17.0 */ -type NotebookDocument struct { // line 7359 +type NotebookDocument struct { // line 7353 // The notebook document's uri. URI URI `json:"uri"` // The type of the notebook. @@ -3093,7 +3085,7 @@ type NotebookDocument struct { // line 7359 * * @since 3.17.0 */ -type NotebookDocumentChangeEvent struct { // line 7471 +type NotebookDocumentChangeEvent struct { // line 7465 /* * The changed meta data if any. * @@ -3109,7 +3101,7 @@ type NotebookDocumentChangeEvent struct { // line 7471 * * @since 3.17.0 */ -type NotebookDocumentClientCapabilities struct { // line 10639 +type NotebookDocumentClientCapabilities struct { // line 10612 /* * Capabilities specific to notebook document synchronization * @@ -3125,13 +3117,13 @@ type NotebookDocumentClientCapabilities struct { // line 10639 * * @since 3.17.0 */ -type NotebookDocumentFilter = Msg_NotebookDocumentFilter // (alias) line 14263 +type NotebookDocumentFilter = Msg_NotebookDocumentFilter // (alias) line 14253 /* * A literal to identify a notebook document in the client. * * @since 3.17.0 */ -type NotebookDocumentIdentifier struct { // line 7587 +type NotebookDocumentIdentifier struct { // line 7581 // The notebook document's uri. URI URI `json:"uri"` } @@ -3141,7 +3133,7 @@ type NotebookDocumentIdentifier struct { // line 7587 * * @since 3.17.0 */ -type NotebookDocumentSyncClientCapabilities struct { // line 12459 +type NotebookDocumentSyncClientCapabilities struct { // line 12432 /* * Whether implementation supports dynamic registration. If this is * set to `true` the client supports the new @@ -3168,7 +3160,7 @@ type NotebookDocumentSyncClientCapabilities struct { // line 12459 * * @since 3.17.0 */ -type NotebookDocumentSyncOptions struct { // line 9821 +type NotebookDocumentSyncOptions struct { // line 9794 // The notebooks to be synced NotebookSelector []PNotebookSelectorPNotebookDocumentSync `json:"notebookSelector"` /* @@ -3183,13 +3175,13 @@ type NotebookDocumentSyncOptions struct { // line 9821 * * @since 3.17.0 */ -type NotebookDocumentSyncRegistrationOptions struct { // line 9941 +type NotebookDocumentSyncRegistrationOptions struct { // line 9914 NotebookDocumentSyncOptions StaticRegistrationOptions } // A text document identifier to optionally denote a specific version of a text document. -type OptionalVersionedTextDocumentIdentifier struct { // line 9363 +type OptionalVersionedTextDocumentIdentifier struct { // line 9342 /* * The version number of this document. If a versioned text document identifier * is sent from the server to the client and the file is not open in the editor @@ -3202,307 +3194,307 @@ type OptionalVersionedTextDocumentIdentifier struct { // line 9363 } // created for Or [FEditRangePItemDefaults Range] -type OrFEditRangePItemDefaults struct { // line 4791 +type OrFEditRangePItemDefaults struct { // line 4769 Value interface{} `json:"value"` } // created for Or [NotebookDocumentFilter string] -type OrFNotebookPNotebookSelector struct { // line 9838 +type OrFNotebookPNotebookSelector struct { // line 9811 Value interface{} `json:"value"` } // created for Or [Location PLocationMsg_workspace_symbol] -type OrPLocation_workspace_symbol struct { // line 5540 +type OrPLocation_workspace_symbol struct { // line 5520 Value interface{} `json:"value"` } // created for Or [[]string string] -type OrPSection_workspace_didChangeConfiguration struct { // line 4186 +type OrPSection_workspace_didChangeConfiguration struct { // line 4163 Value interface{} `json:"value"` } // created for Or [MarkupContent string] -type OrPTooltipPLabel struct { // line 7081 +type OrPTooltipPLabel struct { // line 7075 Value interface{} `json:"value"` } // created for Or [MarkupContent string] -type OrPTooltip_textDocument_inlayHint struct { // line 3722 +type OrPTooltip_textDocument_inlayHint struct { // line 3699 Value interface{} `json:"value"` } // created for Or [int32 string] -type Or_CancelParams_id struct { // line 6204 +type Or_CancelParams_id struct { // line 6184 Value interface{} `json:"value"` } // created for Or [MarkupContent string] -type Or_CompletionItem_documentation struct { // line 4605 +type Or_CompletionItem_documentation struct { // line 4582 Value interface{} `json:"value"` } // created for Or [InsertReplaceEdit TextEdit] -type Or_CompletionItem_textEdit struct { // line 4687 +type Or_CompletionItem_textEdit struct { // line 4665 Value interface{} `json:"value"` } // created for Or [Location []Location] -type Or_Definition struct { // line 13780 +type Or_Definition struct { // line 13753 Value interface{} `json:"value"` } // created for Or [int32 string] -type Or_Diagnostic_code struct { // line 8568 +type Or_Diagnostic_code struct { // line 8547 Value interface{} `json:"value"` } // created for Or [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport] -type Or_DocumentDiagnosticReport struct { // line 13912 +type Or_DocumentDiagnosticReport struct { // line 13885 Value interface{} `json:"value"` } // created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] -type Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value struct { // line 3845 +type Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value struct { // line 3822 Value interface{} `json:"value"` } // created for Or [NotebookCellTextDocumentFilter TextDocumentFilter] -type Or_DocumentFilter struct { // line 14121 +type Or_DocumentFilter struct { // line 14095 Value interface{} `json:"value"` } // created for Or [MarkedString MarkupContent []MarkedString] -type Or_Hover_contents struct { // line 4913 +type Or_Hover_contents struct { // line 4891 Value interface{} `json:"value"` } // created for Or [[]InlayHintLabelPart string] -type Or_InlayHint_label struct { // line 3681 +type Or_InlayHint_label struct { // line 3658 Value interface{} `json:"value"` } // created for Or [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup] -type Or_InlineValue struct { // line 13890 +type Or_InlineValue struct { // line 13863 Value interface{} `json:"value"` } // created for Or [Msg_MarkedString string] -type Or_MarkedString struct { // line 14087 +type Or_MarkedString struct { // line 14060 Value interface{} `json:"value"` } // created for Or [NotebookDocumentFilter string] -type Or_NotebookCellTextDocumentFilter_notebook struct { // line 10145 +type Or_NotebookCellTextDocumentFilter_notebook struct { // line 10118 Value interface{} `json:"value"` } // created for Or [NotebookDocumentFilter string] -type Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook struct { // line 9884 +type Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook struct { // line 9857 Value interface{} `json:"value"` } // created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] -type Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value struct { // line 7174 +type Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value struct { // line 7168 Value interface{} `json:"value"` } // created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] -type Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value struct { // line 7213 +type Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value struct { // line 7207 Value interface{} `json:"value"` } // created for Or [URI WorkspaceFolder] -type Or_RelativePattern_baseUri struct { // line 10768 +type Or_RelativePattern_baseUri struct { // line 10741 Value interface{} `json:"value"` } // created for Or [CodeAction Command] -type Or_Result_textDocument_codeAction_Item0_Elem struct { // line 1380 +type Or_Result_textDocument_codeAction_Item0_Elem struct { // line 1371 Value interface{} `json:"value"` } // created for Or [FFullPRequests bool] -type Or_SemanticTokensClientCapabilities_requests_full struct { // line 12224 +type Or_SemanticTokensClientCapabilities_requests_full struct { // line 12197 Value interface{} `json:"value"` } // created for Or [FRangePRequests bool] -type Or_SemanticTokensClientCapabilities_requests_range struct { // line 12204 +type Or_SemanticTokensClientCapabilities_requests_range struct { // line 12177 Value interface{} `json:"value"` } // created for Or [PFullESemanticTokensOptions bool] -type Or_SemanticTokensOptions_full struct { // line 6585 +type Or_SemanticTokensOptions_full struct { // line 6579 Value interface{} `json:"value"` } // created for Or [PRangeESemanticTokensOptions bool] -type Or_SemanticTokensOptions_range struct { // line 6565 +type Or_SemanticTokensOptions_range struct { // line 6559 Value interface{} `json:"value"` } // created for Or [CallHierarchyOptions CallHierarchyRegistrationOptions bool] -type Or_ServerCapabilities_callHierarchyProvider struct { // line 8248 +type Or_ServerCapabilities_callHierarchyProvider struct { // line 8227 Value interface{} `json:"value"` } // created for Or [CodeActionOptions bool] -type Or_ServerCapabilities_codeActionProvider struct { // line 8056 +type Or_ServerCapabilities_codeActionProvider struct { // line 8035 Value interface{} `json:"value"` } // created for Or [DocumentColorOptions DocumentColorRegistrationOptions bool] -type Or_ServerCapabilities_colorProvider struct { // line 8092 +type Or_ServerCapabilities_colorProvider struct { // line 8071 Value interface{} `json:"value"` } // created for Or [DeclarationOptions DeclarationRegistrationOptions bool] -type Or_ServerCapabilities_declarationProvider struct { // line 7918 +type Or_ServerCapabilities_declarationProvider struct { // line 7897 Value interface{} `json:"value"` } // created for Or [DefinitionOptions bool] -type Or_ServerCapabilities_definitionProvider struct { // line 7940 +type Or_ServerCapabilities_definitionProvider struct { // line 7919 Value interface{} `json:"value"` } // created for Or [DiagnosticOptions DiagnosticRegistrationOptions] -type Or_ServerCapabilities_diagnosticProvider struct { // line 8405 +type Or_ServerCapabilities_diagnosticProvider struct { // line 8384 Value interface{} `json:"value"` } // created for Or [DocumentFormattingOptions bool] -type Or_ServerCapabilities_documentFormattingProvider struct { // line 8132 +type Or_ServerCapabilities_documentFormattingProvider struct { // line 8111 Value interface{} `json:"value"` } // created for Or [DocumentHighlightOptions bool] -type Or_ServerCapabilities_documentHighlightProvider struct { // line 8020 +type Or_ServerCapabilities_documentHighlightProvider struct { // line 7999 Value interface{} `json:"value"` } // created for Or [DocumentRangeFormattingOptions bool] -type Or_ServerCapabilities_documentRangeFormattingProvider struct { // line 8150 +type Or_ServerCapabilities_documentRangeFormattingProvider struct { // line 8129 Value interface{} `json:"value"` } // created for Or [DocumentSymbolOptions bool] -type Or_ServerCapabilities_documentSymbolProvider struct { // line 8038 +type Or_ServerCapabilities_documentSymbolProvider struct { // line 8017 Value interface{} `json:"value"` } // created for Or [FoldingRangeOptions FoldingRangeRegistrationOptions bool] -type Or_ServerCapabilities_foldingRangeProvider struct { // line 8195 +type Or_ServerCapabilities_foldingRangeProvider struct { // line 8174 Value interface{} `json:"value"` } // created for Or [HoverOptions bool] -type Or_ServerCapabilities_hoverProvider struct { // line 7891 +type Or_ServerCapabilities_hoverProvider struct { // line 7870 Value interface{} `json:"value"` } // created for Or [ImplementationOptions ImplementationRegistrationOptions bool] -type Or_ServerCapabilities_implementationProvider struct { // line 7980 +type Or_ServerCapabilities_implementationProvider struct { // line 7959 Value interface{} `json:"value"` } // created for Or [InlayHintOptions InlayHintRegistrationOptions bool] -type Or_ServerCapabilities_inlayHintProvider struct { // line 8382 +type Or_ServerCapabilities_inlayHintProvider struct { // line 8361 Value interface{} `json:"value"` } // created for Or [InlineValueOptions InlineValueRegistrationOptions bool] -type Or_ServerCapabilities_inlineValueProvider struct { // line 8359 +type Or_ServerCapabilities_inlineValueProvider struct { // line 8338 Value interface{} `json:"value"` } // created for Or [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool] -type Or_ServerCapabilities_linkedEditingRangeProvider struct { // line 8271 +type Or_ServerCapabilities_linkedEditingRangeProvider struct { // line 8250 Value interface{} `json:"value"` } // created for Or [MonikerOptions MonikerRegistrationOptions bool] -type Or_ServerCapabilities_monikerProvider struct { // line 8313 +type Or_ServerCapabilities_monikerProvider struct { // line 8292 Value interface{} `json:"value"` } // created for Or [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions] -type Or_ServerCapabilities_notebookDocumentSync struct { // line 7863 +type Or_ServerCapabilities_notebookDocumentSync struct { // line 7842 Value interface{} `json:"value"` } // created for Or [ReferenceOptions bool] -type Or_ServerCapabilities_referencesProvider struct { // line 8002 +type Or_ServerCapabilities_referencesProvider struct { // line 7981 Value interface{} `json:"value"` } // created for Or [RenameOptions bool] -type Or_ServerCapabilities_renameProvider struct { // line 8177 +type Or_ServerCapabilities_renameProvider struct { // line 8156 Value interface{} `json:"value"` } // created for Or [SelectionRangeOptions SelectionRangeRegistrationOptions bool] -type Or_ServerCapabilities_selectionRangeProvider struct { // line 8217 +type Or_ServerCapabilities_selectionRangeProvider struct { // line 8196 Value interface{} `json:"value"` } // created for Or [SemanticTokensOptions SemanticTokensRegistrationOptions] -type Or_ServerCapabilities_semanticTokensProvider struct { // line 8294 +type Or_ServerCapabilities_semanticTokensProvider struct { // line 8273 Value interface{} `json:"value"` } // created for Or [TextDocumentSyncKind TextDocumentSyncOptions] -type Or_ServerCapabilities_textDocumentSync struct { // line 7845 +type Or_ServerCapabilities_textDocumentSync struct { // line 7824 Value interface{} `json:"value"` } // created for Or [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool] -type Or_ServerCapabilities_typeDefinitionProvider struct { // line 7958 +type Or_ServerCapabilities_typeDefinitionProvider struct { // line 7937 Value interface{} `json:"value"` } // created for Or [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool] -type Or_ServerCapabilities_typeHierarchyProvider struct { // line 8336 +type Or_ServerCapabilities_typeHierarchyProvider struct { // line 8315 Value interface{} `json:"value"` } // created for Or [WorkspaceSymbolOptions bool] -type Or_ServerCapabilities_workspaceSymbolProvider struct { // line 8114 +type Or_ServerCapabilities_workspaceSymbolProvider struct { // line 8093 Value interface{} `json:"value"` } // created for Or [MarkupContent string] -type Or_SignatureInformation_documentation struct { // line 8862 +type Or_SignatureInformation_documentation struct { // line 8841 Value interface{} `json:"value"` } // created for Or [AnnotatedTextEdit TextEdit] -type Or_TextDocumentEdit_edits_Elem struct { // line 6698 +type Or_TextDocumentEdit_edits_Elem struct { // line 6692 Value interface{} `json:"value"` } // created for Or [SaveOptions bool] -type Or_TextDocumentSyncOptions_save struct { // line 9804 +type Or_TextDocumentSyncOptions_save struct { // line 9777 Value interface{} `json:"value"` } // created for Or [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport] -type Or_WorkspaceDocumentDiagnosticReport struct { // line 14013 +type Or_WorkspaceDocumentDiagnosticReport struct { // line 13986 Value interface{} `json:"value"` } // created for Or [CreateFile DeleteFile RenameFile TextDocumentEdit] -type Or_WorkspaceEdit_documentChanges_Elem struct { // line 3242 +type Or_WorkspaceEdit_documentChanges_Elem struct { // line 3219 Value interface{} `json:"value"` } // created for Or [Declaration []DeclarationLink] -type Or_textDocument_declaration struct { // line 257 +type Or_textDocument_declaration struct { // line 248 Value interface{} `json:"value"` } // created for Literal (Lit_NotebookDocumentChangeEvent_cells) -type PCellsPChange struct { // line 7486 +type PCellsPChange struct { // line 7480 /* * Changes to the cell structure to add or * remove cells. @@ -3518,7 +3510,7 @@ type PCellsPChange struct { // line 7486 } // created for Literal (Lit_WorkspaceEditClientCapabilities_changeAnnotationSupport) -type PChangeAnnotationSupportPWorkspaceEdit struct { // line 10842 +type PChangeAnnotationSupportPWorkspaceEdit struct { // line 10815 /* * Whether the client groups edits with equal labels into tree nodes, * for instance all edits labelled with "Changes in Strings" would @@ -3528,7 +3520,7 @@ type PChangeAnnotationSupportPWorkspaceEdit struct { // line 10842 } // created for Literal (Lit_CodeActionClientCapabilities_codeActionLiteralSupport) -type PCodeActionLiteralSupportPCodeAction struct { // line 11762 +type PCodeActionLiteralSupportPCodeAction struct { // line 11735 /* * The code action kind is support with the following value * set. @@ -3537,7 +3529,7 @@ type PCodeActionLiteralSupportPCodeAction struct { // line 11762 } // created for Literal (Lit_CompletionClientCapabilities_completionItemKind) -type PCompletionItemKindPCompletion struct { // line 11360 +type PCompletionItemKindPCompletion struct { // line 11333 /* * The completion item kind values the client supports. When this * property exists the client also guarantees that it will @@ -3552,7 +3544,7 @@ type PCompletionItemKindPCompletion struct { // line 11360 } // created for Literal (Lit_CompletionClientCapabilities_completionItem) -type PCompletionItemPCompletion struct { // line 11209 +type PCompletionItemPCompletion struct { // line 11182 /* * Client supports snippets as insert text. * @@ -3615,7 +3607,7 @@ type PCompletionItemPCompletion struct { // line 11209 } // created for Literal (Lit_CompletionOptions_completionItem) -type PCompletionItemPCompletionProvider struct { // line 8767 +type PCompletionItemPCompletionProvider struct { // line 8746 /* * The server has support for completion item label * details (see also `CompletionItemLabelDetails`) when @@ -3627,7 +3619,7 @@ type PCompletionItemPCompletionProvider struct { // line 8767 } // created for Literal (Lit_CompletionClientCapabilities_completionList) -type PCompletionListPCompletion struct { // line 11402 +type PCompletionListPCompletion struct { // line 11375 /* * The client supports the following itemDefaults on * a completion list. @@ -3642,7 +3634,7 @@ type PCompletionListPCompletion struct { // line 11402 } // created for Literal (Lit_CodeAction_disabled) -type PDisabledMsg_textDocument_codeAction struct { // line 5446 +type PDisabledMsg_textDocument_codeAction struct { // line 5426 /* * Human readable description of why the code action is currently disabled. * @@ -3652,7 +3644,7 @@ type PDisabledMsg_textDocument_codeAction struct { // line 5446 } // created for Literal (Lit_FoldingRangeClientCapabilities_foldingRangeKind) -type PFoldingRangeKindPFoldingRange struct { // line 12037 +type PFoldingRangeKindPFoldingRange struct { // line 12010 /* * The folding range kind values the client supports. When this * property exists the client also guarantees that it will @@ -3663,7 +3655,7 @@ type PFoldingRangeKindPFoldingRange struct { // line 12037 } // created for Literal (Lit_FoldingRangeClientCapabilities_foldingRange) -type PFoldingRangePFoldingRange struct { // line 12062 +type PFoldingRangePFoldingRange struct { // line 12035 /* * If set, the client signals that it supports setting collapsedText on * folding ranges to display custom labels instead of the default text. @@ -3674,13 +3666,13 @@ type PFoldingRangePFoldingRange struct { // line 12062 } // created for Literal (Lit_SemanticTokensOptions_full_Item1) -type PFullESemanticTokensOptions struct { // line 6592 +type PFullESemanticTokensOptions struct { // line 6586 // The server supports deltas for full documents. Delta bool `json:"delta"` } // created for Literal (Lit_CompletionList_itemDefaults) -type PItemDefaultsMsg_textDocument_completion struct { // line 4772 +type PItemDefaultsMsg_textDocument_completion struct { // line 4750 /* * A default commit character set. * @@ -3714,12 +3706,12 @@ type PItemDefaultsMsg_textDocument_completion struct { // line 4772 } // created for Literal (Lit_WorkspaceSymbol_location_Item1) -type PLocationMsg_workspace_symbol struct { // line 5547 +type PLocationMsg_workspace_symbol struct { // line 5527 URI DocumentURI `json:"uri"` } // created for Literal (Lit_ShowMessageRequestClientCapabilities_messageActionItem) -type PMessageActionItemPShowMessage struct { // line 12490 +type PMessageActionItemPShowMessage struct { // line 12463 /* * Whether the client supports additional attributes which * are preserved and send back to the server in the @@ -3729,7 +3721,7 @@ type PMessageActionItemPShowMessage struct { // line 12490 } // created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0) -type PNotebookSelectorPNotebookDocumentSync struct { // line 9832 +type PNotebookSelectorPNotebookDocumentSync struct { // line 9805 /* * The notebook to be synced If a string * value is provided it matches against the @@ -3741,11 +3733,11 @@ type PNotebookSelectorPNotebookDocumentSync struct { // line 9832 } // created for Literal (Lit_SemanticTokensOptions_range_Item1) -type PRangeESemanticTokensOptions struct { // line 6572 +type PRangeESemanticTokensOptions struct { // line 6566 } // created for Literal (Lit_SemanticTokensClientCapabilities_requests) -type PRequestsPSemanticTokens struct { // line 12198 +type PRequestsPSemanticTokens struct { // line 12171 /* * The client will send the `textDocument/semanticTokens/range` request if * the server provides a corresponding handler. @@ -3759,19 +3751,19 @@ type PRequestsPSemanticTokens struct { // line 12198 } // created for Literal (Lit_CodeActionClientCapabilities_resolveSupport) -type PResolveSupportPCodeAction struct { // line 11827 +type PResolveSupportPCodeAction struct { // line 11800 // The properties that a client can resolve lazily. Properties []string `json:"properties"` } // created for Literal (Lit_InlayHintClientCapabilities_resolveSupport) -type PResolveSupportPInlayHint struct { // line 12410 +type PResolveSupportPInlayHint struct { // line 12383 // The properties that a client can resolve lazily. Properties []string `json:"properties"` } // created for Literal (Lit_WorkspaceSymbolClientCapabilities_resolveSupport) -type PResolveSupportPSymbol struct { // line 10964 +type PResolveSupportPSymbol struct { // line 10937 /* * The properties that a client can resolve lazily. Usually * `location.range` @@ -3780,7 +3772,7 @@ type PResolveSupportPSymbol struct { // line 10964 } // created for Literal (Lit_InitializeResult_serverInfo) -type PServerInfoMsg_initialize struct { // line 4118 +type PServerInfoMsg_initialize struct { // line 4095 // The name of the server as defined by the server. Name string `json:"name"` // The server's version as defined by the server. @@ -3788,7 +3780,7 @@ type PServerInfoMsg_initialize struct { // line 4118 } // created for Literal (Lit_SignatureHelpClientCapabilities_signatureInformation) -type PSignatureInformationPSignatureHelp struct { // line 11469 +type PSignatureInformationPSignatureHelp struct { // line 11442 /* * Client supports the following content formats for the documentation * property. The order describes the preferred format of the client. @@ -3806,7 +3798,7 @@ type PSignatureInformationPSignatureHelp struct { // line 11469 } // created for Literal (Lit_GeneralClientCapabilities_staleRequestSupport) -type PStaleRequestSupportPGeneral struct { // line 10696 +type PStaleRequestSupportPGeneral struct { // line 10669 // The client will actively cancel the request. Cancel bool `json:"cancel"` /* @@ -3818,7 +3810,7 @@ type PStaleRequestSupportPGeneral struct { // line 10696 } // created for Literal (Lit_DocumentSymbolClientCapabilities_symbolKind) -type PSymbolKindPDocumentSymbol struct { // line 11680 +type PSymbolKindPDocumentSymbol struct { // line 11653 /* * The symbol kind values the client supports. When this * property exists the client also guarantees that it will @@ -3833,7 +3825,7 @@ type PSymbolKindPDocumentSymbol struct { // line 11680 } // created for Literal (Lit_WorkspaceSymbolClientCapabilities_symbolKind) -type PSymbolKindPSymbol struct { // line 10916 +type PSymbolKindPSymbol struct { // line 10889 /* * The symbol kind values the client supports. When this * property exists the client also guarantees that it will @@ -3848,28 +3840,28 @@ type PSymbolKindPSymbol struct { // line 10916 } // created for Literal (Lit_DocumentSymbolClientCapabilities_tagSupport) -type PTagSupportPDocumentSymbol struct { // line 11713 +type PTagSupportPDocumentSymbol struct { // line 11686 // The tags supported by the client. ValueSet []SymbolTag `json:"valueSet"` } // created for Literal (Lit_PublishDiagnosticsClientCapabilities_tagSupport) -type PTagSupportPPublishDiagnostics struct { // line 12113 +type PTagSupportPPublishDiagnostics struct { // line 12086 // The tags supported by the client. ValueSet []DiagnosticTag `json:"valueSet"` } // created for Literal (Lit_WorkspaceSymbolClientCapabilities_tagSupport) -type PTagSupportPSymbol struct { // line 10940 +type PTagSupportPSymbol struct { // line 10913 // The tags supported by the client. ValueSet []SymbolTag `json:"valueSet"` } // The parameters of a configuration request. -type ParamConfiguration struct { // line 2207 +type ParamConfiguration struct { // line 2198 Items []ConfigurationItem `json:"items"` } -type ParamInitialize struct { // line 4090 +type ParamInitialize struct { // line 4067 XInitializeParams WorkspaceFoldersInitializeParams } @@ -3878,7 +3870,7 @@ type ParamInitialize struct { // line 4090 * Represents a parameter of a callable-signature. A parameter can * have a label and a doc-comment. */ -type ParameterInformation struct { // line 10089 +type ParameterInformation struct { // line 10062 /* * The label of this parameter information. * @@ -3896,7 +3888,7 @@ type ParameterInformation struct { // line 10089 */ Documentation string `json:"documentation,omitempty"` } -type PartialResultParams struct { // line 2223 +type PartialResultParams struct { // line 6257 /* * An optional token that a server can use to report partial results (e.g. streaming) to * the client. @@ -3915,7 +3907,7 @@ type PartialResultParams struct { // line 2223 * * @since 3.17.0 */ -type Pattern = string // (alias) line 14372 +type Pattern = string // (alias) line 14362 /* * Position in a text document expressed as zero-based line and character * offset. Prior to 3.17 the offsets were always based on a UTF-16 string @@ -3945,7 +3937,7 @@ type Pattern = string // (alias) line 14372 * * @since 3.17.0 - support for negotiated position encoding. */ -type Position struct { // line 6506 +type Position struct { // line 6500 /* * Line position in a document (zero-based). * @@ -3970,20 +3962,20 @@ type Position struct { // line 6506 * * @since 3.17.0 */ -type PositionEncodingKind string // line 13453 +type PositionEncodingKind string // line 13426 type PrepareRename2Gn = Msg_PrepareRename2Gn // (alias) line 13927 -type PrepareRenameParams struct { // line 5944 +type PrepareRenameParams struct { // line 5924 TextDocumentPositionParams WorkDoneProgressParams } type PrepareRenameResult = Msg_PrepareRename2Gn // (alias) line 13927 -type PrepareSupportDefaultBehavior uint32 // line 13748 +type PrepareSupportDefaultBehavior uint32 // line 13721 /* * A previous result id in a workspace pull request. * * @since 3.17.0 */ -type PreviousResultID struct { // line 7336 +type PreviousResultID struct { // line 7330 /* * The URI for which the client knowns a * result id. @@ -3998,7 +3990,7 @@ type PreviousResultID struct { // line 7336 * * @since 3.17.0 */ -type PreviousResultId struct { // line 7336 +type PreviousResultId struct { // line 7330 /* * The URI for which the client knowns a * result id. @@ -4007,15 +3999,15 @@ type PreviousResultId struct { // line 7336 // The value of the previous result id. Value string `json:"value"` } -type ProgressParams struct { // line 6220 +type ProgressParams struct { // line 6200 // The progress token provided by the client or server. Token ProgressToken `json:"token"` // The progress data. Value interface{} `json:"value"` } -type ProgressToken = interface{} // (alias) line 13974 +type ProgressToken = interface{} // (alias) line 13959 // The publish diagnostic client capabilities. -type PublishDiagnosticsClientCapabilities struct { // line 12098 +type PublishDiagnosticsClientCapabilities struct { // line 12071 // Whether the clients accepts diagnostics with related information. RelatedInformation bool `json:"relatedInformation,omitempty"` /* @@ -4049,7 +4041,7 @@ type PublishDiagnosticsClientCapabilities struct { // line 12098 } // The publish diagnostic notification's parameters. -type PublishDiagnosticsParams struct { // line 4484 +type PublishDiagnosticsParams struct { // line 4461 // The URI for which diagnostic information is reported. URI DocumentURI `json:"uri"` /* @@ -4075,15 +4067,15 @@ type PublishDiagnosticsParams struct { // line 4484 * } * ``` */ -type Range struct { // line 6316 +type Range struct { // line 6310 // The range's start position. Start Position `json:"start"` // The range's end position. End Position `json:"end"` } -// Client Capabilities for a [ReferencesRequest](#ReferencesRequest). -type ReferenceClientCapabilities struct { // line 11635 +// Client Capabilities for a {@link ReferencesRequest}. +type ReferenceClientCapabilities struct { // line 11608 // Whether references supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } @@ -4092,32 +4084,32 @@ type ReferenceClientCapabilities struct { // line 11635 * Value-object that contains additional information when * requesting references. */ -type ReferenceContext struct { // line 8950 +type ReferenceContext struct { // line 8929 // Include the declaration of the current symbol. IncludeDeclaration bool `json:"includeDeclaration"` } // Reference options. -type ReferenceOptions struct { // line 8964 +type ReferenceOptions struct { // line 8943 WorkDoneProgressOptions } -// Parameters for a [ReferencesRequest](#ReferencesRequest). -type ReferenceParams struct { // line 5075 +// Parameters for a {@link ReferencesRequest}. +type ReferenceParams struct { // line 5053 Context ReferenceContext `json:"context"` TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -// Registration options for a [ReferencesRequest](#ReferencesRequest). -type ReferenceRegistrationOptions struct { // line 5104 +// Registration options for a {@link ReferencesRequest}. +type ReferenceRegistrationOptions struct { // line 5082 TextDocumentRegistrationOptions ReferenceOptions } // General parameters to to register for an notification or to register a provider. -type Registration struct { // line 7602 +type Registration struct { // line 7596 /* * The id used to register the request. The id can be used to deregister * the request again. @@ -4128,7 +4120,7 @@ type Registration struct { // line 7602 // Options necessary for the registration. RegisterOptions interface{} `json:"registerOptions,omitempty"` } -type RegistrationParams struct { // line 4060 +type RegistrationParams struct { // line 4037 Registrations []Registration `json:"registrations"` } @@ -4137,7 +4129,7 @@ type RegistrationParams struct { // line 4060 * * @since 3.16.0 */ -type RegularExpressionsClientCapabilities struct { // line 12526 +type RegularExpressionsClientCapabilities struct { // line 12499 // The engine's name. Engine string `json:"engine"` // The engine's version. @@ -4149,7 +4141,7 @@ type RegularExpressionsClientCapabilities struct { // line 12526 * * @since 3.17.0 */ -type RelatedFullDocumentDiagnosticReport struct { // line 7162 +type RelatedFullDocumentDiagnosticReport struct { // line 7156 /* * Diagnostics of related documents. This information is useful * in programming languages where code in a file A can generate @@ -4168,7 +4160,7 @@ type RelatedFullDocumentDiagnosticReport struct { // line 7162 * * @since 3.17.0 */ -type RelatedUnchangedDocumentDiagnosticReport struct { // line 7201 +type RelatedUnchangedDocumentDiagnosticReport struct { // line 7195 /* * Diagnostics of related documents. This information is useful * in programming languages where code in a file A can generate @@ -4189,7 +4181,7 @@ type RelatedUnchangedDocumentDiagnosticReport struct { // line 7201 * * @since 3.17.0 */ -type RelativePattern struct { // line 10762 +type RelativePattern struct { // line 10735 /* * A workspace folder or a base URI to which this pattern will be matched * against relatively. @@ -4198,7 +4190,7 @@ type RelativePattern struct { // line 10762 // The actual glob pattern; Pattern Pattern `json:"pattern"` } -type RenameClientCapabilities struct { // line 11960 +type RenameClientCapabilities struct { // line 11933 // Whether rename supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` /* @@ -4230,7 +4222,7 @@ type RenameClientCapabilities struct { // line 11960 } // Rename file operation -type RenameFile struct { // line 6754 +type RenameFile struct { // line 6748 // A rename Kind string `json:"kind"` // The old (existing) location. @@ -4243,7 +4235,7 @@ type RenameFile struct { // line 6754 } // Rename file options -type RenameFileOptions struct { // line 9461 +type RenameFileOptions struct { // line 9440 // Overwrite target if existing. Overwrite wins over `ignoreIfExists` Overwrite bool `json:"overwrite,omitempty"` // Ignores if target exists. @@ -4256,7 +4248,7 @@ type RenameFileOptions struct { // line 9461 * * @since 3.16.0 */ -type RenameFilesParams struct { // line 3304 +type RenameFilesParams struct { // line 3281 /* * An array of all files/folders renamed in this operation. When a folder is renamed, only * the folder will be included, and not its children. @@ -4264,8 +4256,8 @@ type RenameFilesParams struct { // line 3304 Files []FileRename `json:"files"` } -// Provider options for a [RenameRequest](#RenameRequest). -type RenameOptions struct { // line 9289 +// Provider options for a {@link RenameRequest}. +type RenameOptions struct { // line 9268 /* * Renames should be checked and tested before being executed. * @@ -4275,29 +4267,29 @@ type RenameOptions struct { // line 9289 WorkDoneProgressOptions } -// The parameters of a [RenameRequest](#RenameRequest). -type RenameParams struct { // line 5893 +// The parameters of a {@link RenameRequest}. +type RenameParams struct { // line 5873 // The document to rename. TextDocument TextDocumentIdentifier `json:"textDocument"` // The position at which this request was sent. Position Position `json:"position"` /* * The new name of the symbol. If the given name is not valid the - * request must return a [ResponseError](#ResponseError) with an + * request must return a {@link ResponseError} with an * appropriate message set. */ NewName string `json:"newName"` WorkDoneProgressParams } -// Registration options for a [RenameRequest](#RenameRequest). -type RenameRegistrationOptions struct { // line 5929 +// Registration options for a {@link RenameRequest}. +type RenameRegistrationOptions struct { // line 5909 TextDocumentRegistrationOptions RenameOptions } // A generic resource operation. -type ResourceOperation struct { // line 9413 +type ResourceOperation struct { // line 9392 // The resource operation kind. Kind string `json:"kind"` /* @@ -4307,9 +4299,9 @@ type ResourceOperation struct { // line 9413 */ AnnotationID ChangeAnnotationIdentifier `json:"annotationId,omitempty"` } -type ResourceOperationKind string // line 13695 +type ResourceOperationKind string // line 13668 // Save options. -type SaveOptions struct { // line 8485 +type SaveOptions struct { // line 8464 // The client is supposed to include the content on save. IncludeText bool `json:"includeText,omitempty"` } @@ -4318,13 +4310,13 @@ type SaveOptions struct { // line 8485 * A selection range represents a part of a selection hierarchy. A selection range * may have a parent selection range that contains it. */ -type SelectionRange struct { // line 2591 - // The [range](#Range) of this selection range. +type SelectionRange struct { // line 2568 + // The {@link Range range} of this selection range. Range Range `json:"range"` // The parent selection range containing this range. Therefore `parent.range` must contain `this.range`. Parent *SelectionRange `json:"parent,omitempty"` } -type SelectionRangeClientCapabilities struct { // line 12084 +type SelectionRangeClientCapabilities struct { // line 12057 /* * Whether implementation supports dynamic registration for selection range providers. If this is set to `true` * the client supports the new `SelectionRangeRegistrationOptions` return value for the corresponding server @@ -4332,12 +4324,12 @@ type SelectionRangeClientCapabilities struct { // line 12084 */ DynamicRegistration bool `json:"dynamicRegistration,omitempty"` } -type SelectionRangeOptions struct { // line 6529 +type SelectionRangeOptions struct { // line 6523 WorkDoneProgressOptions } // A parameter literal used in selection range requests. -type SelectionRangeParams struct { // line 2556 +type SelectionRangeParams struct { // line 2533 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` // The positions inside the text document. @@ -4345,7 +4337,7 @@ type SelectionRangeParams struct { // line 2556 WorkDoneProgressParams PartialResultParams } -type SelectionRangeRegistrationOptions struct { // line 2614 +type SelectionRangeRegistrationOptions struct { // line 2591 SelectionRangeOptions TextDocumentRegistrationOptions StaticRegistrationOptions @@ -4358,7 +4350,7 @@ type SelectionRangeRegistrationOptions struct { // line 2614 * * @since 3.16.0 */ -type SemanticTokenModifiers string // line 12696 +type SemanticTokenModifiers string // line 12669 /* * A set of predefined token types. This set is not fixed * an clients can specify additional token types via the @@ -4366,9 +4358,9 @@ type SemanticTokenModifiers string // line 12696 * * @since 3.16.0 */ -type SemanticTokenTypes string // line 12589 +type SemanticTokenTypes string // line 12562 // @since 3.16.0 -type SemanticTokens struct { // line 2902 +type SemanticTokens struct { // line 2879 /* * An optional result id. If provided and clients support delta updating * the client will include the result id in the next semantic token request. @@ -4381,7 +4373,7 @@ type SemanticTokens struct { // line 2902 } // @since 3.16.0 -type SemanticTokensClientCapabilities struct { // line 12183 +type SemanticTokensClientCapabilities struct { // line 12156 /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` @@ -4434,14 +4426,14 @@ type SemanticTokensClientCapabilities struct { // line 12183 } // @since 3.16.0 -type SemanticTokensDelta struct { // line 3001 +type SemanticTokensDelta struct { // line 2978 ResultID string `json:"resultId,omitempty"` // The semantic token edits to transform a previous result into a new result. Edits []SemanticTokensEdit `json:"edits"` } // @since 3.16.0 -type SemanticTokensDeltaParams struct { // line 2968 +type SemanticTokensDeltaParams struct { // line 2945 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` /* @@ -4454,12 +4446,12 @@ type SemanticTokensDeltaParams struct { // line 2968 } // @since 3.16.0 -type SemanticTokensDeltaPartialResult struct { // line 3027 +type SemanticTokensDeltaPartialResult struct { // line 3004 Edits []SemanticTokensEdit `json:"edits"` } // @since 3.16.0 -type SemanticTokensEdit struct { // line 6622 +type SemanticTokensEdit struct { // line 6616 // The start offset of the edit. Start uint32 `json:"start"` // The count of elements to remove. @@ -4469,7 +4461,7 @@ type SemanticTokensEdit struct { // line 6622 } // @since 3.16.0 -type SemanticTokensLegend struct { // line 9334 +type SemanticTokensLegend struct { // line 9313 // The token types a server uses. TokenTypes []string `json:"tokenTypes"` // The token modifiers a server uses. @@ -4477,7 +4469,7 @@ type SemanticTokensLegend struct { // line 9334 } // @since 3.16.0 -type SemanticTokensOptions struct { // line 6551 +type SemanticTokensOptions struct { // line 6545 // The legend used by the server Legend SemanticTokensLegend `json:"legend"` /* @@ -4491,7 +4483,7 @@ type SemanticTokensOptions struct { // line 6551 } // @since 3.16.0 -type SemanticTokensParams struct { // line 2877 +type SemanticTokensParams struct { // line 2854 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` WorkDoneProgressParams @@ -4499,12 +4491,12 @@ type SemanticTokensParams struct { // line 2877 } // @since 3.16.0 -type SemanticTokensPartialResult struct { // line 2929 +type SemanticTokensPartialResult struct { // line 2906 Data []uint32 `json:"data"` } // @since 3.16.0 -type SemanticTokensRangeParams struct { // line 3044 +type SemanticTokensRangeParams struct { // line 3021 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` // The range the semantic tokens are requested for. @@ -4514,14 +4506,14 @@ type SemanticTokensRangeParams struct { // line 3044 } // @since 3.16.0 -type SemanticTokensRegistrationOptions struct { // line 2946 +type SemanticTokensRegistrationOptions struct { // line 2923 TextDocumentRegistrationOptions SemanticTokensOptions StaticRegistrationOptions } // @since 3.16.0 -type SemanticTokensWorkspaceClientCapabilities struct { // line 11003 +type SemanticTokensWorkspaceClientCapabilities struct { // line 10976 /* * Whether the client implementation supports a refresh request sent from * the server to the client. @@ -4538,7 +4530,7 @@ type SemanticTokensWorkspaceClientCapabilities struct { // line 11003 * Defines the capabilities provided by a language * server. */ -type ServerCapabilities struct { // line 7829 +type ServerCapabilities struct { // line 7808 /* * The position encoding the server picked from the encodings offered * by the client via the client capability `general.positionEncodings`. @@ -4668,7 +4660,7 @@ type ServerCapabilities struct { // line 7829 // Experimental server capabilities. Experimental interface{} `json:"experimental,omitempty"` } -type SetTraceParams struct { // line 6166 +type SetTraceParams struct { // line 6146 Value TraceValues `json:"value"` } @@ -4677,7 +4669,7 @@ type SetTraceParams struct { // line 6166 * * @since 3.16.0 */ -type ShowDocumentClientCapabilities struct { // line 12511 +type ShowDocumentClientCapabilities struct { // line 12484 /* * The client has support for the showDocument * request. @@ -4690,7 +4682,7 @@ type ShowDocumentClientCapabilities struct { // line 12511 * * @since 3.16.0 */ -type ShowDocumentParams struct { // line 3077 +type ShowDocumentParams struct { // line 3054 // The document uri to show. URI URI `json:"uri"` /* @@ -4720,13 +4712,13 @@ type ShowDocumentParams struct { // line 3077 * * @since 3.16.0 */ -type ShowDocumentResult struct { // line 3119 +type ShowDocumentResult struct { // line 3096 // A boolean indicating if the show was successful. Success bool `json:"success"` } // The parameters of a notification message. -type ShowMessageParams struct { // line 4205 +type ShowMessageParams struct { // line 4182 // The message type. See {@link MessageType} Type MessageType `json:"type"` // The actual message. @@ -4734,11 +4726,11 @@ type ShowMessageParams struct { // line 4205 } // Show message request client capabilities -type ShowMessageRequestClientCapabilities struct { // line 12484 +type ShowMessageRequestClientCapabilities struct { // line 12457 // Capabilities specific to the `MessageActionItem` type. MessageActionItem *PMessageActionItemPShowMessage `json:"messageActionItem,omitempty"` } -type ShowMessageRequestParams struct { // line 4227 +type ShowMessageRequestParams struct { // line 4204 // The message type. See {@link MessageType} Type MessageType `json:"type"` // The actual message. @@ -4752,7 +4744,7 @@ type ShowMessageRequestParams struct { // line 4227 * callable. There can be multiple signature but only one * active and only one active parameter. */ -type SignatureHelp struct { // line 4989 +type SignatureHelp struct { // line 4967 // One or more signatures. Signatures []SignatureInformation `json:"signatures"` /* @@ -4779,8 +4771,8 @@ type SignatureHelp struct { // line 4989 ActiveParameter uint32 `json:"activeParameter,omitempty"` } -// Client Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). -type SignatureHelpClientCapabilities struct { // line 11454 +// Client Capabilities for a {@link SignatureHelpRequest}. +type SignatureHelpClientCapabilities struct { // line 11427 // Whether signature help supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` /* @@ -4804,7 +4796,7 @@ type SignatureHelpClientCapabilities struct { // line 11454 * * @since 3.15.0 */ -type SignatureHelpContext struct { // line 8807 +type SignatureHelpContext struct { // line 8786 // Action that caused signature help to be triggered. TriggerKind SignatureHelpTriggerKind `json:"triggerKind"` /* @@ -4829,8 +4821,8 @@ type SignatureHelpContext struct { // line 8807 ActiveSignatureHelp *SignatureHelp `json:"activeSignatureHelp,omitempty"` } -// Server Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). -type SignatureHelpOptions struct { // line 8902 +// Server Capabilities for a {@link SignatureHelpRequest}. +type SignatureHelpOptions struct { // line 8881 // List of characters that trigger signature help automatically. TriggerCharacters []string `json:"triggerCharacters,omitempty"` /* @@ -4845,8 +4837,8 @@ type SignatureHelpOptions struct { // line 8902 WorkDoneProgressOptions } -// Parameters for a [SignatureHelpRequest](#SignatureHelpRequest). -type SignatureHelpParams struct { // line 4961 +// Parameters for a {@link SignatureHelpRequest}. +type SignatureHelpParams struct { // line 4939 /* * The signature help context. This is only available if the client specifies * to send this using the client capability `textDocument.signatureHelp.contextSupport === true` @@ -4858,8 +4850,8 @@ type SignatureHelpParams struct { // line 4961 WorkDoneProgressParams } -// Registration options for a [SignatureHelpRequest](#SignatureHelpRequest). -type SignatureHelpRegistrationOptions struct { // line 5024 +// Registration options for a {@link SignatureHelpRequest}. +type SignatureHelpRegistrationOptions struct { // line 5002 TextDocumentRegistrationOptions SignatureHelpOptions } @@ -4869,13 +4861,13 @@ type SignatureHelpRegistrationOptions struct { // line 5024 * * @since 3.15.0 */ -type SignatureHelpTriggerKind uint32 // line 13606 +type SignatureHelpTriggerKind uint32 // line 13579 /* * Represents the signature of something callable. A signature * can have a label, like a function-name, a doc-comment, and * a set of parameters. */ -type SignatureInformation struct { // line 8848 +type SignatureInformation struct { // line 8827 /* * The label of this signature. Will be shown in * the UI. @@ -4902,7 +4894,7 @@ type SignatureInformation struct { // line 8848 * Static registration options to be returned in the initialize * request. */ -type StaticRegistrationOptions struct { // line 6348 +type StaticRegistrationOptions struct { // line 6342 /* * The id used to register the request. The id can be used to deregister * the request again. See also Registration#id. @@ -4914,7 +4906,7 @@ type StaticRegistrationOptions struct { // line 6348 * Represents information about programming constructs like variables, classes, * interfaces etc. */ -type SymbolInformation struct { // line 5202 +type SymbolInformation struct { // line 5180 // extends BaseSymbolInformation /* * Indicates if this symbol is deprecated. @@ -4954,22 +4946,22 @@ type SymbolInformation struct { // line 5202 } // A symbol kind. -type SymbolKind uint32 // line 12867 +type SymbolKind uint32 // line 12840 /* * Symbol tags are extra annotations that tweak the rendering of a symbol. * * @since 3.16 */ -type SymbolTag uint32 // line 12981 +type SymbolTag uint32 // line 12954 // Describe options to be used when registered for text document change events. -type TextDocumentChangeRegistrationOptions struct { // line 4334 +type TextDocumentChangeRegistrationOptions struct { // line 4311 // How documents are synced to the server. SyncKind TextDocumentSyncKind `json:"syncKind"` TextDocumentRegistrationOptions } // Text document specific client capabilities. -type TextDocumentClientCapabilities struct { // line 10349 +type TextDocumentClientCapabilities struct { // line 10322 // Defines which synchronization capabilities the client supports. Synchronization *TextDocumentSyncClientCapabilities `json:"synchronization,omitempty"` // Capabilities specific to the `textDocument/completion` request. @@ -5093,14 +5085,14 @@ type TextDocumentClientCapabilities struct { // line 10349 * An event describing a change to a text document. If only a text is provided * it is considered to be the full content of the document. */ -type TextDocumentContentChangeEvent = Msg_TextDocumentContentChangeEvent // (alias) line 14028 +type TextDocumentContentChangeEvent = Msg_TextDocumentContentChangeEvent // (alias) line 14001 /* * Describes textual changes on a text document. A TextDocumentEdit describes all changes * on a document version Si and after they are applied move the document to version Si+1. * So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any * kind of ordering. However the edits must be non overlapping. */ -type TextDocumentEdit struct { // line 6682 +type TextDocumentEdit struct { // line 6676 // The text document to change. TextDocument OptionalVersionedTextDocumentIdentifier `json:"textDocument"` /* @@ -5114,8 +5106,8 @@ type TextDocumentEdit struct { // line 6682 /* * A document filter denotes a document by different properties like - * the [language](#TextDocument.languageId), the [scheme](#Uri.scheme) of - * its resource, or a glob-pattern that is applied to the [path](#TextDocument.fileName). + * the {@link TextDocument.languageId language}, the {@link Uri.scheme scheme} of + * its resource, or a glob-pattern that is applied to the {@link TextDocument.fileName path}. * * Glob patterns can have the following syntax: * - `*` to match one or more characters in a path segment @@ -5130,9 +5122,9 @@ type TextDocumentEdit struct { // line 6682 * * @since 3.17.0 */ -type TextDocumentFilter = Msg_TextDocumentFilter // (alias) line 14154 +type TextDocumentFilter = Msg_TextDocumentFilter // (alias) line 14144 // A literal to identify a text document in the client. -type TextDocumentIdentifier struct { // line 6424 +type TextDocumentIdentifier struct { // line 6418 // The text document's uri. URI DocumentURI `json:"uri"` } @@ -5141,7 +5133,7 @@ type TextDocumentIdentifier struct { // line 6424 * An item to transfer a text document from the client to the * server. */ -type TextDocumentItem struct { // line 7410 +type TextDocumentItem struct { // line 7404 // The text document's uri. URI DocumentURI `json:"uri"` // The text document's language identifier. @@ -5159,7 +5151,7 @@ type TextDocumentItem struct { // line 7410 * A parameter literal used in requests to pass a text document and a position inside that * document. */ -type TextDocumentPositionParams struct { // line 6241 +type TextDocumentPositionParams struct { // line 6221 // The text document. TextDocument TextDocumentIdentifier `json:"textDocument"` // The position inside the text document. @@ -5167,7 +5159,7 @@ type TextDocumentPositionParams struct { // line 6241 } // General text document registration options. -type TextDocumentRegistrationOptions struct { // line 2390 +type TextDocumentRegistrationOptions struct { // line 2367 /* * A document selector to identify the scope of the registration. If set to null * the document selector provided on the client side will be used. @@ -5176,13 +5168,13 @@ type TextDocumentRegistrationOptions struct { // line 2390 } // Represents reasons why a text document is saved. -type TextDocumentSaveReason uint32 // line 13135 +type TextDocumentSaveReason uint32 // line 13108 // Save registration options. -type TextDocumentSaveRegistrationOptions struct { // line 4391 +type TextDocumentSaveRegistrationOptions struct { // line 4368 TextDocumentRegistrationOptions SaveOptions } -type TextDocumentSyncClientCapabilities struct { // line 11153 +type TextDocumentSyncClientCapabilities struct { // line 11126 // Whether text document synchronization supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` // The client supports sending will save notifications. @@ -5201,8 +5193,8 @@ type TextDocumentSyncClientCapabilities struct { // line 11153 * Defines how the host (editor) should sync * document changes to the language server. */ -type TextDocumentSyncKind uint32 // line 13110 -type TextDocumentSyncOptions struct { // line 9762 +type TextDocumentSyncKind uint32 // line 13083 +type TextDocumentSyncOptions struct { // line 9735 /* * Open and close notifications are sent to the server. If omitted open close notification should not * be sent. @@ -5231,7 +5223,7 @@ type TextDocumentSyncOptions struct { // line 9762 } // A text edit applicable to a text document. -type TextEdit struct { // line 4428 +type TextEdit struct { // line 4405 /* * The range of the text document to be manipulated. To insert * text into a document create a range where start === end. @@ -5243,10 +5235,10 @@ type TextEdit struct { // line 4428 */ NewText string `json:"newText"` } -type TokenFormat string // line 13762 -type TraceValues string // line 13409 +type TokenFormat string // line 13735 +type TraceValues string // line 13382 // Since 3.6.0 -type TypeDefinitionClientCapabilities struct { // line 11585 +type TypeDefinitionClientCapabilities struct { // line 11558 /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `TypeDefinitionRegistrationOptions` return value @@ -5260,22 +5252,22 @@ type TypeDefinitionClientCapabilities struct { // line 11585 */ LinkSupport bool `json:"linkSupport,omitempty"` } -type TypeDefinitionOptions struct { // line 6363 +type TypeDefinitionOptions struct { // line 6357 WorkDoneProgressOptions } -type TypeDefinitionParams struct { // line 2131 +type TypeDefinitionParams struct { // line 2122 TextDocumentPositionParams WorkDoneProgressParams PartialResultParams } -type TypeDefinitionRegistrationOptions struct { // line 2151 +type TypeDefinitionRegistrationOptions struct { // line 2142 TextDocumentRegistrationOptions TypeDefinitionOptions StaticRegistrationOptions } // @since 3.17.0 -type TypeHierarchyClientCapabilities struct { // line 12363 +type TypeHierarchyClientCapabilities struct { // line 12336 /* * Whether implementation supports dynamic registration. If this is set to `true` * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` @@ -5285,7 +5277,7 @@ type TypeHierarchyClientCapabilities struct { // line 12363 } // @since 3.17.0 -type TypeHierarchyItem struct { // line 3432 +type TypeHierarchyItem struct { // line 3409 // The name of this item. Name string `json:"name"` // The kind of this item. @@ -5304,7 +5296,7 @@ type TypeHierarchyItem struct { // line 3432 /* * The range that should be selected and revealed when this symbol is being * picked, e.g. the name of a function. Must be contained by the - * [`range`](#TypeHierarchyItem.range). + * {@link TypeHierarchyItem.range `range`}. */ SelectionRange Range `json:"selectionRange"` /* @@ -5321,7 +5313,7 @@ type TypeHierarchyItem struct { // line 3432 * * @since 3.17.0 */ -type TypeHierarchyOptions struct { // line 6941 +type TypeHierarchyOptions struct { // line 6935 WorkDoneProgressOptions } @@ -5330,7 +5322,7 @@ type TypeHierarchyOptions struct { // line 6941 * * @since 3.17.0 */ -type TypeHierarchyPrepareParams struct { // line 3414 +type TypeHierarchyPrepareParams struct { // line 3391 TextDocumentPositionParams WorkDoneProgressParams } @@ -5340,7 +5332,7 @@ type TypeHierarchyPrepareParams struct { // line 3414 * * @since 3.17.0 */ -type TypeHierarchyRegistrationOptions struct { // line 3509 +type TypeHierarchyRegistrationOptions struct { // line 3486 TextDocumentRegistrationOptions TypeHierarchyOptions StaticRegistrationOptions @@ -5351,7 +5343,7 @@ type TypeHierarchyRegistrationOptions struct { // line 3509 * * @since 3.17.0 */ -type TypeHierarchySubtypesParams struct { // line 3555 +type TypeHierarchySubtypesParams struct { // line 3532 Item TypeHierarchyItem `json:"item"` WorkDoneProgressParams PartialResultParams @@ -5362,14 +5354,14 @@ type TypeHierarchySubtypesParams struct { // line 3555 * * @since 3.17.0 */ -type TypeHierarchySupertypesParams struct { // line 3531 +type TypeHierarchySupertypesParams struct { // line 3508 Item TypeHierarchyItem `json:"item"` WorkDoneProgressParams PartialResultParams } // created for Tuple -type UIntCommaUInt struct { // line 10102 +type UIntCommaUInt struct { // line 10075 Fld0 uint32 `json:"fld0"` Fld1 uint32 `json:"fld1"` } @@ -5381,7 +5373,7 @@ type URI = string * * @since 3.17.0 */ -type UnchangedDocumentDiagnosticReport struct { // line 7275 +type UnchangedDocumentDiagnosticReport struct { // line 7269 /* * A document diagnostic report indicating * no changes to the last result. A server can @@ -5401,9 +5393,9 @@ type UnchangedDocumentDiagnosticReport struct { // line 7275 * * @since 3.16.0 */ -type UniquenessLevel string // line 12997 +type UniquenessLevel string // line 12970 // General parameters to unregister a request or notification. -type Unregistration struct { // line 7633 +type Unregistration struct { // line 7627 /* * The id used to unregister the request or notification. Usually an id * provided during the register request. @@ -5412,7 +5404,7 @@ type Unregistration struct { // line 7633 // The method to unregister for. Method string `json:"method"` } -type UnregistrationParams struct { // line 4075 +type UnregistrationParams struct { // line 4052 Unregisterations []Unregistration `json:"unregisterations"` } @@ -5421,7 +5413,7 @@ type UnregistrationParams struct { // line 4075 * * @since 3.17.0 */ -type VersionedNotebookDocumentIdentifier struct { // line 7448 +type VersionedNotebookDocumentIdentifier struct { // line 7442 // The version number of this notebook document. Version int32 `json:"version"` // The notebook document's uri. @@ -5429,19 +5421,19 @@ type VersionedNotebookDocumentIdentifier struct { // line 7448 } // A text document identifier to denote a specific version of a text document. -type VersionedTextDocumentIdentifier struct { // line 8465 +type VersionedTextDocumentIdentifier struct { // line 8444 // The version number of this document. Version int32 `json:"version"` TextDocumentIdentifier } type WatchKind = uint32 // line 13505// The parameters sent in a will save text document notification. -type WillSaveTextDocumentParams struct { // line 4406 +type WillSaveTextDocumentParams struct { // line 4383 // The document that will be saved. TextDocument TextDocumentIdentifier `json:"textDocument"` // The 'TextDocumentSaveReason'. Reason TextDocumentSaveReason `json:"reason"` } -type WindowClientCapabilities struct { // line 10655 +type WindowClientCapabilities struct { // line 10628 /* * It indicates whether the client supports server initiated * progress using the `window/workDoneProgress/create` request. @@ -5467,7 +5459,7 @@ type WindowClientCapabilities struct { // line 10655 */ ShowDocument *ShowDocumentClientCapabilities `json:"showDocument,omitempty"` } -type WorkDoneProgressBegin struct { // line 6059 +type WorkDoneProgressBegin struct { // line 6039 Kind string `json:"kind"` /* * Mandatory title of the progress operation. Used to briefly inform about @@ -5500,15 +5492,15 @@ type WorkDoneProgressBegin struct { // line 6059 */ Percentage uint32 `json:"percentage,omitempty"` } -type WorkDoneProgressCancelParams struct { // line 2647 +type WorkDoneProgressCancelParams struct { // line 2624 // The token to be used to report progress. Token ProgressToken `json:"token"` } -type WorkDoneProgressCreateParams struct { // line 2634 +type WorkDoneProgressCreateParams struct { // line 2611 // The token to be used to report progress. Token ProgressToken `json:"token"` } -type WorkDoneProgressEnd struct { // line 6145 +type WorkDoneProgressEnd struct { // line 6125 Kind string `json:"kind"` /* * Optional, a final message indicating to for example indicate the outcome @@ -5516,20 +5508,20 @@ type WorkDoneProgressEnd struct { // line 6145 */ Message string `json:"message,omitempty"` } -type WorkDoneProgressOptions struct { // line 2377 +type WorkDoneProgressOptions struct { // line 2354 WorkDoneProgress bool `json:"workDoneProgress,omitempty"` } // created for And -type WorkDoneProgressOptionsAndTextDocumentRegistrationOptions struct { // line 204 +type WorkDoneProgressOptionsAndTextDocumentRegistrationOptions struct { // line 195 WorkDoneProgressOptions TextDocumentRegistrationOptions } -type WorkDoneProgressParams struct { // line 6263 +type WorkDoneProgressParams struct { // line 6243 // An optional token that a server can use to report work done progress. WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` } -type WorkDoneProgressReport struct { // line 6106 +type WorkDoneProgressReport struct { // line 6086 Kind string `json:"kind"` /* * Controls enablement state of a cancel button. @@ -5558,7 +5550,7 @@ type WorkDoneProgressReport struct { // line 6106 } // created for Literal (Lit_ServerCapabilities_workspace) -type Workspace6Gn struct { // line 8424 +type Workspace6Gn struct { // line 8403 /* * The server supports workspace folder. * @@ -5574,7 +5566,7 @@ type Workspace6Gn struct { // line 8424 } // Workspace specific client capabilities. -type WorkspaceClientCapabilities struct { // line 10210 +type WorkspaceClientCapabilities struct { // line 10183 /* * The client supports applying batch edits * to the workspace by supporting the request @@ -5651,7 +5643,7 @@ type WorkspaceClientCapabilities struct { // line 10210 * * @since 3.17.0 */ -type WorkspaceDiagnosticParams struct { // line 3899 +type WorkspaceDiagnosticParams struct { // line 3876 // The additional identifier provided during registration. Identifier string `json:"identifier,omitempty"` /* @@ -5668,7 +5660,7 @@ type WorkspaceDiagnosticParams struct { // line 3899 * * @since 3.17.0 */ -type WorkspaceDiagnosticReport struct { // line 3936 +type WorkspaceDiagnosticReport struct { // line 3913 Items []WorkspaceDocumentDiagnosticReport `json:"items"` } @@ -5677,7 +5669,7 @@ type WorkspaceDiagnosticReport struct { // line 3936 * * @since 3.17.0 */ -type WorkspaceDiagnosticReportPartialResult struct { // line 3953 +type WorkspaceDiagnosticReportPartialResult struct { // line 3930 Items []WorkspaceDocumentDiagnosticReport `json:"items"` } @@ -5686,7 +5678,7 @@ type WorkspaceDiagnosticReportPartialResult struct { // line 3953 * * @since 3.17.0 */ -type WorkspaceDocumentDiagnosticReport = Or_WorkspaceDocumentDiagnosticReport // (alias) line 14010 +type WorkspaceDocumentDiagnosticReport = Or_WorkspaceDocumentDiagnosticReport // (alias) line 13983 /* * A workspace edit represents changes to many resources managed in the workspace. The edit * should either provide `changes` or `documentChanges`. If documentChanges are present @@ -5701,7 +5693,7 @@ type WorkspaceDocumentDiagnosticReport = Or_WorkspaceDocumentDiagnosticReport // * cause failure of the operation. How the client recovers from the failure is described by * the client capability: `workspace.workspaceEdit.failureHandling` */ -type WorkspaceEdit struct { // line 3215 +type WorkspaceEdit struct { // line 3192 // Holds changes to existing resources. Changes map[DocumentURI][]TextEdit `json:"changes,omitempty"` /* @@ -5727,7 +5719,7 @@ type WorkspaceEdit struct { // line 3215 */ ChangeAnnotations map[ChangeAnnotationIdentifier]ChangeAnnotation `json:"changeAnnotations,omitempty"` } -type WorkspaceEditClientCapabilities struct { // line 10794 +type WorkspaceEditClientCapabilities struct { // line 10767 // The client supports versioned document changes in `WorkspaceEdit`s DocumentChanges bool `json:"documentChanges,omitempty"` /* @@ -5764,7 +5756,7 @@ type WorkspaceEditClientCapabilities struct { // line 10794 } // A workspace folder inside a client. -type WorkspaceFolder struct { // line 2171 +type WorkspaceFolder struct { // line 2162 // The associated URI for this workspace folder. URI URI `json:"uri"` /* @@ -5773,7 +5765,7 @@ type WorkspaceFolder struct { // line 2171 */ Name string `json:"name"` } -type WorkspaceFolders5Gn struct { // line 9959 +type WorkspaceFolders5Gn struct { // line 9932 // The server has support for workspace folders Supported bool `json:"supported,omitempty"` /* @@ -5789,13 +5781,13 @@ type WorkspaceFolders5Gn struct { // line 9959 } // The workspace folder change event. -type WorkspaceFoldersChangeEvent struct { // line 6373 +type WorkspaceFoldersChangeEvent struct { // line 6367 // The array of added workspace folders Added []WorkspaceFolder `json:"added"` // The array of the removed workspace folders Removed []WorkspaceFolder `json:"removed"` } -type WorkspaceFoldersInitializeParams struct { // line 7802 +type WorkspaceFoldersInitializeParams struct { // line 7781 /* * The workspace folders configured in the client when the server starts. * @@ -5807,7 +5799,7 @@ type WorkspaceFoldersInitializeParams struct { // line 7802 */ WorkspaceFolders []WorkspaceFolder `json:"workspaceFolders,omitempty"` } -type WorkspaceFoldersServerCapabilities struct { // line 9959 +type WorkspaceFoldersServerCapabilities struct { // line 9932 // The server has support for workspace folders Supported bool `json:"supported,omitempty"` /* @@ -5827,7 +5819,7 @@ type WorkspaceFoldersServerCapabilities struct { // line 9959 * * @since 3.17.0 */ -type WorkspaceFullDocumentDiagnosticReport struct { // line 9542 +type WorkspaceFullDocumentDiagnosticReport struct { // line 9521 // The URI for which diagnostic information is reported. URI DocumentURI `json:"uri"` /* @@ -5845,7 +5837,7 @@ type WorkspaceFullDocumentDiagnosticReport struct { // line 9542 * * @since 3.17.0 */ -type WorkspaceSymbol struct { // line 5534 +type WorkspaceSymbol struct { // line 5514 /* * The location of the symbol. Whether a server is allowed to * return a location without a range depends on the client @@ -5862,8 +5854,8 @@ type WorkspaceSymbol struct { // line 5534 BaseSymbolInformation } -// Client capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). -type WorkspaceSymbolClientCapabilities struct { // line 10901 +// Client capabilities for a {@link WorkspaceSymbolRequest}. +type WorkspaceSymbolClientCapabilities struct { // line 10874 // Symbol request supports dynamic registration. DynamicRegistration bool `json:"dynamicRegistration,omitempty"` // Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. @@ -5885,8 +5877,8 @@ type WorkspaceSymbolClientCapabilities struct { // line 10901 ResolveSupport *PResolveSupportPSymbol `json:"resolveSupport,omitempty"` } -// Server capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). -type WorkspaceSymbolOptions struct { // line 9125 +// Server capabilities for a {@link WorkspaceSymbolRequest}. +type WorkspaceSymbolOptions struct { // line 9104 /* * The server provides support to resolve additional * information for a workspace symbol. @@ -5897,8 +5889,8 @@ type WorkspaceSymbolOptions struct { // line 9125 WorkDoneProgressOptions } -// The parameters of a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). -type WorkspaceSymbolParams struct { // line 5510 +// The parameters of a {@link WorkspaceSymbolRequest}. +type WorkspaceSymbolParams struct { // line 5490 /* * A query string to filter symbols by. Clients may send an empty * string here to request all symbols. @@ -5908,8 +5900,8 @@ type WorkspaceSymbolParams struct { // line 5510 PartialResultParams } -// Registration options for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). -type WorkspaceSymbolRegistrationOptions struct { // line 5583 +// Registration options for a {@link WorkspaceSymbolRequest}. +type WorkspaceSymbolRegistrationOptions struct { // line 5563 WorkspaceSymbolOptions } @@ -5918,7 +5910,7 @@ type WorkspaceSymbolRegistrationOptions struct { // line 5583 * * @since 3.17.0 */ -type WorkspaceUnchangedDocumentDiagnosticReport struct { // line 9580 +type WorkspaceUnchangedDocumentDiagnosticReport struct { // line 9559 // The URI for which diagnostic information is reported. URI DocumentURI `json:"uri"` /* @@ -5930,7 +5922,7 @@ type WorkspaceUnchangedDocumentDiagnosticReport struct { // line 9580 } // The initialize parameters -type XInitializeParams struct { // line 7655 +type XInitializeParams struct { // line 7649 /* * The process Id of the parent process that started * the server. @@ -5976,12 +5968,12 @@ type XInitializeParams struct { // line 7655 // User provided initialization options. InitializationOptions interface{} `json:"initializationOptions,omitempty"` // The initial trace setting. If omitted trace is disabled ('off'). - Trace string `json:"trace,omitempty"` + Trace *TraceValues `json:"trace,omitempty"` WorkDoneProgressParams } // The initialize parameters -type _InitializeParams struct { // line 7655 +type _InitializeParams struct { // line 7649 /* * The process Id of the parent process that started * the server. @@ -6027,18 +6019,18 @@ type _InitializeParams struct { // line 7655 // User provided initialization options. InitializationOptions interface{} `json:"initializationOptions,omitempty"` // The initial trace setting. If omitted trace is disabled ('off'). - Trace string `json:"trace,omitempty"` + Trace *TraceValues `json:"trace,omitempty"` WorkDoneProgressParams } const ( // A set of predefined code action kinds // Empty kind. - Empty CodeActionKind = "" // line 13359 + Empty CodeActionKind = "" // line 13332 // Base kind for quickfix actions: 'quickfix' - QuickFix CodeActionKind = "quickfix" // line 13364 + QuickFix CodeActionKind = "quickfix" // line 13337 // Base kind for refactoring actions: 'refactor' - Refactor CodeActionKind = "refactor" // line 13369 + Refactor CodeActionKind = "refactor" // line 13342 /* * Base kind for refactoring extraction actions: 'refactor.extract' * @@ -6050,7 +6042,7 @@ const ( * - Extract interface from class * - ... */ - RefactorExtract CodeActionKind = "refactor.extract" // line 13374 + RefactorExtract CodeActionKind = "refactor.extract" // line 13347 /* * Base kind for refactoring inline actions: 'refactor.inline' * @@ -6061,7 +6053,7 @@ const ( * - Inline constant * - ... */ - RefactorInline CodeActionKind = "refactor.inline" // line 13379 + RefactorInline CodeActionKind = "refactor.inline" // line 13352 /* * Base kind for refactoring rewrite actions: 'refactor.rewrite' * @@ -6074,15 +6066,15 @@ const ( * - Move method to base class * - ... */ - RefactorRewrite CodeActionKind = "refactor.rewrite" // line 13384 + RefactorRewrite CodeActionKind = "refactor.rewrite" // line 13357 /* * Base kind for source actions: `source` * * Source code actions apply to the entire file. */ - Source CodeActionKind = "source" // line 13389 + Source CodeActionKind = "source" // line 13362 // Base kind for an organize imports source action: `source.organizeImports` - SourceOrganizeImports CodeActionKind = "source.organizeImports" // line 13394 + SourceOrganizeImports CodeActionKind = "source.organizeImports" // line 13367 /* * Base kind for auto-fix source actions: `source.fixAll`. * @@ -6091,47 +6083,47 @@ const ( * * @since 3.15.0 */ - SourceFixAll CodeActionKind = "source.fixAll" // line 13399 + SourceFixAll CodeActionKind = "source.fixAll" // line 13372 /* * The reason why code actions were requested. * * @since 3.17.0 */ // Code actions were explicitly requested by the user or by an extension. - CodeActionInvoked CodeActionTriggerKind = 1 // line 13639 + CodeActionInvoked CodeActionTriggerKind = 1 // line 13612 /* * Code actions were requested automatically. * * This typically happens when current selection in a file changes, but can * also be triggered when file content changes. */ - CodeActionAutomatic CodeActionTriggerKind = 2 // line 13644 + CodeActionAutomatic CodeActionTriggerKind = 2 // line 13617 // The kind of a completion entry. - TextCompletion CompletionItemKind = 1 // line 13167 - MethodCompletion CompletionItemKind = 2 // line 13171 - FunctionCompletion CompletionItemKind = 3 // line 13175 - ConstructorCompletion CompletionItemKind = 4 // line 13179 - FieldCompletion CompletionItemKind = 5 // line 13183 - VariableCompletion CompletionItemKind = 6 // line 13187 - ClassCompletion CompletionItemKind = 7 // line 13191 - InterfaceCompletion CompletionItemKind = 8 // line 13195 - ModuleCompletion CompletionItemKind = 9 // line 13199 - PropertyCompletion CompletionItemKind = 10 // line 13203 - UnitCompletion CompletionItemKind = 11 // line 13207 - ValueCompletion CompletionItemKind = 12 // line 13211 - EnumCompletion CompletionItemKind = 13 // line 13215 - KeywordCompletion CompletionItemKind = 14 // line 13219 - SnippetCompletion CompletionItemKind = 15 // line 13223 - ColorCompletion CompletionItemKind = 16 // line 13227 - FileCompletion CompletionItemKind = 17 // line 13231 - ReferenceCompletion CompletionItemKind = 18 // line 13235 - FolderCompletion CompletionItemKind = 19 // line 13239 - EnumMemberCompletion CompletionItemKind = 20 // line 13243 - ConstantCompletion CompletionItemKind = 21 // line 13247 - StructCompletion CompletionItemKind = 22 // line 13251 - EventCompletion CompletionItemKind = 23 // line 13255 - OperatorCompletion CompletionItemKind = 24 // line 13259 - TypeParameterCompletion CompletionItemKind = 25 // line 13263 + TextCompletion CompletionItemKind = 1 // line 13140 + MethodCompletion CompletionItemKind = 2 // line 13144 + FunctionCompletion CompletionItemKind = 3 // line 13148 + ConstructorCompletion CompletionItemKind = 4 // line 13152 + FieldCompletion CompletionItemKind = 5 // line 13156 + VariableCompletion CompletionItemKind = 6 // line 13160 + ClassCompletion CompletionItemKind = 7 // line 13164 + InterfaceCompletion CompletionItemKind = 8 // line 13168 + ModuleCompletion CompletionItemKind = 9 // line 13172 + PropertyCompletion CompletionItemKind = 10 // line 13176 + UnitCompletion CompletionItemKind = 11 // line 13180 + ValueCompletion CompletionItemKind = 12 // line 13184 + EnumCompletion CompletionItemKind = 13 // line 13188 + KeywordCompletion CompletionItemKind = 14 // line 13192 + SnippetCompletion CompletionItemKind = 15 // line 13196 + ColorCompletion CompletionItemKind = 16 // line 13200 + FileCompletion CompletionItemKind = 17 // line 13204 + ReferenceCompletion CompletionItemKind = 18 // line 13208 + FolderCompletion CompletionItemKind = 19 // line 13212 + EnumMemberCompletion CompletionItemKind = 20 // line 13216 + ConstantCompletion CompletionItemKind = 21 // line 13220 + StructCompletion CompletionItemKind = 22 // line 13224 + EventCompletion CompletionItemKind = 23 // line 13228 + OperatorCompletion CompletionItemKind = 24 // line 13232 + TypeParameterCompletion CompletionItemKind = 25 // line 13236 /* * Completion item tags are extra annotations that tweak the rendering of a completion * item. @@ -6139,29 +6131,29 @@ const ( * @since 3.15.0 */ // Render a completion as obsolete, usually using a strike-out. - ComplDeprecated CompletionItemTag = 1 // line 13277 + ComplDeprecated CompletionItemTag = 1 // line 13250 // How a completion was triggered /* * Completion was triggered by typing an identifier (24x7 code * complete), manual invocation (e.g Ctrl+Space) or via API. */ - Invoked CompletionTriggerKind = 1 // line 13588 + Invoked CompletionTriggerKind = 1 // line 13561 /* * Completion was triggered by a trigger character specified by * the `triggerCharacters` properties of the `CompletionRegistrationOptions`. */ - TriggerCharacter CompletionTriggerKind = 2 // line 13593 + TriggerCharacter CompletionTriggerKind = 2 // line 13566 // Completion was re-triggered as current completion list is incomplete - TriggerForIncompleteCompletions CompletionTriggerKind = 3 // line 13598 + TriggerForIncompleteCompletions CompletionTriggerKind = 3 // line 13571 // The diagnostic's severity. // Reports an error. - SeverityError DiagnosticSeverity = 1 // line 13537 + SeverityError DiagnosticSeverity = 1 // line 13510 // Reports a warning. - SeverityWarning DiagnosticSeverity = 2 // line 13542 + SeverityWarning DiagnosticSeverity = 2 // line 13515 // Reports an information. - SeverityInformation DiagnosticSeverity = 3 // line 13547 + SeverityInformation DiagnosticSeverity = 3 // line 13520 // Reports a hint. - SeverityHint DiagnosticSeverity = 4 // line 13552 + SeverityHint DiagnosticSeverity = 4 // line 13525 /* * The diagnostic tags. * @@ -6173,13 +6165,13 @@ const ( * Clients are allowed to render diagnostics with this tag faded out instead of having * an error squiggle. */ - Unnecessary DiagnosticTag = 1 // line 13567 + Unnecessary DiagnosticTag = 1 // line 13540 /* * Deprecated or obsolete code. * * Clients are allowed to rendered diagnostics with this tag strike through. */ - Deprecated DiagnosticTag = 2 // line 13572 + Deprecated DiagnosticTag = 2 // line 13545 /* * The document diagnostic report kinds. * @@ -6189,59 +6181,59 @@ const ( * A diagnostic report with a full * set of problems. */ - DiagnosticFull DocumentDiagnosticReportKind = "full" // line 12755 + DiagnosticFull DocumentDiagnosticReportKind = "full" // line 12728 /* * A report indicating that the last * returned report is still accurate. */ - DiagnosticUnchanged DocumentDiagnosticReportKind = "unchanged" // line 12760 + DiagnosticUnchanged DocumentDiagnosticReportKind = "unchanged" // line 12733 // A document highlight kind. // A textual occurrence. - Text DocumentHighlightKind = 1 // line 13334 + Text DocumentHighlightKind = 1 // line 13307 // Read-access of a symbol, like reading a variable. - Read DocumentHighlightKind = 2 // line 13339 + Read DocumentHighlightKind = 2 // line 13312 // Write-access of a symbol, like writing to a variable. - Write DocumentHighlightKind = 3 // line 13344 + Write DocumentHighlightKind = 3 // line 13317 // Predefined error codes. - ParseError ErrorCodes = -32700 // line 12776 - InvalidRequest ErrorCodes = -32600 // line 12780 - MethodNotFound ErrorCodes = -32601 // line 12784 - InvalidParams ErrorCodes = -32602 // line 12788 - InternalError ErrorCodes = -32603 // line 12792 + ParseError ErrorCodes = -32700 // line 12749 + InvalidRequest ErrorCodes = -32600 // line 12753 + MethodNotFound ErrorCodes = -32601 // line 12757 + InvalidParams ErrorCodes = -32602 // line 12761 + InternalError ErrorCodes = -32603 // line 12765 /* * Error code indicating that a server received a notification or * request before the server has received the `initialize` request. */ - ServerNotInitialized ErrorCodes = -32002 // line 12796 - UnknownErrorCode ErrorCodes = -32001 // line 12801 + ServerNotInitialized ErrorCodes = -32002 // line 12769 + UnknownErrorCode ErrorCodes = -32001 // line 12774 /* * Applying the workspace change is simply aborted if one of the changes provided * fails. All operations executed before the failing operation stay executed. */ - Abort FailureHandlingKind = "abort" // line 13726 + Abort FailureHandlingKind = "abort" // line 13699 /* * All operations are executed transactional. That means they either all * succeed or no changes at all are applied to the workspace. */ - Transactional FailureHandlingKind = "transactional" // line 13731 + Transactional FailureHandlingKind = "transactional" // line 13704 /* * If the workspace edit contains only textual file changes they are executed transactional. * If resource changes (create, rename or delete file) are part of the change the failure * handling strategy is abort. */ - TextOnlyTransactional FailureHandlingKind = "textOnlyTransactional" // line 13736 + TextOnlyTransactional FailureHandlingKind = "textOnlyTransactional" // line 13709 /* * The client tries to undo the operations already executed. But there is no * guarantee that this is succeeding. */ - Undo FailureHandlingKind = "undo" // line 13741 + Undo FailureHandlingKind = "undo" // line 13714 // The file event type // The file got created. - Created FileChangeType = 1 // line 13487 + Created FileChangeType = 1 // line 13460 // The file got changed. - Changed FileChangeType = 2 // line 13492 + Changed FileChangeType = 2 // line 13465 // The file got deleted. - Deleted FileChangeType = 3 // line 13497 + Deleted FileChangeType = 3 // line 13470 /* * A pattern kind describing if a glob pattern matches a file a folder or * both. @@ -6249,31 +6241,31 @@ const ( * @since 3.16.0 */ // The pattern matches a file only. - FilePattern FileOperationPatternKind = "file" // line 13660 + FilePattern FileOperationPatternKind = "file" // line 13633 // The pattern matches a folder only. - FolderPattern FileOperationPatternKind = "folder" // line 13665 + FolderPattern FileOperationPatternKind = "folder" // line 13638 // A set of predefined range kinds. // Folding range for a comment - Comment FoldingRangeKind = "comment" // line 12848 + Comment FoldingRangeKind = "comment" // line 12821 // Folding range for an import or include - Imports FoldingRangeKind = "imports" // line 12853 + Imports FoldingRangeKind = "imports" // line 12826 // Folding range for a region (e.g. `#region`) - Region FoldingRangeKind = "region" // line 12858 + Region FoldingRangeKind = "region" // line 12831 /* * Inlay hint kinds. * * @since 3.17.0 */ // An inlay hint that for a type annotation. - Type InlayHintKind = 1 // line 13066 + Type InlayHintKind = 1 // line 13039 // An inlay hint that is for a parameter. - Parameter InlayHintKind = 2 // line 13071 + Parameter InlayHintKind = 2 // line 13044 /* * Defines whether the insert text in a completion item should be interpreted as * plain text or a snippet. */ // The primary text to be inserted is treated as a plain string. - PlainTextTextFormat InsertTextFormat = 1 // line 13293 + PlainTextTextFormat InsertTextFormat = 1 // line 13266 /* * The primary text to be inserted is treated as a snippet. * @@ -6284,7 +6276,7 @@ const ( * * See also: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax */ - SnippetTextFormat InsertTextFormat = 2 // line 13298 + SnippetTextFormat InsertTextFormat = 2 // line 13271 /* * How whitespace and indentation is handled during completion * item insertion. @@ -6298,7 +6290,7 @@ const ( * The client will not apply any kind of adjustments to the * string. */ - AsIs InsertTextMode = 1 // line 13313 + AsIs InsertTextMode = 1 // line 13286 /* * The editor adjusts leading whitespace of new lines so that * they match the indentation up to the cursor of the line for @@ -6308,7 +6300,7 @@ const ( * multi line completion item is indented using 2 tabs and all * following lines inserted will be indented using 2 tabs as well. */ - AdjustIndentation InsertTextMode = 2 // line 13318 + AdjustIndentation InsertTextMode = 2 // line 13291 /* * A request failed but it was syntactically correct, e.g the * method name was known and the parameters were valid. The error @@ -6317,7 +6309,7 @@ const ( * * @since 3.17.0 */ - RequestFailed LSPErrorCodes = -32803 // line 12816 + RequestFailed LSPErrorCodes = -32803 // line 12789 /* * The server cancelled the request. This error code should * only be used for requests that explicitly support being @@ -6325,7 +6317,7 @@ const ( * * @since 3.17.0 */ - ServerCancelled LSPErrorCodes = -32802 // line 12822 + ServerCancelled LSPErrorCodes = -32802 // line 12795 /* * The server detected that the content of a document got * modified outside normal conditions. A server should @@ -6336,12 +6328,12 @@ const ( * If a client decides that a result is not of any use anymore * the client should cancel the request. */ - ContentModified LSPErrorCodes = -32801 // line 12828 + ContentModified LSPErrorCodes = -32801 // line 12801 /* * The client has canceled a request and a server as detected * the cancel. */ - RequestCancelled LSPErrorCodes = -32800 // line 12833 + RequestCancelled LSPErrorCodes = -32800 // line 12806 /* * Describes the content type that a client supports in various * result literals like `Hover`, `ParameterInfo` or `CompletionItem`. @@ -6350,55 +6342,55 @@ const ( * are reserved for internal usage. */ // Plain text is supported as a content format - PlainText MarkupKind = "plaintext" // line 13440 + PlainText MarkupKind = "plaintext" // line 13413 // Markdown is supported as a content format - Markdown MarkupKind = "markdown" // line 13445 + Markdown MarkupKind = "markdown" // line 13418 // The message type // An error message. - Error MessageType = 1 // line 13087 + Error MessageType = 1 // line 13060 // A warning message. - Warning MessageType = 2 // line 13092 + Warning MessageType = 2 // line 13065 // An information message. - Info MessageType = 3 // line 13097 + Info MessageType = 3 // line 13070 // A log message. - Log MessageType = 4 // line 13102 + Log MessageType = 4 // line 13075 /* * The moniker kind. * * @since 3.16.0 */ // The moniker represent a symbol that is imported into a project - Import MonikerKind = "import" // line 13040 + Import MonikerKind = "import" // line 13013 // The moniker represents a symbol that is exported from a project - Export MonikerKind = "export" // line 13045 + Export MonikerKind = "export" // line 13018 /* * The moniker represents a symbol that is local to a project (e.g. a local * variable of a function, a class not visible outside the project, ...) */ - Local MonikerKind = "local" // line 13050 + Local MonikerKind = "local" // line 13023 /* * A notebook cell kind. * * @since 3.17.0 */ // A markup-cell is formatted source that is used for display. - Markup NotebookCellKind = 1 // line 13681 + Markup NotebookCellKind = 1 // line 13654 // A code-cell is source code. - Code NotebookCellKind = 2 // line 13686 + Code NotebookCellKind = 2 // line 13659 /* * A set of predefined position encoding kinds. * * @since 3.17.0 */ // Character offsets count UTF-8 code units. - UTF8 PositionEncodingKind = "utf-8" // line 13460 + UTF8 PositionEncodingKind = "utf-8" // line 13433 /* * Character offsets count UTF-16 code units. * * This is the default and must always be supported * by servers */ - UTF16 PositionEncodingKind = "utf-16" // line 13465 + UTF16 PositionEncodingKind = "utf-16" // line 13438 /* * Character offsets count UTF-32 code units. * @@ -6406,18 +6398,18 @@ const ( * so this `PositionEncodingKind` may also be used for an * encoding-agnostic representation of character offsets. */ - UTF32 PositionEncodingKind = "utf-32" // line 13470 + UTF32 PositionEncodingKind = "utf-32" // line 13443 /* * The client's default behavior is to select the identifier * according the to language's syntax rule. */ - Identifier PrepareSupportDefaultBehavior = 1 // line 13755 + Identifier PrepareSupportDefaultBehavior = 1 // line 13728 // Supports creating new files and folders. - Create ResourceOperationKind = "create" // line 13702 + Create ResourceOperationKind = "create" // line 13675 // Supports renaming existing files and folders. - Rename ResourceOperationKind = "rename" // line 13707 + Rename ResourceOperationKind = "rename" // line 13680 // Supports deleting existing files and folders. - Delete ResourceOperationKind = "delete" // line 13712 + Delete ResourceOperationKind = "delete" // line 13685 /* * A set of predefined token modifiers. This set is not fixed * an clients can specify additional token types via the @@ -6425,16 +6417,16 @@ const ( * * @since 3.16.0 */ - ModDeclaration SemanticTokenModifiers = "declaration" // line 12703 - ModDefinition SemanticTokenModifiers = "definition" // line 12707 - ModReadonly SemanticTokenModifiers = "readonly" // line 12711 - ModStatic SemanticTokenModifiers = "static" // line 12715 - ModDeprecated SemanticTokenModifiers = "deprecated" // line 12719 - ModAbstract SemanticTokenModifiers = "abstract" // line 12723 - ModAsync SemanticTokenModifiers = "async" // line 12727 - ModModification SemanticTokenModifiers = "modification" // line 12731 - ModDocumentation SemanticTokenModifiers = "documentation" // line 12735 - ModDefaultLibrary SemanticTokenModifiers = "defaultLibrary" // line 12739 + ModDeclaration SemanticTokenModifiers = "declaration" // line 12676 + ModDefinition SemanticTokenModifiers = "definition" // line 12680 + ModReadonly SemanticTokenModifiers = "readonly" // line 12684 + ModStatic SemanticTokenModifiers = "static" // line 12688 + ModDeprecated SemanticTokenModifiers = "deprecated" // line 12692 + ModAbstract SemanticTokenModifiers = "abstract" // line 12696 + ModAsync SemanticTokenModifiers = "async" // line 12700 + ModModification SemanticTokenModifiers = "modification" // line 12704 + ModDocumentation SemanticTokenModifiers = "documentation" // line 12708 + ModDefaultLibrary SemanticTokenModifiers = "defaultLibrary" // line 12712 /* * A set of predefined token types. This set is not fixed * an clients can specify additional token types via the @@ -6442,132 +6434,132 @@ const ( * * @since 3.16.0 */ - NamespaceType SemanticTokenTypes = "namespace" // line 12596 + NamespaceType SemanticTokenTypes = "namespace" // line 12569 /* * Represents a generic type. Acts as a fallback for types which can't be mapped to * a specific type like class or enum. */ - TypeType SemanticTokenTypes = "type" // line 12600 - ClassType SemanticTokenTypes = "class" // line 12605 - EnumType SemanticTokenTypes = "enum" // line 12609 - InterfaceType SemanticTokenTypes = "interface" // line 12613 - StructType SemanticTokenTypes = "struct" // line 12617 - TypeParameterType SemanticTokenTypes = "typeParameter" // line 12621 - ParameterType SemanticTokenTypes = "parameter" // line 12625 - VariableType SemanticTokenTypes = "variable" // line 12629 - PropertyType SemanticTokenTypes = "property" // line 12633 - EnumMemberType SemanticTokenTypes = "enumMember" // line 12637 - EventType SemanticTokenTypes = "event" // line 12641 - FunctionType SemanticTokenTypes = "function" // line 12645 - MethodType SemanticTokenTypes = "method" // line 12649 - MacroType SemanticTokenTypes = "macro" // line 12653 - KeywordType SemanticTokenTypes = "keyword" // line 12657 - ModifierType SemanticTokenTypes = "modifier" // line 12661 - CommentType SemanticTokenTypes = "comment" // line 12665 - StringType SemanticTokenTypes = "string" // line 12669 - NumberType SemanticTokenTypes = "number" // line 12673 - RegexpType SemanticTokenTypes = "regexp" // line 12677 - OperatorType SemanticTokenTypes = "operator" // line 12681 + TypeType SemanticTokenTypes = "type" // line 12573 + ClassType SemanticTokenTypes = "class" // line 12578 + EnumType SemanticTokenTypes = "enum" // line 12582 + InterfaceType SemanticTokenTypes = "interface" // line 12586 + StructType SemanticTokenTypes = "struct" // line 12590 + TypeParameterType SemanticTokenTypes = "typeParameter" // line 12594 + ParameterType SemanticTokenTypes = "parameter" // line 12598 + VariableType SemanticTokenTypes = "variable" // line 12602 + PropertyType SemanticTokenTypes = "property" // line 12606 + EnumMemberType SemanticTokenTypes = "enumMember" // line 12610 + EventType SemanticTokenTypes = "event" // line 12614 + FunctionType SemanticTokenTypes = "function" // line 12618 + MethodType SemanticTokenTypes = "method" // line 12622 + MacroType SemanticTokenTypes = "macro" // line 12626 + KeywordType SemanticTokenTypes = "keyword" // line 12630 + ModifierType SemanticTokenTypes = "modifier" // line 12634 + CommentType SemanticTokenTypes = "comment" // line 12638 + StringType SemanticTokenTypes = "string" // line 12642 + NumberType SemanticTokenTypes = "number" // line 12646 + RegexpType SemanticTokenTypes = "regexp" // line 12650 + OperatorType SemanticTokenTypes = "operator" // line 12654 // @since 3.17.0 - DecoratorType SemanticTokenTypes = "decorator" // line 12685 + DecoratorType SemanticTokenTypes = "decorator" // line 12658 /* * How a signature help was triggered. * * @since 3.15.0 */ // Signature help was invoked manually by the user or by a command. - SigInvoked SignatureHelpTriggerKind = 1 // line 13613 + SigInvoked SignatureHelpTriggerKind = 1 // line 13586 // Signature help was triggered by a trigger character. - SigTriggerCharacter SignatureHelpTriggerKind = 2 // line 13618 + SigTriggerCharacter SignatureHelpTriggerKind = 2 // line 13591 // Signature help was triggered by the cursor moving or by the document content changing. - SigContentChange SignatureHelpTriggerKind = 3 // line 13623 + SigContentChange SignatureHelpTriggerKind = 3 // line 13596 // A symbol kind. - File SymbolKind = 1 // line 12874 - Module SymbolKind = 2 // line 12878 - Namespace SymbolKind = 3 // line 12882 - Package SymbolKind = 4 // line 12886 - Class SymbolKind = 5 // line 12890 - Method SymbolKind = 6 // line 12894 - Property SymbolKind = 7 // line 12898 - Field SymbolKind = 8 // line 12902 - Constructor SymbolKind = 9 // line 12906 - Enum SymbolKind = 10 // line 12910 - Interface SymbolKind = 11 // line 12914 - Function SymbolKind = 12 // line 12918 - Variable SymbolKind = 13 // line 12922 - Constant SymbolKind = 14 // line 12926 - String SymbolKind = 15 // line 12930 - Number SymbolKind = 16 // line 12934 - Boolean SymbolKind = 17 // line 12938 - Array SymbolKind = 18 // line 12942 - Object SymbolKind = 19 // line 12946 - Key SymbolKind = 20 // line 12950 - Null SymbolKind = 21 // line 12954 - EnumMember SymbolKind = 22 // line 12958 - Struct SymbolKind = 23 // line 12962 - Event SymbolKind = 24 // line 12966 - Operator SymbolKind = 25 // line 12970 - TypeParameter SymbolKind = 26 // line 12974 + File SymbolKind = 1 // line 12847 + Module SymbolKind = 2 // line 12851 + Namespace SymbolKind = 3 // line 12855 + Package SymbolKind = 4 // line 12859 + Class SymbolKind = 5 // line 12863 + Method SymbolKind = 6 // line 12867 + Property SymbolKind = 7 // line 12871 + Field SymbolKind = 8 // line 12875 + Constructor SymbolKind = 9 // line 12879 + Enum SymbolKind = 10 // line 12883 + Interface SymbolKind = 11 // line 12887 + Function SymbolKind = 12 // line 12891 + Variable SymbolKind = 13 // line 12895 + Constant SymbolKind = 14 // line 12899 + String SymbolKind = 15 // line 12903 + Number SymbolKind = 16 // line 12907 + Boolean SymbolKind = 17 // line 12911 + Array SymbolKind = 18 // line 12915 + Object SymbolKind = 19 // line 12919 + Key SymbolKind = 20 // line 12923 + Null SymbolKind = 21 // line 12927 + EnumMember SymbolKind = 22 // line 12931 + Struct SymbolKind = 23 // line 12935 + Event SymbolKind = 24 // line 12939 + Operator SymbolKind = 25 // line 12943 + TypeParameter SymbolKind = 26 // line 12947 /* * Symbol tags are extra annotations that tweak the rendering of a symbol. * * @since 3.16 */ // Render a symbol as obsolete, usually using a strike-out. - DeprecatedSymbol SymbolTag = 1 // line 12988 + DeprecatedSymbol SymbolTag = 1 // line 12961 // Represents reasons why a text document is saved. /* * Manually triggered, e.g. by the user pressing save, by starting debugging, * or by an API call. */ - Manual TextDocumentSaveReason = 1 // line 13142 + Manual TextDocumentSaveReason = 1 // line 13115 // Automatic after a delay. - AfterDelay TextDocumentSaveReason = 2 // line 13147 + AfterDelay TextDocumentSaveReason = 2 // line 13120 // When the editor lost focus. - FocusOut TextDocumentSaveReason = 3 // line 13152 + FocusOut TextDocumentSaveReason = 3 // line 13125 /* * Defines how the host (editor) should sync * document changes to the language server. */ // Documents should not be synced at all. - None TextDocumentSyncKind = 0 // line 13117 + None TextDocumentSyncKind = 0 // line 13090 /* * Documents are synced by always sending the full content * of the document. */ - Full TextDocumentSyncKind = 1 // line 13122 + Full TextDocumentSyncKind = 1 // line 13095 /* * Documents are synced by sending the full content on open. * After that only incremental updates to the document are * send. */ - Incremental TextDocumentSyncKind = 2 // line 13127 - Relative TokenFormat = "relative" // line 13769 + Incremental TextDocumentSyncKind = 2 // line 13100 + Relative TokenFormat = "relative" // line 13742 // Turn tracing off. - Off TraceValues = "off" // line 13416 + Off TraceValues = "off" // line 13389 // Trace messages only. - Messages TraceValues = "messages" // line 13421 + Messages TraceValues = "messages" // line 13394 // Verbose message tracing. - Verbose TraceValues = "verbose" // line 13426 + Verbose TraceValues = "verbose" // line 13399 /* * Moniker uniqueness level to define scope of the moniker. * * @since 3.16.0 */ // The moniker is only unique inside a document - Document UniquenessLevel = "document" // line 13004 + Document UniquenessLevel = "document" // line 12977 // The moniker is unique inside a project for which a dump got created - Project UniquenessLevel = "project" // line 13009 + Project UniquenessLevel = "project" // line 12982 // The moniker is unique inside the group to which a project belongs - Group UniquenessLevel = "group" // line 13014 + Group UniquenessLevel = "group" // line 12987 // The moniker is unique inside the moniker scheme. - Scheme UniquenessLevel = "scheme" // line 13019 + Scheme UniquenessLevel = "scheme" // line 12992 // The moniker is globally unique - Global UniquenessLevel = "global" // line 13024 + Global UniquenessLevel = "global" // line 12997 // Interested in create events. - WatchCreate WatchKind = 1 // line 13512 + WatchCreate WatchKind = 1 // line 13485 // Interested in change events - WatchChange WatchKind = 2 // line 13517 + WatchChange WatchKind = 2 // line 13490 // Interested in delete events - WatchDelete WatchKind = 4 // line 13522 + WatchDelete WatchKind = 4 // line 13495 ) diff --git a/gopls/internal/lsp/protocol/tsserver.go b/gopls/internal/lsp/protocol/tsserver.go index d96801080aa..8669a4e798a 100644 --- a/gopls/internal/lsp/protocol/tsserver.go +++ b/gopls/internal/lsp/protocol/tsserver.go @@ -7,7 +7,7 @@ package protocol // Code generated from version 3.17.0 of protocol/metaModel.json. -// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of 2023-01-23) +// git hash 9b742021fb04ad081aa3676a9eecf4fa612084b4 (as of 2023-01-30) import ( "context" @@ -76,7 +76,6 @@ type Server interface { Supertypes(context.Context, *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) // typeHierarchy/supertypes WorkDoneProgressCancel(context.Context, *WorkDoneProgressCancelParams) error // window/workDoneProgress/cancel DiagnosticWorkspace(context.Context, *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) // workspace/diagnostic - DiagnosticRefresh(context.Context) error // workspace/diagnostic/refresh DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error // workspace/didChangeConfiguration DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error // workspace/didChangeWatchedFiles DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error // workspace/didChangeWorkspaceFolders @@ -84,9 +83,6 @@ type Server interface { DidDeleteFiles(context.Context, *DeleteFilesParams) error // workspace/didDeleteFiles DidRenameFiles(context.Context, *RenameFilesParams) error // workspace/didRenameFiles ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{}, error) // workspace/executeCommand - InlayHintRefresh(context.Context) error // workspace/inlayHint/refresh - InlineValueRefresh(context.Context) error // workspace/inlineValue/refresh - SemanticTokensRefresh(context.Context) error // workspace/semanticTokens/refresh Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error) // workspace/symbol WillCreateFiles(context.Context, *CreateFilesParams) (*WorkspaceEdit, error) // workspace/willCreateFiles WillDeleteFiles(context.Context, *DeleteFilesParams) (*WorkspaceEdit, error) // workspace/willDeleteFiles @@ -634,9 +630,6 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, return true, reply(ctx, nil, err) } return true, reply(ctx, resp, nil) - case "workspace/diagnostic/refresh": - err := server.DiagnosticRefresh(ctx) - return true, reply(ctx, nil, err) case "workspace/didChangeConfiguration": var params DidChangeConfigurationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -689,15 +682,6 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, return true, reply(ctx, nil, err) } return true, reply(ctx, resp, nil) - case "workspace/inlayHint/refresh": - err := server.InlayHintRefresh(ctx) - return true, reply(ctx, nil, err) - case "workspace/inlineValue/refresh": - err := server.InlineValueRefresh(ctx) - return true, reply(ctx, nil, err) - case "workspace/semanticTokens/refresh": - err := server.SemanticTokensRefresh(ctx) - return true, reply(ctx, nil, err) case "workspace/symbol": var params WorkspaceSymbolParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { @@ -1106,9 +1090,6 @@ func (s *serverDispatcher) DiagnosticWorkspace(ctx context.Context, params *Work } return result, nil } -func (s *serverDispatcher) DiagnosticRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/diagnostic/refresh", nil, nil) -} func (s *serverDispatcher) DidChangeConfiguration(ctx context.Context, params *DidChangeConfigurationParams) error { return s.sender.Notify(ctx, "workspace/didChangeConfiguration", params) } @@ -1134,15 +1115,6 @@ func (s *serverDispatcher) ExecuteCommand(ctx context.Context, params *ExecuteCo } return result, nil } -func (s *serverDispatcher) InlayHintRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/inlayHint/refresh", nil, nil) -} -func (s *serverDispatcher) InlineValueRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/inlineValue/refresh", nil, nil) -} -func (s *serverDispatcher) SemanticTokensRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/semanticTokens/refresh", nil, nil) -} func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) { var result []SymbolInformation if err := s.sender.Call(ctx, "workspace/symbol", params, &result); err != nil { diff --git a/gopls/internal/lsp/server_gen.go b/gopls/internal/lsp/server_gen.go index c6f618d6ef4..2c6e9954d23 100644 --- a/gopls/internal/lsp/server_gen.go +++ b/gopls/internal/lsp/server_gen.go @@ -40,10 +40,6 @@ func (s *Server) Diagnostic(context.Context, *string) (*string, error) { return nil, notImplemented("Diagnostic") } -func (s *Server) DiagnosticRefresh(context.Context) error { - return notImplemented("DiagnosticRefresh") -} - func (s *Server) DiagnosticWorkspace(context.Context, *protocol.WorkspaceDiagnosticParams) (*protocol.WorkspaceDiagnosticReport, error) { return nil, notImplemented("DiagnosticWorkspace") } @@ -160,18 +156,10 @@ func (s *Server) InlayHint(ctx context.Context, params *protocol.InlayHintParams return s.inlayHint(ctx, params) } -func (s *Server) InlayHintRefresh(context.Context) error { - return notImplemented("InlayHintRefresh") -} - func (s *Server) InlineValue(context.Context, *protocol.InlineValueParams) ([]protocol.InlineValue, error) { return nil, notImplemented("InlineValue") } -func (s *Server) InlineValueRefresh(context.Context) error { - return notImplemented("InlineValueRefresh") -} - func (s *Server) LinkedEditingRange(context.Context, *protocol.LinkedEditingRangeParams) (*protocol.LinkedEditingRanges, error) { return nil, notImplemented("LinkedEditingRange") } @@ -260,10 +248,6 @@ func (s *Server) SemanticTokensRange(ctx context.Context, p *protocol.SemanticTo return s.semanticTokensRange(ctx, p) } -func (s *Server) SemanticTokensRefresh(ctx context.Context) error { - return s.semanticTokensRefresh(ctx) -} - func (s *Server) SetTrace(context.Context, *protocol.SetTraceParams) error { return notImplemented("SetTrace") } From bd5dfbb418a5bf32b879b69ebe164771b55db1f3 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Tue, 31 Jan 2023 11:07:02 +0800 Subject: [PATCH 697/723] all: fix some comments Change-Id: I44a562ec6d71dcf638333a855083f46201ef9a5e Reviewed-on: https://go-review.googlesource.com/c/tools/+/464236 TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Ian Lance Taylor Reviewed-by: Ian Lance Taylor Run-TryBot: Ian Lance Taylor Reviewed-by: Michael Knyszek --- cmd/fiximports/main.go | 2 +- cmd/guru/guru.go | 4 ++-- cmd/guru/referrers.go | 2 +- cmd/signature-fuzzer/fuzz-runner/runner.go | 2 +- go/analysis/internal/checker/checker.go | 2 +- go/internal/gccgoimporter/parser.go | 2 +- go/pointer/api.go | 2 +- go/pointer/gen.go | 2 +- go/ssa/instantiate.go | 2 +- go/ssa/util.go | 2 +- godoc/index.go | 2 +- godoc/meta.go | 4 ++-- gopls/internal/lsp/cache/load.go | 2 +- gopls/internal/lsp/cache/mod.go | 2 +- gopls/internal/lsp/cmd/cmd.go | 2 +- gopls/internal/lsp/debug/serve.go | 8 ++++---- gopls/internal/lsp/fake/editor.go | 2 +- gopls/internal/lsp/progress/progress.go | 6 +++--- gopls/internal/lsp/protocol/mapper.go | 4 ++-- gopls/internal/lsp/source/completion/snippet.go | 4 ++-- gopls/internal/lsp/source/rename_check.go | 2 +- gopls/internal/lsp/source/view.go | 2 +- internal/gcimporter/iexport.go | 2 +- internal/pkgbits/decoder.go | 2 +- internal/pkgbits/encoder.go | 2 +- refactor/rename/check.go | 2 +- 26 files changed, 35 insertions(+), 35 deletions(-) diff --git a/cmd/fiximports/main.go b/cmd/fiximports/main.go index 515d4a6ca92..8eeacd1eda3 100644 --- a/cmd/fiximports/main.go +++ b/cmd/fiximports/main.go @@ -389,7 +389,7 @@ func rewritePackage(client *listPackage, canonical map[string]canonicalName) boo return ok } -// rewrite reads, modifies, and writes filename, replacing all imports +// rewriteFile reads, modifies, and writes filename, replacing all imports // of packages P in canonical by canonical[P]. // It records in used which canonical packages were imported. // used[P]=="" indicates that P was imported but its canonical path is unknown. diff --git a/cmd/guru/guru.go b/cmd/guru/guru.go index 18bb0840a1e..7a42aaa3ae3 100644 --- a/cmd/guru/guru.go +++ b/cmd/guru/guru.go @@ -55,12 +55,12 @@ type queryPos struct { info *loader.PackageInfo // type info for the queried package (nil for fastQueryPos) } -// TypeString prints type T relative to the query position. +// typeString prints type T relative to the query position. func (qpos *queryPos) typeString(T types.Type) string { return types.TypeString(T, types.RelativeTo(qpos.info.Pkg)) } -// ObjectString prints object obj relative to the query position. +// objectString prints object obj relative to the query position. func (qpos *queryPos) objectString(obj types.Object) string { return types.ObjectString(obj, types.RelativeTo(qpos.info.Pkg)) } diff --git a/cmd/guru/referrers.go b/cmd/guru/referrers.go index 78c8ef91a8c..d75196bf93a 100644 --- a/cmd/guru/referrers.go +++ b/cmd/guru/referrers.go @@ -703,7 +703,7 @@ type referrersPackageResult struct { refs []*ast.Ident // set of all other references to it } -// forEachRef calls f(id, text) for id in r.refs, in order. +// foreachRef calls f(id, text) for id in r.refs, in order. // Text is the text of the line on which id appears. func (r *referrersPackageResult) foreachRef(f func(id *ast.Ident, text string)) { // Show referring lines, like grep. diff --git a/cmd/signature-fuzzer/fuzz-runner/runner.go b/cmd/signature-fuzzer/fuzz-runner/runner.go index 4e5b413f3ff..b77b218f5a8 100644 --- a/cmd/signature-fuzzer/fuzz-runner/runner.go +++ b/cmd/signature-fuzzer/fuzz-runner/runner.go @@ -107,7 +107,7 @@ func docmd(cmd []string, dir string) int { return st } -// docodmout forks and execs command 'cmd' in dir 'dir', redirecting +// docmdout forks and execs command 'cmd' in dir 'dir', redirecting // stderr and stdout from the execution to file 'outfile'. func docmdout(cmd []string, dir string, outfile string) int { of, err := os.OpenFile(outfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index cf76bdebe71..530d23d38c6 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -929,7 +929,7 @@ func factType(fact analysis.Fact) reflect.Type { return t } -// allObjectFacts implements Pass.AllObjectFacts. +// allPackageFacts implements Pass.AllPackageFacts. func (act *action) allPackageFacts() []analysis.PackageFact { facts := make([]analysis.PackageFact, 0, len(act.packageFacts)) for k := range act.packageFacts { diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go index 5c24845831a..9fdb6f8b059 100644 --- a/go/internal/gccgoimporter/parser.go +++ b/go/internal/gccgoimporter/parser.go @@ -1167,7 +1167,7 @@ func (p *parser) maybeCreatePackage() { } } -// parseInitDateDirective parses an InitDataDirective: +// parseInitDataDirective parses an InitDataDirective: // // InitDataDirective = ( "v1" | "v2" | "v3" ) ";" | // "priority" int ";" | diff --git a/go/pointer/api.go b/go/pointer/api.go index 64de1100351..8c9a8c7752b 100644 --- a/go/pointer/api.go +++ b/go/pointer/api.go @@ -97,7 +97,7 @@ func (c *Config) AddQuery(v ssa.Value) { c.Queries[v] = struct{}{} } -// AddQuery adds v to Config.IndirectQueries. +// AddIndirectQuery adds v to Config.IndirectQueries. // Precondition: CanPoint(v.Type().Underlying().(*types.Pointer).Elem()). func (c *Config) AddIndirectQuery(v ssa.Value) { if c.IndirectQueries == nil { diff --git a/go/pointer/gen.go b/go/pointer/gen.go index bee656b6237..5e527f21ab2 100644 --- a/go/pointer/gen.go +++ b/go/pointer/gen.go @@ -206,7 +206,7 @@ func (a *analysis) makeRtype(T types.Type) nodeid { return id } -// rtypeValue returns the type of the *reflect.rtype-tagged object obj. +// rtypeTaggedValue returns the type of the *reflect.rtype-tagged object obj. func (a *analysis) rtypeTaggedValue(obj nodeid) types.Type { tDyn, t, _ := a.taggedValue(obj) if tDyn != a.reflectRtypePtr { diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go index f73594bb41b..f6b2533f24b 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go @@ -12,7 +12,7 @@ import ( "golang.org/x/tools/internal/typeparams" ) -// Instances returns all of the instances generated by runtime types for this function in an unspecified order. +// _Instances returns all of the instances generated by runtime types for this function in an unspecified order. // // Thread-safe. // diff --git a/go/ssa/util.go b/go/ssa/util.go index 6b0aada9e8a..aef57dc3ffb 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -83,7 +83,7 @@ func isRuneSlice(t types.Type) bool { return false } -// isBasicConvType returns true when a type set can be +// isBasicConvTypes returns true when a type set can be // one side of a Convert operation. This is when: // - All are basic, []byte, or []rune. // - At least 1 is basic. diff --git a/godoc/index.go b/godoc/index.go index 4dc3362a7e2..4471f59167a 100644 --- a/godoc/index.go +++ b/godoc/index.go @@ -1422,7 +1422,7 @@ func (x *Index) LookupRegexp(r *regexp.Regexp, n int) (found int, result []FileL return } -// InvalidateIndex should be called whenever any of the file systems +// invalidateIndex should be called whenever any of the file systems // under godoc's observation change so that the indexer is kicked on. func (c *Corpus) invalidateIndex() { c.fsModified.Set(nil) diff --git a/godoc/meta.go b/godoc/meta.go index 751b72e7161..76a27508b68 100644 --- a/godoc/meta.go +++ b/godoc/meta.go @@ -60,7 +60,7 @@ func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) { return } -// UpdateMetadata scans $GOROOT/doc for HTML and Markdown files, reads their metadata, +// updateMetadata scans $GOROOT/doc for HTML and Markdown files, reads their metadata, // and updates the DocMetadata map. func (c *Corpus) updateMetadata() { metadata := make(map[string]*Metadata) @@ -147,7 +147,7 @@ func (c *Corpus) refreshMetadata() { } } -// RefreshMetadataLoop runs forever, updating DocMetadata when the underlying +// refreshMetadataLoop runs forever, updating DocMetadata when the underlying // file system changes. It should be launched in a goroutine. func (c *Corpus) refreshMetadataLoop() { for { diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 9b321cec574..98a8fece431 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -274,7 +274,7 @@ func (m *moduleErrorMap) Error() string { return buf.String() } -// workspaceLayoutErrors returns an error decribing a misconfiguration of the +// workspaceLayoutError returns an error describing a misconfiguration of the // workspace, along with related diagnostic. // // The unusual argument ordering of results is intentional: if the resulting diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go index 9244f698757..4a3d8db1b80 100644 --- a/gopls/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -285,7 +285,7 @@ func modWhyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle) ( return why, nil } -// extractGoCommandError tries to parse errors that come from the go command +// extractGoCommandErrors tries to parse errors that come from the go command // and shape them into go.mod diagnostics. // TODO: rename this to 'load errors' func (s *snapshot) extractGoCommandErrors(ctx context.Context, goCmdError error) []*source.Diagnostic { diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index 96d43d34662..74725f70ee5 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -229,7 +229,7 @@ func (app *Application) Run(ctx context.Context, args ...string) error { return tool.CommandLineErrorf("Unknown command %v", command) } -// commands returns the set of commands supported by the gopls tool on the +// Commands returns the set of commands supported by the gopls tool on the // command line. // The command is specified by the first non flag argument. func (app *Application) Commands() []tool.Application { diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go index 3e6a905197d..e79e88c3f6d 100644 --- a/gopls/internal/lsp/debug/serve.go +++ b/gopls/internal/lsp/debug/serve.go @@ -192,14 +192,14 @@ type Server struct { ClientID string } -// AddClient adds a client to the set being served. +// addClient adds a client to the set being served. func (st *State) addClient(session *cache.Session) { st.mu.Lock() defer st.mu.Unlock() st.clients = append(st.clients, &Client{Session: session}) } -// DropClient removes a client from the set being served. +// dropClient removes a client from the set being served. func (st *State) dropClient(session *cache.Session) { st.mu.Lock() defer st.mu.Unlock() @@ -213,7 +213,7 @@ func (st *State) dropClient(session *cache.Session) { } } -// AddServer adds a server to the set being queried. In practice, there should +// updateServer updates a server to the set being queried. In practice, there should // be at most one remote server. func (st *State) updateServer(server *Server) { st.mu.Lock() @@ -232,7 +232,7 @@ func (st *State) updateServer(server *Server) { st.servers = append(st.servers, server) } -// DropServer drops a server from the set being queried. +// dropServer drops a server from the set being queried. func (st *State) dropServer(id string) { st.mu.Lock() defer st.mu.Unlock() diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 08074400e55..1e5b0790539 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -793,7 +793,7 @@ func (e *Editor) GoToTypeDefinition(ctx context.Context, loc protocol.Location) return e.extractFirstLocation(ctx, resp) } -// extractFirstPathAndPos returns the first location. +// extractFirstLocation returns the first location. // It opens the file if needed. func (e *Editor) extractFirstLocation(ctx context.Context, locs []protocol.Location) (protocol.Location, error) { if len(locs) == 0 { diff --git a/gopls/internal/lsp/progress/progress.go b/gopls/internal/lsp/progress/progress.go index 31a8cb67d1d..32ac91186a9 100644 --- a/gopls/internal/lsp/progress/progress.go +++ b/gopls/internal/lsp/progress/progress.go @@ -12,9 +12,9 @@ import ( "strings" "sync" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/xcontext" ) @@ -167,7 +167,7 @@ func (wd *WorkDone) doCancel() { } } -// report reports an update on WorkDone report back to the client. +// Report reports an update on WorkDone report back to the client. func (wd *WorkDone) Report(ctx context.Context, message string, percentage float64) { ctx = xcontext.Detach(ctx) // progress messages should not be cancelled if wd == nil { @@ -202,7 +202,7 @@ func (wd *WorkDone) Report(ctx context.Context, message string, percentage float } } -// end reports a workdone completion back to the client. +// End reports a workdone completion back to the client. func (wd *WorkDone) End(ctx context.Context, message string) { ctx = xcontext.Detach(ctx) // progress messages should not be cancelled if wd == nil { diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go index 6a93c39273f..a0f11722e53 100644 --- a/gopls/internal/lsp/protocol/mapper.go +++ b/gopls/internal/lsp/protocol/mapper.go @@ -419,7 +419,7 @@ func (m *Mapper) PosPosition(tf *token.File, pos token.Pos) (Position, error) { return m.OffsetPosition(offset) } -// PosPosition converts a token range to a protocol (UTF-16) location. +// PosLocation converts a token range to a protocol (UTF-16) location. func (m *Mapper) PosLocation(tf *token.File, start, end token.Pos) (Location, error) { startOffset, endOffset, err := safetoken.Offsets(tf, start, end) if err != nil { @@ -432,7 +432,7 @@ func (m *Mapper) PosLocation(tf *token.File, start, end token.Pos) (Location, er return m.RangeLocation(rng), nil } -// PosPosition converts a token range to a protocol (UTF-16) range. +// PosRange converts a token range to a protocol (UTF-16) range. func (m *Mapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { startOffset, endOffset, err := safetoken.Offsets(tf, start, end) if err != nil { diff --git a/gopls/internal/lsp/source/completion/snippet.go b/gopls/internal/lsp/source/completion/snippet.go index 53577232939..f4ea767e9dc 100644 --- a/gopls/internal/lsp/source/completion/snippet.go +++ b/gopls/internal/lsp/source/completion/snippet.go @@ -11,7 +11,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/snippet" ) -// structFieldSnippets calculates the snippet for struct literal field names. +// structFieldSnippet calculates the snippet for struct literal field names. func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snippet.Builder) { if !c.wantStructFieldCompletions() { return @@ -49,7 +49,7 @@ func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snip } } -// functionCallSnippets calculates the snippet for function calls. +// functionCallSnippet calculates the snippet for function calls. func (c *completer) functionCallSnippet(name string, tparams, params []string, snip *snippet.Builder) { // If there is no suffix then we need to reuse existing call parens // "()" if present. If there is an identifier suffix then we always diff --git a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go index 5495e6a2259..d01d2289296 100644 --- a/gopls/internal/lsp/source/rename_check.go +++ b/gopls/internal/lsp/source/rename_check.go @@ -441,7 +441,7 @@ func (r *renamer) checkStructField(from *types.Var) { r.checkSelections(from) } -// checkSelection checks that all uses and selections that resolve to +// checkSelections checks that all uses and selections that resolve to // the specified object would continue to do so after the renaming. func (r *renamer) checkSelections(from types.Object) { for typ, pkg := range r.packages { diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index c581286edca..2227c5cd5e8 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -419,7 +419,7 @@ func (pgf *ParsedGoFile) NodeLocation(node ast.Node) (protocol.Location, error) return pgf.Mapper.PosLocation(pgf.Tok, node.Pos(), node.End()) } -// RangeToSpanRange parses a protocol Range back into the go/token domain. +// RangeToTokenRange parses a protocol Range back into the go/token domain. func (pgf *ParsedGoFile) RangeToTokenRange(r protocol.Range) (safetoken.Range, error) { start, end, err := pgf.Mapper.RangeOffsets(r) if err != nil { diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 5e01184ae72..8bd51eccf16 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -369,7 +369,7 @@ func (p *iexporter) stringOff(s string) uint64 { return off } -// fileIndex returns the index of the token.File and the byte offset of pos within it. +// fileIndexAndOffset returns the index of the token.File and the byte offset of pos within it. func (p *iexporter) fileIndexAndOffset(file *token.File, pos token.Pos) (uint64, uint64) { index, ok := p.fileInfo[file] if !ok { diff --git a/internal/pkgbits/decoder.go b/internal/pkgbits/decoder.go index 3205c9a16c5..b92e8e6eb32 100644 --- a/internal/pkgbits/decoder.go +++ b/internal/pkgbits/decoder.go @@ -373,7 +373,7 @@ func (r *Decoder) Int64() int64 { return r.rawVarint() } -// Int64 decodes and returns a uint64 value from the element bitstream. +// Uint64 decodes and returns a uint64 value from the element bitstream. func (r *Decoder) Uint64() uint64 { r.Sync(SyncUint64) return r.rawUvarint() diff --git a/internal/pkgbits/encoder.go b/internal/pkgbits/encoder.go index e98e41171e1..6482617a4fc 100644 --- a/internal/pkgbits/encoder.go +++ b/internal/pkgbits/encoder.go @@ -293,7 +293,7 @@ func (w *Encoder) Len(x int) { assert(x >= 0); w.Uint64(uint64(x)) } // Int encodes and writes an int value into the element bitstream. func (w *Encoder) Int(x int) { w.Int64(int64(x)) } -// Len encodes and writes a uint value into the element bitstream. +// Uint encodes and writes a uint value into the element bitstream. func (w *Encoder) Uint(x uint) { w.Uint64(uint64(x)) } // Reloc encodes and writes a relocation for the given (section, diff --git a/refactor/rename/check.go b/refactor/rename/check.go index 7a0627b96c7..9f29b98a0a4 100644 --- a/refactor/rename/check.go +++ b/refactor/rename/check.go @@ -478,7 +478,7 @@ func (r *renamer) checkStructField(from *types.Var) { r.checkSelections(from) } -// checkSelection checks that all uses and selections that resolve to +// checkSelections checks that all uses and selections that resolve to // the specified object would continue to do so after the renaming. func (r *renamer) checkSelections(from types.Object) { for pkg, info := range r.packages { From 407bbed8c52312cc5c2d55f32d9d0077de0288ce Mon Sep 17 00:00:00 2001 From: Tim King Date: Wed, 14 Dec 2022 10:51:10 -0800 Subject: [PATCH 698/723] go/analysis: improve error message on duplicate fixes Improves the error message on conflicting overlapping fix suggestions. Example output: ``` conflicting edits from other and rename on /path/example/foo.go first edits: --- base +++ other @@ -1,8 +1,8 @@ package other func Foo() { - bar := 12 + bbaz := 12 - _ = bar + _ = bbaz } // the end second edits: --- base +++ rename @@ -1,8 +1,8 @@ package other func Foo() { - bar := 12 + baz := 12 - _ = bar + _ = baz } // the end ``` Rewrite of applyFixes to no longer use trees and to consolidate edits by file uniqueness. Fixes golang/go#56535 Change-Id: Ia683888c51a97a357715796b434c2d6ef92c4fef Reviewed-on: https://go-review.googlesource.com/c/tools/+/457615 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Tim King gopls-CI: kokoro --- go/analysis/internal/checker/checker.go | 226 +++++++++++-------- go/analysis/internal/checker/checker_test.go | 53 +++-- go/analysis/internal/checker/fix_test.go | 170 +++++++++++++- 3 files changed, 339 insertions(+), 110 deletions(-) diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index 530d23d38c6..8f49e8fc76b 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -15,7 +15,6 @@ import ( "flag" "fmt" "go/format" - "go/parser" "go/token" "go/types" "io/ioutil" @@ -33,6 +32,8 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/internal/analysisflags" "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/robustio" ) var ( @@ -308,6 +309,9 @@ func analyze(pkgs []*packages.Package, analyzers []*analysis.Analyzer) []*action } func applyFixes(roots []*action) error { + // visit all of the actions and accumulate the suggested edits. + paths := make(map[robustio.FileID]string) + editsByAction := make(map[robustio.FileID]map[*action][]diff.Edit) visited := make(map[*action]bool) var apply func(*action) error var visitAll func(actions []*action) error @@ -326,76 +330,43 @@ func applyFixes(roots []*action) error { return nil } - // TODO(matloob): Is this tree business too complicated? (After all this is Go!) - // Just create a set (map) of edits, sort by pos and call it a day? - type offsetedit struct { - start, end int - newText []byte - } // TextEdit using byteOffsets instead of pos - type node struct { - edit offsetedit - left, right *node - } - // Edits x and y are equivalent. - equiv := func(x, y offsetedit) bool { - return x.start == y.start && x.end == y.end && bytes.Equal(x.newText, y.newText) - } - - var insert func(tree **node, edit offsetedit) error - insert = func(treeptr **node, edit offsetedit) error { - if *treeptr == nil { - *treeptr = &node{edit, nil, nil} - return nil - } - tree := *treeptr - if edit.end <= tree.edit.start { - return insert(&tree.left, edit) - } else if edit.start >= tree.edit.end { - return insert(&tree.right, edit) - } - if equiv(edit, tree.edit) { // equivalent edits? - // We skip over equivalent edits without considering them - // an error. This handles identical edits coming from the - // multiple ways of loading a package into a - // *go/packages.Packages for testing, e.g. packages "p" and "p [p.test]". - return nil - } - - // Overlapping text edit. - return fmt.Errorf("analyses applying overlapping text edits affecting pos range (%v, %v) and (%v, %v)", - edit.start, edit.end, tree.edit.start, tree.edit.end) - - } - - editsForFile := make(map[*token.File]*node) - apply = func(act *action) error { + editsForTokenFile := make(map[*token.File][]diff.Edit) for _, diag := range act.diagnostics { for _, sf := range diag.SuggestedFixes { for _, edit := range sf.TextEdits { // Validate the edit. + // Any error here indicates a bug in the analyzer. + file := act.pkg.Fset.File(edit.Pos) + if file == nil { + return fmt.Errorf("analysis %q suggests invalid fix: missing file info for pos (%v)", + act.a.Name, edit.Pos) + } if edit.Pos > edit.End { - return fmt.Errorf( - "diagnostic for analysis %v contains Suggested Fix with malformed edit: pos (%v) > end (%v)", + return fmt.Errorf("analysis %q suggests invalid fix: pos (%v) > end (%v)", act.a.Name, edit.Pos, edit.End) } - file, endfile := act.pkg.Fset.File(edit.Pos), act.pkg.Fset.File(edit.End) - if file == nil || endfile == nil || file != endfile { - return (fmt.Errorf( - "diagnostic for analysis %v contains Suggested Fix with malformed spanning files %v and %v", - act.a.Name, file.Name(), endfile.Name())) + if eof := token.Pos(file.Base() + file.Size()); edit.End > eof { + return fmt.Errorf("analysis %q suggests invalid fix: end (%v) past end of file (%v)", + act.a.Name, edit.End, eof) } - start, end := file.Offset(edit.Pos), file.Offset(edit.End) - - // TODO(matloob): Validate that edits do not affect other packages. - root := editsForFile[file] - if err := insert(&root, offsetedit{start, end, edit.NewText}); err != nil { - return err - } - editsForFile[file] = root // In case the root changed + edit := diff.Edit{Start: file.Offset(edit.Pos), End: file.Offset(edit.End), New: string(edit.NewText)} + editsForTokenFile[file] = append(editsForTokenFile[file], edit) } } } + + for f, edits := range editsForTokenFile { + id, _, err := robustio.GetFileID(f.Name()) + if err != nil { + return err + } + if _, hasId := paths[id]; !hasId { + paths[id] = f.Name() + editsByAction[id] = make(map[*action][]diff.Edit) + } + editsByAction[id][act] = edits + } return nil } @@ -403,59 +374,126 @@ func applyFixes(roots []*action) error { return err } - fset := token.NewFileSet() // Shared by parse calls below - // Now we've got a set of valid edits for each file. Get the new file contents. - for f, tree := range editsForFile { - contents, err := ioutil.ReadFile(f.Name()) - if err != nil { - return err + // Validate and group the edits to each actual file. + editsByPath := make(map[string][]diff.Edit) + for id, actToEdits := range editsByAction { + path := paths[id] + actions := make([]*action, 0, len(actToEdits)) + for act := range actToEdits { + actions = append(actions, act) } - cur := 0 // current position in the file - - var out bytes.Buffer - - var recurse func(*node) - recurse = func(node *node) { - if node.left != nil { - recurse(node.left) + // Does any action create conflicting edits? + for _, act := range actions { + edits := actToEdits[act] + if _, invalid := validateEdits(edits); invalid > 0 { + name, x, y := act.a.Name, edits[invalid-1], edits[invalid] + return diff3Conflict(path, name, name, []diff.Edit{x}, []diff.Edit{y}) } + } - edit := node.edit - if edit.start > cur { - out.Write(contents[cur:edit.start]) - out.Write(edit.newText) - } else if cur == 0 && edit.start == 0 { // edit starts at first character? - out.Write(edit.newText) + // Does any pair of different actions create edits that conflict? + for j := range actions { + for k := range actions[:j] { + x, y := actions[j], actions[k] + if x.a.Name > y.a.Name { + x, y = y, x + } + xedits, yedits := actToEdits[x], actToEdits[y] + combined := append(xedits, yedits...) + if _, invalid := validateEdits(combined); invalid > 0 { + // TODO: consider applying each action's consistent list of edits entirely, + // and then using a three-way merge (such as GNU diff3) on the resulting + // files to report more precisely the parts that actually conflict. + return diff3Conflict(path, x.a.Name, y.a.Name, xedits, yedits) + } } - cur = edit.end + } - if node.right != nil { - recurse(node.right) - } + var edits []diff.Edit + for act := range actToEdits { + edits = append(edits, actToEdits[act]...) } - recurse(tree) - // Write out the rest of the file. - if cur < len(contents) { - out.Write(contents[cur:]) + editsByPath[path], _ = validateEdits(edits) // remove duplicates. already validated. + } + + // Now we've got a set of valid edits for each file. Apply them. + for path, edits := range editsByPath { + contents, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + applied, err := diff.Apply(string(contents), edits) + if err != nil { + return err } + out := []byte(applied) // Try to format the file. - ff, err := parser.ParseFile(fset, f.Name(), out.Bytes(), parser.ParseComments) - if err == nil { - var buf bytes.Buffer - if err = format.Node(&buf, fset, ff); err == nil { - out = buf - } + if formatted, err := format.Source(out); err == nil { + out = formatted } - if err := ioutil.WriteFile(f.Name(), out.Bytes(), 0644); err != nil { + if err := ioutil.WriteFile(path, out, 0644); err != nil { return err } } return nil } +// validateEdits returns a list of edits that is sorted and +// contains no duplicate edits. Returns the index of some +// overlapping adjacent edits if there is one and <0 if the +// edits are valid. +func validateEdits(edits []diff.Edit) ([]diff.Edit, int) { + if len(edits) == 0 { + return nil, -1 + } + equivalent := func(x, y diff.Edit) bool { + return x.Start == y.Start && x.End == y.End && x.New == y.New + } + diff.SortEdits(edits) + unique := []diff.Edit{edits[0]} + invalid := -1 + for i := 1; i < len(edits); i++ { + prev, cur := edits[i-1], edits[i] + // We skip over equivalent edits without considering them + // an error. This handles identical edits coming from the + // multiple ways of loading a package into a + // *go/packages.Packages for testing, e.g. packages "p" and "p [p.test]". + if !equivalent(prev, cur) { + unique = append(unique, cur) + if prev.End > cur.Start { + invalid = i + } + } + } + return unique, invalid +} + +// diff3Conflict returns an error describing two conflicting sets of +// edits on a file at path. +func diff3Conflict(path string, xlabel, ylabel string, xedits, yedits []diff.Edit) error { + contents, err := ioutil.ReadFile(path) + if err != nil { + return err + } + oldlabel, old := "base", string(contents) + + xdiff, err := diff.ToUnified(oldlabel, xlabel, old, xedits) + if err != nil { + return err + } + ydiff, err := diff.ToUnified(oldlabel, ylabel, old, yedits) + if err != nil { + return err + } + + return fmt.Errorf("conflicting edits from %s and %s on %s\nfirst edits:\n%s\nsecond edits:\n%s", + xlabel, ylabel, path, xdiff, ydiff) +} + // printDiagnostics prints the diagnostics for the root packages in either // plain text or JSON format. JSON format also includes errors for any // dependencies. diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go index f07963fa008..34acae81e16 100644 --- a/go/analysis/internal/checker/checker_test.go +++ b/go/analysis/internal/checker/checker_test.go @@ -69,30 +69,55 @@ var analyzer = &analysis.Analyzer{ Run: run, } +var other = &analysis.Analyzer{ // like analyzer but with a different Name. + Name: "other", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + func run(pass *analysis.Pass) (interface{}, error) { const ( - from = "bar" - to = "baz" + from = "bar" + to = "baz" + conflict = "conflict" // add conflicting edits to package conflict. + duplicate = "duplicate" // add duplicate edits to package conflict. + other = "other" // add conflicting edits to package other from different analyzers. ) + + if pass.Analyzer.Name == other { + if pass.Pkg.Name() != other { + return nil, nil // only apply Analyzer other to packages named other + } + } + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{(*ast.Ident)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { ident := n.(*ast.Ident) if ident.Name == from { msg := fmt.Sprintf("renaming %q to %q", from, to) + edits := []analysis.TextEdit{ + {Pos: ident.Pos(), End: ident.End(), NewText: []byte(to)}, + } + switch pass.Pkg.Name() { + case conflict: + edits = append(edits, []analysis.TextEdit{ + {Pos: ident.Pos() - 1, End: ident.End(), NewText: []byte(to)}, + {Pos: ident.Pos(), End: ident.End() - 1, NewText: []byte(to)}, + {Pos: ident.Pos(), End: ident.End(), NewText: []byte("lorem ipsum")}, + }...) + case duplicate: + edits = append(edits, edits...) + case other: + if pass.Analyzer.Name == other { + edits[0].Pos = edits[0].Pos + 1 // shift by one to mismatch analyzer and other + } + } pass.Report(analysis.Diagnostic{ - Pos: ident.Pos(), - End: ident.End(), - Message: msg, - SuggestedFixes: []analysis.SuggestedFix{{ - Message: msg, - TextEdits: []analysis.TextEdit{{ - Pos: ident.Pos(), - End: ident.End(), - NewText: []byte(to), - }}, - }}, - }) + Pos: ident.Pos(), + End: ident.End(), + Message: msg, + SuggestedFixes: []analysis.SuggestedFix{{Message: msg, TextEdits: edits}}}) } }) diff --git a/go/analysis/internal/checker/fix_test.go b/go/analysis/internal/checker/fix_test.go index a114c01b645..3ea92b38cc1 100644 --- a/go/analysis/internal/checker/fix_test.go +++ b/go/analysis/internal/checker/fix_test.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path" + "regexp" "runtime" "testing" @@ -23,7 +24,7 @@ func main() { checker.Fix = true patterns := flag.Args() - code := checker.Run(patterns, []*analysis.Analyzer{analyzer}) + code := checker.Run(patterns, []*analysis.Analyzer{analyzer, other}) os.Exit(code) } @@ -74,6 +75,15 @@ func Foo() { _ = bar } +// the end +`, + "duplicate/dup.go": `package duplicate + +func Foo() { + bar := 14 + _ = bar +} + // the end `, } @@ -103,6 +113,15 @@ func Foo() { _ = baz } +// the end +`, + "duplicate/dup.go": `package duplicate + +func Foo() { + baz := 14 + _ = baz +} + // the end `, } @@ -112,7 +131,7 @@ func Foo() { } defer cleanup() - args := []string{"-test.run=TestFixes", "--", "rename"} + args := []string{"-test.run=TestFixes", "--", "rename", "duplicate"} cmd := exec.Command(os.Args[0], args...) cmd.Env = append(os.Environ(), "TESTFIXES_CHILD=1", "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off") @@ -141,3 +160,150 @@ func Foo() { } } } + +// TestConflict ensures that checker.Run detects conflicts correctly. +// This test fork/execs the main function above. +func TestConflict(t *testing.T) { + oses := map[string]bool{"darwin": true, "linux": true} + if !oses[runtime.GOOS] { + t.Skipf("skipping fork/exec test on this platform") + } + + if os.Getenv("TESTCONFLICT_CHILD") == "1" { + // child process + + // replace [progname -test.run=TestConflict -- ...] + // by [progname ...] + os.Args = os.Args[2:] + os.Args[0] = "vet" + main() + panic("unreachable") + } + + testenv.NeedsTool(t, "go") + + files := map[string]string{ + "conflict/foo.go": `package conflict + +func Foo() { + bar := 12 + _ = bar +} + +// the end +`, + } + dir, cleanup, err := analysistest.WriteFiles(files) + if err != nil { + t.Fatalf("Creating test files failed with %s", err) + } + defer cleanup() + + args := []string{"-test.run=TestConflict", "--", "conflict"} + cmd := exec.Command(os.Args[0], args...) + cmd.Env = append(os.Environ(), "TESTCONFLICT_CHILD=1", "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off") + + out, err := cmd.CombinedOutput() + var exitcode int + if err, ok := err.(*exec.ExitError); ok { + exitcode = err.ExitCode() // requires go1.12 + } + const errExitCode = 1 + if exitcode != errExitCode { + t.Errorf("%s: exited %d, want %d", args, exitcode, errExitCode) + } + + pattern := `conflicting edits from rename and rename on /.*/conflict/foo.go` + matched, err := regexp.Match(pattern, out) + if err != nil { + t.Errorf("error matching pattern %s: %v", pattern, err) + } else if !matched { + t.Errorf("%s: output was=<<%s>>. Expected it to match <<%s>>", args, out, pattern) + } + + // No files updated + for name, want := range files { + path := path.Join(dir, "src", name) + contents, err := ioutil.ReadFile(path) + if err != nil { + t.Errorf("error reading %s: %v", path, err) + } + if got := string(contents); got != want { + t.Errorf("contents of %s file updated. got=%s, want=%s", path, got, want) + } + } +} + +// TestOther ensures that checker.Run reports conflicts from +// distinct actions correctly. +// This test fork/execs the main function above. +func TestOther(t *testing.T) { + oses := map[string]bool{"darwin": true, "linux": true} + if !oses[runtime.GOOS] { + t.Skipf("skipping fork/exec test on this platform") + } + + if os.Getenv("TESTOTHER_CHILD") == "1" { + // child process + + // replace [progname -test.run=TestOther -- ...] + // by [progname ...] + os.Args = os.Args[2:] + os.Args[0] = "vet" + main() + panic("unreachable") + } + + testenv.NeedsTool(t, "go") + + files := map[string]string{ + "other/foo.go": `package other + +func Foo() { + bar := 12 + _ = bar +} + +// the end +`, + } + dir, cleanup, err := analysistest.WriteFiles(files) + if err != nil { + t.Fatalf("Creating test files failed with %s", err) + } + defer cleanup() + + args := []string{"-test.run=TestOther", "--", "other"} + cmd := exec.Command(os.Args[0], args...) + cmd.Env = append(os.Environ(), "TESTOTHER_CHILD=1", "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off") + + out, err := cmd.CombinedOutput() + var exitcode int + if err, ok := err.(*exec.ExitError); ok { + exitcode = err.ExitCode() // requires go1.12 + } + const errExitCode = 1 + if exitcode != errExitCode { + t.Errorf("%s: exited %d, want %d", args, exitcode, errExitCode) + } + + pattern := `conflicting edits from other and rename on /.*/other/foo.go` + matched, err := regexp.Match(pattern, out) + if err != nil { + t.Errorf("error matching pattern %s: %v", pattern, err) + } else if !matched { + t.Errorf("%s: output was=<<%s>>. Expected it to match <<%s>>", args, out, pattern) + } + + // No files updated + for name, want := range files { + path := path.Join(dir, "src", name) + contents, err := ioutil.ReadFile(path) + if err != nil { + t.Errorf("error reading %s: %v", path, err) + } + if got := string(contents); got != want { + t.Errorf("contents of %s file updated. got=%s, want=%s", path, got, want) + } + } +} From 2ea4b81bfc6a041be08fa477321c513249c86bd6 Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 15 Dec 2022 20:59:08 -0800 Subject: [PATCH 699/723] go/ast/inspector: skip ranges that do not contain a node type Skips inspecting a range of nodes that do not contain any nodes of a given type. Computes a bitmask of type nodes between push and pop events. Skips forward to pop if nodes in between cannot match the type mask. Benchmarking against previous implementation on "net": - Preorder filtered by *ast.FuncDecl and *ast.FuncLit traversal is faster by 11x. - Preorder filtered by *ast.CallExpr is faster by 10%. - Unfiltered traveral is 5% slower. - Constructing events 3% slower. - Break even for additional computation is 5 *CallExpr filtered traversals or 1 *Func{Decl,Lit} filtered traversal. Change-Id: If4cb566474b84186ff42fb80ed7e1ebb0f692cc2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/458075 TryBot-Result: Gopher Robot Run-TryBot: Tim King Reviewed-by: Alan Donovan --- go/ast/inspector/inspector.go | 68 ++++++++++++++++++++++-------- go/ast/inspector/inspector_test.go | 44 +++++++++++++++++-- 2 files changed, 91 insertions(+), 21 deletions(-) diff --git a/go/ast/inspector/inspector.go b/go/ast/inspector/inspector.go index af5e17feeea..3fbfebf3693 100644 --- a/go/ast/inspector/inspector.go +++ b/go/ast/inspector/inspector.go @@ -53,10 +53,13 @@ func New(files []*ast.File) *Inspector { // of an ast.Node during a traversal. type event struct { node ast.Node - typ uint64 // typeOf(node) - index int // 1 + index of corresponding pop event, or 0 if this is a pop + typ uint64 // typeOf(node) on push event, or union of typ strictly between push and pop events on pop events + index int // index of corresponding push or pop event } +// TODO: Experiment with storing only the second word of event.node (unsafe.Pointer). +// Type can be recovered from the sole bit in typ. + // Preorder visits all the nodes of the files supplied to New in // depth-first order. It calls f(n) for each node n before it visits // n's children. @@ -72,10 +75,17 @@ func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) { mask := maskOf(types) for i := 0; i < len(in.events); { ev := in.events[i] - if ev.typ&mask != 0 { - if ev.index > 0 { + if ev.index > i { + // push + if ev.typ&mask != 0 { f(ev.node) } + pop := ev.index + if in.events[pop].typ&mask == 0 { + // Subtrees do not contain types: skip them and pop. + i = pop + 1 + continue + } } i++ } @@ -94,15 +104,24 @@ func (in *Inspector) Nodes(types []ast.Node, f func(n ast.Node, push bool) (proc mask := maskOf(types) for i := 0; i < len(in.events); { ev := in.events[i] - if ev.typ&mask != 0 { - if ev.index > 0 { - // push + if ev.index > i { + // push + pop := ev.index + if ev.typ&mask != 0 { if !f(ev.node, true) { - i = ev.index // jump to corresponding pop + 1 + i = pop + 1 // jump to corresponding pop + 1 continue } - } else { - // pop + } + if in.events[pop].typ&mask == 0 { + // Subtrees do not contain types: skip them. + i = pop + continue + } + } else { + // pop + push := ev.index + if in.events[push].typ&mask != 0 { f(ev.node, false) } } @@ -119,19 +138,26 @@ func (in *Inspector) WithStack(types []ast.Node, f func(n ast.Node, push bool, s var stack []ast.Node for i := 0; i < len(in.events); { ev := in.events[i] - if ev.index > 0 { + if ev.index > i { // push + pop := ev.index stack = append(stack, ev.node) if ev.typ&mask != 0 { if !f(ev.node, true, stack) { - i = ev.index + i = pop + 1 stack = stack[:len(stack)-1] continue } } + if in.events[pop].typ&mask == 0 { + // Subtrees does not contain types: skip them. + i = pop + continue + } } else { // pop - if ev.typ&mask != 0 { + push := ev.index + if in.events[push].typ&mask != 0 { f(ev.node, false, stack) } stack = stack[:len(stack)-1] @@ -157,25 +183,31 @@ func traverse(files []*ast.File) []event { events := make([]event, 0, capacity) var stack []event + stack = append(stack, event{}) // include an extra event so file nodes have a parent for _, f := range files { ast.Inspect(f, func(n ast.Node) bool { if n != nil { // push ev := event{ node: n, - typ: typeOf(n), + typ: 0, // temporarily used to accumulate type bits of subtree index: len(events), // push event temporarily holds own index } stack = append(stack, ev) events = append(events, ev) } else { // pop - ev := stack[len(stack)-1] - stack = stack[:len(stack)-1] + top := len(stack) - 1 + ev := stack[top] + typ := typeOf(ev.node) + push := ev.index + parent := top - 1 - events[ev.index].index = len(events) + 1 // make push refer to pop + events[push].typ = typ // set type of push + stack[parent].typ |= typ | ev.typ // parent's typ contains push and pop's typs. + events[push].index = len(events) // make push refer to pop - ev.index = 0 // turn ev into a pop event + stack = stack[:top] events = append(events, ev) } return true diff --git a/go/ast/inspector/inspector_test.go b/go/ast/inspector/inspector_test.go index 9e539189680..e88d584b5c0 100644 --- a/go/ast/inspector/inspector_test.go +++ b/go/ast/inspector/inspector_test.go @@ -244,9 +244,11 @@ func typeOf(n ast.Node) string { // but a break-even point (NewInspector/(ASTInspect-Inspect)) of about 5 // traversals. // -// BenchmarkNewInspector 4.5 ms -// BenchmarkNewInspect 0.33ms -// BenchmarkASTInspect 1.2 ms +// BenchmarkASTInspect 1.0 ms +// BenchmarkNewInspector 2.2 ms +// BenchmarkInspect 0.39ms +// BenchmarkInspectFilter 0.01ms +// BenchmarkInspectCalls 0.14ms func BenchmarkNewInspector(b *testing.B) { // Measure one-time construction overhead. @@ -274,6 +276,42 @@ func BenchmarkInspect(b *testing.B) { } } +func BenchmarkInspectFilter(b *testing.B) { + b.StopTimer() + inspect := inspector.New(netFiles) + b.StartTimer() + + // Measure marginal cost of traversal. + nodeFilter := []ast.Node{(*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)} + var ndecls, nlits int + for i := 0; i < b.N; i++ { + inspect.Preorder(nodeFilter, func(n ast.Node) { + switch n.(type) { + case *ast.FuncDecl: + ndecls++ + case *ast.FuncLit: + nlits++ + } + }) + } +} + +func BenchmarkInspectCalls(b *testing.B) { + b.StopTimer() + inspect := inspector.New(netFiles) + b.StartTimer() + + // Measure marginal cost of traversal. + nodeFilter := []ast.Node{(*ast.CallExpr)(nil)} + var ncalls int + for i := 0; i < b.N; i++ { + inspect.Preorder(nodeFilter, func(n ast.Node) { + _ = n.(*ast.CallExpr) + ncalls++ + }) + } +} + func BenchmarkASTInspect(b *testing.B) { var ndecls, nlits int for i := 0; i < b.N; i++ { From e170d456ff305e5277500e0875d4e8b69f52297e Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Sat, 21 Jan 2023 14:18:09 +0700 Subject: [PATCH 700/723] gopls/internal/lsp: add clear builtin For golang/go#56351 Change-Id: I5948b776cf7fde275a329d02a4f5728579f13ee9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/463055 Run-TryBot: Cuong Manh Le Auto-Submit: Cuong Manh Le gopls-CI: kokoro Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- gopls/internal/lsp/completion_test.go | 17 +++++++++++++++-- .../lsp/testdata/builtins/builtin_go118.go | 4 ++-- .../lsp/testdata/builtins/builtin_go121.go | 8 ++++++++ .../internal/lsp/testdata/builtins/builtins.go | 1 + gopls/internal/lsp/tests/util_go121.go | 12 ++++++++++++ 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 gopls/internal/lsp/testdata/builtins/builtin_go121.go create mode 100644 gopls/internal/lsp/tests/util_go121.go diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index cd3bcec992c..d9a3e0df666 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -25,8 +25,8 @@ func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal") opts.ExperimentalPostfixCompletions = strings.Contains(string(src.URI()), "postfix") }) - got = tests.FilterBuiltins(src, got) - want := expected(t, test, items) + got = filterSkipCompletionItems(tests.FilterBuiltins(src, got)) + want := filterSkipCompletionItems(expected(t, test, items)) if diff := tests.DiffCompletionItems(want, got); diff != "" { t.Errorf("mismatching completion items (-want +got):\n%s", diff) } @@ -174,3 +174,16 @@ func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*sourc } return list.Items } + +func filterSkipCompletionItems(items []protocol.CompletionItem) []protocol.CompletionItem { + n := 0 + for _, item := range items { + // TODO(cuonglm): remove once https://go-review.googlesource.com/c/go/+/462935 land. + if item.Label == "clear" { + continue + } + items[n] = item + n++ + } + return items[:n] +} diff --git a/gopls/internal/lsp/testdata/builtins/builtin_go118.go b/gopls/internal/lsp/testdata/builtins/builtin_go118.go index 1fa0b14a4a1..dabffcc679c 100644 --- a/gopls/internal/lsp/testdata/builtins/builtin_go118.go +++ b/gopls/internal/lsp/testdata/builtins/builtin_go118.go @@ -1,5 +1,5 @@ -//go:build go1.18 -// +build go1.18 +//go:build go1.18 && !go1.21 +// +build go1.18,!go1.21 package builtins diff --git a/gopls/internal/lsp/testdata/builtins/builtin_go121.go b/gopls/internal/lsp/testdata/builtins/builtin_go121.go new file mode 100644 index 00000000000..cb8e8fae3ab --- /dev/null +++ b/gopls/internal/lsp/testdata/builtins/builtin_go121.go @@ -0,0 +1,8 @@ +//go:build go1.21 +// +build go1.21 + +package builtins + +func _() { + //@complete("", any, append, bool, byte, cap, clear, close, comparable, complex, complex128, complex64, copy, delete, error, _false, float32, float64, imag, int, int16, int32, int64, int8, len, make, new, panic, print, println, real, recover, rune, string, _true, uint, uint16, uint32, uint64, uint8, uintptr, _nil) +} diff --git a/gopls/internal/lsp/testdata/builtins/builtins.go b/gopls/internal/lsp/testdata/builtins/builtins.go index d30176cfbae..75c6e418312 100644 --- a/gopls/internal/lsp/testdata/builtins/builtins.go +++ b/gopls/internal/lsp/testdata/builtins/builtins.go @@ -8,6 +8,7 @@ package builtins /* bool */ //@item(bool, "bool", "", "type") /* byte */ //@item(byte, "byte", "", "type") /* cap(v Type) int */ //@item(cap, "cap", "func(v Type) int", "func") +/* clear[T interface{ ~[]Type | ~map[Type]Type1 }](t T) */ //@item(clear, "clear", "func(t T)", "func") /* close(c chan<- Type) */ //@item(close, "close", "func(c chan<- Type)", "func") /* comparable */ //@item(comparable, "comparable", "", "interface") /* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") diff --git a/gopls/internal/lsp/tests/util_go121.go b/gopls/internal/lsp/tests/util_go121.go new file mode 100644 index 00000000000..93065864802 --- /dev/null +++ b/gopls/internal/lsp/tests/util_go121.go @@ -0,0 +1,12 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 +// +build go1.21 + +package tests + +func init() { + builtins["clear"] = true +} From c276ee52bc1c61017e41cdd8caa3e8ae018a3147 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 31 Jan 2023 11:21:32 -0500 Subject: [PATCH 701/723] internal/robustio: fix signature of getFileID on plan9 Also fix the failing new gopls command integration tests, which observe a different exit code on plan9. Change-Id: I5d0c11549deb3d3643f4d7772f0659dfda3600ea Reviewed-on: https://go-review.googlesource.com/c/tools/+/464297 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Bryan Mills --- .../internal/lsp/cmd/test/integration_test.go | 112 +++++++++--------- internal/robustio/robustio_plan9.go | 7 +- 2 files changed, 60 insertions(+), 59 deletions(-) diff --git a/gopls/internal/lsp/cmd/test/integration_test.go b/gopls/internal/lsp/cmd/test/integration_test.go index 7b0f852fa42..956fb59059b 100644 --- a/gopls/internal/lsp/cmd/test/integration_test.go +++ b/gopls/internal/lsp/cmd/test/integration_test.go @@ -57,14 +57,14 @@ func TestVersion(t *testing.T) { // basic { res := gopls(t, tree, "version") - res.checkExit(0) + res.checkExit(true) res.checkStdout(want) } // -json flag { res := gopls(t, tree, "version", "-json") - res.checkExit(0) + res.checkExit(true) var v debug.ServerVersion if res.toJSON(&v) { if v.Version != want { @@ -97,7 +97,7 @@ var _ = fmt.Sprintf("%d", "123") // no files { res := gopls(t, tree, "check") - res.checkExit(0) + res.checkExit(true) if res.stdout != "" { t.Errorf("unexpected output: %v", res) } @@ -106,14 +106,14 @@ var _ = fmt.Sprintf("%d", "123") // one file { res := gopls(t, tree, "check", "./a.go") - res.checkExit(0) + res.checkExit(true) res.checkStdout("fmt.Sprintf format %s has arg 123 of wrong type int") } // two files { res := gopls(t, tree, "check", "./a.go", "./b.go") - res.checkExit(0) + res.checkExit(true) res.checkStdout(`a.go:.* fmt.Sprintf format %s has arg 123 of wrong type int`) res.checkStdout(`b.go:.* fmt.Sprintf format %d has arg "123" of wrong type string`) } @@ -142,19 +142,19 @@ func h() { // missing position { res := gopls(t, tree, "call_hierarchy") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // wrong place { res := gopls(t, tree, "call_hierarchy", "a.go:1") - res.checkExit(2) + res.checkExit(false) res.checkStderr("identifier not found") } // f is called once from g and twice from h. { res := gopls(t, tree, "call_hierarchy", "a.go:2:6") - res.checkExit(0) + res.checkExit(true) // We use regexp '.' as an OS-agnostic path separator. res.checkStdout("ranges 7:2-3, 8:2-3 in ..a.go from/to function h in ..a.go:6:6-7") res.checkStdout("ranges 4:2-3 in ..a.go from/to function g in ..a.go:3:6-7") @@ -184,26 +184,26 @@ func g() { // missing position { res := gopls(t, tree, "definition") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // intra-package { res := gopls(t, tree, "definition", "a.go:7:2") // "f()" - res.checkExit(0) + res.checkExit(true) res.checkStdout("a.go:3:6-7: defined here as func f") } // cross-package { res := gopls(t, tree, "definition", "a.go:4:7") // "Println" - res.checkExit(0) + res.checkExit(true) res.checkStdout("print.go.* defined here as func fmt.Println") res.checkStdout("Println formats using the default formats for its operands") } // -json and -markdown { res := gopls(t, tree, "definition", "-json", "-markdown", "a.go:4:7") - res.checkExit(0) + res.checkExit(true) var defn cmd.Definition if res.toJSON(&defn) { if !strings.HasPrefix(defn.Description, "```go\nfunc fmt.Println") { @@ -231,13 +231,13 @@ func f(x int) { // missing filename { res := gopls(t, tree, "folding_ranges") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // success { res := gopls(t, tree, "folding_ranges", "a.go") - res.checkExit(0) + res.checkExit(true) res.checkStdout("2:8-2:13") // params (x int) res.checkStdout("2:16-4:1") // body { ... } } @@ -259,12 +259,12 @@ func f() {} // no files => nop { res := gopls(t, tree, "format") - res.checkExit(0) + res.checkExit(true) } // default => print formatted result { res := gopls(t, tree, "format", "a.go") - res.checkExit(0) + res.checkExit(true) if res.stdout != want { t.Errorf("format: got <<%s>>, want <<%s>>", res.stdout, want) } @@ -272,19 +272,19 @@ func f() {} // start/end position not supported (unless equal to start/end of file) { res := gopls(t, tree, "format", "a.go:1-2") - res.checkExit(2) + res.checkExit(false) res.checkStderr("only full file formatting supported") } // -list: show only file names { res := gopls(t, tree, "format", "-list", "a.go") - res.checkExit(0) + res.checkExit(true) res.checkStdout("a.go") } // -diff prints a unified diff { res := gopls(t, tree, "format", "-diff", "a.go") - res.checkExit(0) + res.checkExit(true) // We omit the filenames as they vary by OS. want := ` -package a ; func f ( ) { } @@ -297,7 +297,7 @@ func f() {} // -write updates the file { res := gopls(t, tree, "format", "-write", "a.go") - res.checkExit(0) + res.checkExit(true) res.checkStdout("^$") // empty checkContent(t, filepath.Join(tree, "a.go"), want) } @@ -320,13 +320,13 @@ func f() { // no arguments { res := gopls(t, tree, "highlight") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // all occurrences of Println { res := gopls(t, tree, "highlight", "a.go:4:7") - res.checkExit(0) + res.checkExit(true) res.checkStdout("a.go:4:6-13") res.checkStdout("a.go:5:6-13") } @@ -347,13 +347,13 @@ func (T) String() string { return "" } // no arguments { res := gopls(t, tree, "implementation") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // T.String { res := gopls(t, tree, "implementation", "a.go:4:10") - res.checkExit(0) + res.checkExit(true) // TODO(adonovan): extract and check the content of the reported ranges? // We use regexp '.' as an OS-agnostic path separator. res.checkStdout("fmt.print.go:") // fmt.Stringer.String @@ -385,13 +385,13 @@ func _() { // no arguments { res := gopls(t, tree, "imports") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // default: print with imports { res := gopls(t, tree, "imports", "a.go") - res.checkExit(0) + res.checkExit(true) if res.stdout != want { t.Errorf("format: got <<%s>>, want <<%s>>", res.stdout, want) } @@ -399,13 +399,13 @@ func _() { // -diff: show a unified diff { res := gopls(t, tree, "imports", "-diff", "a.go") - res.checkExit(0) + res.checkExit(true) res.checkStdout(regexp.QuoteMeta(`+import "fmt"`)) } // -write: update file { res := gopls(t, tree, "imports", "-write", "a.go") - res.checkExit(0) + res.checkExit(true) checkContent(t, filepath.Join(tree, "a.go"), want) } } @@ -427,13 +427,13 @@ func f() {} // no arguments { res := gopls(t, tree, "links") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // success { res := gopls(t, tree, "links", "a.go") - res.checkExit(0) + res.checkExit(true) res.checkStdout("https://go.dev/cl") res.checkStdout("https://pkg.go.dev") res.checkStdout("https://blog.go.dev/") @@ -441,7 +441,7 @@ func f() {} // -json { res := gopls(t, tree, "links", "-json", "a.go") - res.checkExit(0) + res.checkExit(true) res.checkStdout("https://pkg.go.dev") res.checkStdout("https://go.dev/cl") res.checkStdout("https://blog.go.dev/") // at 5:21-5:41 @@ -481,13 +481,13 @@ func g() { // no arguments { res := gopls(t, tree, "references") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // fmt.Println { res := gopls(t, tree, "references", "a.go:4:10") - res.checkExit(0) + res.checkExit(true) res.checkStdout("a.go:4:6-13") res.checkStdout("b.go:4:6-13") } @@ -512,13 +512,13 @@ func f() { // no arguments { res := gopls(t, tree, "signature") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // at 123 inside fmt.Println() call { res := gopls(t, tree, "signature", "a.go:4:15") - res.checkExit(0) + res.checkExit(true) res.checkStdout("Println\\(a ...") res.checkStdout("Println formats using the default formats...") } @@ -540,25 +540,25 @@ func oldname() {} // no arguments { res := gopls(t, tree, "prepare_rename") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // in 'package' keyword { res := gopls(t, tree, "prepare_rename", "a.go:1:3") - res.checkExit(2) + res.checkExit(false) res.checkStderr("request is not valid at the given position") } // in 'package' identifier (not supported by client) { res := gopls(t, tree, "prepare_rename", "a.go:1:9") - res.checkExit(2) + res.checkExit(false) res.checkStderr("can't rename package") } // in func oldname { res := gopls(t, tree, "prepare_rename", "a.go:2:9") - res.checkExit(0) + res.checkExit(true) res.checkStdout("a.go:2:6-13") // all of "oldname" } } @@ -579,31 +579,31 @@ func oldname() {} // no arguments { res := gopls(t, tree, "rename") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 2 arguments") } // missing newname { res := gopls(t, tree, "rename", "a.go:1:3") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 2 arguments") } // in 'package' keyword { res := gopls(t, tree, "rename", "a.go:1:3", "newname") - res.checkExit(2) + res.checkExit(false) res.checkStderr("no object found") } // in 'package' identifier { res := gopls(t, tree, "rename", "a.go:1:9", "newname") - res.checkExit(2) + res.checkExit(false) res.checkStderr(`cannot rename package: module path .* same as the package path, so .* no effect`) } // success, func oldname (and -diff) { res := gopls(t, tree, "rename", "-diff", "a.go:2:9", "newname") - res.checkExit(0) + res.checkExit(true) res.checkStdout(regexp.QuoteMeta("-func oldname() {}")) res.checkStdout(regexp.QuoteMeta("+func newname() {}")) } @@ -627,13 +627,13 @@ const c = 0 // no files { res := gopls(t, tree, "symbols") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // success { res := gopls(t, tree, "symbols", "a.go:123:456") // (line/col ignored) - res.checkExit(0) + res.checkExit(true) res.checkStdout("f Function 2:6-2:7") res.checkStdout("v Variable 3:5-3:6") res.checkStdout("c Constant 4:7-4:8") @@ -658,13 +658,13 @@ const c = 0 // no files { res := gopls(t, tree, "semtok") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expected one file name") } // success { res := gopls(t, tree, "semtok", "a.go") - res.checkExit(0) + res.checkExit(true) got := res.stdout want := ` /*⇒7,keyword,[]*/package /*⇒1,namespace,[]*/a @@ -703,13 +703,13 @@ func f() (int, string) { return 0, "" } // no arguments { res := gopls(t, tree, "fix") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects at least 1 argument") } // success (-a enables fillreturns) { res := gopls(t, tree, "fix", "-a", "a.go") - res.checkExit(0) + res.checkExit(true) got := res.stdout if got != want { t.Errorf("fix: got <<%s>>, want <<%s>>", got, want) @@ -738,13 +738,13 @@ func someFunctionName() // no files { res := gopls(t, tree, "workspace_symbol") - res.checkExit(2) + res.checkExit(false) res.checkStderr("expects 1 argument") } // success { res := gopls(t, tree, "workspace_symbol", "meFun") - res.checkExit(0) + res.checkExit(true) res.checkStdout("a.go:2:6-22 someFunctionName Function") } } @@ -846,11 +846,11 @@ func (res *result) String() string { } // checkExit asserts that gopls returned the expected exit code. -func (res *result) checkExit(code int) { +func (res *result) checkExit(success bool) { res.t.Helper() - if res.exitcode != code { - res.t.Errorf("%s: exited with code %d, want %d (%s)", - res.command, res.exitcode, code, res) + if (res.exitcode == 0) != success { + res.t.Errorf("%s: exited with code %d, want success: %t (%s)", + res.command, res.exitcode, success, res) } } diff --git a/internal/robustio/robustio_plan9.go b/internal/robustio/robustio_plan9.go index ba1255f777e..9fa4cacb5a3 100644 --- a/internal/robustio/robustio_plan9.go +++ b/internal/robustio/robustio_plan9.go @@ -10,16 +10,17 @@ package robustio import ( "os" "syscall" + "time" ) -func getFileID(filename string) (FileID, error) { +func getFileID(filename string) (FileID, time.Time, error) { fi, err := os.Stat(filename) if err != nil { - return FileID{}, err + return FileID{}, time.Time{}, err } dir := fi.Sys().(*syscall.Dir) return FileID{ device: uint64(dir.Type)<<32 | uint64(dir.Dev), inode: dir.Qid.Path, - }, nil + }, fi.ModTime(), nil } From 86fdadc60bbc6e44086a41a771683e005c91a899 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 30 Jan 2023 17:48:24 -0500 Subject: [PATCH 702/723] gopls/internal/lsp/safetoken: delete safetoken.Range Now that its scope has been substantially reduced, there's no need for this type, so this change expands it out to a triple (or pair in some cases). The defensive check in NewRange is never needed. Alsoo, rename RangeToTokenRange to RangePos, following the convention. Also, fix incorrect uses of the term "wire format". Change-Id: I5bfc5bf8ce6c9504166cc928cad1df2df000737a Reviewed-on: https://go-review.googlesource.com/c/tools/+/464056 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Alan Donovan --- gopls/doc/design/implementation.md | 2 +- .../lsp/analysis/fillstruct/fillstruct.go | 6 +- .../lsp/analysis/undeclaredname/undeclared.go | 4 +- gopls/internal/lsp/code_action.go | 6 +- gopls/internal/lsp/protocol/doc.go | 4 +- gopls/internal/lsp/protocol/mapper.go | 5 +- gopls/internal/lsp/safetoken/range.go | 62 --------- gopls/internal/lsp/source/call_hierarchy.go | 4 +- .../lsp/source/completion/completion.go | 33 +++-- .../lsp/source/completion/definition.go | 5 +- .../internal/lsp/source/completion/package.go | 12 +- gopls/internal/lsp/source/extract.go | 129 +++++++++--------- gopls/internal/lsp/source/fix.go | 7 +- gopls/internal/lsp/source/inlay_hint.go | 4 +- gopls/internal/lsp/source/stub.go | 6 +- gopls/internal/lsp/source/view.go | 8 +- 16 files changed, 124 insertions(+), 173 deletions(-) delete mode 100644 gopls/internal/lsp/safetoken/range.go diff --git a/gopls/doc/design/implementation.md b/gopls/doc/design/implementation.md index a8f7f0b0e01..859ec1c1219 100644 --- a/gopls/doc/design/implementation.md +++ b/gopls/doc/design/implementation.md @@ -29,7 +29,7 @@ Package | Description [internal/lsp/cache] | the cache layer [internal/lsp/cmd] | the gopls command line layer [internal/lsp/debug] | features to aid in debugging gopls -[internal/lsp/protocol] | the lsp protocol layer and wire format +[internal/lsp/protocol] | the types of LSP request and response messages [internal/lsp/source] | the core feature implementations [internal/span] | a package for dealing with source file locations [internal/memoize] | a function invocation cache used to reduce the work done diff --git a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go index 1b331572073..af29a3632f1 100644 --- a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -135,12 +135,12 @@ func run(pass *analysis.Pass) (interface{}, error) { // SuggestedFix computes the suggested fix for the kinds of // diagnostics produced by the Analyzer above. -func SuggestedFix(fset *token.FileSet, rng safetoken.Range, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { +func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { if info == nil { return nil, fmt.Errorf("nil types.Info") } - pos := rng.Start // don't use the end + pos := start // don't use the end // TODO(rstambler): Using ast.Inspect would probably be more efficient than // calling PathEnclosingInterval. Switch this approach. @@ -205,7 +205,7 @@ func SuggestedFix(fset *token.FileSet, rng safetoken.Range, content []byte, file } fieldTyps = append(fieldTyps, field.Type()) } - matches := analysisinternal.MatchingIdents(fieldTyps, file, rng.Start, info, pkg) + matches := analysisinternal.MatchingIdents(fieldTyps, file, start, info, pkg) var elts []ast.Expr for i, fieldTyp := range fieldTyps { if fieldTyp == nil { diff --git a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go index 996bf2ddb60..043979408d0 100644 --- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -121,8 +121,8 @@ func runForError(pass *analysis.Pass, err types.Error) { }) } -func SuggestedFix(fset *token.FileSet, rng safetoken.Range, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { - pos := rng.Start // don't use the end +func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { + pos := start // don't use the end path, _ := astutil.PathEnclosingInterval(file, pos, pos) if len(path) < 2 { return nil, fmt.Errorf("no expression found") diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 58bb59e16fc..ef9f921ec2f 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -320,13 +320,13 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI if err != nil { return nil, fmt.Errorf("getting file for Identifier: %w", err) } - srng, err := pgf.RangeToTokenRange(rng) + start, end, err := pgf.RangePos(rng) if err != nil { return nil, err } puri := protocol.URIFromSpanURI(uri) var commands []protocol.Command - if _, ok, methodOk, _ := source.CanExtractFunction(pgf.Tok, srng, pgf.Src, pgf.File); ok { + 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: source.ExtractFunction, @@ -348,7 +348,7 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI commands = append(commands, cmd) } } - if _, _, ok, _ := source.CanExtractVariable(srng, pgf.File); ok { + if _, _, ok, _ := source.CanExtractVariable(start, end, pgf.File); ok { cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ URI: puri, Fix: source.ExtractVariable, diff --git a/gopls/internal/lsp/protocol/doc.go b/gopls/internal/lsp/protocol/doc.go index 2ffdf51287e..4eb03a00751 100644 --- a/gopls/internal/lsp/protocol/doc.go +++ b/gopls/internal/lsp/protocol/doc.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. -// Package protocol contains the structs that map directly to the wire format -// of the "Language Server Protocol". +// Package protocol contains the structs that map directly to the +// request and response messages of the Language Server Protocol. // // It is a literal transcription, with unmodified comments, and only the changes // required to make it go code. diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go index a0f11722e53..d61524d836e 100644 --- a/gopls/internal/lsp/protocol/mapper.go +++ b/gopls/internal/lsp/protocol/mapper.go @@ -18,14 +18,13 @@ package protocol // token.Pos // token.FileSet // token.File -// safetoken.Range = (file token.File, start, end token.Pos) // // Because File.Offset and File.Pos panic on invalid inputs, // we do not call them directly and instead use the safetoken package // for these conversions. This is enforced by a static check. // // Beware also that the methods of token.File have two bugs for which -// safetoken contain workarounds: +// safetoken contains workarounds: // - #57490, whereby the parser may create ast.Nodes during error // recovery whose computed positions are out of bounds (EOF+1). // - #41029, whereby the wrong line number is returned for the EOF position. @@ -45,7 +44,7 @@ package protocol // they are also useful for parsing user-provided positions (e.g. in // the CLI) before we have access to file contents. // -// 4. protocol, the LSP wire format. +// 4. protocol, the LSP RPC message format. // // protocol.Position = (Line, Character uint32) // protocol.Range = (start, end Position) diff --git a/gopls/internal/lsp/safetoken/range.go b/gopls/internal/lsp/safetoken/range.go deleted file mode 100644 index f44b4499dfc..00000000000 --- a/gopls/internal/lsp/safetoken/range.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package safetoken - -import "go/token" - -// Range represents a source code range in token.Pos form. -// -// It also carries the token.File that produced the position, -// so that it is capable of returning (file, line, col8) information. -// However it cannot be converted to protocol (UTF-16) form -// without access to file content; to do that, use a protocol.ContentMapper. -// -// TODO(adonovan): Eliminate most/all uses of Range in gopls, as -// without a Mapper it's not really self-contained. It is mostly used -// by completion. Given access to complete.mapper, it could use a pair -// of byte offsets instead. -type Range struct { - TokFile *token.File // non-nil - Start, End token.Pos // both IsValid() -} - -// NewRange creates a new Range from a token.File and two positions within it. -// The given start position must be valid; if end is invalid, start is used as -// the end position. -// -// (If you only have a token.FileSet, use file = fset.File(start). But -// most callers know exactly which token.File they're dealing with and -// should pass it explicitly. Not only does this save a lookup, but it -// brings us a step closer to eliminating the global FileSet.) -func NewRange(file *token.File, start, end token.Pos) Range { - if file == nil { - panic("nil *token.File") - } - if !start.IsValid() { - panic("invalid start token.Pos") - } - if !end.IsValid() { - end = start - } - - // TODO(adonovan): ideally we would make this stronger assertion: - // - // // Assert that file is non-nil and contains start and end. - // _ = file.Offset(start) - // _ = file.Offset(end) - // - // but some callers (e.g. packageCompletionSurrounding) don't ensure this precondition. - - return Range{ - TokFile: file, - Start: start, - End: end, - } -} - -// IsPoint returns true if the range represents a single point. -func (r Range) IsPoint() bool { - return r.Start == r.End -} diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index 31fdf491f3b..d9efcbe1524 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -119,7 +119,7 @@ func enclosingNodeCallItem(ctx context.Context, snapshot Snapshot, pkgPath Packa if err != nil { return protocol.CallHierarchyItem{}, err } - srcRng, err := pgf.RangeToTokenRange(loc.Range) + start, end, err := pgf.RangePos(loc.Range) if err != nil { return protocol.CallHierarchyItem{}, err } @@ -128,7 +128,7 @@ func enclosingNodeCallItem(ctx context.Context, snapshot Snapshot, pkgPath Packa var funcDecl *ast.FuncDecl var funcLit *ast.FuncLit // innermost function literal var litCount int - path, _ := astutil.PathEnclosingInterval(pgf.File, srcRng.Start, srcRng.End) + path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) outer: for _, node := range path { switch n := node.(type) { diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index f27190c8915..ad6e79f4573 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -24,7 +24,6 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" @@ -288,10 +287,10 @@ type completionContext struct { // A Selection represents the cursor position and surrounding identifier. type Selection struct { - content string - cursor token.Pos // relative to rng.TokFile - rng safetoken.Range - mapper *protocol.Mapper + content string + tokFile *token.File + start, end, cursor token.Pos // relative to rng.TokFile + mapper *protocol.Mapper } func (p Selection) Content() string { @@ -299,15 +298,15 @@ func (p Selection) Content() string { } func (p Selection) Range() (protocol.Range, error) { - return p.mapper.PosRange(p.rng.TokFile, p.rng.Start, p.rng.End) + return p.mapper.PosRange(p.tokFile, p.start, p.end) } func (p Selection) Prefix() string { - return p.content[:p.cursor-p.rng.Start] + return p.content[:p.cursor-p.start] } func (p Selection) Suffix() string { - return p.content[p.cursor-p.rng.Start:] + return p.content[p.cursor-p.start:] } func (c *completer) setSurrounding(ident *ast.Ident) { @@ -322,8 +321,10 @@ func (c *completer) setSurrounding(ident *ast.Ident) { content: ident.Name, cursor: c.pos, // Overwrite the prefix only. - rng: safetoken.NewRange(c.tokFile, ident.Pos(), ident.End()), - mapper: c.mapper, + tokFile: c.tokFile, + start: ident.Pos(), + end: ident.End(), + mapper: c.mapper, } c.setMatcherFromPrefix(c.surrounding.Prefix()) @@ -345,7 +346,9 @@ func (c *completer) getSurrounding() *Selection { c.surrounding = &Selection{ content: "", cursor: c.pos, - rng: safetoken.NewRange(c.tokFile, c.pos, c.pos), + tokFile: c.tokFile, + start: c.pos, + end: c.pos, mapper: c.mapper, } } @@ -798,7 +801,9 @@ func (c *completer) populateImportCompletions(ctx context.Context, searchImport c.surrounding = &Selection{ content: content, cursor: c.pos, - rng: safetoken.NewRange(c.tokFile, start, end), + tokFile: c.tokFile, + start: start, + end: end, mapper: c.mapper, } @@ -1019,7 +1024,9 @@ func (c *completer) setSurroundingForComment(comments *ast.CommentGroup) { c.surrounding = &Selection{ content: cursorComment.Text[start:end], cursor: c.pos, - rng: safetoken.NewRange(c.tokFile, token.Pos(int(cursorComment.Slash)+start), token.Pos(int(cursorComment.Slash)+end)), + tokFile: c.tokFile, + start: token.Pos(int(cursorComment.Slash) + start), + end: token.Pos(int(cursorComment.Slash) + end), mapper: c.mapper, } c.setMatcherFromPrefix(c.surrounding.Prefix()) diff --git a/gopls/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go index 564d82c2bfd..a0160a1a1e3 100644 --- a/gopls/internal/lsp/source/completion/definition.go +++ b/gopls/internal/lsp/source/completion/definition.go @@ -12,7 +12,6 @@ import ( "unicode/utf8" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/snippet" "golang.org/x/tools/gopls/internal/lsp/source" ) @@ -40,7 +39,9 @@ func definition(path []ast.Node, obj types.Object, pgf *source.ParsedGoFile) ([] sel := &Selection{ content: "", cursor: start, - rng: safetoken.NewRange(pgf.Tok, start, end), + tokFile: pgf.Tok, + start: start, + end: end, mapper: pgf.Mapper, } var ans []CompletionItem diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go index f6b514889c4..f3bc30688c3 100644 --- a/gopls/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -90,7 +90,9 @@ func packageCompletionSurrounding(pgf *source.ParsedGoFile, offset int) (*Select return &Selection{ content: name.Name, cursor: cursor, - rng: safetoken.NewRange(tok, name.Pos(), name.End()), + tokFile: tok, + start: name.Pos(), + end: name.End(), mapper: m, }, nil } @@ -128,7 +130,9 @@ func packageCompletionSurrounding(pgf *source.ParsedGoFile, offset int) (*Select return &Selection{ content: content, cursor: cursor, - rng: safetoken.NewRange(tok, start, end), + tokFile: tok, + start: start, + end: end, mapper: m, }, nil } @@ -149,8 +153,10 @@ func packageCompletionSurrounding(pgf *source.ParsedGoFile, offset int) (*Select // The surrounding range in this case is the cursor. return &Selection{ content: "", + tokFile: tok, + start: cursor, + end: cursor, cursor: cursor, - rng: safetoken.NewRange(tok, cursor, cursor), mapper: m, }, nil } diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index 6f3c7800998..56e8a5e236a 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -23,11 +23,11 @@ import ( "golang.org/x/tools/internal/bug" ) -func extractVariable(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { +func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { tokFile := fset.File(file.Pos()) - expr, path, ok, err := CanExtractVariable(rng, file) + expr, path, ok, err := CanExtractVariable(start, end, file) if !ok { - return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", safetoken.StartPosition(fset, rng.Start), err) + return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", safetoken.StartPosition(fset, start), err) } // Create new AST node for extracted code. @@ -88,8 +88,8 @@ func extractVariable(fset *token.FileSet, rng safetoken.Range, src []byte, file NewText: []byte(assignment), }, { - Pos: rng.Start, - End: rng.End, + Pos: start, + End: end, NewText: []byte(lhs), }, }, @@ -98,11 +98,11 @@ func extractVariable(fset *token.FileSet, rng safetoken.Range, src []byte, file // CanExtractVariable reports whether the code in the given range can be // extracted to a variable. -func CanExtractVariable(rng safetoken.Range, file *ast.File) (ast.Expr, []ast.Node, bool, error) { - if rng.Start == rng.End { +func CanExtractVariable(start, end token.Pos, file *ast.File) (ast.Expr, []ast.Node, bool, error) { + if start == end { return nil, nil, false, fmt.Errorf("start and end are equal") } - path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.End) + path, _ := astutil.PathEnclosingInterval(file, start, end) if len(path) == 0 { return nil, nil, false, fmt.Errorf("no path enclosing interval") } @@ -112,7 +112,7 @@ func CanExtractVariable(rng safetoken.Range, file *ast.File) (ast.Expr, []ast.No } } node := path[0] - if rng.Start != node.Pos() || rng.End != node.End() { + if start != node.Pos() || end != node.End() { return nil, nil, false, fmt.Errorf("range does not map to an AST node") } expr, ok := node.(ast.Expr) @@ -189,13 +189,13 @@ type returnVariable struct { } // extractMethod refactors the selected block of code into a new method. -func extractMethod(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { - return extractFunctionMethod(fset, rng, src, file, pkg, info, true) +func extractMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { + return extractFunctionMethod(fset, start, end, src, file, pkg, info, true) } // extractFunction refactors the selected block of code into a new function. -func extractFunction(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { - return extractFunctionMethod(fset, rng, src, file, pkg, info, false) +func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { + return extractFunctionMethod(fset, start, end, src, file, pkg, info, false) } // extractFunctionMethod refactors the selected block of code into a new function/method. @@ -206,7 +206,7 @@ func extractFunction(fset *token.FileSet, rng safetoken.Range, src []byte, file // and return values of the extracted function/method. Lastly, we construct the call // of the function/method and insert this call as well as the extracted function/method into // their proper locations. -func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info, isMethod bool) (*analysis.SuggestedFix, error) { +func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info, isMethod bool) (*analysis.SuggestedFix, error) { errorPrefix := "extractFunction" if isMethod { errorPrefix = "extractMethod" @@ -216,12 +216,12 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, if tok == nil { return nil, bug.Errorf("no file for position") } - p, ok, methodOk, err := CanExtractFunction(tok, rng, src, file) + p, ok, methodOk, err := CanExtractFunction(tok, start, end, src, file) if (!ok && !isMethod) || (!methodOk && isMethod) { return nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix, - safetoken.StartPosition(fset, rng.Start), err) + safetoken.StartPosition(fset, start), err) } - tok, path, rng, outer, start := p.tok, p.path, p.rng, p.outer, p.start + tok, path, start, end, outer, node := p.tok, p.path, p.start, p.end, p.outer, p.node fileScope := info.Scopes[file] if fileScope == nil { return nil, fmt.Errorf("%s: file scope is empty", errorPrefix) @@ -236,13 +236,13 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, // non-nested return statements are guaranteed to execute. var retStmts []*ast.ReturnStmt var hasNonNestedReturn bool - startParent := findParent(outer, start) + startParent := findParent(outer, node) ast.Inspect(outer, func(n ast.Node) bool { if n == nil { return false } - if n.Pos() < rng.Start || n.End() > rng.End { - return n.Pos() <= rng.End + if n.Pos() < start || n.End() > end { + return n.Pos() <= end } ret, ok := n.(*ast.ReturnStmt) if !ok { @@ -260,7 +260,7 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, // we must determine the signature of the extracted function. We will then replace // the block with an assignment statement that calls the extracted function with // the appropriate parameters and return values. - variables, err := collectFreeVars(info, file, fileScope, pkgScope, rng, path[0]) + variables, err := collectFreeVars(info, file, fileScope, pkgScope, start, end, path[0]) if err != nil { return nil, err } @@ -343,7 +343,7 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, if v.obj.Parent() == nil { return nil, fmt.Errorf("parent nil") } - isUsed, firstUseAfter := objUsed(info, safetoken.NewRange(tok, rng.End, v.obj.Parent().End()), v.obj) + isUsed, firstUseAfter := objUsed(info, end, v.obj.Parent().End(), v.obj) if v.assigned && isUsed && !varOverridden(info, firstUseAfter, v.obj, v.free, outer) { returnTypes = append(returnTypes, &ast.Field{Type: typ}) returns = append(returns, identifier) @@ -400,7 +400,7 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, // We put the selection in a constructed file. We can then traverse and edit // the extracted selection without modifying the original AST. - startOffset, endOffset, err := safetoken.Offsets(tok, rng.Start, rng.End) + startOffset, endOffset, err := safetoken.Offsets(tok, start, end) if err != nil { return nil, err } @@ -499,7 +499,7 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, // statements in the selection. Update the type signature of the extracted // function and construct the if statement that will be inserted in the enclosing // function. - retVars, ifReturn, err = generateReturnInfo(enclosing, pkg, path, file, info, fset, rng.Start, hasNonNestedReturn) + retVars, ifReturn, err = generateReturnInfo(enclosing, pkg, path, file, info, fset, start, hasNonNestedReturn) if err != nil { return nil, err } @@ -534,7 +534,7 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, funName = name } else { name = "newFunction" - funName, _ = generateAvailableIdentifier(rng.Start, file, path, info, name, 0) + funName, _ = generateAvailableIdentifier(start, file, path, info, name, 0) } extractedFunCall := generateFuncCall(hasNonNestedReturn, hasReturnValues, params, append(returns, getNames(retVars)...), funName, sym, receiverName) @@ -587,7 +587,7 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, // Find all the comments within the range and print them to be put somewhere. // TODO(suzmue): print these in the extracted function at the correct place. for _, cg := range file.Comments { - if cg.Pos().IsValid() && cg.Pos() < rng.End && cg.Pos() >= rng.Start { + if cg.Pos().IsValid() && cg.Pos() < end && cg.Pos() >= start { for _, c := range cg.List { fmt.Fprintln(&commentBuf, c.Text) } @@ -602,7 +602,7 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, } before := src[outerStart:startOffset] after := src[endOffset:outerEnd] - indent, err := calculateIndentation(src, tok, start) + indent, err := calculateIndentation(src, tok, node) if err != nil { return nil, err } @@ -652,12 +652,12 @@ func extractFunctionMethod(fset *token.FileSet, rng safetoken.Range, src []byte, // their cursors for whitespace. To support this use case, we must manually adjust the // ranges to match the correct AST node. In this particular example, we would adjust // rng.Start forward to the start of 'if' and rng.End backward to after '}'. -func adjustRangeForCommentsAndWhiteSpace(rng safetoken.Range, tok *token.File, content []byte, file *ast.File) (safetoken.Range, error) { +func adjustRangeForCommentsAndWhiteSpace(tok *token.File, start, end token.Pos, content []byte, file *ast.File) (token.Pos, token.Pos, error) { // Adjust the end of the range to after leading whitespace and comments. - prevStart, start := token.NoPos, rng.Start + prevStart := token.NoPos startComment := sort.Search(len(file.Comments), func(i int) bool { // Find the index for the first comment that ends after range start. - return file.Comments[i].End() > rng.Start + return file.Comments[i].End() > start }) for prevStart != start { prevStart = start @@ -670,7 +670,7 @@ func adjustRangeForCommentsAndWhiteSpace(rng safetoken.Range, tok *token.File, c // Move forwards to find a non-whitespace character. offset, err := safetoken.Offset(tok, start) if err != nil { - return safetoken.Range{}, err + return 0, 0, err } for offset < len(content) && isGoWhiteSpace(content[offset]) { offset++ @@ -679,10 +679,10 @@ func adjustRangeForCommentsAndWhiteSpace(rng safetoken.Range, tok *token.File, c } // Adjust the end of the range to before trailing whitespace and comments. - prevEnd, end := token.NoPos, rng.End + prevEnd := token.NoPos endComment := sort.Search(len(file.Comments), func(i int) bool { // Find the index for the first comment that ends after the range end. - return file.Comments[i].End() >= rng.End + return file.Comments[i].End() >= end }) // Search will return n if not found, so we need to adjust if there are no // comments that would match. @@ -700,7 +700,7 @@ func adjustRangeForCommentsAndWhiteSpace(rng safetoken.Range, tok *token.File, c // Move backwards to find a non-whitespace character. offset, err := safetoken.Offset(tok, end) if err != nil { - return safetoken.Range{}, err + return 0, 0, err } for offset > 0 && isGoWhiteSpace(content[offset-1]) { offset-- @@ -708,7 +708,7 @@ func adjustRangeForCommentsAndWhiteSpace(rng safetoken.Range, tok *token.File, c end = tok.Pos(offset) } - return safetoken.NewRange(tok, start, end), nil + return start, end, nil } // isGoWhiteSpace returns true if b is a considered white space in @@ -752,7 +752,7 @@ type variable struct { // variables will be used as arguments in the extracted function. It also returns a // list of identifiers that may need to be returned by the extracted function. // Some of the code in this function has been adapted from tools/cmd/guru/freevars.go. -func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *types.Scope, rng safetoken.Range, node ast.Node) ([]*variable, error) { +func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *types.Scope, start, end token.Pos, node ast.Node) ([]*variable, error) { // id returns non-nil if n denotes an object that is referenced by the span // and defined either within the span or in the lexical environment. The bool // return value acts as an indicator for where it was defined. @@ -777,7 +777,7 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *type if scope == fileScope || scope == pkgScope { return nil, false // defined at file or package scope } - if rng.Start <= obj.Pos() && obj.Pos() <= rng.End { + if start <= obj.Pos() && obj.Pos() <= end { return obj, false // defined within selection => not free } return obj, true @@ -802,7 +802,7 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *type if n == nil { return false } - if rng.Start <= n.Pos() && n.End() <= rng.End { + if start <= n.Pos() && n.End() <= end { var obj types.Object var isFree, prune bool switch n := n.(type) { @@ -828,7 +828,7 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *type } } } - return n.Pos() <= rng.End + return n.Pos() <= end }) // Find identifiers that are initialized or whose values are altered at some @@ -845,8 +845,8 @@ func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *type if n == nil { return false } - if n.Pos() < rng.Start || n.End() > rng.End { - return n.Pos() <= rng.End + if n.Pos() < start || n.End() > end { + return n.Pos() <= end } switch n := n.(type) { case *ast.AssignStmt: @@ -959,25 +959,25 @@ func referencesObj(info *types.Info, expr ast.Expr, obj types.Object) bool { } type fnExtractParams struct { - tok *token.File - path []ast.Node - rng safetoken.Range - outer *ast.FuncDecl - start ast.Node + tok *token.File + start, end token.Pos + path []ast.Node + outer *ast.FuncDecl + node ast.Node } // CanExtractFunction reports whether the code in the given range can be // extracted to a function. -func CanExtractFunction(tok *token.File, rng safetoken.Range, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) { - if rng.Start == rng.End { +func CanExtractFunction(tok *token.File, start, end token.Pos, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) { + if start == end { return nil, false, false, fmt.Errorf("start and end are equal") } var err error - rng, err = adjustRangeForCommentsAndWhiteSpace(rng, tok, src, file) + start, end, err = adjustRangeForCommentsAndWhiteSpace(tok, start, end, src, file) if err != nil { return nil, false, false, err } - path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.End) + path, _ := astutil.PathEnclosingInterval(file, start, end) if len(path) == 0 { return nil, false, false, fmt.Errorf("no path enclosing interval") } @@ -1001,52 +1001,53 @@ func CanExtractFunction(tok *token.File, rng safetoken.Range, src []byte, file * } // Find the nodes at the start and end of the selection. - var start, end ast.Node + var startNode, endNode ast.Node ast.Inspect(outer, func(n ast.Node) bool { if n == nil { return false } // Do not override 'start' with a node that begins at the same location // but is nested further from 'outer'. - if start == nil && n.Pos() == rng.Start && n.End() <= rng.End { - start = n + if startNode == nil && n.Pos() == start && n.End() <= end { + startNode = n } - if end == nil && n.End() == rng.End && n.Pos() >= rng.Start { - end = n + if endNode == nil && n.End() == end && n.Pos() >= start { + endNode = n } - return n.Pos() <= rng.End + return n.Pos() <= end }) - if start == nil || end == nil { + if startNode == nil || endNode == nil { return nil, false, false, fmt.Errorf("range does not map to AST nodes") } // If the region is a blockStmt, use the first and last nodes in the block // statement. // { ... } => { ... } - if blockStmt, ok := start.(*ast.BlockStmt); ok { + if blockStmt, ok := startNode.(*ast.BlockStmt); ok { if len(blockStmt.List) == 0 { return nil, false, false, fmt.Errorf("range maps to empty block statement") } - start, end = blockStmt.List[0], blockStmt.List[len(blockStmt.List)-1] - rng.Start, rng.End = start.Pos(), end.End() + startNode, endNode = blockStmt.List[0], blockStmt.List[len(blockStmt.List)-1] + start, end = startNode.Pos(), endNode.End() } return &fnExtractParams{ tok: tok, + start: start, + end: end, path: path, - rng: rng, outer: outer, - start: start, + node: startNode, }, true, outer.Recv != nil, nil } // objUsed checks if the object is used within the range. It returns the first // occurrence of the object in the range, if it exists. -func objUsed(info *types.Info, rng safetoken.Range, obj types.Object) (bool, *ast.Ident) { +func objUsed(info *types.Info, start, end token.Pos, obj types.Object) (bool, *ast.Ident) { var firstUse *ast.Ident for id, objUse := range info.Uses { if obj != objUse { continue } - if id.Pos() < rng.Start || id.End() > rng.End { + if id.Pos() < start || id.End() > end { continue } if firstUse == nil || id.Pos() < firstUse.Pos() { diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 49bce04b6e0..d5eca7658ba 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" ) @@ -28,7 +27,7 @@ type ( // separately. Such analyzers should provide a function with a signature of // SuggestedFixFunc. SuggestedFixFunc func(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) - singleFileFixFunc func(fset *token.FileSet, rng safetoken.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) + singleFileFixFunc func(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) ) const ( @@ -57,11 +56,11 @@ func singleFile(sf singleFileFixFunc) SuggestedFixFunc { if err != nil { return nil, nil, err } - rng, err := pgf.RangeToTokenRange(pRng) + start, end, err := pgf.RangePos(pRng) if err != nil { return nil, nil, err } - fix, err := sf(pkg.FileSet(), rng, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()) + fix, err := sf(pkg.FileSet(), start, end, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()) return pkg.FileSet(), fix, err } } diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go index da2e31a27c8..c5520d9f9dd 100644 --- a/gopls/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -109,11 +109,11 @@ func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng proto start, end := pgf.File.Pos(), pgf.File.End() if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character { // Adjust start and end for the specified range. - rng, err := pgf.RangeToTokenRange(pRng) + var err error + start, end, err = pgf.RangePos(pRng) if err != nil { return nil, err } - start, end = rng.Start, rng.End } var hints []protocol.InlayHint diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go index c3f2661765d..5f6fe14a852 100644 --- a/gopls/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -218,12 +218,12 @@ func deduceIfaceName(concretePkg, ifacePkg *types.Package, ifaceObj types.Object } func getStubNodes(pgf *ParsedGoFile, pRng protocol.Range) ([]ast.Node, token.Pos, error) { - rng, err := pgf.RangeToTokenRange(pRng) + start, end, err := pgf.RangePos(pRng) if err != nil { return nil, 0, err } - nodes, _ := astutil.PathEnclosingInterval(pgf.File, rng.Start, rng.End) - return nodes, rng.Start, nil + nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end) + return nodes, start, nil } /* diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 2227c5cd5e8..8798e1b8eed 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -419,13 +419,13 @@ func (pgf *ParsedGoFile) NodeLocation(node ast.Node) (protocol.Location, error) return pgf.Mapper.PosLocation(pgf.Tok, node.Pos(), node.End()) } -// RangeToTokenRange parses a protocol Range back into the go/token domain. -func (pgf *ParsedGoFile) RangeToTokenRange(r protocol.Range) (safetoken.Range, error) { +// RangePos parses a protocol Range back into the go/token domain. +func (pgf *ParsedGoFile) RangePos(r protocol.Range) (token.Pos, token.Pos, error) { start, end, err := pgf.Mapper.RangeOffsets(r) if err != nil { - return safetoken.Range{}, err + return token.NoPos, token.NoPos, err } - return safetoken.NewRange(pgf.Tok, pgf.Tok.Pos(start), pgf.Tok.Pos(end)), nil + return pgf.Tok.Pos(start), pgf.Tok.Pos(end), nil } // A ParsedModule contains the results of parsing a go.mod file. From 41adf8d4f9275dc0f1a7d1e831f8878405658816 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 1 Feb 2023 13:39:30 -0500 Subject: [PATCH 703/723] gopls/internal/lsp/tests: remove StripSubscripts Type parameter strings have not had subscripts for a long time. Change-Id: If43c443720335ae98d6b032f23b02639c639af6b Reviewed-on: https://go-review.googlesource.com/c/tools/+/464475 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Robert Findley --- go/types/objectpath/objectpath_test.go | 23 ++--------------------- gopls/internal/lsp/lsp_test.go | 3 +-- gopls/internal/lsp/tests/util.go | 19 ------------------- internal/gcimporter/gcimporter_test.go | 16 ---------------- 4 files changed, 3 insertions(+), 58 deletions(-) diff --git a/go/types/objectpath/objectpath_test.go b/go/types/objectpath/objectpath_test.go index 39e7b1bcdf6..adfad2cd2cd 100644 --- a/go/types/objectpath/objectpath_test.go +++ b/go/types/objectpath/objectpath_test.go @@ -182,7 +182,7 @@ func testPath(prog *loader.Program, test pathTest) error { return fmt.Errorf("Object(%s, %q) returned error %q, want %q", pkg.Path(), test.path, err, test.wantErr) } if test.wantErr != "" { - if got := stripSubscripts(err.Error()); got != test.wantErr { + if got := err.Error(); got != test.wantErr { return fmt.Errorf("Object(%s, %q) error was %q, want %q", pkg.Path(), test.path, got, test.wantErr) } @@ -190,7 +190,7 @@ func testPath(prog *loader.Program, test pathTest) error { } // Inv: err == nil - if objString := stripSubscripts(obj.String()); objString != test.wantobj { + if objString := obj.String(); objString != test.wantobj { return fmt.Errorf("Object(%s, %q) = %s, want %s", pkg.Path(), test.path, objString, test.wantobj) } if obj.Pkg() != pkg { @@ -215,25 +215,6 @@ func testPath(prog *loader.Program, test pathTest) error { return nil } -// stripSubscripts removes type parameter id subscripts. -// -// TODO(rfindley): remove this function once subscripts are removed from the -// type parameter type string. -func stripSubscripts(s string) string { - var runes []rune - for _, r := range s { - // For debugging/uniqueness purposes, TypeString on a type parameter adds a - // subscript corresponding to the type parameter's unique id. This is going - // to be removed, but in the meantime we skip the subscript runes to get a - // deterministic output. - if '₀' <= r && r < '₀'+10 { - continue // trim type parameter subscripts - } - runes = append(runes, r) - } - return string(runes) -} - // TestSourceAndExportData uses objectpath to compute a correspondence // of objects between two versions of the same package, one loaded from // source, the other from export data. diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index e6488284b81..753954a9bcd 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -722,8 +722,7 @@ func (r *runner) Definition(t *testing.T, _ span.Span, d tests.Definition) { expectHover := string(r.data.Golden(t, tag, d.Src.URI().Filename(), func() ([]byte, error) { return []byte(hover.Contents.Value), nil })) - got := tests.StripSubscripts(hover.Contents.Value) - expectHover = tests.StripSubscripts(expectHover) + got := hover.Contents.Value if got != expectHover { tests.CheckSameMarkdown(t, got, expectHover) } diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index 344a768fac9..fd65ecb55fa 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -519,25 +519,6 @@ func WorkspaceSymbolsTestTypeToMatcher(typ WorkspaceSymbolsTestType) source.Symb } } -// StripSubscripts removes type parameter id subscripts. -// -// TODO(rfindley): remove this function once subscripts are removed from the -// type parameter type string. -func StripSubscripts(s string) string { - var runes []rune - for _, r := range s { - // For debugging/uniqueness purposes, TypeString on a type parameter adds a - // subscript corresponding to the type parameter's unique id. This is going - // to be removed, but in the meantime we skip the subscript runes to get a - // deterministic output. - if '₀' <= r && r < '₀'+10 { - continue // trim type parameter subscripts - } - runes = append(runes, r) - } - return string(runes) -} - // LocationsToSpans converts protocol location into span form for testing. func LocationsToSpans(data *Data, locs []protocol.Location) ([]span.Span, error) { spans := make([]span.Span, len(locs)) diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index afc54475cec..6ef704c53a5 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -237,14 +237,12 @@ func TestImportTypeparamTests(t *testing.T) { importedObj := imported.Scope().Lookup(name) got := types.ObjectString(importedObj, types.RelativeTo(imported)) - got = sanitizeObjectString(got) checkedObj := checked.Scope().Lookup(name) if checkedObj == nil { t.Fatalf("imported object %q was not type-checked", name) } want := types.ObjectString(checkedObj, types.RelativeTo(checked)) - want = sanitizeObjectString(want) if got != want { t.Errorf("imported %q as %q, want %q", name, got, want) @@ -261,20 +259,6 @@ func TestImportTypeparamTests(t *testing.T) { } } -// sanitizeObjectString removes type parameter debugging markers from an object -// string, to normalize it for comparison. -// TODO(rfindley): this should not be necessary. -func sanitizeObjectString(s string) string { - var runes []rune - for _, r := range s { - if '₀' <= r && r < '₀'+10 { - continue // trim type parameter subscripts - } - runes = append(runes, r) - } - return string(runes) -} - func checkFile(t *testing.T, filename string, src []byte) *types.Package { fset := token.NewFileSet() f, err := goparser.ParseFile(fset, filename, src, 0) From 0cfddb3c3da72ca6d8baa1b01918eb8f1f9acf38 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 1 Feb 2023 14:06:19 +0700 Subject: [PATCH 704/723] gopls/internal/lsp: enable clear builtin completion test For golang/go#56351 Change-Id: I3a6e3ef58285d46fe239e900d98fdb5617a1733c Reviewed-on: https://go-review.googlesource.com/c/tools/+/464240 TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Run-TryBot: Cuong Manh Le Reviewed-by: Michael Knyszek Auto-Submit: Cuong Manh Le gopls-CI: kokoro --- gopls/internal/lsp/completion_test.go | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index d9a3e0df666..cd3bcec992c 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -25,8 +25,8 @@ func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal") opts.ExperimentalPostfixCompletions = strings.Contains(string(src.URI()), "postfix") }) - got = filterSkipCompletionItems(tests.FilterBuiltins(src, got)) - want := filterSkipCompletionItems(expected(t, test, items)) + got = tests.FilterBuiltins(src, got) + want := expected(t, test, items) if diff := tests.DiffCompletionItems(want, got); diff != "" { t.Errorf("mismatching completion items (-want +got):\n%s", diff) } @@ -174,16 +174,3 @@ func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*sourc } return list.Items } - -func filterSkipCompletionItems(items []protocol.CompletionItem) []protocol.CompletionItem { - n := 0 - for _, item := range items { - // TODO(cuonglm): remove once https://go-review.googlesource.com/c/go/+/462935 land. - if item.Label == "clear" { - continue - } - items[n] = item - n++ - } - return items[:n] -} From 6bd0d004858b61494c563b151514dea3a341a123 Mon Sep 17 00:00:00 2001 From: Viktor Blomqvist Date: Thu, 26 Jan 2023 21:48:26 +0100 Subject: [PATCH 705/723] gopls/internal/lsp: go to definition from linkname directive Enables jump to definition on the second argument in //go:linkname localname importpath.name if importpath is a transitive (possibly reverse) dependency of the package where the directive is located. Updates golang/go#57312 Change-Id: I59fa5821ffd44449cf49045a88b429f21e22febc Reviewed-on: https://go-review.googlesource.com/c/tools/+/463755 Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley --- gopls/internal/lsp/definition.go | 6 + gopls/internal/lsp/source/linkname.go | 165 ++++++++++++++++++ .../internal/regtest/misc/definition_test.go | 98 +++++++++++ 3 files changed, 269 insertions(+) create mode 100644 gopls/internal/lsp/source/linkname.go diff --git a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go index cea57c3f630..6259d4dbb84 100644 --- a/gopls/internal/lsp/definition.go +++ b/gopls/internal/lsp/definition.go @@ -6,6 +6,7 @@ package lsp import ( "context" + "errors" "fmt" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -24,6 +25,11 @@ func (s *Server) definition(ctx context.Context, params *protocol.DefinitionPara case source.Tmpl: return template.Definition(snapshot, fh, params.Position) case source.Go: + // Partial support for jumping from linkname directive (position at 2nd argument). + locations, err := source.LinknameDefinition(ctx, snapshot, fh, params.Position) + if !errors.Is(err, source.ErrNoLinkname) { + return locations, err + } return source.Definition(ctx, snapshot, fh, params.Position) default: return nil, fmt.Errorf("can't find definitions for file type %s", kind) diff --git a/gopls/internal/lsp/source/linkname.go b/gopls/internal/lsp/source/linkname.go new file mode 100644 index 00000000000..4fb667e6860 --- /dev/null +++ b/gopls/internal/lsp/source/linkname.go @@ -0,0 +1,165 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "context" + "errors" + "fmt" + "go/token" + "strings" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" +) + +// ErrNoLinkname is returned by LinknameDefinition when no linkname +// directive is found at a particular position. +// As such it indicates that other definitions could be worth checking. +var ErrNoLinkname = errors.New("no linkname directive found") + +// LinknameDefinition finds the definition of the linkname directive in fh at pos. +// If there is no linkname directive at pos, returns ErrNoLinkname. +func LinknameDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.Location, error) { + pkgPath, name := parseLinkname(ctx, snapshot, fh, pos) + if pkgPath == "" { + return nil, ErrNoLinkname + } + return findLinkname(ctx, snapshot, fh, pos, PackagePath(pkgPath), name) +} + +// parseLinkname attempts to parse a go:linkname declaration at the given pos. +// If successful, it returns the package path and object name referenced by the second +// argument of the linkname directive. +// +// If the position is not in the second argument of a go:linkname directive, or parsing fails, it returns "", "". +func parseLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (pkgPath, name string) { + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + if err != nil { + return "", "" + } + + span, err := pgf.Mapper.PositionPoint(pos) + if err != nil { + return "", "" + } + atLine := span.Line() + atColumn := span.Column() + + // Looking for pkgpath in '//go:linkname f pkgpath.g'. + // (We ignore 1-arg linkname directives.) + directive, column := findLinknameOnLine(pgf, atLine) + parts := strings.Fields(directive) + if len(parts) != 3 { + return "", "" + } + + // Inside 2nd arg [start, end]? + end := column + len(directive) + start := end - len(parts[2]) + if !(start <= atColumn && atColumn <= end) { + return "", "" + } + linkname := parts[2] + + // Split the pkg path from the name. + dot := strings.LastIndexByte(linkname, '.') + if dot < 0 { + return "", "" + } + return linkname[:dot], linkname[dot+1:] +} + +// findLinknameOnLine returns the first linkname directive on line and the column it starts at. +// Returns "", 0 if no linkname directive is found on the line. +func findLinknameOnLine(pgf *ParsedGoFile, line int) (string, int) { + for _, grp := range pgf.File.Comments { + for _, com := range grp.List { + if strings.HasPrefix(com.Text, "//go:linkname") { + p := safetoken.Position(pgf.Tok, com.Pos()) + if p.Line == line { + return com.Text, p.Column + } + } + } + } + return "", 0 +} + +// findLinkname searches dependencies of packages containing fh for an object +// with linker name matching the given package path and name. +func findLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position, pkgPath PackagePath, name string) ([]protocol.Location, error) { + metas, err := snapshot.MetadataForFile(ctx, fh.URI()) + if err != nil { + return nil, err + } + if len(metas) == 0 { + return nil, fmt.Errorf("no package found for file %q", fh.URI()) + } + + // Find package starting from narrowest package metadata. + pkgMeta := findPackageInDeps(snapshot, metas[0], pkgPath) + if pkgMeta == nil { + // Fall back to searching reverse dependencies. + reverse, err := snapshot.ReverseDependencies(ctx, metas[0].ID, true /* transitive */) + if err != nil { + return nil, err + } + for _, dep := range reverse { + if dep.PkgPath == pkgPath { + pkgMeta = dep + break + } + } + if pkgMeta == nil { + return nil, fmt.Errorf("cannot find package %q", pkgPath) + } + } + + // When found, type check the desired package (snapshot.TypeCheck in TypecheckFull mode), + pkgs, err := snapshot.TypeCheck(ctx, TypecheckFull, pkgMeta.ID) + if err != nil { + return nil, err + } + pkg := pkgs[0] + + obj := pkg.GetTypes().Scope().Lookup(name) + if obj == nil { + return nil, fmt.Errorf("package %q does not define %s", pkgPath, name) + } + + objURI := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) + pgf, err := pkg.File(span.URIFromPath(objURI.Filename)) + if err != nil { + return nil, err + } + loc, err := pgf.PosLocation(obj.Pos(), obj.Pos()+token.Pos(len(name))) + if err != nil { + return nil, err + } + return []protocol.Location{loc}, nil +} + +// findPackageInDeps returns the dependency of meta of the specified package path, if any. +func findPackageInDeps(snapshot Snapshot, meta *Metadata, pkgPath PackagePath) *Metadata { + seen := make(map[*Metadata]bool) + var visit func(*Metadata) *Metadata + visit = func(meta *Metadata) *Metadata { + if !seen[meta] { + seen[meta] = true + if meta.PkgPath == pkgPath { + return meta + } + for _, id := range meta.DepsByPkgPath { + if m := visit(snapshot.Metadata(id)); m != nil { + return m + } + } + } + return nil + } + return visit(meta) +} diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index 70a3336e72b..7767ac5aaa3 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -49,6 +49,104 @@ func TestGoToInternalDefinition(t *testing.T) { }) } +const linknameDefinition = ` +-- go.mod -- +module mod.com + +-- upper/upper.go -- +package upper + +import ( + _ "unsafe" + + _ "mod.com/middle" +) + +//go:linkname foo mod.com/lower.bar +func foo() string + +-- middle/middle.go -- +package middle + +import ( + _ "mod.com/lower" +) + +-- lower/lower.s -- + +-- lower/lower.go -- +package lower + +func bar() string { + return "bar as foo" +}` + +func TestGoToLinknameDefinition(t *testing.T) { + Run(t, linknameDefinition, func(t *testing.T, env *Env) { + env.OpenFile("upper/upper.go") + + // Jump from directives 2nd arg. + start := env.RegexpSearch("upper/upper.go", `lower.bar`) + loc := env.GoToDefinition(start) + name := env.Sandbox.Workdir.URIToPath(loc.URI) + if want := "lower/lower.go"; name != want { + t.Errorf("GoToDefinition: got file %q, want %q", name, want) + } + if want := env.RegexpSearch("lower/lower.go", `bar`); loc != want { + t.Errorf("GoToDefinition: got position %v, want %v", loc, want) + } + }) +} + +const linknameDefinitionReverse = ` +-- go.mod -- +module mod.com + +-- upper/upper.s -- + +-- upper/upper.go -- +package upper + +import ( + _ "mod.com/middle" +) + +func foo() string + +-- middle/middle.go -- +package middle + +import ( + _ "mod.com/lower" +) + +-- lower/lower.go -- +package lower + +import _ "unsafe" + +//go:linkname bar mod.com/upper.foo +func bar() string { + return "bar as foo" +}` + +func TestGoToLinknameDefinitionInReverseDep(t *testing.T) { + Run(t, linknameDefinitionReverse, func(t *testing.T, env *Env) { + env.OpenFile("lower/lower.go") + + // Jump from directives 2nd arg. + start := env.RegexpSearch("lower/lower.go", `upper.foo`) + loc := env.GoToDefinition(start) + name := env.Sandbox.Workdir.URIToPath(loc.URI) + if want := "upper/upper.go"; name != want { + t.Errorf("GoToDefinition: got file %q, want %q", name, want) + } + if want := env.RegexpSearch("upper/upper.go", `foo`); loc != want { + t.Errorf("GoToDefinition: got position %v, want %v", loc, want) + } + }) +} + const stdlibDefinition = ` -- go.mod -- module mod.com From 4e8ff892aedc4b89db90a8612f698d4c8579e23f Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 1 Feb 2023 13:39:47 -0800 Subject: [PATCH 706/723] internal/imports: update stdlib index for 1.20 Change-Id: Ia4f847652ca17f38d83214d8211be99365d9eca3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/464342 Auto-Submit: Brad Fitzpatrick gopls-CI: kokoro Run-TryBot: Brad Fitzpatrick Run-TryBot: Robert Findley Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Reviewed-by: Damien Neil --- internal/imports/zstdlib.go | 182 ++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/internal/imports/zstdlib.go b/internal/imports/zstdlib.go index 5db9b2d4c73..93396440ef0 100644 --- a/internal/imports/zstdlib.go +++ b/internal/imports/zstdlib.go @@ -10,6 +10,7 @@ var stdlib = map[string][]string{ "archive/tar": { "ErrFieldTooLong", "ErrHeader", + "ErrInsecurePath", "ErrWriteAfterClose", "ErrWriteTooLong", "FileInfoHeader", @@ -45,6 +46,7 @@ var stdlib = map[string][]string{ "ErrAlgorithm", "ErrChecksum", "ErrFormat", + "ErrInsecurePath", "File", "FileHeader", "FileInfoHeader", @@ -87,12 +89,15 @@ var stdlib = map[string][]string{ }, "bytes": { "Buffer", + "Clone", "Compare", "Contains", "ContainsAny", "ContainsRune", "Count", "Cut", + "CutPrefix", + "CutSuffix", "Equal", "EqualFold", "ErrTooLarge", @@ -224,12 +229,15 @@ var stdlib = map[string][]string{ }, "context": { "Background", + "CancelCauseFunc", "CancelFunc", "Canceled", + "Cause", "Context", "DeadlineExceeded", "TODO", "WithCancel", + "WithCancelCause", "WithDeadline", "WithTimeout", "WithValue", @@ -306,6 +314,15 @@ var stdlib = map[string][]string{ "Sign", "Verify", }, + "crypto/ecdh": { + "Curve", + "P256", + "P384", + "P521", + "PrivateKey", + "PublicKey", + "X25519", + }, "crypto/ecdsa": { "GenerateKey", "PrivateKey", @@ -318,6 +335,7 @@ var stdlib = map[string][]string{ "crypto/ed25519": { "GenerateKey", "NewKeyFromSeed", + "Options", "PrivateKey", "PrivateKeySize", "PublicKey", @@ -326,6 +344,7 @@ var stdlib = map[string][]string{ "Sign", "SignatureSize", "Verify", + "VerifyWithOptions", }, "crypto/elliptic": { "Curve", @@ -423,10 +442,12 @@ var stdlib = map[string][]string{ "ConstantTimeEq", "ConstantTimeLessOrEq", "ConstantTimeSelect", + "XORBytes", }, "crypto/tls": { "Certificate", "CertificateRequestInfo", + "CertificateVerificationError", "CipherSuite", "CipherSuiteName", "CipherSuites", @@ -604,6 +625,7 @@ var stdlib = map[string][]string{ "SHA384WithRSAPSS", "SHA512WithRSA", "SHA512WithRSAPSS", + "SetFallbackRoots", "SignatureAlgorithm", "SystemCertPool", "SystemRootsError", @@ -1828,19 +1850,42 @@ var stdlib = map[string][]string{ "R_INFO32", "R_LARCH", "R_LARCH_32", + "R_LARCH_32_PCREL", "R_LARCH_64", + "R_LARCH_ABS64_HI12", + "R_LARCH_ABS64_LO20", + "R_LARCH_ABS_HI20", + "R_LARCH_ABS_LO12", "R_LARCH_ADD16", "R_LARCH_ADD24", "R_LARCH_ADD32", "R_LARCH_ADD64", "R_LARCH_ADD8", + "R_LARCH_B16", + "R_LARCH_B21", + "R_LARCH_B26", "R_LARCH_COPY", + "R_LARCH_GNU_VTENTRY", + "R_LARCH_GNU_VTINHERIT", + "R_LARCH_GOT64_HI12", + "R_LARCH_GOT64_LO20", + "R_LARCH_GOT64_PC_HI12", + "R_LARCH_GOT64_PC_LO20", + "R_LARCH_GOT_HI20", + "R_LARCH_GOT_LO12", + "R_LARCH_GOT_PC_HI20", + "R_LARCH_GOT_PC_LO12", "R_LARCH_IRELATIVE", "R_LARCH_JUMP_SLOT", "R_LARCH_MARK_LA", "R_LARCH_MARK_PCREL", "R_LARCH_NONE", + "R_LARCH_PCALA64_HI12", + "R_LARCH_PCALA64_LO20", + "R_LARCH_PCALA_HI20", + "R_LARCH_PCALA_LO12", "R_LARCH_RELATIVE", + "R_LARCH_RELAX", "R_LARCH_SOP_ADD", "R_LARCH_SOP_AND", "R_LARCH_SOP_ASSERT", @@ -1875,6 +1920,22 @@ var stdlib = map[string][]string{ "R_LARCH_TLS_DTPMOD64", "R_LARCH_TLS_DTPREL32", "R_LARCH_TLS_DTPREL64", + "R_LARCH_TLS_GD_HI20", + "R_LARCH_TLS_GD_PC_HI20", + "R_LARCH_TLS_IE64_HI12", + "R_LARCH_TLS_IE64_LO20", + "R_LARCH_TLS_IE64_PC_HI12", + "R_LARCH_TLS_IE64_PC_LO20", + "R_LARCH_TLS_IE_HI20", + "R_LARCH_TLS_IE_LO12", + "R_LARCH_TLS_IE_PC_HI20", + "R_LARCH_TLS_IE_PC_LO12", + "R_LARCH_TLS_LD_HI20", + "R_LARCH_TLS_LD_PC_HI20", + "R_LARCH_TLS_LE64_HI12", + "R_LARCH_TLS_LE64_LO20", + "R_LARCH_TLS_LE_HI20", + "R_LARCH_TLS_LE_LO12", "R_LARCH_TLS_TPREL32", "R_LARCH_TLS_TPREL64", "R_MIPS", @@ -1938,15 +1999,25 @@ var stdlib = map[string][]string{ "R_PPC64_ADDR16_HIGH", "R_PPC64_ADDR16_HIGHA", "R_PPC64_ADDR16_HIGHER", + "R_PPC64_ADDR16_HIGHER34", "R_PPC64_ADDR16_HIGHERA", + "R_PPC64_ADDR16_HIGHERA34", "R_PPC64_ADDR16_HIGHEST", + "R_PPC64_ADDR16_HIGHEST34", "R_PPC64_ADDR16_HIGHESTA", + "R_PPC64_ADDR16_HIGHESTA34", "R_PPC64_ADDR16_LO", "R_PPC64_ADDR16_LO_DS", "R_PPC64_ADDR24", "R_PPC64_ADDR32", "R_PPC64_ADDR64", "R_PPC64_ADDR64_LOCAL", + "R_PPC64_COPY", + "R_PPC64_D28", + "R_PPC64_D34", + "R_PPC64_D34_HA30", + "R_PPC64_D34_HI30", + "R_PPC64_D34_LO", "R_PPC64_DTPMOD64", "R_PPC64_DTPREL16", "R_PPC64_DTPREL16_DS", @@ -1960,8 +2031,12 @@ var stdlib = map[string][]string{ "R_PPC64_DTPREL16_HIGHESTA", "R_PPC64_DTPREL16_LO", "R_PPC64_DTPREL16_LO_DS", + "R_PPC64_DTPREL34", "R_PPC64_DTPREL64", "R_PPC64_ENTRY", + "R_PPC64_GLOB_DAT", + "R_PPC64_GNU_VTENTRY", + "R_PPC64_GNU_VTINHERIT", "R_PPC64_GOT16", "R_PPC64_GOT16_DS", "R_PPC64_GOT16_HA", @@ -1972,29 +2047,50 @@ var stdlib = map[string][]string{ "R_PPC64_GOT_DTPREL16_HA", "R_PPC64_GOT_DTPREL16_HI", "R_PPC64_GOT_DTPREL16_LO_DS", + "R_PPC64_GOT_DTPREL_PCREL34", + "R_PPC64_GOT_PCREL34", "R_PPC64_GOT_TLSGD16", "R_PPC64_GOT_TLSGD16_HA", "R_PPC64_GOT_TLSGD16_HI", "R_PPC64_GOT_TLSGD16_LO", + "R_PPC64_GOT_TLSGD_PCREL34", "R_PPC64_GOT_TLSLD16", "R_PPC64_GOT_TLSLD16_HA", "R_PPC64_GOT_TLSLD16_HI", "R_PPC64_GOT_TLSLD16_LO", + "R_PPC64_GOT_TLSLD_PCREL34", "R_PPC64_GOT_TPREL16_DS", "R_PPC64_GOT_TPREL16_HA", "R_PPC64_GOT_TPREL16_HI", "R_PPC64_GOT_TPREL16_LO_DS", + "R_PPC64_GOT_TPREL_PCREL34", "R_PPC64_IRELATIVE", "R_PPC64_JMP_IREL", "R_PPC64_JMP_SLOT", "R_PPC64_NONE", + "R_PPC64_PCREL28", + "R_PPC64_PCREL34", + "R_PPC64_PCREL_OPT", + "R_PPC64_PLT16_HA", + "R_PPC64_PLT16_HI", + "R_PPC64_PLT16_LO", "R_PPC64_PLT16_LO_DS", + "R_PPC64_PLT32", + "R_PPC64_PLT64", + "R_PPC64_PLTCALL", + "R_PPC64_PLTCALL_NOTOC", "R_PPC64_PLTGOT16", "R_PPC64_PLTGOT16_DS", "R_PPC64_PLTGOT16_HA", "R_PPC64_PLTGOT16_HI", "R_PPC64_PLTGOT16_LO", "R_PPC64_PLTGOT_LO_DS", + "R_PPC64_PLTREL32", + "R_PPC64_PLTREL64", + "R_PPC64_PLTSEQ", + "R_PPC64_PLTSEQ_NOTOC", + "R_PPC64_PLT_PCREL34", + "R_PPC64_PLT_PCREL34_NOTOC", "R_PPC64_REL14", "R_PPC64_REL14_BRNTAKEN", "R_PPC64_REL14_BRTAKEN", @@ -2002,13 +2098,28 @@ var stdlib = map[string][]string{ "R_PPC64_REL16DX_HA", "R_PPC64_REL16_HA", "R_PPC64_REL16_HI", + "R_PPC64_REL16_HIGH", + "R_PPC64_REL16_HIGHA", + "R_PPC64_REL16_HIGHER", + "R_PPC64_REL16_HIGHER34", + "R_PPC64_REL16_HIGHERA", + "R_PPC64_REL16_HIGHERA34", + "R_PPC64_REL16_HIGHEST", + "R_PPC64_REL16_HIGHEST34", + "R_PPC64_REL16_HIGHESTA", + "R_PPC64_REL16_HIGHESTA34", "R_PPC64_REL16_LO", "R_PPC64_REL24", "R_PPC64_REL24_NOTOC", + "R_PPC64_REL30", "R_PPC64_REL32", "R_PPC64_REL64", "R_PPC64_RELATIVE", + "R_PPC64_SECTOFF", "R_PPC64_SECTOFF_DS", + "R_PPC64_SECTOFF_HA", + "R_PPC64_SECTOFF_HI", + "R_PPC64_SECTOFF_LO", "R_PPC64_SECTOFF_LO_DS", "R_PPC64_TLS", "R_PPC64_TLSGD", @@ -2033,7 +2144,11 @@ var stdlib = map[string][]string{ "R_PPC64_TPREL16_HIGHESTA", "R_PPC64_TPREL16_LO", "R_PPC64_TPREL16_LO_DS", + "R_PPC64_TPREL34", "R_PPC64_TPREL64", + "R_PPC64_UADDR16", + "R_PPC64_UADDR32", + "R_PPC64_UADDR64", "R_PPC_ADDR14", "R_PPC_ADDR14_BRNTAKEN", "R_PPC_ADDR14_BRTAKEN", @@ -2581,6 +2696,9 @@ var stdlib = map[string][]string{ "IMAGE_FILE_MACHINE_POWERPC", "IMAGE_FILE_MACHINE_POWERPCFP", "IMAGE_FILE_MACHINE_R4000", + "IMAGE_FILE_MACHINE_RISCV128", + "IMAGE_FILE_MACHINE_RISCV32", + "IMAGE_FILE_MACHINE_RISCV64", "IMAGE_FILE_MACHINE_SH3", "IMAGE_FILE_MACHINE_SH3DSP", "IMAGE_FILE_MACHINE_SH4", @@ -2846,6 +2964,7 @@ var stdlib = map[string][]string{ "errors": { "As", "Is", + "Join", "New", "Unwrap", }, @@ -2916,6 +3035,7 @@ var stdlib = map[string][]string{ "Appendf", "Appendln", "Errorf", + "FormatString", "Formatter", "Fprint", "Fprintf", @@ -3401,6 +3521,7 @@ var stdlib = map[string][]string{ "RecvOnly", "RelativeTo", "Rune", + "Satisfies", "Scope", "Selection", "SelectionKind", @@ -3702,8 +3823,10 @@ var stdlib = map[string][]string{ "LimitedReader", "MultiReader", "MultiWriter", + "NewOffsetWriter", "NewSectionReader", "NopCloser", + "OffsetWriter", "Pipe", "PipeReader", "PipeWriter", @@ -3770,6 +3893,7 @@ var stdlib = map[string][]string{ "ReadDirFile", "ReadFile", "ReadFileFS", + "SkipAll", "SkipDir", "Stat", "StatFS", @@ -4140,6 +4264,7 @@ var stdlib = map[string][]string{ "FlagLoopback", "FlagMulticast", "FlagPointToPoint", + "FlagRunning", "FlagUp", "Flags", "HardwareAddr", @@ -4284,6 +4409,7 @@ var stdlib = map[string][]string{ "NewFileTransport", "NewRequest", "NewRequestWithContext", + "NewResponseController", "NewServeMux", "NoBody", "NotFound", @@ -4303,6 +4429,7 @@ var stdlib = map[string][]string{ "RedirectHandler", "Request", "Response", + "ResponseController", "ResponseWriter", "RoundTripper", "SameSite", @@ -4445,6 +4572,7 @@ var stdlib = map[string][]string{ "NewProxyClientConn", "NewServerConn", "NewSingleHostReverseProxy", + "ProxyRequest", "ReverseProxy", "ServerConn", }, @@ -4476,6 +4604,8 @@ var stdlib = map[string][]string{ "AddrPortFrom", "IPv4Unspecified", "IPv6LinkLocalAllNodes", + "IPv6LinkLocalAllRouters", + "IPv6Loopback", "IPv6Unspecified", "MustParseAddr", "MustParseAddrPort", @@ -4686,6 +4816,7 @@ var stdlib = map[string][]string{ "CommandContext", "ErrDot", "ErrNotFound", + "ErrWaitDelay", "Error", "ExitError", "LookPath", @@ -4734,11 +4865,13 @@ var stdlib = map[string][]string{ "Glob", "HasPrefix", "IsAbs", + "IsLocal", "Join", "ListSeparator", "Match", "Rel", "Separator", + "SkipAll", "SkipDir", "Split", "SplitList", @@ -4860,6 +4993,7 @@ var stdlib = map[string][]string{ "ErrInvalidRepeatOp", "ErrInvalidRepeatSize", "ErrInvalidUTF8", + "ErrLarge", "ErrMissingBracket", "ErrMissingParen", "ErrMissingRepeatArgument", @@ -4968,8 +5102,16 @@ var stdlib = map[string][]string{ }, "runtime/cgo": { "Handle", + "Incomplete", "NewHandle", }, + "runtime/coverage": { + "ClearCounters", + "WriteCounters", + "WriteCountersDir", + "WriteMeta", + "WriteMetaDir", + }, "runtime/debug": { "BuildInfo", "BuildSetting", @@ -5103,6 +5245,8 @@ var stdlib = map[string][]string{ "ContainsRune", "Count", "Cut", + "CutPrefix", + "CutSuffix", "EqualFold", "Fields", "FieldsFunc", @@ -5272,6 +5416,7 @@ var stdlib = map[string][]string{ "AF_TIPC", "AF_UNIX", "AF_UNSPEC", + "AF_UTUN", "AF_VENDOR00", "AF_VENDOR01", "AF_VENDOR02", @@ -5610,20 +5755,25 @@ var stdlib = map[string][]string{ "CLOCAL", "CLONE_CHILD_CLEARTID", "CLONE_CHILD_SETTID", + "CLONE_CLEAR_SIGHAND", "CLONE_CSIGNAL", "CLONE_DETACHED", "CLONE_FILES", "CLONE_FS", + "CLONE_INTO_CGROUP", "CLONE_IO", + "CLONE_NEWCGROUP", "CLONE_NEWIPC", "CLONE_NEWNET", "CLONE_NEWNS", "CLONE_NEWPID", + "CLONE_NEWTIME", "CLONE_NEWUSER", "CLONE_NEWUTS", "CLONE_PARENT", "CLONE_PARENT_SETTID", "CLONE_PID", + "CLONE_PIDFD", "CLONE_PTRACE", "CLONE_SETTLS", "CLONE_SIGHAND", @@ -6166,6 +6316,7 @@ var stdlib = map[string][]string{ "EPROTONOSUPPORT", "EPROTOTYPE", "EPWROFF", + "EQFULL", "ERANGE", "EREMCHG", "EREMOTE", @@ -6592,6 +6743,7 @@ var stdlib = map[string][]string{ "F_DUPFD", "F_DUPFD_CLOEXEC", "F_EXLCK", + "F_FINDSIGS", "F_FLUSH_DATA", "F_FREEZE_FS", "F_FSCTL", @@ -6602,6 +6754,7 @@ var stdlib = map[string][]string{ "F_FSPRIV", "F_FSVOID", "F_FULLFSYNC", + "F_GETCODEDIR", "F_GETFD", "F_GETFL", "F_GETLEASE", @@ -6615,6 +6768,7 @@ var stdlib = map[string][]string{ "F_GETPATH_MTMINFO", "F_GETPIPE_SZ", "F_GETPROTECTIONCLASS", + "F_GETPROTECTIONLEVEL", "F_GETSIG", "F_GLOBAL_NOCACHE", "F_LOCK", @@ -6647,6 +6801,7 @@ var stdlib = map[string][]string{ "F_SETLK64", "F_SETLKW", "F_SETLKW64", + "F_SETLKWTIMEOUT", "F_SETLK_REMOTE", "F_SETNOSIGPIPE", "F_SETOWN", @@ -6656,9 +6811,11 @@ var stdlib = map[string][]string{ "F_SETSIG", "F_SETSIZE", "F_SHLCK", + "F_SINGLE_WRITER", "F_TEST", "F_THAW_FS", "F_TLOCK", + "F_TRANSCODEKEY", "F_ULOCK", "F_UNLCK", "F_UNLCKSYS", @@ -7854,12 +8011,20 @@ var stdlib = map[string][]string{ "NOFLSH", "NOTE_ABSOLUTE", "NOTE_ATTRIB", + "NOTE_BACKGROUND", "NOTE_CHILD", + "NOTE_CRITICAL", "NOTE_DELETE", "NOTE_EOF", "NOTE_EXEC", "NOTE_EXIT", "NOTE_EXITSTATUS", + "NOTE_EXIT_CSERROR", + "NOTE_EXIT_DECRYPTFAIL", + "NOTE_EXIT_DETAIL", + "NOTE_EXIT_DETAIL_MASK", + "NOTE_EXIT_MEMORY", + "NOTE_EXIT_REPARENTED", "NOTE_EXTEND", "NOTE_FFAND", "NOTE_FFCOPY", @@ -7868,6 +8033,7 @@ var stdlib = map[string][]string{ "NOTE_FFNOP", "NOTE_FFOR", "NOTE_FORK", + "NOTE_LEEWAY", "NOTE_LINK", "NOTE_LOWAT", "NOTE_NONE", @@ -7946,6 +8112,7 @@ var stdlib = map[string][]string{ "O_CREAT", "O_DIRECT", "O_DIRECTORY", + "O_DP_GETRAWENCRYPTED", "O_DSYNC", "O_EVTONLY", "O_EXCL", @@ -8235,6 +8402,7 @@ var stdlib = map[string][]string{ "RLIMIT_AS", "RLIMIT_CORE", "RLIMIT_CPU", + "RLIMIT_CPU_USAGE_MONITOR", "RLIMIT_DATA", "RLIMIT_FSIZE", "RLIMIT_NOFILE", @@ -8347,9 +8515,11 @@ var stdlib = map[string][]string{ "RTF_PROTO1", "RTF_PROTO2", "RTF_PROTO3", + "RTF_PROXY", "RTF_REINSTATE", "RTF_REJECT", "RTF_RNH_LOCKED", + "RTF_ROUTER", "RTF_SOURCE", "RTF_SRC", "RTF_STATIC", @@ -8868,6 +9038,7 @@ var stdlib = map[string][]string{ "SO_NO_OFFLOAD", "SO_NP_EXTENSIONS", "SO_NREAD", + "SO_NUMRCVPKT", "SO_NWRITE", "SO_OOBINLINE", "SO_OVERFLOWED", @@ -9037,6 +9208,7 @@ var stdlib = map[string][]string{ "SYS_CREAT", "SYS_CREATE_MODULE", "SYS_CSOPS", + "SYS_CSOPS_AUDITTOKEN", "SYS_DELETE", "SYS_DELETE_MODULE", "SYS_DUP", @@ -9223,6 +9395,7 @@ var stdlib = map[string][]string{ "SYS_JAIL_GET", "SYS_JAIL_REMOVE", "SYS_JAIL_SET", + "SYS_KAS_INFO", "SYS_KDEBUG_TRACE", "SYS_KENV", "SYS_KEVENT", @@ -9250,6 +9423,7 @@ var stdlib = map[string][]string{ "SYS_LCHMOD", "SYS_LCHOWN", "SYS_LCHOWN32", + "SYS_LEDGER", "SYS_LGETFH", "SYS_LGETXATTR", "SYS_LINK", @@ -9346,6 +9520,7 @@ var stdlib = map[string][]string{ "SYS_OPENAT", "SYS_OPENBSD_POLL", "SYS_OPEN_BY_HANDLE_AT", + "SYS_OPEN_DPROTECTED_NP", "SYS_OPEN_EXTENDED", "SYS_OPEN_NOCANCEL", "SYS_OVADVISE", @@ -9978,6 +10153,7 @@ var stdlib = map[string][]string{ "TCP_CONNECTIONTIMEOUT", "TCP_CORK", "TCP_DEFER_ACCEPT", + "TCP_ENABLE_ECN", "TCP_INFO", "TCP_KEEPALIVE", "TCP_KEEPCNT", @@ -10000,11 +10176,13 @@ var stdlib = map[string][]string{ "TCP_NODELAY", "TCP_NOOPT", "TCP_NOPUSH", + "TCP_NOTSENT_LOWAT", "TCP_NSTATES", "TCP_QUICKACK", "TCP_RXT_CONNDROPTIME", "TCP_RXT_FINDROP", "TCP_SACK_ENABLE", + "TCP_SENDMOREACKS", "TCP_SYNCNT", "TCP_VENDOR", "TCP_WINDOW_CLAMP", @@ -10540,6 +10718,8 @@ var stdlib = map[string][]string{ "April", "August", "Date", + "DateOnly", + "DateTime", "December", "Duration", "February", @@ -10594,6 +10774,7 @@ var stdlib = map[string][]string{ "Tick", "Ticker", "Time", + "TimeOnly", "Timer", "Tuesday", "UTC", @@ -10892,6 +11073,7 @@ var stdlib = map[string][]string{ "Zs", }, "unicode/utf16": { + "AppendRune", "Decode", "DecodeRune", "Encode", From 4e98188bf60f529dcf5832694e53f1a7e3a2a614 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Thu, 2 Feb 2023 09:54:37 -0500 Subject: [PATCH 707/723] internal/imports: use go/packages instead of cmd/api to compute symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API of syscall/js package is generated by reusing cmd/api. That has stopped being a runnable command after an internal refactor in Go 1.20. Since cmd/api was never really supported or meant to be used outside of GOROOT, switch to using go/packages and compute the API more directly ourselves. Also use the same approach to generate the API of package unsafe (whose API is also not present in GOROOT/api files at this time) instead of a fixed list that needs manual maintenance. This adds Add and Slice that were added to package unsafe in Go 1.17. It also removes ArbitraryType, since that symbol isn't a part of package unsafe's API but used in its documentation—it seems like an oversight that it was added in CL 24463. This CL intentionally leaves out unsafe's SliceData, String, StringData that were added in Go 1.20, so I can test out the new relui workflow to send a CL that regenerates this package. Fixes golang/go#58245. For golang/go#38706. Change-Id: Ibe0d89bf0469691bd16e0d0f501e3762256f2239 Reviewed-on: https://go-review.googlesource.com/c/tools/+/464715 Reviewed-by: Robert Findley Reviewed-by: Dmitri Shuralyov Auto-Submit: Dmitri Shuralyov TryBot-Result: Gopher Robot gopls-CI: kokoro Run-TryBot: Dmitri Shuralyov --- internal/imports/mkstdlib.go | 39 ++++++++++++++++++++---------------- internal/imports/zstdlib.go | 3 ++- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/internal/imports/mkstdlib.go b/internal/imports/mkstdlib.go index a73f024c1d0..470b93f1df2 100644 --- a/internal/imports/mkstdlib.go +++ b/internal/imports/mkstdlib.go @@ -15,6 +15,7 @@ import ( "bytes" "fmt" "go/format" + "go/token" "io" "io/ioutil" "log" @@ -25,7 +26,7 @@ import ( "sort" "strings" - exec "golang.org/x/sys/execabs" + "golang.org/x/tools/go/packages" ) func mustOpen(name string) io.Reader { @@ -42,8 +43,6 @@ func api(base string) string { var sym = regexp.MustCompile(`^pkg (\S+).*?, (?:var|func|type|const) ([A-Z]\w*)`) -var unsafeSyms = map[string]bool{"Alignof": true, "ArbitraryType": true, "Offsetof": true, "Pointer": true, "Sizeof": true} - func main() { var buf bytes.Buffer outf := func(format string, args ...interface{}) { @@ -60,10 +59,13 @@ func main() { f := readAPI() sc := bufio.NewScanner(f) + // The APIs of the syscall/js and unsafe packages need to be computed explicitly, + // because they're not included in the GOROOT/api/go1.*.txt files at this time. pkgs := map[string]map[string]bool{ - "unsafe": unsafeSyms, + "syscall/js": syms("syscall/js", "GOOS=js", "GOARCH=wasm"), + "unsafe": syms("unsafe"), } - paths := []string{"unsafe"} + paths := []string{"syscall/js", "unsafe"} for sc.Scan() { l := sc.Text() @@ -118,23 +120,26 @@ func readAPI() io.Reader { readers = append(readers, mustOpen(api(name))) } } - // The API of the syscall/js package needs to be computed explicitly, - // because it's not included in the GOROOT/api/go1.*.txt files at this time. - readers = append(readers, syscallJSAPI()) return io.MultiReader(readers...) } -// syscallJSAPI returns the API of the syscall/js package. -// It's computed from the contents of $(go env GOROOT)/src/syscall/js. -func syscallJSAPI() io.Reader { - var exeSuffix string - if runtime.GOOS == "windows" { - exeSuffix = ".exe" +// syms computes the exported symbols in the specified package. +func syms(pkg string, extraEnv ...string) map[string]bool { + var env []string + if len(extraEnv) != 0 { + env = append(os.Environ(), extraEnv...) } - cmd := exec.Command("go"+exeSuffix, "run", "cmd/api", "-contexts", "js-wasm", "syscall/js") - out, err := cmd.Output() + pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedTypes, Env: env}, pkg) if err != nil { log.Fatalln(err) + } else if len(pkgs) != 1 { + log.Fatalf("got %d packages, want one package %q", len(pkgs), pkg) + } + syms := make(map[string]bool) + for _, name := range pkgs[0].Types.Scope().Names() { + if token.IsExported(name) { + syms[name] = true + } } - return bytes.NewReader(out) + return syms } diff --git a/internal/imports/zstdlib.go b/internal/imports/zstdlib.go index 93396440ef0..a03ff2c024d 100644 --- a/internal/imports/zstdlib.go +++ b/internal/imports/zstdlib.go @@ -11102,10 +11102,11 @@ var stdlib = map[string][]string{ "ValidString", }, "unsafe": { + "Add", "Alignof", - "ArbitraryType", "Offsetof", "Pointer", "Sizeof", + "Slice", }, } From 30f191f36d2b3d1701093fb6ab0258d139649d92 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Thu, 2 Feb 2023 16:37:07 +0000 Subject: [PATCH 708/723] internal/imports: update stdlib index for Go 1.20 For golang/go#38706. Change-Id: Iefd858ec1848481a26d6bb88076ff8a3c2f8a818 Reviewed-on: https://go-review.googlesource.com/c/tools/+/464875 Reviewed-by: Dmitri Shuralyov Reviewed-by: Michael Knyszek TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Gopher Robot Reviewed-by: Dmitri Shuralyov Run-TryBot: Gopher Robot --- internal/imports/zstdlib.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/imports/zstdlib.go b/internal/imports/zstdlib.go index a03ff2c024d..31a75949cdc 100644 --- a/internal/imports/zstdlib.go +++ b/internal/imports/zstdlib.go @@ -11108,5 +11108,8 @@ var stdlib = map[string][]string{ "Pointer", "Sizeof", "Slice", + "SliceData", + "String", + "StringData", }, } From 66f8f719913cd2fb9b32d174f9ea6ef663009468 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 31 Jan 2023 12:21:36 -0500 Subject: [PATCH 709/723] gopls/internal/lsp/source: use syntax alone in FormatVarType FormatVarType re-formats a variable type using syntax, in order to get accurate presentation of aliases and ellipses. However, as a result it required typed syntax trees for the type declaration, which may exist in a distinct package from the current package. In the near future we may not have typed syntax trees for these packages. We could type-check on demand, but (1) that could be costly and (2) it may break qualification using the go/types qualifier. Instead, perform this operation using a qualifier based on syntax and metadata, so that we only need a fully parsed file rather than a fully type-checked package. The resulting expressions may be inaccurate due to built-ins, "." imported packages, or missing metadata, but that seems acceptable for the current use-cases of this function, which are in completion and signature help. While doing this, add a FormatNodeWithFile helper that allows formatting a node from a *token.File, rather than *token.FileSet. This can help us avoid relying on a global fileset. To facilitate this, move the GetLines helper from internal/gcimporter into a shared tokeninternal package. For golang/go#57987 Change-Id: I3b8a5256bc2261be8b5175ee360b9336228928ac Reviewed-on: https://go-review.googlesource.com/c/tools/+/464301 Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan --- gopls/internal/lsp/semantic.go | 4 +- gopls/internal/lsp/source/call_hierarchy.go | 2 +- .../lsp/source/completion/completion.go | 4 +- .../internal/lsp/source/completion/format.go | 13 +- .../internal/lsp/source/completion/literal.go | 36 ++- gopls/internal/lsp/source/hover.go | 117 ++++++-- gopls/internal/lsp/source/identifier.go | 4 +- gopls/internal/lsp/source/signature_help.go | 8 +- gopls/internal/lsp/source/types_format.go | 284 +++++++++++------- gopls/internal/lsp/source/util.go | 187 +++++++++++- gopls/internal/lsp/source/view.go | 14 +- .../regtest/completion/completion_test.go | 4 +- internal/gcimporter/iexport.go | 52 +--- internal/tokeninternal/tokeninternal.go | 59 ++++ 14 files changed, 571 insertions(+), 217 deletions(-) create mode 100644 internal/tokeninternal/tokeninternal.go diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 5b7655a3426..a3a1bcaaed2 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -220,9 +220,7 @@ type encoded struct { ctx context.Context // metadataSource is used to resolve imports - metadataSource interface { - Metadata(source.PackageID) *source.Metadata - } + metadataSource source.MetadataSource tokTypes, tokMods []string pgf *source.ParsedGoFile rng *protocol.Range diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go index d9efcbe1524..6d67dc03391 100644 --- a/gopls/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -231,7 +231,7 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pp pro return nil, err } - declNode, _ := FindDeclAndField([]*ast.File{declPGF.File}, declPos) + declNode, _, _ := FindDeclInfo([]*ast.File{declPGF.File}, declPos) if declNode == nil { // TODO(rfindley): why don't we return an error here, or even bug.Errorf? return nil, nil diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index ad6e79f4573..61d1a5a6b68 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -156,7 +156,8 @@ func (ipm insensitivePrefixMatcher) Score(candidateLabel string) float32 { type completer struct { snapshot source.Snapshot pkg source.Package - qf types.Qualifier + qf types.Qualifier // for qualifying typed expressions + mq source.MetadataQualifier // for syntactic qualifying opts *completionOptions // completionContext contains information about the trigger for this @@ -509,6 +510,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan pkg: pkg, snapshot: snapshot, qf: source.Qualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()), + mq: source.MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()), completionContext: completionContext{ triggerCharacter: protoContext.TriggerCharacter, triggerKind: protoContext.TriggerKind, diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go index 16da642fe57..dfa61a09a7e 100644 --- a/gopls/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -81,7 +81,11 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e if _, ok := obj.Type().(*types.Struct); ok { detail = "struct{...}" // for anonymous structs } else if obj.IsField() { - detail = source.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qf) + var err error + detail, err = source.FormatVarType(ctx, c.snapshot, c.pkg, c.file, obj, c.qf, c.mq) + if err != nil { + return CompletionItem{}, err + } } if obj.IsField() { kind = protocol.FieldCompletion @@ -130,7 +134,10 @@ Suffixes: switch mod { case invoke: if sig, ok := funcType.Underlying().(*types.Signature); ok { - s := source.NewSignature(ctx, c.snapshot, c.pkg, sig, nil, c.qf) + s, err := source.NewSignature(ctx, c.snapshot, c.pkg, c.file, sig, nil, c.qf, c.mq) + if err != nil { + return CompletionItem{}, err + } c.functionCallSnippet("", s.TypeParams(), s.Params(), &snip) if sig.Results().Len() == 1 { funcType = sig.Results().At(0).Type() @@ -243,7 +250,7 @@ Suffixes: return item, nil } - decl, _ := source.FindDeclAndField(pkg.GetSyntax(), obj.Pos()) // may be nil + decl, _, _ := source.FindDeclInfo(pkg.GetSyntax(), obj.Pos()) // may be nil hover, err := source.FindHoverContext(ctx, c.snapshot, pkg, obj, decl, nil) if err != nil { event.Error(ctx, "failed to find Hover", err, tag.URI.Of(uri)) diff --git a/gopls/internal/lsp/source/completion/literal.go b/gopls/internal/lsp/source/completion/literal.go index 9d4b0e6fba9..6777f7333ed 100644 --- a/gopls/internal/lsp/source/completion/literal.go +++ b/gopls/internal/lsp/source/completion/literal.go @@ -202,10 +202,18 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m // If the param has no name in the signature, guess a name based // on the type. Use an empty qualifier to ignore the package. // For example, we want to name "http.Request" "r", not "hr". - name = source.FormatVarType(ctx, c.snapshot, c.pkg, p, func(p *types.Package) string { - return "" - }) - name = abbreviateTypeName(name) + typeName, err := source.FormatVarType(ctx, c.snapshot, c.pkg, c.file, p, + func(p *types.Package) string { return "" }, + func(source.PackageName, source.ImportPath, source.PackagePath) string { return "" }) + if err != nil { + // In general, the only error we should encounter while formatting is + // context cancellation. + if ctx.Err() == nil { + event.Error(ctx, "formatting var type", err) + } + return + } + name = abbreviateTypeName(typeName) } paramNames[i] = name if name != "_" { @@ -264,7 +272,15 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m // of "i int, j int". if i == sig.Params().Len()-1 || !types.Identical(p.Type(), sig.Params().At(i+1).Type()) { snip.WriteText(" ") - typeStr := source.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qf) + typeStr, err := source.FormatVarType(ctx, c.snapshot, c.pkg, c.file, p, c.qf, c.mq) + if err != nil { + // In general, the only error we should encounter while formatting is + // context cancellation. + if ctx.Err() == nil { + event.Error(ctx, "formatting var type", err) + } + return + } if sig.Variadic() && i == sig.Params().Len()-1 { typeStr = strings.Replace(typeStr, "[]", "...", 1) } @@ -314,7 +330,15 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m snip.WriteText(name + " ") } - text := source.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qf) + text, err := source.FormatVarType(ctx, c.snapshot, c.pkg, c.file, r, c.qf, c.mq) + if err != nil { + // In general, the only error we should encounter while formatting is + // context cancellation. + if ctx.Err() == nil { + event.Error(ctx, "formatting var type", err) + } + return + } if tp, _ := r.Type().(*typeparams.TypeParam); tp != nil && !c.typeParamInScope(tp) { snip.WritePlaceholder(func(snip *snippet.Builder) { snip.WriteText(text) diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index e67e4d897f5..1d48152fe54 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -24,6 +24,7 @@ import ( "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" @@ -498,6 +499,41 @@ func objectString(obj types.Object, qf types.Qualifier, inferred *types.Signatur return str } +// parseFull fully parses the file corresponding to position pos, referenced +// from the given srcpkg. +// +// It returns the resulting ParsedGoFile as well as new pos contained in the +// parsed file. +func parseFull(ctx context.Context, snapshot Snapshot, srcpkg Package, pos token.Pos) (*ParsedGoFile, token.Pos, error) { + f := srcpkg.FileSet().File(pos) + if f == nil { + return nil, 0, bug.Errorf("internal error: no file for position %d in %s", pos, srcpkg.Metadata().ID) + } + + uri := span.URIFromPath(f.Name()) + fh, err := snapshot.GetFile(ctx, uri) + if err != nil { + return nil, 0, err + } + + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + if err != nil { + return nil, 0, err + } + + offset, err := safetoken.Offset(f, pos) + if err != nil { + return nil, 0, bug.Errorf("offset out of bounds in %q", uri) + } + + fullPos, err := safetoken.Pos(pgf.Tok, offset) + if err != nil { + return nil, 0, err + } + + return pgf, fullPos, nil +} + // FindHoverContext returns a HoverContext struct for an AST node and its // declaration object. node should be the actual node used in type checking, // while fullNode could be a separate node with more complete syntactic @@ -637,7 +673,7 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob break } - _, field := FindDeclAndField(pkg.GetSyntax(), obj.Pos()) + _, _, field := FindDeclInfo(pkg.GetSyntax(), obj.Pos()) if field != nil { comment := field.Doc if comment.Text() == "" { @@ -893,16 +929,19 @@ func anyNonEmpty(x []string) bool { return false } -// FindDeclAndField returns the var/func/type/const Decl that declares -// the identifier at pos, searching the given list of file syntax -// trees. If pos is the position of an ast.Field or one of its Names -// or Ellipsis.Elt, the field is returned, along with the innermost -// enclosing Decl, which could be only loosely related---consider: +// FindDeclInfo returns the syntax nodes involved in the declaration of the +// types.Object with position pos, searching the given list of file syntax +// trees. +// +// Pos may be the position of the name-defining identifier in a FuncDecl, +// ValueSpec, TypeSpec, Field, or as a special case the position of +// Ellipsis.Elt in an ellipsis field. // -// var decl = f( func(field int) {} ) +// If found, the resulting decl, spec, and field will be the inner-most +// instance of each node type surrounding pos. // -// It returns (nil, nil) if no Field or Decl is found at pos. -func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *ast.Field) { +// It returns a nil decl if no object-defining node is found at pos. +func FindDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spec, field *ast.Field) { // panic(found{}) breaks off the traversal and // causes the function to return normally. type found struct{} @@ -933,33 +972,45 @@ func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *a switch n := n.(type) { case *ast.Field: - checkField := func(f ast.Node) { - if f.Pos() == pos { - field = n - for i := len(stack) - 1; i >= 0; i-- { - if d, ok := stack[i].(ast.Decl); ok { - decl = d // innermost enclosing decl - break - } + findEnclosingDeclAndSpec := func() { + for i := len(stack) - 1; i >= 0; i-- { + switch n := stack[i].(type) { + case ast.Spec: + spec = n + case ast.Decl: + decl = n + return } + } + } + + // Check each field name since you can have + // multiple names for the same type expression. + for _, id := range n.Names { + if id.Pos() == pos { + field = n + findEnclosingDeclAndSpec() panic(found{}) } } // Check *ast.Field itself. This handles embedded // fields which have no associated *ast.Ident name. - checkField(n) - - // Check each field name since you can have - // multiple names for the same type expression. - for _, name := range n.Names { - checkField(name) + if n.Pos() == pos { + field = n + findEnclosingDeclAndSpec() + panic(found{}) } - // Also check "X" in "...X". This makes it easy - // to format variadic signature params properly. - if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil { - checkField(ell.Elt) + // Also check "X" in "...X". This makes it easy to format variadic + // signature params properly. + // + // TODO(rfindley): I don't understand this comment. How does finding the + // field in this case make it easier to format variadic signature params? + if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil && ell.Elt.Pos() == pos { + field = n + findEnclosingDeclAndSpec() + panic(found{}) } case *ast.FuncDecl: @@ -969,17 +1020,19 @@ func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *a } case *ast.GenDecl: - for _, spec := range n.Specs { - switch spec := spec.(type) { + for _, s := range n.Specs { + switch s := s.(type) { case *ast.TypeSpec: - if spec.Name.Pos() == pos { + if s.Name.Pos() == pos { decl = n + spec = s panic(found{}) } case *ast.ValueSpec: - for _, id := range spec.Names { + for _, id := range s.Names { if id.Pos() == pos { decl = n + spec = s panic(found{}) } } @@ -992,5 +1045,5 @@ func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *a ast.Inspect(file, f) } - return nil, nil + return nil, nil, nil } diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go index 9497c657377..1e1bc527fa5 100644 --- a/gopls/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -274,9 +274,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if err != nil { return nil, err } - // TODO(adonovan): there's no need to inspect the entire GetSyntax() slice: - // we already know it's declFile.File. - result.Declaration.node, _ = FindDeclAndField(declPkg.GetSyntax(), declPos) // may be nil + result.Declaration.node, _, _ = FindDeclInfo([]*ast.File{declFile.File}, declPos) // may be nil result.Declaration.nodeFile = declFile // Ensure that we have the full declaration, in case the declaration was diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index 4902496466a..17549b8e95f 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -101,7 +101,7 @@ FindCall: if err != nil { return nil, 0, err } - node, _ := FindDeclAndField(declPkg.GetSyntax(), obj.Pos()) // may be nil + node, _, _ := FindDeclInfo(declPkg.GetSyntax(), obj.Pos()) // may be nil d, err := FindHoverContext(ctx, snapshot, pkg, obj, node, nil) if err != nil { return nil, 0, err @@ -111,7 +111,11 @@ FindCall: } else { name = "func" } - s := NewSignature(ctx, snapshot, pkg, sig, comment, qf) + mq := MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()) + s, err := NewSignature(ctx, snapshot, pkg, pgf.File, sig, comment, qf, mq) + if err != nil { + return nil, 0, err + } paramInfo := make([]protocol.ParameterInformation, 0, len(s.params)) for _, p := range s.params { paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p}) diff --git a/gopls/internal/lsp/source/types_format.go b/gopls/internal/lsp/source/types_format.go index 7a2605c72bd..379d11aa449 100644 --- a/gopls/internal/lsp/source/types_format.go +++ b/gopls/internal/lsp/source/types_format.go @@ -16,6 +16,7 @@ import ( "strings" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/typeparams" @@ -193,7 +194,7 @@ func FormatTypeParams(tparams *typeparams.TypeParamList) string { } // NewSignature returns formatted signature for a types.Signature struct. -func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signature, comment *ast.CommentGroup, qf types.Qualifier) *signature { +func NewSignature(ctx context.Context, s Snapshot, pkg Package, srcFile *ast.File, sig *types.Signature, comment *ast.CommentGroup, qf types.Qualifier, mq MetadataQualifier) (*signature, error) { var tparams []string tpList := typeparams.ForSignature(sig) for i := 0; i < tpList.Len(); i++ { @@ -206,7 +207,10 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa params := make([]string, 0, sig.Params().Len()) for i := 0; i < sig.Params().Len(); i++ { el := sig.Params().At(i) - typ := FormatVarType(ctx, s, pkg, el, qf) + typ, err := FormatVarType(ctx, s, pkg, srcFile, el, qf, mq) + if err != nil { + return nil, err + } p := typ if el.Name() != "" { p = el.Name() + " " + typ @@ -221,7 +225,10 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa needResultParens = true } el := sig.Results().At(i) - typ := FormatVarType(ctx, s, pkg, el, qf) + typ, err := FormatVarType(ctx, s, pkg, srcFile, el, qf, mq) + if err != nil { + return nil, err + } if el.Name() == "" { results = append(results, typ) } else { @@ -248,170 +255,239 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa results: results, variadic: sig.Variadic(), needResultParens: needResultParens, - } + }, nil } // FormatVarType formats a *types.Var, accounting for type aliases. // To do this, it looks in the AST of the file in which the object is declared. // On any errors, it always falls back to types.TypeString. -func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj *types.Var, qf types.Qualifier) string { - pkg, err := FindPackageFromPos(ctx, snapshot, srcpkg, obj.Pos()) +// +// TODO(rfindley): this function could return the actual name used in syntax, +// for better parameter names. +func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, srcFile *ast.File, obj *types.Var, qf types.Qualifier, mq MetadataQualifier) (string, error) { + // TODO(rfindley): This looks wrong. The previous comment said: + // "If the given expr refers to a type parameter, then use the + // object's Type instead of the type parameter declaration. This helps + // format the instantiated type as opposed to the original undeclared + // generic type". + // + // But of course, if obj is a type param, we are formatting a generic type + // and not an instantiated type. Handling for instantiated types must be done + // at a higher level. + // + // Left this during refactoring in order to preserve pre-existing logic. + if typeparams.IsTypeParam(obj.Type()) { + return types.TypeString(obj.Type(), qf), nil + } + + if obj.Pkg() == nil || !obj.Pos().IsValid() { + // This is defensive, though it is extremely unlikely we'll ever have a + // builtin var. + return types.TypeString(obj.Type(), qf), nil + } + + targetpgf, pos, err := parseFull(ctx, snapshot, srcpkg, obj.Pos()) if err != nil { - return types.TypeString(obj.Type(), qf) + return "", err // e.g. ctx cancelled + } + + targetMeta := findFileInDepsMetadata(snapshot, srcpkg.Metadata(), targetpgf.URI) + if targetMeta == nil { + // If we have an object from type-checking, it should exist in a file in + // the forward transitive closure. + return "", bug.Errorf("failed to find file %q in deps of %q", targetpgf.URI, srcpkg.Metadata().ID) + } + + decl, spec, field := FindDeclInfo([]*ast.File{targetpgf.File}, pos) + + // We can't handle type parameters correctly, so we fall back on TypeString + // for parameterized decls. + if decl, _ := decl.(*ast.FuncDecl); decl != nil { + if typeparams.ForFuncType(decl.Type).NumFields() > 0 { + return types.TypeString(obj.Type(), qf), nil // in generic function + } + if decl.Recv != nil && len(decl.Recv.List) > 0 { + if x, _, _, _ := typeparams.UnpackIndexExpr(decl.Recv.List[0].Type); x != nil { + return types.TypeString(obj.Type(), qf), nil // in method of generic type + } + } + } + if spec, _ := spec.(*ast.TypeSpec); spec != nil && typeparams.ForTypeSpec(spec).NumFields() > 0 { + return types.TypeString(obj.Type(), qf), nil // in generic type decl } - _, field := FindDeclAndField(pkg.GetSyntax(), obj.Pos()) if field == nil { - return types.TypeString(obj.Type(), qf) + // TODO(rfindley): we should never reach here from an ordinary var, so + // should probably return an error here. + return types.TypeString(obj.Type(), qf), nil } expr := field.Type - // If the given expr refers to a type parameter, then use the - // object's Type instead of the type parameter declaration. This helps - // format the instantiated type as opposed to the original undeclared - // generic type. - if typeparams.IsTypeParam(pkg.GetTypesInfo().Types[expr].Type) { - return types.TypeString(obj.Type(), qf) - } + rq := requalifier(snapshot, targetpgf.File, targetMeta, mq) // The type names in the AST may not be correctly qualified. // Determine the package name to use based on the package that originated // the query and the package in which the type is declared. // We then qualify the value by cloning the AST node and editing it. - clonedInfo := make(map[token.Pos]*types.PkgName) - qualified := cloneExpr(expr, pkg.GetTypesInfo(), clonedInfo) + expr = qualifyTypeExpr(expr, rq) // If the request came from a different package than the one in which the // types are defined, we may need to modify the qualifiers. - qualified = qualifyExpr(qualified, srcpkg, pkg, clonedInfo, qf) - fmted := FormatNode(srcpkg.FileSet(), qualified) - return fmted -} - -// qualifyExpr applies the "pkgName." prefix to any *ast.Ident in the expr. -func qualifyExpr(expr ast.Expr, srcpkg, pkg Package, clonedInfo map[token.Pos]*types.PkgName, qf types.Qualifier) ast.Expr { - ast.Inspect(expr, func(n ast.Node) bool { - switch n := n.(type) { - case *ast.ArrayType, *ast.ChanType, *ast.Ellipsis, - *ast.FuncType, *ast.MapType, *ast.ParenExpr, - *ast.StarExpr, *ast.StructType, *ast.FieldList, *ast.Field: - // These are the only types that are cloned by cloneExpr below, - // so these are the only types that we can traverse and potentially - // modify. This is not an ideal approach, but it works for now. - - // TODO(rFindley): can we eliminate this filtering entirely? This caused - // bugs in the past (golang/go#50539) - return true - case *ast.SelectorExpr: - // We may need to change any selectors in which the X is a package - // name and the Sel is exported. - x, ok := n.X.(*ast.Ident) - if !ok { - return false - } - obj, ok := clonedInfo[x.Pos()] - if !ok { - return false - } - x.Name = qf(obj.Imported()) - return false - case *ast.Ident: - if srcpkg == pkg { - return false - } - // Only add the qualifier if the identifier is exported. - if ast.IsExported(n.Name) { - pkgName := qf(pkg.GetTypes()) - n.Name = pkgName + "." + n.Name - } - } - return false - }) - return expr + return FormatNodeFile(targetpgf.Tok, expr), nil } -// cloneExpr only clones expressions that appear in the parameters or return -// values of a function declaration. The original expression may be returned -// to the caller in 2 cases: -// -// 1. The expression has no pointer fields. -// 2. The expression cannot appear in an *ast.FuncType, making it -// unnecessary to clone. +// qualifyTypeExpr clones the type expression expr after re-qualifying type +// names using the given function, which accepts the current syntactic +// qualifier (possibly "" for unqualified idents), and returns a new qualifier +// (again, possibly "" if the identifier should be unqualified). // -// This function also keeps track of selector expressions in which the X is a -// package name and marks them in a map along with their type information, so -// that this information can be used when rewriting the expression. +// The resulting expression may be inaccurate: without type-checking we don't +// properly account for "." imported identifiers or builtins. // -// NOTE: This function is tailored to the use case of qualifyExpr, and should -// be used with caution. -func cloneExpr(expr ast.Expr, info *types.Info, clonedInfo map[token.Pos]*types.PkgName) ast.Expr { +// TODO(rfindley): add many more tests for this function. +func qualifyTypeExpr(expr ast.Expr, qf func(string) string) ast.Expr { switch expr := expr.(type) { case *ast.ArrayType: return &ast.ArrayType{ Lbrack: expr.Lbrack, - Elt: cloneExpr(expr.Elt, info, clonedInfo), + Elt: qualifyTypeExpr(expr.Elt, qf), Len: expr.Len, } + + case *ast.BinaryExpr: + if expr.Op != token.OR { + return expr + } + return &ast.BinaryExpr{ + X: qualifyTypeExpr(expr.X, qf), + OpPos: expr.OpPos, + Op: expr.Op, + Y: qualifyTypeExpr(expr.Y, qf), + } + case *ast.ChanType: return &ast.ChanType{ Arrow: expr.Arrow, Begin: expr.Begin, Dir: expr.Dir, - Value: cloneExpr(expr.Value, info, clonedInfo), + Value: qualifyTypeExpr(expr.Value, qf), } + case *ast.Ellipsis: return &ast.Ellipsis{ Ellipsis: expr.Ellipsis, - Elt: cloneExpr(expr.Elt, info, clonedInfo), + Elt: qualifyTypeExpr(expr.Elt, qf), } + case *ast.FuncType: return &ast.FuncType{ Func: expr.Func, - Params: cloneFieldList(expr.Params, info, clonedInfo), - Results: cloneFieldList(expr.Results, info, clonedInfo), + Params: qualifyFieldList(expr.Params, qf), + Results: qualifyFieldList(expr.Results, qf), } + case *ast.Ident: - return cloneIdent(expr) + // Unqualified type (builtin, package local, or dot-imported). + + // Don't qualify names that look like builtins. + // + // Without type-checking this may be inaccurate. It could be made accurate + // by doing syntactic object resolution for the entire package, but that + // does not seem worthwhile and we generally want to avoid using + // ast.Object, which may be inaccurate. + if obj := types.Universe.Lookup(expr.Name); obj != nil { + return expr + } + + newName := qf("") + if newName != "" { + return &ast.SelectorExpr{ + X: &ast.Ident{ + NamePos: expr.Pos(), + Name: newName, + }, + Sel: expr, + } + } + return expr + + case *ast.IndexExpr: + return &ast.IndexExpr{ + X: qualifyTypeExpr(expr.X, qf), + Lbrack: expr.Lbrack, + Index: qualifyTypeExpr(expr.Index, qf), + Rbrack: expr.Rbrack, + } + + case *typeparams.IndexListExpr: + indices := make([]ast.Expr, len(expr.Indices)) + for i, idx := range expr.Indices { + indices[i] = qualifyTypeExpr(idx, qf) + } + return &typeparams.IndexListExpr{ + X: qualifyTypeExpr(expr.X, qf), + Lbrack: expr.Lbrack, + Indices: indices, + Rbrack: expr.Rbrack, + } + + case *ast.InterfaceType: + return &ast.InterfaceType{ + Interface: expr.Interface, + Methods: qualifyFieldList(expr.Methods, qf), + Incomplete: expr.Incomplete, + } + case *ast.MapType: return &ast.MapType{ Map: expr.Map, - Key: cloneExpr(expr.Key, info, clonedInfo), - Value: cloneExpr(expr.Value, info, clonedInfo), + Key: qualifyTypeExpr(expr.Key, qf), + Value: qualifyTypeExpr(expr.Value, qf), } + case *ast.ParenExpr: return &ast.ParenExpr{ Lparen: expr.Lparen, Rparen: expr.Rparen, - X: cloneExpr(expr.X, info, clonedInfo), + X: qualifyTypeExpr(expr.X, qf), } + case *ast.SelectorExpr: - s := &ast.SelectorExpr{ - Sel: cloneIdent(expr.Sel), - X: cloneExpr(expr.X, info, clonedInfo), - } - if x, ok := expr.X.(*ast.Ident); ok && ast.IsExported(expr.Sel.Name) { - if obj, ok := info.ObjectOf(x).(*types.PkgName); ok { - clonedInfo[s.X.Pos()] = obj + if id, ok := expr.X.(*ast.Ident); ok { + // qualified type + newName := qf(id.Name) + if newName == "" { + return expr.Sel + } + return &ast.SelectorExpr{ + X: &ast.Ident{ + NamePos: id.NamePos, + Name: newName, + }, + Sel: expr.Sel, } } - return s + return expr + case *ast.StarExpr: return &ast.StarExpr{ Star: expr.Star, - X: cloneExpr(expr.X, info, clonedInfo), + X: qualifyTypeExpr(expr.X, qf), } + case *ast.StructType: return &ast.StructType{ Struct: expr.Struct, - Fields: cloneFieldList(expr.Fields, info, clonedInfo), + Fields: qualifyFieldList(expr.Fields, qf), Incomplete: expr.Incomplete, } + default: return expr } } -func cloneFieldList(fl *ast.FieldList, info *types.Info, clonedInfo map[token.Pos]*types.PkgName) *ast.FieldList { +func qualifyFieldList(fl *ast.FieldList, qf func(string) string) *ast.FieldList { if fl == nil { return nil } @@ -423,16 +499,12 @@ func cloneFieldList(fl *ast.FieldList, info *types.Info, clonedInfo map[token.Po } list := make([]*ast.Field, 0, len(fl.List)) for _, f := range fl.List { - var names []*ast.Ident - for _, n := range f.Names { - names = append(names, cloneIdent(n)) - } list = append(list, &ast.Field{ Comment: f.Comment, Doc: f.Doc, - Names: names, + Names: f.Names, Tag: f.Tag, - Type: cloneExpr(f.Type, info, clonedInfo), + Type: qualifyTypeExpr(f.Type, qf), }) } return &ast.FieldList{ @@ -441,11 +513,3 @@ func cloneFieldList(fl *ast.FieldList, info *types.Info, clonedInfo map[token.Po List: list, } } - -func cloneIdent(ident *ast.Ident) *ast.Ident { - return &ast.Ident{ - NamePos: ident.NamePos, - Name: ident.Name, - Obj: ident.Obj, - } -} diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index c9a510224e3..6a8dc26cd5a 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -20,6 +20,8 @@ import ( "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/tokeninternal" "golang.org/x/tools/internal/typeparams" ) @@ -161,6 +163,24 @@ func FormatNode(fset *token.FileSet, n ast.Node) string { return buf.String() } +// FormatNodeFile is like FormatNode, but requires only the token.File for the +// syntax containing the given ast node. +func FormatNodeFile(file *token.File, n ast.Node) string { + fset := SingletonFileSet(file) + return FormatNode(fset, n) +} + +// SingletonFileSet creates a new token.FileSet containing a file that is +// identical to f (same base, size, and line), for use in APIs that require a +// FileSet. +func SingletonFileSet(f *token.File) *token.FileSet { + fset := token.NewFileSet() + f2 := fset.AddFile(f.Name(), f.Base(), f.Size()) + lines := tokeninternal.GetLines(f) + f2.SetLines(lines) + return fset +} + // Deref returns a pointer's element type, traversing as many levels as needed. // Otherwise it returns typ. // @@ -236,13 +256,46 @@ func findFileInDeps(ctx context.Context, snapshot Snapshot, pkg Package, uri spa return nil, nil, fmt.Errorf("no file for %s in deps of package %s", uri, pkg.Metadata().ID) } +// findFileInDepsMetadata 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 findFileInDepsMetadata(s MetadataSource, m *Metadata, uri span.URI) *Metadata { + seen := make(map[PackageID]bool) + var search func(*Metadata) *Metadata + search = func(m *Metadata) *Metadata { + if seen[m.ID] { + return nil + } + seen[m.ID] = true + for _, cgf := range m.CompiledGoFiles { + if cgf == uri { + return m + } + } + for _, dep := range m.DepsByPkgPath { + m := s.Metadata(dep) + if m == nil { + bug.Reportf("nil metadata for %q", dep) + continue + } + if found := search(m); found != nil { + return found + } + } + return nil + } + return search(m) +} + // recursiveDeps finds unique transitive dependencies of m, including m itself. // // Invariant: for the resulting slice res, res[0] == m.ID. // // TODO(rfindley): consider replacing this with a snapshot.ForwardDependencies // method, or exposing the metadata graph itself. -func recursiveDeps(s interface{ Metadata(PackageID) *Metadata }, m *Metadata) []PackageID { +func recursiveDeps(s MetadataSource, m *Metadata) []PackageID { seen := make(map[PackageID]bool) var ids []PackageID var add func(*Metadata) @@ -254,6 +307,10 @@ func recursiveDeps(s interface{ Metadata(PackageID) *Metadata }, m *Metadata) [] ids = append(ids, m.ID) for _, dep := range m.DepsByPkgPath { m := s.Metadata(dep) + if m == nil { + bug.Reportf("nil metadata for %q", dep) + continue + } add(m) } } @@ -329,6 +386,134 @@ func Qualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qualifie } } +// requalifier returns a function that re-qualifies identifiers and qualified +// identifiers contained in targetFile using the given metadata qualifier. +func requalifier(s MetadataSource, targetFile *ast.File, targetMeta *Metadata, mq MetadataQualifier) func(string) string { + qm := map[string]string{ + "": mq(targetMeta.Name, "", targetMeta.PkgPath), + } + + // Construct mapping of import paths to their defined or implicit names. + for _, imp := range targetFile.Imports { + name, pkgName, impPath, pkgPath := importInfo(s, imp, targetMeta) + + // Re-map the target name for the source file. + qm[name] = mq(pkgName, impPath, pkgPath) + } + + return func(name string) string { + if newName, ok := qm[name]; ok { + return newName + } + return name + } +} + +// A MetadataQualifier is a function that qualifies an identifier declared in a +// package with the given package name, import path, and package path. +// +// In scenarios where metadata is missing the provided PackageName and +// PackagePath may be empty, but ImportPath must always be non-empty. +type MetadataQualifier func(PackageName, ImportPath, PackagePath) string + +// MetadataQualifierForFile returns a metadata qualifier that chooses the best +// qualification of an imported package relative to the file f in package with +// metadata m. +func MetadataQualifierForFile(s MetadataSource, f *ast.File, m *Metadata) MetadataQualifier { + // Record local names for import paths. + localNames := make(map[ImportPath]string) // local names for imports in f + for _, imp := range f.Imports { + name, _, impPath, _ := importInfo(s, imp, m) + localNames[impPath] = name + } + + // Record a package path -> import path mapping. + inverseDeps := make(map[PackageID]PackagePath) + for path, id := range m.DepsByPkgPath { + inverseDeps[id] = path + } + importsByPkgPath := make(map[PackagePath]ImportPath) // best import paths by pkgPath + for impPath, id := range m.DepsByImpPath { + if id == "" { + continue + } + pkgPath := inverseDeps[id] + _, hasPath := importsByPkgPath[pkgPath] + _, hasImp := localNames[impPath] + // In rare cases, there may be multiple import paths with the same package + // path. In such scenarios, prefer an import path that already exists in + // the file. + if !hasPath || hasImp { + importsByPkgPath[pkgPath] = impPath + } + } + + return func(pkgName PackageName, impPath ImportPath, pkgPath PackagePath) string { + // If supplied, translate the package path to an import path in the source + // package. + if pkgPath != "" { + if srcImp := importsByPkgPath[pkgPath]; srcImp != "" { + impPath = srcImp + } + if pkgPath == m.PkgPath { + return "" + } + } + if localName, ok := localNames[impPath]; ok && impPath != "" { + return string(localName) + } + if pkgName != "" { + return string(pkgName) + } + idx := strings.LastIndexByte(string(impPath), '/') + return string(impPath[idx+1:]) + } +} + +// importInfo collects information about the import specified by imp, +// extracting its file-local name, package name, import path, and package path. +// +// If metadata is missing for the import, the resulting package name and +// package path may be empty, and the file local name may be guessed based on +// the import path. +// +// Note: previous versions of this helper used a PackageID->PackagePath map +// extracted from m, for extracting package path even in the case where +// metadata for a dep was missing. This should not be necessary, as we should +// always have metadata for IDs contained in DepsByPkgPath. +func importInfo(s MetadataSource, imp *ast.ImportSpec, m *Metadata) (string, PackageName, ImportPath, PackagePath) { + var ( + name string // local name + pkgName PackageName + impPath = UnquoteImportPath(imp) + pkgPath PackagePath + ) + + // If the import has a local name, use it. + if imp.Name != nil { + name = imp.Name.Name + } + + // Try to find metadata for the import. If successful and there is no local + // name, the package name is the local name. + if depID := m.DepsByImpPath[impPath]; depID != "" { + if depm := s.Metadata(depID); depm != nil { + if name == "" { + name = string(depm.Name) + } + pkgName = depm.Name + pkgPath = depm.PkgPath + } + } + + // If the local name is still unknown, guess it based on the import path. + if name == "" { + idx := strings.LastIndexByte(string(impPath), '/') + name = string(impPath[idx+1:]) + } + return name, pkgName, impPath, pkgPath +} + // isDirective reports whether c is a comment directive. // // Copied and adapted from go/src/go/ast/ast.go. diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 8798e1b8eed..b86c3ccd23a 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -354,14 +354,22 @@ type View interface { GoVersionString() string } -// A FileSource maps uris to FileHandles. This abstraction exists both for -// testability, and so that algorithms can be run equally on session and -// snapshot files. +// A FileSource maps uris to FileHandles. type FileSource interface { // GetFile returns the FileHandle for a given URI. GetFile(ctx context.Context, uri span.URI) (FileHandle, error) } +// A MetadataSource maps package IDs to metadata. +// +// TODO(rfindley): replace this with a concrete metadata graph, once it is +// exposed from the snapshot. +type MetadataSource interface { + // Metadata returns Metadata for the given package ID, or nil if it does not + // exist. + Metadata(PackageID) *Metadata +} + // A ParsedGoFile contains the results of parsing a Go file. type ParsedGoFile struct { URI span.URI diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 7d9214fa52d..753cd68550e 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -482,7 +482,7 @@ func doit() { t.Error(diff) } if completions.Items[0].Tags == nil { - t.Errorf("expected Tags to show deprecation %#v", diff[0]) + t.Errorf("expected Tags to show deprecation %#v", completions.Items[0].Tags) } loc = env.RegexpSearch("prog.go", "= badP") loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte("= badP"))) @@ -492,7 +492,7 @@ func doit() { t.Error(diff) } if completions.Items[0].Tags == nil { - t.Errorf("expected Tags to show deprecation %#v", diff[0]) + t.Errorf("expected Tags to show deprecation %#v", completions.Items[0].Tags) } }) } diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 8bd51eccf16..ba53cdcdd10 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -21,9 +21,8 @@ import ( "sort" "strconv" "strings" - "sync" - "unsafe" + "golang.org/x/tools/internal/tokeninternal" "golang.org/x/tools/internal/typeparams" ) @@ -210,7 +209,7 @@ func (p *iexporter) encodeFile(w *intWriter, file *token.File, needed []uint64) // Sort the set of needed offsets. Duplicates are harmless. sort.Slice(needed, func(i, j int) bool { return needed[i] < needed[j] }) - lines := getLines(file) // byte offset of each line start + lines := tokeninternal.GetLines(file) // byte offset of each line start w.uint64(uint64(len(lines))) // Rather than record the entire array of line start offsets, @@ -1179,50 +1178,3 @@ func (q *objQueue) popHead() types.Object { q.head++ return obj } - -// getLines returns the table of line-start offsets from a token.File. -func getLines(file *token.File) []int { - // Use this variant once proposal #57708 is implemented: - // - // if file, ok := file.(interface{ Lines() []int }); ok { - // return file.Lines() - // } - - // This declaration must match that of token.File. - // This creates a risk of dependency skew. - // For now we check that the size of the two - // declarations is the same, on the (fragile) assumption - // that future changes would add fields. - type tokenFile119 struct { - _ string - _ int - _ int - mu sync.Mutex // we're not complete monsters - lines []int - _ []struct{} - } - type tokenFile118 struct { - _ *token.FileSet // deleted in go1.19 - tokenFile119 - } - - type uP = unsafe.Pointer - switch unsafe.Sizeof(*file) { - case unsafe.Sizeof(tokenFile118{}): - var ptr *tokenFile118 - *(*uP)(uP(&ptr)) = uP(file) - ptr.mu.Lock() - defer ptr.mu.Unlock() - return ptr.lines - - case unsafe.Sizeof(tokenFile119{}): - var ptr *tokenFile119 - *(*uP)(uP(&ptr)) = uP(file) - ptr.mu.Lock() - defer ptr.mu.Unlock() - return ptr.lines - - default: - panic("unexpected token.File size") - } -} diff --git a/internal/tokeninternal/tokeninternal.go b/internal/tokeninternal/tokeninternal.go new file mode 100644 index 00000000000..a3fb2d4f29d --- /dev/null +++ b/internal/tokeninternal/tokeninternal.go @@ -0,0 +1,59 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// package tokeninternal provides access to some internal features of the token +// package. +package tokeninternal + +import ( + "go/token" + "sync" + "unsafe" +) + +// GetLines returns the table of line-start offsets from a token.File. +func GetLines(file *token.File) []int { + // token.File has a Lines method on Go 1.21 and later. + if file, ok := (interface{})(file).(interface{ Lines() []int }); ok { + return file.Lines() + } + + // This declaration must match that of token.File. + // This creates a risk of dependency skew. + // For now we check that the size of the two + // declarations is the same, on the (fragile) assumption + // that future changes would add fields. + type tokenFile119 struct { + _ string + _ int + _ int + mu sync.Mutex // we're not complete monsters + lines []int + _ []struct{} + } + type tokenFile118 struct { + _ *token.FileSet // deleted in go1.19 + tokenFile119 + } + + type uP = unsafe.Pointer + switch unsafe.Sizeof(*file) { + case unsafe.Sizeof(tokenFile118{}): + var ptr *tokenFile118 + *(*uP)(uP(&ptr)) = uP(file) + ptr.mu.Lock() + defer ptr.mu.Unlock() + return ptr.lines + + case unsafe.Sizeof(tokenFile119{}): + var ptr *tokenFile119 + *(*uP)(uP(&ptr)) = uP(file) + ptr.mu.Lock() + defer ptr.mu.Unlock() + return ptr.lines + + default: + panic("unexpected token.File size") + } +} From dd1c468586abf42c35d01253e89b152c4d12db93 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 1 Feb 2023 13:07:45 -0500 Subject: [PATCH 710/723] gopls/internal/lsp/source: simplify extracting object hover doc Add a new helper HoverDocForObject, which finds the relevant object documentation without needing to type-check. This eliminates the last use of FindPackageFromPos. For golang/go#57987 Change-Id: Ic9deec78d68156e9ead3831a8247f8c30259a3c6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/464455 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro TryBot-Result: Gopher Robot --- .../internal/lsp/source/completion/format.go | 21 +++------ gopls/internal/lsp/source/hover.go | 45 +++++++++++++++++++ gopls/internal/lsp/source/signature_help.go | 9 +--- gopls/internal/lsp/source/util.go | 16 ------- 4 files changed, 54 insertions(+), 37 deletions(-) diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go index dfa61a09a7e..738879bf87f 100644 --- a/gopls/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -19,7 +19,6 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typeparams" ) @@ -242,27 +241,21 @@ Suffixes: if !pos.IsValid() { return item, nil } - uri := span.URIFromPath(pos.Filename) - // Find the source file of the candidate. - pkg, err := source.FindPackageFromPos(ctx, c.snapshot, c.pkg, obj.Pos()) + comment, err := source.HoverDocForObject(ctx, c.snapshot, c.pkg, obj) if err != nil { - return item, nil - } - - decl, _, _ := source.FindDeclInfo(pkg.GetSyntax(), obj.Pos()) // may be nil - hover, err := source.FindHoverContext(ctx, c.snapshot, pkg, obj, decl, nil) - if err != nil { - event.Error(ctx, "failed to find Hover", err, tag.URI.Of(uri)) + event.Error(ctx, fmt.Sprintf("failed to find Hover for %q", obj.Name()), err) return item, nil } if c.opts.fullDocumentation { - item.Documentation = hover.Comment.Text() + item.Documentation = comment.Text() } else { - item.Documentation = doc.Synopsis(hover.Comment.Text()) + item.Documentation = doc.Synopsis(comment.Text()) } // The desired pattern is `^// Deprecated`, but the prefix has been removed - if strings.HasPrefix(hover.Comment.Text(), "Deprecated") { + // TODO(rfindley): It doesn't look like this does the right thing for + // multi-line comments. + if strings.HasPrefix(comment.Text(), "Deprecated") { if c.snapshot.View().Options().CompletionTags { item.Tags = []protocol.CompletionItemTag{protocol.ComplDeprecated} } else if c.snapshot.View().Options().CompletionDeprecated { diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 1d48152fe54..85d66de7844 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -499,6 +499,51 @@ func objectString(obj types.Object, qf types.Qualifier, inferred *types.Signatur return str } +// HoverDocForObject returns the best doc comment for obj, referenced by srcpkg. +// +// TODO(rfindley): there appears to be zero(!) tests for this functionality. +func HoverDocForObject(ctx context.Context, snapshot Snapshot, srcpkg Package, obj types.Object) (*ast.CommentGroup, error) { + if _, isTypeName := obj.(*types.TypeName); isTypeName { + if _, isTypeParam := obj.Type().(*typeparams.TypeParam); isTypeParam { + return nil, nil + } + } + + pgf, pos, err := parseFull(ctx, snapshot, srcpkg, obj.Pos()) + if err != nil { + return nil, fmt.Errorf("re-parsing: %v", err) + } + + decl, spec, field := FindDeclInfo([]*ast.File{pgf.File}, pos) + if field != nil && field.Doc != nil { + return field.Doc, nil + } + switch decl := decl.(type) { + case *ast.FuncDecl: + return decl.Doc, nil + case *ast.GenDecl: + switch spec := spec.(type) { + case *ast.ValueSpec: + if spec.Doc != nil { + return spec.Doc, nil + } + if decl.Doc != nil { + return decl.Doc, nil + } + return spec.Comment, nil + case *ast.TypeSpec: + if spec.Doc != nil { + return spec.Doc, nil + } + if decl.Doc != nil { + return decl.Doc, nil + } + return spec.Comment, nil + } + } + return nil, nil +} + // parseFull fully parses the file corresponding to position pos, referenced // from the given srcpkg. // diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index 17549b8e95f..0f81b857ba0 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -97,17 +97,12 @@ FindCall: comment *ast.CommentGroup ) if obj != nil { - declPkg, err := FindPackageFromPos(ctx, snapshot, pkg, obj.Pos()) - if err != nil { - return nil, 0, err - } - node, _, _ := FindDeclInfo(declPkg.GetSyntax(), obj.Pos()) // may be nil - d, err := FindHoverContext(ctx, snapshot, pkg, obj, node, nil) + d, err := HoverDocForObject(ctx, snapshot, pkg, obj) if err != nil { return nil, 0, err } name = obj.Name() - comment = d.Comment + comment = d } else { name = "func" } diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index 6a8dc26cd5a..8340c5ec9d4 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -100,22 +100,6 @@ func posToMappedRange(ctx context.Context, snapshot Snapshot, pkg Package, pos, return pgf.PosMappedRange(pos, end) } -// FindPackageFromPos returns the Package for the given position, which must be -// among the transitive dependencies of pkg. -// -// TODO(rfindley): is this the best factoring of this API? This function is -// really a trivial wrapper around findFileInDeps, which may be a more useful -// function to expose. -func FindPackageFromPos(ctx context.Context, snapshot Snapshot, pkg Package, pos token.Pos) (Package, error) { - if !pos.IsValid() { - return nil, fmt.Errorf("invalid position") - } - fileName := pkg.FileSet().File(pos).Name() - uri := span.URIFromPath(fileName) - _, pkg, err := findFileInDeps(ctx, snapshot, pkg, uri) - return pkg, err -} - // Matches cgo generated comment as well as the proposed standard: // // https://golang.org/s/generatedcode From 8111118043894a9c0eab886fc370ca123dcf48f1 Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 28 Oct 2021 10:20:43 -0700 Subject: [PATCH 711/723] go/analysis/internal/facts: fix cycle in importMap. This change resolves two interrelated outstanding bugs in handling Named types and type declarations in importMap(). 1) Named types did not visit their Underlying() types. 2) Whether Named types were fully visited was order dependent. Previously a Named type was not fully visited (methods visited, etc) when the type declaration was visited by addObj() before the Named type was visited by addType(). Fixes golang/go#49469 Change-Id: Ibf9c6d9afd4958d474149edf2749d994199f14b1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/362414 gopls-CI: kokoro TryBot-Result: Gopher Robot Run-TryBot: Tim King Reviewed-by: Robert Findley --- internal/facts/facts_test.go | 184 ++++++++++++++++++++++++++++++++++- internal/facts/imports.go | 23 +++-- 2 files changed, 198 insertions(+), 9 deletions(-) diff --git a/internal/facts/facts_test.go b/internal/facts/facts_test.go index 5c7b12ef1d4..ad875153954 100644 --- a/internal/facts/facts_test.go +++ b/internal/facts/facts_test.go @@ -7,10 +7,13 @@ package facts_test import ( "encoding/gob" "fmt" + "go/ast" + "go/parser" "go/token" "go/types" "os" "reflect" + "strings" "testing" "golang.org/x/tools/go/analysis/analysistest" @@ -75,13 +78,66 @@ func TestEncodeDecode(t *testing.T) { {"c", []lookup{ {"b.B", "myFact(b.B)"}, {"b.F", "myFact(b.F)"}, - //{"b.F(nil)()", "myFact(a.T)"}, // no fact; TODO(adonovan): investigate + {"b.F(nil)()", "myFact(a.T)"}, {"C", "myFact(c.C)"}, {"C{}[0]", "myFact(b.B)"}, {"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2) }}, }, }, + { + name: "underlying", + // c->b->a + // c does not import a directly or use any of its types, but it does use + // the types within a indirectly. c.q has the type a.a so package a should + // be included by importMap. + files: map[string]string{ + "a/a.go": `package a; type a int; type T *a`, + "b/b.go": `package b; import "a"; type B a.T`, + "c/c.go": `package c; import "b"; type C b.B; var q = *C(nil)`, + }, + plookups: []pkgLookups{ + {"a", []lookup{ + {"a", "myFact(a.a)"}, + {"T", "myFact(a.T)"}, + }}, + {"b", []lookup{ + {"B", "myFact(b.B)"}, + {"B(nil)", "myFact(b.B)"}, + {"*(B(nil))", "myFact(a.a)"}, + }}, + {"c", []lookup{ + {"C", "myFact(c.C)"}, + {"C(nil)", "myFact(c.C)"}, + {"*C(nil)", "myFact(a.a)"}, + {"q", "myFact(a.a)"}, + }}, + }, + }, + { + name: "methods", + // c->b->a + // c does not import a directly or use any of its types, but it does use + // the types within a indirectly via a method. + files: map[string]string{ + "a/a.go": `package a; type T int`, + "b/b.go": `package b; import "a"; type B struct{}; func (_ B) M() a.T { return 0 }`, + "c/c.go": `package c; import "b"; var C b.B`, + }, + plookups: []pkgLookups{ + {"a", []lookup{ + {"T", "myFact(a.T)"}, + }}, + {"b", []lookup{ + {"B{}", "myFact(b.B)"}, + {"B{}.M()", "myFact(a.T)"}, + }}, + {"c", []lookup{ + {"C", "myFact(b.B)"}, + {"C.M()", "myFact(a.T)"}, + }}, + }, + }, { name: "globals", files: map[string]string{ @@ -155,7 +211,7 @@ func TestEncodeDecode(t *testing.T) { var G3 N3[a.T3] var G4 N4[a.T4] var G5 N5[t5] - + func F6[T a.T6]() T { var x T; return x } `, "c/c.go": `package c; import "b"; @@ -382,3 +438,127 @@ func TestFactFilter(t *testing.T) { t.Errorf("AllObjectFacts: got %v, want %v", got, wantObjFacts) } } + +// TestMalformed checks that facts can be encoded and decoded *despite* +// types.Config.Check returning an error. Importing facts is expected to +// happen when Analyzers have RunDespiteErrors set to true. So this +// needs to robust, e.g. no infinite loops. +func TestMalformed(t *testing.T) { + if !typeparams.Enabled { + t.Skip("type parameters are not enabled") + } + var findPkg func(*types.Package, string) *types.Package + findPkg = func(p *types.Package, name string) *types.Package { + if p.Name() == name { + return p + } + for _, o := range p.Imports() { + if f := findPkg(o, name); f != nil { + return f + } + } + return nil + } + + type pkgTest struct { + content string + err string // if non-empty, expected substring of err.Error() from conf.Check(). + wants map[string]string // package path to expected name + } + tests := []struct { + name string + pkgs []pkgTest + }{ + { + name: "initialization-cycle", + pkgs: []pkgTest{ + { + content: `package a; type N[T any] struct { F *N[N[T]] }`, + err: "instantiation cycle:", + wants: map[string]string{"a": "myFact(a.[N])", "b": "no package", "c": "no package"}, + }, + { + content: `package b; import "a"; type B a.N[int]`, + wants: map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "no package"}, + }, + { + content: `package c; import "b"; var C b.B`, + wants: map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "myFact(c.[C])"}, + }, + }, + }, + } + + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + // setup for test wide variables. + packages := make(map[string]*types.Package) + conf := types.Config{ + Importer: closure(packages), + Error: func(err error) {}, // do not stop on first type checking error + } + fset := token.NewFileSet() + factmap := make(map[string][]byte) + read := func(imp *types.Package) ([]byte, error) { return factmap[imp.Path()], nil } + + // Processes the pkgs in order. For package, export a package fact, + // and use this fact to verify which package facts are reachable via Decode. + // We allow for packages to have type checking errors. + for i, pkgTest := range test.pkgs { + // parse + f, err := parser.ParseFile(fset, fmt.Sprintf("%d.go", i), pkgTest.content, 0) + if err != nil { + t.Fatal(err) + } + + // typecheck + pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, nil) + var got string + if err != nil { + got = err.Error() + } + if !strings.Contains(got, pkgTest.err) { + t.Fatalf("%s: type checking error %q did not match pattern %q", pkg.Path(), err.Error(), pkgTest.err) + } + packages[pkg.Path()] = pkg + + // decode facts + facts, err := facts.NewDecoder(pkg).Decode(read) + if err != nil { + t.Fatalf("Decode failed: %v", err) + } + + // export facts + fact := &myFact{fmt.Sprintf("%s.%s", pkg.Name(), pkg.Scope().Names())} + facts.ExportPackageFact(fact) + + // import facts + for other, want := range pkgTest.wants { + fact := new(myFact) + var got string + if found := findPkg(pkg, other); found == nil { + got = "no package" + } else if facts.ImportPackageFact(found, fact) { + got = fact.String() + } else { + got = "no fact" + } + if got != want { + t.Errorf("in %s, ImportPackageFact(%s, %T) = %s, want %s", + pkg.Path(), other, fact, got, want) + } + } + + // encode facts + factmap[pkg.Path()] = facts.Encode() + } + }) + } +} + +type closure map[string]*types.Package + +func (c closure) Import(path string) (*types.Package, error) { return c[path], nil } diff --git a/internal/facts/imports.go b/internal/facts/imports.go index a3aa90dd1c5..7b21668660c 100644 --- a/internal/facts/imports.go +++ b/internal/facts/imports.go @@ -25,21 +25,20 @@ import ( // by obtaining it from the internals of the gcexportdata decoder. func importMap(imports []*types.Package) map[string]*types.Package { objects := make(map[types.Object]bool) + typs := make(map[types.Type]bool) // Named and TypeParam packages := make(map[string]*types.Package) - var addObj func(obj types.Object) bool + var addObj func(obj types.Object) var addType func(T types.Type) - addObj = func(obj types.Object) bool { + addObj = func(obj types.Object) { if !objects[obj] { objects[obj] = true addType(obj.Type()) if pkg := obj.Pkg(); pkg != nil { packages[pkg.Path()] = pkg } - return true } - return false } addType = func(T types.Type) { @@ -47,8 +46,16 @@ func importMap(imports []*types.Package) map[string]*types.Package { case *types.Basic: // nop case *types.Named: - if addObj(T.Obj()) { - // TODO(taking): Investigate why the Underlying type is not added here. + // Remove infinite expansions of *types.Named by always looking at the origin. + // Some named types with type parameters [that will not type check] have + // infinite expansions: + // type N[T any] struct { F *N[N[T]] } + // importMap() is called on such types when Analyzer.RunDespiteErrors is true. + T = typeparams.NamedTypeOrigin(T).(*types.Named) + if !typs[T] { + typs[T] = true + addObj(T.Obj()) + addType(T.Underlying()) for i := 0; i < T.NumMethods(); i++ { addObj(T.Method(i)) } @@ -102,7 +109,9 @@ func importMap(imports []*types.Package) map[string]*types.Package { addType(T.Term(i).Type()) } case *typeparams.TypeParam: - if addObj(T.Obj()) { + if !typs[T] { + typs[T] = true + addObj(T.Obj()) addType(T.Constraint()) } } From 6b6857acbe7de7b6d80fbaf60964712dca2aac9f Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Mon, 6 Feb 2023 16:18:32 +0000 Subject: [PATCH 712/723] gopls: fix typos in comments and doc Change-Id: Ia71c87a4f0180869aa10fe31e6c2cf19d1ac713d GitHub-Last-Rev: ebb377550ce442cdf19101311082f541afaed77e GitHub-Pull-Request: golang/tools#427 Reviewed-on: https://go-review.googlesource.com/c/tools/+/465315 Reviewed-by: Robert Findley gopls-CI: kokoro Reviewed-by: Jamal Carvalho Run-TryBot: Robert Findley TryBot-Result: Gopher Robot --- gopls/doc/semantictokens.md | 2 +- gopls/internal/govulncheck/types.go | 2 +- gopls/internal/lsp/cache/analysis.go | 2 +- gopls/internal/lsp/cache/check.go | 2 +- gopls/internal/lsp/cmd/semantictokens.go | 2 +- gopls/internal/lsp/lsprpc/lsprpc.go | 2 +- gopls/internal/lsp/mod/diagnostics.go | 4 ++-- gopls/internal/lsp/mod/hover.go | 2 +- gopls/internal/lsp/protocol/generate/main.go | 2 +- gopls/internal/lsp/regtest/expectation.go | 2 +- gopls/internal/lsp/source/completion/completion.go | 2 +- gopls/internal/lsp/source/completion/format.go | 2 +- gopls/internal/lsp/source/diagnostics.go | 2 +- gopls/internal/lsp/source/hover.go | 2 +- gopls/internal/lsp/source/view.go | 4 ++-- gopls/internal/lsp/testdata/keywords/keywords.go | 2 +- gopls/internal/regtest/completion/completion_test.go | 2 +- gopls/internal/regtest/misc/semantictokens_test.go | 2 +- gopls/internal/span/span.go | 2 +- gopls/test/debug/debug_test.go | 2 +- 20 files changed, 22 insertions(+), 22 deletions(-) diff --git a/gopls/doc/semantictokens.md b/gopls/doc/semantictokens.md index c9124b796e0..a1e140d29ec 100644 --- a/gopls/doc/semantictokens.md +++ b/gopls/doc/semantictokens.md @@ -57,7 +57,7 @@ different runes by their Unicode language assignment, or some other Unicode prop being [confusable](http://www.unicode.org/Public/security/10.0.0/confusables.txt). Gopls does not come close to either of these principles. Semantic tokens are returned for -identifiers, keywords, operators, comments, and literals. (Sematic tokens do not +identifiers, keywords, operators, comments, and literals. (Semantic tokens do not cover the file. They are not returned for white space or punctuation, and there is no semantic token for labels.) The following describes more precisely what gopls diff --git a/gopls/internal/govulncheck/types.go b/gopls/internal/govulncheck/types.go index 71984519e02..2881cf4bc40 100644 --- a/gopls/internal/govulncheck/types.go +++ b/gopls/internal/govulncheck/types.go @@ -14,7 +14,7 @@ type Result struct { // Mode contains the source of the vulnerability info. // Clients of the gopls.fetch_vulncheck_result command may need - // to interprete the vulnerabilities differently based on the + // to interpret the vulnerabilities differently based on the // analysis mode. For example, Vuln without callstack traces // indicate a vulnerability that is not used if the result was // from 'govulncheck' analysis mode. On the other hand, Vuln diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 0005b0e44f1..1e1910834ff 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -501,7 +501,7 @@ func analysisCacheKey(analyzers []*analysis.Analyzer, m *source.Metadata, compil // In principle, a key must be the hash of an // unambiguous encoding of all the relevant data. - // If it's ambiguous, we risk collisons. + // If it's ambiguous, we risk collisions. // analyzers fmt.Fprintf(hasher, "analyzers: %d\n", len(analyzers)) diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index 03691e50e8a..b8d7d0298d5 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -261,7 +261,7 @@ func computePackageKey(s *snapshot, inputs typeCheckInputs) packageHandleKey { // In principle, a key must be the hash of an // unambiguous encoding of all the relevant data. - // If it's ambiguous, we risk collisons. + // If it's ambiguous, we risk collisions. // package identifiers fmt.Fprintf(hasher, "package: %s %s %s\n", inputs.id, inputs.name, inputs.pkgPath) diff --git a/gopls/internal/lsp/cmd/semantictokens.go b/gopls/internal/lsp/cmd/semantictokens.go index 3749ed2aefc..6747e468707 100644 --- a/gopls/internal/lsp/cmd/semantictokens.go +++ b/gopls/internal/lsp/cmd/semantictokens.go @@ -39,7 +39,7 @@ import ( // 0-based: the first line is line 0, the first character of a line // is character 0, and characters are counted as UTF-16 code points // gopls (and Go error messages): -// 1-based: the first line is line1, the first chararcter of a line +// 1-based: the first line is line1, the first character of a line // is character 0, and characters are counted as bytes // internal (as used in marks, and lines:=bytes.Split(buf, '\n')) // 0-based: lines and character positions are 1 less than in diff --git a/gopls/internal/lsp/lsprpc/lsprpc.go b/gopls/internal/lsp/lsprpc/lsprpc.go index d43e68ec0d2..6b02cf5aa65 100644 --- a/gopls/internal/lsp/lsprpc/lsprpc.go +++ b/gopls/internal/lsp/lsprpc/lsprpc.go @@ -242,7 +242,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) { - // This call to os.Execuable is redundant, and will be eliminated by the + // This call to os.Executable is redundant, and will be eliminated by the // transition to the V2 API. goplsPath, err := os.Executable() if err != nil { diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go index 7b598cf7273..ca2971dbcb9 100644 --- a/gopls/internal/lsp/mod/diagnostics.go +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -43,7 +43,7 @@ func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[span } // VulnerabilityDiagnostics returns vulnerability diagnostics for the active modules in the -// workspace with known vulnerabilites. +// workspace with known vulnerabilities. func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", source.SnapshotLabels(snapshot)...) defer done() @@ -252,7 +252,7 @@ func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, // user from quickly locating the next module to fix. // Ideally we should rerun the analysis with the updated module // dependencies or any other code changes, but we are not yet - // in the position of automatically triggerring the analysis + // in the position of automatically triggering the analysis // (govulncheck can take a while). We also don't know exactly what // part of source code was changed since `vulns` was computed. // As a heuristic, we assume that a user upgrades the affecting diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go index 3236635b1c3..fbd3c000013 100644 --- a/gopls/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -191,7 +191,7 @@ func lookupVulns(vulns *govulncheck.Result, modpath, version string) (affecting, // user from quickly locating the next module to fix. // Ideally we should rerun the analysis with the updated module // dependencies or any other code changes, but we are not yet - // in the position of automatically triggerring the analysis + // in the position of automatically triggering the analysis // (govulncheck can take a while). We also don't know exactly what // part of source code was changed since `vulns` was computed. // As a heuristic, we assume that a user upgrades the affecting diff --git a/gopls/internal/lsp/protocol/generate/main.go b/gopls/internal/lsp/protocol/generate/main.go index 94af77b5b16..b00a00e000f 100644 --- a/gopls/internal/lsp/protocol/generate/main.go +++ b/gopls/internal/lsp/protocol/generate/main.go @@ -167,7 +167,7 @@ func writeprotocol() { types["PrepareRenameResult"] = "type PrepareRenameResult = Msg_PrepareRename2Gn // (alias) line 13927\n" for _, k := range types.keys() { if k == "WatchKind" { - types[k] = "type WatchKind = uint32 // line 13505" // strict gopls compatibility neads the '=' + types[k] = "type WatchKind = uint32 // line 13505" // strict gopls compatibility needs the '=' } out.WriteString(types[k]) } diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index cba4c60639e..9c50b294661 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -447,7 +447,7 @@ func NoErrorLogs() Expectation { // of type typ matching the regexp re a certain number of times. // // The count argument specifies the expected number of matching logs. If -// atLeast is set, this is a lower bound, otherwise there must be exactly cound +// atLeast is set, this is a lower bound, otherwise there must be exactly count // matching logs. func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) Expectation { rec, err := regexp.Compile(re) diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go index 61d1a5a6b68..b78699f8043 100644 --- a/gopls/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -1428,7 +1428,7 @@ func (c *completer) lexical(ctx context.Context) error { return nil } -// injectType manufacters candidates based on the given type. This is +// injectType manufactures candidates based on the given type. This is // intended for types not discoverable via lexical search, such as // composite and/or generic types. For example, if the type is "[]int", // this method makes sure you get candidates "[]int{}" and "[]int" diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go index 738879bf87f..732943f2417 100644 --- a/gopls/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -39,7 +39,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e } cand.score *= float64(matchScore) - // Ignore deep candidates that wont be in the MaxDeepCompletions anyway. + // Ignore deep candidates that won't be in the MaxDeepCompletions anyway. if len(cand.path) != 0 && !c.deepState.isHighScore(cand.score) { return CompletionItem{}, errLowScore } diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go index 042db178504..13f2e2d6d2d 100644 --- a/gopls/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -19,7 +19,7 @@ type SuggestedFix struct { } type RelatedInformation struct { - // TOOD(adonovan): replace these two fields by a protocol.Location. + // TODO(adonovan): replace these two fields by a protocol.Location. URI span.URI Range protocol.Range Message string diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 85d66de7844..e0b469a8815 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -178,7 +178,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr } start, end = lit.Pos(), lit.End() case token.INT: - // It's an integer, scan only if it is a hex litteral whose bitsize in + // It's an integer, scan only if it is a hex literal whose bitsize in // ranging from 8 to 32. if !(strings.HasPrefix(lit.Value, "0x") && len(lit.Value[2:]) >= 2 && len(lit.Value[2:]) <= 8) { return 0, protocol.Range{}, ErrNoRuneFound diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index b86c3ccd23a..58385f7ae97 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -328,12 +328,12 @@ type View interface { // ClearModuleUpgrades clears all upgrades for the modules in modfile. ClearModuleUpgrades(modfile span.URI) - // Vulnerabilites returns known vulnerabilities for the given modfile. + // Vulnerabilities returns known vulnerabilities for the given modfile. // TODO(suzmue): replace command.Vuln with a different type, maybe // https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary? Vulnerabilities(modfile ...span.URI) map[span.URI]*govulncheck.Result - // SetVulnerabilities resets the list of vulnerabilites that exists for the given modules + // SetVulnerabilities resets the list of vulnerabilities that exists for the given modules // required by modfile. SetVulnerabilities(modfile span.URI, vulncheckResult *govulncheck.Result) diff --git a/gopls/internal/lsp/testdata/keywords/keywords.go b/gopls/internal/lsp/testdata/keywords/keywords.go index 1fa2c12baa1..0bcaa63bffb 100644 --- a/gopls/internal/lsp/testdata/keywords/keywords.go +++ b/gopls/internal/lsp/testdata/keywords/keywords.go @@ -16,7 +16,7 @@ func _() { type _ str //@rank(" //", struct, string) switch test { - case 1: // TODO: trying to complete case here will break because the parser wont return *ast.Ident + case 1: // TODO: trying to complete case here will break because the parser won't return *ast.Ident b //@complete(" //", break) case 2: f //@complete(" //", fallthrough, for) diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 753cd68550e..9859158ae87 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -548,7 +548,7 @@ package foo tests := []struct { line string // the sole line in the buffer after the package statement pat string // the pattern to search for - want []string // expected competions + want []string // expected completions }{ {"func T", "T", []string{"TestXxx(t *testing.T)", "TestMain(m *testing.M)"}}, {"func T()", "T", []string{"TestMain", "Test"}}, diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index a0e22456e43..a96024b9ca2 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -51,7 +51,7 @@ func TestSemantic_2527(t *testing.T) { if !typeparams.Enabled { t.Skip("type parameters are needed for this test") } - // these are the expected types of identfiers in textt order + // these are the expected types of identifiers in text order want := []result{ {"package", "keyword", ""}, {"foo", "namespace", ""}, diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go index 69e89ae123a..07345c8ef50 100644 --- a/gopls/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -85,7 +85,7 @@ func SortSpans(spans []Span) { // compare implements a three-valued ordered comparison of Spans. func compare(a, b Span) int { - // This is a textual comparison. It does not peform path + // This is a textual comparison. It does not perform path // cleaning, case folding, resolution of symbolic links, // testing for existence, or any I/O. if cmp := strings.Compare(string(a.URI()), string(b.URI())); cmp != 0 { diff --git a/gopls/test/debug/debug_test.go b/gopls/test/debug/debug_test.go index 9bd89288a54..72e5d6513c0 100644 --- a/gopls/test/debug/debug_test.go +++ b/gopls/test/debug/debug_test.go @@ -6,7 +6,7 @@ package debug_test // Provide 'static type checking' of the templates. This guards against changes is various // gopls datastructures causing template execution to fail. The checking is done by -// the github.com/jba/templatecheck pacakge. Before that is run, the test checks that +// the github.com/jba/templatecheck package. Before that is run, the test checks that // its list of templates and their arguments corresponds to the arguments in // calls to render(). The test assumes that all uses of templates are done through render(). From f124b50345853d5b6885e7459fc91d7dda3d9307 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Sat, 4 Feb 2023 17:28:11 +0000 Subject: [PATCH 713/723] cmd/stringer: streamline test subprocesses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Execute the test binary itself as cmd/stringer instead of invoking (and cleaning up after) 'go build'. - Replace os.MkdirTemp with T.TempDir Changes are similar to https://go.dev/cl/377836. Change-Id: I5f9fca20e0f1f045826c385d556257fc5982ed53 GitHub-Last-Rev: f6c6b7735c97d3c2f508de91a268cf6795291ffd GitHub-Pull-Request: golang/tools#425 Reviewed-on: https://go-review.googlesource.com/c/tools/+/464350 Reviewed-by: Bryan Mills Auto-Submit: Bryan Mills Reviewed-by: Daniel Martí Reviewed-by: Michael Knyszek TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills --- cmd/stringer/endtoend_test.go | 54 +++++++++++++++++++++-------------- cmd/stringer/golden_test.go | 7 +---- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/cmd/stringer/endtoend_test.go b/cmd/stringer/endtoend_test.go index 268c2f61be2..29eb91860ff 100644 --- a/cmd/stringer/endtoend_test.go +++ b/cmd/stringer/endtoend_test.go @@ -19,9 +19,9 @@ import ( "path" "path/filepath" "strings" + "sync" "testing" - "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" ) @@ -30,9 +30,22 @@ import ( // we run stringer -type X and then compile and run the program. The resulting // binary panics if the String method for X is not correct, including for error cases. +func TestMain(m *testing.M) { + if os.Getenv("STRINGER_TEST_IS_STRINGER") != "" { + main() + os.Exit(0) + } + + // Inform subprocesses that they should run the cmd/stringer main instead of + // running tests. It's a close approximation to building and running the real + // command, and much less complicated and expensive to build and clean up. + os.Setenv("STRINGER_TEST_IS_STRINGER", "1") + + os.Exit(m.Run()) +} + func TestEndToEnd(t *testing.T) { - dir, stringer := buildStringer(t) - defer os.RemoveAll(dir) + stringer := stringerPath(t) // Read the testdata directory. fd, err := os.Open("testdata") if err != nil { @@ -64,7 +77,7 @@ func TestEndToEnd(t *testing.T) { t.Logf("cgo is not enabled for %s", name) continue } - stringerCompileAndRun(t, dir, stringer, typeName(name), name) + stringerCompileAndRun(t, t.TempDir(), stringer, typeName(name), name) } } @@ -91,8 +104,8 @@ func moreTests(t *testing.T, dirname, prefix string) []string { // TestTags verifies that the -tags flag works as advertised. func TestTags(t *testing.T) { - dir, stringer := buildStringer(t) - defer os.RemoveAll(dir) + stringer := stringerPath(t) + dir := t.TempDir() var ( protectedConst = []byte("TagProtected") output = filepath.Join(dir, "const_string.go") @@ -139,8 +152,8 @@ func TestTags(t *testing.T) { // TestConstValueChange verifies that if a constant value changes and // the stringer code is not regenerated, we'll get a compiler error. func TestConstValueChange(t *testing.T) { - dir, stringer := buildStringer(t) - defer os.RemoveAll(dir) + stringer := stringerPath(t) + dir := t.TempDir() source := filepath.Join(dir, "day.go") err := copy(source, filepath.Join("testdata", "day.go")) if err != nil { @@ -178,21 +191,20 @@ func TestConstValueChange(t *testing.T) { } } -// buildStringer creates a temporary directory and installs stringer there. -func buildStringer(t *testing.T) (dir string, stringer string) { - t.Helper() - testenv.NeedsTool(t, "go") +var exe struct { + path string + err error + once sync.Once +} - dir, err := os.MkdirTemp("", "stringer") - if err != nil { - t.Fatal(err) - } - stringer = filepath.Join(dir, "stringer.exe") - err = run("go", "build", "-o", stringer) - if err != nil { - t.Fatalf("building stringer: %s", err) +func stringerPath(t *testing.T) string { + exe.once.Do(func() { + exe.path, exe.err = os.Executable() + }) + if exe.err != nil { + t.Fatal(exe.err) } - return dir, stringer + return exe.path } // stringerCompileAndRun runs stringer for the named file and compiles and diff --git a/cmd/stringer/golden_test.go b/cmd/stringer/golden_test.go index 2e2ec58de69..250af05f903 100644 --- a/cmd/stringer/golden_test.go +++ b/cmd/stringer/golden_test.go @@ -451,12 +451,7 @@ func (i Token) String() string { func TestGolden(t *testing.T) { testenv.NeedsTool(t, "go") - dir, err := os.MkdirTemp("", "stringer") - if err != nil { - t.Error(err) - } - defer os.RemoveAll(dir) - + dir := t.TempDir() for _, test := range golden { g := Generator{ trimPrefix: test.trimPrefix, From a762c82c1bf92071826228d76d50278e350b2632 Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 13 Dec 2022 13:50:19 -0800 Subject: [PATCH 714/723] go/ssa: add MultiConvert instruction Adds a new MultiConvert instruction. MultiConvert instructions are a catch all for conversions involving a typeparameter that would result in multiple different types of conversion instruction [sequences]. Updates golang/go#56849 Change-Id: I6c2f53ccef1b933406096d6ca2867f1007a13bd3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/457436 gopls-CI: kokoro Run-TryBot: Tim King Reviewed-by: Alan Donovan Reviewed-by: Zvonimir Pavlinovic TryBot-Result: Gopher Robot --- go/callgraph/vta/graph.go | 68 +++++++++++ go/ssa/builder_go120_test.go | 52 ++++++++- go/ssa/doc.go | 1 + go/ssa/emit.go | 217 ++++++++++++++++++----------------- go/ssa/interp/ops.go | 2 + go/ssa/print.go | 26 +++++ go/ssa/sanity.go | 2 +- go/ssa/ssa.go | 34 ++++++ go/ssa/util.go | 8 +- 9 files changed, 298 insertions(+), 112 deletions(-) diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index ebb497149e1..2537123f4c4 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -12,6 +12,7 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typeparams" ) // node interface for VTA nodes. @@ -375,6 +376,8 @@ func (b *builder) instr(instr ssa.Instruction) { // SliceToArrayPointer: t1 = slice to array pointer *[4]T <- []T (t0) // No interesting flow as sliceArrayElem(t1) == sliceArrayElem(t0). return + case *ssa.MultiConvert: + b.multiconvert(i) default: panic(fmt.Sprintf("unsupported instruction %v\n", instr)) } @@ -655,6 +658,71 @@ func addReturnFlows(b *builder, r *ssa.Return, site ssa.Value) { } } +func (b *builder) multiconvert(c *ssa.MultiConvert) { + // TODO(zpavlinovic): decide what to do on MultiConvert long term. + // TODO(zpavlinovic): add unit tests. + typeSetOf := func(typ types.Type) []*typeparams.Term { + // This is a adaptation of x/exp/typeparams.NormalTerms which x/tools cannot depend on. + var terms []*typeparams.Term + var err error + switch typ := typ.(type) { + case *typeparams.TypeParam: + terms, err = typeparams.StructuralTerms(typ) + case *typeparams.Union: + terms, err = typeparams.UnionTermSet(typ) + case *types.Interface: + terms, err = typeparams.InterfaceTermSet(typ) + default: + // Common case. + // Specializing the len=1 case to avoid a slice + // had no measurable space/time benefit. + terms = []*typeparams.Term{typeparams.NewTerm(false, typ)} + } + + if err != nil { + return nil + } + return terms + } + // isValuePreserving returns true if a conversion from ut_src to + // ut_dst is value-preserving, i.e. just a change of type. + // Precondition: neither argument is a named type. + isValuePreserving := func(ut_src, ut_dst types.Type) bool { + // Identical underlying types? + if types.IdenticalIgnoreTags(ut_dst, ut_src) { + return true + } + + switch ut_dst.(type) { + case *types.Chan: + // Conversion between channel types? + _, ok := ut_src.(*types.Chan) + return ok + + case *types.Pointer: + // Conversion between pointers with identical base types? + _, ok := ut_src.(*types.Pointer) + return ok + } + return false + } + dst_terms := typeSetOf(c.Type()) + src_terms := typeSetOf(c.X.Type()) + for _, s := range src_terms { + us := s.Type().Underlying() + for _, d := range dst_terms { + ud := d.Type().Underlying() + if isValuePreserving(us, ud) { + // This is equivalent to a ChangeType. + b.addInFlowAliasEdges(b.nodeFromVal(c), b.nodeFromVal(c.X)) + return + } + // This is equivalent to either: SliceToArrayPointer,, + // SliceToArrayPointer+Deref, Size 0 Array constant, or a Convert. + } + } +} + // addInFlowEdge adds s -> d to g if d is node that can have an inflow, i.e., a node // that represents an interface or an unresolved function value. Otherwise, there // is no interesting type flow so the edge is omitted. diff --git a/go/ssa/builder_go120_test.go b/go/ssa/builder_go120_test.go index 04fb11a2d22..a691f938c04 100644 --- a/go/ssa/builder_go120_test.go +++ b/go/ssa/builder_go120_test.go @@ -29,6 +29,55 @@ func TestBuildPackageGo120(t *testing.T) { {"slice to zero length array type parameter", "package p; var s []byte; func f[T ~[0]byte]() { tmp := (T)(s); var z T; _ = tmp == z}", nil}, {"slice to non-zero length array type parameter", "package p; var s []byte; func h[T ~[1]byte | [4]byte]() { tmp := T(s); var z T; _ = tmp == z}", nil}, {"slice to maybe-zero length array type parameter", "package p; var s []byte; func g[T ~[0]byte | [4]byte]() { tmp := T(s); var z T; _ = tmp == z}", nil}, + { + "rune sequence to sequence cast patterns", ` + package p + // Each of fXX functions describes a 1.20 legal cast between sequences of runes + // as []rune, pointers to rune arrays, rune arrays, or strings. + // + // Comments listed given the current emitted instructions [approximately]. + // If multiple conversions are needed, these are seperated by |. + // rune was selected as it leads to string casts (byte is similar). + // The length 2 is not significant. + // Multiple array lengths may occur in a cast in practice (including 0). + func f00[S string, D string](s S) { _ = D(s) } // ChangeType + func f01[S string, D []rune](s S) { _ = D(s) } // Convert + func f02[S string, D []rune | string](s S) { _ = D(s) } // ChangeType | Convert + func f03[S [2]rune, D [2]rune](s S) { _ = D(s) } // ChangeType + func f04[S *[2]rune, D *[2]rune](s S) { _ = D(s) } // ChangeType + func f05[S []rune, D string](s S) { _ = D(s) } // Convert + func f06[S []rune, D [2]rune](s S) { _ = D(s) } // SliceToArrayPointer; Deref + func f07[S []rune, D [2]rune | string](s S) { _ = D(s) } // SliceToArrayPointer; Deref | Convert + func f08[S []rune, D *[2]rune](s S) { _ = D(s) } // SliceToArrayPointer + func f09[S []rune, D *[2]rune | string](s S) { _ = D(s) } // SliceToArrayPointer; Deref | Convert + func f10[S []rune, D *[2]rune | [2]rune](s S) { _ = D(s) } // SliceToArrayPointer | SliceToArrayPointer; Deref + func f11[S []rune, D *[2]rune | [2]rune | string](s S) { _ = D(s) } // SliceToArrayPointer | SliceToArrayPointer; Deref | Convert + func f12[S []rune, D []rune](s S) { _ = D(s) } // ChangeType + func f13[S []rune, D []rune | string](s S) { _ = D(s) } // Convert | ChangeType + func f14[S []rune, D []rune | [2]rune](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer; Deref + func f15[S []rune, D []rune | [2]rune | string](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer; Deref | Convert + func f16[S []rune, D []rune | *[2]rune](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer + func f17[S []rune, D []rune | *[2]rune | string](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer | Convert + func f18[S []rune, D []rune | *[2]rune | [2]rune](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer | SliceToArrayPointer; Deref + func f19[S []rune, D []rune | *[2]rune | [2]rune | string](s S) { _ = D(s) } // ChangeType | SliceToArrayPointer | SliceToArrayPointer; Deref | Convert + func f20[S []rune | string, D string](s S) { _ = D(s) } // Convert | ChangeType + func f21[S []rune | string, D []rune](s S) { _ = D(s) } // Convert | ChangeType + func f22[S []rune | string, D []rune | string](s S) { _ = D(s) } // ChangeType | Convert | Convert | ChangeType + func f23[S []rune | [2]rune, D [2]rune](s S) { _ = D(s) } // SliceToArrayPointer; Deref | ChangeType + func f24[S []rune | *[2]rune, D *[2]rune](s S) { _ = D(s) } // SliceToArrayPointer | ChangeType + `, nil, + }, + { + "matching named and underlying types", ` + package p + type a string + type b string + func g0[S []rune | a | b, D []rune | a | b](s S) { _ = D(s) } + func g1[S []rune | ~string, D []rune | a | b](s S) { _ = D(s) } + func g2[S []rune | a | b, D []rune | ~string](s S) { _ = D(s) } + func g3[S []rune | ~string, D []rune |~string](s S) { _ = D(s) } + `, nil, + }, } for _, tc := range tests { @@ -44,7 +93,8 @@ func TestBuildPackageGo120(t *testing.T) { pkg := types.NewPackage("p", "") conf := &types.Config{Importer: tc.importer} - if _, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions); err != nil { + _, _, err = ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions) + if err != nil { t.Errorf("unexpected error: %v", err) } }) diff --git a/go/ssa/doc.go b/go/ssa/doc.go index 13d02413a67..afda476b369 100644 --- a/go/ssa/doc.go +++ b/go/ssa/doc.go @@ -66,6 +66,7 @@ // *FieldAddr ✔ ✔ // *FreeVar ✔ // *Function ✔ ✔ (func) +// *GenericConvert ✔ ✔ // *Global ✔ ✔ (var) // *Go ✔ // *If ✔ diff --git a/go/ssa/emit.go b/go/ssa/emit.go index f52d87a16a1..1731c797506 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -168,32 +168,6 @@ func isValuePreserving(ut_src, ut_dst types.Type) bool { return false } -// isSliceToArrayPointer reports whether ut_src is a slice type -// that can be converted to a pointer to an array type ut_dst. -// Precondition: neither argument is a named type. -func isSliceToArrayPointer(ut_src, ut_dst types.Type) bool { - if slice, ok := ut_src.(*types.Slice); ok { - if ptr, ok := ut_dst.(*types.Pointer); ok { - if arr, ok := ptr.Elem().Underlying().(*types.Array); ok { - return types.Identical(slice.Elem(), arr.Elem()) - } - } - } - return false -} - -// isSliceToArray reports whether ut_src is a slice type -// that can be converted to an array type ut_dst. -// Precondition: neither argument is a named type. -func isSliceToArray(ut_src, ut_dst types.Type) bool { - if slice, ok := ut_src.(*types.Slice); ok { - if arr, ok := ut_dst.(*types.Array); ok { - return types.Identical(slice.Elem(), arr.Elem()) - } - } - return false -} - // emitConv emits to f code to convert Value val to exactly type typ, // and returns the converted value. Implicit conversions are required // by language assignability rules in assignments, parameter passing, @@ -208,23 +182,15 @@ func emitConv(f *Function, val Value, typ types.Type) Value { ut_dst := typ.Underlying() ut_src := t_src.Underlying() - dst_types := typeSetOf(ut_dst) - src_types := typeSetOf(ut_src) - - // Just a change of type, but not value or representation? - preserving := underIs(src_types, func(s types.Type) bool { - return underIs(dst_types, func(d types.Type) bool { - return s != nil && d != nil && isValuePreserving(s, d) // all (s -> d) are value preserving. - }) - }) - if preserving { - c := &ChangeType{X: val} - c.setType(typ) - return f.emit(c) - } - // Conversion to, or construction of a value of, an interface type? if isNonTypeParamInterface(typ) { + // Interface name change? + if isValuePreserving(ut_src, ut_dst) { + c := &ChangeType{X: val} + c.setType(typ) + return f.emit(c) + } + // Assignment from one interface type to another? if isNonTypeParamInterface(t_src) { c := &ChangeInterface{X: val} @@ -247,9 +213,83 @@ func emitConv(f *Function, val Value, typ types.Type) Value { return f.emit(mi) } + // In the common case, the typesets of src and dst are singletons + // and we emit an appropriate conversion. But if either contains + // a type parameter, the conversion may represent a cross product, + // in which case which we emit a MultiConvert. + dst_terms := typeSetOf(ut_dst) + src_terms := typeSetOf(ut_src) + + // conversionCase describes an instruction pattern that maybe emitted to + // model d <- s for d in dst_terms and s in src_terms. + // Multiple conversions can match the same pattern. + type conversionCase uint8 + const ( + changeType conversionCase = 1 << iota + sliceToArray + sliceToArrayPtr + sliceTo0Array + sliceTo0ArrayPtr + convert + ) + classify := func(s, d types.Type) conversionCase { + // Just a change of type, but not value or representation? + if isValuePreserving(s, d) { + return changeType + } + + // Conversion from slice to array or slice to array pointer? + if slice, ok := s.(*types.Slice); ok { + var arr *types.Array + var ptr bool + // Conversion from slice to array pointer? + switch d := d.(type) { + case *types.Array: + arr = d + case *types.Pointer: + arr, _ = d.Elem().Underlying().(*types.Array) + ptr = true + } + if arr != nil && types.Identical(slice.Elem(), arr.Elem()) { + if arr.Len() == 0 { + if ptr { + return sliceTo0ArrayPtr + } else { + return sliceTo0Array + } + } + if ptr { + return sliceToArrayPtr + } else { + return sliceToArray + } + } + } + + // The only remaining case in well-typed code is a representation- + // changing conversion of basic types (possibly with []byte/[]rune). + if !isBasic(s) && !isBasic(d) { + panic(fmt.Sprintf("in %s: cannot convert term %s (%s [within %s]) to type %s [within %s]", f, val, val.Type(), s, typ, d)) + } + return convert + } + + var classifications conversionCase + for _, s := range src_terms { + us := s.Type().Underlying() + for _, d := range dst_terms { + ud := d.Type().Underlying() + classifications |= classify(us, ud) + } + } + if classifications == 0 { + panic(fmt.Sprintf("in %s: cannot convert %s (%s) to %s", f, val, val.Type(), typ)) + } + // Conversion of a compile-time constant value? if c, ok := val.(*Const); ok { - if isBasic(ut_dst) || c.Value == nil { + // Conversion to a basic type? + if isBasic(ut_dst) { // Conversion of a compile-time constant to // another constant type results in a new // constant of the destination type and @@ -257,42 +297,49 @@ func emitConv(f *Function, val Value, typ types.Type) Value { // We don't truncate the value yet. return NewConst(c.Value, typ) } + // Can we always convert from zero value without panicking? + const mayPanic = sliceToArray | sliceToArrayPtr + if c.Value == nil && classifications&mayPanic == 0 { + return NewConst(nil, typ) + } // We're converting from constant to non-constant type, // e.g. string -> []byte/[]rune. } - // Conversion from slice to array pointer? - slice2ptr := underIs(src_types, func(s types.Type) bool { - return underIs(dst_types, func(d types.Type) bool { - return s != nil && d != nil && isSliceToArrayPointer(s, d) // all (s->d) are slice to array pointer conversion. - }) - }) - if slice2ptr { + switch classifications { + case changeType: // representation-preserving change + c := &ChangeType{X: val} + c.setType(typ) + return f.emit(c) + + case sliceToArrayPtr, sliceTo0ArrayPtr: // slice to array pointer c := &SliceToArrayPointer{X: val} c.setType(typ) return f.emit(c) - } - // Conversion from slice to array? - slice2array := underIs(src_types, func(s types.Type) bool { - return underIs(dst_types, func(d types.Type) bool { - return s != nil && d != nil && isSliceToArray(s, d) // all (s->d) are slice to array conversion. - }) - }) - if slice2array { - return emitSliceToArray(f, val, typ) - } + case sliceToArray: // slice to arrays (not zero-length) + ptype := types.NewPointer(typ) + p := &SliceToArrayPointer{X: val} + p.setType(ptype) + x := f.emit(p) + unOp := &UnOp{Op: token.MUL, X: x} + unOp.setType(typ) + return f.emit(unOp) + + case sliceTo0Array: // slice to zero-length arrays (constant) + return zeroConst(typ) - // A representation-changing conversion? - // All of ut_src or ut_dst is basic, byte slice, or rune slice? - if isBasicConvTypes(src_types) || isBasicConvTypes(dst_types) { + case convert: // representation-changing conversion c := &Convert{X: val} c.setType(typ) return f.emit(c) - } - panic(fmt.Sprintf("in %s: cannot convert %s (%s) to %s", f, val, val.Type(), typ)) + default: // multiple conversion + c := &MultiConvert{X: val, from: src_terms, to: dst_terms} + c.setType(typ) + return f.emit(c) + } } // emitTypeCoercion emits to f code to coerce the type of a @@ -490,48 +537,6 @@ func emitFieldSelection(f *Function, v Value, index int, wantAddr bool, id *ast. return v } -// emitSliceToArray emits to f code to convert a slice value to an array value. -// -// Precondition: all types in type set of typ are arrays and convertible to all -// types in the type set of val.Type(). -func emitSliceToArray(f *Function, val Value, typ types.Type) Value { - // Emit the following: - // if val == nil && len(typ) == 0 { - // ptr = &[0]T{} - // } else { - // ptr = SliceToArrayPointer(val) - // } - // v = *ptr - - ptype := types.NewPointer(typ) - p := &SliceToArrayPointer{X: val} - p.setType(ptype) - ptr := f.emit(p) - - nilb := f.newBasicBlock("slicetoarray.nil") - nonnilb := f.newBasicBlock("slicetoarray.nonnil") - done := f.newBasicBlock("slicetoarray.done") - - cond := emitCompare(f, token.EQL, ptr, zeroConst(ptype), token.NoPos) - emitIf(f, cond, nilb, nonnilb) - f.currentBlock = nilb - - zero := f.addLocal(typ, token.NoPos) - emitJump(f, done) - f.currentBlock = nonnilb - - emitJump(f, done) - f.currentBlock = done - - phi := &Phi{Edges: []Value{zero, ptr}, Comment: "slicetoarray"} - phi.pos = val.Pos() - phi.setType(ptype) - x := f.emit(phi) - unOp := &UnOp{Op: token.MUL, X: x} - unOp.setType(typ) - return f.emit(unOp) -} - // zeroValue emits to f code to produce a zero value of type t, // and returns it. func zeroValue(f *Function, t types.Type) Value { diff --git a/go/ssa/interp/ops.go b/go/ssa/interp/ops.go index 0ea10da9e02..39830bc8fcb 100644 --- a/go/ssa/interp/ops.go +++ b/go/ssa/interp/ops.go @@ -932,6 +932,8 @@ func typeAssert(i *interpreter, instr *ssa.TypeAssert, itf iface) value { } else { err = fmt.Sprintf("interface conversion: interface is %s, not %s", itf.t, instr.AssertedType) } + // Note: if instr.Underlying==true ever becomes reachable from interp check that + // types.Identical(itf.t.Underlying(), instr.AssertedType) if err != "" { if !instr.CommaOk { diff --git a/go/ssa/print.go b/go/ssa/print.go index e40bbfa2d21..8b783196e49 100644 --- a/go/ssa/print.go +++ b/go/ssa/print.go @@ -51,6 +51,14 @@ func relType(t types.Type, from *types.Package) string { return s } +func relTerm(term *typeparams.Term, from *types.Package) string { + s := relType(term.Type(), from) + if term.Tilde() { + return "~" + s + } + return s +} + func relString(m Member, from *types.Package) string { // NB: not all globals have an Object (e.g. init$guard), // so use Package().Object not Object.Package(). @@ -174,6 +182,24 @@ func (v *ChangeInterface) String() string { return printConv("change interfa func (v *SliceToArrayPointer) String() string { return printConv("slice to array pointer", v, v.X) } func (v *MakeInterface) String() string { return printConv("make", v, v.X) } +func (v *MultiConvert) String() string { + from := v.Parent().relPkg() + + var b strings.Builder + b.WriteString(printConv("multiconvert", v, v.X)) + b.WriteString(" [") + for i, s := range v.from { + for j, d := range v.to { + if i != 0 || j != 0 { + b.WriteString(" | ") + } + fmt.Fprintf(&b, "%s <- %s", relTerm(d, from), relTerm(s, from)) + } + } + b.WriteString("]") + return b.String() +} + func (v *MakeClosure) String() string { var b bytes.Buffer fmt.Fprintf(&b, "make closure %s", relName(v.Fn, v)) diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 994e2631f16..88ad374ded0 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -140,7 +140,7 @@ func (s *sanity) checkInstr(idx int, instr Instruction) { s.errorf("convert %s -> %s: at least one type must be basic (or all basic, []byte, or []rune)", from, to) } } - + case *MultiConvert: case *Defer: case *Extract: case *Field: diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 3108de2e4a8..5904b817b31 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -661,6 +661,30 @@ type Convert struct { X Value } +// The MultiConvert instruction yields the conversion of value X to type +// Type(). Either X.Type() or Type() must be a type parameter. Each +// type in the type set of X.Type() can be converted to each type in the +// type set of Type(). +// +// See the documentation for Convert, ChangeType, and SliceToArrayPointer +// for the conversions that are permitted. Additionally conversions of +// slices to arrays are permitted. +// +// This operation can fail dynamically (see SliceToArrayPointer). +// +// Pos() returns the ast.CallExpr.Lparen, if the instruction arose +// from an explicit conversion in the source. +// +// Example printed form: +// +// t1 = multiconvert D <- S (t0) [*[2]rune <- []rune | string <- []rune] +type MultiConvert struct { + register + X Value + from []*typeparams.Term + to []*typeparams.Term +} + // ChangeInterface constructs a value of one interface type from a // value of another interface type known to be assignable to it. // This operation cannot fail. @@ -689,6 +713,9 @@ type ChangeInterface struct { // all types in the type set of Type() which must all be pointer to array // types. // +// This operation can fail dynamically if the length of the slice is less +// than the length of the array. +// // Example printed form: // // t1 = slice to array pointer *[4]byte <- []byte (t0) @@ -1024,6 +1051,9 @@ type Next struct { // is AssertedType's zero value. The components of the pair must be // accessed using the Extract instruction. // +// If Underlying: tests whether interface value X has the underlying +// type AssertedType. +// // If AssertedType is a concrete type, TypeAssert checks whether the // dynamic type in interface X is equal to it, and if so, the result // of the conversion is a copy of the value in the interface. @@ -1642,6 +1672,10 @@ func (v *Convert) Operands(rands []*Value) []*Value { return append(rands, &v.X) } +func (v *MultiConvert) Operands(rands []*Value) []*Value { + return append(rands, &v.X) +} + func (v *SliceToArrayPointer) Operands(rands []*Value) []*Value { return append(rands, &v.X) } diff --git a/go/ssa/util.go b/go/ssa/util.go index aef57dc3ffb..db53aebee43 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -65,19 +65,19 @@ func isString(t types.Type) bool { return isBasic(t) && t.(*types.Basic).Info()&types.IsString != 0 } -// isByteSlice reports whether t is []byte. +// isByteSlice reports whether t is of the form []~bytes. func isByteSlice(t types.Type) bool { if b, ok := t.(*types.Slice); ok { - e, _ := b.Elem().(*types.Basic) + e, _ := b.Elem().Underlying().(*types.Basic) return e != nil && e.Kind() == types.Byte } return false } -// isRuneSlice reports whether t is []rune. +// isRuneSlice reports whether t is of the form []~runes. func isRuneSlice(t types.Type) bool { if b, ok := t.(*types.Slice); ok { - e, _ := b.Elem().(*types.Basic) + e, _ := b.Elem().Underlying().(*types.Basic) return e != nil && e.Kind() == types.Rune } return false From edddc5fc3223b7f12772f78079fcbcac3bd47d97 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 7 Feb 2023 09:31:53 -0500 Subject: [PATCH 715/723] go/packages: don't discard errors loading export data In the course of debugging something unrelated, I noticed that loadFromExportData's error result was always discarded. This change appends it to the set of package errors. Change-Id: I32a7cbceb7cffd29cedac19eeff8b092663813f0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/466075 Run-TryBot: Alan Donovan TryBot-Result: Gopher Robot Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- go/packages/packages.go | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/go/packages/packages.go b/go/packages/packages.go index 1d5f0e45b2b..0f1505b808a 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -878,12 +878,19 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { // never has to create a types.Package for an indirect dependency, // which would then require that such created packages be explicitly // inserted back into the Import graph as a final step after export data loading. + // (Hence this return is after the Types assignment.) // The Diamond test exercises this case. if !lpkg.needtypes && !lpkg.needsrc { return } if !lpkg.needsrc { - ld.loadFromExportData(lpkg) + if err := ld.loadFromExportData(lpkg); err != nil { + lpkg.Errors = append(lpkg.Errors, Error{ + Pos: "-", + Msg: err.Error(), + Kind: UnknownError, // e.g. can't find/open/parse export data + }) + } return // not a source package, don't get syntax trees } @@ -970,7 +977,8 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { // The config requested loading sources and types, but sources are missing. // Add an error to the package and fall back to loading from export data. appendError(Error{"-", fmt.Sprintf("sources missing for package %s", lpkg.ID), ParseError}) - ld.loadFromExportData(lpkg) + _ = ld.loadFromExportData(lpkg) // ignore any secondary errors + return // can't get syntax trees for this package } @@ -1194,9 +1202,10 @@ func sameFile(x, y string) bool { return false } -// loadFromExportData returns type information for the specified +// loadFromExportData ensures that type information is present for the specified // package, loading it from an export data file on the first request. -func (ld *loader) loadFromExportData(lpkg *loaderPackage) (*types.Package, error) { +// On success it sets lpkg.Types to a new Package. +func (ld *loader) loadFromExportData(lpkg *loaderPackage) error { if lpkg.PkgPath == "" { log.Fatalf("internal error: Package %s has no PkgPath", lpkg) } @@ -1207,8 +1216,8 @@ func (ld *loader) loadFromExportData(lpkg *loaderPackage) (*types.Package, error // must be sequential. (Finer-grained locking would require // changes to the gcexportdata API.) // - // The exportMu lock guards the Package.Pkg field and the - // types.Package it points to, for each Package in the graph. + // The exportMu lock guards the lpkg.Types field and the + // types.Package it points to, for each loaderPackage in the graph. // // Not all accesses to Package.Pkg need to be protected by exportMu: // graph ordering ensures that direct dependencies of source @@ -1217,18 +1226,18 @@ func (ld *loader) loadFromExportData(lpkg *loaderPackage) (*types.Package, error defer ld.exportMu.Unlock() if tpkg := lpkg.Types; tpkg != nil && tpkg.Complete() { - return tpkg, nil // cache hit + return nil // cache hit } lpkg.IllTyped = true // fail safe if lpkg.ExportFile == "" { // Errors while building export data will have been printed to stderr. - return nil, fmt.Errorf("no export data file") + return fmt.Errorf("no export data file") } f, err := os.Open(lpkg.ExportFile) if err != nil { - return nil, err + return err } defer f.Close() @@ -1240,7 +1249,7 @@ func (ld *loader) loadFromExportData(lpkg *loaderPackage) (*types.Package, error // queries.) r, err := gcexportdata.NewReader(f) if err != nil { - return nil, fmt.Errorf("reading %s: %v", lpkg.ExportFile, err) + return fmt.Errorf("reading %s: %v", lpkg.ExportFile, err) } // Build the view. @@ -1284,7 +1293,7 @@ func (ld *loader) loadFromExportData(lpkg *loaderPackage) (*types.Package, error // (May modify incomplete packages in view but not create new ones.) tpkg, err := gcexportdata.Read(r, ld.Fset, view, lpkg.PkgPath) if err != nil { - return nil, fmt.Errorf("reading %s: %v", lpkg.ExportFile, err) + return fmt.Errorf("reading %s: %v", lpkg.ExportFile, err) } if _, ok := view["go.shape"]; ok { // Account for the pseudopackage "go.shape" that gets @@ -1297,8 +1306,7 @@ func (ld *loader) loadFromExportData(lpkg *loaderPackage) (*types.Package, error lpkg.Types = tpkg lpkg.IllTyped = false - - return tpkg, nil + return nil } // impliedLoadMode returns loadMode with its dependencies. From 2b149ce94bddeffe0922f298a327f9f7fc5a4fce Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 3 Feb 2023 18:21:28 -0500 Subject: [PATCH 716/723] gopls/internal/regtest: add a regtest-based version of the marker tests Add a new implementation of the gopls marker tests that shares the same testing environment as the regression tests. Along the way, revisit the semantics of the marker framework, to address some problems we've identified over the years. Specifically: - Split tests into self-contained txtar encoded files. Each file determines an isolated set of markers, and is executed in a separate session. - Change the mechanisms for golden content, so that it is joined by identifiers, and passed to the test method as an argument. This makes it more apparent where golden content is used, and makes the identity of golden content stable under manipulations of the source (as opposed to some arbitrary munging of the note position) - Allow custom argument conversion that may be convenient for LSP-based test functions, by avoiding the packagestest framework and instead building directly on top of the x/tools/go/expect package. As an initial proof of concept, this allowed using a protocol.Location as a test method argument. - Add significant documentation and examples. - Deprecate the @hover marker in the old marker tests (gopls/internal/lsp). I believe that this lays the foundation to address the remaining concerns enumerated in golang/go#54845, as this new design solves the isolation problem, the problem of golden file naming, and the lack of clarity around the definition and construction of test annotations. For golang/go#54845 Change-Id: I796f35c14370b9651316baa1f86c21c63cec25c7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/465255 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Robert Findley gopls-CI: kokoro --- gopls/internal/lsp/fake/editor.go | 13 + gopls/internal/lsp/lsp_test.go | 43 +- gopls/internal/lsp/regtest/marker.go | 677 ++++++++++++++++++ .../lsp/testdata/basiclit/basiclit.go | 43 -- .../testdata/godef/hover_generics/hover.go | 17 - .../godef/hover_generics/hover.go.golden | 39 - .../testdata/godef/infer_generics/inferred.go | 12 - .../godef/infer_generics/inferred.go.golden | 16 - .../lsp/testdata/summary_go1.18.txt.golden | 2 +- gopls/internal/lsp/tests/markdown_go118.go | 27 +- gopls/internal/lsp/tests/markdown_go119.go | 20 +- gopls/internal/lsp/tests/tests.go | 21 +- gopls/internal/regtest/marker/marker_test.go | 21 + .../marker/testdata/hover/basiclit.txt | 60 ++ .../marker/testdata/hover/generics.txt | 71 ++ .../regtest/marker/testdata/hover/hover.txt | 29 + gopls/internal/regtest/misc/hover_test.go | 2 + 17 files changed, 902 insertions(+), 211 deletions(-) create mode 100644 gopls/internal/lsp/regtest/marker.go delete mode 100644 gopls/internal/lsp/testdata/godef/hover_generics/hover.go delete mode 100644 gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden delete mode 100644 gopls/internal/lsp/testdata/godef/infer_generics/inferred.go delete mode 100644 gopls/internal/lsp/testdata/godef/infer_generics/inferred.go.golden create mode 100644 gopls/internal/regtest/marker/marker_test.go create mode 100644 gopls/internal/regtest/marker/testdata/hover/basiclit.txt create mode 100644 gopls/internal/regtest/marker/testdata/hover/generics.txt create mode 100644 gopls/internal/regtest/marker/testdata/hover/hover.txt diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index 1e5b0790539..e5bc9ba8e5e 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -702,6 +702,19 @@ func (e *Editor) BufferText(name string) (string, bool) { return buf.text(), true } +// Mapper returns the protocol.Mapper for the given buffer name. +// +// If there is no open buffer with that name, it returns nil. +func (e *Editor) Mapper(name string) *protocol.Mapper { + e.mu.Lock() + defer e.mu.Unlock() + buf, ok := e.buffers[name] + if !ok { + return nil + } + return buf.mapper +} + // BufferVersion returns the current version of the buffer corresponding to // name (or 0 if it is not being edited). func (e *Editor) BufferVersion(name string) int { diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 753954a9bcd..6d15a46bd00 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -44,8 +44,8 @@ func TestMain(m *testing.M) { } // TestLSP runs the marker tests in files beneath testdata/ using -// implementations of each of the marker operations (e.g. @hover) that -// make LSP RPCs (e.g. textDocument/hover) to a gopls server. +// implementations of each of the marker operations (e.g. @codelens) that +// make LSP RPCs (e.g. textDocument/codeLens) to a gopls server. func TestLSP(t *testing.T) { tests.RunTests(t, "testdata", true, testLSP) } @@ -719,12 +719,12 @@ func (r *runner) Definition(t *testing.T, _ span.Span, d tests.Definition) { if hover != nil { didSomething = true tag := fmt.Sprintf("%s-hoverdef", d.Name) - expectHover := string(r.data.Golden(t, tag, d.Src.URI().Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, tag, d.Src.URI().Filename(), func() ([]byte, error) { return []byte(hover.Contents.Value), nil })) got := hover.Contents.Value - if got != expectHover { - tests.CheckSameMarkdown(t, got, expectHover) + if diff := tests.DiffMarkdown(want, got); diff != "" { + t.Errorf("%s: markdown mismatch:\n%s", d.Src, diff) } } if !d.OnlyHover { @@ -818,39 +818,6 @@ func (r *runner) Highlight(t *testing.T, src span.Span, spans []span.Span) { } } -func (r *runner) Hover(t *testing.T, src span.Span, text string) { - m, err := r.data.Mapper(src.URI()) - if err != nil { - t.Fatal(err) - } - loc, err := m.SpanLocation(src) - if err != nil { - t.Fatalf("failed for %v", err) - } - params := &protocol.HoverParams{ - TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } - hover, err := r.server.Hover(r.ctx, params) - if err != nil { - t.Fatal(err) - } - if text == "" { - if hover != nil { - t.Errorf("want nil, got %v\n", hover) - } - } else { - if hover == nil { - t.Fatalf("want hover result to include %s, but got nil", text) - } - if got := hover.Contents.Value; got != text { - t.Errorf("want %v, got %v\n", text, got) - } - if want, got := loc.Range, hover.Range; want != got { - t.Errorf("want range %v, got %v instead", want, got) - } - } -} - func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { // This test is substantially the same as (*runner).References in source/source_test.go. // TODO(adonovan): Factor (and remove fluff). Where should the common code live? diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go new file mode 100644 index 00000000000..768c59cf111 --- /dev/null +++ b/gopls/internal/lsp/regtest/marker.go @@ -0,0 +1,677 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package regtest + +import ( + "bytes" + "context" + "flag" + "fmt" + "go/token" + "io/fs" + "os" + "path/filepath" + "reflect" + "sort" + "strings" + "testing" + + "golang.org/x/tools/go/expect" + "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/txtar" +) + +var updateGolden = flag.Bool("update", false, "if set, update test data during marker tests") + +// RunMarkerTests runs "marker" tests in the given test data directory. +// +// A marker test uses the '//@' marker syntax of the x/tools/go/expect package +// to annotate source code with various information such as locations and +// arguments of LSP operations to be executed by the test. The syntax following +// '@' is parsed as a comma-separated list of ordinary Go function calls, for +// example +// +// //@foo(a, "b", 3),bar(0) +// +// and delegates to a corresponding function to perform LSP-related operations. +// See the Marker types documentation below for a list of supported markers. +// +// Each call argument is coerced to the type of the corresponding parameter of +// the designated function. The coercion logic may use the surrounding context, +// such as the position or nearby text. See the Argument coercion section below +// for the full set of special conversions. As a special case, the blank +// identifier '_' is treated as the zero value of the parameter type. +// +// The test runner collects test cases by searching the given directory for +// files with the .txt extension. Each file is interpreted as a txtar archive, +// which is extracted to a temporary directory. The relative path to the .txt +// file is used as the subtest name. The preliminary section of the file +// (before the first archive entry) is a free-form comment. +// +// These tests were inspired by (and in many places copied from) a previous +// iteration of the marker tests built on top of the packagestest framework. +// Key design decisions motivating this reimplementation are as follows: +// - The old tests had a single global session, causing interaction at a +// distance and several awkward workarounds. +// - The old tests could not be safely parallelized, because certain tests +// manipulated the server options +// - Relatedly, the old tests did not have a logic grouping of assertions into +// a single unit, resulting in clusters of files serving clusters of +// entangled assertions. +// - The old tests used locations in the source as test names and as the +// identity of golden content, meaning that a single edit could change the +// name of an arbitrary number of subtests, and making it difficult to +// manually edit golden content. +// - The old tests did not hew closely to LSP concepts, resulting in, for +// example, each marker implementation doing its own position +// transformations, and inventing its own mechanism for configuration. +// - The old tests had an ad-hoc session initialization process. The regtest +// environment has had more time devoted to its initialization, and has a +// more convenient API. +// - The old tests lacked documentation, and often had failures that were hard +// to understand. By starting from scratch, we can revisit these aspects. +// +// # Special files +// +// There are three types of file within the test archive that are given special +// treatment by the test runner: +// - "flags": this file is parsed as flags configuring the MarkerTest +// instance. For example, -min_go=go1.18 sets the minimum required Go version +// for the test. +// - "settings.json": (*) this file is parsed as JSON, and used as the +// session configuration (see gopls/doc/settings.md) +// - Golden files: Within the archive, file names starting with '@' are +// treated as "golden" content, and are not written to disk, but instead are +// made available to test methods expecting an argument of type *Golden, +// using the identifier following '@'. For example, if the first parameter of +// Foo were of type *Golden, the test runner would coerce the identifier a in +// the call @foo(a, "b", 3) into a *Golden by collecting golden file data +// starting with "@a/". +// +// # Marker types +// +// The following markers are supported within marker tests: +// - @diag(location, regexp): (***) see Special markers below. +// - @hover(src, dst location, g Golden): perform a textDocument/hover at the +// src location, and check that the result spans the dst location, with hover +// content matching "hover.md" in the golden data g. +// - @loc(name, location): (**) see [Special markers] below. +// +// # Argument conversion +// +// In additon to passing through literals as basic types, the marker test +// runner supports the following coercions into non-basic types: +// - string->regexp: strings are parsed as regular expressions +// - string->location: strings are parsed as regular expressions and used to +// match the first location in the line preceding the note +// - name->location: identifiers may reference named locations created using +// the @loc marker. +// - name->Golden: identifiers match the golden content contained in archive +// files prefixed by @. +// +// # Special markers +// +// There are two markers that have additional special handling, rather than +// just invoking the test method of the same name: +// - @loc(name, location): (**) specifies a named location in the source. These +// locations may be referenced by other markers. +// - @diag(location, regexp): (***) specifies an expected diagnostic +// matching the given regexp at the given location. The test runner requires +// a 1:1 correspondence between observed diagnostics and diag annotations: +// it is an error if the test runner receives a publishDiagnostics +// notification for a diagnostic that is not annotated, or if a diagnostic +// annotation does not match an existing diagnostic. +// +// # Example +// +// Here is a complete example: +// +// -- a.go -- +// package a +// +// const abc = 0x2a //@hover("b", "abc", abc),hover(" =", "abc", abc) +// -- @abc/hover.md -- +// ```go +// const abc untyped int = 42 +// ``` +// +// @hover("b", "abc", abc),hover(" =", "abc", abc) +// +// In this example, the @hover annotation tells the test runner to run the +// hoverMarker function, which has parameters: +// +// (env *Env, src, dsc protocol.Location, g *Golden). +// +// The env argument holds the implicit test environment, including fake editor +// with open files, and sandboxed directory. +// +// Argument converters translate the "b" and "abc" arguments into locations by +// interpreting each one as a regular expression and finding the location of +// its first match on the preceding portion of the line, and the abc identifier +// into a dictionary of golden content containing "hover.md". Then the +// hoverMarker method executes a textDocument/hover LSP request at the src +// position, and ensures the result spans "abc", with the markdown content from +// hover.md. (Note that the markdown content includes the expect annotation as +// the doc comment.) +// +// The next hover on the same line asserts the same result, but initiates the +// hover immediately after "abc" in the source. This tests that we find the +// preceding identifier when hovering. +// +// # Updating golden files +// +// To update golden content in the test archive, it is easier to regenerate +// content automatically rather than edit it by hand. To do this, run the +// tests with the -update flag. Only tests that actually run will be updated. +// +// In some cases, golden content will vary by Go version (for example, gopls +// produces different markdown at Go versions before the 1.19 go/doc update). +// By convention, the golden content in test archives should match the output +// at Go tip. Each test function can normalize golden content for older Go +// versions. +// +// # TODO +// +// This API is a work-in-progress, as we migrate existing marker tests from +// internal/lsp/tests. +// +// Remaining TODO: +// - parallelize/optimize test execution +// - actually support regexp locations? +// - (*) add support for per-test editor settings (via a settings.json file) +// - (**) add support for locs +// - (***) add special handling for diagnostics +// - add support for per-test environment? +// - reorganize regtest packages (and rename to just 'test'?) +// +// Existing marker tests to port: +// - CallHierarchy +// - CodeLens +// - Diagnostics +// - CompletionItems +// - Completions +// - CompletionSnippets +// - UnimportedCompletions +// - DeepCompletions +// - FuzzyCompletions +// - CaseSensitiveCompletions +// - RankCompletions +// - FoldingRanges +// - Formats +// - Imports +// - SemanticTokens +// - SuggestedFixes +// - FunctionExtractions +// - MethodExtractions +// - Definitions +// - Implementations +// - Highlights +// - References +// - Renames +// - PrepareRenames +// - Symbols +// - InlayHints +// - WorkspaceSymbols +// - Signatures +// - Links +// - AddImport +// - SelectionRanges +func RunMarkerTests(t *testing.T, dir string) { + tests, err := loadMarkerTests(dir) + if err != nil { + t.Fatal(err) + } + + // Opt: use a shared cache. + // TODO: opt: use a memoize store with no eviction. + cache := cache.New(nil, nil) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // TODO(rfindley): it may be more useful to have full support for build + // constraints. + if test.minGoVersion != "" { + var go1point int + if _, err := fmt.Sscanf(test.minGoVersion, "go1.%d", &go1point); err != nil { + t.Fatalf("parsing -min_go version: %v", err) + } + testenv.NeedsGo1Point(t, 18) + } + test.executed = true + env := newEnv(t, cache, test.files) + // TODO(rfindley): make it easier to clean up the regtest environment. + defer env.Editor.Shutdown(context.Background()) // ignore error + defer env.Sandbox.Close() // ignore error + + // Open all files so that we operate consistently with LSP clients, and + // (pragmatically) so that we have a Mapper available via the fake + // editor. + // + // This also allows avoiding mutating the editor state in tests. + for file := range test.files { + env.OpenFile(file) + } + + // Invoke each method in the test. + for _, note := range test.notes { + posn := safetoken.StartPosition(test.fset, note.Pos) + mi, ok := markers[note.Name] + if !ok { + t.Errorf("%s: no marker function named %s", posn, note.Name) + continue + } + + // The first converter corresponds to the *Env argument. All others + // must be coerced from the marker syntax. + if got, want := len(note.Args), len(mi.converters); got != want { + t.Errorf("%s: got %d argumentsto %s, expect %d", posn, got, note.Name, want) + continue + } + + args := []reflect.Value{reflect.ValueOf(env)} + hasErrors := false + for i, in := range note.Args { + // Special handling for the blank identifier: treat it as the zero + // value. + if ident, ok := in.(expect.Identifier); ok && ident == "_" { + zero := reflect.Zero(mi.paramTypes[i]) + args = append(args, zero) + continue + } + out, err := mi.converters[i](env, test, note, in) + if err != nil { + t.Errorf("%s: converting argument #%d of %s (%v): %v", posn, i, note.Name, in, err) + hasErrors = true + } + args = append(args, reflect.ValueOf(out)) + } + + if !hasErrors { + mi.fn.Call(args) + } + } + }) + } + + // If updateGolden is set, golden content was updated during text execution, + // so we can now update the test data. + // TODO(rfindley): even when -update is not set, compare updated content with + // actual content. + if *updateGolden { + if err := writeMarkerTests(dir, tests); err != nil { + t.Fatalf("failed to -update: %v", err) + } + } +} + +// supported markers, excluding @loc and @diag which are handled separately. +// +// Each marker func must accept an *Env as its first argument, with subsequent +// arguments coerced from the arguments to the marker annotation. +// +// Marker funcs should not mutate the test environment (e.g. via opening files +// or applying edits in the editor). +var markers = map[string]markerInfo{ + "hover": makeMarker(hoverMarker), +} + +// MarkerTest holds all the test data extracted from a test txtar archive. +// +// See the documentation for RunMarkerTests for more information on the archive +// format. +type MarkerTest struct { + name string // relative path to the txtar file in the testdata dir + fset *token.FileSet // fileset used for parsing notes + files map[string][]byte + notes []*expect.Note + golden map[string]*Golden + + // executed tracks whether the test was executed. + // + // When -update is set, only tests that were actually executed are written. + executed bool + + // flags holds flags extracted from the special "flags" archive file. + flags []string + + // Parsed flags values. + minGoVersion string +} + +// flagSet returns the flagset used for parsing the special "flags" file in the +// test archive. +func (t *MarkerTest) flagSet() *flag.FlagSet { + flags := flag.NewFlagSet(t.name, flag.ContinueOnError) + flags.StringVar(&t.minGoVersion, "min_go", "", "if set, the minimum go1.X version required for this test") + return flags +} + +// Golden holds extracted golden content for a single @ prefix. The +// +// When -update is set, golden captures the updated golden contents for later +// writing. +type Golden struct { + data map[string][]byte + updated map[string][]byte +} + +// Get returns golden content for the given name, which corresponds to the +// relative path following the golden prefix @/. For example, to access +// the content of @foo/path/to/result.json from the Golden associated with +// @foo, name should be "path/to/result.json". +// +// If -update is set, the given update function will be called to get the +// updated golden content that should be written back to testdata. +func (g *Golden) Get(t testing.TB, name string, update func() []byte) []byte { + if *updateGolden { + d := update() + if existing, ok := g.updated[name]; ok { + // Multiple tests may reference the same golden data, but if they do they + // must agree about its expected content. + if diff := compare.Text(string(existing), string(d)); diff != "" { + t.Fatalf("conflicting updates for golden data %s:\n%s", name, diff) + } + } + if g.updated == nil { + g.updated = make(map[string][]byte) + } + g.updated[name] = d + return d + } + return g.data[name] +} + +// loadMarkerTests walks the given dir looking for .txt files, which it +// interprets as a txtar archive. +// +// See the documentation for RunMarkerTests for more details on the test data +// archive. +// +// TODO(rfindley): this test could sanity check the results. For example, it is +// too easy to write "// @" instead of "//@", which we will happy skip silently. +func loadMarkerTests(dir string) ([]*MarkerTest, error) { + var tests []*MarkerTest + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if strings.HasSuffix(path, ".txt") { + content, err := os.ReadFile(path) + if err != nil { + return err + } + archive := txtar.Parse(content) + name := strings.TrimPrefix(path, dir+string(filepath.Separator)) + test, err := loadMarkerTest(name, archive) + if err != nil { + return fmt.Errorf("%s: %v", path, err) + } + tests = append(tests, test) + } + return nil + }) + return tests, err +} + +func loadMarkerTest(name string, archive *txtar.Archive) (*MarkerTest, error) { + test := &MarkerTest{ + name: name, + fset: token.NewFileSet(), + files: make(map[string][]byte), + golden: make(map[string]*Golden), + } + for _, file := range archive.Files { + if file.Name == "flags" { + test.flags = strings.Fields(string(file.Data)) + if err := test.flagSet().Parse(test.flags); err != nil { + return nil, fmt.Errorf("parsing flags: %v", err) + } + continue + } + if strings.HasPrefix(file.Name, "@") { + // golden content + // TODO: use strings.Cut once we are on 1.18+. + idx := strings.IndexByte(file.Name, '/') + if idx < 0 { + return nil, fmt.Errorf("golden file path %q must contain '/'", file.Name) + } + goldenID := file.Name[len("@"):idx] + if _, ok := test.golden[goldenID]; !ok { + test.golden[goldenID] = &Golden{ + data: make(map[string][]byte), + } + } + test.golden[goldenID].data[file.Name[idx+len("/"):]] = file.Data + } else { + // ordinary file content + notes, err := expect.Parse(test.fset, file.Name, file.Data) + if err != nil { + return nil, fmt.Errorf("parsing notes in %q: %v", file.Name, err) + } + test.notes = append(test.notes, notes...) + test.files[file.Name] = file.Data + } + } + return test, nil +} + +// writeMarkerTests writes the updated golden content to the test data files. +func writeMarkerTests(dir string, tests []*MarkerTest) error { + for _, test := range tests { + if !test.executed { + continue + } + arch := &txtar.Archive{} + + // Special configuration files go first. + if len(test.flags) > 0 { + flags := strings.Join(test.flags, " ") + arch.Files = append(arch.Files, txtar.File{Name: "flags", Data: []byte(flags)}) + } + + // ...followed by ordinary files + var files []txtar.File + for name, data := range test.files { + files = append(files, txtar.File{Name: name, Data: data}) + } + sort.Slice(files, func(i, j int) bool { + return files[i].Name < files[j].Name + }) + arch.Files = append(arch.Files, files...) + + // ...followed by golden files + var goldenFiles []txtar.File + for id, golden := range test.golden { + for name, data := range golden.updated { + fullName := "@" + id + "/" + name + goldenFiles = append(goldenFiles, txtar.File{Name: fullName, Data: data}) + } + } + sort.Slice(goldenFiles, func(i, j int) bool { + return goldenFiles[i].Name < goldenFiles[j].Name + }) + arch.Files = append(arch.Files, goldenFiles...) + + data := txtar.Format(arch) + filename := filepath.Join(dir, test.name) + if err := os.WriteFile(filename, data, 0644); err != nil { + return err + } + } + return nil +} + +// newEnv creates a new environment for a marker test. +// +// TODO(rfindley): simplify and refactor the construction of testing +// environments across regtests, marker tests, and benchmarks. +func newEnv(t *testing.T, cache *cache.Cache, files map[string][]byte) *Env { + sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ + RootDir: t.TempDir(), + GOPROXY: "https://proxy.golang.org", + Files: files, + }) + if err != nil { + t.Fatal(err) + } + + // 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") + + awaiter := NewAwaiter(sandbox.Workdir) + ss := lsprpc.NewStreamServer(cache, false, hooks.Options) + server := servertest.NewPipeServer(ss, jsonrpc2.NewRawStream) + editor, err := fake.NewEditor(sandbox, fake.EditorConfig{}).Connect(ctx, server, awaiter.Hooks()) + if err != nil { + sandbox.Close() // ignore error + t.Fatal(err) + } + if err := awaiter.Await(ctx, InitialWorkspaceLoad); err != nil { + sandbox.Close() // ignore error + t.Fatal(err) + } + return &Env{ + T: t, + Ctx: ctx, + Editor: editor, + Sandbox: sandbox, + Awaiter: awaiter, + } +} + +type markerInfo struct { + fn reflect.Value // the func to invoke + paramTypes []reflect.Type // parameter types, for zero values + converters []converter // to convert non-blank arguments +} + +type converter func(*Env, *MarkerTest, *expect.Note, interface{}) (interface{}, error) + +// makeMarker uses reflection to load markerInfo for the given func value. +func makeMarker(fn interface{}) markerInfo { + mi := markerInfo{ + fn: reflect.ValueOf(fn), + } + mtyp := mi.fn.Type() + if mtyp.NumIn() == 0 || mtyp.In(0) != envType { + panic(fmt.Sprintf("marker function %#v must accept *Env as its first argument", mi.fn)) + } + if mtyp.NumOut() != 0 { + panic(fmt.Sprintf("marker function %#v must not have results", mi.fn)) + } + for a := 1; a < mtyp.NumIn(); a++ { + in := mtyp.In(a) + mi.paramTypes = append(mi.paramTypes, in) + c := makeConverter(in) + mi.converters = append(mi.converters, c) + } + return mi +} + +// Types with special conversions. +var ( + envType = reflect.TypeOf(&Env{}) + locationType = reflect.TypeOf(protocol.Location{}) + goldenType = reflect.TypeOf(&Golden{}) +) + +func makeConverter(paramType reflect.Type) converter { + switch paramType { + case locationType: + return locationConverter + case goldenType: + return goldenConverter + default: + return func(_ *Env, _ *MarkerTest, _ *expect.Note, arg interface{}) (interface{}, error) { + if argType := reflect.TypeOf(arg); argType != paramType { + return nil, fmt.Errorf("cannot convert type %s to %s", argType, paramType) + } + return arg, nil + } + } +} + +// locationConverter converts a string argument into the protocol location +// corresponding to the first position of the string in the line preceding the +// note. +func locationConverter(env *Env, test *MarkerTest, note *expect.Note, arg interface{}) (interface{}, error) { + file := test.fset.File(note.Pos) + posn := safetoken.StartPosition(test.fset, note.Pos) + lineStart := file.LineStart(posn.Line) + startOff, endOff, err := safetoken.Offsets(file, lineStart, note.Pos) + if err != nil { + return nil, err + } + m := env.Editor.Mapper(file.Name()) + substr, ok := arg.(string) + if !ok { + return nil, fmt.Errorf("cannot convert argument type %T to location (must be a string to match the preceding line)", arg) + } + + preceding := m.Content[startOff:endOff] + idx := bytes.Index(preceding, []byte(substr)) + if idx < 0 { + return nil, fmt.Errorf("substring %q not found in %q", substr, preceding) + } + off := startOff + idx + loc, err := m.OffsetLocation(off, off+len(substr)) + return loc, err +} + +// goldenConverter converts an identifier into the Golden directory of content +// prefixed by @ in the test archive file. +func goldenConverter(_ *Env, test *MarkerTest, note *expect.Note, arg interface{}) (interface{}, error) { + switch arg := arg.(type) { + case expect.Identifier: + golden := test.golden[string(arg)] + // If there was no golden content for this identifier, we must create one + // to handle the case where -update_golden is set: we need a place to store + // the updated content. + if golden == nil { + golden = new(Golden) + test.golden[string(arg)] = golden + } + return golden, nil + default: + return nil, fmt.Errorf("invalid input type %T: golden key must be an identifier", arg) + } +} + +// hoverMarker implements the @hover marker, running textDocument/hover at the +// given src location and asserting that the resulting hover is over the dst +// location (typically a span surrounding src), and that the markdown content +// matches the golden content. +func hoverMarker(env *Env, src, dst protocol.Location, golden *Golden) { + content, gotDst := env.Hover(src) + if gotDst != dst { + env.T.Errorf("%s: hover location does not match:\n\tgot: %s\n\twant %s)", src, gotDst, dst) + } + gotMD := "" + if content != nil { + gotMD = content.Value + } + wantMD := "" + if golden != nil { + wantMD = string(golden.Get(env.T, "hover.md", func() []byte { return []byte(gotMD) })) + } + // Normalize newline termination: archive files can't express non-newline + // terminated files. + if strings.HasSuffix(wantMD, "\n") && !strings.HasSuffix(gotMD, "\n") { + gotMD += "\n" + } + if diff := tests.DiffMarkdown(wantMD, gotMD); diff != "" { + env.T.Errorf("%s: hover markdown mismatch (-want +got):\n%s", src, diff) + } +} diff --git a/gopls/internal/lsp/testdata/basiclit/basiclit.go b/gopls/internal/lsp/testdata/basiclit/basiclit.go index 9829003d357..ab895dc011c 100644 --- a/gopls/internal/lsp/testdata/basiclit/basiclit.go +++ b/gopls/internal/lsp/testdata/basiclit/basiclit.go @@ -10,47 +10,4 @@ func _() { _ = 1. //@complete(".") _ = 'a' //@complete("' ") - - _ = 'a' //@hover("'a'", "'a', U+0061, LATIN SMALL LETTER A") - _ = 0x61 //@hover("0x61", "'a', U+0061, LATIN SMALL LETTER A") - - _ = '\u2211' //@hover("'\\u2211'", "'∑', U+2211, N-ARY SUMMATION") - _ = 0x2211 //@hover("0x2211", "'∑', U+2211, N-ARY SUMMATION") - _ = "foo \u2211 bar" //@hover("\\u2211", "'∑', U+2211, N-ARY SUMMATION") - - _ = '\a' //@hover("'\\a'", "U+0007, control") - _ = "foo \a bar" //@hover("\\a", "U+0007, control") - - _ = '\U0001F30A' //@hover("'\\U0001F30A'", "'🌊', U+1F30A, WATER WAVE") - _ = 0x0001F30A //@hover("0x0001F30A", "'🌊', U+1F30A, WATER WAVE") - _ = "foo \U0001F30A bar" //@hover("\\U0001F30A", "'🌊', U+1F30A, WATER WAVE") - - _ = '\x7E' //@hover("'\\x7E'", "'~', U+007E, TILDE") - _ = "foo \x7E bar" //@hover("\\x7E", "'~', U+007E, TILDE") - _ = "foo \a bar" //@hover("\\a", "U+0007, control") - - _ = '\173' //@hover("'\\173'", "'{', U+007B, LEFT CURLY BRACKET") - _ = "foo \173 bar" //@hover("\\173", "'{', U+007B, LEFT CURLY BRACKET") - _ = "foo \173 bar \u2211 baz" //@hover("\\173", "'{', U+007B, LEFT CURLY BRACKET") - _ = "foo \173 bar \u2211 baz" //@hover("\\u2211", "'∑', U+2211, N-ARY SUMMATION") - _ = "foo\173bar\u2211baz" //@hover("\\173", "'{', U+007B, LEFT CURLY BRACKET") - _ = "foo\173bar\u2211baz" //@hover("\\u2211", "'∑', U+2211, N-ARY SUMMATION") - - // search for runes in string only if there is an escaped sequence - _ = "hello" //@hover("\"hello\"", "") - - // incorrect escaped rune sequences - _ = '\0' //@hover("'\\0'", "") - _ = '\u22111' //@hover("'\\u22111'", "") - _ = '\U00110000' //@hover("'\\U00110000'", "") - _ = '\u12e45'//@hover("'\\u12e45'", "") - _ = '\xa' //@hover("'\\xa'", "") - _ = 'aa' //@hover("'aa'", "") - - // other basic lits - _ = 1 //@hover("1", "") - _ = 1.2 //@hover("1.2", "") - _ = 1.2i //@hover("1.2i", "") - _ = 0123 //@hover("0123", "") - _ = 0x1234567890 //@hover("0x1234567890", "") } diff --git a/gopls/internal/lsp/testdata/godef/hover_generics/hover.go b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go deleted file mode 100644 index a26980a5e15..00000000000 --- a/gopls/internal/lsp/testdata/godef/hover_generics/hover.go +++ /dev/null @@ -1,17 +0,0 @@ -package hover - -type value[T any] struct { //@mark(value, "value"),hoverdef("value", value),mark(valueTdecl, "T"),hoverdef("T",valueTdecl) - val T //@mark(valueTparam, "T"),hoverdef("T", valueTparam) - Q int //@mark(valueQfield, "Q"),hoverdef("Q", valueQfield) -} - -type Value[T any] struct { //@mark(ValueTdecl, "T"),hoverdef("T",ValueTdecl) - val T //@mark(ValueTparam, "T"),hoverdef("T", ValueTparam) - Q int //@mark(ValueQfield, "Q"),hoverdef("Q", ValueQfield) -} - -// disabled - see issue #54822 -func F[P interface{ ~int | string }]() { // mark(Pparam, "P"),hoverdef("P",Pparam) - // disabled - see issue #54822 - var _ P // mark(Pvar, "P"),hoverdef("P",Pvar) -} diff --git a/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden deleted file mode 100644 index fb03865bf8b..00000000000 --- a/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden +++ /dev/null @@ -1,39 +0,0 @@ --- ValueQfield-hoverdef -- -```go -field Q int -``` - -@mark(ValueQfield, "Q"),hoverdef("Q", ValueQfield) - - -[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/hover_generics#Value.Q) --- ValueTdecl-hoverdef -- -```go -type parameter T any -``` --- ValueTparam-hoverdef -- -```go -type parameter T any -``` --- value-hoverdef -- -```go -type value[T any] struct { - val T //@mark(valueTparam, "T"),hoverdef("T", valueTparam) - Q int //@mark(valueQfield, "Q"),hoverdef("Q", valueQfield) -} -``` --- valueQfield-hoverdef -- -```go -field Q int -``` - -@mark(valueQfield, "Q"),hoverdef("Q", valueQfield) - --- valueTdecl-hoverdef -- -```go -type parameter T any -``` --- valueTparam-hoverdef -- -```go -type parameter T any -``` diff --git a/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go b/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go deleted file mode 100644 index 2d92a959083..00000000000 --- a/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go +++ /dev/null @@ -1,12 +0,0 @@ -package inferred - -func app[S interface{ ~[]E }, E interface{}](s S, e E) S { - return append(s, e) -} - -func _() { - _ = app[[]int] //@mark(constrInfer, "app"),hoverdef("app", constrInfer) - _ = app[[]int, int] //@mark(instance, "app"),hoverdef("app", instance) - _ = app[[]int]([]int{}, 0) //@mark(partialInfer, "app"),hoverdef("app", partialInfer) - _ = app([]int{}, 0) //@mark(argInfer, "app"),hoverdef("app", argInfer) -} diff --git a/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go.golden b/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go.golden deleted file mode 100644 index 3fcc5f43539..00000000000 --- a/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go.golden +++ /dev/null @@ -1,16 +0,0 @@ --- argInfer-hoverdef -- -```go -func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S -``` --- constrInfer-hoverdef -- -```go -func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S -``` --- instance-hoverdef -- -```go -func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S -``` --- partialInfer-hoverdef -- -```go -func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S -``` diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 735259e6428..9d62f8b16f1 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -16,7 +16,7 @@ SemanticTokenCount = 3 SuggestedFixCount = 71 FunctionExtractionCount = 27 MethodExtractionCount = 6 -DefinitionsCount = 110 +DefinitionsCount = 99 TypeDefinitionsCount = 18 HighlightsCount = 69 InlayHintsCount = 5 diff --git a/gopls/internal/lsp/tests/markdown_go118.go b/gopls/internal/lsp/tests/markdown_go118.go index c8c5eef172b..55fe106b8cc 100644 --- a/gopls/internal/lsp/tests/markdown_go118.go +++ b/gopls/internal/lsp/tests/markdown_go118.go @@ -10,24 +10,20 @@ package tests import ( "regexp" "strings" - "testing" "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) -// The markdown in the golden files matches the converter in comment.go, -// but for go1.19 and later the conversion is done using go/doc/comment. -// Compared to the newer version, the older version -// has extra escapes, and treats code blocks slightly differently. -func CheckSameMarkdown(t *testing.T, got, want string) { - t.Helper() - - got = normalizeMarkdown(got) +// DiffMarkdown compares two markdown strings produced by parsing go doc +// comments. +// +// For go1.19 and later, markdown conversion is done using go/doc/comment. +// Compared to the newer version, the older version has extra escapes, and +// treats code blocks slightly differently. +func DiffMarkdown(want, got string) string { want = normalizeMarkdown(want) - - if diff := compare.Text(want, got); diff != "" { - t.Errorf("normalized markdown differs:\n%s", diff) - } + got = normalizeMarkdown(got) + return compare.Text(want, got) } // normalizeMarkdown normalizes whitespace and escaping of the input string, to @@ -52,10 +48,15 @@ func normalizeMarkdown(input string) string { `\@`, `@`, `\(`, `(`, `\)`, `)`, + `\{`, `{`, + `\}`, `}`, `\"`, `"`, `\.`, `.`, `\-`, `-`, `\'`, `'`, + `\+`, `+`, + `\~`, `~`, + `\=`, `=`, `\n\n\n`, `\n\n`, // Note that these are *escaped* newlines. ).Replace(input) diff --git a/gopls/internal/lsp/tests/markdown_go119.go b/gopls/internal/lsp/tests/markdown_go119.go index e09ed62c4aa..a7fcf1a42ef 100644 --- a/gopls/internal/lsp/tests/markdown_go119.go +++ b/gopls/internal/lsp/tests/markdown_go119.go @@ -8,19 +8,15 @@ package tests import ( - "testing" - "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) -// The markdown in the golden files matches the converter in comment.go, -// but for go1.19 and later the conversion is done using go/doc/comment. -// Compared to the newer version, the older version -// has extra escapes, and treats code blocks slightly differently. -func CheckSameMarkdown(t *testing.T, got, want string) { - t.Helper() - - if diff := compare.Text(want, got); diff != "" { - t.Errorf("normalized markdown differs:\n%s", diff) - } +// DiffMarkdown compares two markdown strings produced by parsing go doc +// comments. +// +// For go1.19 and later, markdown conversion is done using go/doc/comment. +// Compared to the newer version, the older version has extra escapes, and +// treats code blocks slightly differently. +func DiffMarkdown(want, got string) string { + return compare.Text(want, got) } diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index eaa2bb9b078..2a370b7b732 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -93,7 +93,6 @@ type WorkspaceSymbols = map[WorkspaceSymbolsTestType]map[span.URI][]string type Signatures = map[span.Span]*protocol.SignatureHelp type Links = map[span.URI][]Link type AddImport = map[span.URI]string -type Hovers = map[span.Span]string type SelectionRanges = []span.Span type Data struct { @@ -129,7 +128,6 @@ type Data struct { Signatures Signatures Links Links AddImport AddImport - Hovers Hovers SelectionRanges SelectionRanges fragments map[string]string @@ -144,7 +142,7 @@ type Data struct { } // The Tests interface abstracts the LSP-based implementation of the marker -// test operators (such as @hover) appearing in files beneath ../testdata/. +// test operators (such as @codelens) appearing in files beneath ../testdata/. // // TODO(adonovan): reduce duplication; see https://github.com/golang/go/issues/54845. // There is only one implementation (*runner in ../lsp_test.go), so @@ -179,7 +177,6 @@ type Tests interface { SignatureHelp(*testing.T, span.Span, *protocol.SignatureHelp) Link(*testing.T, span.URI, []Link) AddImport(*testing.T, span.URI, string) - Hover(*testing.T, span.Span, string) SelectionRanges(*testing.T, span.Span) } @@ -332,7 +329,6 @@ func load(t testing.TB, mode string, dir string) *Data { Signatures: make(Signatures), Links: make(Links), AddImport: make(AddImport), - Hovers: make(Hovers), dir: dir, fragments: map[string]string{}, @@ -483,7 +479,6 @@ func load(t testing.TB, mode string, dir string) *Data { "implementations": datum.collectImplementations, "typdef": datum.collectTypeDefinitions, "hoverdef": datum.collectHoverDefinitions, - "hover": datum.collectHovers, "highlight": datum.collectHighlights, "inlayHint": datum.collectInlayHints, "refs": datum.collectReferences, @@ -782,16 +777,6 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) - t.Run("Hover", func(t *testing.T) { - t.Helper() - for pos, info := range data.Hovers { - t.Run(SpanName(pos), func(t *testing.T) { - t.Helper() - tests.Hover(t, pos, info) - }) - } - }) - t.Run("InlayHints", func(t *testing.T) { t.Helper() for _, src := range data.InlayHints { @@ -1294,10 +1279,6 @@ func (data *Data) collectHoverDefinitions(src, target span.Span) { } } -func (data *Data) collectHovers(src span.Span, expected string) { - data.Hovers[src] = expected -} - func (data *Data) collectTypeDefinitions(src, target span.Span) { data.Definitions[src] = Definition{ Src: src, diff --git a/gopls/internal/regtest/marker/marker_test.go b/gopls/internal/regtest/marker/marker_test.go new file mode 100644 index 00000000000..ac051a555e0 --- /dev/null +++ b/gopls/internal/regtest/marker/marker_test.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package marker + +import ( + "testing" + + . "golang.org/x/tools/gopls/internal/lsp/regtest" +) + +// Note: we use a separate package for the marker tests so that we can easily +// compare their performance to the existing marker tests in ./internal/lsp. + +// TestMarkers runs the marker tests from the testdata directory. +// +// See RunMarkerTests for details on how marker tests work. +func TestMarkers(t *testing.T) { + RunMarkerTests(t, "testdata") +} diff --git a/gopls/internal/regtest/marker/testdata/hover/basiclit.txt b/gopls/internal/regtest/marker/testdata/hover/basiclit.txt new file mode 100644 index 00000000000..30fe16e21cc --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/hover/basiclit.txt @@ -0,0 +1,60 @@ +This test checks gopls behavior when hovering over basic literals. +-- basiclit.go -- +package basiclit + +func _() { + _ = 'a' //@hover("'a'", "'a'", latinA) + _ = 0x61 //@hover("0x61", "0x61", latinA) + + _ = '\u2211' //@hover("'\\u2211'", "'\\u2211'", summation) + _ = 0x2211 //@hover("0x2211", "0x2211", summation) + _ = "foo \u2211 bar" //@hover("\\u2211", "\\u2211", summation) + + _ = '\a' //@hover("'\\a'", "'\\a'", control) + _ = "foo \a bar" //@hover("\\a", "\\a", control) + + _ = '\U0001F30A' //@hover("'\\U0001F30A'", "'\\U0001F30A'", waterWave) + _ = 0x0001F30A //@hover("0x0001F30A", "0x0001F30A", waterWave) + _ = "foo \U0001F30A bar" //@hover("\\U0001F30A", "\\U0001F30A", waterWave) + + _ = '\x7E' //@hover("'\\x7E'", `'\x7E'`, tilde) + _ = "foo \x7E bar" //@hover("\\x7E", "\\x7E", tilde) + _ = "foo \a bar" //@hover("\\a", "\\a", control) + + _ = '\173' //@hover("'\\173'", "'\\173'", leftCurly) + _ = "foo \173 bar" //@hover("\\173","\\173", leftCurly) + _ = "foo \173 bar \u2211 baz" //@hover("\\173","\\173", leftCurly) + _ = "foo \173 bar \u2211 baz" //@hover("\\u2211","\\u2211", summation) + _ = "foo\173bar\u2211baz" //@hover("\\173","\\173", leftCurly) + _ = "foo\173bar\u2211baz" //@hover("\\u2211","\\u2211", summation) + + // search for runes in string only if there is an escaped sequence + _ = "hello" //@hover("\"hello\"", _, _) + + // incorrect escaped rune sequences + _ = '\0' //@hover("'\\0'", _, _) + _ = '\u22111' //@hover("'\\u22111'", _, _) + _ = '\U00110000' //@hover("'\\U00110000'", _, _) + _ = '\u12e45'//@hover("'\\u12e45'", _, _) + _ = '\xa' //@hover("'\\xa'", _, _) + _ = 'aa' //@hover("'aa'", _, _) + + // other basic lits + _ = 1 //@hover("1", _, _) + _ = 1.2 //@hover("1.2", _, _) + _ = 1.2i //@hover("1.2i", _, _) + _ = 0123 //@hover("0123", _, _) + _ = 0x1234567890 //@hover("0x1234567890", _, _) +) +-- @control/hover.md -- +U+0007, control +-- @latinA/hover.md -- +'a', U+0061, LATIN SMALL LETTER A +-- @leftCurly/hover.md -- +'{', U+007B, LEFT CURLY BRACKET +-- @summation/hover.md -- +'∑', U+2211, N-ARY SUMMATION +-- @tilde/hover.md -- +'~', U+007E, TILDE +-- @waterWave/hover.md -- +'🌊', U+1F30A, WATER WAVE diff --git a/gopls/internal/regtest/marker/testdata/hover/generics.txt b/gopls/internal/regtest/marker/testdata/hover/generics.txt new file mode 100644 index 00000000000..137981f3813 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/hover/generics.txt @@ -0,0 +1,71 @@ +This file contains tests for hovering over generic Go code. +-- flags -- +-min_go=go1.18 +-- generics.go -- +package generics + +type value[T any] struct { //hover("lue", "value", value),hover("T", "T", valueT) + val T //@hover("T", "T", valuevalT) + Q int //@hover("Q", "Q", valueQ) +} + +type Value[T any] struct { //@hover("T", "T", ValueT) + val T //@hover("T", "T", ValuevalT) + 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) +} +-- go.mod -- +// A go.mod is require for correct pkgsite links. +// TODO(rfindley): don't link to ad-hoc or command-line-arguments packages! +module mod.com + +go 1.18 +-- inferred.go -- +package generics + +func app[S interface{ ~[]E }, E interface{}](s S, e E) S { + return append(s, e) +} + +func _() { + _ = app[[]int] //@hover("app", "app", appint) + _ = app[[]int, int] //@hover("app", "app", appint) + _ = app[[]int]([]int{}, 0) //@hover("app", "app", appint) + _ = app([]int{}, 0) //@hover("app", "app", appint) +} +-- @ValueQ/hover.md -- +```go +field Q int +``` + +@hover("Q", "Q", ValueQ) + + +[`(generics.Value).Q` on pkg.go.dev](https://pkg.go.dev/mod.com#Value.Q) +-- @ValueT/hover.md -- +```go +type parameter T any +``` +-- @ValuevalT/hover.md -- +```go +type parameter T any +``` +-- @appint/hover.md -- +```go +func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S +``` +-- @valueQ/hover.md -- +```go +field Q int +``` + +@hover("Q", "Q", valueQ) +-- @valuevalT/hover.md -- +```go +type parameter T any +``` diff --git a/gopls/internal/regtest/marker/testdata/hover/hover.txt b/gopls/internal/regtest/marker/testdata/hover/hover.txt new file mode 100644 index 00000000000..f9cd3311bc8 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/hover/hover.txt @@ -0,0 +1,29 @@ +This test demonstrates some features of the new marker test runner. +-- a.go -- +package a + +const abc = 0x2a //@hover("b", "abc", abc),hover(" =", "abc", abc) +-- typeswitch.go -- +package a + +func _() { + var y interface{} + switch x := y.(type) { //@hover("x", "x", x) + case int: + println(x) //@hover("x", "x", xint),hover(")", "x", xint) + } +} +-- @abc/hover.md -- +```go +const abc untyped int = 42 +``` + +@hover("b", "abc", abc),hover(" =", "abc", abc) +-- @x/hover.md -- +```go +var x interface{} +``` +-- @xint/hover.md -- +```go +var x int +``` diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go index 7054993079c..8b5f089ab3d 100644 --- a/gopls/internal/regtest/misc/hover_test.go +++ b/gopls/internal/regtest/misc/hover_test.go @@ -115,6 +115,8 @@ func main() { // Tests that hovering does not trigger the panic in golang/go#48249. func TestPanicInHoverBrokenCode(t *testing.T) { + // Note: this test can not be expressed as a marker test, as it must use + // content without a trailing newline. const source = ` -- main.go -- package main From 24a13c6fade52381ab08896106c15e4dd1429fd1 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 6 Feb 2023 18:08:12 -0500 Subject: [PATCH 717/723] gopls/internal/regtest: fill out features of the new marker tests Add missing features to the new marker test implementation, implement a few new markers, and port some tests to demonstrate the new structure. Additionally, improve UX following some experience working with these tests. Specifically: - Add support for settings.json. This was necessary for standard library hover, since full documentation was too verbose and varied across Go versions. - Ensure that the ordering of ordinary archive files is preserved. I kept having go.mod sorted below go files, which harms readability. - Add a helper to provide a nice location summary for test output, formatting both local and global (=archive-wide) positions. - Add support for both regexp and string locations conversion. - Add the loc marker, which is pre-processed to make named locations available to other markers. - Add the diag marker, which defines a 1:1 pairing between observed and expected diagnostics. - Add the def marker, which runs textDocument/definition. - Port around half of the godef tests, which include both def and hover markers. While doing so, try to extract related assertions into separate tests, to improve organization and documentation and reduce test size. Remaining tests will have to wait, as this CL was getting too big. For golang/go#54845 Change-Id: Id9fe22c00ebd1b3a96eeacc5c0e82fca9c95c680 Reviewed-on: https://go-review.googlesource.com/c/tools/+/465895 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/fake/editor.go | 3 + gopls/internal/lsp/regtest/expectation.go | 20 + gopls/internal/lsp/regtest/marker.go | 474 ++++++++++++----- gopls/internal/lsp/testdata/godef/a/a.go | 111 ---- .../internal/lsp/testdata/godef/a/a.go.golden | 230 --------- gopls/internal/lsp/testdata/godef/a/a_test.go | 8 - .../lsp/testdata/godef/a/a_test.go.golden | 26 - gopls/internal/lsp/testdata/godef/a/f.go | 1 + gopls/internal/lsp/testdata/godef/a/random.go | 31 -- .../lsp/testdata/godef/a/random.go.golden | 113 ----- gopls/internal/lsp/testdata/godef/b/b.go | 57 --- .../internal/lsp/testdata/godef/b/b.go.golden | 480 ------------------ gopls/internal/lsp/testdata/godef/b/c.go | 8 - .../internal/lsp/testdata/godef/b/c.go.golden | 76 --- .../internal/lsp/testdata/godef/b/c.go.saved | 7 - gopls/internal/lsp/testdata/godef/b/h.go | 10 - .../internal/lsp/testdata/godef/b/h.go.golden | 13 - .../internal/lsp/testdata/summary.txt.golden | 2 +- .../lsp/testdata/summary_go1.18.txt.golden | 2 +- .../marker/testdata/definition/embed.txt | 226 +++++++++ .../marker/testdata/definition/import.txt | 52 ++ .../marker/testdata/definition/misc.txt | 230 +++++++++ .../marker/testdata/hover/basiclit.txt | 6 +- .../regtest/marker/testdata/hover/const.txt | 18 + .../marker/testdata/hover/generics.txt | 3 +- .../regtest/marker/testdata/hover/std.txt | 80 +++ 26 files changed, 973 insertions(+), 1314 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/godef/a/a.go delete mode 100644 gopls/internal/lsp/testdata/godef/a/a.go.golden delete mode 100644 gopls/internal/lsp/testdata/godef/a/a_test.go delete mode 100644 gopls/internal/lsp/testdata/godef/a/a_test.go.golden delete mode 100644 gopls/internal/lsp/testdata/godef/a/random.go delete mode 100644 gopls/internal/lsp/testdata/godef/a/random.go.golden delete mode 100644 gopls/internal/lsp/testdata/godef/b/b.go delete mode 100644 gopls/internal/lsp/testdata/godef/b/b.go.golden delete mode 100644 gopls/internal/lsp/testdata/godef/b/c.go delete mode 100644 gopls/internal/lsp/testdata/godef/b/c.go.golden delete mode 100644 gopls/internal/lsp/testdata/godef/b/c.go.saved delete mode 100644 gopls/internal/lsp/testdata/godef/b/h.go delete mode 100644 gopls/internal/lsp/testdata/godef/b/h.go.golden create mode 100644 gopls/internal/regtest/marker/testdata/definition/embed.txt create mode 100644 gopls/internal/regtest/marker/testdata/definition/import.txt create mode 100644 gopls/internal/regtest/marker/testdata/definition/misc.txt create mode 100644 gopls/internal/regtest/marker/testdata/hover/const.txt create mode 100644 gopls/internal/regtest/marker/testdata/hover/std.txt diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index e5bc9ba8e5e..84d7fb85fda 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -774,6 +774,9 @@ func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty // GoToDefinition jumps to the definition of the symbol at the given position // in an open buffer. It returns the location of the resulting jump. +// +// TODO(rfindley): rename to "Definition", to be consistent with LSP +// terminology. func (e *Editor) GoToDefinition(ctx context.Context, loc protocol.Location) (protocol.Location, error) { if err := e.checkBufferLocation(loc); err != nil { return protocol.Location{}, err diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go index 9c50b294661..9d9f023d92a 100644 --- a/gopls/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -171,6 +171,26 @@ func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) E } } +// ReadAllDiagnostics is an expectation that stores all published diagnostics +// into the provided map, whenever it is evaluated. +// +// It can be used in combination with OnceMet or AfterChange to capture the +// state of diagnostics when other expectations are satisfied. +func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Expectation { + check := func(s State) Verdict { + allDiags := make(map[string]*protocol.PublishDiagnosticsParams) + for name, diags := range s.diagnostics { + allDiags[name] = diags + } + *into = allDiags + return Met + } + return Expectation{ + Check: check, + Description: "read all diagnostics", + } +} + // NoOutstandingWork asserts that there is no work initiated using the LSP // $/progress API that has not completed. func NoOutstandingWork() Expectation { diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index 768c59cf111..1bba223ae70 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -7,6 +7,7 @@ package regtest import ( "bytes" "context" + "encoding/json" "flag" "fmt" "go/token" @@ -14,6 +15,7 @@ import ( "os" "path/filepath" "reflect" + "regexp" "sort" "strings" "testing" @@ -91,49 +93,44 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m // - "flags": this file is parsed as flags configuring the MarkerTest // instance. For example, -min_go=go1.18 sets the minimum required Go version // for the test. -// - "settings.json": (*) this file is parsed as JSON, and used as the +// - "settings.json": this file is parsed as JSON, and used as the // session configuration (see gopls/doc/settings.md) // - Golden files: Within the archive, file names starting with '@' are // treated as "golden" content, and are not written to disk, but instead are // made available to test methods expecting an argument of type *Golden, // using the identifier following '@'. For example, if the first parameter of -// Foo were of type *Golden, the test runner would coerce the identifier a in -// the call @foo(a, "b", 3) into a *Golden by collecting golden file data -// starting with "@a/". +// Foo were of type *Golden, the test runner would convert the identifier a +// in the call @foo(a, "b", 3) into a *Golden by collecting golden file +// data starting with "@a/". // // # Marker types // // The following markers are supported within marker tests: -// - @diag(location, regexp): (***) see Special markers below. -// - @hover(src, dst location, g Golden): perform a textDocument/hover at the -// src location, and check that the result spans the dst location, with hover +// - diag(location, regexp): specifies an expected diagnostic matching the +// given regexp at the given location. The test runner requires +// a 1:1 correspondence between observed diagnostics and diag annotations +// - def(src, dst location): perform a textDocument/definition request at +// the src location, and check the the result points to the dst location. +// - hover(src, dst location, g Golden): perform a textDocument/hover at the +// src location, and checks that the result is the dst location, with hover // content matching "hover.md" in the golden data g. -// - @loc(name, location): (**) see [Special markers] below. +// - loc(name, location): specifies the name of a location in the source. These +// locations may be referenced by other markers. // // # Argument conversion // -// In additon to passing through literals as basic types, the marker test -// runner supports the following coercions into non-basic types: -// - string->regexp: strings are parsed as regular expressions -// - string->location: strings are parsed as regular expressions and used to -// match the first location in the line preceding the note -// - name->location: identifiers may reference named locations created using -// the @loc marker. -// - name->Golden: identifiers match the golden content contained in archive -// files prefixed by @. -// -// # Special markers -// -// There are two markers that have additional special handling, rather than -// just invoking the test method of the same name: -// - @loc(name, location): (**) specifies a named location in the source. These -// locations may be referenced by other markers. -// - @diag(location, regexp): (***) specifies an expected diagnostic -// matching the given regexp at the given location. The test runner requires -// a 1:1 correspondence between observed diagnostics and diag annotations: -// it is an error if the test runner receives a publishDiagnostics -// notification for a diagnostic that is not annotated, or if a diagnostic -// annotation does not match an existing diagnostic. +// In additon to the types supported by go/expect, the marker test runner +// applies the following argument conversions from argument type to parameter +// type: +// - string->regexp: the argument parsed as a regular expressions +// - string->location: the location of the first instance of the +// argument in the partial line preceding the note +// - regexp->location: the location of the first match for the argument in +// the partial line preceding the note If the regular expression contains +// exactly one subgroup, the position of the subgroup is used rather than the +// position of the submatch. +// - name->location: the named location corresponding to the argument +// - name->Golden: the golden content prefixed by @ // // # Example // @@ -183,6 +180,8 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m // at Go tip. Each test function can normalize golden content for older Go // versions. // +// -update does not cause missing @diag markers to be added. +// // # TODO // // This API is a work-in-progress, as we migrate existing marker tests from @@ -190,10 +189,6 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m // // Remaining TODO: // - parallelize/optimize test execution -// - actually support regexp locations? -// - (*) add support for per-test editor settings (via a settings.json file) -// - (**) add support for locs -// - (***) add special handling for diagnostics // - add support for per-test environment? // - reorganize regtest packages (and rename to just 'test'?) // @@ -251,10 +246,16 @@ func RunMarkerTests(t *testing.T, dir string) { testenv.NeedsGo1Point(t, 18) } test.executed = true - env := newEnv(t, cache, test.files) + c := &markerContext{ + test: test, + env: newEnv(t, cache, test.files, test.settings), + + locations: make(map[expect.Identifier]protocol.Location), + diags: make(map[protocol.Location][]protocol.Diagnostic), + } // TODO(rfindley): make it easier to clean up the regtest environment. - defer env.Editor.Shutdown(context.Background()) // ignore error - defer env.Sandbox.Close() // ignore error + defer c.env.Editor.Shutdown(context.Background()) // ignore error + defer c.env.Sandbox.Close() // ignore error // Open all files so that we operate consistently with LSP clients, and // (pragmatically) so that we have a Mapper available via the fake @@ -262,45 +263,55 @@ func RunMarkerTests(t *testing.T, dir string) { // // This also allows avoiding mutating the editor state in tests. for file := range test.files { - env.OpenFile(file) + c.env.OpenFile(file) } - // Invoke each method in the test. + // Pre-process locations. + var notes []*expect.Note for _, note := range test.notes { - posn := safetoken.StartPosition(test.fset, note.Pos) + switch note.Name { + case "loc": + mi := markers[note.Name] + if err := runMarker(c, mi, note); err != nil { + t.Error(err) + } + default: + notes = append(notes, note) + } + } + + // Wait for the didOpen notifications to be processed, then collect + // diagnostics. + var diags map[string]*protocol.PublishDiagnosticsParams + c.env.AfterChange(ReadAllDiagnostics(&diags)) + for path, params := range diags { + uri := c.env.Sandbox.Workdir.URI(path) + for _, diag := range params.Diagnostics { + loc := protocol.Location{ + URI: uri, + Range: diag.Range, + } + c.diags[loc] = append(c.diags[loc], diag) + } + } + + // Invoke each remaining note function in the test. + for _, note := range notes { mi, ok := markers[note.Name] if !ok { + posn := safetoken.StartPosition(test.fset, note.Pos) t.Errorf("%s: no marker function named %s", posn, note.Name) continue } - - // The first converter corresponds to the *Env argument. All others - // must be coerced from the marker syntax. - if got, want := len(note.Args), len(mi.converters); got != want { - t.Errorf("%s: got %d argumentsto %s, expect %d", posn, got, note.Name, want) - continue - } - - args := []reflect.Value{reflect.ValueOf(env)} - hasErrors := false - for i, in := range note.Args { - // Special handling for the blank identifier: treat it as the zero - // value. - if ident, ok := in.(expect.Identifier); ok && ident == "_" { - zero := reflect.Zero(mi.paramTypes[i]) - args = append(args, zero) - continue - } - out, err := mi.converters[i](env, test, note, in) - if err != nil { - t.Errorf("%s: converting argument #%d of %s (%v): %v", posn, i, note.Name, in, err) - hasErrors = true - } - args = append(args, reflect.ValueOf(out)) + if err := runMarker(c, mi, note); err != nil { + t.Error(err) } + } - if !hasErrors { - mi.fn.Call(args) + // Any remaining (un-eliminated) diagnostics are an error. + for loc, diags := range c.diags { + for _, diag := range diags { + t.Errorf("%s: unexpected diagnostic: %q", c.fmtLoc(loc), diag.Message) } } }) @@ -317,15 +328,47 @@ func RunMarkerTests(t *testing.T, dir string) { } } -// supported markers, excluding @loc and @diag which are handled separately. +// runMarker calls mi.fn with the arguments coerced from note. +func runMarker(c *markerContext, mi markerInfo, note *expect.Note) error { + posn := safetoken.StartPosition(c.test.fset, note.Pos) + // The first converter corresponds to the *Env argument. All others + // must be coerced from the marker syntax. + if got, want := len(note.Args), len(mi.converters); got != want { + return fmt.Errorf("%s: got %d arguments to %s, expect %d", posn, got, note.Name, want) + } + + args := []reflect.Value{reflect.ValueOf(c)} + for i, in := range note.Args { + // Special handling for the blank identifier: treat it as the zero + // value. + if ident, ok := in.(expect.Identifier); ok && ident == "_" { + zero := reflect.Zero(mi.paramTypes[i]) + args = append(args, zero) + continue + } + out, err := mi.converters[i](c, note, in) + if err != nil { + return fmt.Errorf("%s: converting argument #%d of %s (%v): %v", posn, i, note.Name, in, err) + } + args = append(args, reflect.ValueOf(out)) + } + + mi.fn.Call(args) + return nil +} + +// Supported markers. // -// Each marker func must accept an *Env as its first argument, with subsequent -// arguments coerced from the arguments to the marker annotation. +// Each marker func must accept an markerContext as its first argument, with +// subsequent arguments coerced from the marker arguments. // // Marker funcs should not mutate the test environment (e.g. via opening files // or applying edits in the editor). var markers = map[string]markerInfo{ + "def": makeMarker(defMarker), + "diag": makeMarker(diagMarker), "hover": makeMarker(hoverMarker), + "loc": makeMarker(locMarker), } // MarkerTest holds all the test data extracted from a test txtar archive. @@ -333,11 +376,13 @@ var markers = map[string]markerInfo{ // See the documentation for RunMarkerTests for more information on the archive // format. type MarkerTest struct { - name string // relative path to the txtar file in the testdata dir - fset *token.FileSet // fileset used for parsing notes - files map[string][]byte - notes []*expect.Note - golden map[string]*Golden + name string // relative path to the txtar file in the testdata dir + fset *token.FileSet // fileset used for parsing notes + archive *txtar.Archive // original test archive + settings map[string]interface{} // gopls settings + files map[string][]byte // data files from the archive (excluding special files) + notes []*expect.Note // extracted notes from data files + golden map[string]*Golden // extracted golden content, by identifier name // executed tracks whether the test was executed. // @@ -346,7 +391,6 @@ type MarkerTest struct { // flags holds flags extracted from the special "flags" archive file. flags []string - // Parsed flags values. minGoVersion string } @@ -364,6 +408,7 @@ func (t *MarkerTest) flagSet() *flag.FlagSet { // When -update is set, golden captures the updated golden contents for later // writing. type Golden struct { + id string data map[string][]byte updated map[string][]byte } @@ -382,7 +427,7 @@ func (g *Golden) Get(t testing.TB, name string, update func() []byte) []byte { // Multiple tests may reference the same golden data, but if they do they // must agree about its expected content. if diff := compare.Text(string(existing), string(d)); diff != "" { - t.Fatalf("conflicting updates for golden data %s:\n%s", name, diff) + t.Errorf("conflicting updates for golden data %s/%s:\n%s", g.id, name, diff) } } if g.updated == nil { @@ -425,10 +470,11 @@ func loadMarkerTests(dir string) ([]*MarkerTest, error) { func loadMarkerTest(name string, archive *txtar.Archive) (*MarkerTest, error) { test := &MarkerTest{ - name: name, - fset: token.NewFileSet(), - files: make(map[string][]byte), - golden: make(map[string]*Golden), + name: name, + fset: token.NewFileSet(), + archive: archive, + files: make(map[string][]byte), + golden: make(map[string]*Golden), } for _, file := range archive.Files { if file.Name == "flags" { @@ -438,8 +484,13 @@ func loadMarkerTest(name string, archive *txtar.Archive) (*MarkerTest, error) { } continue } - if strings.HasPrefix(file.Name, "@") { - // golden content + if file.Name == "settings.json" { + if err := json.Unmarshal(file.Data, &test.settings); err != nil { + return nil, err + } + continue + } + if strings.HasPrefix(file.Name, "@") { // golden content // TODO: use strings.Cut once we are on 1.18+. idx := strings.IndexByte(file.Name, '/') if idx < 0 { @@ -448,20 +499,23 @@ func loadMarkerTest(name string, archive *txtar.Archive) (*MarkerTest, error) { goldenID := file.Name[len("@"):idx] if _, ok := test.golden[goldenID]; !ok { test.golden[goldenID] = &Golden{ + id: goldenID, data: make(map[string][]byte), } } test.golden[goldenID].data[file.Name[idx+len("/"):]] = file.Data - } else { - // ordinary file content - notes, err := expect.Parse(test.fset, file.Name, file.Data) - if err != nil { - return nil, fmt.Errorf("parsing notes in %q: %v", file.Name, err) - } - test.notes = append(test.notes, notes...) - test.files[file.Name] = file.Data + continue + } + + // ordinary file content + notes, err := expect.Parse(test.fset, file.Name, file.Data) + if err != nil { + return nil, fmt.Errorf("parsing notes in %q: %v", file.Name, err) } + test.notes = append(test.notes, notes...) + test.files[file.Name] = file.Data } + return test, nil } @@ -471,25 +525,32 @@ func writeMarkerTests(dir string, tests []*MarkerTest) error { if !test.executed { continue } - arch := &txtar.Archive{} + arch := &txtar.Archive{ + Comment: test.archive.Comment, + } // Special configuration files go first. if len(test.flags) > 0 { flags := strings.Join(test.flags, " ") arch.Files = append(arch.Files, txtar.File{Name: "flags", Data: []byte(flags)}) } + if len(test.settings) > 0 { + data, err := json.MarshalIndent(test.settings, "", "\t") + if err != nil { + return err + } + arch.Files = append(arch.Files, txtar.File{Name: "settings.json", Data: data}) + } - // ...followed by ordinary files - var files []txtar.File - for name, data := range test.files { - files = append(files, txtar.File{Name: name, Data: data}) + // ...followed by ordinary files. Preserve the order they appeared in the + // original archive. + for _, file := range test.archive.Files { + if _, ok := test.files[file.Name]; ok { // ordinary file + arch.Files = append(arch.Files, file) + } } - sort.Slice(files, func(i, j int) bool { - return files[i].Name < files[j].Name - }) - arch.Files = append(arch.Files, files...) - // ...followed by golden files + // ...followed by golden files. var goldenFiles []txtar.File for id, golden := range test.golden { for name, data := range golden.updated { @@ -497,6 +558,8 @@ func writeMarkerTests(dir string, tests []*MarkerTest) error { goldenFiles = append(goldenFiles, txtar.File{Name: fullName, Data: data}) } } + // Unlike ordinary files, golden content is usually not manually edited, so + // we sort lexically. sort.Slice(goldenFiles, func(i, j int) bool { return goldenFiles[i].Name < goldenFiles[j].Name }) @@ -515,7 +578,7 @@ func writeMarkerTests(dir string, tests []*MarkerTest) error { // // TODO(rfindley): simplify and refactor the construction of testing // environments across regtests, marker tests, and benchmarks. -func newEnv(t *testing.T, cache *cache.Cache, files map[string][]byte) *Env { +func newEnv(t *testing.T, cache *cache.Cache, files map[string][]byte, settings map[string]interface{}) *Env { sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ RootDir: t.TempDir(), GOPROXY: "https://proxy.golang.org", @@ -533,7 +596,10 @@ func newEnv(t *testing.T, cache *cache.Cache, files map[string][]byte) *Env { awaiter := NewAwaiter(sandbox.Workdir) ss := lsprpc.NewStreamServer(cache, false, hooks.Options) server := servertest.NewPipeServer(ss, jsonrpc2.NewRawStream) - editor, err := fake.NewEditor(sandbox, fake.EditorConfig{}).Connect(ctx, server, awaiter.Hooks()) + config := fake.EditorConfig{ + Settings: settings, + } + editor, err := fake.NewEditor(sandbox, config).Connect(ctx, server, awaiter.Hooks()) if err != nil { sandbox.Close() // ignore error t.Fatal(err) @@ -557,7 +623,59 @@ type markerInfo struct { converters []converter // to convert non-blank arguments } -type converter func(*Env, *MarkerTest, *expect.Note, interface{}) (interface{}, error) +type markerContext struct { + test *MarkerTest + env *Env + + // Collected information. + locations map[expect.Identifier]protocol.Location + diags map[protocol.Location][]protocol.Diagnostic +} + +// fmtLoc formats the given location in the context of the test, using +// archive-relative paths for files, and including the line number in the full +// archive file. +func (c markerContext) fmtLoc(loc protocol.Location) string { + if loc == (protocol.Location{}) { + return "" + } + lines := bytes.Count(c.test.archive.Comment, []byte("\n")) + var name string + for _, f := range c.test.archive.Files { + lines++ // -- separator -- + uri := c.env.Sandbox.Workdir.URI(f.Name) + if uri == loc.URI { + name = f.Name + break + } + lines += bytes.Count(f.Data, []byte("\n")) + } + if name == "" { + c.env.T.Errorf("unable to find %s in test archive", loc) + return "" + } + m := c.env.Editor.Mapper(name) + s, err := m.LocationSpan(loc) + if err != nil { + c.env.T.Errorf("error formatting location %s: %v", loc, err) + return "" + } + + innerSpan := fmt.Sprintf("%d:%d", s.Start().Line(), s.Start().Column()) // relative to the embedded file + outerSpan := fmt.Sprintf("%d:%d", lines+s.Start().Line(), s.Start().Column()) // relative to the archive file + if s.End().Line() == s.Start().Line() { + innerSpan += fmt.Sprintf("-%d", s.End().Column()) + outerSpan += fmt.Sprintf("-%d", s.End().Column()) + } else { + innerSpan += fmt.Sprintf("-%d:%d", s.End().Line(), s.End().Column()) + innerSpan += fmt.Sprintf("-%d:%d", lines+s.End().Line(), s.End().Column()) + } + + return fmt.Sprintf("%s:%s (%s:%s)", name, innerSpan, c.test.name, outerSpan) +} + +// converter is the signature of argument converters. +type converter func(*markerContext, *expect.Note, interface{}) (interface{}, error) // makeMarker uses reflection to load markerInfo for the given func value. func makeMarker(fn interface{}) markerInfo { @@ -565,8 +683,8 @@ func makeMarker(fn interface{}) markerInfo { fn: reflect.ValueOf(fn), } mtyp := mi.fn.Type() - if mtyp.NumIn() == 0 || mtyp.In(0) != envType { - panic(fmt.Sprintf("marker function %#v must accept *Env as its first argument", mi.fn)) + if mtyp.NumIn() == 0 || mtyp.In(0) != markerContextType { + panic(fmt.Sprintf("marker function %#v must accept markerContext as its first argument", mi.fn)) } if mtyp.NumOut() != 0 { panic(fmt.Sprintf("marker function %#v must not have results", mi.fn)) @@ -582,19 +700,20 @@ func makeMarker(fn interface{}) markerInfo { // Types with special conversions. var ( - envType = reflect.TypeOf(&Env{}) - locationType = reflect.TypeOf(protocol.Location{}) - goldenType = reflect.TypeOf(&Golden{}) + goldenType = reflect.TypeOf(&Golden{}) + locationType = reflect.TypeOf(protocol.Location{}) + markerContextType = reflect.TypeOf(&markerContext{}) + regexpType = reflect.TypeOf(®exp.Regexp{}) ) func makeConverter(paramType reflect.Type) converter { switch paramType { - case locationType: - return locationConverter case goldenType: return goldenConverter + case locationType: + return locationConverter default: - return func(_ *Env, _ *MarkerTest, _ *expect.Note, arg interface{}) (interface{}, error) { + return func(_ *markerContext, _ *expect.Note, arg interface{}) (interface{}, error) { if argType := reflect.TypeOf(arg); argType != paramType { return nil, fmt.Errorf("cannot convert type %s to %s", argType, paramType) } @@ -606,42 +725,85 @@ func makeConverter(paramType reflect.Type) converter { // locationConverter converts a string argument into the protocol location // corresponding to the first position of the string in the line preceding the // note. -func locationConverter(env *Env, test *MarkerTest, note *expect.Note, arg interface{}) (interface{}, error) { - file := test.fset.File(note.Pos) - posn := safetoken.StartPosition(test.fset, note.Pos) - lineStart := file.LineStart(posn.Line) - startOff, endOff, err := safetoken.Offsets(file, lineStart, note.Pos) +func locationConverter(c *markerContext, note *expect.Note, arg interface{}) (interface{}, error) { + switch arg := arg.(type) { + case string: + startOff, preceding, m, err := linePreceding(c, note.Pos) + if err != nil { + return protocol.Location{}, err + } + idx := bytes.Index(preceding, []byte(arg)) + if idx < 0 { + return nil, fmt.Errorf("substring %q not found in %q", arg, preceding) + } + off := startOff + idx + return m.OffsetLocation(off, off+len(arg)) + case *regexp.Regexp: + return findRegexpInLine(c, note.Pos, arg) + case expect.Identifier: + loc, ok := c.locations[arg] + if !ok { + return nil, fmt.Errorf("no location named %q", arg) + } + return loc, nil + default: + return nil, fmt.Errorf("cannot convert argument type %T to location (must be a string to match the preceding line)", arg) + } +} + +// findRegexpInLine searches the partial line preceding pos for a match for the +// regular expression re, returning a location spanning the first match. If re +// contains exactly one subgroup, the position of this subgroup match is +// returned rather than the position of the full match. +func findRegexpInLine(c *markerContext, pos token.Pos, re *regexp.Regexp) (protocol.Location, error) { + startOff, preceding, m, err := linePreceding(c, pos) if err != nil { - return nil, err + return protocol.Location{}, err } - m := env.Editor.Mapper(file.Name()) - substr, ok := arg.(string) - if !ok { - return nil, fmt.Errorf("cannot convert argument type %T to location (must be a string to match the preceding line)", arg) + + matches := re.FindSubmatchIndex(preceding) + if len(matches) == 0 { + return protocol.Location{}, fmt.Errorf("no match for regexp %q found in %q", re, string(preceding)) + } + var start, end int + switch len(matches) { + case 2: + // no subgroups: return the range of the regexp expression + start, end = matches[0], matches[1] + case 4: + // one subgroup: return its range + start, end = matches[2], matches[3] + default: + return protocol.Location{}, fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d", re, len(matches)/2-1) } - preceding := m.Content[startOff:endOff] - idx := bytes.Index(preceding, []byte(substr)) - if idx < 0 { - return nil, fmt.Errorf("substring %q not found in %q", substr, preceding) + return m.OffsetLocation(start+startOff, end+startOff) +} + +func linePreceding(c *markerContext, pos token.Pos) (int, []byte, *protocol.Mapper, error) { + file := c.test.fset.File(pos) + posn := safetoken.Position(file, pos) + lineStart := file.LineStart(posn.Line) + startOff, endOff, err := safetoken.Offsets(file, lineStart, pos) + if err != nil { + return 0, nil, nil, err } - off := startOff + idx - loc, err := m.OffsetLocation(off, off+len(substr)) - return loc, err + m := c.env.Editor.Mapper(file.Name()) + return startOff, m.Content[startOff:endOff], m, nil } -// goldenConverter converts an identifier into the Golden directory of content +// goldenConverter convers an identifier into the Golden directory of content // prefixed by @ in the test archive file. -func goldenConverter(_ *Env, test *MarkerTest, note *expect.Note, arg interface{}) (interface{}, error) { +func goldenConverter(c *markerContext, note *expect.Note, arg interface{}) (interface{}, error) { switch arg := arg.(type) { case expect.Identifier: - golden := test.golden[string(arg)] + golden := c.test.golden[string(arg)] // If there was no golden content for this identifier, we must create one - // to handle the case where -update_golden is set: we need a place to store + // to handle the case where -update is set: we need a place to store // the updated content. if golden == nil { golden = new(Golden) - test.golden[string(arg)] = golden + c.test.golden[string(arg)] = golden } return golden, nil default: @@ -649,14 +811,26 @@ func goldenConverter(_ *Env, test *MarkerTest, note *expect.Note, arg interface{ } } +// defMarker implements the @godef marker, running textDocument/definition at +// the given src location and asserting that there is exactly one resulting +// location, matching dst. +// +// TODO(rfindley): support a variadic destination set. +func defMarker(c *markerContext, src, dst protocol.Location) { + got := c.env.GoToDefinition(src) + if got != dst { + c.env.T.Errorf("%s: definition location does not match:\n\tgot: %s\n\twant %s", c.fmtLoc(src), c.fmtLoc(got), c.fmtLoc(dst)) + } +} + // hoverMarker implements the @hover marker, running textDocument/hover at the // given src location and asserting that the resulting hover is over the dst // location (typically a span surrounding src), and that the markdown content // matches the golden content. -func hoverMarker(env *Env, src, dst protocol.Location, golden *Golden) { - content, gotDst := env.Hover(src) +func hoverMarker(c *markerContext, src, dst protocol.Location, golden *Golden) { + content, gotDst := c.env.Hover(src) if gotDst != dst { - env.T.Errorf("%s: hover location does not match:\n\tgot: %s\n\twant %s)", src, gotDst, dst) + c.env.T.Errorf("%s: hover location does not match:\n\tgot: %s\n\twant %s)", c.fmtLoc(src), c.fmtLoc(gotDst), c.fmtLoc(dst)) } gotMD := "" if content != nil { @@ -664,7 +838,7 @@ func hoverMarker(env *Env, src, dst protocol.Location, golden *Golden) { } wantMD := "" if golden != nil { - wantMD = string(golden.Get(env.T, "hover.md", func() []byte { return []byte(gotMD) })) + wantMD = string(golden.Get(c.env.T, "hover.md", func() []byte { return []byte(gotMD) })) } // Normalize newline termination: archive files can't express non-newline // terminated files. @@ -672,6 +846,30 @@ func hoverMarker(env *Env, src, dst protocol.Location, golden *Golden) { gotMD += "\n" } if diff := tests.DiffMarkdown(wantMD, gotMD); diff != "" { - env.T.Errorf("%s: hover markdown mismatch (-want +got):\n%s", src, diff) + c.env.T.Errorf("%s: hover markdown mismatch (-want +got):\n%s", c.fmtLoc(src), diff) + } +} + +// locMarker implements the @loc hover marker. It is executed before other +// markers, so that locations are available. +func locMarker(c *markerContext, name expect.Identifier, loc protocol.Location) { + c.locations[name] = loc +} + +// diagMarker implements the @diag hover marker. It eliminates diagnostics from +// the observed set in the markerContext. +func diagMarker(c *markerContext, loc protocol.Location, re *regexp.Regexp) { + idx := -1 + diags := c.diags[loc] + for i, diag := range diags { + if re.MatchString(diag.Message) { + idx = i + break + } + } + if idx >= 0 { + c.diags[loc] = append(diags[:idx], diags[idx+1:]...) + } else { + c.env.T.Errorf("%s: no diagnostic matches %q", c.fmtLoc(loc), re) } } diff --git a/gopls/internal/lsp/testdata/godef/a/a.go b/gopls/internal/lsp/testdata/godef/a/a.go deleted file mode 100644 index 53ca6ddc412..00000000000 --- a/gopls/internal/lsp/testdata/godef/a/a.go +++ /dev/null @@ -1,111 +0,0 @@ -// Package a is a package for testing go to definition. -package a //@mark(aPackage, "a "),hoverdef("a ", aPackage) - -import ( - "fmt" - "go/types" - "sync" -) - -var ( - // x is a variable. - x string //@x,hoverdef("x", x) -) - -// Constant block. When I hover on h, I should see this comment. -const ( - // When I hover on g, I should see this comment. - g = 1 //@g,hoverdef("g", g) - - h = 2 //@h,hoverdef("h", h) -) - -// z is a variable too. -var z string //@z,hoverdef("z", z) - -type A string //@mark(AString, "A") - -func AStuff() { //@AStuff - x := 5 - Random2(x) //@godef("dom2", Random2) - Random() //@godef("()", Random) - - var err error //@err - fmt.Printf("%v", err) //@godef("err", err) - - var y string //@string,hoverdef("string", string) - _ = make([]int, 0) //@make,hoverdef("make", make) - - var mu sync.Mutex - mu.Lock() //@Lock,hoverdef("Lock", Lock) - - var typ *types.Named //@mark(typesImport, "types"),hoverdef("types", typesImport) - typ.Obj().Name() //@Name,hoverdef("Name", Name) -} - -type A struct { -} - -func (_ A) Hi() {} //@mark(AHi, "Hi") - -type S struct { - Field int //@mark(AField, "Field") - R // embed a struct - H // embed an interface -} - -type R struct { - Field2 int //@mark(AField2, "Field2") -} - -func (_ R) Hey() {} //@mark(AHey, "Hey") - -type H interface { //@H - Goodbye() //@mark(AGoodbye, "Goodbye") -} - -type I interface { //@I - B() //@mark(AB, "B") - J -} - -type J interface { //@J - Hello() //@mark(AHello, "Hello") -} - -func _() { - // 1st type declaration block - type ( - a struct { //@mark(declBlockA, "a"),hoverdef("a", declBlockA) - x string - } - ) - - // 2nd type declaration block - type ( - // b has a comment - b struct{} //@mark(declBlockB, "b"),hoverdef("b", declBlockB) - ) - - // 3rd type declaration block - type ( - // c is a struct - c struct { //@mark(declBlockC, "c"),hoverdef("c", declBlockC) - f string - } - - d string //@mark(declBlockD, "d"),hoverdef("d", declBlockD) - ) - - type ( - e struct { //@mark(declBlockE, "e"),hoverdef("e", declBlockE) - f float64 - } // e has a comment - ) -} - -var ( - hh H //@hoverdef("H", H) - ii I //@hoverdef("I", I) - jj J //@hoverdef("J", J) -) diff --git a/gopls/internal/lsp/testdata/godef/a/a.go.golden b/gopls/internal/lsp/testdata/godef/a/a.go.golden deleted file mode 100644 index 470396d068c..00000000000 --- a/gopls/internal/lsp/testdata/godef/a/a.go.golden +++ /dev/null @@ -1,230 +0,0 @@ --- H-hoverdef -- -```go -type H interface { - Goodbye() //@mark(AGoodbye, "Goodbye") -} -``` - -[`a.H` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#H) --- I-hoverdef -- -```go -type I interface { - B() //@mark(AB, "B") - J -} -``` - -[`a.I` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#I) --- J-hoverdef -- -```go -type J interface { - Hello() //@mark(AHello, "Hello") -} -``` - -[`a.J` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#J) --- Lock-hoverdef -- -```go -func (*sync.Mutex).Lock() -``` - -Lock locks m. - - -[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock) --- Name-hoverdef -- -```go -func (*types.object).Name() string -``` - -Name returns the object's (package-local, unqualified) name. - - -[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name) --- Random-definition -- -godef/a/random.go:3:6-12: defined here as ```go -func Random() int -``` - -[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random) --- Random-definition-json -- -{ - "span": { - "uri": "file://godef/a/random.go", - "start": { - "line": 3, - "column": 6, - "offset": 16 - }, - "end": { - "line": 3, - "column": 12, - "offset": 22 - } - }, - "description": "```go\nfunc Random() int\n```\n\n[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random)" -} - --- Random-hoverdef -- -```go -func Random() int -``` - -[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random) --- Random2-definition -- -godef/a/random.go:8:6-13: defined here as ```go -func Random2(y int) int -``` - -[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random2) --- Random2-definition-json -- -{ - "span": { - "uri": "file://godef/a/random.go", - "start": { - "line": 8, - "column": 6, - "offset": 71 - }, - "end": { - "line": 8, - "column": 13, - "offset": 78 - } - }, - "description": "```go\nfunc Random2(y int) int\n```\n\n[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random2)" -} - --- Random2-hoverdef -- -```go -func Random2(y int) int -``` - -[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random2) --- aPackage-hoverdef -- -Package a is a package for testing go to definition. - --- declBlockA-hoverdef -- -```go -type a struct { - x string -} -``` - -1st type declaration block - --- declBlockB-hoverdef -- -```go -type b struct{} -``` - -b has a comment - --- declBlockC-hoverdef -- -```go -type c struct { - f string -} -``` - -c is a struct - --- declBlockD-hoverdef -- -```go -type d string -``` - -3rd type declaration block - --- declBlockE-hoverdef -- -```go -type e struct { - f float64 -} -``` - -e has a comment - --- err-definition -- -godef/a/a.go:33:6-9: defined here as ```go -var err error -``` - -@err --- err-definition-json -- -{ - "span": { - "uri": "file://godef/a/a.go", - "start": { - "line": 33, - "column": 6, - "offset": 612 - }, - "end": { - "line": 33, - "column": 9, - "offset": 615 - } - }, - "description": "```go\nvar err error\n```\n\n@err" -} - --- err-hoverdef -- -```go -var err error -``` - -@err - --- g-hoverdef -- -```go -const g untyped int = 1 -``` - -When I hover on g, I should see this comment. - --- h-hoverdef -- -```go -const h untyped int = 2 -``` - -Constant block. - --- make-hoverdef -- -```go -func make(t Type, size ...int) Type -``` - -The make built-in function allocates and initializes an object of type slice, map, or chan (only). - - -[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make) --- string-hoverdef -- -```go -type string string -``` - -string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. - - -[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string) --- typesImport-hoverdef -- -```go -package types ("go/types") -``` - -[`types` on pkg.go.dev](https://pkg.go.dev/go/types) --- x-hoverdef -- -```go -var x string -``` - -x is a variable. - --- z-hoverdef -- -```go -var z string -``` - -z is a variable too. - diff --git a/gopls/internal/lsp/testdata/godef/a/a_test.go b/gopls/internal/lsp/testdata/godef/a/a_test.go deleted file mode 100644 index 77bd633b6c0..00000000000 --- a/gopls/internal/lsp/testdata/godef/a/a_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package a - -import ( - "testing" -) - -func TestA(t *testing.T) { //@TestA,godef(TestA, TestA) -} diff --git a/gopls/internal/lsp/testdata/godef/a/a_test.go.golden b/gopls/internal/lsp/testdata/godef/a/a_test.go.golden deleted file mode 100644 index e5cb3d799cc..00000000000 --- a/gopls/internal/lsp/testdata/godef/a/a_test.go.golden +++ /dev/null @@ -1,26 +0,0 @@ --- TestA-definition -- -godef/a/a_test.go:7:6-11: defined here as ```go -func TestA(t *testing.T) -``` --- TestA-definition-json -- -{ - "span": { - "uri": "file://godef/a/a_test.go", - "start": { - "line": 7, - "column": 6, - "offset": 39 - }, - "end": { - "line": 7, - "column": 11, - "offset": 44 - } - }, - "description": "```go\nfunc TestA(t *testing.T)\n```" -} - --- TestA-hoverdef -- -```go -func TestA(t *testing.T) -``` diff --git a/gopls/internal/lsp/testdata/godef/a/f.go b/gopls/internal/lsp/testdata/godef/a/f.go index 589c45fc1ae..10f88262a81 100644 --- a/gopls/internal/lsp/testdata/godef/a/f.go +++ b/gopls/internal/lsp/testdata/godef/a/f.go @@ -1,3 +1,4 @@ +// Package a is a package for testing go to definition. package a import "fmt" diff --git a/gopls/internal/lsp/testdata/godef/a/random.go b/gopls/internal/lsp/testdata/godef/a/random.go deleted file mode 100644 index 62055c1fcec..00000000000 --- a/gopls/internal/lsp/testdata/godef/a/random.go +++ /dev/null @@ -1,31 +0,0 @@ -package a - -func Random() int { //@Random - y := 6 + 7 - return y -} - -func Random2(y int) int { //@Random2,mark(RandomParamY, "y") - return y //@godef("y", RandomParamY) -} - -type Pos struct { - x, y int //@mark(PosX, "x"),mark(PosY, "y") -} - -// Typ has a comment. Its fields do not. -type Typ struct{ field string } //@mark(TypField, "field") - -func _() { - x := &Typ{} - x.field //@godef("field", TypField) -} - -func (p *Pos) Sum() int { //@mark(PosSum, "Sum") - return p.x + p.y //@godef("x", PosX) -} - -func _() { - var p Pos - _ = p.Sum() //@godef("()", PosSum) -} diff --git a/gopls/internal/lsp/testdata/godef/a/random.go.golden b/gopls/internal/lsp/testdata/godef/a/random.go.golden deleted file mode 100644 index d7ba51d1e82..00000000000 --- a/gopls/internal/lsp/testdata/godef/a/random.go.golden +++ /dev/null @@ -1,113 +0,0 @@ --- PosSum-definition -- -godef/a/random.go:24:15-18: defined here as ```go -func (*Pos).Sum() int -``` - -[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Pos.Sum) --- PosSum-definition-json -- -{ - "span": { - "uri": "file://godef/a/random.go", - "start": { - "line": 24, - "column": 15, - "offset": 413 - }, - "end": { - "line": 24, - "column": 18, - "offset": 416 - } - }, - "description": "```go\nfunc (*Pos).Sum() int\n```\n\n[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Pos.Sum)" -} - --- PosSum-hoverdef -- -```go -func (*Pos).Sum() int -``` - -[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Pos.Sum) --- PosX-definition -- -godef/a/random.go:13:2-3: defined here as ```go -field x int -``` - -@mark(PosX, "x"),mark(PosY, "y") --- PosX-definition-json -- -{ - "span": { - "uri": "file://godef/a/random.go", - "start": { - "line": 13, - "column": 2, - "offset": 187 - }, - "end": { - "line": 13, - "column": 3, - "offset": 188 - } - }, - "description": "```go\nfield x int\n```\n\n@mark(PosX, \"x\"),mark(PosY, \"y\")" -} - --- PosX-hoverdef -- -```go -field x int -``` - -@mark(PosX, "x"),mark(PosY, "y") - --- RandomParamY-definition -- -godef/a/random.go:8:14-15: defined here as ```go -var y int -``` --- RandomParamY-definition-json -- -{ - "span": { - "uri": "file://godef/a/random.go", - "start": { - "line": 8, - "column": 14, - "offset": 79 - }, - "end": { - "line": 8, - "column": 15, - "offset": 80 - } - }, - "description": "```go\nvar y int\n```" -} - --- RandomParamY-hoverdef -- -```go -var y int -``` --- TypField-definition -- -godef/a/random.go:17:18-23: defined here as ```go -field field string -``` --- TypField-definition-json -- -{ - "span": { - "uri": "file://godef/a/random.go", - "start": { - "line": 17, - "column": 18, - "offset": 292 - }, - "end": { - "line": 17, - "column": 23, - "offset": 297 - } - }, - "description": "```go\nfield field string\n```" -} - --- TypField-hoverdef -- -```go -field field string -``` diff --git a/gopls/internal/lsp/testdata/godef/b/b.go b/gopls/internal/lsp/testdata/godef/b/b.go deleted file mode 100644 index ee536ecfdc3..00000000000 --- a/gopls/internal/lsp/testdata/godef/b/b.go +++ /dev/null @@ -1,57 +0,0 @@ -package b - -import ( - myFoo "golang.org/lsptests/foo" //@mark(myFoo, "myFoo"),godef("myFoo", myFoo) - "golang.org/lsptests/godef/a" //@mark(AImport, re"\".*\"") -) - -type Embed struct { - *a.A - a.I - a.S -} - -func _() { - e := Embed{} - e.Hi() //@hoverdef("Hi", AHi) - e.B() //@hoverdef("B", AB) - e.Field //@hoverdef("Field", AField) - e.Field2 //@hoverdef("Field2", AField2) - e.Hello() //@hoverdef("Hello", AHello) - e.Hey() //@hoverdef("Hey", AHey) - e.Goodbye() //@hoverdef("Goodbye", AGoodbye) -} - -type aAlias = a.A //@mark(aAlias, "aAlias") - -type S1 struct { //@S1 - F1 int //@mark(S1F1, "F1") - S2 //@godef("S2", S2),mark(S1S2, "S2") - a.A //@godef("A", AString) - aAlias //@godef("a", aAlias) -} - -type S2 struct { //@S2 - F1 string //@mark(S2F1, "F1") - F2 int //@mark(S2F2, "F2") - *a.A //@godef("A", AString),godef("a",AImport) -} - -type S3 struct { - F1 struct { - a.A //@godef("A", AString) - } -} - -func Bar() { - a.AStuff() //@godef("AStuff", AStuff) - var x S1 //@godef("S1", S1) - _ = x.S2 //@godef("S2", S1S2) - _ = x.F1 //@godef("F1", S1F1) - _ = x.F2 //@godef("F2", S2F2) - _ = x.S2.F1 //@godef("F1", S2F1) - - var _ *myFoo.StructFoo //@godef("myFoo", myFoo) -} - -const X = 0 //@mark(bX, "X"),godef("X", bX) diff --git a/gopls/internal/lsp/testdata/godef/b/b.go.golden b/gopls/internal/lsp/testdata/godef/b/b.go.golden deleted file mode 100644 index cfe3917ba88..00000000000 --- a/gopls/internal/lsp/testdata/godef/b/b.go.golden +++ /dev/null @@ -1,480 +0,0 @@ --- AB-hoverdef -- -```go -func (a.I).B() -``` - -@mark(AB, "B") - - -[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#I.B) --- AField-hoverdef -- -```go -field Field int -``` - -@mark(AField, "Field") - - -[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#S.Field) --- AField2-hoverdef -- -```go -field Field2 int -``` - -@mark(AField2, "Field2") - - -[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#R.Field2) --- AGoodbye-hoverdef -- -```go -func (a.H).Goodbye() -``` - -@mark(AGoodbye, "Goodbye") - - -[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#H.Goodbye) --- AHello-hoverdef -- -```go -func (a.J).Hello() -``` - -@mark(AHello, "Hello") - - -[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#J.Hello) --- AHey-hoverdef -- -```go -func (a.R).Hey() -``` - -[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#R.Hey) --- AHi-hoverdef -- -```go -func (a.A).Hi() -``` - -[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A.Hi) --- AImport-definition -- -godef/b/b.go:5:2-31: defined here as ```go -package a ("golang.org/lsptests/godef/a") -``` - -[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a) --- AImport-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 5, - "column": 2, - "offset": 100 - }, - "end": { - "line": 5, - "column": 31, - "offset": 129 - } - }, - "description": "```go\npackage a (\"golang.org/lsptests/godef/a\")\n```\n\n[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a)" -} - --- AImport-hoverdef -- -```go -package a ("golang.org/lsptests/godef/a") -``` - -[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a) --- AString-definition -- -godef/a/a.go:26:6-7: defined here as ```go -type A string - -func (a.A).Hi() -``` - -@mark(AString, "A") - - -[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A) --- AString-definition-json -- -{ - "span": { - "uri": "file://godef/a/a.go", - "start": { - "line": 26, - "column": 6, - "offset": 467 - }, - "end": { - "line": 26, - "column": 7, - "offset": 468 - } - }, - "description": "```go\ntype A string\n\nfunc (a.A).Hi()\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A)" -} - --- AString-hoverdef -- -```go -type A string - -func (a.A).Hi() -``` - -@mark(AString, "A") - - -[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A) --- AStuff-definition -- -godef/a/a.go:28:6-12: defined here as ```go -func a.AStuff() -``` - -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff) --- AStuff-definition-json -- -{ - "span": { - "uri": "file://godef/a/a.go", - "start": { - "line": 28, - "column": 6, - "offset": 504 - }, - "end": { - "line": 28, - "column": 12, - "offset": 510 - } - }, - "description": "```go\nfunc a.AStuff()\n```\n\n[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff)" -} - --- AStuff-hoverdef -- -```go -func a.AStuff() -``` - -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff) --- S1-definition -- -godef/b/b.go:27:6-8: defined here as ```go -type S1 struct { - F1 int //@mark(S1F1, "F1") - S2 //@godef("S2", S2),mark(S1S2, "S2") - a.A //@godef("A", AString) - aAlias //@godef("a", aAlias) -} -``` - -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) --- S1-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 27, - "column": 6, - "offset": 563 - }, - "end": { - "line": 27, - "column": 8, - "offset": 565 - } - }, - "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1)" -} - --- S1-hoverdef -- -```go -type S1 struct { - F1 int //@mark(S1F1, "F1") - S2 //@godef("S2", S2),mark(S1S2, "S2") - a.A //@godef("A", AString) - aAlias //@godef("a", aAlias) -} -``` - -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) --- S1F1-definition -- -godef/b/b.go:28:2-4: defined here as ```go -field F1 int -``` - -@mark(S1F1, "F1") - - -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) --- S1F1-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 28, - "column": 2, - "offset": 582 - }, - "end": { - "line": 28, - "column": 4, - "offset": 584 - } - }, - "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1)" -} - --- S1F1-hoverdef -- -```go -field F1 int -``` - -@mark(S1F1, "F1") - - -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) --- S1S2-definition -- -godef/b/b.go:29:2-4: defined here as ```go -field S2 S2 -``` - -@godef("S2", S2),mark(S1S2, "S2") - - -[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.S2) --- S1S2-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 29, - "column": 2, - "offset": 614 - }, - "end": { - "line": 29, - "column": 4, - "offset": 616 - } - }, - "description": "```go\nfield S2 S2\n```\n\n@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.S2)" -} - --- S1S2-hoverdef -- -```go -field S2 S2 -``` - -@godef("S2", S2),mark(S1S2, "S2") - - -[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.S2) --- S2-definition -- -godef/b/b.go:34:6-8: defined here as ```go -type S2 struct { - F1 string //@mark(S2F1, "F1") - F2 int //@mark(S2F2, "F2") - *a.A //@godef("A", AString),godef("a",AImport) -} -``` - -[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2) --- S2-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 34, - "column": 6, - "offset": 738 - }, - "end": { - "line": 34, - "column": 8, - "offset": 740 - } - }, - "description": "```go\ntype S2 struct {\n\tF1 string //@mark(S2F1, \"F1\")\n\tF2 int //@mark(S2F2, \"F2\")\n\t*a.A //@godef(\"A\", AString),godef(\"a\",AImport)\n}\n```\n\n[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2)" -} - --- S2-hoverdef -- -```go -type S2 struct { - F1 string //@mark(S2F1, "F1") - F2 int //@mark(S2F2, "F2") - *a.A //@godef("A", AString),godef("a",AImport) -} -``` - -[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2) --- S2F1-definition -- -godef/b/b.go:35:2-4: defined here as ```go -field F1 string -``` - -@mark(S2F1, "F1") - - -[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F1) --- S2F1-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 35, - "column": 2, - "offset": 757 - }, - "end": { - "line": 35, - "column": 4, - "offset": 759 - } - }, - "description": "```go\nfield F1 string\n```\n\n@mark(S2F1, \"F1\")\n\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F1)" -} - --- S2F1-hoverdef -- -```go -field F1 string -``` - -@mark(S2F1, "F1") - - -[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F1) --- S2F2-definition -- -godef/b/b.go:36:2-4: defined here as ```go -field F2 int -``` - -@mark(S2F2, "F2") - - -[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F2) --- S2F2-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 36, - "column": 2, - "offset": 790 - }, - "end": { - "line": 36, - "column": 4, - "offset": 792 - } - }, - "description": "```go\nfield F2 int\n```\n\n@mark(S2F2, \"F2\")\n\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F2)" -} - --- S2F2-hoverdef -- -```go -field F2 int -``` - -@mark(S2F2, "F2") - - -[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F2) --- aAlias-definition -- -godef/b/b.go:25:6-12: defined here as ```go -type aAlias = a.A - -func (a.A).Hi() -``` - -@mark(aAlias, "aAlias") --- aAlias-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 25, - "column": 6, - "offset": 518 - }, - "end": { - "line": 25, - "column": 12, - "offset": 524 - } - }, - "description": "```go\ntype aAlias = a.A\n\nfunc (a.A).Hi()\n```\n\n@mark(aAlias, \"aAlias\")" -} - --- aAlias-hoverdef -- -```go -type aAlias = a.A - -func (a.A).Hi() -``` - -@mark(aAlias, "aAlias") - --- bX-definition -- -godef/b/b.go:57:7-8: defined here as ```go -const X untyped int = 0 -``` - -@mark(bX, "X"),godef("X", bX) - - -[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#X) --- bX-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 57, - "column": 7, - "offset": 1225 - }, - "end": { - "line": 57, - "column": 8, - "offset": 1226 - } - }, - "description": "```go\nconst X untyped int = 0\n```\n\n@mark(bX, \"X\"),godef(\"X\", bX)\n\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#X)" -} - --- bX-hoverdef -- -```go -const X untyped int = 0 -``` - -@mark(bX, "X"),godef("X", bX) - - -[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#X) --- myFoo-definition -- -godef/b/b.go:4:2-7: defined here as ```go -package myFoo ("golang.org/lsptests/foo") -``` - -[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/foo) --- myFoo-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 4, - "column": 2, - "offset": 21 - }, - "end": { - "line": 4, - "column": 7, - "offset": 26 - } - }, - "description": "```go\npackage myFoo (\"golang.org/lsptests/foo\")\n```\n\n[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/foo)" -} - --- myFoo-hoverdef -- -```go -package myFoo ("golang.org/lsptests/foo") -``` - -[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/foo) diff --git a/gopls/internal/lsp/testdata/godef/b/c.go b/gopls/internal/lsp/testdata/godef/b/c.go deleted file mode 100644 index c8daf62422a..00000000000 --- a/gopls/internal/lsp/testdata/godef/b/c.go +++ /dev/null @@ -1,8 +0,0 @@ -package b - -// This is the in-editor version of the file. -// The on-disk version is in c.go.saved. - -var _ = S1{ //@godef("S1", S1) - F1: 99, //@godef("F1", S1F1) -} diff --git a/gopls/internal/lsp/testdata/godef/b/c.go.golden b/gopls/internal/lsp/testdata/godef/b/c.go.golden deleted file mode 100644 index 575bd1e7b51..00000000000 --- a/gopls/internal/lsp/testdata/godef/b/c.go.golden +++ /dev/null @@ -1,76 +0,0 @@ --- S1-definition -- -godef/b/b.go:27:6-8: defined here as ```go -type S1 struct { - F1 int //@mark(S1F1, "F1") - S2 //@godef("S2", S2),mark(S1S2, "S2") - a.A //@godef("A", AString) - aAlias //@godef("a", aAlias) -} -``` - -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) --- S1-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 27, - "column": 6, - "offset": 563 - }, - "end": { - "line": 27, - "column": 8, - "offset": 565 - } - }, - "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1)" -} - --- S1-hoverdef -- -```go -type S1 struct { - F1 int //@mark(S1F1, "F1") - S2 //@godef("S2", S2),mark(S1S2, "S2") - a.A //@godef("A", AString) - aAlias //@godef("a", aAlias) -} -``` - -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) --- S1F1-definition -- -godef/b/b.go:28:2-4: defined here as ```go -field F1 int -``` - -@mark(S1F1, "F1") - - -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) --- S1F1-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 28, - "column": 2, - "offset": 582 - }, - "end": { - "line": 28, - "column": 4, - "offset": 584 - } - }, - "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1)" -} - --- S1F1-hoverdef -- -```go -field F1 int -``` - -@mark(S1F1, "F1") - - -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) diff --git a/gopls/internal/lsp/testdata/godef/b/c.go.saved b/gopls/internal/lsp/testdata/godef/b/c.go.saved deleted file mode 100644 index ff1a8794b48..00000000000 --- a/gopls/internal/lsp/testdata/godef/b/c.go.saved +++ /dev/null @@ -1,7 +0,0 @@ -package b - -// This is the on-disk version of c.go, which represents -// the in-editor version of the file. - -} - diff --git a/gopls/internal/lsp/testdata/godef/b/h.go b/gopls/internal/lsp/testdata/godef/b/h.go deleted file mode 100644 index 88017643336..00000000000 --- a/gopls/internal/lsp/testdata/godef/b/h.go +++ /dev/null @@ -1,10 +0,0 @@ -package b - -import . "golang.org/lsptests/godef/a" - -func _() { - // variable of type a.A - var _ A //@mark(AVariable, "_"),hoverdef("_", AVariable) - - AStuff() //@hoverdef("AStuff", AStuff) -} diff --git a/gopls/internal/lsp/testdata/godef/b/h.go.golden b/gopls/internal/lsp/testdata/godef/b/h.go.golden deleted file mode 100644 index 04c7a291338..00000000000 --- a/gopls/internal/lsp/testdata/godef/b/h.go.golden +++ /dev/null @@ -1,13 +0,0 @@ --- AStuff-hoverdef -- -```go -func AStuff() -``` - -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff) --- AVariable-hoverdef -- -```go -var _ A -``` - -variable of type a.A - diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 478b6a6ca92..985361ba710 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -16,7 +16,7 @@ SemanticTokenCount = 3 SuggestedFixCount = 65 FunctionExtractionCount = 27 MethodExtractionCount = 6 -DefinitionsCount = 99 +DefinitionsCount = 47 TypeDefinitionsCount = 18 HighlightsCount = 69 InlayHintsCount = 4 diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 9d62f8b16f1..9ae4d13649d 100644 --- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -16,7 +16,7 @@ SemanticTokenCount = 3 SuggestedFixCount = 71 FunctionExtractionCount = 27 MethodExtractionCount = 6 -DefinitionsCount = 99 +DefinitionsCount = 47 TypeDefinitionsCount = 18 HighlightsCount = 69 InlayHintsCount = 5 diff --git a/gopls/internal/regtest/marker/testdata/definition/embed.txt b/gopls/internal/regtest/marker/testdata/definition/embed.txt new file mode 100644 index 00000000000..b1131d86362 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/definition/embed.txt @@ -0,0 +1,226 @@ +This test checks definition and hover operations over embedded fields and methods. +-- go.mod -- +module mod.com + +go 1.18 +-- a/a.go -- +package a + +type A string //@loc(AString, "A") + +func (_ A) Hi() {} //@loc(AHi, "Hi") + +type S struct { + Field int //@loc(SField, "Field") + R // embed a struct + H // embed an interface +} + +type R struct { + Field2 int //@loc(RField2, "Field2") +} + +func (_ R) Hey() {} //@loc(RHey, "Hey") + +type H interface { //@loc(H, "H") + Goodbye() //@loc(HGoodbye, "Goodbye") +} + +type I interface { //@loc(I, "I") + B() //@loc(IB, "B") + J +} + +type J interface { //@loc(J, "J") + Hello() //@loc(JHello, "Hello") +} + +-- b/b.go -- +package b + +import "mod.com/a" //@loc(AImport, re"\".*\"") + +type Embed struct { + *a.A + a.I + a.S +} + +func _() { + e := Embed{} + e.Hi() //@def("Hi", AHi),hover("Hi", "Hi", AHi) + e.B() //@def("B", IB),hover("B", "B", IB) + _ = e.Field //@def("Field", SField),hover("Field", "Field", SField) + _ = e.Field2 //@def("Field2", RField2),hover("Field2", "Field2", RField2) + e.Hello() //@def("Hello", JHello),hover("Hello", "Hello",JHello) + e.Hey() //@def("Hey", RHey), hover("Hey", "Hey", RHey) + e.Goodbye() //@def("Goodbye", HGoodbye), hover("Goodbye", "Goodbye", HGoodbye) +} + +type aAlias = a.A //@loc(aAlias, "aAlias") + +type S1 struct { //@loc(S1, "S1") + F1 int //@loc(S1F1, "F1") + S2 //@loc(S1S2, "S2"),def("S2", S2),hover("S2", "S2", S2) + a.A //@def("A", AString),hover("A", "A", aA) + aAlias //@def("a", aAlias),hover("a", "aAlias", aAlias) +} + +type S2 struct { //@loc(S2, "S2") + F1 string //@loc(S2F1, "F1") + F2 int //@loc(S2F2, "F2") + *a.A //@def("A", AString),def("a",AImport) +} + +type S3 struct { + F1 struct { + a.A //@def("A", AString) + } +} + +func Bar() { + var x S1 //@def("S1", S1),hover("S1", "S1", S1) + _ = x.S2 //@def("S2", S1S2),hover("S2", "S2", S1S2) + _ = x.F1 //@def("F1", S1F1),hover("F1", "F1", S1F1) + _ = x.F2 //@def("F2", S2F2),hover("F2", "F2", S2F2) + _ = x.S2.F1 //@def("F1", S2F1),hover("F1", "F1", S2F1) +} +-- b/c.go -- +package b + +var _ = S1{ //@def("S1", S1),hover("S1", "S1", S1) + F1: 99, //@def("F1", S1F1),hover("F1", "F1", S1F1) +} +-- @AHi/hover.md -- +```go +func (a.A).Hi() +``` + +[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/mod.com/a#A.Hi) +-- @HGoodbye/hover.md -- +```go +func (a.H).Goodbye() +``` + +@loc(HGoodbye, "Goodbye") + + +[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/mod.com/a#H.Goodbye) +-- @IB/hover.md -- +```go +func (a.I).B() +``` + +@loc(IB, "B") + + +[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/mod.com/a#I.B) +-- @JHello/hover.md -- +```go +func (a.J).Hello() +``` + +@loc(JHello, "Hello") + + +[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/mod.com/a#J.Hello) +-- @RField2/hover.md -- +```go +field Field2 int +``` + +@loc(RField2, "Field2") + + +[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/mod.com/a#R.Field2) +-- @RHey/hover.md -- +```go +func (a.R).Hey() +``` + +[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/mod.com/a#R.Hey) +-- @S1/hover.md -- +```go +type S1 struct { + F1 int //@loc(S1F1, "F1") + S2 //@loc(S1S2, "S2"),def("S2", S2),hover("S2", "S2", S2) + a.A //@def("A", AString),hover("A", "A", aA) + aAlias //@def("a", aAlias),hover("a", "aAlias", aAlias) +} +``` + +[`b.S1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1) +-- @S1F1/hover.md -- +```go +field F1 int +``` + +@loc(S1F1, "F1") + + +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1.F1) +-- @S1S2/hover.md -- +```go +field S2 S2 +``` + +@loc(S1S2, "S2"),def("S2", S2),hover("S2", "S2", S2) + + +[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1.S2) +-- @S2/hover.md -- +```go +type S2 struct { + F1 string //@loc(S2F1, "F1") + F2 int //@loc(S2F2, "F2") + *a.A //@def("A", AString),def("a",AImport) +} +``` + +[`b.S2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2) +-- @S2F1/hover.md -- +```go +field F1 string +``` + +@loc(S2F1, "F1") + + +[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2.F1) +-- @S2F2/hover.md -- +```go +field F2 int +``` + +@loc(S2F2, "F2") + + +[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2.F2) +-- @SField/hover.md -- +```go +field Field int +``` + +@loc(SField, "Field") + + +[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/mod.com/a#S.Field) +-- @aA/hover.md -- +```go +type A string + +func (a.A).Hi() +``` + +@loc(AString, "A") + + +[`a.A` on pkg.go.dev](https://pkg.go.dev/mod.com/a#A) +-- @aAlias/hover.md -- +```go +type aAlias = a.A + +func (a.A).Hi() +``` + +@loc(aAlias, "aAlias") diff --git a/gopls/internal/regtest/marker/testdata/definition/import.txt b/gopls/internal/regtest/marker/testdata/definition/import.txt new file mode 100644 index 00000000000..9e5e5929aa9 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/definition/import.txt @@ -0,0 +1,52 @@ +This test checks definition and hover over imports. +-- go.mod -- +module mod.com + +go 1.18 +-- foo/foo.go -- +package foo + +type Foo struct{} + +// DoFoo does foo. +func DoFoo() {} //@loc(DoFoo, "DoFoo") +-- bar/bar.go -- +package bar + +import ( + myFoo "mod.com/foo" //@loc(myFoo, "myFoo") +) + +var _ *myFoo.Foo //@def("myFoo", myFoo),hover("myFoo", "myFoo", myFoo) +-- bar/dotimport.go -- +package bar + +import . "mod.com/foo" + +func _() { + // variable of type foo.Foo + var _ Foo //@hover("_", "_", FooVar) + + DoFoo() //@hover("DoFoo", "DoFoo", DoFoo) +} +-- @DoFoo/hover.md -- +```go +func DoFoo() +``` + +DoFoo does foo. + + +[`foo.DoFoo` on pkg.go.dev](https://pkg.go.dev/mod.com/foo#DoFoo) +-- @FooVar/hover.md -- +```go +var _ Foo +``` + +variable of type foo.Foo +-- @myFoo/hover.md -- +```go +package myFoo ("mod.com/foo") +``` + +[`myFoo` on pkg.go.dev](https://pkg.go.dev/mod.com/foo) diff --git a/gopls/internal/regtest/marker/testdata/definition/misc.txt b/gopls/internal/regtest/marker/testdata/definition/misc.txt new file mode 100644 index 00000000000..48f5d340c43 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/definition/misc.txt @@ -0,0 +1,230 @@ +This test exercises miscellaneous definition and hover requests. +-- go.mod -- +module mod.com + +go 1.16 +-- a.go -- +package a //@loc(aPackage, re"package (a)"),hover(aPackage, aPackage, aPackage) + +var ( + // x is a variable. + x string //@loc(x, "x"),hover(x, x, hoverx) +) + +// Constant block. When I hover on h, I should see this comment. +const ( + // When I hover on g, I should see this comment. + g = 1 //@hover("g", "g", hoverg) + + h = 2 //@hover("h", "h", hoverh) +) + +// z is a variable too. +var z string //@loc(z, "z"),hover(z, z, hoverz) + +func AStuff() { //@loc(AStuff, "AStuff") + x := 5 + Random2(x) //@def("dom2", Random2) + Random() //@def("()", Random) +} + +type H interface { //@loc(H, "H") + Goodbye() +} + +type I interface { //@loc(I, "I") + B() + J +} + +type J interface { //@loc(J, "J") + Hello() +} + +func _() { + // 1st type declaration block + type ( + a struct { //@hover("a", "a", hoverDeclBlocka) + x string + } + ) + + // 2nd type declaration block + type ( + // b has a comment + b struct{} //@hover("b", "b", hoverDeclBlockb) + ) + + // 3rd type declaration block + type ( + // c is a struct + c struct { //@hover("c", "c", hoverDeclBlockc) + f string + } + + d string //@hover("d", "d", hoverDeclBlockd) + ) + + type ( + e struct { //@hover("e", "e", hoverDeclBlocke) + f float64 + } // e has a comment + ) +} + +var ( + hh H //@hover("H", "H", hoverH) + ii I //@hover("I", "I", hoverI) + jj J //@hover("J", "J", hoverJ) +) +-- a_test.go -- +package a + +import ( + "testing" +) + +func TestA(t *testing.T) { //@hover("TestA", "TestA", hoverTestA) +} +-- random.go -- +package a + +func Random() int { //@loc(Random, "Random") + y := 6 + 7 + return y +} + +func Random2(y int) int { //@loc(Random2, "Random2"),loc(RandomParamY, "y") + return y //@def("y", RandomParamY),hover("y", "y", hovery) +} + +type Pos struct { + x, y int //@loc(PosX, "x"),loc(PosY, "y") +} + +// Typ has a comment. Its fields do not. +type Typ struct{ field string } //@loc(TypField, "field") + +func _() { + x := &Typ{} + _ = x.field //@def("field", TypField),hover("field", "field", hoverfield) +} + +func (p *Pos) Sum() int { //@loc(PosSum, "Sum") + return p.x + p.y //@hover("x", "x", hoverpx) +} + +func _() { + var p Pos + _ = p.Sum() //@def("()", PosSum),hover("()", `Sum`, hoverSum) +} +-- @aPackage/hover.md -- +-- @hoverDeclBlocka/hover.md -- +```go +type a struct { + x string +} +``` + +1st type declaration block +-- @hoverDeclBlockb/hover.md -- +```go +type b struct{} +``` + +b has a comment +-- @hoverDeclBlockc/hover.md -- +```go +type c struct { + f string +} +``` + +c is a struct +-- @hoverDeclBlockd/hover.md -- +```go +type d string +``` + +3rd type declaration block +-- @hoverDeclBlocke/hover.md -- +```go +type e struct { + f float64 +} +``` + +e has a comment +-- @hoverH/hover.md -- +```go +type H interface { + Goodbye() +} +``` + +[`a.H` on pkg.go.dev](https://pkg.go.dev/mod.com#H) +-- @hoverI/hover.md -- +```go +type I interface { + B() + J +} +``` + +[`a.I` on pkg.go.dev](https://pkg.go.dev/mod.com#I) +-- @hoverJ/hover.md -- +```go +type J interface { + Hello() +} +``` + +[`a.J` on pkg.go.dev](https://pkg.go.dev/mod.com#J) +-- @hoverSum/hover.md -- +```go +func (*Pos).Sum() int +``` + +[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/mod.com#Pos.Sum) +-- @hoverTestA/hover.md -- +```go +func TestA(t *testing.T) +``` +-- @hoverfield/hover.md -- +```go +field field string +``` +-- @hoverg/hover.md -- +```go +const g untyped int = 1 +``` + +When I hover on g, I should see this comment. +-- @hoverh/hover.md -- +```go +const h untyped int = 2 +``` + +Constant block. When I hover on h, I should see this comment. +-- @hoverpx/hover.md -- +```go +field x int +``` + +@loc(PosX, "x"),loc(PosY, "y") +-- @hoverx/hover.md -- +```go +var x string +``` + +x is a variable. +-- @hovery/hover.md -- +```go +var y int +``` +-- @hoverz/hover.md -- +```go +var z string +``` + +z is a variable too. diff --git a/gopls/internal/regtest/marker/testdata/hover/basiclit.txt b/gopls/internal/regtest/marker/testdata/hover/basiclit.txt index 30fe16e21cc..32527420d01 100644 --- a/gopls/internal/regtest/marker/testdata/hover/basiclit.txt +++ b/gopls/internal/regtest/marker/testdata/hover/basiclit.txt @@ -17,7 +17,7 @@ func _() { _ = 0x0001F30A //@hover("0x0001F30A", "0x0001F30A", waterWave) _ = "foo \U0001F30A bar" //@hover("\\U0001F30A", "\\U0001F30A", waterWave) - _ = '\x7E' //@hover("'\\x7E'", `'\x7E'`, tilde) + _ = '\x7E' //@hover("'\\x7E'", "'\\x7E'", tilde) _ = "foo \x7E bar" //@hover("\\x7E", "\\x7E", tilde) _ = "foo \a bar" //@hover("\\a", "\\a", control) @@ -29,10 +29,10 @@ func _() { _ = "foo\173bar\u2211baz" //@hover("\\u2211","\\u2211", summation) // search for runes in string only if there is an escaped sequence - _ = "hello" //@hover("\"hello\"", _, _) + _ = "hello" //@hover(`"hello"`, _, _) // incorrect escaped rune sequences - _ = '\0' //@hover("'\\0'", _, _) + _ = '\0' //@hover("'\\0'", _, _),diag(re`\\0()'`, re"illegal character") _ = '\u22111' //@hover("'\\u22111'", _, _) _ = '\U00110000' //@hover("'\\U00110000'", _, _) _ = '\u12e45'//@hover("'\\u12e45'", _, _) diff --git a/gopls/internal/regtest/marker/testdata/hover/const.txt b/gopls/internal/regtest/marker/testdata/hover/const.txt new file mode 100644 index 00000000000..cdb0e51e27d --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/hover/const.txt @@ -0,0 +1,18 @@ +This test checks hovering over constants. +-- go.mod -- +module mod.com + +go 1.18 +-- c.go -- +package c + +const X = 0 //@hover("X", "X", bX) +-- @bX/hover.md -- +```go +const X untyped int = 0 +``` + +@hover("X", "X", bX) + + +[`c.X` on pkg.go.dev](https://pkg.go.dev/mod.com#X) diff --git a/gopls/internal/regtest/marker/testdata/hover/generics.txt b/gopls/internal/regtest/marker/testdata/hover/generics.txt index 137981f3813..2c526d82b97 100644 --- a/gopls/internal/regtest/marker/testdata/hover/generics.txt +++ b/gopls/internal/regtest/marker/testdata/hover/generics.txt @@ -35,7 +35,8 @@ 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) + // TODO(rfindley): eliminate this diagnostic. + _ = app[[]int]([]int{}, 0) //@hover("app", "app", appint),diag("[[]int]", re"unnecessary type arguments") _ = app([]int{}, 0) //@hover("app", "app", appint) } -- @ValueQ/hover.md -- diff --git a/gopls/internal/regtest/marker/testdata/hover/std.txt b/gopls/internal/regtest/marker/testdata/hover/std.txt new file mode 100644 index 00000000000..a526b5211eb --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/hover/std.txt @@ -0,0 +1,80 @@ +This test checks hover results for built-in or standard library symbols. + +It uses synopsis documentation as full documentation for some of these +built-ins varies across Go versions, where as it just so happens that the +synopsis does not. + +In the future we may need to limit this test to the latest Go version to avoid +documentation churn. +-- settings.json -- +{ + "hoverKind": "SynopsisDocumentation" +} +-- go.mod -- +module mod.com + +go 1.18 +-- std.go -- +package std + +import ( + "fmt" + "go/types" + "sync" +) + +func _() { + var err error //@loc(err, "err") + fmt.Printf("%v", err) //@def("err", err) + + var _ string //@hover("string", "string", hoverstring) + _ = make([]int, 0) //@hover("make", "make", hovermake) + + var mu sync.Mutex + mu.Lock() //@hover("Lock", "Lock", hoverLock) + + var typ *types.Named //@hover("types", "types", hoverTypes) + typ.Obj().Name() //@hover("Name", "Name", hoverName) +} +-- @hoverLock/hover.md -- +```go +func (*sync.Mutex).Lock() +``` + +Lock locks m. + + +[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock) +-- @hoverName/hover.md -- +```go +func (*types.object).Name() string +``` + +Name returns the object's (package-local, unqualified) name. + + +[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name) +-- @hoverTypes/hover.md -- +```go +package types ("go/types") +``` + +[`types` on pkg.go.dev](https://pkg.go.dev/go/types) +-- @hovermake/hover.md -- +```go +func make(t Type, size ...int) Type +``` + +The make built-in function allocates and initializes an object of type slice, map, or chan (only). + + +[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make) +-- @hoverstring/hover.md -- +```go +type string string +``` + +string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. + + +[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string) From 69920f2e63b8c2fcebf58ce7b2a665cba9b2c5f1 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 7 Feb 2023 11:17:37 -0500 Subject: [PATCH 718/723] gopls/internal/regtest/marker: add missing tests for hover Add additional tests for hover behavior. These tests would have failed in earlier patchsets of the subsequent CL rewriting hover. Also, add support for the special "env" file to the marker framework. Change-Id: Iecbd4994a6c1261f87163d50793fbbc5f26ea1ba Reviewed-on: https://go-review.googlesource.com/c/tools/+/466135 TryBot-Result: Gopher Robot Run-TryBot: Robert Findley Reviewed-by: Alan Donovan gopls-CI: kokoro --- gopls/internal/lsp/regtest/marker.go | 37 +++++- .../marker/testdata/hover/goprivate.txt | 27 ++++ .../marker/testdata/hover/linkable.txt | 123 ++++++++++++++++++ 3 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 gopls/internal/regtest/marker/testdata/hover/goprivate.txt create mode 100644 gopls/internal/regtest/marker/testdata/hover/linkable.txt diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index 1bba223ae70..3fe5ccd2870 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -95,6 +95,8 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m // for the test. // - "settings.json": this file is parsed as JSON, and used as the // session configuration (see gopls/doc/settings.md) +// - "env": this file is parsed as a list of VAR=VALUE fields specifying the +// editor environment. // - Golden files: Within the archive, file names starting with '@' are // treated as "golden" content, and are not written to disk, but instead are // made available to test methods expecting an argument of type *Golden, @@ -189,7 +191,6 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m // // Remaining TODO: // - parallelize/optimize test execution -// - add support for per-test environment? // - reorganize regtest packages (and rename to just 'test'?) // // Existing marker tests to port: @@ -246,9 +247,13 @@ func RunMarkerTests(t *testing.T, dir string) { testenv.NeedsGo1Point(t, 18) } test.executed = true + config := fake.EditorConfig{ + Settings: test.settings, + Env: test.env, + } c := &markerContext{ test: test, - env: newEnv(t, cache, test.files, test.settings), + env: newEnv(t, cache, test.files, config), locations: make(map[expect.Identifier]protocol.Location), diags: make(map[protocol.Location][]protocol.Diagnostic), @@ -380,6 +385,7 @@ type MarkerTest struct { fset *token.FileSet // fileset used for parsing notes archive *txtar.Archive // original test archive settings map[string]interface{} // gopls settings + env map[string]string // editor environment files map[string][]byte // data files from the archive (excluding special files) notes []*expect.Note // extracted notes from data files golden map[string]*Golden // extracted golden content, by identifier name @@ -490,6 +496,19 @@ func loadMarkerTest(name string, archive *txtar.Archive) (*MarkerTest, error) { } continue } + if file.Name == "env" { + test.env = make(map[string]string) + fields := strings.Fields(string(file.Data)) + for _, field := range fields { + // TODO: use strings.Cut once we are on 1.18+. + idx := strings.IndexByte(field, '=') + if idx < 0 { + return nil, fmt.Errorf("env vars must be formatted as var=value, got %q", field) + } + test.env[field[:idx]] = field[idx+1:] + } + continue + } if strings.HasPrefix(file.Name, "@") { // golden content // TODO: use strings.Cut once we are on 1.18+. idx := strings.IndexByte(file.Name, '/') @@ -541,6 +560,15 @@ func writeMarkerTests(dir string, tests []*MarkerTest) error { } arch.Files = append(arch.Files, txtar.File{Name: "settings.json", Data: data}) } + if len(test.env) > 0 { + var vars []string + for k, v := range test.env { + vars = append(vars, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(vars) + data := []byte(strings.Join(vars, "\n")) + arch.Files = append(arch.Files, txtar.File{Name: "env", Data: data}) + } // ...followed by ordinary files. Preserve the order they appeared in the // original archive. @@ -578,7 +606,7 @@ func writeMarkerTests(dir string, tests []*MarkerTest) error { // // TODO(rfindley): simplify and refactor the construction of testing // environments across regtests, marker tests, and benchmarks. -func newEnv(t *testing.T, cache *cache.Cache, files map[string][]byte, settings map[string]interface{}) *Env { +func newEnv(t *testing.T, cache *cache.Cache, files map[string][]byte, config fake.EditorConfig) *Env { sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ RootDir: t.TempDir(), GOPROXY: "https://proxy.golang.org", @@ -596,9 +624,6 @@ func newEnv(t *testing.T, cache *cache.Cache, files map[string][]byte, settings awaiter := NewAwaiter(sandbox.Workdir) ss := lsprpc.NewStreamServer(cache, false, hooks.Options) server := servertest.NewPipeServer(ss, jsonrpc2.NewRawStream) - config := fake.EditorConfig{ - Settings: settings, - } editor, err := fake.NewEditor(sandbox, config).Connect(ctx, server, awaiter.Hooks()) if err != nil { sandbox.Close() // ignore error diff --git a/gopls/internal/regtest/marker/testdata/hover/goprivate.txt b/gopls/internal/regtest/marker/testdata/hover/goprivate.txt new file mode 100644 index 00000000000..4c309ef38cf --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/hover/goprivate.txt @@ -0,0 +1,27 @@ +This test checks that links in hover obey GOPRIVATE. +-- env -- +GOPRIVATE=mod.com +-- go.mod -- +module mod.com +-- p.go -- +package p + +// T should not be linked, as it is private. +type T struct{} //@hover("T", "T", T) +-- lib/lib.go -- +package lib + +// GOPRIVATE should also match nested packages. +type L struct{} //@hover("L", "L", L) +-- @L/hover.md -- +```go +type L struct{} +``` + +GOPRIVATE should also match nested packages. +-- @T/hover.md -- +```go +type T struct{} +``` + +T should not be linked, as it is private. diff --git a/gopls/internal/regtest/marker/testdata/hover/linkable.txt b/gopls/internal/regtest/marker/testdata/hover/linkable.txt new file mode 100644 index 00000000000..b82600a1e13 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/hover/linkable.txt @@ -0,0 +1,123 @@ +This test checks that we correctly determine pkgsite links for various +identifiers. + +We should only produce links that work, meaning the object is reachable via the +package's public API. +-- go.mod -- +module mod.com + +go 1.18 +-- p.go -- +package p + +type E struct { + Embed int +} + +// T is in the package scope, and so should be linkable. +type T struct{ //@hover("T", "T", T) + // Only exported fields should be linkable + + f int //@hover("f", "f", f) + F int //@hover("F", "F", F) + + E + + // TODO(rfindley): is the link here correct? It ignores N. + N struct { + // Nested fields should also be linkable. + Nested int //@hover("Nested", "Nested", Nested) + } +} +// M is an exported method, and so should be linkable. +func (T) M() {} + +// m is not exported, and so should not be linkable. +func (T) m() {} + +func _() { + var t T + + // Embedded fields should be linkable. + _ = t.Embed //@hover("Embed", "Embed", Embed) + + // Local variables should not be linkable, even if they are capitalized. + var X int //@hover("X", "X", X) + _ = X + + // Local types should not be linkable, even if they are capitalized. + type Local struct { //@hover("Local", "Local", Local) + E + } + + // But the embedded field should still be linkable. + var l Local + _ = l.Embed //@hover("Embed", "Embed", Embed) +} +-- @Embed/hover.md -- +```go +field Embed int +``` + +[`(p.E).Embed` on pkg.go.dev](https://pkg.go.dev/mod.com#E.Embed) +-- @F/hover.md -- +```go +field F int +``` + +@hover("F", "F", F) + + +[`(p.T).F` on pkg.go.dev](https://pkg.go.dev/mod.com#T.F) +-- @Local/hover.md -- +```go +type Local struct { + E +} +``` + +Local types should not be linkable, even if they are capitalized. +-- @Nested/hover.md -- +```go +field Nested int +``` + +Nested fields should also be linkable. + + +[`(p.T).Nested` on pkg.go.dev](https://pkg.go.dev/mod.com#T.Nested) +-- @T/hover.md -- +```go +type T struct { + f int //@hover("f", "f", f) + F int //@hover("F", "F", F) + + E + + // TODO(rfindley): is the link here correct? It ignores N. + N struct { + // Nested fields should also be linkable. + Nested int //@hover("Nested", "Nested", Nested) + } +} + +func (T).M() +func (T).m() +``` + +T is in the package scope, and so should be linkable. + + +[`p.T` on pkg.go.dev](https://pkg.go.dev/mod.com#T) +-- @X/hover.md -- +```go +var X int +``` + +Local variables should not be linkable, even if they are capitalized. +-- @f/hover.md -- +```go +field f int +``` + +@hover("f", "f", f) From 10a39ef32d4bd0adc0bd4062ae6d291ea88accd0 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 7 Feb 2023 15:29:15 -0500 Subject: [PATCH 719/723] gopls/internal/lsp/regtest: address additional comments on marker.go This CL addresses code review comments from earlier CLs that were easier to to in a follow-up. Change-Id: Ib4bb4cd828377727bdc6dae606fb03d4e06024a6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/466143 Run-TryBot: Robert Findley Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot gopls-CI: kokoro --- gopls/internal/lsp/regtest/marker.go | 163 ++++++++++++++++----------- 1 file changed, 100 insertions(+), 63 deletions(-) diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index 3fe5ccd2870..8411e54b1dc 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -36,7 +36,7 @@ import ( "golang.org/x/tools/txtar" ) -var updateGolden = flag.Bool("update", false, "if set, update test data during marker tests") +var update = flag.Bool("update", false, "if set, update test data during marker tests") // RunMarkerTests runs "marker" tests in the given test data directory. // @@ -90,9 +90,10 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m // // There are three types of file within the test archive that are given special // treatment by the test runner: -// - "flags": this file is parsed as flags configuring the MarkerTest -// instance. For example, -min_go=go1.18 sets the minimum required Go version -// for the test. +// - "flags": this file is treated as a whitespace-separated list of flags +// that configure the MarkerTest instance. For example, -min_go=go1.18 sets +// the minimum required Go version for the test. +// TODO(rfindley): support flag values containing whitespace. // - "settings.json": this file is parsed as JSON, and used as the // session configuration (see gopls/doc/settings.md) // - "env": this file is parsed as a list of VAR=VALUE fields specifying the @@ -116,23 +117,32 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m // - hover(src, dst location, g Golden): perform a textDocument/hover at the // src location, and checks that the result is the dst location, with hover // content matching "hover.md" in the golden data g. -// - loc(name, location): specifies the name of a location in the source. These +// - loc(name, location): specifies the name for a location in the source. These // locations may be referenced by other markers. // // # Argument conversion // -// In additon to the types supported by go/expect, the marker test runner -// applies the following argument conversions from argument type to parameter -// type: -// - string->regexp: the argument parsed as a regular expressions -// - string->location: the location of the first instance of the -// argument in the partial line preceding the note -// - regexp->location: the location of the first match for the argument in -// the partial line preceding the note If the regular expression contains -// exactly one subgroup, the position of the subgroup is used rather than the -// position of the submatch. -// - name->location: the named location corresponding to the argument -// - name->Golden: the golden content prefixed by @ +// Marker arguments are first parsed by the go/expect package, which accepts +// the following tokens as defined by the Go spec: +// - string, int64, float64, and rune literals +// - true and false +// - nil +// - identifiers (type expect.Identifier) +// - regular expressions, denoted the two tokens re"abc" (type *regexp.Regexp) +// +// These values are passed as arguments to the corresponding parameter of the +// test function. Additional value conversions may occur for these argument -> +// parameter type pairs: +// - string->regexp: the argument is parsed as a regular expressions. +// - string->location: the argument is converted to the location of the first +// instance of the argument in the partial line preceding the note. +// - regexp->location: the argument is converted to the location of the first +// match for the argument in the partial line preceding the note. If the +// regular expression contains exactly one subgroup, the position of the +// subgroup is used rather than the position of the submatch. +// - name->location: the argument is replaced by the named location. +// - name->Golden: the argument is used to look up golden content prefixed by +// @. // // # Example // @@ -152,10 +162,10 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m // In this example, the @hover annotation tells the test runner to run the // hoverMarker function, which has parameters: // -// (env *Env, src, dsc protocol.Location, g *Golden). +// (c *markerContext, src, dsc protocol.Location, g *Golden). // -// The env argument holds the implicit test environment, including fake editor -// with open files, and sandboxed directory. +// The first argument holds the test context, including fake editor with open +// files, and sandboxed directory. // // Argument converters translate the "b" and "abc" arguments into locations by // interpreting each one as a regular expression and finding the location of @@ -182,7 +192,7 @@ var updateGolden = flag.Bool("update", false, "if set, update test data during m // at Go tip. Each test function can normalize golden content for older Go // versions. // -// -update does not cause missing @diag markers to be added. +// Note that -update does not cause missing @diag or @loc markers to be added. // // # TODO // @@ -304,8 +314,7 @@ func RunMarkerTests(t *testing.T, dir string) { for _, note := range notes { mi, ok := markers[note.Name] if !ok { - posn := safetoken.StartPosition(test.fset, note.Pos) - t.Errorf("%s: no marker function named %s", posn, note.Name) + t.Errorf("%s: no marker function named %s", c.fmtPos(note.Pos), note.Name) continue } if err := runMarker(c, mi, note); err != nil { @@ -326,7 +335,7 @@ func RunMarkerTests(t *testing.T, dir string) { // so we can now update the test data. // TODO(rfindley): even when -update is not set, compare updated content with // actual content. - if *updateGolden { + if *update { if err := writeMarkerTests(dir, tests); err != nil { t.Fatalf("failed to -update: %v", err) } @@ -335,11 +344,10 @@ func RunMarkerTests(t *testing.T, dir string) { // runMarker calls mi.fn with the arguments coerced from note. func runMarker(c *markerContext, mi markerInfo, note *expect.Note) error { - posn := safetoken.StartPosition(c.test.fset, note.Pos) // The first converter corresponds to the *Env argument. All others // must be coerced from the marker syntax. if got, want := len(note.Args), len(mi.converters); got != want { - return fmt.Errorf("%s: got %d arguments to %s, expect %d", posn, got, note.Name, want) + return fmt.Errorf("%s: got %d arguments to %s, expect %d", c.fmtPos(note.Pos), got, note.Name, want) } args := []reflect.Value{reflect.ValueOf(c)} @@ -353,7 +361,7 @@ func runMarker(c *markerContext, mi markerInfo, note *expect.Note) error { } out, err := mi.converters[i](c, note, in) if err != nil { - return fmt.Errorf("%s: converting argument #%d of %s (%v): %v", posn, i, note.Name, in, err) + return fmt.Errorf("%s: converting argument #%d of %s (%v): %v", c.fmtPos(note.Pos), i, note.Name, in, err) } args = append(args, reflect.ValueOf(out)) } @@ -426,9 +434,9 @@ type Golden struct { // // If -update is set, the given update function will be called to get the // updated golden content that should be written back to testdata. -func (g *Golden) Get(t testing.TB, name string, update func() []byte) []byte { - if *updateGolden { - d := update() +func (g *Golden) Get(t testing.TB, name string, getUpdate func() []byte) []byte { + if *update { + d := getUpdate() if existing, ok := g.updated[name]; ok { // Multiple tests may reference the same golden data, but if they do they // must agree about its expected content. @@ -483,61 +491,67 @@ func loadMarkerTest(name string, archive *txtar.Archive) (*MarkerTest, error) { golden: make(map[string]*Golden), } for _, file := range archive.Files { - if file.Name == "flags" { + switch { + case file.Name == "flags": test.flags = strings.Fields(string(file.Data)) if err := test.flagSet().Parse(test.flags); err != nil { return nil, fmt.Errorf("parsing flags: %v", err) } - continue - } - if file.Name == "settings.json" { + + case file.Name == "settings.json": if err := json.Unmarshal(file.Data, &test.settings); err != nil { return nil, err } - continue - } - if file.Name == "env" { + + case file.Name == "env": test.env = make(map[string]string) fields := strings.Fields(string(file.Data)) for _, field := range fields { // TODO: use strings.Cut once we are on 1.18+. - idx := strings.IndexByte(field, '=') - if idx < 0 { + key, value, ok := cut(field, "=") + if !ok { return nil, fmt.Errorf("env vars must be formatted as var=value, got %q", field) } - test.env[field[:idx]] = field[idx+1:] + test.env[key] = value } - continue - } - if strings.HasPrefix(file.Name, "@") { // golden content - // TODO: use strings.Cut once we are on 1.18+. - idx := strings.IndexByte(file.Name, '/') - if idx < 0 { + + case strings.HasPrefix(file.Name, "@"): // golden content + prefix, name, ok := cut(file.Name, "/") + if !ok { return nil, fmt.Errorf("golden file path %q must contain '/'", file.Name) } - goldenID := file.Name[len("@"):idx] + goldenID := prefix[len("@"):] if _, ok := test.golden[goldenID]; !ok { test.golden[goldenID] = &Golden{ id: goldenID, data: make(map[string][]byte), } } - test.golden[goldenID].data[file.Name[idx+len("/"):]] = file.Data - continue - } + test.golden[goldenID].data[name] = file.Data - // ordinary file content - notes, err := expect.Parse(test.fset, file.Name, file.Data) - if err != nil { - return nil, fmt.Errorf("parsing notes in %q: %v", file.Name, err) + default: // ordinary file content + notes, err := expect.Parse(test.fset, file.Name, file.Data) + if err != nil { + return nil, fmt.Errorf("parsing notes in %q: %v", file.Name, err) + } + test.notes = append(test.notes, notes...) + test.files[file.Name] = file.Data } - test.notes = append(test.notes, notes...) - test.files[file.Name] = file.Data } return test, nil } +// cut is a copy of strings.Cut. +// +// TODO: once we only support Go 1.18+, just use strings.Cut. +func cut(s, sep string) (before, after string, found bool) { + if i := strings.Index(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true + } + return s, "", false +} + // writeMarkerTests writes the updated golden content to the test data files. func writeMarkerTests(dir string, tests []*MarkerTest) error { for _, test := range tests { @@ -657,8 +671,29 @@ type markerContext struct { diags map[protocol.Location][]protocol.Diagnostic } +// fmtLoc formats the given pos in the context of the test, using +// archive-relative paths for files and including the line number in the full +// archive file. +func (c markerContext) fmtPos(pos token.Pos) string { + file := c.test.fset.File(pos) + if file == nil { + c.env.T.Errorf("position %d not in test fileset", pos) + return "" + } + m := c.env.Editor.Mapper(file.Name()) + if m == nil { + c.env.T.Errorf("%s is not open", file.Name()) + return "" + } + loc, err := m.PosLocation(file, pos, pos) + if err != nil { + c.env.T.Errorf("Mapper(%s).PosLocation failed: %v", file.Name(), err) + } + return c.fmtLoc(loc) +} + // fmtLoc formats the given location in the context of the test, using -// archive-relative paths for files, and including the line number in the full +// archive-relative paths for files and including the line number in the full // archive file. func (c markerContext) fmtLoc(loc protocol.Location) string { if loc == (protocol.Location{}) { @@ -688,12 +723,14 @@ func (c markerContext) fmtLoc(loc protocol.Location) string { innerSpan := fmt.Sprintf("%d:%d", s.Start().Line(), s.Start().Column()) // relative to the embedded file outerSpan := fmt.Sprintf("%d:%d", lines+s.Start().Line(), s.Start().Column()) // relative to the archive file - if s.End().Line() == s.Start().Line() { - innerSpan += fmt.Sprintf("-%d", s.End().Column()) - outerSpan += fmt.Sprintf("-%d", s.End().Column()) - } else { - innerSpan += fmt.Sprintf("-%d:%d", s.End().Line(), s.End().Column()) - innerSpan += fmt.Sprintf("-%d:%d", lines+s.End().Line(), s.End().Column()) + if s.Start() != s.End() { + if s.End().Line() == s.Start().Line() { + innerSpan += fmt.Sprintf("-%d", s.End().Column()) + outerSpan += fmt.Sprintf("-%d", s.End().Column()) + } else { + innerSpan += fmt.Sprintf("-%d:%d", s.End().Line(), s.End().Column()) + innerSpan += fmt.Sprintf("-%d:%d", lines+s.End().Line(), s.End().Column()) + } } return fmt.Sprintf("%s:%s (%s:%s)", name, innerSpan, c.test.name, outerSpan) From ebad375bab94c682db69281cfb1e334f3f027d53 Mon Sep 17 00:00:00 2001 From: Dan Kortschak Date: Tue, 7 Feb 2023 20:25:08 +1030 Subject: [PATCH 720/723] gopls/internal/lsp/protocol: prevent license rendering in godoc Change-Id: I6de75a869ee1b210c692ef47e3c672589f76337c Reviewed-on: https://go-review.googlesource.com/c/tools/+/465995 Run-TryBot: Suzy Mueller Reviewed-by: David Chase Auto-Submit: Suzy Mueller Reviewed-by: Suzy Mueller gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/internal/lsp/protocol/tsdocument_changes.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gopls/internal/lsp/protocol/tsdocument_changes.go b/gopls/internal/lsp/protocol/tsdocument_changes.go index 7296a151ac2..2c7a524e178 100644 --- a/gopls/internal/lsp/protocol/tsdocument_changes.go +++ b/gopls/internal/lsp/protocol/tsdocument_changes.go @@ -1,6 +1,7 @@ // Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. + package protocol import ( From 1ace7dbcb0dee7c24be0672c77979cd86f966322 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Tue, 7 Feb 2023 15:25:24 -0700 Subject: [PATCH 721/723] go,gopls: remove license from package doc comments Inspired by http://go.dev/cl/465995, add a blank line between license and package statement to avoid the license appearing in the package doc comment. Change-Id: I867e0729ca1fdc1bcee34964954549e5c92f2b31 Reviewed-on: https://go-review.googlesource.com/c/tools/+/466215 TryBot-Result: Gopher Robot gopls-CI: kokoro Reviewed-by: Robert Findley Run-TryBot: Suzy Mueller --- go/analysis/passes/ifaceassert/parameterized.go | 1 + go/ssa/subst.go | 1 + gopls/internal/lsp/cache/os_windows.go | 1 + 3 files changed, 3 insertions(+) diff --git a/go/analysis/passes/ifaceassert/parameterized.go b/go/analysis/passes/ifaceassert/parameterized.go index 1285ecf1367..b35f62dc730 100644 --- a/go/analysis/passes/ifaceassert/parameterized.go +++ b/go/analysis/passes/ifaceassert/parameterized.go @@ -1,6 +1,7 @@ // Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. + package ifaceassert import ( diff --git a/go/ssa/subst.go b/go/ssa/subst.go index 396626befca..d7f8ae4a700 100644 --- a/go/ssa/subst.go +++ b/go/ssa/subst.go @@ -1,6 +1,7 @@ // Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. + package ssa import ( diff --git a/gopls/internal/lsp/cache/os_windows.go b/gopls/internal/lsp/cache/os_windows.go index 7ff1cce7469..2feded84d7a 100644 --- a/gopls/internal/lsp/cache/os_windows.go +++ b/gopls/internal/lsp/cache/os_windows.go @@ -1,6 +1,7 @@ // 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 ( From 545ca87cb53fecbea3162bffc4057580c456a4d0 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Wed, 8 Feb 2023 16:36:21 -0500 Subject: [PATCH 722/723] gopls/internal/regtest/marker: require go/packages Add a missing testenv condition to the marker test runner. Fixes golang/go#58396 Change-Id: I69a7fe6dfab011ba789a066e2c7677c0315eed00 Reviewed-on: https://go-review.googlesource.com/c/tools/+/466715 Run-TryBot: Robert Findley TryBot-Result: Gopher Robot gopls-CI: kokoro Auto-Submit: Robert Findley Reviewed-by: Bryan Mills --- gopls/internal/lsp/regtest/marker.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index 8411e54b1dc..691a608c91e 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -236,6 +236,9 @@ var update = flag.Bool("update", false, "if set, update test data during marker // - AddImport // - SelectionRanges func RunMarkerTests(t *testing.T, dir string) { + // The marker tests must be able to run go/packages.Load. + testenv.NeedsGoPackages(t) + tests, err := loadMarkerTests(dir) if err != nil { t.Fatal(err) From d0863f03daeff79ab917de5768285bb077f77b20 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Wed, 8 Feb 2023 21:54:13 +0000 Subject: [PATCH 723/723] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Once this CL is submitted, and post-submit testing succeeds on all first-class ports across all supported Go versions, this repository will be tagged with its next minor version. Change-Id: I4837788c74cc58a98186c4517be6231efec17a8c Reviewed-on: https://go-review.googlesource.com/c/tools/+/466735 Reviewed-by: Dmitri Shuralyov Reviewed-by: Heschi Kreinick Run-TryBot: Gopher Robot Reviewed-by: Dmitri Shuralyov gopls-CI: kokoro Auto-Submit: Gopher Robot TryBot-Result: Gopher Robot --- go.mod | 6 +++--- go.sum | 16 ++++++++-------- gopls/go.mod | 6 +++--- gopls/go.sum | 17 ++++++++--------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index d90447639aa..b8944bbd385 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.18 // tagx:compat 1.16 require ( github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.7.0 - golang.org/x/net v0.5.0 - golang.org/x/sys v0.4.0 + golang.org/x/mod v0.8.0 + golang.org/x/net v0.6.0 + golang.org/x/sys v0.5.0 ) require golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index fbb3d74e321..044d1bf1316 100644 --- a/go.sum +++ b/go.sum @@ -3,13 +3,13 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -19,15 +19,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/gopls/go.mod b/gopls/go.mod index 3b86c1fe724..da7984f9de2 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -7,10 +7,10 @@ require ( github.com/jba/printsrc v0.2.2 github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 - golang.org/x/mod v0.7.0 + golang.org/x/mod v0.8.0 golang.org/x/sync v0.1.0 - golang.org/x/sys v0.4.0 - golang.org/x/text v0.6.0 + golang.org/x/sys v0.5.0 + golang.org/x/text v0.7.0 golang.org/x/tools v0.4.1-0.20221217013628-b4dfc36097e2 golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815 gopkg.in/yaml.v3 v3.0.1 diff --git a/gopls/go.sum b/gopls/go.sum index ba8eabfa058..fd01bae2d38 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -52,10 +52,11 @@ golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQ golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -67,15 +68,13 @@ golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a h1:KWIh6uTTw7r3PEz1N1OIEM8pr5bf1uP1n6JL5Ml56X8= -golang.org/x/vuln v0.0.0-20221212182831-af59454a8a0a/go.mod h1:54iI0rrZVM8VdIvTrT/sdlVfMUJWOgvTRQN24CEtZk0= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815 h1:A9kONVi4+AnuOr1dopsibH6hLi1Huy54cbeJxnq4vmU= golang.org/x/vuln v0.0.0-20230110180137-6ad3e3d07815/go.mod h1:XJiVExZgoZfrrxoTeVsFYrSSk1snhfpOEC95JL+A4T0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=